@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 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((p) => (p instanceof Error ? p.stack || p.message : typeof p === 'string' ? p : JSON.stringify(p))).join(' ')
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
- module.exports = { tail: () => [...recent], info: write('info'), warn: write('warn'), error: write('error'), show: () => out()?.show(true), channel: out }
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.18.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
- ws.onopen = () => { ws.send(JSON.stringify({ type: 'hello', id: this.id, key: this.key })) }
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
- if (!this.active || event.code === 4403 || event.code === 4409) return
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 = () => {} // onclose follows, and retries
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
- try { return await mainRun(o) } finally { await sync.flush() }
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davait/aipictury",
3
- "version": "1.18.1",
3
+ "version": "1.19.1",
4
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
5
  "bin": {
6
6
  "aipictury": "main.js"
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 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 */ }
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
- return { info: write('info'), warn: write('warn'), error: write('error'), appendLine: write('info'), append: write('info'), show() {}, clear() {}, dispose() {} }
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. */