heyquilt 0.3.122 → 0.3.140
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/RELEASES.md +279 -17
- package/bin/quilt.js +62 -152
- package/desktop/main.js +16 -2
- package/package.json +3 -2
- package/src/account.js +0 -29
- package/src/agent-task-workflow.js +2 -2
- package/src/agents/common.js +13 -0
- package/src/ai-vm.js +9 -1
- package/src/api/activity.js +5 -0
- package/src/api/memory-store.js +16 -38
- package/src/api/notify.js +134 -0
- package/src/api/routes/browsers.js +29 -0
- package/src/api/routes/relay.js +26 -3
- package/src/api/routes/session-migrate.js +7 -4
- package/src/api/routes/sessions.js +73 -1
- package/src/api/routes/teams.js +0 -23
- package/src/api/server.js +17 -25
- package/src/api/supabase-store.js +43 -56
- package/src/autowork.js +182 -16
- package/src/branch-view.js +6 -131
- package/src/connection.js +36 -9
- package/src/control.js +131 -48
- package/src/duties.js +1 -1
- package/src/force-date.js +27 -0
- package/src/fsutil.js +13 -3
- package/src/github-commit.js +3 -315
- package/src/history.js +13 -3
- package/src/mcp.js +93 -167
- package/src/merge3.js +11 -3
- package/src/now-summary.js +113 -0
- package/src/pathrules.js +12 -0
- package/src/protocol.js +10 -2
- package/src/quilt-default.js +84 -0
- package/src/quilt-history.js +106 -0
- package/src/quilt-remote.js +78 -0
- package/src/relay-ask.js +3 -66
- package/src/relay-cluster.js +3 -5
- package/src/relay-lease.js +6 -2
- package/src/relay-mcp.js +154 -432
- package/src/relay-prune.js +6 -145
- package/src/relay-turn-sync.js +1 -1
- package/src/release-words.js +2 -2
- package/src/runner.js +20 -7
- package/src/seam-access.js +97 -0
- package/src/seam-claims.js +53 -0
- package/src/seam-url.js +15 -0
- package/src/server.js +1516 -1383
- package/src/session-follow.js +461 -27
- package/src/session-layout3.js +410 -4
- package/src/session-migrate.js +15 -11
- package/src/session-quilt.js +1 -1
- package/src/session-seam.js +233 -20
- package/src/session.js +745 -3438
- package/src/source-live/bring-in.js +283 -0
- package/src/source-live/live-block.js +504 -33
- package/src/source-live/live-square.js +16 -5
- package/src/status.js +7 -58
- package/src/turn-sync.js +15 -2
- package/src/ui/agent-guide.js +1 -1
- package/src/ui/app.css +85 -20
- package/src/ui/app.js +4 -1
- package/src/ui/changes.js +5 -14
- package/src/ui/chat.js +9 -4
- package/src/ui/cloud-ai-model.js +13 -0
- package/src/ui/cloud-ai.js +10 -2
- package/src/ui/common.js +6 -4
- package/src/ui/fileview.js +20 -7
- package/src/ui/force-words.js +19 -0
- package/src/ui/git-rule.js +9 -28
- package/src/ui/home.js +3 -223
- package/src/ui/loom-model.js +15 -1
- package/src/ui/quiltview.js +213 -0
- package/src/ui/seam-model.js +251 -0
- package/src/ui/session.js +377 -489
- package/src/ui/tree.js +2 -8
- package/src/ui/voice-call.js +83 -14
- package/src/ui/voice-model.js +39 -0
- package/src/ui/voice.js +167 -17
- package/src/ui/workspaces.js +2 -122
- package/src/ui-server.js +81 -66
- package/src/update-check.js +25 -1
- package/src/voice.js +13 -6
- package/src/api/routes/github.js +0 -393
- package/src/branches.js +0 -194
- package/src/clash.js +0 -128
- package/src/commit-request.js +0 -59
- package/src/commit.js +0 -213
- package/src/git-hook.js +0 -48
- package/src/git.js +0 -147
- package/src/gitignore.js +0 -49
- package/src/gitstate.js +0 -1033
- package/src/quilts/AGENTS.md +0 -1
- package/src/quilts/CLAUDE.md +0 -1
- package/src/quilts/accounts.js +0 -136
- package/src/quilts/affected.js +0 -132
- package/src/quilts/archiver.js +0 -175
- package/src/quilts/autorevert.js +0 -49
- package/src/quilts/bisect-run.js +0 -31
- package/src/quilts/blame.js +0 -103
- package/src/quilts/block.js +0 -347
- package/src/quilts/bucket.js +0 -25
- package/src/quilts/cbor.js +0 -92
- package/src/quilts/checks.js +0 -308
- package/src/quilts/claims.js +0 -144
- package/src/quilts/cli.js +0 -536
- package/src/quilts/client.js +0 -137
- package/src/quilts/conflict-format.js +0 -190
- package/src/quilts/describe.js +0 -296
- package/src/quilts/dogfood.js +0 -82
- package/src/quilts/errors.js +0 -8
- package/src/quilts/events.js +0 -171
- package/src/quilts/export-git.js +0 -130
- package/src/quilts/export-remote.js +0 -156
- package/src/quilts/export.js +0 -81
- package/src/quilts/fastmerge.js +0 -41
- package/src/quilts/hash.js +0 -44
- package/src/quilts/history.js +0 -266
- package/src/quilts/ids.js +0 -12
- package/src/quilts/import-follow.js +0 -236
- package/src/quilts/import-git.js +0 -294
- package/src/quilts/import.js +0 -196
- package/src/quilts/index.js +0 -94
- package/src/quilts/keys.js +0 -186
- package/src/quilts/limits.js +0 -67
- package/src/quilts/lines.js +0 -49
- package/src/quilts/live.js +0 -50
- package/src/quilts/merge-smart.js +0 -114
- package/src/quilts/merge.js +0 -501
- package/src/quilts/oauth.js +0 -202
- package/src/quilts/objects.js +0 -144
- package/src/quilts/pack.js +0 -118
- package/src/quilts/records.js +0 -211
- package/src/quilts/resolutions.js +0 -69
- package/src/quilts/reviews.js +0 -208
- package/src/quilts/routes.js +0 -186
- package/src/quilts/runner.js +0 -95
- package/src/quilts/server.js +0 -144
- package/src/quilts/service.js +0 -963
- package/src/quilts/setup.js +0 -41
- package/src/quilts/signing.js +0 -149
- package/src/quilts/squares.js +0 -239
- package/src/quilts/store-memory.js +0 -844
- package/src/quilts/store-rpc.js +0 -90
- package/src/quilts/store-supabase.js +0 -13
- package/src/quilts/tags.js +0 -64
- package/src/quilts/textdiff.js +0 -96
- package/src/quilts/tokens.js +0 -66
- package/src/quilts/tools.js +0 -403
- package/src/quilts/tree.js +0 -351
- package/src/quilts/webhook.js +0 -116
- package/src/quilts/window.js +0 -121
- package/src/quilts/xdiff.js +0 -435
- package/src/relay-ci.js +0 -111
- package/src/relay-clash.js +0 -214
- package/src/relay-commit.js +0 -918
- package/src/relay-filekey.js +0 -46
- package/src/relay-git.js +0 -274
- package/src/relay-githttp.js +0 -166
- package/src/relay-github.js +0 -109
- package/src/relay-outbound.js +0 -1295
- package/src/relay-repo-moves.js +0 -43
- package/src/relay-sync.js +0 -795
- package/src/relay-upstream.js +0 -889
- package/src/sync-words.js +0 -42
- package/src/ui/branches.js +0 -111
- package/src/upstream.js +0 -192
package/bin/quilt.js
CHANGED
|
@@ -28,7 +28,7 @@ Usage:
|
|
|
28
28
|
quilt sync [--dir <folder>] Agents in a sandbox: sync this folder with the session once and exit
|
|
29
29
|
(needs QUILT_AGENT_KEY; run it at the start and end of every turn).
|
|
30
30
|
With the key, status, messages, say, tasks, task, history, claim,
|
|
31
|
-
release, focus,
|
|
31
|
+
release, focus, tool and mcp work too
|
|
32
32
|
quilt setup Connect Claude Code / Cursor / others via MCP
|
|
33
33
|
quilt status Show collaborators, claims, activity, chat
|
|
34
34
|
quilt history [path] [--by name] [--since 2h] [--task id] [--diff] [-n 30]
|
|
@@ -46,16 +46,12 @@ Usage:
|
|
|
46
46
|
quilt run -- <command> [args...] Run a command with the session's secrets: it gets NAME=quilt and
|
|
47
47
|
NAME's base URL (STRIPE_BASE_URL), and its requests go through
|
|
48
48
|
Quilt's vault; it never sees a value
|
|
49
|
-
quilt commit
|
|
50
|
-
Ask a person to commit for you (when agents may not push here)
|
|
51
|
-
quilt commits Open commit requests and recent commits
|
|
52
|
-
quilt github Whether agents can push here through GitHub, and the link that connects it
|
|
49
|
+
quilt commit <message> In a session that is a quilt: commit the square this folder follows
|
|
53
50
|
quilt focus <what you're doing> Tell collaborators what you're working on
|
|
54
51
|
quilt claim <path|glob> [reason] Mark files as yours for now
|
|
55
52
|
quilt release <path|glob|*> Release a claim
|
|
56
53
|
quilt chat-link [name] [--minutes N] Owner: a link for a chat-only AI (ChatGPT, claude.ai, Grok); 10 minutes
|
|
57
54
|
quilt chat-link extend <name> <minutes> Owner: keep a chat link working for that long from now
|
|
58
|
-
quilt github-token [--clear] Owner: a read-only GitHub token (read from stdin) so the relay brings in commits while everyone's offline
|
|
59
55
|
quilt invite Print this session's invite code
|
|
60
56
|
quilt workspace list|files|get|put|write|mkdir|mv|rm The workspace library (quilt workspace for help)
|
|
61
57
|
quilt stop Shut down everything quilt is running (relay, app, syncs)
|
|
@@ -108,13 +104,8 @@ async function main () {
|
|
|
108
104
|
case 'webhook': return webhookCmd()
|
|
109
105
|
case 'wake': return wakeCmd()
|
|
110
106
|
case 'get': return getFile()
|
|
111
|
-
case 'commit
|
|
112
|
-
case 'commits': return commits()
|
|
113
|
-
case 'github': return githubStatusCmd()
|
|
114
|
-
case 'git-token': return gitToken()
|
|
107
|
+
case 'commit': if (!argv.join(' ').trim()) fail('Usage: quilt commit <message>'); return simple('/square-commit', { message: argv.join(' ') }, (r) => r.text)
|
|
115
108
|
case 'run': return runCmd()
|
|
116
|
-
case 'git-credential': return gitCredential()
|
|
117
|
-
case 'git-pre-push': return gitPrePush()
|
|
118
109
|
case 'chat': return chat()
|
|
119
110
|
case 'focus': return simple('/focus', { text: argv.join(' ') }, () => 'focus updated')
|
|
120
111
|
case 'claim': return simple('/claim', { pattern: argv[0], note: argv.slice(1).join(' ') }, (r) =>
|
|
@@ -130,20 +121,6 @@ async function main () {
|
|
|
130
121
|
const name = argv.filter((a, k) => !(i >= 0 && (k === i || k === i + 1))).join(' ')
|
|
131
122
|
return simple('/chat-link', { name, minutes }, (r) => `Chat link for ${r.name}, until ${new Date(r.expiresAt).toLocaleTimeString()}:\n\n ${r.url}\n\nPaste it into ChatGPT, claude.ai or Grok with "Open this link and follow it to join our Quilt session."\nAnyone with it can act as ${r.name}. Extend it with \`quilt chat-link extend ${r.name} <minutes>\`; once it runs out, make a new one.`)
|
|
132
123
|
}
|
|
133
|
-
case 'github-token': {
|
|
134
|
-
// Read from stdin, never the command line (where shell history and ps would keep it).
|
|
135
|
-
if (argv[0] === '--clear') return simple('/github-token', { token: '' }, () => 'The relay no longer has a GitHub token for this session.')
|
|
136
|
-
if (argv.length) fail('usage: quilt github-token [--clear] (the token is read from stdin: pbpaste | quilt github-token)')
|
|
137
|
-
if (process.stdin.isTTY) process.stderr.write('Paste a read-only GitHub token (contents: read) and press Enter: ')
|
|
138
|
-
const token = await new Promise((resolve) => {
|
|
139
|
-
let buf = ''
|
|
140
|
-
process.stdin.setEncoding('utf8')
|
|
141
|
-
process.stdin.on('data', (d) => { buf += d; if (process.stdin.isTTY && buf.includes('\n')) { process.stdin.pause(); resolve(buf) } })
|
|
142
|
-
process.stdin.on('end', () => resolve(buf))
|
|
143
|
-
})
|
|
144
|
-
if (!token.trim()) fail('no token given')
|
|
145
|
-
return simple('/github-token', { token: token.trim() }, () => 'The relay has the token: it brings in commits from your private repository while nobody\'s folder is online.')
|
|
146
|
-
}
|
|
147
124
|
case 'session': return sessionCmd()
|
|
148
125
|
case 'invite': return invite()
|
|
149
126
|
case 'workspace': case 'workspaces': return workspaceCmd()
|
|
@@ -234,6 +211,8 @@ async function apiCmd () {
|
|
|
234
211
|
siteUrl: env.QUILT_SITE_URL || 'http://localhost:3000',
|
|
235
212
|
trustProxy: /^(1|true|yes)$/i.test(env.QUILT_TRUST_PROXY || ''), log: console.log,
|
|
236
213
|
workspaces: env.QUILT_WORKSPACES === '1' || env.QUILT_WORKSPACES === 'true',
|
|
214
|
+
// New sessions are quilts from the start (Seam's plans/quilt-source-control/16, flags order 8): apps read it from /v1/features.
|
|
215
|
+
quiltSourceDefault: env.QUILT_SOURCE_DEFAULT === '1' || env.QUILT_SOURCE_DEFAULT === 'true',
|
|
237
216
|
// Secrets: the vault (quilt-vault) and the secret it calls this API with; off when not set.
|
|
238
217
|
vaultSecret: env.QUILT_VAULT_SECRET || '',
|
|
239
218
|
vaultUrl: env.QUILT_VAULT_URL || 'https://vault.heyquilt.com',
|
|
@@ -242,7 +221,7 @@ async function apiCmd () {
|
|
|
242
221
|
// Its own machines through Fly's Machines API (QUILT_API_FLY_TOKEN): more as load climbs, fewer as it drops (src/api/scaler.js).
|
|
243
222
|
apiFly: (await import('../src/api/api-machines.js')).apiFlyFromEnv(env),
|
|
244
223
|
...(env.QUILT_API_MAX_MACHINES ? { maxMachines: Number(env.QUILT_API_MAX_MACHINES) } : {}),
|
|
245
|
-
// Seam is its own service (docs/seam-service.md): its old routes here go on to it.
|
|
224
|
+
// Seam is its own service (Seam's docs/seam-service.md): its old routes here go on to it.
|
|
246
225
|
...(env.QUILT_SEAM_URL !== undefined ? { seamUrl: env.QUILT_SEAM_URL } : {}),
|
|
247
226
|
...(env.QUILT_SEAM_CLIENT_ID ? { seamClientId: env.QUILT_SEAM_CLIENT_ID } : {}),
|
|
248
227
|
...(env.QUILT_SEAM_SITE_URL ? { seamSiteUrl: env.QUILT_SEAM_SITE_URL } : {}),
|
|
@@ -355,6 +334,7 @@ async function join () {
|
|
|
355
334
|
|
|
356
335
|
const { unsupportedRelay } = await import('../src/settings.js')
|
|
357
336
|
let conn
|
|
337
|
+
let fresh = false // a session this command starts: a quilt from the start when the API says so (src/quilt-default.js)
|
|
358
338
|
if (positionals[0]) {
|
|
359
339
|
try { conn = decodeInvite(positionals[0]) } catch (err) { fail(err.message) }
|
|
360
340
|
} else if (values.room) {
|
|
@@ -366,6 +346,7 @@ async function join () {
|
|
|
366
346
|
} else {
|
|
367
347
|
if (saved.server) console.log("This folder's last session ran on your computer's own relay, which Quilt no longer supports. Starting a new session.")
|
|
368
348
|
conn = newConn()
|
|
349
|
+
fresh = true
|
|
369
350
|
console.log('starting a new session')
|
|
370
351
|
}
|
|
371
352
|
|
|
@@ -407,6 +388,7 @@ async function join () {
|
|
|
407
388
|
apiAuth: auth.apiAuth,
|
|
408
389
|
identity: auth.identity,
|
|
409
390
|
inviteServer: saved.room === conn.room ? saved.inviteServer : undefined,
|
|
391
|
+
newSession: fresh,
|
|
410
392
|
onLog: (m) => console.log(`[${stamp()}] ${m}`),
|
|
411
393
|
onDebug: process.env.QUILT_DEBUG ? (m) => console.log(`[${stamp()}] debug: ${m}`) : undefined,
|
|
412
394
|
onFatal: (err) => fail(whyStopped(err))
|
|
@@ -471,6 +453,12 @@ async function withAgentKey () {
|
|
|
471
453
|
return true
|
|
472
454
|
}
|
|
473
455
|
|
|
456
|
+
/** All of stdin, for `quilt tool <name> -` (the tool's arguments as JSON). */
|
|
457
|
+
const readStdin = () => new Promise((resolve) => {
|
|
458
|
+
let s = ''
|
|
459
|
+
process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { s += c }); process.stdin.on('end', () => resolve(s)); process.stdin.on('error', () => resolve(s))
|
|
460
|
+
})
|
|
461
|
+
|
|
474
462
|
/** The sandbox's QUILT_AGENT_KEY (src/ui/agent-env-key.js), or null; a malformed one stops here. */
|
|
475
463
|
async function envKey () {
|
|
476
464
|
const { agentEnvKeyFromEnv } = await import('../src/ui/agent-env-key.js')
|
|
@@ -582,7 +570,7 @@ async function doSetup () {
|
|
|
582
570
|
console.log('\nAn AI tool that was already open picks up the "quilt" MCP server when it restarts.')
|
|
583
571
|
}
|
|
584
572
|
|
|
585
|
-
// `quilt session migrate [--dry-run]` (plans/quilt-source-control/16): what making this folder's
|
|
573
|
+
// `quilt session migrate [--dry-run]` (Seam's plans/quilt-source-control/16): what making this folder's
|
|
586
574
|
// session a quilt (Seam) would do, read only, or, without --dry-run, the move itself. The relay
|
|
587
575
|
// does both, behind QUILT_SOURCE_URL there, and only for the session's owner. --switch then moves
|
|
588
576
|
// the session's files into its squares (layout 3); --back returns it to its branches. `follow <folder>`
|
|
@@ -592,11 +580,45 @@ async function sessionCmd () {
|
|
|
592
580
|
if (!argv[1]) fail('Usage: quilt session follow <folder>')
|
|
593
581
|
return simple('/quilt-follow', { dir: path.resolve(argv[1]) }, (r) => r.text)
|
|
594
582
|
}
|
|
595
|
-
if (argv[0] !== 'migrate') fail('Usage: quilt session migrate [--dry-run | --switch | --back] | quilt session follow <folder>')
|
|
583
|
+
if (argv[0] !== 'migrate') fail('Usage: quilt session migrate [--dry-run | --link-only | --switch | --back [--carried <square,...>] | --shadow <quilt> | --unshadow] | quilt session follow <folder>')
|
|
596
584
|
if (argv.includes('--dry-run')) return simple('/quilt-plan', {}, (r) => r.text)
|
|
597
|
-
if (argv.includes('--
|
|
598
|
-
|
|
599
|
-
|
|
585
|
+
if (argv.includes('--shadow')) {
|
|
586
|
+
const q = argv[argv.indexOf('--shadow') + 1]
|
|
587
|
+
if (!q || q.startsWith('-')) fail('Usage: quilt session migrate --shadow <quilt> (the quilt an import of this session\'s git remote fills)')
|
|
588
|
+
return simple('/quilt-shadow', { quilt: q }, (r) => r.text)
|
|
589
|
+
}
|
|
590
|
+
if (argv.includes('--unshadow')) return simple('/quilt-shadow', { quilt: null }, (r) => r.text)
|
|
591
|
+
if (argv.includes('--switch')) return sessionMove('/quilt-switch', {}, 'into its squares')
|
|
592
|
+
if (argv.includes('--back')) {
|
|
593
|
+
// --carried <id,id>: open squares whose work was carried over by hand (seam diff <square> | git apply).
|
|
594
|
+
const i = argv.indexOf('--carried')
|
|
595
|
+
const carried = i >= 0 && argv[i + 1] ? argv[i + 1].split(',').map((x) => x.trim().replace(/^sq:/, '')).filter(Boolean) : []
|
|
596
|
+
return sessionMove('/quilt-unswitch', { carried }, 'back to its branches')
|
|
597
|
+
}
|
|
598
|
+
// "Make this a quilt": made a quilt and moved into its squares in one go (16 flag 9); --link-only stops after the first.
|
|
599
|
+
if (argv.includes('--link-only')) return sessionMove('/quilt-migrate', {}, 'into Seam')
|
|
600
|
+
return sessionMove('/quilt-make', {}, 'into Seam')
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* A move of the session (into Seam, or back), through this folder's app. The relay does it; on a large
|
|
605
|
+
* session that takes minutes, so it says so first. An app that can't follow a square (older than
|
|
606
|
+
* 0.3.127) is sent away by the relay at the end of the move, and this command's answer is lost with it:
|
|
607
|
+
* refused here before anything moves. If the app stops answering meanwhile, the relay still finishes
|
|
608
|
+
* (or undoes) the move on its own: said, with how to see which.
|
|
609
|
+
*/
|
|
610
|
+
async function sessionMove (route, body, where) {
|
|
611
|
+
const { d, call } = await daemonOrFail()
|
|
612
|
+
let info = null
|
|
613
|
+
try { info = await call(d, 'GET', '/info') } catch (err) { fail(err.message) }
|
|
614
|
+
const { moveRefusal } = await import('../src/control.js')
|
|
615
|
+
const refused = moveRefusal(info, (await import('../src/releases.js')).currentVersion())
|
|
616
|
+
if (refused) fail(refused)
|
|
617
|
+
console.error(`Moving the session ${where}. The relay does it; a large session takes a few minutes. Keep Quilt open until this answers.`)
|
|
618
|
+
try { console.log((await call(d, 'POST', route, body)).text) } catch (err) {
|
|
619
|
+
if (err.cut) fail(`Quilt's app for ${d.dir} stopped answering while the relay moved the session (${err.code || 'the connection closed'}), so this command never heard how it went. The relay finishes the move, or undoes it, on its own. Open the session in Quilt again (if it says the session needs a newer version of Quilt, update Quilt first); then quilt status says whether this folder follows its square.`)
|
|
620
|
+
fail(err.message)
|
|
621
|
+
}
|
|
600
622
|
}
|
|
601
623
|
|
|
602
624
|
async function daemonOrFail () {
|
|
@@ -614,12 +636,16 @@ async function status () {
|
|
|
614
636
|
async function history () {
|
|
615
637
|
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { by: { type: 'string' }, since: { type: 'string' }, task: { type: 'string' }, diff: { type: 'boolean' }, n: { type: 'string', short: 'n' } } })
|
|
616
638
|
const { formatHistory } = await import('../src/history.js')
|
|
617
|
-
await simple('/history', { path: positionals[0], by: values.by, since: values.since, task: values.task, limit: Number(values.n || 30) }, (r) => formatHistory(r.entries, { withDiff: !!values.diff }))
|
|
639
|
+
await simple('/history', { path: positionals[0], by: values.by, since: values.since, task: values.task, limit: Number(values.n || 30), with_diff: !!values.diff }, (r) => `${formatHistory(r.entries, { withDiff: !!values.diff })}${r.note ? `\n\n${r.note}` : ''}`)
|
|
618
640
|
}
|
|
619
641
|
|
|
620
642
|
async function simple (route, body, format, method = 'POST') {
|
|
621
643
|
const { d, call } = await daemonOrFail()
|
|
622
|
-
try { console.log(format(await call(d, method, route, body))) } catch (err) {
|
|
644
|
+
try { console.log(format(await call(d, method, route, body))) } catch (err) {
|
|
645
|
+
// The app doesn't know the route: it runs an older Quilt than this command.
|
|
646
|
+
if (err.status === 404 && err.message === 'not found') fail(`Quilt's app for ${d.dir} runs an older Quilt that doesn't have this yet. Update Quilt (or restart it on the new version), then try again.`)
|
|
647
|
+
fail(err.message)
|
|
648
|
+
}
|
|
623
649
|
}
|
|
624
650
|
|
|
625
651
|
/** Splits "@bob rest of message" into { to: 'bob', rest: [...] }. */
|
|
@@ -734,99 +760,6 @@ async function wakeCmd () {
|
|
|
734
760
|
return simple('/wake', { args }, (r) => r.text)
|
|
735
761
|
}
|
|
736
762
|
|
|
737
|
-
async function commitRequest () {
|
|
738
|
-
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { files: { type: 'string' }, task: { type: 'string' } } })
|
|
739
|
-
const files = values.files ? values.files.split(',').map((f) => f.trim()).filter(Boolean) : undefined
|
|
740
|
-
await simple('/commit-request', { message: positionals.join(' '), files, task: values.task }, (r) =>
|
|
741
|
-
`asked for a commit [${r.id}] of ${r.files.length} file${r.files.length === 1 ? '' : 's'}: ${r.files.slice(0, 12).join(', ')}${r.files.length > 12 ? ', …' : ''}\n${r.committer}`)
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
// git in an agent's folder runs these (installGitHelpers, gitstate.js); `--dir` is the folder, whose
|
|
745
|
-
// running session answers. They never print anything but what git expects, and never fail a git
|
|
746
|
-
// command when Quilt isn't running: git then behaves as it would without Quilt. Nor do they make git
|
|
747
|
-
// wait: a session that doesn't answer within HELPER_WAIT is taken as no session, and the whole helper
|
|
748
|
-
// exits by HELPER_LIMIT whatever is still pending (the env overrides are for tests).
|
|
749
|
-
const HELPER_WAIT = Number(process.env.QUILT_HELPER_WAIT_MS) || 4000
|
|
750
|
-
const HELPER_LIMIT = Number(process.env.QUILT_HELPER_LIMIT_MS) || 10000
|
|
751
|
-
function helperDeadline () { setTimeout(() => process.exit(0), HELPER_LIMIT).unref() }
|
|
752
|
-
function helperDir () {
|
|
753
|
-
const i = argv.indexOf('--dir')
|
|
754
|
-
const dir = i >= 0 ? argv[i + 1] : process.cwd()
|
|
755
|
-
if (i >= 0) argv.splice(i, 2)
|
|
756
|
-
return dir
|
|
757
|
-
}
|
|
758
|
-
async function helperDaemon (dir) {
|
|
759
|
-
const { findDaemon, call } = await import('../src/control.js')
|
|
760
|
-
const d = findDaemon(dir)
|
|
761
|
-
return d ? { d, call } : null
|
|
762
|
-
}
|
|
763
|
-
// git closes stdin after what it sends; one that stays open is read for HELPER_WAIT at most.
|
|
764
|
-
const readStdin = () => new Promise((resolve) => {
|
|
765
|
-
let s = ''
|
|
766
|
-
const done = () => { clearTimeout(t); process.stdin.pause(); resolve(s) }
|
|
767
|
-
const t = setTimeout(done, HELPER_WAIT)
|
|
768
|
-
process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { s += c }); process.stdin.on('end', done); process.stdin.on('error', done)
|
|
769
|
-
})
|
|
770
|
-
|
|
771
|
-
/**
|
|
772
|
-
* git's credential helper protocol: `get` answers with the session's token for the host; store and erase
|
|
773
|
-
* are nothing to do. `--fallback` (a person's folder, git for everyone): when no Quilt session answers
|
|
774
|
-
* (the app is closed) or it has no token, the person's own helpers are asked instead, as git would have
|
|
775
|
-
* without Quilt: `git credential fill` from outside the folder, so only their global and system config count.
|
|
776
|
-
*/
|
|
777
|
-
async function gitCredential () {
|
|
778
|
-
const dir = helperDir()
|
|
779
|
-
const fallback = argv.includes('--fallback')
|
|
780
|
-
if (fallback) argv.splice(argv.indexOf('--fallback'), 1)
|
|
781
|
-
if (argv[0] !== 'get') return
|
|
782
|
-
helperDeadline()
|
|
783
|
-
const input = await readStdin()
|
|
784
|
-
const fields = Object.fromEntries(input.split('\n').filter((l) => l.includes('=')).map((l) => { const i = l.indexOf('='); return [l.slice(0, i), l.slice(i + 1)] }))
|
|
785
|
-
try {
|
|
786
|
-
const h = await helperDaemon(dir)
|
|
787
|
-
if (h) {
|
|
788
|
-
const r = await h.call(h.d, 'POST', '/git/credential', { host: fields.host || '', protocol: fields.protocol || '' }, { timeoutMs: HELPER_WAIT })
|
|
789
|
-
if (r && r.username && r.password) return process.stdout.write(`username=${r.username}\npassword=${r.password}\n`)
|
|
790
|
-
}
|
|
791
|
-
} catch {}
|
|
792
|
-
if (fallback) await ownCredential(input)
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
/** The person's own credentials for what git asked (`input`, git's credential fields), or nothing: never a prompt. */
|
|
796
|
-
async function ownCredential (input) {
|
|
797
|
-
const os = await import('node:os')
|
|
798
|
-
const { execFile } = await import('node:child_process')
|
|
799
|
-
const away = os.tmpdir()
|
|
800
|
-
const out = await new Promise((resolve) => {
|
|
801
|
-
const c = execFile('git', ['credential', 'fill'], { cwd: away, encoding: 'utf8', timeout: Math.max(1000, HELPER_LIMIT - HELPER_WAIT - 1000), killSignal: 'SIGKILL', env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', SSH_ASKPASS: '', GIT_CEILING_DIRECTORIES: path.dirname(away) } }, (err, stdout) => resolve(err ? '' : stdout))
|
|
802
|
-
c.stdin.end(input.endsWith('\n\n') ? input : `${input.replace(/\n*$/, '')}\n\n`)
|
|
803
|
-
})
|
|
804
|
-
const lines = out.split('\n').filter((l) => /^(username|password)=/.test(l))
|
|
805
|
-
if (lines.length === 2) process.stdout.write(`${lines.join('\n')}\n`)
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/** The pre-push hook: refs on stdin (local ref, local sha, remote ref, remote sha), the remote's name and URL as arguments. Exits 1 with the reason when the push may not go. */
|
|
809
|
-
async function gitPrePush () {
|
|
810
|
-
const dir = helperDir()
|
|
811
|
-
const [remote = 'origin'] = argv
|
|
812
|
-
helperDeadline()
|
|
813
|
-
const input = await readStdin()
|
|
814
|
-
const refs = input.split('\n').filter(Boolean).map((l) => { const [localRef, localSha, remoteRef, remoteSha] = l.trim().split(/\s+/); return { localRef, localSha, remoteRef, remoteSha } })
|
|
815
|
-
try {
|
|
816
|
-
const h = await helperDaemon(dir)
|
|
817
|
-
if (!h) return
|
|
818
|
-
const r = await h.call(h.d, 'POST', '/git/push-check', { remote, refs }, { timeoutMs: HELPER_WAIT })
|
|
819
|
-
if (r && r.ok === false) { process.stderr.write(`Quilt: ${r.why || 'this push is not allowed here'}\n`); process.exitCode = 1 }
|
|
820
|
-
} catch {}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
/** A short-lived token for this folder's repository (agents), for gh or GitHub's API. */
|
|
824
|
-
/** `quilt git-token` no longer prints a token (a value in a terminal is a value in an AI's context). */
|
|
825
|
-
async function gitToken () {
|
|
826
|
-
const { GIT_TOKEN_GONE } = await import('../src/secret-run.js')
|
|
827
|
-
fail(GIT_TOKEN_GONE)
|
|
828
|
-
}
|
|
829
|
-
|
|
830
763
|
/** `quilt run -- <command>`: the command gets its secrets' stand-ins and gateways (src/secret-run.js). */
|
|
831
764
|
async function runCmd () {
|
|
832
765
|
const { RUN_USAGE, runSource, runWithSecrets } = await import('../src/secret-run.js')
|
|
@@ -840,30 +773,6 @@ async function runCmd () {
|
|
|
840
773
|
try { process.exitCode = await runWithSecrets({ command, source }) } catch (err) { fail(err.message) }
|
|
841
774
|
}
|
|
842
775
|
|
|
843
|
-
async function githubStatusCmd () {
|
|
844
|
-
const { d, call } = await daemonOrFail()
|
|
845
|
-
const st = await call(d, 'GET', '/status')
|
|
846
|
-
const mine = (st.branches || []).find((b) => st.git && b.name === st.git.key) || (st.branches || []).find((b) => b.default)
|
|
847
|
-
const g = mine && mine.relay && mine.relay.github
|
|
848
|
-
const repo = mine && mine.relay && mine.relay.repo
|
|
849
|
-
if (!repo) return console.log('This session does not follow a GitHub repository (no member\'s folder is a clone of one), so there is nothing to connect.')
|
|
850
|
-
const words = { ok: `Git is on for everyone here, through @${g && g.login}'s connection: people and agents push with git, nothing to set up`, 'not-connected': 'not connected: nobody in the session has connected GitHub yet', 'not-installed': 'Quilt is not installed on this repository on GitHub yet', 'no-access': `no connected GitHub account (@${g && g.login}) may write to it`, 'not-github': 'not on GitHub' }
|
|
851
|
-
console.log(`${repo}: ${g ? words[g.state] || g.state : 'not checked yet'}`)
|
|
852
|
-
if (g && g.state === 'ok') return
|
|
853
|
-
const { readAccount } = await import('../src/account.js')
|
|
854
|
-
const token = readAccount()?.token
|
|
855
|
-
if (!token) return console.log('Sign in (quilt login), then run this again for the link that connects GitHub.')
|
|
856
|
-
const { githubConnectUrl } = await import('../src/account.js')
|
|
857
|
-
try { console.log(`Connect it (one click, once, by someone whose GitHub account can write to the repository):\n ${await githubConnectUrl({ token, repo })}`) } catch (err) { console.log(`Could not get the link: ${err.message}`) }
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
async function commits () {
|
|
861
|
-
const { d, call } = await daemonOrFail()
|
|
862
|
-
const { describeCommitRequests } = await import('../src/commit.js')
|
|
863
|
-
const c = await call(d, 'GET', '/commits')
|
|
864
|
-
console.log(plain(describeCommitRequests(c, { canCommit: !!c.canCommit })))
|
|
865
|
-
}
|
|
866
|
-
|
|
867
776
|
async function getFile () {
|
|
868
777
|
if (!argv[0]) fail('usage: quilt get <message-id> [dest]')
|
|
869
778
|
await simple('/get', { id: argv[0], dest: argv[1] ? path.resolve(argv[1]) : undefined }, (r) => `saved to ${r.path}`)
|
|
@@ -982,4 +891,5 @@ function fail (msg) {
|
|
|
982
891
|
process.exit(1)
|
|
983
892
|
}
|
|
984
893
|
|
|
985
|
-
|
|
894
|
+
// An app that can't be reached is said in words (control.js unreachable), not as a stack.
|
|
895
|
+
main().catch((err) => fail(err.unreachable ? err.message : err.stack || err.message))
|
package/desktop/main.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// keep syncing from the menu bar when the window is closed, and invite links
|
|
3
3
|
// (quilt://join?invite=…) open straight into the join screen.
|
|
4
4
|
import '../src/quiet-warnings.js'
|
|
5
|
-
import { app, BrowserWindow, Tray, Menu, shell, dialog, ipcMain, nativeTheme, systemPreferences } from 'electron'
|
|
5
|
+
import { app, BrowserWindow, Tray, Menu, shell, dialog, ipcMain, nativeTheme, systemPreferences, desktopCapturer } from 'electron'
|
|
6
6
|
import { execFile } from 'node:child_process'
|
|
7
7
|
import fs from 'node:fs'
|
|
8
8
|
import os from 'node:os'
|
|
@@ -18,6 +18,7 @@ import { registerOnStart } from '../src/integrations.js'
|
|
|
18
18
|
import { adoptLoginShellPath } from '../src/shell-path.js'
|
|
19
19
|
import { getSettings } from '../src/settings.js'
|
|
20
20
|
import { allowPermission, checkPermission } from './media-permission.js'
|
|
21
|
+
import { shareHandler } from './screen-share.js'
|
|
21
22
|
|
|
22
23
|
adoptLegacyEnv()
|
|
23
24
|
// Chromium's own error log goes to the terminal the app runs in. In a voice call it reports every
|
|
@@ -40,6 +41,7 @@ let tray = null
|
|
|
40
41
|
let quitting = false
|
|
41
42
|
let pendingInvite = null
|
|
42
43
|
let rendererReady = false
|
|
44
|
+
let shareWired = false // the screen share handler (and its answer channel), set up with the first window
|
|
43
45
|
|
|
44
46
|
/** The invite link inside a quilt:// URL, or null. */
|
|
45
47
|
function inviteFrom (url) {
|
|
@@ -206,10 +208,22 @@ function createWindow () {
|
|
|
206
208
|
const ses = win.webContents.session
|
|
207
209
|
ses.setPermissionRequestHandler((wc, permission, done, details) => {
|
|
208
210
|
if (!allowPermission(permission, details, origin)) return done(false)
|
|
209
|
-
if (permission === 'media' && process.platform === 'darwin') return systemPreferences.askForMediaAccess('microphone').then(done, () => done(false))
|
|
211
|
+
if (permission === 'media' && process.platform === 'darwin' && details.mediaTypes?.includes('audio')) return systemPreferences.askForMediaAccess('microphone').then(done, () => done(false))
|
|
210
212
|
done(true)
|
|
211
213
|
})
|
|
212
214
|
ses.setPermissionCheckHandler((wc, permission, requestingOrigin, details) => checkPermission(permission, requestingOrigin, details, origin))
|
|
215
|
+
// Sharing a screen in a call: the Mac's own picker where there is one (macOS 15+), otherwise the
|
|
216
|
+
// page shows what can be shared (screen-share.js).
|
|
217
|
+
if (!shareWired) {
|
|
218
|
+
shareWired = true
|
|
219
|
+
ses.setDisplayMediaRequestHandler(shareHandler({
|
|
220
|
+
win: () => win,
|
|
221
|
+
appOrigin: origin,
|
|
222
|
+
capturer: desktopCapturer,
|
|
223
|
+
ipcMain,
|
|
224
|
+
screenAccess: () => process.platform === 'darwin' ? systemPreferences.getMediaAccessStatus('screen') : 'granted'
|
|
225
|
+
}), { useSystemPicker: true })
|
|
226
|
+
}
|
|
213
227
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
214
228
|
if (/^https?:/.test(url) && !url.startsWith(origin)) shell.openExternal(url)
|
|
215
229
|
return { action: 'deny' }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heyquilt",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.140",
|
|
4
4
|
"description": "Real-time, tool-agnostic pair vibe coding: sync a project folder live between collaborators using any AI coding tool.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Daniel Carmichael",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@fontsource/ibm-plex-mono": "^5.3.0",
|
|
39
39
|
"@fontsource/poppins": "^5.3.0",
|
|
40
|
-
"@heyquilt/seam": "
|
|
40
|
+
"@heyquilt/seam": "0.237.313",
|
|
41
41
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
42
42
|
"@noble/hashes": "^2.4.0",
|
|
43
43
|
"@supabase/supabase-js": "^2.117.2",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"desktop/preload.cjs",
|
|
85
85
|
"desktop/updater.js",
|
|
86
86
|
"desktop/media-permission.js",
|
|
87
|
+
"desktop/screen-share.js",
|
|
87
88
|
"desktop/icons/*.png",
|
|
88
89
|
"desktop/icons/*.ico",
|
|
89
90
|
"package.json",
|
package/src/account.js
CHANGED
|
@@ -224,35 +224,6 @@ export async function removeSandboxKey ({ token, id, keyId, api = apiUrl(), fetc
|
|
|
224
224
|
return call(fetchImpl, api, 'DELETE', sandbox$(id, `/${encodeURIComponent(keyId)}`), null, token)
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
/** This account's GitHub connection (Quilt's GitHub App): { available, connected, login? }. */
|
|
228
|
-
export async function githubStatus ({ token, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
229
|
-
return call(fetchImpl, api, 'GET', '/v1/me/github', null, token)
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/** Where to send this person to connect GitHub (install Quilt's app on their repositories). */
|
|
233
|
-
export async function githubConnectUrl ({ token, repo = '', scope = '', id = '', api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
234
|
-
const r = await call(fetchImpl, api, 'POST', '/v1/github/connect', { ...(repo ? { repo } : {}), ...(scope ? { scope, id } : {}) }, token)
|
|
235
|
-
if (!r.url) throw new Error(BAD_REPLY)
|
|
236
|
-
return r.url
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** A workspace's GitHub (plan §2): its connections, access mode and repositories, and its org's switch. */
|
|
240
|
-
export async function workspaceGithub ({ token, id, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
241
|
-
return call(fetchImpl, api, 'GET', `/v1/workspaces/${encodeURIComponent(id)}/github`, null, token)
|
|
242
|
-
}
|
|
243
|
-
export async function setWorkspaceGithub ({ token, id, access, repos, agentCommits, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
244
|
-
// Only what is given changes: the access mode (with its repositories) and/or what agents may commit ('' for each session).
|
|
245
|
-
const body = { ...(access !== undefined ? { access, repos } : {}), ...(agentCommits !== undefined ? { agentCommits } : {}) }
|
|
246
|
-
return call(fetchImpl, api, 'PUT', `/v1/workspaces/${encodeURIComponent(id)}/github`, body, token)
|
|
247
|
-
}
|
|
248
|
-
export async function removeWorkspaceGithub ({ token, id, connectedBy = '', api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
249
|
-
return call(fetchImpl, api, 'DELETE', `/v1/workspaces/${encodeURIComponent(id)}/github/connection`, connectedBy ? { connectedBy } : {}, token)
|
|
250
|
-
}
|
|
251
|
-
export async function setOrgGithub ({ token, slug, requireGithub, agentCommits, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
252
|
-
const body = { ...(requireGithub !== undefined ? { requireGithub: !!requireGithub } : {}), ...(agentCommits !== undefined ? { agentCommits } : {}) }
|
|
253
|
-
return call(fetchImpl, api, 'PUT', `/v1/orgs/${encodeURIComponent(slug)}/github`, body, token)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
227
|
// Access types, grants and session invites, as this computer's account (see
|
|
257
228
|
// docs/superpowers/specs/2026-10-02-access-types-and-invites-design.md). Each throws with
|
|
258
229
|
// .status when the API says no (401 once the token is revoked).
|
|
@@ -98,7 +98,7 @@ function agoText (ts, now) {
|
|
|
98
98
|
* @param {string} [o.me]
|
|
99
99
|
* @param {number} [o.now]
|
|
100
100
|
*/
|
|
101
|
-
export function pickupBrief ({ task, history = [], claims = [], checklist = '', me = '', now = Date.now() }) {
|
|
101
|
+
export function pickupBrief ({ task, history = [], claims = [], checklist = '', me = '', now = Date.now(), workflow = TASK_WORKFLOW }) {
|
|
102
102
|
const files = task?.files || []
|
|
103
103
|
const lines = [`Picked up "${task?.title || 'this ticket'}" [${task?.id || ''}].`]
|
|
104
104
|
lines.push(files.length ? `Files: ${files.join(', ')}` : 'Files: none listed. Add them with quilt_assign_task once you know which you will touch.')
|
|
@@ -124,7 +124,7 @@ export function pickupBrief ({ task, history = [], claims = [], checklist = '',
|
|
|
124
124
|
lines.push('', 'Claims to respect (ask for the file with quilt_request_file instead of editing it):')
|
|
125
125
|
for (const c of others) lines.push(`- ${c.pattern} by ${c.by}${c.note ? ` (${c.note})` : ''}`)
|
|
126
126
|
}
|
|
127
|
-
lines.push('',
|
|
127
|
+
lines.push('', workflow, '', checklistBlock(checklist))
|
|
128
128
|
return lines.join('\n')
|
|
129
129
|
}
|
|
130
130
|
|
package/src/agents/common.js
CHANGED
|
@@ -19,3 +19,16 @@ export function toolLabel (client) {
|
|
|
19
19
|
if (/gemini/i.test(n)) return 'Other'
|
|
20
20
|
return n || 'AI agent'
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
// The tools a person can be shown as using.
|
|
24
|
+
export const KNOWN_TOOLS = new Set(['Claude Code', 'Cursor', 'Codex', 'xAI', 'Windsurf', 'Zed'])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The tool to show for a person: the one they set, unless the AIs connected through their app
|
|
28
|
+
* (MCP client names) are all another tool. A set tool can be a stale guess (Brandon showed as
|
|
29
|
+
* Windsurf while his Claude Code was connected); the AI that connects is what they really use.
|
|
30
|
+
*/
|
|
31
|
+
export function toolInUse (tool, agents = []) {
|
|
32
|
+
const used = [...agents].map(toolLabel).filter((t) => KNOWN_TOOLS.has(t))
|
|
33
|
+
return used.length && !used.includes(tool) ? used[used.length - 1] : tool
|
|
34
|
+
}
|
package/src/ai-vm.js
CHANGED
|
@@ -526,9 +526,17 @@ export class AiVm {
|
|
|
526
526
|
kind: 'agent',
|
|
527
527
|
passes: auth.passes,
|
|
528
528
|
identity: auth.identity,
|
|
529
|
+
// The accounts API (and Seam, through it) as this cloud AI, with its own key (seam-access.js).
|
|
530
|
+
apiAuth: auth.apiAuth,
|
|
529
531
|
agentFeed: false,
|
|
530
532
|
onLog: (line) => this.log(`[${room}] ${line}`),
|
|
531
|
-
|
|
533
|
+
// Stopped, not only forgotten: a session left running keeps its timers and the whole room in memory.
|
|
534
|
+
onFatal: (err) => {
|
|
535
|
+
this.log(`[${room}] stopped: ${err.message}`)
|
|
536
|
+
const r = this.runs.get(room)
|
|
537
|
+
this.runs.delete(room)
|
|
538
|
+
if (r) r.run.stop().catch(() => {})
|
|
539
|
+
}
|
|
532
540
|
})
|
|
533
541
|
this.runs.set(room, { run, link })
|
|
534
542
|
// What reaches it in the session counts as activity: the machine stays up while there is work,
|
package/src/api/activity.js
CHANGED
|
@@ -136,6 +136,9 @@ export function summarize ({ me, sessions, visits, now, tz = 'UTC' }) {
|
|
|
136
136
|
people.sort((a, b) => b.togetherMs - a.togetherMs || a.name.localeCompare(b.name))
|
|
137
137
|
const ownerRows = s.ownerAccount ? inRoom.filter((v) => v.account === s.ownerAccount) : []
|
|
138
138
|
const open = inRoom.some((v) => v.endedAt == null)
|
|
139
|
+
// Who is in it right now, among me and the people listed (an open visit).
|
|
140
|
+
const listed = new Set([me, ...people.map((p) => p.account)])
|
|
141
|
+
const here = [...new Set(inRoom.filter((v) => v.endedAt == null && listed.has(v.account)).map((v) => v.account))]
|
|
139
142
|
rows.push({
|
|
140
143
|
room: s.room,
|
|
141
144
|
name: s.name || '',
|
|
@@ -143,6 +146,8 @@ export function summarize ({ me, sessions, visits, now, tz = 'UTC' }) {
|
|
|
143
146
|
mine: !!s.ownerAccount && s.ownerAccount === me,
|
|
144
147
|
createdAt: s.createdAt,
|
|
145
148
|
lastActiveAt: open ? now : s.lastActiveAt,
|
|
149
|
+
live: open,
|
|
150
|
+
here,
|
|
146
151
|
myTotalMs: total(mine),
|
|
147
152
|
people
|
|
148
153
|
})
|
package/src/api/memory-store.js
CHANGED
|
@@ -30,7 +30,8 @@ export function createMemoryStore ({ now = Date.now } = {}) {
|
|
|
30
30
|
const orgGithub = new Map() // org id -> { requireGithub, updatedAt }
|
|
31
31
|
const teamGithub = new Map() // team id -> { agentCommits, updatedAt }
|
|
32
32
|
const settings = new Map() // the API's own settings (a webhook secret it made), key -> value
|
|
33
|
-
const leases = new Map() //
|
|
33
|
+
const leases = new Map() // key -> { room, holder, generation, heartbeatAt } (relay_leases)
|
|
34
|
+
const authSessions = new Map()
|
|
34
35
|
const users = new Map(); const orgs = new Map(); const roles = new Map(); const members = new Map()
|
|
35
36
|
const teams = new Map(); const teamMembers = new Map(); const invites = new Map(); const requests = new Map()
|
|
36
37
|
const agentInvites = new Map(); const keyRows = new Map(); const appKeys = new Map()
|
|
@@ -389,41 +390,19 @@ export function createMemoryStore ({ now = Date.now } = {}) {
|
|
|
389
390
|
},
|
|
390
391
|
|
|
391
392
|
// The address a person signs in with, and whether they've confirmed it.
|
|
392
|
-
async userEmail (userId) { const u = users.get(userId); return u ? {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
async
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
async
|
|
400
|
-
|
|
401
|
-
async workspaceGithub (workspaceId) { const w = workspaceGithub.get(workspaceId); return w ? { access: w.access, repos: [...w.repos], agentCommits: w.agentCommits || '' } : { access: 'open', repos: [], agentCommits: '' } },
|
|
402
|
-
// Only what is given changes: { access, repos } and/or { agentCommits } ('' for each session to decide).
|
|
403
|
-
async setWorkspaceGithub (workspaceId, { access, repos, agentCommits }) {
|
|
404
|
-
const w = workspaceGithub.get(workspaceId) || { access: 'open', repos: [], agentCommits: '' }
|
|
405
|
-
workspaceGithub.set(workspaceId, { access: access ?? w.access, repos: repos ? [...repos] : w.repos, agentCommits: agentCommits ?? w.agentCommits ?? '', updatedAt: now() })
|
|
406
|
-
},
|
|
407
|
-
// An org's GitHub switch (org_github): whether it requires GitHub.
|
|
408
|
-
async teamGithub (teamId) { const r = teamGithub.get(teamId); return { agentCommits: (r && r.agentCommits) || '' } },
|
|
409
|
-
async setTeamGithub (teamId, { agentCommits }) { teamGithub.set(teamId, { agentCommits: agentCommits || '', updatedAt: now() }) },
|
|
410
|
-
// The rules of the teams `memberId` is on: [{ teamId, name, agentCommits }], only those that set one.
|
|
411
|
-
async teamGitRules (memberId) {
|
|
412
|
-
return all(teamMembers, (tm) => tm.memberId === memberId)
|
|
413
|
-
.map((tm) => ({ team: teams.get(tm.teamId), rule: teamGithub.get(tm.teamId) }))
|
|
414
|
-
.filter((x) => x.team && x.rule && x.rule.agentCommits)
|
|
415
|
-
.map((x) => ({ teamId: x.team.id, name: x.team.name, agentCommits: x.rule.agentCommits }))
|
|
416
|
-
},
|
|
417
|
-
async orgGithub (orgId) { const o = orgGithub.get(orgId); return { requireGithub: !!(o && o.requireGithub), agentCommits: (o && o.agentCommits) || '' } },
|
|
418
|
-
async setOrgGithub (orgId, { requireGithub, agentCommits }) {
|
|
419
|
-
const o = orgGithub.get(orgId) || { requireGithub: false, agentCommits: '' }
|
|
420
|
-
orgGithub.set(orgId, { requireGithub: requireGithub ?? o.requireGithub, agentCommits: agentCommits ?? o.agentCommits ?? '', updatedAt: now() })
|
|
421
|
-
},
|
|
393
|
+
async userEmail (userId) { const u = users.get(userId); return u ? { email: u.email, confirmed: u.confirmed } : null },
|
|
394
|
+
// Email notices (notify.js): what each person turned on, and what was already sent (kept on the account).
|
|
395
|
+
setNotify (userId, notify) { const u = users.get(userId); if (u) u.notify = { ...notify } },
|
|
396
|
+
async notifyUsers () { return all(users, (u) => u.confirmed && u.notify).map((u) => ({ userId: [...users].find(([, x]) => x === u)[0], email: u.email, notify: { ...u.notify }, sent: { ...(u.notifySent || {}) } })) },
|
|
397
|
+
async setNotifySent (userId, sent) { const u = users.get(userId); if (u) u.notifySent = { ...sent } },
|
|
398
|
+
// A person's browser sign-ins (supabase-store.js: auth.sessions), for signing out one at a time.
|
|
399
|
+
addAuthSession (userId, s) { authSessions.set(s.id, { userId, createdAt: now(), lastActiveAt: now(), userAgent: '', ip: '', ...s }) },
|
|
400
|
+
async authSessions (userId) { return all(authSessions, (s) => s.userId === userId).sort((a, b) => b.lastActiveAt - a.lastActiveAt).map(({ userId: _, ...s }) => ({ ...s })) },
|
|
401
|
+
async endAuthSession (userId, id) { const s = authSessions.get(id); if (!s || s.userId !== userId) return false; authSessions.delete(id); return true },
|
|
422
402
|
// The API's own settings (app_settings): a value it keeps across starts.
|
|
423
403
|
async getSetting (key) { return settings.has(key) ? settings.get(key) : null },
|
|
424
|
-
//
|
|
425
|
-
async takeLease ({
|
|
426
|
-
const k = `${repo}\0${branch}`
|
|
404
|
+
// Relay leases (relay_leases; mirrors the SQL functions). `key`: the relay's name for what it holds.
|
|
405
|
+
async takeLease ({ key: k, room, holder = '', ttlMs }) {
|
|
427
406
|
const cur = leases.get(k)
|
|
428
407
|
const t = now()
|
|
429
408
|
if (!cur) { leases.set(k, { room, holder, generation: 1, heartbeatAt: t }); return { ok: true, generation: 1, room } }
|
|
@@ -433,15 +412,14 @@ export function createMemoryStore ({ now = Date.now } = {}) {
|
|
|
433
412
|
leases.set(k, { room, holder, generation: cur.generation + 1, heartbeatAt: t })
|
|
434
413
|
return { ok: true, generation: cur.generation + 1, room }
|
|
435
414
|
},
|
|
436
|
-
async heartbeatLease ({
|
|
437
|
-
const cur = leases.get(
|
|
415
|
+
async heartbeatLease ({ key: k, room, generation, ttlMs }) {
|
|
416
|
+
const cur = leases.get(k)
|
|
438
417
|
if (!cur || cur.room !== room || cur.generation !== generation) return { ok: false, lost: true, room: cur ? cur.room : '' }
|
|
439
418
|
if (cur.heartbeatAt < now() - ttlMs) return { ok: false, expired: true }
|
|
440
419
|
cur.heartbeatAt = now()
|
|
441
420
|
return { ok: true, generation }
|
|
442
421
|
},
|
|
443
|
-
async releaseLease ({
|
|
444
|
-
const k = `${repo}\0${branch}`
|
|
422
|
+
async releaseLease ({ key: k, room, generation }) {
|
|
445
423
|
const cur = leases.get(k)
|
|
446
424
|
if (cur && cur.room === room && cur.generation === generation) leases.delete(k)
|
|
447
425
|
return { ok: true }
|