@davait/aipictury 1.18.1 → 1.19.1
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.
- package/README.md +12 -0
- package/ext/extension.js +2 -0
- package/ext/log.js +71 -3
- package/ext/package.json +1 -1
- package/ext/remote.js +11 -5
- package/main.js +22 -1
- package/package.json +1 -1
- package/vscode-shim.js +33 -8
package/README.md
CHANGED
|
@@ -34,6 +34,18 @@ aipictury -p "…" --allowedTools "Bash(npm test:*),Edit" --disallowedTools "Web
|
|
|
34
34
|
aipictury -p "…" --mcp-config servers.json --add-dir ../shared --append-system-prompt "Be brief."
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
## Logs
|
|
38
|
+
|
|
39
|
+
`~/.aipictury/logs/cli.log` keeps what went wrong. `--debug` (or `AIPICTURY_DEBUG=1`) writes every
|
|
40
|
+
level — each request to the server with its status and time, each step of the run — as JSON lines
|
|
41
|
+
to `~/.cache/aipictury/cli.jsonl` (rotated at 5 MB, one older file kept), to read with `jq`:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
jq -c 'select(.level=="warn" or .level=="error")' ~/.cache/aipictury/cli.jsonl
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
No token and no conversation text is ever written.
|
|
48
|
+
|
|
37
49
|
## License
|
|
38
50
|
|
|
39
51
|
Copyright © 2026 Davait, Inc. All rights reserved. Proprietary software: see LICENSE.md.
|
package/ext/extension.js
CHANGED
|
@@ -2298,6 +2298,8 @@ function activate(context) {
|
|
|
2298
2298
|
// This window names itself on every call to its GPU server (gpuRun.js).
|
|
2299
2299
|
gpuRun.setApp({ client: 'vscode', server: serverUrl })
|
|
2300
2300
|
gpuRun.nameCalls()
|
|
2301
|
+
// …and every call to it is in the log (Output → AIPictury, at Debug: method, path, status, ms).
|
|
2302
|
+
log.watchFetch({ server: serverUrl })
|
|
2301
2303
|
try { moveConversationsToDisk(context) } catch { /* tried again next start */ }
|
|
2302
2304
|
syncCommandEnv().catch(() => { /* again before each run */ })
|
|
2303
2305
|
undoOldThinkingSwitch(context).catch(() => { /* the setting stays as it was */ })
|
package/ext/log.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
// The extension's log: View → Output → "AIPictury", or the "AIPictury: Show Logs" command.
|
|
3
3
|
// What is worth reading after something went wrong — a turn, a failure, a stall, a setting
|
|
4
|
-
// that could not be written — never the conversation itself.
|
|
4
|
+
// that could not be written — never the conversation itself, never a token.
|
|
5
|
+
//
|
|
6
|
+
// Levels as VS Code's log channel has them: error, warn, info are always shown; debug and trace
|
|
7
|
+
// (every request to the server with its status and time, the websocket's events, a run's steps)
|
|
8
|
+
// when the channel's level is set to Debug or Trace (the gear in the Output view, or
|
|
9
|
+
// `code --log aipictury.aipictury:debug`). In the terminal (the CLI) the same module writes to a
|
|
10
|
+
// file — cli/vscode-shim.js stands in for `vscode` — and `--debug` turns the two lower levels on.
|
|
11
|
+
//
|
|
12
|
+
// log.event('debug', 'http.request', { method: 'POST', path: '/api/agent/turn', status: 200, ms: 812 })
|
|
5
13
|
const vscode = require('vscode')
|
|
6
14
|
|
|
7
15
|
let channel = null
|
|
@@ -17,8 +25,10 @@ function out() {
|
|
|
17
25
|
const recent = []
|
|
18
26
|
const KEEP = 400
|
|
19
27
|
|
|
28
|
+
const say = (p) => (p instanceof Error ? p.stack || p.message : typeof p === 'string' ? p : JSON.stringify(p))
|
|
29
|
+
|
|
20
30
|
const write = (level) => (...parts) => {
|
|
21
|
-
const text = parts.map(
|
|
31
|
+
const text = parts.map(say).join(' ')
|
|
22
32
|
recent.push(`${new Date().toISOString()} [${level}] ${text}`)
|
|
23
33
|
if (recent.length > KEEP) recent.splice(0, recent.length - KEEP)
|
|
24
34
|
const c = out()
|
|
@@ -26,4 +36,62 @@ const write = (level) => (...parts) => {
|
|
|
26
36
|
else if (c) c.appendLine(`[${level}] ${text}`)
|
|
27
37
|
}
|
|
28
38
|
|
|
29
|
-
|
|
39
|
+
const levels = { trace: write('trace'), debug: write('debug'), info: write('info'), warn: write('warn'), error: write('error') }
|
|
40
|
+
|
|
41
|
+
/** A short form of a value for a record's field: one line, at most 160 characters. */
|
|
42
|
+
function short(value) {
|
|
43
|
+
if (value === undefined || value === null) return 'null'
|
|
44
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value)
|
|
45
|
+
const line = String(text).replace(/\s+/g, ' ')
|
|
46
|
+
return line.length > 160 ? `${line.slice(0, 157)}…` : line
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* One structured record: `name` is what happened ("http.request", "ws.open", "run.ended"), the
|
|
51
|
+
* fields what it happened to and how. Written as `name key=value …`; in the terminal as JSON.
|
|
52
|
+
*/
|
|
53
|
+
function event(level, name, fields = {}) {
|
|
54
|
+
const c = out()
|
|
55
|
+
if (c && typeof c.record === 'function') { c.record(level, name, fields); return }
|
|
56
|
+
const shown = Object.entries(fields).filter(([, v]) => v !== undefined).map(([k, v]) => `${k}=${short(v)}`).join(' ')
|
|
57
|
+
;(levels[level] || levels.info)(`${name}${shown ? ` ${shown}` : ''}`)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A URL's path, never its query: a websocket's token rides there. */
|
|
61
|
+
function pathOf(url) {
|
|
62
|
+
try { return new URL(url).pathname } catch { return String(url).split('?')[0] }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Every call to the GPU server in the log: method, path, status and milliseconds at debug; a
|
|
67
|
+
* failure to connect, or the server's error, at warn. `server()`: its address. Installed once.
|
|
68
|
+
*/
|
|
69
|
+
function watchFetch({ server }) {
|
|
70
|
+
if (globalThis.fetch.watched) return
|
|
71
|
+
const base = globalThis.fetch
|
|
72
|
+
const watched = async (input, init = {}) => {
|
|
73
|
+
const url = typeof input === 'string' ? input : input?.url || String(input)
|
|
74
|
+
const home = String(server() || '').replace(/\/+$/, '')
|
|
75
|
+
if (!home || !url.startsWith(home)) return base(input, init)
|
|
76
|
+
const method = init.method || (typeof input === 'object' && input?.method) || 'GET'
|
|
77
|
+
const started = Date.now()
|
|
78
|
+
try {
|
|
79
|
+
const response = await base(input, init)
|
|
80
|
+
const ms = Date.now() - started
|
|
81
|
+
const level = response.status >= 500 ? 'warn' : 'debug'
|
|
82
|
+
event(level, 'http.request', { method, path: pathOf(url), status: response.status, ms })
|
|
83
|
+
return response
|
|
84
|
+
} catch (error) {
|
|
85
|
+
// Stopped on purpose (a run stopped: AbortError) is not a failure; a timeout is.
|
|
86
|
+
const aborted = error?.name === 'AbortError'
|
|
87
|
+
event(aborted ? 'debug' : 'warn', 'http.failed', { method, path: pathOf(url), ms: Date.now() - started,
|
|
88
|
+
error: aborted ? 'aborted' : error?.name === 'TimeoutError' ? 'timed out' : error?.message || String(error) })
|
|
89
|
+
throw error
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
watched.watched = true
|
|
93
|
+
watched.namesRuns = base.namesRuns
|
|
94
|
+
globalThis.fetch = watched
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = { tail: () => [...recent], ...levels, event, watchFetch, pathOf, show: () => out()?.show(true), channel: out }
|
package/ext/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "aipictury",
|
|
3
3
|
"displayName": "AIPictury",
|
|
4
4
|
"description": "A coding agent and a generation studio running on your own GPU, with uncensored local models.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.19.1",
|
|
6
6
|
"publisher": "aipictury",
|
|
7
7
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
8
8
|
"engines": {
|
package/ext/remote.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// or run a slash command: those stay in VS Code.
|
|
11
11
|
// No VS Code here: the panel is handed in.
|
|
12
12
|
const crypto = require('crypto')
|
|
13
|
+
const logs = require('./log')
|
|
13
14
|
|
|
14
15
|
const ITEMS = 60 // transcript entries sent (the latest)
|
|
15
16
|
const TEXT_CAP = 8000 // characters of one entry
|
|
@@ -103,22 +104,27 @@ class RemoteLink {
|
|
|
103
104
|
if (!this.active) return
|
|
104
105
|
const ws = new WebSocket(`${this.server.replace(/^http/, 'ws')}/ws/remote/host${token ? `?token=${encodeURIComponent(token)}` : ''}`)
|
|
105
106
|
this.ws = ws
|
|
106
|
-
|
|
107
|
+
const opened = Date.now()
|
|
108
|
+
logs.event('debug', 'ws.connecting', { path: '/ws/remote/host', attempt: this.tries + 1, signedIn: Boolean(token) })
|
|
109
|
+
ws.onopen = () => { logs.event('debug', 'ws.open', { path: '/ws/remote/host', ms: Date.now() - opened }); ws.send(JSON.stringify({ type: 'hello', id: this.id, key: this.key })) }
|
|
107
110
|
ws.onmessage = (event) => {
|
|
108
111
|
let msg
|
|
109
|
-
try { msg = JSON.parse(String(event.data)) } catch { return }
|
|
112
|
+
try { msg = JSON.parse(String(event.data)) } catch { logs.event('debug', 'ws.message', { path: '/ws/remote/host', unreadable: true }); return }
|
|
113
|
+
logs.event('trace', 'ws.message', { path: '/ws/remote/host', type: msg.type, viewers: msg.viewers, command: msg.command?.type })
|
|
110
114
|
if (msg.type === 'ready') { this.tries = 0; this.connected = true; this.setViewers(msg.viewers); this.send() }
|
|
111
115
|
if (msg.type === 'viewers') this.setViewers(msg.viewers)
|
|
112
|
-
if (msg.type === 'command') this.command(msg.command || {}).catch((error) => this.log(`remote command failed: ${error.message}`))
|
|
116
|
+
if (msg.type === 'command') this.command(msg.command || {}).catch((error) => { logs.event('warn', 'remote.command_failed', { type: msg.command?.type, error: error.message }); this.log(`remote command failed: ${error.message}`) })
|
|
113
117
|
}
|
|
114
118
|
ws.onclose = (event) => {
|
|
115
119
|
this.connected = false
|
|
116
|
-
|
|
120
|
+
const refused = event.code === 4403 || event.code === 4409
|
|
121
|
+
logs.event(refused ? 'warn' : 'debug', 'ws.close', { path: '/ws/remote/host', code: event.code, reason: event.reason || undefined, seconds: Math.round((Date.now() - opened) / 1000), retrying: this.active && !refused })
|
|
122
|
+
if (!this.active || refused) return
|
|
117
123
|
const wait = RETRY[Math.min(this.tries, RETRY.length - 1)]
|
|
118
124
|
this.tries += 1
|
|
119
125
|
this.retry = setTimeout(() => this.connect(), wait)
|
|
120
126
|
}
|
|
121
|
-
ws.onerror = () => {}
|
|
127
|
+
ws.onerror = (event) => { logs.event('debug', 'ws.error', { path: '/ws/remote/host', error: event?.message || event?.error?.message || 'socket error' }) } // onclose follows, and retries
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
setViewers(n) { this.viewers = n || 0; this.onViewers(this.viewers) }
|
package/main.js
CHANGED
|
@@ -48,6 +48,7 @@ Options:
|
|
|
48
48
|
--effort low|medium|high|max|ultra how hard it reasons
|
|
49
49
|
--max-turns <n> stop after this many model turns (print mode)
|
|
50
50
|
--server <url> the AIPictury server (default: aipictury.server, or AIPICTURY_URL)
|
|
51
|
+
--debug a detailed log of this run (or AIPICTURY_DEBUG=1), see Logs below
|
|
51
52
|
-v, --version the version
|
|
52
53
|
-h, --help this help
|
|
53
54
|
|
|
@@ -55,6 +56,9 @@ In a conversation: / for commands (/help lists them; Tab completes), @path menti
|
|
|
55
56
|
!command runs it here, a line ending in \\ continues on the next. Esc or Ctrl+C stops a run;
|
|
56
57
|
Ctrl+C twice at the prompt quits. Up and down bring back what you typed, across runs.
|
|
57
58
|
Settings: ~/.aipictury/cli.json (the extension's names, e.g. {"aipictury.approval": "ask"}).
|
|
59
|
+
Logs: ~/.aipictury/logs/cli.log (what went wrong); with --debug, every request to the server and
|
|
60
|
+
every step of the run as JSON lines in ~/.cache/aipictury/cli.jsonl (rotated at 5 MB, one kept):
|
|
61
|
+
jq -c 'select(.level=="warn" or .level=="error")' ~/.cache/aipictury/cli.jsonl
|
|
58
62
|
Copyright © 2026 Davait, Inc. All rights reserved. See LICENSE.md.
|
|
59
63
|
`
|
|
60
64
|
|
|
@@ -80,6 +84,7 @@ function parseArgs(argv) {
|
|
|
80
84
|
else if (a === '--effort') o.effort = next()
|
|
81
85
|
else if (a === '--max-turns') o.maxTurns = Number(next())
|
|
82
86
|
else if (a === '--server') o.server = next()
|
|
87
|
+
else if (a === '--debug') o.debug = true
|
|
83
88
|
else if (a === '-v' || a === '--version') o.version = true
|
|
84
89
|
else if (a === '-h' || a === '--help') o.help = true
|
|
85
90
|
else if (a.startsWith('-')) throw new Error(`unknown option ${a} — aipictury --help`)
|
|
@@ -101,6 +106,10 @@ function setup(o) {
|
|
|
101
106
|
vscode.state.root = process.cwd()
|
|
102
107
|
vscode.state.settingsFile = path.join(home, 'cli.json')
|
|
103
108
|
vscode.state.logFile = path.join(home, 'logs', 'cli.log')
|
|
109
|
+
if (o.debug || /^(1|true|yes|on)$/i.test(process.env.AIPICTURY_DEBUG || '')) {
|
|
110
|
+
const cache = process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache')
|
|
111
|
+
vscode.state.debugFile = process.env.AIPICTURY_DEBUG_LOG || path.join(cache, 'aipictury', 'cli.jsonl')
|
|
112
|
+
}
|
|
104
113
|
for (const [k, v] of Object.entries(pkg.contributes.configuration.properties)) if ('default' in v) vscode.state.defaults[k] = v.default
|
|
105
114
|
// The terminal starts as Claude Code's does: asking before changes and commands (in print
|
|
106
115
|
// mode, refusing them) — a script gets only what it allows. cli.json or --permission-mode
|
|
@@ -716,7 +725,19 @@ async function main() {
|
|
|
716
725
|
// Every call to the GPU server carries the account's token (the server may require it), and only those.
|
|
717
726
|
const { serverFetch } = require(path.join(EXT, 'account.js'))
|
|
718
727
|
globalThis.fetch = serverFetch({ fetch: globalThis.fetch, account, server })
|
|
719
|
-
|
|
728
|
+
// …and each of those calls is in the log (--debug: method, path, status, ms).
|
|
729
|
+
const log = require(path.join(EXT, 'log.js'))
|
|
730
|
+
log.watchFetch({ server })
|
|
731
|
+
log.event('info', 'cli.start', { version: VERSION, mode: o.print ? 'print' : 'interactive', format: o.print ? o.format : undefined, server: server(), model: o.model, effort: o.effort, permission: o.mode, cwd: process.cwd(), debug: vscode.state.debugFile || undefined })
|
|
732
|
+
const started = Date.now()
|
|
733
|
+
try {
|
|
734
|
+
const code = await mainRun(o)
|
|
735
|
+
log.event('info', 'cli.end', { code, seconds: Math.round((Date.now() - started) / 1000) })
|
|
736
|
+
return code
|
|
737
|
+
} catch (error) {
|
|
738
|
+
log.event('error', 'cli.failed', { error: error.stack || error.message, seconds: Math.round((Date.now() - started) / 1000) })
|
|
739
|
+
throw error
|
|
740
|
+
} finally { await sync.flush() }
|
|
720
741
|
}
|
|
721
742
|
|
|
722
743
|
async function mainRun(o) {
|
package/package.json
CHANGED
package/vscode-shim.js
CHANGED
|
@@ -14,8 +14,10 @@ const state = {
|
|
|
14
14
|
defaults: {}, // "aipictury.x" → default, from the extension's package.json
|
|
15
15
|
settings: {}, // "aipictury.x" → value, from ~/.aipictury/cli.json and flags
|
|
16
16
|
settingsFile: null,
|
|
17
|
-
logFile: null,
|
|
17
|
+
logFile: null, // ~/.aipictury/logs/cli.log: info and up, readable lines
|
|
18
|
+
debugFile: null, // --debug / AIPICTURY_DEBUG: every level, JSON lines (main.js)
|
|
18
19
|
}
|
|
20
|
+
const DEBUG_MB = 5 // the debug log is rotated at this size, one older file kept
|
|
19
21
|
|
|
20
22
|
class Uri {
|
|
21
23
|
constructor(scheme, p, extra = {}) {
|
|
@@ -151,15 +153,38 @@ class EventEmitter {
|
|
|
151
153
|
dispose() { this.listeners.clear() }
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
/** Appends a line to `file`, making its folder; past `limitMb` the file is rotated (one kept). */
|
|
157
|
+
function appendLine(file, line, limitMb = 0) {
|
|
158
|
+
try {
|
|
159
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
160
|
+
if (limitMb) {
|
|
161
|
+
let size = 0
|
|
162
|
+
try { size = fs.statSync(file).size } catch { /* none yet */ }
|
|
163
|
+
if (size > limitMb * 1024 * 1024) fs.renameSync(file, `${file}.1`)
|
|
164
|
+
}
|
|
165
|
+
fs.appendFileSync(file, `${line}\n`)
|
|
166
|
+
} catch { /* a log is never worth failing for */ }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const SHOWN = { error: 0, warn: 1, info: 2, debug: 3, trace: 4 }
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The extension's log (log.js) in the terminal: info and up as readable lines in cli.log; with
|
|
173
|
+
* --debug, every level — each request to the server, the run's steps — as JSON lines in the
|
|
174
|
+
* debug file (one object a line: ts, level, event, msg and the event's fields).
|
|
175
|
+
*/
|
|
154
176
|
function outputChannel() {
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
177
|
+
const record = (level, event, fields = {}, text = null) => {
|
|
178
|
+
if (SHOWN[level] <= SHOWN.info && state.logFile) {
|
|
179
|
+
const shown = text ?? `${event} ${Object.entries(fields).map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(' ')}`.trim()
|
|
180
|
+
appendLine(state.logFile, `${new Date().toISOString()} [${level}] ${shown}`)
|
|
181
|
+
}
|
|
182
|
+
if (state.debugFile) {
|
|
183
|
+
appendLine(state.debugFile, JSON.stringify({ ts: new Date().toISOString(), level, event, ...(text !== null ? { msg: text } : {}), ...fields }), DEBUG_MB)
|
|
184
|
+
}
|
|
161
185
|
}
|
|
162
|
-
|
|
186
|
+
const write = (level) => (text) => record(level, 'log', {}, text)
|
|
187
|
+
return { record, trace: write('trace'), debug: write('debug'), info: write('info'), warn: write('warn'), error: write('error'), appendLine: write('info'), append: write('info'), show() {}, clear() {}, dispose() {} }
|
|
163
188
|
}
|
|
164
189
|
|
|
165
190
|
/** Where an installed VS Code keeps its files: its ripgrep is used for search, as in the editor. */
|