@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
@@ -0,0 +1,287 @@
1
+ 'use strict'
2
+ // The AIPictury account in VS Code (docs/accounts-plan.md). Using the extension needs it: signed
3
+ // out, the panel shows a sign-in screen and nothing reaches the agent. (The server checks the
4
+ // account too: this is the experience, the server is the lock.) Signed in, what is yours is kept
5
+ // in step with the account: conversations (every save, rename, archive and delete, through the
6
+ // session store), your rules, your preferences, the project's rules, each day's usage.
7
+ // Signing out leaves nothing: the secret, the token, every cache and every scheduled push go.
8
+ const vscode = require('vscode')
9
+ const os = require('os')
10
+ const crypto = require('crypto')
11
+ const { Account, emulatorHosts, serverFetch } = require('./account')
12
+ const gpuServer = require('./gpuServer')
13
+ const { Cloud, PREFERENCES } = require('./cloud')
14
+ const sessions = require('./sessions')
15
+ const memory = require('./memory')
16
+ const usage = require('./usage')
17
+ const prefs = require('./prefs')
18
+ const log = require('./log')
19
+
20
+ const SECRET = 'aipictury.account'
21
+ // VS Code settings that follow you (a subset of cloud.PREFERENCES; the rest come from settings.json).
22
+ const VSCODE_PREFS = ['effort', 'approval', 'showThinking', 'focusView', 'autoMemory', 'notifications', 'useCtrlEnterToSend', 'browser.headless']
23
+ const config = () => vscode.workspace.getConfiguration('aipictury')
24
+ const folder = () => vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || null
25
+
26
+ function register(context, { registry, sessionsView, serverUrl }) {
27
+ const firebase = require('./media/vendor/firebase-config.json')
28
+ const account = new Account({
29
+ config: firebase, client: 'VS Code',
30
+ store: {
31
+ get: async () => { const v = await context.secrets.get(SECRET); try { return v ? JSON.parse(v) : undefined } catch { return undefined } },
32
+ set: async (v) => context.secrets.store(SECRET, JSON.stringify(v)),
33
+ delete: async () => context.secrets.delete(SECRET),
34
+ },
35
+ })
36
+ let deviceId = context.globalState.get('aipictury.deviceId')
37
+ if (!deviceId) { deviceId = crypto.randomUUID(); context.globalState.update('aipictury.deviceId', deviceId) }
38
+ const cloud = new Cloud({
39
+ account, projectId: firebase.projectId, emulator: emulatorHosts().firestore, log: (l) => log.warn(l),
40
+ bucket: firebase.storageBucket, storageEmulator: emulatorHosts().storage,
41
+ device: { id: deviceId, name: os.hostname(), app: 'vscode', platform: process.platform, version: context.extension?.packageJSON?.version || '' },
42
+ })
43
+ let user = null
44
+ const signedIn = () => !!user
45
+ /** Is this kind of thing kept with the account, here and now? */
46
+ const keeps = (kind) => {
47
+ if (!user || !config().get(`cloud.${kind}`, true)) return false
48
+ if (kind === 'conversations' || kind === 'memory') {
49
+ try { if (prefs.load(folder()).merged.cloudSync === false) return false } catch { /* no settings file */ }
50
+ }
51
+ return true
52
+ }
53
+
54
+ // ---- the status bar ----
55
+ const status = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 90)
56
+ status.command = 'aipictury.account'
57
+ const showStatus = () => {
58
+ status.text = user ? `$(account) ${user.displayName || user.email}` : '$(account) Sign in to AIPictury'
59
+ status.tooltip = user ? `AIPictury: signed in as ${user.email}. Click for your account.` : 'AIPictury needs your account: click to sign in or create one.'
60
+ status.backgroundColor = user ? undefined : new vscode.ThemeColor('statusBarItem.warningBackground')
61
+ status.show()
62
+ }
63
+ context.subscriptions.push(status)
64
+
65
+ // ---- keeping things in step ----
66
+ context.subscriptions.push(sessions.onWrite((e) => {
67
+ if (!e.folder || !keeps('conversations')) return
68
+ if (e.type === 'remove') { cloud.deleteConversation(e.folder, e.id).catch((err) => log.warn(`cloud: ${e.id} not deleted: ${err.message}`)); return }
69
+ cloud.schedule(e.folder, e.entry, e.type === 'patch' ? 500 : 5000).catch(() => {})
70
+ }))
71
+ memory.useAccountRules({ active: async () => keeps('memory'), get: () => cloud.getRules(), set: (rules) => cloud.setRules(rules) })
72
+ const pushProjectRules = debounce(async () => {
73
+ if (!keeps('memory') || !folder()) return
74
+ try { await cloud.setProjectMemory(folder(), await projectRulesText()) } catch (err) { log.warn(`cloud: project rules not pushed: ${err.message}`) }
75
+ }, 2000)
76
+ context.subscriptions.push(memory.onProjectRules(pushProjectRules))
77
+ context.subscriptions.push(usage.onRecord((entry, now) => {
78
+ if (!keeps('usage')) return
79
+ cloud.addUsage(new Date(now).toISOString().slice(0, 10), entry).catch((err) => log.warn(`cloud: usage not added: ${err.message}`))
80
+ }))
81
+ const pushPreferences = debounce(async () => {
82
+ if (!keeps('settings')) return
83
+ try { await cloud.setPreferences(localPreferences()) } catch (err) { log.warn(`cloud: preferences not pushed: ${err.message}`) }
84
+ }, 3000)
85
+ context.subscriptions.push(vscode.workspace.onDidChangeConfiguration((e) => {
86
+ if (applying) return
87
+ if (VSCODE_PREFS.some((k) => e.affectsConfiguration(`aipictury.${k}`))) pushPreferences()
88
+ }))
89
+
90
+ /** This project's conversations made here before the account had them (or changed since):
91
+ * carried up, so the account's history — on the web too — is the whole of it. Each once,
92
+ * and only when it changed (the push compares hashes). */
93
+ async function bringUp() {
94
+ const here = folder()
95
+ if (!here) return
96
+ try {
97
+ const up = await cloud.bringUp(here, sessions.storeFor(here), { still: () => Boolean(user) })
98
+ if (up) { log.info(`cloud: ${up} conversation(s) of this project carried up to the account`); sessionsView?.refresh?.() }
99
+ } catch (err) { log.warn(`cloud: history not carried up: ${err.message}`) }
100
+ }
101
+
102
+ let applying = false
103
+ /** After signing in: what the account has is brought here, what is only here goes up. */
104
+ async function settle() {
105
+ if (!['conversations', 'memory', 'settings', 'usage'].some((k) => keeps(k))) return
106
+ await cloud.hello()
107
+ if (keeps('conversations')) await bringUp()
108
+ if (keeps('memory')) {
109
+ // Your rules: the first time, the server's list moves into the account.
110
+ const rules = await cloud.getRules()
111
+ if (!rules.length) {
112
+ const server = serverUrl()
113
+ try {
114
+ const r = await fetch(`${server}/api/memory`, { signal: AbortSignal.timeout(8000) })
115
+ const old = r.ok ? (await r.json()).rules || [] : []
116
+ if (old.length) await cloud.setRules(old.map((x) => ({ id: String(x.id), text: x.text, created: Date.now() })))
117
+ } catch { /* the server's list stays where it is */ }
118
+ }
119
+ // The project's rules: brought back when this checkout has none, kept up to date otherwise.
120
+ if (folder()) {
121
+ const local = await projectRulesText()
122
+ const remote = await cloud.getProjectMemory(folder())
123
+ if (!local.trim() && remote?.text) await vscode.workspace.fs.writeFile(memory.projectUri(), new TextEncoder().encode(remote.text))
124
+ else if (local.trim() && local !== remote?.text) await cloud.setProjectMemory(folder(), local)
125
+ }
126
+ }
127
+ if (keeps('settings')) {
128
+ const remote = await cloud.getPreferences()
129
+ if (remote?.values && Object.keys(remote.values).length) {
130
+ applying = true
131
+ try {
132
+ for (const k of VSCODE_PREFS) if (k in remote.values && config().get(k) !== remote.values[k]) await config().update(k, remote.values[k], vscode.ConfigurationTarget.Global)
133
+ } finally { applying = false }
134
+ } else await cloud.setPreferences(localPreferences())
135
+ }
136
+ }
137
+
138
+ function changed(next) {
139
+ user = next
140
+ showStatus()
141
+ vscode.commands.executeCommand('setContext', 'aipictury.signedIn', !!user)
142
+ for (const p of registry.all()) p.postAccount?.(user)
143
+ sessionsView?.setCloud?.(user ? cloud : null)
144
+ if (user) {
145
+ findServer()
146
+ settle().catch((err) => log.warn(`account: not settled: ${err.message}`))
147
+ } else gpuServer.adopt(null) // signed out: the account's server goes with it
148
+ require('./about').refreshOpen()
149
+ }
150
+ context.subscriptions.push(account.onChange(changed))
151
+
152
+ // The GPU server chosen with the account (aipictury.com): used when this machine sets none,
153
+ // and looked at again now and then — it may be changed on the web.
154
+ async function findServer() {
155
+ try {
156
+ const prefs = await cloud.getPreferences()
157
+ if (user) await gpuServer.adopt(prefs?.values?.gpuServer)
158
+ } catch (err) { log.warn(`account: GPU server not read: ${err.message}`) }
159
+ }
160
+ // Every render the agent makes here is kept with the account (Renders, on the web too).
161
+ context.subscriptions.push(require('./mediagen').onRendered((server, jobId, keep = () => {}) => {
162
+ if (!user) return
163
+ cloud.keepRender(server, jobId, (_, state) => keep(state)).then((kept) => kept && log.info(`render ${jobId} kept with the account`))
164
+ .catch((err) => { keep({ stage: 'failed', error: err.message }); log.warn(`render ${jobId} not kept with the account: ${err.message}`) })
165
+ }))
166
+ // Asked from the web ("control it from here"): this window, when it has the conversation, shares
167
+ // it by remote control. One long request at a time, while signed in; an older server: later.
168
+ let listening = true
169
+ context.subscriptions.push({ dispose: () => { listening = false } })
170
+ ;(async function listenForAsks() {
171
+ let since = Date.now() / 1000
172
+ while (listening) {
173
+ const server = serverUrl()
174
+ if (!user || !server) { await new Promise((r) => setTimeout(r, 10_000)); continue }
175
+ try {
176
+ const r = await fetch(`${server}/api/remote/requests?since=${since}&wait=25`, { signal: AbortSignal.timeout(40_000) })
177
+ if (!r.ok) { await new Promise((res) => setTimeout(res, r.status === 404 ? 120_000 : 15_000)); continue }
178
+ const body = await r.json()
179
+ since = body.now || since
180
+ for (const ask of body.asks || []) await registry.shareAsked?.(ask.conversation).catch((err) => log.warn(`remote control not started: ${err.message}`))
181
+ } catch { await new Promise((res) => setTimeout(res, 5000)) }
182
+ }
183
+ })()
184
+ const recheck = setInterval(() => { if (user) findServer() }, 10 * 60_000)
185
+ context.subscriptions.push({ dispose: () => clearInterval(recheck) })
186
+ context.subscriptions.push(gpuServer.onChange((url) => {
187
+ log.info(`GPU server: ${url || 'none set'} (${gpuServer.source() || 'nowhere'})`)
188
+ for (const p of registry.all()) p.refreshStatus?.()
189
+ require('./about').refreshOpen()
190
+ }))
191
+
192
+ async function signIn() {
193
+ try {
194
+ await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'AIPictury: sign in in your browser…', cancellable: true },
195
+ (_, token) => {
196
+ const ctrl = new AbortController()
197
+ token.onCancellationRequested(() => ctrl.abort())
198
+ return account.signIn({ signal: ctrl.signal, open: (url) => vscode.env.openExternal(vscode.Uri.parse(url)) })
199
+ })
200
+ vscode.window.showInformationMessage(`AIPictury: signed in as ${user?.email || user?.displayName}.`)
201
+ } catch (error) {
202
+ if (!/cancelled/.test(error.message)) vscode.window.showErrorMessage(`AIPictury: not signed in — ${error.message}`)
203
+ }
204
+ }
205
+
206
+ async function signOut() {
207
+ for (const p of registry.all()) p.abort?.abort() // nothing keeps running for an account that left
208
+ await account.signOut() // the secret, the token; the cloud forgets (onChange)
209
+ vscode.window.showInformationMessage('AIPictury: signed out. Nothing of the account is kept on this machine.')
210
+ }
211
+
212
+ async function deleteCloudData() {
213
+ const yes = 'Delete everything'
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)
216
+ if (ok !== yes) return null
217
+ const n = await cloud.deleteEverything()
218
+ return `Deleted (${n} records). Your account stays — sign out or keep using it.`
219
+ }
220
+
221
+ function localPreferences() {
222
+ const values = {}
223
+ for (const k of VSCODE_PREFS) values[k] = config().get(k)
224
+ try {
225
+ const merged = prefs.load(folder()).merged
226
+ for (const k of ['outputStyle', 'skillVisibility', 'enabledPlugins', 'permissions']) if (merged[k] !== undefined) values[k] = merged[k]
227
+ } catch { /* no settings file */ }
228
+ return Object.fromEntries(Object.entries(values).filter(([k, v]) => PREFERENCES.includes(k) && v !== undefined))
229
+ }
230
+
231
+ // Every call to the GPU server carries the account's token (the server may require it: AUTH),
232
+ // and only calls to that server — never another site.
233
+ const plainFetch = globalThis.fetch
234
+ globalThis.fetch = serverFetch({ fetch: plainFetch, account, server: serverUrl, signedIn: () => Boolean(user) })
235
+ context.subscriptions.push({ dispose: () => { globalThis.fetch = plainFetch } })
236
+
237
+ account.user().then(changed).catch(() => changed(null))
238
+ return { account, cloud, signedIn, signIn, signOut, deleteCloudData, keeps, user: () => user }
239
+ }
240
+
241
+ async function projectRulesText() {
242
+ try { return new TextDecoder().decode(await vscode.workspace.fs.readFile(memory.projectUri())) } catch { return '' }
243
+ }
244
+
245
+ function debounce(fn, ms) {
246
+ let t = null
247
+ return () => { clearTimeout(t); t = setTimeout(fn, ms) }
248
+ }
249
+
250
+ /** The Account dialog (dialogs.js's component). */
251
+ function accountSpec(ctl, note) {
252
+ const u = ctl.user()
253
+ if (!u) {
254
+ return { id: 'account', title: 'Your AIPictury account',
255
+ note: note || 'AIPictury needs your account (the same as at aipictury.com). Signing in happens in your browser.',
256
+ sections: [{ title: 'Signed out', items: [{ id: 'signin', label: 'Sign in, or create an account', detail: 'Email and password, or Google.', actions: [{ id: 'signin', label: 'Sign in', always: true }] }] }] }
257
+ }
258
+ const kinds = [['conversations', 'Conversations', 'Every conversation of your projects, so another machine (or the CLI) can open it.'],
259
+ ['memory', 'Rules', 'Your rules, and each project\'s .aipictury/memory.md.'],
260
+ ['settings', 'Preferences', 'Effort, permission mode, reasoning, focus view… (never keys, tokens or MCP settings).'],
261
+ ['usage', 'Usage', 'Each day\'s turns, tokens, tools and models.']]
262
+ return { id: 'account', title: 'Your AIPictury account',
263
+ note: note || `Signed in as ${u.email || u.displayName}. What is yours is kept with the account, readable by you alone.`,
264
+ sections: [
265
+ { title: 'Kept with your account', items: kinds.map(([id, label, detail]) => ({ id: `keep:${id}`, label, detail, toggle: { on: config().get(`cloud.${id}`, true) } })) },
266
+ { title: 'Account', items: [
267
+ { 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 }] },
269
+ ] },
270
+ ] }
271
+ }
272
+
273
+ function accountHandler(ctl) {
274
+ return async (action, message) => {
275
+ const item = String(message.item || '')
276
+ 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.')
279
+ }
280
+ if (action === 'signin') { await ctl.signIn(); return accountSpec(ctl) }
281
+ if (action === 'signout') { await ctl.signOut(); return accountSpec(ctl, 'Signed out.') }
282
+ if (action === 'delete') { const said = await ctl.deleteCloudData(); return accountSpec(ctl, said || undefined) }
283
+ return undefined
284
+ }
285
+ }
286
+
287
+ module.exports = { register, accountSpec, accountHandler, VSCODE_PREFS, SECRET }