@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/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@davait/aipictury",
3
+ "version": "1.9.2",
4
+ "description": "The AIPictury agent in a terminal: the VS Code extension's agent, on your own GPU server, signed in to your AIPictury account.",
5
+ "bin": {
6
+ "aipictury": "main.js"
7
+ },
8
+ "engines": {
9
+ "node": ">=20"
10
+ },
11
+ "scripts": {
12
+ "prepack": "node pack.mjs",
13
+ "postpack": "node pack.mjs --clean",
14
+ "lint": "../vscode/node_modules/.bin/eslint . --max-warnings=0",
15
+ "test": "node test/cli-check.mjs"
16
+ },
17
+ "author": "Davait, Inc.",
18
+ "license": "SEE LICENSE IN LICENSE.md",
19
+ "homepage": "https://aipictury.com",
20
+ "keywords": [
21
+ "ai",
22
+ "agent",
23
+ "cli",
24
+ "coding",
25
+ "local",
26
+ "gpu",
27
+ "aipictury"
28
+ ],
29
+ "files": [
30
+ "main.js",
31
+ "terminal.js",
32
+ "vscode-shim.js",
33
+ "ext/",
34
+ "LICENSE.md",
35
+ "README.md"
36
+ ],
37
+ "dependencies": {
38
+ "playwright-core": "^1.63.0"
39
+ },
40
+ "publishConfig": {
41
+ "access": "public"
42
+ }
43
+ }
package/terminal.js ADDED
@@ -0,0 +1,124 @@
1
+ 'use strict'
2
+ // The terminal's own pieces for the interactive mode: colours, a spinner while the agent works,
3
+ // the answer's Markdown drawn as it streams, and completion for /commands and @files.
4
+ // Copyright © 2026 Davait, Inc. All rights reserved.
5
+
6
+ const tty = !!process.stdout.isTTY
7
+ const paint = (code) => (t) => (tty ? `\x1b[${code}m${t}\x1b[0m` : String(t))
8
+ const dim = paint(2); const bold = paint(1); const italic = paint(3); const red = paint(31); const green = paint(32)
9
+ const yellow = paint(33); const cyan = paint(36); const magenta = paint(35)
10
+
11
+ // What a line looks like on screen, without its colours: for fitting it to the terminal.
12
+ const visible = (s) => String(s).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, '') // eslint-disable-line no-control-regex
13
+
14
+ /** `code`, **bold**, *italic*, [text](url) — inside a line, code spans left as they are. */
15
+ function inline(line) {
16
+ return line.split(/(`[^`]+`)/).map((part) => {
17
+ if (/^`[^`]+`$/.test(part)) return cyan(part.slice(1, -1))
18
+ return part
19
+ .replace(/\*\*([^*]+)\*\*/g, (_, t) => bold(t))
20
+ .replace(/(^|[\s(])\*([^*\s][^*]*)\*(?=[\s).,;:!?]|$)/g, (_, pre, t) => `${pre}${italic(t)}`)
21
+ .replace(/(^|[\s(])_([^_\s][^_]*)_(?=[\s).,;:!?]|$)/g, (_, pre, t) => `${pre}${italic(t)}`)
22
+ .replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, t, url) => `${t} ${dim(`(${url})`)}`)
23
+ }).join('')
24
+ }
25
+
26
+ /** One finished line of Markdown, for the terminal. `state` carries whether it is inside a fence. */
27
+ function markdownLine(line, state) {
28
+ if (/^\s*(```|~~~)/.test(line)) { state.code = !state.code; return dim(line) }
29
+ if (state.code) return `${dim('│')} ${line}`
30
+ let m
31
+ if ((m = /^(#{1,6})\s+(.*)$/.exec(line))) return m[1].length <= 2 ? bold(inline(m[2])) : bold(dim(inline(m[2])))
32
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) return dim('─'.repeat(40))
33
+ if ((m = /^(\s*)>\s?(.*)$/.exec(line))) return `${m[1]}${dim('│')} ${dim(inline(m[2]))}`
34
+ if ((m = /^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$/.exec(line))) return `${m[1]}${m[2] === ' ' ? '☐' : '☑'} ${inline(m[3])}`
35
+ if ((m = /^(\s*)[-*+]\s+(.*)$/.exec(line))) return `${m[1]}${dim('•')} ${inline(m[2])}`
36
+ if (/^\s*\|.*\|\s*$/.test(line)) return /^\s*\|[\s:|-]+\|\s*$/.test(line) ? dim(line) : inline(line)
37
+ return inline(line)
38
+ }
39
+
40
+ /**
41
+ * The answer as it streams: each piece written at once, and each line drawn again as Markdown
42
+ * once it is whole (when it still fits on one terminal row; a longer one stays as it came).
43
+ */
44
+ class MarkdownStream {
45
+ constructor(out = process.stdout) { this.out = out; this.state = { code: false }; this.line = '' }
46
+
47
+ push(text) {
48
+ const parts = String(text).split('\n')
49
+ parts.forEach((part, i) => {
50
+ if (i > 0) this.finish('\n')
51
+ if (part) { this.out.write(part); this.line += part }
52
+ })
53
+ }
54
+
55
+ finish(end = '') {
56
+ const width = this.out.columns || 80
57
+ const drawn = markdownLine(this.line, this.state)
58
+ if (tty && this.line && visible(this.line).length < width - 1) this.out.write(`\r\x1b[K${drawn}`)
59
+ this.out.write(end)
60
+ this.line = ''
61
+ }
62
+
63
+ end() { if (this.line) this.finish(); this.state = { code: false } }
64
+ }
65
+
66
+ const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
67
+
68
+ /** "⠹ working · 12s · esc to stop", kept on the line below the output while the agent works. */
69
+ class Spinner {
70
+ constructor({ out = process.stdout, busyTyping = () => false } = {}) {
71
+ this.out = out
72
+ this.busyTyping = busyTyping
73
+ this.on = tty
74
+ this.timer = null
75
+ this.frame = 0
76
+ this.shown = false
77
+ }
78
+
79
+ start(label = 'working') {
80
+ if (!this.on) return
81
+ this.label = label
82
+ this.since ||= Date.now()
83
+ if (!this.timer) this.timer = setInterval(() => this.draw(), 100)
84
+ this.draw()
85
+ }
86
+
87
+ draw() {
88
+ // Someone typing ahead owns the line: the spinner waits.
89
+ if (this.busyTyping()) { this.clear(); return }
90
+ const secs = Math.round((Date.now() - this.since) / 1000)
91
+ this.out.write(`\r\x1b[K${magenta(FRAMES[this.frame++ % FRAMES.length])} ${dim(`${this.label} · ${secs}s · esc to stop`)}`)
92
+ this.shown = true
93
+ }
94
+
95
+ clear() { if (this.shown) { this.out.write('\r\x1b[K'); this.shown = false } }
96
+
97
+ /** Hidden for output; start() brings it back with the same clock. */
98
+ pause() { if (this.timer) { clearInterval(this.timer); this.timer = null } this.clear() }
99
+
100
+ stop() { this.pause(); this.since = 0 }
101
+ }
102
+
103
+ /**
104
+ * Completion for the prompt: /commands at the start of a line, @files anywhere.
105
+ * `commands()` lists the names; `files(query)` resolves to paths (the extension's own fuzzy search).
106
+ */
107
+ function completer({ commands, files }) {
108
+ return (line, done) => {
109
+ const slash = /^\/([\w:.-]*)$/.exec(line)
110
+ if (slash) {
111
+ const names = commands().map((n) => `/${n}`)
112
+ const hits = names.filter((n) => n.startsWith(line))
113
+ done(null, [hits.length ? hits : names, line])
114
+ return
115
+ }
116
+ const at = /(?:^|\s)(@[^\s@]*)$/.exec(line)
117
+ if (!at) { done(null, [[], line]); return }
118
+ Promise.resolve(files(at[1].slice(1))).then((found) => {
119
+ done(null, [found.slice(0, 30).map((f) => `@${f}`), at[1]])
120
+ }, () => done(null, [[], line]))
121
+ }
122
+ }
123
+
124
+ module.exports = { tty, dim, bold, italic, red, green, yellow, cyan, magenta, visible, inline, markdownLine, MarkdownStream, Spinner, completer }
package/vscode-shim.js ADDED
@@ -0,0 +1,231 @@
1
+ 'use strict'
2
+ // The few VS Code APIs the agent's code uses, for the terminal: the CLI runs the extension's own
3
+ // agent (agent.js, tools, rules, hooks, MCP, skills, sandbox) with this module standing in for
4
+ // `vscode`. Files are the disk, the workspace is the folder the CLI was started in, settings come
5
+ // from ~/.aipictury/cli.json (and the extension's defaults), the log is a file. What only VS
6
+ // Code has — language servers, open editors, notebooks, its commands — is absent, and the CLI
7
+ // does not offer the tools that need it.
8
+ const fs = require('fs')
9
+ const os = require('os')
10
+ const path = require('path')
11
+
12
+ const state = {
13
+ root: process.cwd(),
14
+ defaults: {}, // "aipictury.x" → default, from the extension's package.json
15
+ settings: {}, // "aipictury.x" → value, from ~/.aipictury/cli.json and flags
16
+ settingsFile: null,
17
+ logFile: null,
18
+ }
19
+
20
+ class Uri {
21
+ constructor(scheme, p, extra = {}) {
22
+ this.scheme = scheme
23
+ this.path = p
24
+ this.fsPath = scheme === 'file' ? path.normalize(p) : p
25
+ Object.assign(this, extra)
26
+ }
27
+
28
+ static file(p) { return new Uri('file', path.resolve(p)) }
29
+
30
+ static parse(text) {
31
+ try {
32
+ const u = new URL(text)
33
+ if (u.protocol === 'file:') return Uri.file(decodeURIComponent(u.pathname))
34
+ return new Uri(u.protocol.slice(0, -1), u.pathname, { authority: u.host, query: u.search.slice(1), fragment: u.hash.slice(1), href: text })
35
+ } catch { return Uri.file(text) }
36
+ }
37
+
38
+ static joinPath(base, ...parts) { return Uri.file(path.join(base.fsPath, ...parts)) }
39
+
40
+ with(change = {}) { return change.path ? Uri.file(change.path) : this }
41
+
42
+ toString() { return this.scheme === 'file' ? `file://${this.fsPath}` : this.href || `${this.scheme}:${this.path}` }
43
+ }
44
+
45
+ const FileType = { Unknown: 0, File: 1, Directory: 2, SymbolicLink: 64 }
46
+ const asUri = (u) => (typeof u === 'string' ? Uri.file(u) : u)
47
+
48
+ const fileSystem = {
49
+ async readFile(uri) { return new Uint8Array(await fs.promises.readFile(asUri(uri).fsPath)) },
50
+ async writeFile(uri, data) {
51
+ const p = asUri(uri).fsPath
52
+ await fs.promises.mkdir(path.dirname(p), { recursive: true })
53
+ await fs.promises.writeFile(p, data)
54
+ },
55
+ async stat(uri) {
56
+ const s = await fs.promises.stat(asUri(uri).fsPath)
57
+ return { type: s.isDirectory() ? FileType.Directory : FileType.File, size: s.size, mtime: s.mtimeMs, ctime: s.ctimeMs }
58
+ },
59
+ async readDirectory(uri) {
60
+ const list = await fs.promises.readdir(asUri(uri).fsPath, { withFileTypes: true })
61
+ return list.map((d) => [d.name, d.isDirectory() ? FileType.Directory : d.isSymbolicLink() ? FileType.SymbolicLink : FileType.File])
62
+ },
63
+ async createDirectory(uri) { await fs.promises.mkdir(asUri(uri).fsPath, { recursive: true }) },
64
+ async delete(uri, { recursive = false } = {}) { await fs.promises.rm(asUri(uri).fsPath, { recursive, force: false }) },
65
+ async copy(from, to, { overwrite = false } = {}) {
66
+ await fs.promises.cp(asUri(from).fsPath, asUri(to).fsPath, { recursive: true, force: overwrite, errorOnExist: !overwrite })
67
+ },
68
+ async rename(from, to, { overwrite = false } = {}) {
69
+ if (!overwrite && fs.existsSync(asUri(to).fsPath)) throw new Error(`${asUri(to).fsPath} exists`)
70
+ await fs.promises.rename(asUri(from).fsPath, asUri(to).fsPath)
71
+ },
72
+ }
73
+
74
+ class RelativePattern {
75
+ constructor(base, pattern) {
76
+ this.baseUri = typeof base === 'string' ? Uri.file(base) : base.uri || base
77
+ this.base = this.baseUri.fsPath
78
+ this.pattern = pattern
79
+ }
80
+ }
81
+
82
+ /** A glob as a regular expression: ** any depth, * one segment, ? one character, {a,b}. */
83
+ function globRe(glob) {
84
+ let out = ''
85
+ for (let i = 0; i < glob.length; i += 1) {
86
+ const c = glob[i]
87
+ if (c === '*' && glob[i + 1] === '*') { i += 1; if (glob[i + 1] === '/') { i += 1; out += '(?:.*/)?' } else out += '.*' } else if (c === '*') out += '[^/]*'
88
+ else if (c === '?') out += '[^/]'
89
+ else if (c === '{') { const end = glob.indexOf('}', i); out += `(?:${glob.slice(i + 1, end).split(',').map((p) => p.replace(/[.+^$()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*')).join('|')})`; i = end } else out += c.replace(/[.+^$()|[\]\\]/g, '\\$&')
90
+ }
91
+ return new RegExp(`^${out}$`)
92
+ }
93
+
94
+ /** Files under the workspace (or a pattern's base) matching a glob, as VS Code's findFiles. */
95
+ async function findFiles(include, exclude, max = 10_000) {
96
+ const base = include instanceof RelativePattern ? include.base : state.root
97
+ const match = globRe(include instanceof RelativePattern ? include.pattern : include)
98
+ const skip = exclude ? globRe(exclude instanceof RelativePattern ? exclude.pattern : exclude) : null
99
+ const out = []
100
+ const walk = async (dir) => {
101
+ let list
102
+ try { list = await fs.promises.readdir(dir, { withFileTypes: true }) } catch { return }
103
+ for (const d of list) {
104
+ if (out.length >= max) return
105
+ const full = path.join(dir, d.name)
106
+ const rel = path.relative(base, full).split(path.sep).join('/')
107
+ if (d.name === '.git' || (skip && (skip.test(rel) || skip.test(`${rel}/`) || skip.test(`${rel}/x`)))) continue
108
+ if (d.isDirectory()) await walk(full)
109
+ else if (match.test(rel)) out.push(Uri.file(full))
110
+ }
111
+ }
112
+ await walk(base)
113
+ return out
114
+ }
115
+
116
+ function getConfiguration(section) {
117
+ const key = (k) => (section ? `${section}.${k}` : k)
118
+ return {
119
+ get(k, fallback) {
120
+ const full = key(k)
121
+ if (full in state.settings) return state.settings[full]
122
+ if (full in state.defaults) return state.defaults[full]
123
+ // An object setting asked for by its section ("sandbox" → sandbox.enabled…).
124
+ const prefix = `${full}.`
125
+ const nested = Object.keys({ ...state.defaults, ...state.settings }).filter((n) => n.startsWith(prefix))
126
+ if (nested.length) return Object.fromEntries(nested.map((n) => [n.slice(prefix.length), n in state.settings ? state.settings[n] : state.defaults[n]]))
127
+ return fallback
128
+ },
129
+ has(k) { return key(k) in state.settings || key(k) in state.defaults },
130
+ inspect(k) { return { key: key(k), defaultValue: state.defaults[key(k)], globalValue: state.settings[key(k)] } },
131
+ async update(k, value) {
132
+ const full = key(k)
133
+ if (value === undefined) delete state.settings[full]
134
+ else state.settings[full] = value
135
+ if (state.settingsFile) {
136
+ let saved = {}
137
+ try { saved = JSON.parse(fs.readFileSync(state.settingsFile, 'utf8')) } catch { /* none yet */ }
138
+ if (value === undefined) delete saved[full]; else saved[full] = value
139
+ fs.mkdirSync(path.dirname(state.settingsFile), { recursive: true })
140
+ fs.writeFileSync(state.settingsFile, `${JSON.stringify(saved, null, 2)}\n`)
141
+ }
142
+ },
143
+ }
144
+ }
145
+
146
+ const noEvent = () => ({ dispose() {} })
147
+
148
+ class EventEmitter {
149
+ constructor() { this.listeners = new Set(); this.event = (fn) => { this.listeners.add(fn); return { dispose: () => this.listeners.delete(fn) } } }
150
+ fire(value) { for (const fn of this.listeners) fn(value) }
151
+ dispose() { this.listeners.clear() }
152
+ }
153
+
154
+ function outputChannel() {
155
+ const write = (level) => (text) => {
156
+ if (!state.logFile) return
157
+ try {
158
+ fs.mkdirSync(path.dirname(state.logFile), { recursive: true })
159
+ fs.appendFileSync(state.logFile, `${new Date().toISOString()} [${level}] ${text}\n`)
160
+ } catch { /* a log is never worth failing for */ }
161
+ }
162
+ return { info: write('info'), warn: write('warn'), error: write('error'), appendLine: write('info'), append: write('info'), show() {}, clear() {}, dispose() {} }
163
+ }
164
+
165
+ /** Where an installed VS Code keeps its files: its ripgrep is used for search, as in the editor. */
166
+ function appRoot() {
167
+ const candidates = {
168
+ darwin: ['/Applications/Visual Studio Code.app/Contents/Resources/app', path.join(os.homedir(), 'Applications/Visual Studio Code.app/Contents/Resources/app')],
169
+ linux: ['/usr/share/code/resources/app', '/opt/visual-studio-code/resources/app', '/snap/code/current/usr/share/code/resources/app'],
170
+ win32: [path.join(process.env.LOCALAPPDATA || '', 'Programs/Microsoft VS Code/resources/app'), path.join(process.env.PROGRAMFILES || '', 'Microsoft VS Code/resources/app')],
171
+ }[process.platform] || []
172
+ return candidates.find((p) => fs.existsSync(p)) || ''
173
+ }
174
+
175
+ const needsEditor = (what) => async () => { throw new Error(`${what} needs VS Code — it is not available in the terminal`) }
176
+
177
+ const workspace = {
178
+ fs: fileSystem,
179
+ get workspaceFolders() { return [{ uri: Uri.file(state.root), name: path.basename(state.root), index: 0 }] },
180
+ getWorkspaceFolder(uri) {
181
+ const p = asUri(uri).fsPath
182
+ return p === state.root || p.startsWith(state.root + path.sep) ? this.workspaceFolders[0] : undefined
183
+ },
184
+ asRelativePath(p) { return path.relative(state.root, typeof p === 'string' ? p : p.fsPath) },
185
+ findFiles,
186
+ textDocuments: [],
187
+ async openTextDocument(uri) {
188
+ const u = asUri(uri)
189
+ const text = await fs.promises.readFile(u.fsPath, 'utf8')
190
+ return { uri: u, isDirty: false, isClosed: false, languageId: path.extname(u.fsPath).slice(1), lineCount: text.split('\n').length, getText: () => text, save: async () => true }
191
+ },
192
+ getConfiguration,
193
+ onDidChangeConfiguration: noEvent,
194
+ onDidChangeTextDocument: noEvent,
195
+ onDidSaveTextDocument: noEvent,
196
+ onDidChangeNotebookDocument: noEvent,
197
+ openNotebookDocument: needsEditor('running a notebook cell'),
198
+ createFileSystemWatcher: () => ({ onDidChange: noEvent, onDidCreate: noEvent, onDidDelete: noEvent, dispose() {} }),
199
+ }
200
+
201
+ module.exports = {
202
+ version: '1.139.0-cli',
203
+ state,
204
+ Uri,
205
+ FileType,
206
+ RelativePattern,
207
+ EventEmitter,
208
+ Disposable: class { constructor(fn) { this.dispose = fn || (() => {}) } },
209
+ Position: class { constructor(line, character) { this.line = line; this.character = character } },
210
+ Range: class { constructor(a, b, c, d) { this.start = { line: a, character: b }; this.end = { line: c, character: d } } },
211
+ DiagnosticSeverity: { Error: 0, Warning: 1, Information: 2, Hint: 3 },
212
+ ConfigurationTarget: { Global: 1, Workspace: 2, WorkspaceFolder: 3 },
213
+ NotebookCellKind: { Markup: 1, Code: 2 },
214
+ SymbolKind: new Proxy({}, { get: (_, k) => k }),
215
+ workspace,
216
+ languages: { getDiagnostics: () => [], onDidChangeDiagnostics: noEvent },
217
+ window: {
218
+ activeTextEditor: undefined,
219
+ visibleTextEditors: [],
220
+ createOutputChannel: outputChannel,
221
+ showInformationMessage: async () => undefined,
222
+ showWarningMessage: async () => undefined,
223
+ showErrorMessage: async () => undefined,
224
+ showQuickPick: async () => undefined,
225
+ showInputBox: async () => undefined,
226
+ showNotebookDocument: needsEditor('showing a notebook'),
227
+ },
228
+ commands: { executeCommand: needsEditor('that command'), registerCommand: () => ({ dispose() {} }) },
229
+ env: { appRoot: appRoot(), uriScheme: 'vscode', clipboard: { writeText: async () => {}, readText: async () => '' }, openExternal: async () => false },
230
+ extensions: { getExtension: () => undefined },
231
+ }