@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.
- package/LICENSE.md +77 -0
- package/README.md +39 -0
- package/ext/about.js +209 -0
- package/ext/account.js +242 -0
- package/ext/accountUi.js +287 -0
- package/ext/agent.js +1929 -0
- package/ext/browser.js +263 -0
- package/ext/cloud.js +385 -0
- package/ext/context.js +241 -0
- package/ext/deps.js +142 -0
- package/ext/dialogs.js +803 -0
- package/ext/diff.js +150 -0
- package/ext/extension.js +2483 -0
- package/ext/feedback.js +55 -0
- package/ext/files.js +565 -0
- package/ext/firestore.js +131 -0
- package/ext/gpuRun.js +77 -0
- package/ext/gpuServer.js +76 -0
- package/ext/hooks.js +153 -0
- package/ext/log.js +29 -0
- package/ext/lsp.js +122 -0
- package/ext/mcp.js +469 -0
- package/ext/mcpAuth.js +128 -0
- package/ext/media/vendor/firebase-config.json +9 -0
- package/ext/media/vendor/signin.js +1 -0
- package/ext/mediagen.js +174 -0
- package/ext/memory.js +117 -0
- package/ext/notebook.js +82 -0
- package/ext/package.json +1234 -0
- package/ext/paths.js +50 -0
- package/ext/permissions.js +229 -0
- package/ext/plans.js +110 -0
- package/ext/plugins.js +243 -0
- package/ext/prefs.js +157 -0
- package/ext/remote.js +170 -0
- package/ext/review.js +126 -0
- package/ext/sandbox.js +79 -0
- package/ext/scheduler.js +87 -0
- package/ext/sessions.js +228 -0
- package/ext/sessionsView.js +449 -0
- package/ext/shell.js +357 -0
- package/ext/storage.js +80 -0
- package/ext/tasklist.js +112 -0
- package/ext/text.js +31 -0
- package/ext/tools.js +781 -0
- package/ext/usage.js +77 -0
- package/ext/voice.js +135 -0
- package/ext/workbench.js +162 -0
- package/ext/workflow.js +51 -0
- package/ext/worktree.js +83 -0
- package/main.js +758 -0
- package/package.json +43 -0
- package/terminal.js +124 -0
- package/vscode-shim.js +231 -0
package/ext/accountUi.js
ADDED
|
@@ -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 }
|