{"format":1,"manifest":{"name":"ai-helpers","publisher":"vyloxi","version":"1.0.0","displayName":"AI Helpers","description":"Write a commit message or pull request description from your changes, explain selected code, or turn English into a regular expression. Bring your own API key.","engines":{"ferrum":"^0.1.0"},"categories":["AI"],"icon":"icon.png","repository":"https://github.com/voukengdongmofrankysteve/ferrum-market","license":"MIT","main":"extension.js","activationEvents":["onCommand:aiHelpers.commitMessage","onCommand:aiHelpers.prDescription","onCommand:aiHelpers.explainSelection","onCommand:aiHelpers.regexFromEnglish","onCommand:aiHelpers.forgetConsent"],"permissions":["network","terminal","workspace.read","workspace.write"],"contributes":{"commands":[{"command":"aiHelpers.commitMessage","title":"Write a Commit Message from Staged Changes","category":"AI Helpers"},{"command":"aiHelpers.prDescription","title":"Write a Pull Request Description...","category":"AI Helpers"},{"command":"aiHelpers.explainSelection","title":"Explain the Selected Code","category":"AI Helpers"},{"command":"aiHelpers.regexFromEnglish","title":"Regular Expression from a Description...","category":"AI Helpers"},{"command":"aiHelpers.forgetConsent","title":"Ask Again Before Sending Anything","category":"AI Helpers"}],"configuration":{"title":"AI Helpers","properties":{"aiHelpers.provider":{"type":"string","enum":["openai","anthropic"],"default":"openai","description":"openai works with OpenAI and any OpenAI-compatible service (including local ones). anthropic is the Claude API."},"aiHelpers.baseUrl":{"type":"string","default":"","description":"The service address. Empty uses https://api.openai.com/v1 or https://api.anthropic.com. https is required (http only for localhost)."},"aiHelpers.model":{"type":"string","default":"","description":"The model name. Empty uses gpt-4o-mini for openai and claude-sonnet-5-5 for anthropic."},"aiHelpers.apiKey":{"type":"string","default":"","description":"Your API key. It is stored in your Ferrum settings in plain text on this computer."},"aiHelpers.baseBranch":{"type":"string","default":"main","description":"The branch pull request descriptions compare against."}}}}},"files":{"icon.png":{"encoding":"base64","content":"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"},"extension.js":{"encoding":"utf8","content":"// AI Helpers: write a commit message from your staged changes, a pull request description from your branch, explain the selected code,\n// or turn a description into a regular expression. You bring your own API key. The text you ask about is sent to the provider you choose\n// (OpenAI-compatible or Anthropic) and nowhere else. Ferrum asks before sending anything the first time for each provider address.\n\nconst DIR = '.ferrum/ai';\nconst MAX_CHARS = 24000;\n\nconst PROVIDERS = {\n  openai: { label: 'OpenAI-compatible', url: 'https://api.openai.com/v1', model: 'gpt-4o-mini' },\n  anthropic: { label: 'Anthropic', url: 'https://api.anthropic.com', model: 'claude-sonnet-5-5' },\n};\n\n/** Cuts a long diff so it fits, keeping the start of each file. Tells the model it was cut. */\nfunction clip(text, max = MAX_CHARS) {\n  if (text.length <= max) return text;\n  const head = text.slice(0, Math.floor(max * 0.8));\n  const tail = text.slice(-Math.floor(max * 0.15));\n  return head + '\\n\\n[... ' + (text.length - head.length - tail.length) + ' characters cut ...]\\n\\n' + tail;\n}\n\nconst SYSTEM = {\n  commit: 'You write git commit messages in the Conventional Commits style. Reply with only the message: a subject line of at most 72 characters (type(scope): summary, imperative mood, no final period), then if it helps a blank line and a short body of plain sentences. No code fences, no quotes, no commentary.',\n  pr: 'You write pull request descriptions. Reply in Markdown with a one sentence summary, then \"## What changed\" as a short bullet list, then \"## Why\" if the reason is clear from the diff, then \"## How to test\" with concrete steps. Be accurate and brief. Do not invent anything that is not in the diff.',\n  explain: 'You explain code to a developer. Say what it does in plain words, then note anything surprising, risky or worth fixing, briefly. Use short paragraphs or a short list. Do not rewrite the code unless asked.',\n  regex: 'You turn a description into one regular expression. Reply with only the expression on the first line, with no delimiters or flags in it. On the second line write the flags if any are needed (or nothing). Then a blank line and one short sentence explaining it. The target flavor is given by the user.',\n};\n\n/** Builds the request for a provider. Returns { url, headers, body } or throws when something is missing. */\nfunction buildRequest(provider, baseUrl, apiKey, model, system, user) {\n  if (!apiKey) throw new Error('Set your API key in the settings (aiHelpers.apiKey).');\n  const base = (baseUrl || PROVIDERS[provider].url).replace(/\\/+$/, '');\n  let u;\n  try { u = new URL(base); } catch { throw new Error('aiHelpers.baseUrl is not a valid address.'); }\n  if (u.protocol !== 'https:' && !/^(localhost|127\\.0\\.0\\.1|\\[::1\\])$/.test(u.hostname)) throw new Error('The provider address must use https (http only works for localhost).');\n  const m = model || PROVIDERS[provider].model;\n  if (provider === 'anthropic') {\n    return {\n      host: u.host,\n      url: base + '/v1/messages',\n      headers: { 'content-type': 'application/json', 'x-api-key': apiKey, 'anthropic-version': '2023-06-01' },\n      body: { model: m, max_tokens: 1500, system, messages: [{ role: 'user', content: user }] },\n    };\n  }\n  return {\n    host: u.host,\n    url: base + '/chat/completions',\n    headers: { 'content-type': 'application/json', authorization: 'Bearer ' + apiKey },\n    body: { model: m, temperature: 0.2, max_tokens: 1500, messages: [{ role: 'system', content: system }, { role: 'user', content: user }] },\n  };\n}\n\n/** The reply text from either provider's response, or throws the provider's error message. */\nfunction readReply(provider, json) {\n  if (json && json.error) throw new Error(typeof json.error === 'string' ? json.error : json.error.message || 'The provider returned an error.');\n  const text = provider === 'anthropic' ? (json.content || []).filter((b) => b.type === 'text').map((b) => b.text).join('') : json.choices && json.choices[0] && json.choices[0].message && json.choices[0].message.content;\n  if (!text) throw new Error('The provider returned an empty reply.');\n  return String(text).trim();\n}\n\n/** Removes code fences and surrounding quotes a model may add around a commit message. */\nfunction cleanCommit(text) {\n  return text.replace(/^```[a-z]*\\n?|\\n?```$/g, '').replace(/^[\"'`]+|[\"'`]+$/g, '').trim();\n}\n\n/** The expression, flags and note from a regex reply. */\nfunction parseRegex(text) {\n  const lines = String(text).replace(/^```[a-z]*\\n?|\\n?```$/g, '').trim().split('\\n');\n  const pattern = (lines[0] || '').trim().replace(/^\\/(.*)\\/[a-z]*$/, '$1');\n  const flags = /^[gimsuy]{1,6}$/.test((lines[1] || '').trim()) ? lines[1].trim() : '';\n  const note = lines.slice(flags ? 2 : 1).join(' ').trim();\n  return { pattern, flags, note };\n}\n\nconst esc = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nfunction textPage(title, body, source) {\n  return `<style>body{font-family:var(--ui);line-height:1.6} pre{font:13px/1.55 var(--mono,monospace);white-space:pre-wrap;word-break:break-word;background:var(--raised,rgba(128,128,128,.12));border-radius:6px;padding:12px} .dim{color:var(--fg-dim);font-size:12px} button{background:var(--raised,transparent);color:var(--fg);border:1px solid var(--border);border-radius:5px;padding:5px 12px;cursor:pointer} button:hover{border-color:var(--accent)}</style><h3>${esc(title)}</h3><div class=\"dim\">${esc(source)}</div><pre id=\"t\">${esc(body)}</pre><button id=\"b\">Insert into the file</button><script>document.getElementById('b').onclick=()=>ferrumPanel.postMessage({type:'insert',text:document.getElementById('t').textContent});</script>`;\n}\n\nexports._test = { clip, buildRequest, readReply, cleanCommit, parseRegex, SYSTEM, PROVIDERS };\n\nexports.activate = function (context) {\n  const reg = (id, fn) => context.subscriptions.push(ferrum.commands.register(id, fn));\n  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));\n  const cfg = () => ({\n    provider: ferrum.settings.get('aiHelpers.provider') === 'anthropic' ? 'anthropic' : 'openai',\n    baseUrl: String(ferrum.settings.get('aiHelpers.baseUrl') || '').trim(),\n    apiKey: String(ferrum.settings.get('aiHelpers.apiKey') || '').trim(),\n    model: String(ferrum.settings.get('aiHelpers.model') || '').trim(),\n  });\n  let panel = null;\n\n  /** Sends the text to the provider after the person agreed once for this address. */\n  async function ask(system, user, what) {\n    const c = cfg();\n    const req = buildRequest(c.provider, c.baseUrl, c.apiKey, c.model, system, clip(user));\n    const agreed = await ferrum.storage.get('agreed', {});\n    if (!agreed[req.host]) {\n      const ok = await ferrum.window.confirm('To ' + what + ', Ferrum will send the text you chose to ' + req.host + ' using your API key. Nothing else is sent. Allow this?', 'Allow');\n      if (!ok) throw new Error('Cancelled.');\n      agreed[req.host] = true;\n      await ferrum.storage.set('agreed', agreed);\n    }\n    const res = await ferrum.window.withProgress('Asking ' + req.host, () => ferrum.network.fetch(req.url, { method: 'POST', headers: req.headers, body: JSON.stringify(req.body) }));\n    const text = await res.text();\n    let json;\n    try { json = JSON.parse(text); } catch { throw new Error('The provider answered with something that is not JSON (HTTP ' + res.status + ').'); }\n    if (!res.ok && !json.error) throw new Error('The provider answered HTTP ' + res.status + '.');\n    return readReply(c.provider, json);\n  }\n\n  /** Runs `git ...` into a file and waits for it, so the text can be read back. */\n  async function gitToFile(args, name) {\n    if (!ferrum.workspace.root) throw new Error('Open a folder that is a git repository first.');\n    const file = DIR + '/' + name;\n    await ferrum.workspace.writeFile(file, '');\n    await ferrum.terminal.run(`git ${args} --output=\"${file}\"`);\n    const t0 = Date.now();\n    let text = '';\n    let stable = 0;\n    let last = -1;\n    while (Date.now() - t0 < 20000) {\n      await sleep(500);\n      try { text = await ferrum.workspace.readFile(file); } catch { text = ''; }\n      if (text.length && text.length === last) { if (++stable >= 1) break; } else stable = 0;\n      last = text.length;\n    }\n    return text;\n  }\n\n  async function show(title, body, source) {\n    const html = textPage(title, body, source);\n    if (panel) return panel.setHtml(html);\n    panel = await ferrum.window.showPanel({ title, html });\n    panel.onDidClose(() => (panel = null));\n    panel.onMessage(async (m) => {\n      if (m.type !== 'insert') return;\n      const d = await ferrum.editor.active();\n      if (d) await ferrum.editor.insert(String(m.text || ''));\n      else ferrum.window.showMessage('Open a file to insert into.', 'warn');\n    });\n  }\n\n  const guard = (fn) => async () => {\n    try { await fn(); } catch (e) { if (e.message !== 'Cancelled.') ferrum.window.showMessage(e.message, 'warn'); }\n  };\n\n  reg('aiHelpers.commitMessage', guard(async () => {\n    const diff = await gitToFile('diff --staged', 'staged.diff');\n    if (!diff.trim()) return ferrum.window.showMessage('Nothing is staged. Run \"git add\" on the files you want to commit first.', 'warn');\n    const msg = cleanCommit(await ask(SYSTEM.commit, 'Write the commit message for this staged diff:\\n\\n' + diff, 'write a commit message'));\n    const doc = await ferrum.editor.active();\n    if (doc && /COMMIT_EDITMSG$/.test(doc.path || '')) await ferrum.editor.insert(msg);\n    else await show('Commit message', msg, 'From your staged changes. Review it before you commit.');\n  }));\n\n  reg('aiHelpers.prDescription', guard(async () => {\n    const base = ((await ferrum.window.showInputBox({ prompt: 'Compare this branch with which branch?', value: String(ferrum.settings.get('aiHelpers.baseBranch') || 'main') })) || '').trim();\n    if (!base) return;\n    if (!/^[A-Za-z0-9._/-]+$/.test(base)) return ferrum.window.showMessage('That branch name has characters I will not put in a command.', 'warn');\n    const diff = await gitToFile(`diff ${base}...HEAD`, 'branch.diff');\n    if (!diff.trim()) return ferrum.window.showMessage('No difference from ' + base + ' found.', 'warn');\n    const text = await ask(SYSTEM.pr, 'Write the pull request description for this diff against ' + base + ':\\n\\n' + diff, 'write a pull request description');\n    await show('Pull request description', text, 'From the changes against ' + base + '. Review it before you post it.');\n  }));\n\n  reg('aiHelpers.explainSelection', guard(async () => {\n    const doc = await ferrum.editor.active();\n    if (!doc || !doc.selectedText) return ferrum.window.showMessage('Select the code to explain.', 'warn');\n    const text = await ask(SYSTEM.explain, 'Language: ' + (doc.language || 'unknown') + '\\n\\n' + doc.selectedText, 'explain this code');\n    await show('Explanation', text, doc.name + ', lines ' + (doc.selection.start.line + 1) + ' to ' + (doc.selection.end.line + 1));\n  }));\n\n  reg('aiHelpers.regexFromEnglish', guard(async () => {\n    const what = ((await ferrum.window.showInputBox({ prompt: 'Describe what the pattern should match', placeholder: 'a date like 2026-10-22, or an email ending in .org' })) || '').trim();\n    if (!what) return;\n    const doc = await ferrum.editor.active();\n    const flavor = doc && doc.language ? doc.language : 'JavaScript';\n    const reply = await ask(SYSTEM.regex, 'Flavor: ' + flavor + '\\nMatch: ' + what, 'write a regular expression');\n    const r = parseRegex(reply);\n    if (!r.pattern) throw new Error('No pattern came back.');\n    try { new RegExp(r.pattern, r.flags); } catch { ferrum.window.showMessage('The pattern may use syntax JavaScript does not know, but it can still be right for ' + flavor + '.'); }\n    const pick = await ferrum.window.showQuickPick([{ label: 'Insert ' + r.pattern, detail: r.note, what: 'insert' }, { label: 'Insert /' + r.pattern + '/' + r.flags, detail: 'As a JavaScript literal', what: 'literal' }], { placeholder: r.note || 'Pattern ready' });\n    if (!pick || !doc) return;\n    await ferrum.editor.insert(pick.what === 'literal' ? '/' + r.pattern + '/' + r.flags : r.pattern);\n  }));\n\n  reg('aiHelpers.forgetConsent', async () => {\n    await ferrum.storage.set('agreed', {});\n    ferrum.window.showMessage('Ferrum will ask again before sending anything.');\n  });\n};\n\nexports.deactivate = function () {};\n"},"README.md":{"encoding":"utf8","content":"# AI Helpers\n\nFour small helpers that use a language model **you** choose.\n\n- **Write a Commit Message from Staged Changes**: reads `git diff --staged` and proposes a Conventional Commits message. If a commit message file is open it is inserted there.\n- **Write a Pull Request Description...**: reads the diff against your base branch and writes a summary, what changed, why and how to test.\n- **Explain the Selected Code**: plain words, and anything surprising or risky.\n- **Regular Expression from a Description...**: describe what to match, get a pattern for the language you are editing.\n\nResults open in a panel with an **Insert into the file** button, so you always review before using them.\n\n## Set up\n\nOpen the settings for this extension and fill in `aiHelpers.apiKey`. Choose `openai` (also works with other OpenAI-compatible services and local servers: set `aiHelpers.baseUrl`) or `anthropic`. The model has a default you can change.\n\n## What is sent, and where\n\n- Only the text you asked about: the staged diff, the branch diff, your selection, or your description. Long diffs are shortened to about 24,000 characters.\n- It goes to the address in your settings and nowhere else, over https. The first time for each address, Ferrum **asks you to allow it**. **Ask Again Before Sending Anything** resets that.\n- Diffs can contain secrets and private code. Check what is staged before you use it on code you may not share.\n- **Your API key is stored in plain text in your Ferrum settings.** Use a key with a spending limit, and do not commit your settings file.\n\n## How it reads your changes\n\nThe extension cannot read the terminal. It types `git diff ... --output=.ferrum/ai/...` in the terminal (you see it) and reads the file back. This needs git and a repository.\n\n## Permissions\n\n**network** to call the service, **terminal** to run git, **workspace.read** and **workspace.write** for the diff file in `.ferrum/ai`.\n"}}}