@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/main.js ADDED
@@ -0,0 +1,758 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+ // aipictury — the AIPictury agent in a terminal, as Claude Code's `claude`: the extension's own
4
+ // agent (its tools, permission rules, hooks, MCP servers, skills, sandbox), on your AIPictury
5
+ // server's model, working in the folder it is started in. Interactive, or one prompt with -p.
6
+ // Conversations are the extension's too: one started here can be carried on in VS Code, and the
7
+ // other way round (--continue, --resume).
8
+ const Module = require('module')
9
+ const path = require('path')
10
+ const fs = require('fs')
11
+ const os = require('os')
12
+ const readline = require('readline')
13
+
14
+ // ---- `vscode` is the stand-in, for every module of the agent ----
15
+ const SHIM = require.resolve('./vscode-shim.js')
16
+ const resolveFilename = Module._resolveFilename
17
+ Module._resolveFilename = function (request, ...rest) { return request === 'vscode' ? SHIM : resolveFilename.call(this, request, ...rest) }
18
+ const vscode = require('./vscode-shim.js')
19
+
20
+ // The agent's modules: packed beside the CLI (npm: ext/, see pack.mjs), or the extension's own
21
+ // folder when run from the repository.
22
+ const EXT = fs.existsSync(path.join(__dirname, 'ext', 'agent.js')) ? path.join(__dirname, 'ext') : path.join(__dirname, '..', 'vscode')
23
+ const pkg = require(path.join(EXT, 'package.json'))
24
+ const VERSION = pkg.version
25
+
26
+ const HELP = `aipictury ${VERSION} — the AIPictury agent in a terminal
27
+
28
+ Usage:
29
+ aipictury login [--no-browser] sign in to your AIPictury account (needed), in the browser
30
+ aipictury logout | whoami sign out on this machine | who is signed in
31
+ aipictury [prompt] interactive (a first prompt is optional)
32
+ aipictury -p "prompt" one prompt, the answer printed, then exit
33
+ cat log.txt | aipictury -p "why does it fail?"
34
+
35
+ Options:
36
+ -p, --print print mode: no questions asked (what needs a yes is refused)
37
+ --output-format text|json|stream-json print mode's output (default text)
38
+ -c, --continue carry on the latest conversation of this folder
39
+ -r, --resume [id] carry on a conversation (none given: list them)
40
+ --permission-mode <mode> default (asks; the start) | acceptEdits | auto | plan | bypassPermissions
41
+ --dangerously-skip-permissions bypassPermissions, for this run
42
+ --allowedTools "<rule>,…" allowed without asking, as permission rules: "Bash(npm test:*),Edit"
43
+ --disallowedTools "<rule>,…" denied, as permission rules; a bare tool ("Bash", "WebFetch") is not even offered
44
+ --mcp-config <file|json> MCP servers for this run (a file, or JSON with "mcpServers"); repeatable
45
+ --add-dir <folder> another folder the agent may work in, besides this one; repeatable
46
+ --append-system-prompt "<text>" added at the end of the system prompt
47
+ --model <name> the model (default: the server's agent model)
48
+ --effort low|medium|high|max|ultra how hard it reasons
49
+ --max-turns <n> stop after this many model turns (print mode)
50
+ --server <url> the AIPictury server (default: aipictury.server, or AIPICTURY_URL)
51
+ -v, --version the version
52
+ -h, --help this help
53
+
54
+ In a conversation: / for commands (/help lists them; Tab completes), @path mentions a file,
55
+ !command runs it here, a line ending in \\ continues on the next. Esc or Ctrl+C stops a run;
56
+ Ctrl+C twice at the prompt quits. Up and down bring back what you typed, across runs.
57
+ Settings: ~/.aipictury/cli.json (the extension's names, e.g. {"aipictury.approval": "ask"}).
58
+ Copyright © 2026 Davait, Inc. All rights reserved. See LICENSE.md.
59
+ `
60
+
61
+ const MODE_OF = { default: 'ask', acceptEdits: 'edits', auto: 'never', plan: 'plan', bypassPermissions: 'bypass', ask: 'ask', edits: 'edits', never: 'never', bypass: 'bypass' }
62
+
63
+ function parseArgs(argv) {
64
+ const o = { prompt: [], format: 'text' }
65
+ for (let i = 0; i < argv.length; i += 1) {
66
+ const a = argv[i]
67
+ const next = () => { if (i + 1 >= argv.length) throw new Error(`${a} needs a value`); i += 1; return argv[i] }
68
+ if (a === '-p' || a === '--print') o.print = true
69
+ else if (a === '-c' || a === '--continue') o.continue = true
70
+ else if (a === '-r' || a === '--resume') o.resume = argv[i + 1] && !argv[i + 1].startsWith('-') ? next() : true
71
+ else if (a === '--output-format') o.format = next()
72
+ else if (a === '--permission-mode') o.mode = next()
73
+ else if (a === '--dangerously-skip-permissions') o.mode = 'bypassPermissions'
74
+ else if (a === '--allowedTools' || a === '--allowed-tools') o.allowed = next().split(/[,\s]+(?![^(]*\))/).filter(Boolean)
75
+ else if (a === '--disallowedTools' || a === '--disallowed-tools') o.denied = next().split(/[,\s]+(?![^(]*\))/).filter(Boolean)
76
+ else if (a === '--mcp-config') (o.mcp ||= []).push(next())
77
+ else if (a === '--add-dir') (o.dirs ||= []).push(next())
78
+ else if (a === '--append-system-prompt') o.appendSystem = next()
79
+ else if (a === '--model') o.model = next()
80
+ else if (a === '--effort') o.effort = next()
81
+ else if (a === '--max-turns') o.maxTurns = Number(next())
82
+ else if (a === '--server') o.server = next()
83
+ else if (a === '-v' || a === '--version') o.version = true
84
+ else if (a === '-h' || a === '--help') o.help = true
85
+ else if (a.startsWith('-')) throw new Error(`unknown option ${a} — aipictury --help`)
86
+ else o.prompt.push(a)
87
+ }
88
+ o.prompt = o.prompt.join(' ').trim()
89
+ if (o.mode && !MODE_OF[o.mode]) throw new Error(`--permission-mode is one of: default, acceptEdits, auto, plan, bypassPermissions`)
90
+ if (!['text', 'json', 'stream-json'].includes(o.format)) throw new Error('--output-format is text, json or stream-json')
91
+ return o
92
+ }
93
+
94
+ // ---- the terminal ----
95
+ const tty = process.stdout.isTTY
96
+ const paint = (code) => (t) => (tty ? `\x1b[${code}m${t}\x1b[0m` : String(t))
97
+ const dim = paint(2); const bold = paint(1); const red = paint(31); const green = paint(32); const yellow = paint(33); const cyan = paint(36)
98
+
99
+ function setup(o) {
100
+ const home = process.env.AIPICTURY_HOME || path.join(os.homedir(), '.aipictury')
101
+ vscode.state.root = process.cwd()
102
+ vscode.state.settingsFile = path.join(home, 'cli.json')
103
+ vscode.state.logFile = path.join(home, 'logs', 'cli.log')
104
+ for (const [k, v] of Object.entries(pkg.contributes.configuration.properties)) if ('default' in v) vscode.state.defaults[k] = v.default
105
+ // The terminal starts as Claude Code's does: asking before changes and commands (in print
106
+ // mode, refusing them) — a script gets only what it allows. cli.json or --permission-mode
107
+ // choose otherwise.
108
+ vscode.state.defaults['aipictury.approval'] = 'ask'
109
+ try { Object.assign(vscode.state.settings, JSON.parse(fs.readFileSync(vscode.state.settingsFile, 'utf8'))) } catch { /* no settings yet */ }
110
+ // This run's choices: never written to the settings file.
111
+ if (o.server || process.env.AIPICTURY_URL) vscode.state.settings['aipictury.server'] = (o.server || process.env.AIPICTURY_URL).replace(/\/+$/, '')
112
+ if (o.mode) {
113
+ vscode.state.settings['aipictury.approval'] = MODE_OF[o.mode]
114
+ if (MODE_OF[o.mode] === 'bypass') vscode.state.settings['aipictury.allowDangerouslySkipPermissions'] = true
115
+ }
116
+ if (o.effort) vscode.state.settings['aipictury.effort'] = o.effort
117
+ // Tools that only an editor has are not offered here.
118
+ vscode.cli = true
119
+ RUN.allowed = o.allowed
120
+ RUN.denied = o.denied
121
+ for (const dir of o.dirs || []) {
122
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) throw new Error(`--add-dir: ${dir} is not a folder`)
123
+ }
124
+ if (o.dirs?.length) require(path.join(EXT, 'paths.js')).addRoots(o.dirs)
125
+ if (o.mcp?.length) {
126
+ require(path.join(EXT, 'mcp.js')).addConfigs(o.mcp.map((given) => {
127
+ if (String(given).trim().startsWith('{')) {
128
+ try { return { servers: JSON.parse(given).mcpServers || {} } } catch { throw new Error('--mcp-config: that JSON does not parse') }
129
+ }
130
+ if (!fs.existsSync(given)) throw new Error(`--mcp-config: ${given} does not exist`)
131
+ return path.resolve(given)
132
+ }))
133
+ }
134
+ if (o.appendSystem) require(path.join(EXT, 'agent.js')).appendSystem(o.appendSystem)
135
+ }
136
+
137
+ // The GPU server: --server, AIPICTURY_URL or the settings file; else the one chosen with your account.
138
+ const server = () => require(path.join(EXT, 'gpuServer.js')).url()
139
+
140
+ async function serverConfig() {
141
+ if (!server()) throw new Error('no GPU server is set — choose it at https://aipictury.com (it is kept with your account), or pass --server')
142
+ const r = await fetch(`${server()}/api/vscode/config`, { signal: AbortSignal.timeout(15_000) })
143
+ if (!r.ok) throw new Error(`the server answered ${r.status}${r.status === 401 ? `: ${(await r.json().catch(() => ({}))).detail || 'your account is refused'}` : ''}`)
144
+ return r.json()
145
+ }
146
+
147
+ /** One conversation, saved where the extension keeps this folder's conversations. */
148
+ // What this run was started with that every conversation of it carries (setup).
149
+ const RUN = {}
150
+
151
+ class Conversation {
152
+ constructor(store, entry = null) {
153
+ this.store = store
154
+ this.id = entry?.id || `c${Date.now().toString(36)}`
155
+ this.created = entry?.created || Date.now()
156
+ this.title = entry?.title && entry.renamed ? entry.title : null
157
+ this.aiTitle = entry?.aiTitle || null
158
+ this.history = entry?.history || []
159
+ this.transcript = entry?.transcript || []
160
+ this.todos = entry?.todos || []
161
+ // This run's own rules (--allowedTools, --disallowedTools): every conversation, both modes.
162
+ this.session = { allowAll: false, ...(RUN.allowed?.length ? { commandAllow: RUN.allowed } : {}), ...(RUN.denied?.length ? { commandDeny: RUN.denied } : {}) }
163
+ this.usage = entry?.usage || { turns: 0, prompt: 0, output: 0 }
164
+ }
165
+
166
+ get displayTitle() {
167
+ const first = this.history.find((m) => m.role === 'user')
168
+ return this.title || this.aiTitle || (first ? String(first.content).replace(/\[[^\]]*\]\n?/g, '').replace(/\s+/g, ' ').trim().slice(0, 80) : 'New conversation')
169
+ }
170
+
171
+ save(model) {
172
+ if (!this.history.some((m) => m.role === 'user')) return
173
+ this.store.save({ id: this.id, title: this.displayTitle, renamed: !!this.title, aiTitle: this.aiTitle || undefined, created: this.created,
174
+ updated: Date.now(), server: server(), model: model || null, history: this.history, transcript: this.transcript, todos: this.todos,
175
+ usage: this.usage, origin: 'cli' })
176
+ }
177
+ }
178
+
179
+ /** One turn of the conversation: the agent runs until it answers. Returns the run's result. */
180
+ async function runTurn(convo, text, ui, { model, maxTurns, content } = {}) {
181
+ const agent = require(path.join(EXT, 'agent.js'))
182
+ convo.history.push({ role: 'user', content: content || text })
183
+ convo.transcript.push({ kind: 'user', text, at: Date.now() })
184
+ const controller = new AbortController()
185
+ ui.onStart?.(controller)
186
+ let turns = 0
187
+ let lastText = ''
188
+ try {
189
+ const result = await agent.run({
190
+ server: server(), model, history: convo.history, session: convo.session, signal: controller.signal,
191
+ onDelta: (d) => ui.delta?.(d),
192
+ onThinking: (d) => ui.thinking?.(d),
193
+ onTool: (event) => {
194
+ const key = event.id || `${event.tool}:${JSON.stringify(event.args)}`
195
+ const existing = convo.transcript.find((t) => t.kind === 'tool' && t.key === key)
196
+ if (existing) Object.assign(existing, event); else convo.transcript.push({ kind: 'tool', key, at: Date.now(), ...event })
197
+ ui.tool?.(event)
198
+ },
199
+ onNudge: (reason, detail) => ui.nudge?.(reason, detail),
200
+ askUser: (questions, signal) => ui.askUser(questions, signal),
201
+ approve: (request, signal) => ui.approve(request, signal),
202
+ approvePlan: (plan, signal) => ui.approvePlan(plan, signal),
203
+ onTodos: (todos) => { convo.todos = todos; ui.todos?.(todos) },
204
+ trustHooks: (groups) => ui.trust('hooks', groups),
205
+ trustMcp: (servers) => ui.trust('MCP servers', servers),
206
+ onMessages: (messages) => { convo.history = messages; convo.save(model) },
207
+ onTurn: ({ text: said, meta }) => {
208
+ turns += 1
209
+ if (String(said || '').trim()) { convo.transcript.push({ kind: 'assistant', text: said, meta, at: Date.now() }); lastText = said }
210
+ convo.usage.turns += 1
211
+ convo.usage.prompt += meta?.context_used || 0
212
+ convo.usage.output += meta?.eval_tokens || meta?.tokens || 0
213
+ ui.turn?.({ text: said, meta })
214
+ if (maxTurns && turns >= maxTurns) controller.abort()
215
+ },
216
+ })
217
+ convo.history = result.messages
218
+ if (result.error) convo.transcript.push({ kind: 'error', text: result.error, at: Date.now() })
219
+ convo.save(model)
220
+ return { ...result, text: lastText, turns }
221
+ } finally { ui.onEnd?.() }
222
+ }
223
+
224
+ // ---- print mode: one prompt, no questions ----
225
+ async function printMode(o, convo, text) {
226
+ const started = Date.now()
227
+ const events = o.format === 'stream-json'
228
+ const emit = (obj) => process.stdout.write(`${JSON.stringify(obj)}\n`)
229
+ if (events) emit({ type: 'system', subtype: 'init', session_id: convo.id, cwd: process.cwd(), model: o.model || null, version: VERSION })
230
+ let planText = null
231
+ const ui = {
232
+ delta: () => {},
233
+ tool: (e) => { if (events && (e.status === 'done' || e.status === 'failed' || e.status === 'refused')) emit({ type: 'tool', tool: e.tool, args: e.args, status: e.status, result: typeof e.result === 'string' ? e.result.slice(0, 4000) : undefined }) },
234
+ turn: ({ text: said }) => { if (events && String(said || '').trim()) emit({ type: 'assistant', text: said }) },
235
+ // Nobody to ask: what needs a yes is refused, a question is left to the agent.
236
+ approve: async (request) => ({ decision: 'deny', feedback: `not allowed in print mode (-p): ${request.kind === 'command' ? request.command : request.path || request.tool} — allow it with --allowedTools or --permission-mode` }),
237
+ askUser: async () => ({ dismissed: true }),
238
+ // The plan is the answer: the run ends there, with no further turn.
239
+ approvePlan: async (plan) => { planText = plan; ui.controller?.abort(); return { approved: false, feedback: 'Stop here: the plan is the answer.' } },
240
+ trust: async () => false,
241
+ }
242
+ const result = await runTurn(convo, text, { ...ui, onStart: (c) => { ui.controller = c; if (planText === null) process.once('SIGINT', () => c.abort()) } }, { model: o.model, maxTurns: o.maxTurns })
243
+ const answer = planText ?? result.text
244
+ const failed = !!result.error && !result.stopped
245
+ if (o.format === 'text') process.stdout.write(`${answer || (result.error ? `error: ${result.error}` : '')}\n`)
246
+ else emit({ type: 'result', subtype: failed ? 'error' : 'success', is_error: failed, result: answer, session_id: convo.id, num_turns: result.turns,
247
+ duration_ms: Date.now() - started, error: result.error || undefined, usage: { prompt_tokens: convo.usage.prompt, output_tokens: convo.usage.output } })
248
+ return failed ? 1 : 0
249
+ }
250
+
251
+ // ---- interactive ----
252
+ // What can be typed after / here — the extension's names, where a terminal can do them.
253
+ // The project's custom commands (.aipictury/commands, .claude/commands) and skills follow.
254
+ const SLASH = [
255
+ ['help', 'What can be typed here', ''],
256
+ ['clear', 'Start a new conversation', ''],
257
+ ['compact', 'Summarize the conversation so far, to free context', '[what to focus on]'],
258
+ ['btw', 'Ask a side question without adding it to the conversation', '<question>'],
259
+ ['resume', 'Carry on an earlier conversation of this folder', ''],
260
+ ['rename', 'Rename this conversation', '<title>'],
261
+ ['export', 'Save the conversation as Markdown', '[file]'],
262
+ ['todos', 'The agent\'s to-do list', ''],
263
+ ['model', 'Switch the model', '[name]'],
264
+ ['effort', 'How hard the model reasons', '[low|medium|high|max|ultra]'],
265
+ ['mode', 'When the agent asks first', '[default|acceptEdits|auto|plan]'],
266
+ ['permissions', 'What it may do without asking, must ask about, may never do', '[allow|ask|deny <rule>]'],
267
+ ['mcp', 'MCP servers and their tools', ''],
268
+ ['status', 'Version, account server, model, mode, folder', ''],
269
+ ['usage', 'Tokens and time this conversation has used', ''],
270
+ ['init', 'Write an AIPICTURY.md that explains this codebase to the agent', ''],
271
+ ['review', 'Review the uncommitted changes', '[what to focus on]'],
272
+ ['exit', 'Quit', ''],
273
+ ]
274
+
275
+ async function interactive(o, convo, first, cfg) {
276
+ const T = require('./terminal.js')
277
+ const agent = require(path.join(EXT, 'agent.js'))
278
+ const context = require(path.join(EXT, 'context.js'))
279
+ const ext = require(path.join(EXT, 'extension.js'))
280
+ // A terminal gets line editing, completion and a remembered history; a pipe (a script, a
281
+ // test) plain lines, each one a message.
282
+ const terminal = !!(process.stdin.isTTY && process.stdout.isTTY)
283
+ const historyFile = path.join(HOME(), 'cli-history.json')
284
+ let remembered = []
285
+ try { remembered = terminal ? JSON.parse(fs.readFileSync(historyFile, 'utf8')) : [] } catch { /* none yet */ }
286
+ const names = () => [...SLASH.map(([n]) => n), ...context.commands().map((c) => c.name), ...context.skills().map((s) => s.name)]
287
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, terminal, historySize: 500, history: remembered,
288
+ removeHistoryDuplicates: true,
289
+ completer: terminal ? T.completer({ commands: names, files: async (q) => (await ext.mentionCandidates(q)).map((c) => c.path) }) : undefined })
290
+
291
+ // Every line typed comes here; a question waiting takes the next one, else it is a message.
292
+ const inbox = []
293
+ let waiting = null
294
+ let closed = false
295
+ rl.on('line', (line) => {
296
+ if (waiting?.question) { const w = waiting; waiting = null; w.resolve(line); return }
297
+ inbox.push({ line, at: Date.now() })
298
+ if (running && terminal && line.trim()) say(dim(` queued: ${line.trim().slice(0, 80)}`))
299
+ if (waiting) { const w = waiting; waiting = null; w.resolve() }
300
+ })
301
+ rl.on('close', () => { closed = true; if (waiting) { const w = waiting; waiting = null; w.resolve(null) } })
302
+ const nextLine = () => new Promise((resolve) => {
303
+ if (inbox.length) { resolve(inbox.shift().line); return }
304
+ if (closed) { resolve(null); return }
305
+ waiting = { resolve: () => resolve(inbox.length ? inbox.shift().line : null) }
306
+ })
307
+ /** A question at the prompt: its answer, or null when it is called off (Ctrl+C, Ctrl+D). */
308
+ const ask = (q) => new Promise((resolve) => {
309
+ spinner.pause()
310
+ if (streamed) { md.end(); process.stdout.write('\n'); streamed = false }
311
+ // A script answers with the lines it has already sent.
312
+ if (!terminal && inbox.length) { process.stdout.write(q); resolve(inbox.shift().line); return }
313
+ if (closed) { resolve(null); return }
314
+ waiting = { question: true, resolve: (a) => { rl.setPrompt(PROMPT); resolve(a) } }
315
+ rl.setPrompt(q)
316
+ rl.prompt()
317
+ })
318
+
319
+ let running = null
320
+ let model = o.model || null
321
+ let streamed = false
322
+ let shellContext = [] // what was run with ! since the last message: the next one carries it
323
+ const shown = new Map()
324
+ const PROMPT = `${bold('›')} `
325
+ const md = new T.MarkdownStream()
326
+ const spinner = new T.Spinner({ busyTyping: () => !!rl.line })
327
+ const say = (t) => { spinner.pause(); if (streamed) { md.end(); process.stdout.write('\n'); streamed = false } process.stdout.write(`${t}\n`); if (running) spinner.start() }
328
+ const mode = () => agent.modeNow()
329
+ const label = (e) => {
330
+ const a = e.args || {}
331
+ return String(a.command || a.path || a.pattern || a.url || a.query || a.description || a.title || a.action || '').replace(/\s+/g, ' ').slice(0, 100)
332
+ }
333
+ const ui = {
334
+ onStart: (c) => { running = c; streamed = false; spinner.start('thinking') },
335
+ onEnd: () => { running = null; spinner.stop(); if (streamed) { md.end(); process.stdout.write('\n'); streamed = false } },
336
+ delta: (d) => {
337
+ spinner.pause()
338
+ if (!streamed) { process.stdout.write('\n'); streamed = true }
339
+ md.push(d)
340
+ },
341
+ thinking: () => { if (!streamed) spinner.start('thinking') },
342
+ tool: (e) => {
343
+ const key = e.id || `${e.tool}:${JSON.stringify(e.args)}`
344
+ if (e.status === 'running' && !shown.has(key)) { shown.set(key, true); say(`${cyan('⏺')} ${bold(e.tool)} ${dim(label(e))}`); spinner.start(e.tool) }
345
+ if (['done', 'failed', 'refused', 'plan'].includes(e.status)) {
346
+ const out = typeof e.result === 'string' ? e.result.trim().split('\n') : []
347
+ const mark = e.status === 'done' ? green('⎿') : red('⎿')
348
+ if (!shown.has(key)) say(`${cyan('⏺')} ${bold(e.tool)} ${dim(label(e))}`)
349
+ shown.set(key, 'end')
350
+ say(` ${mark} ${dim(`${out[0]?.slice(0, 160) || e.status}${out.length > 1 ? ` (+${out.length - 1} lines)` : ''}`)}`)
351
+ spinner.start('thinking')
352
+ }
353
+ },
354
+ nudge: (reason, detail) => { if (detail?.text) say(dim(` · ${detail.text}`)) },
355
+ todos: (todos) => say(dim(todos.map((t) => ` ${t.status === 'completed' ? '☑' : t.status === 'in_progress' ? '▸' : '☐'} ${t.content}`).join('\n'))),
356
+ async approve(request) {
357
+ const what = request.kind === 'command' ? `Run ${bold(request.command)}`
358
+ : request.kind === 'edit' ? `${request.before == null ? 'Create' : 'Change'} ${bold(request.path)}`
359
+ : request.tool === 'browser' ? `Use the browser on ${bold(request.site || 'a page')}` : `Use ${bold(request.tool)} ${dim(JSON.stringify(request.args || {}).slice(0, 200))}`
360
+ say(`\n${yellow('?')} ${what}${request.because ? dim(` — ${request.because}`) : ''}`)
361
+ if (request.kind === 'edit') for (const h of (request.hunks || []).slice(0, 6)) say([...h.removed.slice(0, 8).map((l) => red(` - ${l}`)), ...h.added.slice(0, 12).map((l) => green(` + ${l}`))].join('\n'))
362
+ const answer = await ask(` ${bold('y')}es · ${bold('a')}lways${request.rule ? dim(` (${request.rule}, this conversation)`) : ' (this conversation)'} · ${bold('n')}o, or say what to do instead › `)
363
+ spinner.start()
364
+ if (answer === null) return { decision: 'deny', feedback: '' }
365
+ const a = answer.trim()
366
+ if (/^y(es)?$/i.test(a) || a === '') return { decision: 'allow' }
367
+ if (/^a(lways)?$/i.test(a)) return { decision: 'allow_all', destination: 'conversation', prefix: request.prefix }
368
+ return { decision: 'deny', feedback: /^no?$/i.test(a) ? '' : a }
369
+ },
370
+ async askUser(questions) {
371
+ const answers = []
372
+ for (const q of questions) {
373
+ say(`\n${yellow('?')} ${bold(q.question)}`)
374
+ q.options.forEach((opt, i) => say(` ${i + 1}. ${opt.label}${opt.description ? dim(` — ${opt.description}`) : ''}`))
375
+ const answer = await ask(` ${q.multiSelect ? 'numbers (1,3)' : 'a number'}, your own answer, or Enter to let the agent decide › `)
376
+ const a = (answer || '').trim()
377
+ if (!a) { spinner.start(); return { dismissed: true } }
378
+ const nums = a.split(/[,\s]+/).map(Number)
379
+ if (nums.every((n) => Number.isInteger(n) && n >= 1 && n <= q.options.length)) answers.push({ header: q.header, question: q.question, picked: nums.map((n) => q.options[n - 1].label) })
380
+ else answers.push({ header: q.header, question: q.question, picked: [], other: a })
381
+ }
382
+ spinner.start()
383
+ return { answers }
384
+ },
385
+ async approvePlan(plan) {
386
+ say(`\n${bold('The plan:')}\n`)
387
+ const view = new T.MarkdownStream()
388
+ view.push(`${plan}\n`)
389
+ view.end()
390
+ const answer = await ask(`\n ${bold('y')}es, go ahead (asking as now) · ${bold('a')}uto (go ahead, asking only when risky) · or say what to change › `)
391
+ spinner.start()
392
+ if (answer === null) return { approved: false, feedback: '' }
393
+ const a = answer.trim()
394
+ if (/^y(es)?$/i.test(a) || a === '') return { approved: true, mode: 'ask' }
395
+ if (/^a(uto)?$/i.test(a)) return { approved: true, mode: 'never' }
396
+ return { approved: false, feedback: a }
397
+ },
398
+ async trust(what, items) {
399
+ say(`\n${yellow('?')} This project has ${what} of its own: ${dim(JSON.stringify(items).slice(0, 300))}`)
400
+ const answer = await ask(` trust and use them? ${bold('y')}/${bold('n')} › `)
401
+ spinner.start()
402
+ return /^y/i.test((answer || '').trim())
403
+ },
404
+ }
405
+ const contextShare = () => Math.min(100, Math.round((agent.estimateTokens(convo.history) / Math.max(1, agent.budget())) * 100))
406
+ const status = () => `${dim(`${server()} · ${model || cfg?.roles?.agent || 'the agent model'} · ${agent.MODES[mode()]?.label || mode()} · ${process.cwd()}`)}`
407
+ say(`${bold('AIPictury')} ${VERSION} — ${convo.history.length ? `carrying on: ${convo.displayTitle}` : 'a new conversation'}\n${status()}`)
408
+ say(dim(terminal ? '/ for commands (Tab completes) · @ to mention a file · ! runs a command · end a line with \\ for another · Esc stops a run · Ctrl+C twice quits'
409
+ : '/help for commands · Ctrl+C stops a run · Ctrl+C twice quits'))
410
+ let quitArmed = false
411
+ rl.on('SIGINT', () => {
412
+ if (running) { running.abort(); say(dim('\n stopped')); return }
413
+ if (waiting?.question) { const w = waiting; waiting = null; process.stdout.write('\n'); w.resolve(null); return }
414
+ if (quitArmed) { rl.close(); return }
415
+ quitArmed = true
416
+ rl.line && rl.write(null, { ctrl: true, name: 'u' })
417
+ say(dim('\n (Ctrl+C again to quit)'))
418
+ rl.prompt()
419
+ })
420
+ if (terminal) process.stdin.on('keypress', (_, key) => { if (key?.name === 'escape' && running) running.abort() })
421
+
422
+ const turn = async (text, extra = []) => {
423
+ const started = Date.now()
424
+ try {
425
+ // @mentions are read now, as the editor does; what ! ran goes with the message too.
426
+ const mentioned = await ext.resolveMentions(text, convo.session).catch(() => [])
427
+ const parts = [text, ...shellContext.splice(0)]
428
+ if (mentioned.length) parts.push(`Files the user referenced (already read for you):\n\n${mentioned.join('\n\n')}`)
429
+ const r = await runTurn(convo, text, ui, { model, content: [...parts, ...extra].join('\n\n') })
430
+ if (r.error && !r.stopped) say(red(`\n error: ${r.error}`))
431
+ } catch (error) { say(red(`\n error: ${error.message}`)) }
432
+ if (terminal) say(dim(` ${Math.round((Date.now() - started) / 100) / 10}s · ${convo.usage.output} tokens written · context ${contextShare()}% used`))
433
+ }
434
+ const shell = (command) => {
435
+ const { spawnSync } = require('child_process')
436
+ const r = spawnSync(process.env.SHELL || '/bin/sh', ['-c', command], { cwd: process.cwd(), encoding: 'utf8', timeout: 120_000, maxBuffer: 8 * 1024 * 1024 })
437
+ const out = `${r.stdout || ''}${r.stderr || ''}`.replace(/\s+$/, '')
438
+ const lines = out ? out.split('\n') : []
439
+ lines.slice(0, 40).forEach((l) => say(` ${dim('⎿')} ${l}`))
440
+ if (lines.length > 40) say(dim(` … ${lines.length - 40} more lines (all of it goes with your next message)`))
441
+ if (r.status) say(red(` exit ${r.status}`))
442
+ if (r.error) say(red(` ${r.error.message}`))
443
+ shellContext.push(`I ran this myself in the terminal: \`${command}\`${r.status ? ` (exit ${r.status})` : ''}\n\n\`\`\`\n${out.slice(-20_000)}\n\`\`\``)
444
+ }
445
+ const command = async (line) => {
446
+ const [name, ...rest] = line.slice(1).split(/\s+/)
447
+ const arg = rest.join(' ').trim()
448
+ const cfgSet = (k, v) => { vscode.state.settings[`aipictury.${k}`] = v }
449
+ const folder = process.cwd()
450
+ switch (name) {
451
+ case 'help': {
452
+ const width = Math.max(...SLASH.map(([n, , h]) => `/${n} ${h}`.length))
453
+ for (const [n, d, h] of SLASH) say(` ${cyan(`/${n}`)} ${dim(h)}${' '.repeat(width - `/${n} ${h}`.length)} ${d}`)
454
+ const custom = context.commands()
455
+ if (custom.length) say(dim(` this project's commands: ${custom.map((c) => `/${c.name}`).join(', ')}`))
456
+ say(dim(' @path mentions a file · !command runs it here (its output goes with your next message) · a line ending in \\ continues'))
457
+ return true
458
+ }
459
+ case 'exit': case 'quit': rl.close(); return true
460
+ case 'clear': convo.save(model); Object.assign(convo, new Conversation(convo.store)); shellContext = []; say(dim(' a new conversation')); return true
461
+ case 'model': if (arg) { model = arg } say(dim(` model: ${model || cfg?.roles?.agent}${arg ? '' : ` (installed: ${(cfg?.models || []).filter((m) => m.tools).map((m) => m.name).join(', ')})`}`)); return true
462
+ case 'effort': if (arg) { if (!agent.EFFORT[arg]) { say(red(` one of: ${Object.keys(agent.EFFORT).join(', ')}`)); return true } cfgSet('effort', arg) } say(dim(` effort: ${vscode.workspace.getConfiguration('aipictury').get('effort')}`)); return true
463
+ case 'mode': if (arg) { if (!MODE_OF[arg] || MODE_OF[arg] === 'bypass') { say(red(' one of: default, acceptEdits, auto, plan')); return true } cfgSet('approval', MODE_OF[arg]) } say(status()); return true
464
+ case 'status': say(` AIPictury ${VERSION}`); say(status()); say(dim(` conversation ${convo.id}: ${convo.displayTitle}`)); return true
465
+ case 'usage': case 'cost': case 'context':
466
+ say(dim(` ${convo.usage.turns} model turns · ${convo.usage.prompt} tokens read · ${convo.usage.output} written · context ${contextShare()}% used (${agent.estimateTokens(convo.history)} of ${agent.budget()} tokens)`))
467
+ return true
468
+ case 'rename': if (!arg) { say(red(' /rename <title>')); return true } convo.title = arg; convo.save(model); say(dim(` renamed: ${arg}`)); return true
469
+ case 'todos':
470
+ if (!convo.todos.length) say(dim(' no to-do list in this conversation'))
471
+ else ui.todos(convo.todos)
472
+ return true
473
+ case 'export': {
474
+ if (!convo.transcript.length) { say(dim(' nothing to export yet')); return true }
475
+ const file = path.resolve(arg || `aipictury-${convo.id}.md`)
476
+ fs.writeFileSync(file, ext.toMarkdown(convo.displayTitle, convo.transcript))
477
+ say(dim(` saved ${file}`))
478
+ return true
479
+ }
480
+ case 'permissions': case 'allowed-tools': {
481
+ const prefs = require(path.join(EXT, 'prefs.js'))
482
+ const m = /^(allow|ask|deny)\s+(.+)$/.exec(arg)
483
+ if (m) {
484
+ if (!require(path.join(EXT, 'permissions.js')).parseRule(m[2])) { say(red(' write a rule like Bash(npm test:*) or Read(./.env)')); return true }
485
+ const file = prefs.addRule(folder, 'local', m[1], m[2])
486
+ say(dim(` added ${m[2]} to ${m[1]} (${file.replace(os.homedir(), '~')})`))
487
+ return true
488
+ }
489
+ const merged = prefs.load(folder).merged.permissions
490
+ for (const kind of ['allow', 'ask', 'deny']) {
491
+ const rules = merged[kind] || []
492
+ say(` ${bold(kind)} ${dim(`(${rules.length})`)}`)
493
+ for (const r of rules.slice(0, 30)) say(` ${r.rule} ${dim(r.file.replace(os.homedir(), '~'))}`)
494
+ if (rules.length > 30) say(dim(` … ${rules.length - 30} more`))
495
+ }
496
+ if (RUN.allowed?.length || RUN.denied?.length) say(dim(` this run: allowed ${RUN.allowed?.join(', ') || '—'} · denied ${RUN.denied?.join(', ') || '—'}`))
497
+ say(dim(' add one: /permissions allow|ask|deny <rule> (kept for this project, just you)'))
498
+ return true
499
+ }
500
+ case 'mcp': {
501
+ const mcp = require(path.join(EXT, 'mcp.js'))
502
+ const live = new Map(mcp.status().map((s) => [s.name, s]))
503
+ const configured = [...mcp.detailed(folder).entries()]
504
+ if (!configured.length && !live.size) { say(dim(' no MCP servers configured (.mcp.json, ~/.aipictury/mcp.json, --mcp-config)')); return true }
505
+ for (const [n, c] of configured) {
506
+ const s = live.get(n)
507
+ say(` ${bold(n)} ${dim(c.disabled ? 'disabled' : s ? `${s.state}${s.error ? ` — ${s.error}` : ''}` : 'connects when the agent first needs it')}`)
508
+ if (s?.tools?.length) say(dim(` ${s.tools.join(', ')}`))
509
+ }
510
+ return true
511
+ }
512
+ case 'compact': {
513
+ if (convo.history.length < 3) { say(dim(' nothing to summarize yet')); return true }
514
+ const before = agent.estimateTokens(convo.history)
515
+ spinner.start('summarizing'); running = new AbortController()
516
+ try {
517
+ const next = await agent.summarizeEarlier(server(), [{ role: 'system', content: '' }, ...convo.history], { model: model || undefined, force: true, focus: arg, signal: running.signal })
518
+ running = null; spinner.stop()
519
+ if (!next) say(dim(' the conversation is already short — nothing to summarize'))
520
+ else { convo.history = next.slice(1); convo.save(model); say(dim(` summarized: ${before} → ${agent.estimateTokens(convo.history)} tokens`)) }
521
+ } catch (error) { running = null; spinner.stop(); say(red(` could not summarize: ${error.message}`)) }
522
+ return true
523
+ }
524
+ case 'btw': {
525
+ if (!arg) { say(red(' /btw <question>')); return true }
526
+ const side = new T.MarkdownStream()
527
+ spinner.start('thinking'); running = new AbortController()
528
+ let started = false
529
+ try {
530
+ const answer = await agent.sideQuestion(server(), convo.history, arg, { model: model || undefined, session: convo.session, signal: running.signal,
531
+ onDelta: (d) => { spinner.pause(); if (!started) { process.stdout.write(`\n${dim('(aside)')} `); started = true } side.push(d) } })
532
+ if (!started && answer) { spinner.pause(); process.stdout.write(`\n${dim('(aside)')} `); side.push(answer) }
533
+ side.end(); process.stdout.write('\n')
534
+ } catch (error) { say(red(` error: ${error.message}`)) } finally { running = null; spinner.stop() }
535
+ return true
536
+ }
537
+ case 'init': await turn(ext.INIT_PROMPT); return true
538
+ case 'review': await turn(arg ? `${ext.REVIEW_PROMPT}\n\nFocus on: ${arg}` : ext.REVIEW_PROMPT); return true
539
+ case 'resume': {
540
+ const list = convo.store.list().slice(0, 15)
541
+ if (!list.length) { say(dim(' no conversations in this folder yet')); return true }
542
+ list.forEach((e, i) => say(` ${i + 1}. ${e.title} ${dim(new Date(e.updated).toLocaleString())}`))
543
+ const a = ((await ask(' which (number), or Enter › ')) || '').trim()
544
+ const entry = list[Number(a) - 1] && convo.store.get(list[Number(a) - 1].id)
545
+ if (entry) { convo.save(model); Object.assign(convo, new Conversation(convo.store, entry)); say(dim(` carrying on: ${convo.displayTitle}`)) }
546
+ return true
547
+ }
548
+ default: {
549
+ // The project's own commands, in Claude Code's format, and its skills.
550
+ const prompt = context.expandCommand(line)
551
+ if (prompt) {
552
+ const meta = context.commandMeta(line)
553
+ const permissions = require(path.join(EXT, 'permissions.js'))
554
+ const shellMod = require(path.join(EXT, 'shell.js'))
555
+ const allowed = (cmd) => {
556
+ const verdict = permissions.decide('run_command', { command: cmd }, { folder })
557
+ if (verdict.decision === 'deny') return false
558
+ const granted = permissions.decide('run_command', { command: cmd }, { rules: { allow: meta.allowedTools, ask: [], deny: [] } }).decision === 'allow'
559
+ return granted || verdict.decision === 'allow' || shellMod.isReadOnly(cmd)
560
+ }
561
+ const before = convo.session.commandAllow
562
+ convo.session.commandAllow = [...(RUN.allowed || []), ...meta.allowedTools]
563
+ try { await turn(await context.runInline(prompt, { cwd: folder, allowed })) } finally { convo.session.commandAllow = before }
564
+ return true
565
+ }
566
+ const skill = context.skills().find((s) => s.name === name)
567
+ if (skill) { await turn(`Use the ${skill.name} skill${arg ? ` on: ${arg}` : ''}.`); return true }
568
+ return false
569
+ }
570
+ }
571
+ }
572
+
573
+ /** The next message: a line, the lines after it while each ends in \, or a paste of many. */
574
+ const nextMessage = async () => {
575
+ rl.setPrompt(PROMPT)
576
+ rl.prompt()
577
+ let line = await nextLine()
578
+ if (line === null) return null
579
+ const lines = []
580
+ for (;;) {
581
+ if (line.endsWith('\\')) {
582
+ lines.push(line.slice(0, -1))
583
+ rl.setPrompt(dim('… '))
584
+ rl.prompt()
585
+ line = await nextLine()
586
+ if (line === null) break
587
+ continue
588
+ }
589
+ lines.push(line)
590
+ // Pasted lines come in one burst: they are one message. A person types slower.
591
+ if (!terminal) break
592
+ await new Promise((r) => setTimeout(r, 25))
593
+ if (!inbox.length) break
594
+ line = inbox.shift().line
595
+ }
596
+ return lines.join('\n')
597
+ }
598
+
599
+ if (first) await turn(first)
600
+ for (;;) {
601
+ const message = await nextMessage()
602
+ if (message === null) break
603
+ quitArmed = false
604
+ const text = message.trim()
605
+ if (!text) continue
606
+ if (text.startsWith('!') && text.length > 1) { shell(text.slice(1).trim()); continue }
607
+ if (!(text.startsWith('/') && !text.includes('\n') && await command(text))) await turn(text)
608
+ if (closed) break
609
+ }
610
+ convo.save(model)
611
+ if (terminal) {
612
+ try { fs.mkdirSync(HOME(), { recursive: true }); fs.writeFileSync(historyFile, JSON.stringify(rl.history.slice(0, 500)), { mode: 0o600 }) } catch { /* not remembered */ }
613
+ }
614
+ if (!closed) rl.close()
615
+ return 0
616
+ }
617
+
618
+ // ---- the AIPictury account (needed, as in VS Code) ----
619
+ const HOME = () => process.env.AIPICTURY_HOME || path.join(os.homedir(), '.aipictury')
620
+
621
+ function accountFor() {
622
+ const { Account, fileStore } = require(path.join(EXT, 'account.js'))
623
+ const config = require(path.join(EXT, 'media', 'vendor', 'firebase-config.json'))
624
+ return new Account({ store: fileStore(path.join(HOME(), 'credentials.json')), config, client: 'the aipictury CLI' })
625
+ }
626
+
627
+ /** The conversations of this run, kept with the account: what was saved is pushed before exiting. */
628
+ function cloudFor(account) {
629
+ const { Cloud } = require(path.join(EXT, 'cloud.js'))
630
+ const { emulatorHosts } = require(path.join(EXT, 'account.js'))
631
+ const config = require(path.join(EXT, 'media', 'vendor', 'firebase-config.json'))
632
+ const deviceFile = path.join(HOME(), 'device.json')
633
+ let id
634
+ try { id = JSON.parse(fs.readFileSync(deviceFile, 'utf8')).id } catch { /* first run */ }
635
+ if (!id) { id = require('crypto').randomUUID(); fs.mkdirSync(HOME(), { recursive: true }); fs.writeFileSync(deviceFile, JSON.stringify({ id })) }
636
+ const cloud = new Cloud({ account, projectId: config.projectId, emulator: emulatorHosts().firestore,
637
+ bucket: config.storageBucket, storageEmulator: emulatorHosts().storage,
638
+ device: { id, name: os.hostname(), app: 'cli', platform: process.platform, version: VERSION } })
639
+ const pending = new Map()
640
+ const removed = []
641
+ const on = (kind) => vscode.workspace.getConfiguration('aipictury').get(`cloud.${kind}`, true)
642
+ require(path.join(EXT, 'sessions.js')).onWrite((e) => {
643
+ if (!e.folder || !on('conversations')) return
644
+ if (e.type === 'remove') { pending.delete(e.id); removed.push([e.folder, e.id]); return }
645
+ pending.set(e.id, [e.folder, { ...e.entry, origin: 'cli' }])
646
+ })
647
+ // Every render the agent makes is kept with the account; the run waits for it before it ends.
648
+ const keeping = []
649
+ require(path.join(EXT, 'mediagen.js')).onRendered((base, jobId) => {
650
+ keeping.push(cloud.keepRender(base, jobId).catch((err) => process.stderr.write(dim(` (render not kept with your account: ${err.message})\n`))))
651
+ })
652
+ return {
653
+ cloud,
654
+ async flush() {
655
+ await Promise.all(keeping)
656
+ if (!(await account.user())) return
657
+ for (const [folder, entry] of pending.values()) await cloud.pushConversation(folder, entry).catch((err) => process.stderr.write(dim(` (not kept with your account: ${err.message})\n`)))
658
+ for (const [folder, id] of removed) await cloud.deleteConversation(folder, id).catch(() => {})
659
+ pending.clear()
660
+ },
661
+ }
662
+ }
663
+
664
+ function openBrowser(url) {
665
+ const { spawn } = require('child_process')
666
+ const cmd = process.platform === 'darwin' ? ['open', [url]] : process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]] : ['xdg-open', [url]]
667
+ try { spawn(cmd[0], cmd[1], { stdio: 'ignore', detached: true }).unref() } catch { /* the address is printed anyway */ }
668
+ }
669
+
670
+ async function accountCommand(name, argv) {
671
+ const account = accountFor()
672
+ if (name === 'whoami') {
673
+ const u = await account.user()
674
+ if (!u) { process.stdout.write('Not signed in — aipictury login\n'); return 1 }
675
+ process.stdout.write(`${u.email || u.displayName} (${u.uid})\n`)
676
+ return 0
677
+ }
678
+ if (name === 'logout') {
679
+ await account.signOut()
680
+ process.stdout.write('Signed out: nothing of the account is kept on this machine.\n')
681
+ return 0
682
+ }
683
+ const noBrowser = argv.includes('--no-browser')
684
+ try {
685
+ const u = await account.signIn({ open: async (url) => {
686
+ process.stderr.write(`Sign in to AIPictury in your browser:\n ${url}\n`)
687
+ if (!noBrowser) openBrowser(url)
688
+ } })
689
+ process.stdout.write(`Signed in as ${u.email || u.displayName}.\n`)
690
+ return 0
691
+ } catch (error) { process.stderr.write(`aipictury: not signed in — ${error.message}\n`); return 1 }
692
+ }
693
+
694
+ async function main() {
695
+ const first = process.argv[2]
696
+ if (['login', 'logout', 'whoami'].includes(first)) return accountCommand(first, process.argv.slice(3))
697
+ let o
698
+ try { o = parseArgs(process.argv.slice(2)) } catch (error) { process.stderr.write(`aipictury: ${error.message}\n`); return 2 }
699
+ if (o.help) { process.stdout.write(`${HELP}\n`); return 0 }
700
+ if (o.version) { process.stdout.write(`${VERSION}\n`); return 0 }
701
+ setup(o)
702
+ // AIPictury needs the account, here as in VS Code.
703
+ const account = accountFor()
704
+ if (!(await account.user())) { process.stderr.write('aipictury: AIPictury needs your account — run: aipictury login\n'); return 1 }
705
+ try { await account.idToken() } catch (error) {
706
+ if (!(await account.user())) { process.stderr.write('aipictury: your session has ended — run: aipictury login\n'); return 1 }
707
+ process.stderr.write(dim(` (the account cannot be reached now: ${error.message})\n`))
708
+ }
709
+ const sync = cloudFor(account)
710
+ // No server set here: the one chosen with the account (aipictury.com), as the editor does.
711
+ if (!require(path.join(EXT, 'gpuServer.js')).explicit()) {
712
+ try { await require(path.join(EXT, 'gpuServer.js')).adopt((await sync.cloud.getPreferences())?.values?.gpuServer) } catch { /* said below if none */ }
713
+ }
714
+ // Every call to the GPU server carries the account's token (the server may require it), and only those.
715
+ const { serverFetch } = require(path.join(EXT, 'account.js'))
716
+ globalThis.fetch = serverFetch({ fetch: globalThis.fetch, account, server })
717
+ try { return await mainRun(o) } finally { await sync.flush() }
718
+ }
719
+
720
+ async function mainRun(o) {
721
+ const { SessionStore } = require(path.join(EXT, 'sessions.js'))
722
+ const store = new SessionStore(process.cwd())
723
+ let entry = null
724
+ if (o.continue) entry = store.list()[0] ? store.get(store.list()[0].id) : null
725
+ if (typeof o.resume === 'string') {
726
+ entry = store.get(o.resume)
727
+ if (!entry) { process.stderr.write(`aipictury: no conversation ${o.resume} in this folder\n`); return 1 }
728
+ }
729
+ if (o.resume === true && o.print) { process.stderr.write('aipictury: --resume needs an id in print mode\n'); return 2 }
730
+ const convo = new Conversation(store, entry)
731
+ let text = o.prompt
732
+ if (!process.stdin.isTTY && o.print) {
733
+ const piped = fs.readFileSync(0, 'utf8')
734
+ if (piped.trim()) text = text ? `${text}\n\n${piped}` : piped
735
+ }
736
+ let cfg
737
+ try { cfg = await serverConfig() } catch (error) {
738
+ process.stderr.write(server() ? `aipictury: cannot use the server at ${server()} — ${error.message}\n` : `aipictury: ${error.message}\n`)
739
+ return 1
740
+ }
741
+ if (o.print) {
742
+ if (!text) { process.stderr.write('aipictury: -p needs a prompt\n'); return 2 }
743
+ return printMode(o, convo, text)
744
+ }
745
+ if (o.resume === true) {
746
+ const list = store.list().slice(0, 15)
747
+ if (!list.length) process.stdout.write('No conversations in this folder yet.\n')
748
+ else list.forEach((e) => process.stdout.write(`${e.id} ${e.title}\n`))
749
+ return 0
750
+ }
751
+ return interactive(o, convo, text, cfg)
752
+ }
753
+
754
+ main().then((code) => {
755
+ require(path.join(EXT, 'agent.js')).stopBackground?.()
756
+ try { require(path.join(EXT, 'mcp.js')).closeAll() } catch { /* none */ }
757
+ require(path.join(EXT, 'browser.js')).close().finally(() => process.exit(code))
758
+ }, (error) => { process.stderr.write(`aipictury: ${error.stack || error.message}\n`); process.exit(1) })