@davait/aipictury 1.10.0 → 1.10.4

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/ext/accountUi.js CHANGED
@@ -212,10 +212,23 @@ function register(context, { registry, sessionsView, serverUrl }) {
212
212
  async function deleteCloudData() {
213
213
  const yes = 'Delete everything'
214
214
  const ok = await vscode.window.showWarningMessage('Delete everything of yours kept with your AIPictury account?', {
215
- modal: true, detail: 'Conversations, projects, rules, preferences and usage kept in the cloud, from every machine. What is on this machine stays.' }, yes)
215
+ modal: true, detail: 'Conversations and projects, rules, preferences, usage, your pictures and clips (Renders, and every file kept '
216
+ + 'with them), remote-control links and the list of your machines — from every machine and the web app. What is on this '
217
+ + 'machine stays, and is not sent back up by itself: only what you do from now on is kept again (while its switch is on).' }, yes)
216
218
  if (ok !== yes) return null
217
219
  const n = await cloud.deleteEverything()
218
- return `Deleted (${n} records). Your account stays — sign out or keep using it.`
220
+ return `Deleted (${n} records and files). Your account stays — sign out or keep using it.`
221
+ }
222
+
223
+ const KIND_LABEL = { conversations: 'conversations', memory: 'rules', settings: 'preferences', usage: 'usage' }
224
+ /** A switch turned off: also delete what is already kept of that kind? Asked; null to keep it. */
225
+ async function deleteKind(kind) {
226
+ const yes = 'Delete them too'
227
+ const ok = await vscode.window.showWarningMessage(`No longer keeping your ${KIND_LABEL[kind] || kind} with your account. Delete the ones already kept?`, {
228
+ modal: true, detail: 'From every machine and the web app. What is on this machine stays.' }, yes, 'Keep them')
229
+ if (ok !== yes) return null
230
+ const n = await cloud.deleteKind(kind)
231
+ return `No longer sent, and the ${KIND_LABEL[kind] || kind} kept before are deleted (${n} records).`
219
232
  }
220
233
 
221
234
  function localPreferences() {
@@ -235,7 +248,7 @@ function register(context, { registry, sessionsView, serverUrl }) {
235
248
  context.subscriptions.push({ dispose: () => { globalThis.fetch = plainFetch } })
236
249
 
237
250
  account.user().then(changed).catch(() => changed(null))
238
- return { account, cloud, signedIn, signIn, signOut, deleteCloudData, keeps, user: () => user }
251
+ return { account, cloud, signedIn, signIn, signOut, deleteCloudData, deleteKind, keeps, user: () => user }
239
252
  }
240
253
 
241
254
  async function projectRulesText() {
@@ -265,7 +278,7 @@ function accountSpec(ctl, note) {
265
278
  { title: 'Kept with your account', items: kinds.map(([id, label, detail]) => ({ id: `keep:${id}`, label, detail, toggle: { on: config().get(`cloud.${id}`, true) } })) },
266
279
  { title: 'Account', items: [
267
280
  { id: 'signout', label: 'Sign out', detail: 'Forgets the session on this machine: nothing of the account stays here.', actions: [{ id: 'signout', label: 'Sign out', always: true }] },
268
- { id: 'delete', label: 'Delete what is kept in the cloud', detail: 'Everything above, from every machine. Local files stay.', actions: [{ id: 'delete', label: 'Delete…', danger: true, always: true }] },
281
+ { id: 'delete', label: 'Delete what is kept in the cloud', detail: 'Everything above, and your pictures and clips (Renders), from every machine and the web app. Local files stay.', actions: [{ id: 'delete', label: 'Delete…', danger: true, always: true }] },
269
282
  ] },
270
283
  ] }
271
284
  }
@@ -274,8 +287,12 @@ function accountHandler(ctl) {
274
287
  return async (action, message) => {
275
288
  const item = String(message.item || '')
276
289
  if (action === 'toggle' && item.startsWith('keep:')) {
277
- await config().update(`cloud.${item.slice(5)}`, !!message.value, vscode.ConfigurationTarget.Global)
278
- return accountSpec(ctl, message.value ? 'Kept with your account from now on.' : 'No longer sent. What is in the cloud stays until you delete it.')
290
+ const kind = item.slice(5)
291
+ await config().update(`cloud.${kind}`, !!message.value, vscode.ConfigurationTarget.Global)
292
+ if (message.value) return accountSpec(ctl, 'Kept with your account from now on.')
293
+ // Off: what is already kept of it can go too — asked, since it goes from every machine.
294
+ const said = await ctl.deleteKind?.(kind).catch((error) => `No longer sent; deleting what was kept failed: ${error.message}`)
295
+ return accountSpec(ctl, said || 'No longer sent. What is in the cloud stays until you delete it.')
279
296
  }
280
297
  if (action === 'signin') { await ctl.signIn(); return accountSpec(ctl) }
281
298
  if (action === 'signout') { await ctl.signOut(); return accountSpec(ctl, 'Signed out.') }
package/ext/agent.js CHANGED
@@ -73,9 +73,9 @@ const MODES = {
73
73
  plan: { label: 'Plan', icon: '\u25F4',
74
74
  hint: 'Read-only: investigates and proposes a plan, changes nothing.' },
75
75
  never: { label: 'Auto', icon: '\u26A1',
76
- hint: 'Goes ahead with routine work; asks before anything risky — judged by fixed rules and the model.' },
76
+ hint: 'Asks nothing: does the work, takes the recommended choices, and lists at the end what would have been asked and why. Your own ask rules still ask.' },
77
77
  bypass: { label: 'Bypass permissions', icon: '\u26A0',
78
- hint: 'Does everything without asking, with no checks at all. Only with aipictury.allowDangerouslySkipPermissions.' },
78
+ hint: 'Does everything without asking, with no checks at all — not even your ask rules. Only with aipictury.allowDangerouslySkipPermissions.' },
79
79
  }
80
80
 
81
81
  /** The mode in force. Bypass needs its own setting: without it, it is Auto. */
@@ -97,11 +97,11 @@ approves it (and you may then make the changes) or asks for changes to it.`
97
97
 
98
98
  const AUTO_NOTE = `
99
99
 
100
- You are in AUTO mode: the user is not approving each step. Still call ask_user whenever
101
- you choose between real options — it keeps a record of the choice. Unless you mark the
102
- call important, your recommended (first) option is taken at once, you carry on without
103
- waiting, and the user can change it when you finish. Mark important only a decision that
104
- would be costly to undo.`
100
+ You are in AUTO mode: the user is not approving each step, and nothing stops to ask them.
101
+ Still call ask_user whenever you choose between real options — it keeps a record of the
102
+ choice: your recommended (first) option is taken at once, you carry on without waiting, and
103
+ the user can change it when you finish. Commands and tools run without asking too, so take
104
+ the careful way yourself when an action would be costly to undo.`
105
105
 
106
106
  const SYSTEM = `You are AIPictury's coding agent, working inside the user's editor on
107
107
  their own machine. You can read and change the workspace with the tools provided.
@@ -319,11 +319,14 @@ async function approve(call, session, signal, hooks = {}, preview = null, { forc
319
319
  if (mode === 'bypass') return { ok: true }
320
320
  if (mode === 'never') {
321
321
  // Auto: edits inside the workspace go ahead (a checkpoint can undo them); anything else
322
- // is judged — fixed rules, routine work, then the model — and asked when it is risky.
322
+ // is judged — fixed rules, routine work, then the model — and what would have been asked
323
+ // goes ahead all the same, listed at the end of the answer with why.
323
324
  if (WRITES.has(name)) return { ok: true }
324
325
  const verdict = await judge(call, session, signal, hooks, server)
325
326
  if (verdict.verdict === 'allow') return { ok: true, judged: verdict }
326
- because = `Auto mode asks: ${verdict.reason || 'it could not tell whether this is safe'}`
327
+ // Auto never stops to ask: judged risky, it goes ahead too — and is said at the end.
328
+ hooks.onAutoRan?.({ tool: name, args, reason: verdict.reason || 'it could not tell whether this is safe' })
329
+ return { ok: true, judged: verdict, auto: true }
327
330
  }
328
331
  if (mode === 'edits' && !isRun) return { ok: true }
329
332
  if (session.allowAll) return { ok: true }
@@ -538,7 +541,9 @@ async function askUser(args, report, signal, hooks) {
538
541
  }
539
542
  const mode = modeNow()
540
543
  const recommended = (q) => q.options[0].label.replace(/\s*\(recommended\)\s*$/i, '')
541
- if ((unattended(mode) && !args.important) || !hooks.askUser) {
544
+ // Auto takes the recommended option of every question, important ones too (listed at the end,
545
+ // each open to change); the user is never stopped mid-run.
546
+ if (unattended(mode) || !hooks.askUser) {
542
547
  const picks = questions.map((q) => ({ ...q, picked: [q.options[0].label], other: '' }))
543
548
  hooks.onDecision?.(picks)
544
549
  report({ tool: 'ask_user', args: { questions }, status: 'decided', result: picks })
@@ -935,6 +940,13 @@ async function executeTool(call, session, report, signal, server, hooks = {}) {
935
940
  sandbox: box.asked ? 'the agent asks to run this outside the sandbox' : 'the sandbox is on, but this machine has none (sandbox-exec / bubblewrap)' })
936
941
  }
937
942
  if (hooks.hookPermission === 'ask') Object.assign(rule, { decision: 'ask', rule: null, hook: hooks.hookReason || 'a hook' })
943
+ // Auto never stops to ask. Only what the user set up to be asked — an ask rule of theirs, a
944
+ // hook — still asks; the rest (leaving the sandbox, a password field, a new site) goes ahead,
945
+ // and the answer ends with what was done without asking, and why it would have been asked.
946
+ if (modeNow() === 'never' && rule.decision === 'ask' && !rule.rule && !rule.hook) {
947
+ hooks.onAutoRan?.({ tool: name, args, reason: rule.why || rule.sandbox || 'it would have asked' })
948
+ rule.decision = 'allow'
949
+ }
938
950
  const standing = session.allowTools?.includes(name)
939
951
  const needsYes = rule.decision === 'ask'
940
952
  || (rule.decision !== 'allow' && !standing && (WRITES.has(name) || (isRun && (!readOnlyRun || readsOutside)) || (isMcp && !mcp.readOnly(name)) || browserAsks))
@@ -1715,6 +1727,9 @@ async function work({ server, model, history, session, signal,
1715
1727
  let known = null // the dependencies the workspace declares, read when first needed
1716
1728
  const holds = new Map() // package → times a call bringing it in was held
1717
1729
  const hooks = { askUser: ask, onDecision, approve: approveHook, approvePlan, onTodos, onTasks, onFileChange, readSpecial, model, trustHooks, onMonitor, schedule, onHelperDone,
1730
+ // Auto mode did what would have been asked: listed with the decisions, at the end.
1731
+ onAutoRan: ({ tool, args = {}, reason }) => onDecision?.([{ ran: true, tool, header: tool === 'run_command' ? 'Ran' : tool,
1732
+ question: String(args.command || args.path || args.url || args.page || args.action || JSON.stringify(args)).slice(0, 300), reason: String(reason || '') }]),
1718
1733
  // For helpers: what the user said, so a package is judged chosen by them, not by a task text.
1719
1734
  userSaid: () => deps.saidByUser(messages, fromUser) }
1720
1735
  let steps = 0
package/ext/cloud.js CHANGED
@@ -122,7 +122,7 @@ class Cloud {
122
122
  const now = Date.now()
123
123
  const existing = await this.db.get(base)
124
124
  await this.db.set(base, { email: user.email || '', displayName: user.displayName || '', photoURL: user.photoURL || '',
125
- created: existing?.created || now, lastSeen: now })
125
+ created: existing?.created || now, lastSeen: now, ...(existing?.cleared ? { cleared: existing.cleared } : {}) })
126
126
  await this.db.set(`${base}/devices/${this.device.id}`, { name: this.device.name, app: this.device.app,
127
127
  platform: this.device.platform, version: this.device.version, lastSeen: now })
128
128
  }
@@ -229,11 +229,21 @@ class Cloud {
229
229
 
230
230
  /** The conversations of `folder` kept on this machine (`store`: sessions.js) that the account
231
231
  * lacks or has an older copy of, carried up; how many. `still()`: false stops (signed out). */
232
+ /** When everything kept with the account was last deleted (ms; 0: never), read afresh. What is
233
+ * older than that is not carried up again by itself: deleting the cloud copy, the conversations
234
+ * still on a machine came straight back at its next start. What is done after it syncs again. */
235
+ async clearedAt() {
236
+ const user = await this.db.get(await this.base()).catch(() => null)
237
+ return Number(user?.cleared) || 0
238
+ }
239
+
232
240
  async bringUp(folder, store, { still = () => true } = {}) {
233
241
  const remote = new Map((await this.listConversations(folder)).map((c) => [c.id, c]))
242
+ const cleared = await this.clearedAt()
234
243
  let up = 0
235
244
  for (const meta of store.list().slice(0, 500)) {
236
245
  if (!still()) break
246
+ if ((meta.updated || 0) <= cleared) continue // from before the cloud copy was deleted
237
247
  const there = remote.get(meta.id)
238
248
  if (there && (there.updated || 0) >= (meta.updated || 0)) continue
239
249
  const entry = store.get(meta.id)
@@ -362,12 +372,44 @@ class Cloud {
362
372
 
363
373
  async dropRemote(id) { await this.db.delete(`${await this.base()}/remote/${id}`) }
364
374
 
375
+ /** One kind of what is kept, deleted from every machine's cloud copy (its switch turned off):
376
+ * 'conversations' (with their chunks), 'memory' (your rules and each project's), 'settings'
377
+ * (preferences) or 'usage'. Returns how many records went. */
378
+ async deleteKind(kind) {
379
+ const base = await this.base()
380
+ const writes = []
381
+ if (kind === 'conversations' || kind === 'memory') {
382
+ for (const p of await this.db.list(`${base}/projects`)) {
383
+ if (kind === 'memory') { writes.push({ delete: `${base}/projects/${p.id}/memory/current` }); continue }
384
+ for (const c of await this.db.list(`${base}/projects/${p.id}/conversations`)) {
385
+ const chunks = new Set([...Array(c.data.chunks || 0).keys()].map(String))
386
+ for (const k of await this.db.list(`${base}/projects/${p.id}/conversations/${c.id}/chunks`).catch(() => [])) chunks.add(k.id)
387
+ for (const k of chunks) writes.push({ delete: `${base}/projects/${p.id}/conversations/${c.id}/chunks/${k}` })
388
+ writes.push({ delete: `${base}/projects/${p.id}/conversations/${c.id}` })
389
+ }
390
+ }
391
+ if (kind === 'memory') writes.push({ delete: `${base}/memory/rules` })
392
+ } else if (kind === 'settings') {
393
+ writes.push({ delete: `${base}/settings/preferences` })
394
+ } else if (kind === 'usage') {
395
+ for (const d of await this.db.list(`${base}/usage`)) writes.push({ delete: `${base}/usage/${d.id}` })
396
+ } else {
397
+ throw new Error(`nothing kept as ${kind}`)
398
+ }
399
+ await this.db.batch(writes)
400
+ if (kind === 'conversations') this.pushed.clear()
401
+ return writes.length
402
+ }
403
+
365
404
  async deleteEverything() {
366
405
  const base = await this.base()
367
406
  const writes = []
368
407
  for (const p of await this.db.list(`${base}/projects`)) {
369
408
  for (const c of await this.db.list(`${base}/projects/${p.id}/conversations`)) {
370
- for (let i = 0; i < (c.data.chunks || 0); i += 1) writes.push({ delete: `${base}/projects/${p.id}/conversations/${c.id}/chunks/${i}` })
409
+ // Every chunk there is, not as many as the summary says: one left by a save cut short stays otherwise.
410
+ const chunks = new Set([...Array(c.data.chunks || 0).keys()].map(String))
411
+ for (const k of await this.db.list(`${base}/projects/${p.id}/conversations/${c.id}/chunks`).catch(() => [])) chunks.add(k.id)
412
+ for (const k of chunks) writes.push({ delete: `${base}/projects/${p.id}/conversations/${c.id}/chunks/${k}` })
371
413
  writes.push({ delete: `${base}/projects/${p.id}/conversations/${c.id}` })
372
414
  }
373
415
  writes.push({ delete: `${base}/projects/${p.id}/memory/current` }, { delete: `${base}/projects/${p.id}` })
@@ -377,6 +419,8 @@ class Cloud {
377
419
  await this.db.batch(writes)
378
420
  // The media kept with the account (pictures, clips, attachments) go too.
379
421
  if (this.storage) for (const o of await this.storage.list(`${base}/media/`)) { await this.storage.delete(o.name); writes.push(o) }
422
+ // The moment it was cleared: no app carries back up what it still holds from before.
423
+ await this.db.set(base, { cleared: Date.now() })
380
424
  this.pushed.clear()
381
425
  return writes.length
382
426
  }
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.10.0",
5
+ "version": "1.10.4",
6
6
  "publisher": "aipictury",
7
7
  "license": "SEE LICENSE IN LICENSE.md",
8
8
  "engines": {
@@ -848,7 +848,7 @@
848
848
  "Manual — ask before every file change and command",
849
849
  "Edit Automatically — apply file edits without asking; still ask before running commands",
850
850
  "Plan — read-only: investigate and propose a plan, change nothing",
851
- "Auto — routine work without asking; anything risky is asked (judged by fixed rules and the model)",
851
+ "Auto — asks nothing: does the work, takes the recommended choices, lists at the end what would have been asked and why (your own ask rules still ask)",
852
852
  "Bypass permissions — everything without asking, no checks (needs allowDangerouslySkipPermissions)"
853
853
  ],
854
854
  "default": "never",
package/ext/storage.js CHANGED
@@ -26,11 +26,12 @@ class Storage {
26
26
  }
27
27
 
28
28
  /** Stores bytes at `path` ("users/u/media/a.png"). Resolves to the object's metadata. A multipart
29
- * upload, as the SDK's: the type goes in the object's metadata, where the rules read it. */
29
+ * upload, as the SDK's: the type goes in the object's metadata, where the rules read it; and
30
+ * no-store, so no browser that shows it keeps a copy past signing out. */
30
31
  async upload(path, bytes, contentType) {
31
32
  const boundary = `aipictury-${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`
32
33
  const body = Buffer.concat([
33
- Buffer.from(`--${boundary}\r\nContent-Type: application/json; charset=utf-8\r\n\r\n${JSON.stringify({ name: path, contentType })}\r\n`
34
+ Buffer.from(`--${boundary}\r\nContent-Type: application/json; charset=utf-8\r\n\r\n${JSON.stringify({ name: path, contentType, cacheControl: 'private, no-store' })}\r\n`
34
35
  + `--${boundary}\r\nContent-Type: ${contentType}\r\n\r\n`),
35
36
  Buffer.from(bytes),
36
37
  Buffer.from(`\r\n--${boundary}--`),
package/main.js CHANGED
@@ -387,7 +387,7 @@ async function interactive(o, convo, first, cfg) {
387
387
  const view = new T.MarkdownStream()
388
388
  view.push(`${plan}\n`)
389
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 › `)
390
+ const answer = await ask(`\n ${bold('y')}es, go ahead (asking as now) · ${bold('a')}uto (go ahead, asking nothing) · or say what to change › `)
391
391
  spinner.start()
392
392
  if (answer === null) return { approved: false, feedback: '' }
393
393
  const a = answer.trim()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davait/aipictury",
3
- "version": "1.10.0",
3
+ "version": "1.10.4",
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"