@davait/aipictury 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE.md +77 -0
  2. package/README.md +39 -0
  3. package/ext/about.js +209 -0
  4. package/ext/account.js +242 -0
  5. package/ext/accountUi.js +287 -0
  6. package/ext/agent.js +1929 -0
  7. package/ext/browser.js +263 -0
  8. package/ext/cloud.js +385 -0
  9. package/ext/context.js +241 -0
  10. package/ext/deps.js +142 -0
  11. package/ext/dialogs.js +803 -0
  12. package/ext/diff.js +150 -0
  13. package/ext/extension.js +2483 -0
  14. package/ext/feedback.js +55 -0
  15. package/ext/files.js +565 -0
  16. package/ext/firestore.js +131 -0
  17. package/ext/gpuRun.js +77 -0
  18. package/ext/gpuServer.js +76 -0
  19. package/ext/hooks.js +153 -0
  20. package/ext/log.js +29 -0
  21. package/ext/lsp.js +122 -0
  22. package/ext/mcp.js +469 -0
  23. package/ext/mcpAuth.js +128 -0
  24. package/ext/media/vendor/firebase-config.json +9 -0
  25. package/ext/media/vendor/signin.js +1 -0
  26. package/ext/mediagen.js +174 -0
  27. package/ext/memory.js +117 -0
  28. package/ext/notebook.js +82 -0
  29. package/ext/package.json +1234 -0
  30. package/ext/paths.js +50 -0
  31. package/ext/permissions.js +229 -0
  32. package/ext/plans.js +110 -0
  33. package/ext/plugins.js +243 -0
  34. package/ext/prefs.js +157 -0
  35. package/ext/remote.js +170 -0
  36. package/ext/review.js +126 -0
  37. package/ext/sandbox.js +79 -0
  38. package/ext/scheduler.js +87 -0
  39. package/ext/sessions.js +228 -0
  40. package/ext/sessionsView.js +449 -0
  41. package/ext/shell.js +357 -0
  42. package/ext/storage.js +80 -0
  43. package/ext/tasklist.js +112 -0
  44. package/ext/text.js +31 -0
  45. package/ext/tools.js +781 -0
  46. package/ext/usage.js +77 -0
  47. package/ext/voice.js +135 -0
  48. package/ext/workbench.js +162 -0
  49. package/ext/workflow.js +51 -0
  50. package/ext/worktree.js +83 -0
  51. package/main.js +758 -0
  52. package/package.json +43 -0
  53. package/terminal.js +124 -0
  54. package/vscode-shim.js +231 -0
package/ext/context.js ADDED
@@ -0,0 +1,241 @@
1
+ 'use strict'
2
+ // What the project tells the agent, in the files other agents already use:
3
+ // - instructions: AIPICTURY.md, AGENTS.md, CLAUDE.md (and CLAUDE.local.md, .claude/CLAUDE.md)
4
+ // at the workspace root, read into every conversation — so a project set up for Claude
5
+ // Code or another agent works here unchanged;
6
+ // - skills: <dir>/<name>/SKILL.md under .aipictury/skills or .claude/skills (project or home),
7
+ // listed to the model by name and description and read in full when it uses one;
8
+ // - commands: .aipictury/commands/*.md or .claude/commands/*.md, run as /name arguments.
9
+
10
+ const fs = require('fs')
11
+ const os = require('os')
12
+ const path = require('path')
13
+ const { root } = require('./paths')
14
+ const prefs = require('./prefs')
15
+
16
+ const INSTRUCTION_FILES = ['AIPICTURY.md', 'AGENTS.md', 'CLAUDE.md', 'CLAUDE.local.md', '.claude/CLAUDE.md']
17
+ const CAP = 20_000
18
+
19
+ function read(file) { try { return fs.readFileSync(file, 'utf8') } catch { return null } }
20
+ function base() { try { return root().fsPath } catch { return null } }
21
+
22
+ /** The project's instruction files, for the system prompt. */
23
+ function instructions() {
24
+ const dir = base()
25
+ const found = []
26
+ const home = read(path.join(os.homedir(), '.aipictury', 'AIPICTURY.md'))
27
+ if (home?.trim()) found.push({ name: '~/.aipictury/AIPICTURY.md', text: home })
28
+ if (dir) {
29
+ for (const name of INSTRUCTION_FILES) {
30
+ const text = read(path.join(dir, name))
31
+ if (text?.trim()) found.push({ name, text })
32
+ }
33
+ }
34
+ return found
35
+ }
36
+
37
+ function instructionsBlock() {
38
+ const found = instructions()
39
+ if (!found.length) return ''
40
+ return '\n\nThe project gives these instructions. Follow them; they override your defaults:\n'
41
+ + found.map((f) => `\n<instructions file="${f.name}">\n${f.text.slice(0, CAP)}${f.text.length > CAP ? '\n…' : ''}\n</instructions>`).join('\n')
42
+ }
43
+
44
+ /** Frontmatter (--- key: value ---) and the body of a Markdown file. */
45
+ function frontmatter(text) {
46
+ const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text)
47
+ if (!m) return { meta: {}, body: text }
48
+ const meta = {}
49
+ for (const line of m[1].split(/\r?\n/)) {
50
+ const kv = /^([\w-]+):\s*(.*)$/.exec(line)
51
+ if (kv) meta[kv[1]] = kv[2].replace(/^["']|["']$/g, '').trim()
52
+ }
53
+ return { meta, body: text.slice(m[0].length) }
54
+ }
55
+
56
+ function dirs(kind) {
57
+ return sourceDirs(kind).map((d) => d.dir)
58
+ }
59
+
60
+ /** Where a kind of thing (commands, skills, agents, output-styles) is kept: the project's, yours,
61
+ * then the plugins' that are on — each plugin's with its name, for /plugin:command. */
62
+ function sourceDirs(kind) {
63
+ const dir = base()
64
+ const list = []
65
+ if (dir) list.push({ dir: path.join(dir, '.aipictury', kind) }, { dir: path.join(dir, '.claude', kind) })
66
+ list.push({ dir: path.join(os.homedir(), '.aipictury', kind) }, { dir: path.join(os.homedir(), '.claude', kind) })
67
+ try { list.push(...require('./plugins').dirs(dir || undefined, kind)) } catch { /* no plugins */ }
68
+ return list
69
+ }
70
+
71
+ /** Every skill: { name, description, file, dir }. The first found by a name wins. */
72
+ function skills() {
73
+ const out = new Map()
74
+ for (const { dir: d, plugin } of sourceDirs('skills')) {
75
+ let entries
76
+ try { entries = fs.readdirSync(d, { withFileTypes: true }) } catch { continue }
77
+ for (const e of entries) {
78
+ if (!e.isDirectory()) continue
79
+ const file = path.join(d, e.name, 'SKILL.md')
80
+ const text = read(file)
81
+ if (!text) continue
82
+ const { meta } = frontmatter(text)
83
+ const name = plugin ? `${plugin}:${meta.name || e.name}` : meta.name || e.name
84
+ if (!out.has(name)) out.set(name, { name, description: meta.description || '', file, dir: path.join(d, e.name), plugin })
85
+ }
86
+ }
87
+ return [...out.values()]
88
+ }
89
+
90
+ /** How each skill is shown to the model: 'on' (name and description), 'name' (name only),
91
+ * 'off' (hidden, and refused). From the settings files ("skillVisibility"). */
92
+ function skillVisibility() {
93
+ return prefs.load(base() || undefined).merged.skillVisibility || {}
94
+ }
95
+
96
+ /** The skills the model may use, each with how it is shown. */
97
+ function visibleSkills() {
98
+ const seen = skillVisibility()
99
+ return skills().map((s) => ({ ...s, visibility: ['on', 'name', 'off'].includes(seen[s.name]) ? seen[s.name] : 'on' }))
100
+ .filter((s) => s.visibility !== 'off')
101
+ }
102
+
103
+ /** A skill's instructions, for the model. */
104
+ function useSkill(args) {
105
+ const all = visibleSkills()
106
+ const skill = all.find((s) => s.name === String(args.name || args.skill || ''))
107
+ if (!skill) throw new Error(`no skill ${args.name} — available: ${all.map((s) => s.name).join(', ') || 'none'}`)
108
+ const { body } = frontmatter(read(skill.file) || '')
109
+ return `Skill "${skill.name}" (files in ${skill.dir}):\n\n${body.trim()}${args.arguments ? `\n\nArguments: ${args.arguments}` : ''}`
110
+ }
111
+
112
+ /** Custom slash commands: { name, description, hint, file }. */
113
+ function commands() {
114
+ const out = new Map()
115
+ for (const { dir: d, plugin } of sourceDirs('commands')) {
116
+ let files
117
+ try { files = fs.readdirSync(d).filter((f) => f.endsWith('.md')) } catch { continue }
118
+ for (const f of files) {
119
+ const name = plugin ? `${plugin}:${f.replace(/\.md$/, '')}` : f.replace(/\.md$/, '')
120
+ if (out.has(name)) continue
121
+ const { meta, body } = frontmatter(read(path.join(d, f)) || '')
122
+ out.set(name, { name, description: meta.description || body.trim().split('\n')[0].slice(0, 80),
123
+ hint: meta['argument-hint'] || '', file: path.join(d, f) })
124
+ }
125
+ }
126
+ return [...out.values()]
127
+ }
128
+
129
+ /** "/name args" → the prompt it stands for, or null when it is not a custom command. */
130
+ function expandCommand(text) {
131
+ const m = /^\/([\w:.-]+)(?:\s+([\s\S]*))?$/.exec(String(text).trim())
132
+ if (!m) return null
133
+ const cmd = commands().find((c) => c.name === m[1])
134
+ if (!cmd) return null
135
+ const args = (m[2] || '').trim()
136
+ const parts = args.split(/\s+/).filter(Boolean)
137
+ const { body } = frontmatter(read(cmd.file) || '')
138
+ let prompt = body.replace(/\$ARGUMENTS/g, args).replace(/\$(\d+)/g, (_, n) => parts[Number(n) - 1] ?? '')
139
+ if (args && !/\$ARGUMENTS|\$\d/.test(body)) prompt += `\n\n${args}`
140
+ return prompt.trim()
141
+ }
142
+
143
+ /** What a command's frontmatter grants while it runs: allowed-tools (rules) and a model. */
144
+ function commandMeta(text) {
145
+ const m = /^\/([\w:.-]+)/.exec(String(text).trim())
146
+ const cmd = m && commands().find((c) => c.name === m[1])
147
+ if (!cmd) return { allowedTools: [], model: null }
148
+ const { meta } = frontmatter(read(cmd.file) || '')
149
+ const raw = meta['allowed-tools'] || ''
150
+ // "Bash(git add:*), Bash(git status:*)" — commas between rules, not inside their ( ).
151
+ const allowedTools = []
152
+ let depth = 0
153
+ let cur = ''
154
+ for (const c of String(raw)) {
155
+ if (c === '(') depth += 1
156
+ if (c === ')') depth -= 1
157
+ if (c === ',' && depth === 0) { if (cur.trim()) allowedTools.push(cur.trim()); cur = '' } else cur += c
158
+ }
159
+ if (cur.trim()) allowedTools.push(cur.trim())
160
+ return { allowedTools, model: meta.model || null }
161
+ }
162
+
163
+ /** Runs a command's inline shell commands (!`git status`) and puts their output in its place —
164
+ * each only when `allowed(command)` says so; otherwise the prompt says it was not run. */
165
+ async function runInline(prompt, { cwd, allowed, timeout = 30_000 } = {}) {
166
+ const { exec } = require('child_process')
167
+ const found = [...String(prompt).matchAll(/!`([^`\n]+)`/g)]
168
+ let out = String(prompt)
169
+ for (const m of found) {
170
+ const command = m[1].trim()
171
+ let text
172
+ if (!(await allowed(command))) text = `[not run: ${command} — not allowed by the command's allowed-tools or your rules]`
173
+ else {
174
+ text = await new Promise((resolve) => exec(command, { cwd, timeout, maxBuffer: 1_000_000, shell: '/bin/bash' },
175
+ (error, stdout, stderr) => resolve(`${stdout}${stderr}`.trim().slice(0, 8000) || (error ? `(failed: ${error.message.split('\n')[0]})` : '(no output)'))))
176
+ }
177
+ out = out.replace(m[0], text)
178
+ }
179
+ return out
180
+ }
181
+
182
+ // ---- agents: helpers with a role (.aipictury/agents, .claude/agents, plugins) ----
183
+
184
+ function agents() {
185
+ const out = new Map()
186
+ for (const { dir: d, plugin } of sourceDirs('agents')) {
187
+ let files
188
+ try { files = fs.readdirSync(d).filter((f) => f.endsWith('.md')) } catch { continue }
189
+ for (const f of files) {
190
+ const { meta, body } = frontmatter(read(path.join(d, f)) || '')
191
+ const base = meta.name || f.replace(/\.md$/, '')
192
+ const name = plugin ? `${plugin}:${base}` : base
193
+ const tools = Array.isArray(meta.tools) ? meta.tools : String(meta.tools || '').split(',').map((t) => t.trim()).filter(Boolean)
194
+ if (!out.has(name)) out.set(name, { name, description: meta.description || '', tools, model: meta.model || null, body: body.trim(), file: path.join(d, f) })
195
+ }
196
+ }
197
+ return [...out.values()]
198
+ }
199
+
200
+ // ---- output styles: how the agent writes its answers ----
201
+
202
+ const STYLES = [
203
+ { name: 'Default', description: 'Concise, focused on getting the work done', builtIn: true, body: '' },
204
+ { name: 'Explanatory', description: 'Does the work, and explains the choices in brief notes', builtIn: true,
205
+ body: 'Besides doing the task, help the user understand it. Before or after a piece of code, add a short "Insight" '
206
+ + 'note — two or three lines — on why it is written that way: the patterns of this codebase it follows, the '
207
+ + 'trade-off taken, an alternative not taken. Keep the notes specific to the code at hand, never generic.' },
208
+ { name: 'Learning', description: 'Teaches as it goes, and leaves small meaningful parts for you to write', builtIn: true,
209
+ body: 'Teach as you work. Explain your reasoning as you go. When a small, meaningful piece of logic comes up (a '
210
+ + 'condition, a loop body, a function of a few lines), do not write it: leave a clearly marked TODO(human) in '
211
+ + 'the code with a hint, and ask the user to write it. Do the routine parts — wiring, boilerplate — yourself.' },
212
+ ]
213
+
214
+ /** Every output style: the built-in ones and those in .aipictury/output-styles (or .claude/…). */
215
+ function outputStyles() {
216
+ const out = new Map(STYLES.map((s) => [s.name.toLowerCase(), s]))
217
+ for (const d of dirs('output-styles')) {
218
+ let files
219
+ try { files = fs.readdirSync(d).filter((f) => f.endsWith('.md')) } catch { continue }
220
+ for (const f of files) {
221
+ const { meta, body } = frontmatter(read(path.join(d, f)) || '')
222
+ const name = meta.name || f.replace(/\.md$/, '')
223
+ if (!out.has(name.toLowerCase())) out.set(name.toLowerCase(), { name, description: meta.description || '', body: body.trim(), file: path.join(d, f) })
224
+ }
225
+ }
226
+ return [...out.values()]
227
+ }
228
+
229
+ /** The style in force (settings files' "outputStyle"), Default when none or unknown. */
230
+ function outputStyle() {
231
+ const wanted = String(prefs.load(base() || undefined).merged.outputStyle || 'Default').toLowerCase()
232
+ return outputStyles().find((s) => s.name.toLowerCase() === wanted) || STYLES[0]
233
+ }
234
+
235
+ /** What the system prompt gains from the style in force. */
236
+ function styleBlock() {
237
+ const style = outputStyle()
238
+ return style.body ? `\n\n## Output style: ${style.name}\n${style.body}` : ''
239
+ }
240
+
241
+ module.exports = { commandMeta, runInline, agents, sourceDirs, visibleSkills, skillVisibility, outputStyles, outputStyle, styleBlock, STYLES, instructions, instructionsBlock, skills, useSkill, commands, expandCommand, frontmatter, INSTRUCTION_FILES }
package/ext/deps.js ADDED
@@ -0,0 +1,142 @@
1
+ 'use strict'
2
+ // New dependencies are the user's choice. The prompt says so, but a model sometimes picks a
3
+ // library anyway — writes an Express server, runs `npm install multer` — and the user is left
4
+ // with a stack they did not choose, costly to undo. So the agent itself holds such a call:
5
+ // installing a package, or writing code that imports one, that the user never named and the
6
+ // project does not already use. The model is told to ask first; once the user (or, in Auto
7
+ // mode, the recommended option) has chosen, the name is in the conversation and it goes ahead.
8
+ const { builtinModules } = require('module')
9
+ const vscode = require('vscode')
10
+
11
+ // Installing: the package names that follow these commands.
12
+ const INSTALL = /^(?:sudo\s+)?(?:npm\s+(?:install|i|add)|pnpm\s+(?:add|install|i)|yarn\s+(?:global\s+)?add|bun\s+(?:add|install|i)|pip3?\s+install|python3?\s+-m\s+pip\s+install|poetry\s+add|uv\s+(?:add|pip\s+install)|pipx\s+install|cargo\s+add|go\s+get|gem\s+install|composer\s+require)\s+(.+)$/i
13
+ // Options whose next word is a value, not a package.
14
+ const VALUED = new Set(['-r', '--requirement', '-c', '--constraint', '-e', '--editable', '-i', '--index-url',
15
+ '--extra-index-url', '-t', '--target', '--prefix', '--registry', '--save-prefix', '-f', '--find-links'])
16
+ // The user leaving the choice to the agent.
17
+ const DELEGATED = /\b(you (choose|pick|decide)|your (choice|call)|whatever you (want|think|prefer)|any (library|package|framework)|(elegí|elija|elige|decid[ií]) vos|lo que (quieras|prefieras)|a tu criterio|como quieras)\b/i
18
+ const BUILTIN = new Set(builtinModules.map((m) => m.replace(/^node:/, '')))
19
+
20
+ /** The base name a package is known by: scope kept, version, extras and paths dropped. */
21
+ function baseName(spec) {
22
+ let s = String(spec).trim().replace(/^['"]|['"]$/g, '')
23
+ if (!s || /^(\.|\/|~|https?:|git\+|file:)/.test(s) || /\.(txt|toml|cfg|whl|tar\.gz|tgz)$/.test(s)) return ''
24
+ // A Go module is its whole path: github.com/gin-gonic/gin.
25
+ if (/^[\w-]+(\.[\w-]+)+\/[^\s]+/.test(s)) return s.replace(/@[^/]*$/, '').toLowerCase()
26
+ if (s.startsWith('@')) return s.split('/').slice(0, 2).join('/').replace(/@[^/@]*$/, (m) => (m.includes('/') ? m : '')).toLowerCase()
27
+ return s.split(/[@=<>!~[;\s/]/)[0].toLowerCase()
28
+ }
29
+
30
+ /** The packages a shell command would install. */
31
+ function installed(command) {
32
+ const names = []
33
+ for (const part of String(command || '').split(/\s*(?:&&|\|\||;)\s*/)) {
34
+ const m = INSTALL.exec(part.trim())
35
+ if (!m) continue
36
+ const words = m[1].split(/\s+/)
37
+ for (let i = 0; i < words.length; i += 1) {
38
+ if (VALUED.has(words[i])) { i += 1; continue }
39
+ if (words[i].startsWith('-')) continue
40
+ const name = baseName(words[i])
41
+ if (name) names.push(name)
42
+ }
43
+ }
44
+ return names
45
+ }
46
+
47
+ /** The third-party packages a JavaScript or TypeScript file imports. */
48
+ function imported(path, content) {
49
+ if (!/\.(m?[jt]sx?|cjs)$/i.test(String(path || ''))) return []
50
+ const names = new Set()
51
+ const text = String(content || '')
52
+ const found = /(?:\bimport\s+(?:[^'"()]*?\s+from\s+)?|\bexport\s+[^'"()]*?\s+from\s+|\brequire\s*\(\s*|\bimport\s*\(\s*)['"]([^'"]+)['"]/g
53
+ for (const m of text.matchAll(found)) {
54
+ const spec = m[1]
55
+ if (/^(\.|\/|node:|#|~\/|@\/)/.test(spec)) continue
56
+ const name = baseName(spec)
57
+ if (name && !BUILTIN.has(name)) names.add(name)
58
+ }
59
+ return [...names]
60
+ }
61
+
62
+ /** The packages a tool call brings in: installed by a command, or imported by a file written. */
63
+ function brings(name, args) {
64
+ if (name === 'run_command') return installed(args?.command)
65
+ if (name === 'write_file') return imported(args?.path, args?.content)
66
+ if (name === 'edit_file') {
67
+ const edits = Array.isArray(args?.edits) ? args.edits : [args || {}]
68
+ return [...new Set(edits.flatMap((e) => imported(args?.path, e.new_text ?? e.new_string ?? '')))]
69
+ }
70
+ return []
71
+ }
72
+
73
+ /** What the user has said in this conversation, and what they chose when asked. */
74
+ function saidByUser(messages, fromUser) {
75
+ return messages
76
+ .filter((m) => fromUser.has(m) || (m.role === 'tool' && m.tool_name === 'ask_user'))
77
+ .map((m) => (typeof m.content === 'string' ? m.content : (m.content || []).map((p) => p.text || '').join(' ')))
78
+ .join('\n')
79
+ }
80
+
81
+ /** Every dependency the workspace already declares, from its manifests — within 10 s: on a
82
+ * workspace too big to scan in time, fewer are known, and the agent asks rather than waits. */
83
+ async function declared() {
84
+ let timer
85
+ const late = new Promise((resolve) => { timer = setTimeout(() => resolve(new Set()), 10_000) })
86
+ try { return await Promise.race([scanManifests(), late]) } finally { clearTimeout(timer) }
87
+ }
88
+
89
+ async function scanManifests() {
90
+ const names = new Set()
91
+ const read = async (uri) => new TextDecoder().decode(await vscode.workspace.fs.readFile(uri))
92
+ try {
93
+ for (const uri of await vscode.workspace.findFiles('**/package.json', '**/node_modules/**', 30)) {
94
+ try {
95
+ const pkg = JSON.parse(await read(uri))
96
+ for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
97
+ for (const dep of Object.keys(pkg[key] || {})) names.add(dep.toLowerCase())
98
+ }
99
+ } catch { /* not JSON */ }
100
+ }
101
+ for (const uri of await vscode.workspace.findFiles('**/{requirements*.txt,pyproject.toml,Pipfile,Cargo.toml,go.mod,Gemfile,composer.json}', '**/{node_modules,.venv,venv}/**', 30)) {
102
+ try {
103
+ for (const word of (await read(uri)).toLowerCase().match(/[a-z0-9@][a-z0-9._/@-]*/g) || []) names.add(word.replace(/^["']|["',]$/g, ''))
104
+ } catch { /* unreadable */ }
105
+ }
106
+ } catch { /* no workspace */ }
107
+ return names
108
+ }
109
+
110
+ /**
111
+ * The packages in this call the user has not chosen, or none. A name counts as chosen when
112
+ * the project already declares it (or its @types), the user wrote it, or picked it when asked.
113
+ * `attempts` counts holds per package: after two, the call goes through — the model has been
114
+ * told twice, and a hold must never become a dead end.
115
+ */
116
+ function unchosen(names, said, known, attempts) {
117
+ if (!names.length || DELEGATED.test(said)) return []
118
+ const text = said.toLowerCase()
119
+ const words = new Set(text.match(/[a-z0-9@][a-z0-9._/@+-]*/g) || [])
120
+ const chosen = (n) => known.has(n) || known.has(n.replace(/^@types\//, '')) || words.has(n)
121
+ || text.includes(n)
122
+ // @sentry/node is named by "sentry"; github.com/gin-gonic/gin by "gin".
123
+ || (n.startsWith('@') ? words.has(n.slice(1).split('/')[0]) : n.includes('/') && words.has(n.split('/').pop()))
124
+ const open = names.filter((n) => !chosen(n) && (attempts.get(n) || 0) < 2)
125
+ for (const n of open) attempts.set(n, (attempts.get(n) || 0) + 1)
126
+ return open
127
+ }
128
+
129
+ /** What the model is told instead of the call running. */
130
+ function holdNote(names, { helper = false } = {}) {
131
+ const list = names.map((n) => `\`${n}\``).join(', ')
132
+ const head = `Not run: this brings in ${list}, which the user has not chosen and the project does not `
133
+ + 'use yet. Which library, framework, database or service to depend on is the user\'s decision. '
134
+ if (helper) {
135
+ return head + 'You cannot ask them from here: end with your report, saying which choice is needed, '
136
+ + 'the real options and the one you recommend, so the main agent asks.'
137
+ }
138
+ return head + 'Ask with ask_user first — 2 to 4 real options, your recommended one first with "(Recommended)", '
139
+ + 'shaped by what you found in the project — then go ahead with what they choose.'
140
+ }
141
+
142
+ module.exports = { installed, imported, brings, saidByUser, declared, unchosen, holdNote, baseName }