heyquilt 0.3.90 → 0.3.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -11
- package/RELEASES.md +135 -2
- package/bin/quilt.js +45 -12
- package/package.json +3 -3
- package/src/account.js +28 -0
- package/src/agents/claude-code.js +11 -4
- package/src/agents/cursor-transcripts.js +11 -4
- package/src/ai-vm-signin.js +14 -2
- package/src/ai-vm.js +150 -8
- package/src/api/agent-auth.js +9 -3
- package/src/api/fly-machines.js +72 -15
- package/src/api/memory-store.js +142 -6
- package/src/api/relay-cluster.js +337 -43
- package/src/api/relay-machines.js +35 -3
- package/src/api/releases.js +87 -0
- package/src/api/routes/agents.js +111 -16
- package/src/api/routes/ai-vm.js +146 -19
- package/src/api/routes/orgs.js +3 -1
- package/src/api/routes/secrets.js +448 -0
- package/src/api/routes/workspaces.js +8 -1
- package/src/api/sandbox-keys.js +57 -0
- package/src/api/secrets-access.js +93 -0
- package/src/api/server.js +40 -9
- package/src/api/supabase-store.js +121 -11
- package/src/autowork.js +3 -15
- package/src/catchup.js +1 -1
- package/src/clash.js +10 -22
- package/src/control.js +18 -15
- package/src/fsutil.js +11 -1
- package/src/gitstate.js +29 -5
- package/src/hooks.js +1 -1
- package/src/mcp-proxy.js +7 -2
- package/src/mcp.js +22 -25
- package/src/merge3.js +94 -12
- package/src/pass-source.js +9 -2
- package/src/pathrules.js +7 -0
- package/src/relay-clash.js +101 -484
- package/src/relay-githttp.js +36 -7
- package/src/relay-mcp.js +19 -7
- package/src/relay-metrics.js +66 -1
- package/src/relay-outbound.js +154 -25
- package/src/relay-sync.js +133 -136
- package/src/relay-turn-sync.js +250 -51
- package/src/relay-upstream.js +49 -179
- package/src/releases.js +16 -9
- package/src/runner.js +3 -2
- package/src/secret-import.js +55 -0
- package/src/secret-names.js +26 -0
- package/src/secret-run.js +80 -0
- package/src/secrets-client.js +56 -0
- package/src/server.js +71 -44
- package/src/session.js +347 -262
- package/src/status.js +3 -5
- package/src/sync-words.js +1 -1
- package/src/turn-sync-plan.js +13 -10
- package/src/turn-sync.js +32 -11
- package/src/ui/agent-guide.js +4 -4
- package/src/ui/app.css +208 -23
- package/src/ui/app.js +4 -0
- package/src/ui/cloud-ai-model.js +3 -2
- package/src/ui/common.js +2 -0
- package/src/ui/fileview.js +28 -1
- package/src/ui/home.js +32 -33
- package/src/ui/loom.css +18 -2
- package/src/ui/loom.js +41 -8
- package/src/ui/rail-model.js +6 -9
- package/src/ui/releases.js +1 -1
- package/src/ui/sandbox-keys-model.js +53 -0
- package/src/ui/sandbox-keys.js +132 -0
- package/src/ui/secret-rules.js +255 -0
- package/src/ui/secrets-view.js +162 -0
- package/src/ui/secrets.js +283 -0
- package/src/ui/session.js +47 -149
- package/src/ui/tree.js +23 -18
- package/src/ui/workspaces.js +5 -0
- package/src/ui-server.js +140 -28
- package/src/update-check.js +1 -1
- package/src/upstream.js +75 -38
- package/src/vault/crypto.js +96 -0
- package/src/vault/kms.js +61 -0
- package/src/vault/main.js +33 -0
- package/src/vault/server.js +279 -0
- package/src/vault/sigv4.js +38 -0
- package/src/ui/merges.js +0 -158
package/src/mcp-proxy.js
CHANGED
|
@@ -6,9 +6,11 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
|
6
6
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
7
7
|
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
|
8
8
|
import { hostedClient } from './hosted-client.js'
|
|
9
|
+
import { secretNames } from './secrets-client.js'
|
|
10
|
+
import { describeSecretNames, SECRET_NAMES_DESCRIPTION, RUN_GUIDE } from './secret-names.js'
|
|
9
11
|
|
|
10
12
|
const NOTE = (room) => `\n\nYou reach Quilt with this sandbox's QUILT_AGENT_KEY: every tool is about session ${room}, filled in for you. ` +
|
|
11
|
-
'Your folder is synced by `npx heyquilt sync`: run it at the start of every turn (it brings in what changed and says what waits for you) and again at the end (it sends what you changed).'
|
|
13
|
+
'Your folder is synced by `npx heyquilt sync`: run it at the start of every turn (it brings in what changed and says what waits for you) and again at the end (it sends what you changed). ' + RUN_GUIDE
|
|
12
14
|
|
|
13
15
|
/** The tools as the hosted MCP lists them, without `session` (the proxy fills it in). */
|
|
14
16
|
export function withoutSession (tools) {
|
|
@@ -27,9 +29,12 @@ export async function runMcpProxy ({ key, client = hostedClient({ key }), transp
|
|
|
27
29
|
instructions = (init && init.instructions) || ''
|
|
28
30
|
} catch {} // the first tool call says what is wrong (a revoked key, no network)
|
|
29
31
|
const server = new Server({ name: 'quilt', version: '0.2.0' }, { capabilities: { tools: {} }, instructions: instructions + NOTE(key.room) })
|
|
30
|
-
|
|
32
|
+
// quilt_secret_names is answered here, as this agent with its key: a sandbox runs commands
|
|
33
|
+
// (through `quilt run`), unlike the hosted agents the relay's tool speaks to.
|
|
34
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: withoutSession((await client.rpc('tools/list', {})).tools).map((t) => (t.name === 'quilt_secret_names' ? { ...t, description: SECRET_NAMES_DESCRIPTION } : t)) }))
|
|
31
35
|
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
32
36
|
try {
|
|
37
|
+
if (req.params.name === 'quilt_secret_names') return { content: [{ type: 'text', text: describeSecretNames(await secretNames({ api: key.api, token: key.key }, key.room)) }] }
|
|
33
38
|
return await client.callRaw(req.params.name, req.params.arguments || {})
|
|
34
39
|
} catch (e) {
|
|
35
40
|
return { content: [{ type: 'text', text: e.message }], isError: true }
|
package/src/mcp.js
CHANGED
|
@@ -39,6 +39,7 @@ import { UpdateCheck } from './update-check.js'
|
|
|
39
39
|
import { getSettings } from './settings.js'
|
|
40
40
|
import { mergeAction } from './merges.js'
|
|
41
41
|
import { CLASH_DESCRIPTION, clashSchema } from './relay-clash.js'
|
|
42
|
+
import { describeSecretNames, SECRET_NAMES_DESCRIPTION, RUN_GUIDE } from './secret-names.js'
|
|
42
43
|
|
|
43
44
|
// The board's columns, from tasks.js, so this and the hosted MCP (relay-mcp.js) never drift apart.
|
|
44
45
|
const COLUMN_IDS_LIST = COLUMNS.map((c) => c.id)
|
|
@@ -94,12 +95,11 @@ export const MCP_INSTRUCTIONS =
|
|
|
94
95
|
'When you finish a piece of work, call quilt_set_work with "done": it lets go of the files claimed for you. ' +
|
|
95
96
|
'Claim ahead (quilt_claim) only for a larger change across several files. ' +
|
|
96
97
|
'Always re-read a file right before you edit it. ' +
|
|
97
|
-
'If quilt_status lists merges to settle, read quilt_merges before editing those files. ' +
|
|
98
98
|
'If quilt_status starts with While you were away, the session changed since this folder last synced: read quilt_history for those files before editing them. ' +
|
|
99
99
|
'Mentions of you (@yourname) in chat, direct messages to you and tasks handed to you wait in quilt_inbox: read it when you start, and act on each one. ' +
|
|
100
100
|
'Each comes with the conversation before it with its sender; when a message refers to something earlier you do not have, read back with quilt_conversation before you answer, never guess. ' +
|
|
101
101
|
'Git works here as usual: commit only the files you changed and push yourself. When the relay serves the session\'s git, your folder\'s origin is the relay: git signs in with your Quilt pass, and the relay checks each push (the session owner\'s policy, never a force-push, no ignored files) and puts it on GitHub, replayed onto GitHub\'s head if it moved (your folder follows). Otherwise your folder signs in to GitHub through Quilt (a short-lived token for this repository). Either way pushes stay within the owner\'s policy: by default branches of your own. Taking a task (quilt_move_task to doing) puts your folder on quilt/<you>/<task>; push there and Quilt opens the pull request for the task; move the task to QA when it is done and tested; Done merges the pull request once its checks pass (a person moves it to Done unless the owner lets agents push to any branch). When agents may not commit, ask a person with quilt_request_commit. ' +
|
|
102
|
-
'When a branch syncs with GitHub (quilt_branches says so), pushes and merges on GitHub come into the session by themselves; nothing goes to GitHub until someone commits and pushes. A file GitHub and the session both changed is
|
|
102
|
+
'When a branch syncs with GitHub (quilt_branches says so), pushes and merges on GitHub come into the session by themselves; nothing goes to GitHub until someone commits and pushes. A file GitHub and the session both changed is merged by Quilt on its own (the newer edit stays where both changed the same lines). Releasing a file only lets go of your claim: it publishes nothing by itself. ' +
|
|
103
103
|
'Work only in the session has not shipped: push before you move a task to QA. Pushes and merges on GitHub reach the session within seconds: never pull by hand. ' +
|
|
104
104
|
'When you are about to answer someone in chat, quilt_typing shows them you are typing; sending clears it. ' +
|
|
105
105
|
'Chat: ' + CHAT_RULES.replace(/^Send a chat message\. /, '') + ' A message that needs nothing back is settled with quilt_inbox (no_reply: [its id]), not answered. ' +
|
|
@@ -107,6 +107,7 @@ export const MCP_INSTRUCTIONS =
|
|
|
107
107
|
'You are a member of your own in the session, apart from your person and their other AI sessions, named "<their first name> · <label>" after your work (your git branch, or what you first say you are doing); rename yourself with quilt_name_session. Your messages, inbox, claims and duties are your own. People see all of a person\'s AI sessions as one, "<person>\'s AI" ("Daniel\'s AI"): write to another person\'s AI by that name, and what is written to your person\'s AI reaches whichever of their sessions was active last (with none of them live, their cloud AI, if they connected one: Quilt runs their own AI tool for them on their own cloud VM). ' +
|
|
108
108
|
'Share what you are doing with quilt_share: when you start on a request (request and your plan) and when you finish (what you did and the files you changed). Partners see it in their feed, it puts your work on the task board, and it tells the host not to commit under you. ' +
|
|
109
109
|
'When Claude Code is started with the quilt channel, they arrive on their own as <channel source="quilt"> events while you work: treat each like a request from that person, answer with quilt_message, and take a task with quilt_move_task. ' +
|
|
110
|
+
RUN_GUIDE + ' ' +
|
|
110
111
|
TASK_WORKFLOW
|
|
111
112
|
|
|
112
113
|
export async function runMcp () {
|
|
@@ -439,39 +440,29 @@ export async function runMcp () {
|
|
|
439
440
|
return n ? `Withdrew your request for ${rel}.` : `You had not asked for ${rel}.`
|
|
440
441
|
}))
|
|
441
442
|
|
|
442
|
-
|
|
443
|
+
// Quilt merges on its own (session.js autoSettle): these say what it did, and what waits for a holder.
|
|
443
444
|
const mergeLine = (m, me) => {
|
|
444
445
|
const who = m.by === me ? 'you' : m.by
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
: m.oursDeleted ? `${who} ${did(m, true)} and ${other} changed it in the session`
|
|
449
|
-
: m.theirsHash === null ? `${who} ${did(m)} but it was deleted in the session`
|
|
450
|
-
: `${who} ${did(m)} and ${other} changed it in the session`
|
|
451
|
-
return `- \`${m.path}\` (id ${m.id}, ${m.state}): ${what}${m.reason ? ` — ${m.reason}` : ''}`
|
|
446
|
+
const what = m.state !== 'done' ? `${who} ${mergeAction(m)}; ${m.claimedBy || 'someone'} has it claimed, and it goes in, merged by Quilt, when they let go`
|
|
447
|
+
: m.reason || 'merged by Quilt'
|
|
448
|
+
return `- \`${m.path}\`: ${what}`
|
|
452
449
|
}
|
|
453
450
|
|
|
454
451
|
server.registerTool('quilt_merges', {
|
|
455
|
-
description: '
|
|
452
|
+
description: 'What Quilt merged on its own recently: files changed in two places (offline, by a pull or on GitHub, and in the session). Quilt combines them itself; where both changed the same lines the newer edit stays and the other lines are kept in the file\'s history. Nothing here is for you to settle.',
|
|
456
453
|
inputSchema: {}
|
|
457
454
|
}, () => withDaemon(async (d) => {
|
|
458
455
|
const { merges } = await call(d, 'GET', '/merges')
|
|
459
456
|
const me = (await call(d, 'GET', '/info')).name
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
return `Merges to settle:\n${open.map((m) => mergeLine(m, me)).join('\n')}`
|
|
457
|
+
if (!merges.length) return 'Quilt has merged nothing lately.'
|
|
458
|
+
return `Quilt merges on its own; nothing to do:\n${merges.map((m) => mergeLine(m, me)).join('\n')}`
|
|
463
459
|
}))
|
|
464
460
|
|
|
461
|
+
// Kept for guides and prompts from before Quilt merged on its own.
|
|
465
462
|
server.registerTool('quilt_resolve_merge', {
|
|
466
|
-
description: '
|
|
467
|
-
inputSchema: {
|
|
468
|
-
|
|
469
|
-
how: z.enum(['agent', 'mine', 'theirs', 'review']).describe('How it was settled')
|
|
470
|
-
}
|
|
471
|
-
}, ({ id, how }) => withDaemon(async (d) => {
|
|
472
|
-
const r = await call(d, 'POST', '/merges/resolve', { id, how })
|
|
473
|
-
return `Settled the merge of ${r.path} (${how}).`
|
|
474
|
-
}, { gate: gateFor('quilt_resolve_merge') }))
|
|
463
|
+
description: 'Not needed any more: Quilt merges on its own. Kept so older instructions get an answer.',
|
|
464
|
+
inputSchema: { id: z.string().optional(), how: z.string().optional() }
|
|
465
|
+
}, () => ({ content: [{ type: 'text', text: 'Quilt merges on its own now: there is nothing to settle.' }] }))
|
|
475
466
|
|
|
476
467
|
server.registerTool('quilt_clash', {
|
|
477
468
|
description: CLASH_DESCRIPTION,
|
|
@@ -646,7 +637,7 @@ export async function runMcp () {
|
|
|
646
637
|
description: 'Download a file someone shared in chat. Received files are normally saved automatically (see quilt_read_messages); use this to fetch one again or save it into the project.',
|
|
647
638
|
inputSchema: {
|
|
648
639
|
id: z.string().describe('The message id shown next to the file'),
|
|
649
|
-
dest: z.string().optional().describe('Destination path or folder inside the project (default:
|
|
640
|
+
dest: z.string().optional().describe('Destination path or folder inside the project (default: the "Chat files" folder)')
|
|
650
641
|
}
|
|
651
642
|
}, ({ id, dest }) => withDaemon(async (d) => {
|
|
652
643
|
const r = await call(d, 'POST', '/get', { id, dest: dest ? insideProject(d.dir, dest) : undefined })
|
|
@@ -961,7 +952,7 @@ export async function runMcp () {
|
|
|
961
952
|
}, () => withDaemon(async (d) => { const r = await call(d, 'GET', '/sync-status'); return syncStatusText(r.branches || [], { mine: r.mine || '' }) }))
|
|
962
953
|
|
|
963
954
|
server.registerTool('quilt_sync_branch', {
|
|
964
|
-
description: 'Bring commits made outside the session into it now: fetches this folder\'s upstream (say origin/main after a PR merged, or a push from a worktree) and moves the branch forward, with the session\'s uncommitted work merged in. Quilt also does this by itself about once a minute; call it right after you push or merge elsewhere. It says when the fetch fails (git has no credentials here, say). A folder that is not a git clone, or can\'t fetch, asks the relay to bring the commits in from GitHub instead.
|
|
955
|
+
description: 'Bring commits made outside the session into it now: fetches this folder\'s upstream (say origin/main after a PR merged, or a push from a worktree) and moves the branch forward, with the session\'s uncommitted work merged in. Quilt also does this by itself about once a minute; call it right after you push or merge elsewhere. It says when the fetch fails (git has no credentials here, say). A folder that is not a git clone, or can\'t fetch, asks the relay to bring the commits in from GitHub instead. Files that both the commits and the session changed are merged by Quilt on its own.',
|
|
965
956
|
inputSchema: {}
|
|
966
957
|
}, () => withDaemon(async (d) => describeBranchSync(await call(d, 'POST', '/branches/sync'))))
|
|
967
958
|
|
|
@@ -1004,6 +995,11 @@ export async function runMcp () {
|
|
|
1004
995
|
inputSchema: {}
|
|
1005
996
|
}, () => withDaemon(async (d) => (await call(d, 'GET', '/rules')).text))
|
|
1006
997
|
|
|
998
|
+
server.registerTool('quilt_secret_names', {
|
|
999
|
+
description: SECRET_NAMES_DESCRIPTION,
|
|
1000
|
+
inputSchema: {}
|
|
1001
|
+
}, () => withDaemon(async (d) => describeSecretNames({ ...(await call(d, 'GET', '/secrets/names')), hosted: false })))
|
|
1002
|
+
|
|
1007
1003
|
server.registerTool('quilt_session_info', {
|
|
1008
1004
|
description: 'Where the shared project lives on disk, how you appear to others, who is online, and the invite link.',
|
|
1009
1005
|
inputSchema: {}
|
|
@@ -1075,6 +1071,7 @@ export async function runMcp () {
|
|
|
1075
1071
|
const notes = []
|
|
1076
1072
|
if (f.edited && now - f.edited.ts < 10 * 60 * 1000) notes.push(`edited by ${f.edited.by === me ? 'you' : f.edited.by} ${Math.round((now - f.edited.ts) / 1000)}s ago`)
|
|
1077
1073
|
if (f.claim) notes.push(`claimed by ${f.claim.by === me ? 'you' : f.claim.by}${f.claim.note ? `: ${f.claim.note}` : ''}${f.claim.queue && f.claim.queue.length ? `, ${f.claim.queue.length} waiting` : ''}`)
|
|
1074
|
+
if (f.chat) notes.push(`sent in chat by ${f.chat.by === me ? 'you' : f.chat.by}: in your folder only, never synced or committed`)
|
|
1078
1075
|
if (f.binary) notes.push('binary')
|
|
1079
1076
|
return `${f.path}${notes.length ? ` [${notes.join('; ')}]` : ''}`
|
|
1080
1077
|
})
|
package/src/merge3.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// Three-way merge of text, line by line, the way git does it: changes to
|
|
2
2
|
// different lines combine on their own; the same lines changed on both sides
|
|
3
|
-
// are a conflict. Quilt
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import { diff3Merge } from 'node-diff3'
|
|
3
|
+
// are a conflict. Quilt settles those itself (mergeAuto): nobody is asked to merge
|
|
4
|
+
// (docs/superpowers/specs/2026-10-18-automatic-merges.md).
|
|
5
|
+
import { diff3Merge, diffIndices } from 'node-diff3'
|
|
7
6
|
|
|
8
7
|
export const MARK = { start: '<<<<<<< mine', mid: '=======', end: '>>>>>>> session' }
|
|
9
8
|
|
|
@@ -121,20 +120,103 @@ export function mergeSmart (base, ours, theirs, { union = false } = {}) {
|
|
|
121
120
|
for (const r of regions) {
|
|
122
121
|
if (r.ok) { out.push(...r.ok); continue }
|
|
123
122
|
const c = r.conflict
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (both) { out.push(...both); continue }
|
|
127
|
-
if (sameIgnoringWs(c.a, c.b)) { out.push(...c.a); step('whitespace'); continue } // the same change, spaced differently
|
|
128
|
-
if (sameIgnoringWs(c.a, c.o)) { out.push(...c.b); step('whitespace'); continue } // ours only reindented
|
|
129
|
-
if (sameIgnoringWs(c.b, c.o)) { out.push(...c.a); step('whitespace'); continue } // theirs only reindented
|
|
130
|
-
const words = mergeWords(c.o, c.a, c.b)
|
|
131
|
-
if (words) { out.push(...words); step('words'); continue }
|
|
123
|
+
const settled = ladder(c, { union })
|
|
124
|
+
if (settled) { out.push(...settled.lines); step(settled.how); continue }
|
|
132
125
|
conflicts.push({ base: c.o, ours: c.a, theirs: c.b })
|
|
133
126
|
out.push(...c.a)
|
|
134
127
|
}
|
|
135
128
|
return { text: out.join('\n'), conflicts, how }
|
|
136
129
|
}
|
|
137
130
|
|
|
131
|
+
/** mergeSmart's steps for one region both sides changed ({ o, a, b }): { lines, how }, or null when none settles it. */
|
|
132
|
+
function ladder (c, { union = false } = {}) {
|
|
133
|
+
if (union) return { lines: [...c.a, ...c.b], how: 'union' }
|
|
134
|
+
const both = c.o.length === 0 ? addedBoth(c.a, c.b) : sameImport(c.o, c.a, c.b)
|
|
135
|
+
if (both) return { lines: both, how: 'lines' }
|
|
136
|
+
if (sameIgnoringWs(c.a, c.b)) return { lines: c.a, how: 'whitespace' } // the same change, spaced differently
|
|
137
|
+
if (sameIgnoringWs(c.a, c.o)) return { lines: c.b, how: 'whitespace' } // ours only reindented
|
|
138
|
+
if (sameIgnoringWs(c.b, c.o)) return { lines: c.a, how: 'whitespace' } // theirs only reindented
|
|
139
|
+
const words = mergeWords(c.o, c.a, c.b)
|
|
140
|
+
if (words) return { lines: words, how: 'words' }
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* How Quilt combines two versions on its own: mergeSmart's steps, then each region both sides
|
|
146
|
+
* changed differently goes to the newer side (`oursNewer`). `lost` lists the lines that lost:
|
|
147
|
+
* { side: 'ours' | 'theirs', lines, kept: the lines that won, at: their first line in `text` },
|
|
148
|
+
* for the file's history and the notice to whoever wrote them.
|
|
149
|
+
*/
|
|
150
|
+
export function mergeAuto (base, ours, theirs, { oursNewer = false, union = false } = {}) {
|
|
151
|
+
const regions = diff3Merge(lines(ours), lines(base), lines(theirs), { excludeFalseConflicts: true })
|
|
152
|
+
const out = []
|
|
153
|
+
const lost = []
|
|
154
|
+
const newer = (a, b, at) => {
|
|
155
|
+
const [won, lose, side] = oursNewer ? [a, b, 'theirs'] : [b, a, 'ours']
|
|
156
|
+
lost.push({ side, lines: lose, kept: won, at })
|
|
157
|
+
return won
|
|
158
|
+
}
|
|
159
|
+
for (const r of regions) {
|
|
160
|
+
if (r.ok) { out.push(...r.ok); continue }
|
|
161
|
+
const c = r.conflict
|
|
162
|
+
const settled = ladder(c, { union })
|
|
163
|
+
if (settled) { out.push(...settled.lines); continue }
|
|
164
|
+
// Changes next to each other, not over each other (one side edits a line, the other adds one
|
|
165
|
+
// after it): both go in.
|
|
166
|
+
const side = sideBySide(c.o, c.a, c.b)
|
|
167
|
+
if (side) { out.push(...side); continue }
|
|
168
|
+
// The same lines on all three sides (changes next to each other): line by line, so a line only
|
|
169
|
+
// one side changed keeps that change.
|
|
170
|
+
if (c.o.length === c.a.length && c.o.length === c.b.length) {
|
|
171
|
+
for (let i = 0; i < c.o.length; i++) {
|
|
172
|
+
const [o, a, b] = [c.o[i], c.a[i], c.b[i]]
|
|
173
|
+
if (a === b || b === o) out.push(a)
|
|
174
|
+
else if (a === o) out.push(b)
|
|
175
|
+
else out.push(...newer([a], [b], out.length))
|
|
176
|
+
}
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
out.push(...newer(c.a, c.b, out.length))
|
|
180
|
+
}
|
|
181
|
+
return { text: out.join('\n'), lost }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Whether two hunks (one per side) change the same base lines, or one adds lines inside the other's. */
|
|
185
|
+
function overlaps (x, y) {
|
|
186
|
+
if (x.len && y.len) return x.at < y.at + y.len && y.at < x.at + x.len
|
|
187
|
+
if (!x.len && !y.len) return x.at === y.at // both add at one place: which goes first would be a guess
|
|
188
|
+
const [add, change] = x.len ? [y, x] : [x, y]
|
|
189
|
+
return add.at > change.at && add.at < change.at + change.len
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* One region both sides changed, when their edits only touch: each side's edits from base
|
|
194
|
+
* (diffIndices hunks) are applied together, in base order, when no base line is changed by both,
|
|
195
|
+
* and no line is added where the other side changes lines. Returns the lines, or null.
|
|
196
|
+
*/
|
|
197
|
+
function sideBySide (o, a, b) {
|
|
198
|
+
const hunks = [
|
|
199
|
+
...diffIndices(o, a).map((h) => ({ at: h.buffer1[0], len: h.buffer1[1], lines: h.buffer2Content, side: 0 })),
|
|
200
|
+
...diffIndices(o, b).map((h) => ({ at: h.buffer1[0], len: h.buffer1[1], lines: h.buffer2Content, side: 1 }))
|
|
201
|
+
].sort((x, y) => x.at - y.at || x.len - y.len || x.side - y.side)
|
|
202
|
+
for (let i = 0; i < hunks.length; i++) {
|
|
203
|
+
for (let j = i + 1; j < hunks.length; j++) {
|
|
204
|
+
const [x, y] = [hunks[i], hunks[j]]
|
|
205
|
+
if (x.side === y.side) continue
|
|
206
|
+
if (overlaps(x, y)) return null
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
const out = []
|
|
210
|
+
let pos = 0
|
|
211
|
+
for (const h of hunks) {
|
|
212
|
+
out.push(...o.slice(pos, h.at))
|
|
213
|
+
out.push(...h.lines)
|
|
214
|
+
pos = Math.max(pos, h.at + h.len)
|
|
215
|
+
}
|
|
216
|
+
out.push(...o.slice(pos))
|
|
217
|
+
return out
|
|
218
|
+
}
|
|
219
|
+
|
|
138
220
|
/** The paths a .gitattributes text marks merge=union, as a matcher: (rel) => boolean. */
|
|
139
221
|
export function unionMatcher (texts) {
|
|
140
222
|
const patterns = []
|
package/src/pass-source.js
CHANGED
|
@@ -165,12 +165,19 @@ export function agentPasses ({ name, dir, fetch: fetchImpl = globalThis.fetch, n
|
|
|
165
165
|
* this computer's account. `name` is the name saved on this computer, so a
|
|
166
166
|
* session can start before (or without, when offline) its first pass.
|
|
167
167
|
*/
|
|
168
|
+
/** The accounts API as this computer's account, signed in now (a sign-in since is picked up). */
|
|
169
|
+
export async function personApiAuth () {
|
|
170
|
+
const account = readAccount()
|
|
171
|
+
if (!account) throw new Error(NOT_SIGNED_IN)
|
|
172
|
+
return { api: apiUrl(), token: account.token }
|
|
173
|
+
}
|
|
174
|
+
|
|
168
175
|
export function sessionPasses ({ agent = null, dir } = {}) {
|
|
169
176
|
if (agent) {
|
|
170
177
|
const saved = readAgent({ name: agent, dir })
|
|
171
|
-
return { passes: agentPasses({ name: agent, dir }), identity: saved.identity, kind: 'agent', name: saved.name || agent }
|
|
178
|
+
return { passes: agentPasses({ name: agent, dir }), identity: saved.identity, kind: 'agent', name: saved.name || agent, apiAuth: async () => { const s = await agentAccess({ name: agent, dir }); return { api: s.api, token: s.accessKey } } }
|
|
172
179
|
}
|
|
173
180
|
const account = readAccount()
|
|
174
181
|
if (!account) throw new Error(NOT_SIGNED_IN)
|
|
175
|
-
return { passes: personPasses({ token: account.token }), identity: null, kind: 'human', name: account.account.name || null }
|
|
182
|
+
return { passes: personPasses({ token: account.token }), identity: null, kind: 'human', name: account.account.name || null, apiAuth: personApiAuth }
|
|
176
183
|
}
|
package/src/pathrules.js
CHANGED
|
@@ -9,11 +9,18 @@ export const LARGE_FILE_BYTES = 256 * 1024
|
|
|
9
9
|
// The most a stored file may be (the relay may allow less).
|
|
10
10
|
export const MAX_STORED_BINARY_BYTES = 100 * 1024 * 1024
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Where each person's app saves the files sent in the session's chat (session.js): only those
|
|
14
|
+
* who can see the message get the file, so the folder is never synced, committed or pushed.
|
|
15
|
+
*/
|
|
16
|
+
export const CHAT_FILES = 'Chat files'
|
|
17
|
+
|
|
12
18
|
// Never synced, regardless of .gitignore. .env files are excluded so secrets
|
|
13
19
|
// stay on each person's machine; Claude Code worktrees are whole private
|
|
14
20
|
// copies of the project.
|
|
15
21
|
export const ALWAYS_IGNORED = [
|
|
16
22
|
'.git', '.quilt', '.cowove', 'node_modules', '.DS_Store', 'Thumbs.db', '.claude/worktrees',
|
|
23
|
+
`/${CHAT_FILES}`,
|
|
17
24
|
// Each person's own Claude Code settings (permissions, Quilt's hooks) stay on their machine.
|
|
18
25
|
'.claude/settings.local.json',
|
|
19
26
|
// Build output and caches: large, machine-specific, and rebuilt by each person.
|