heyquilt 0.0.1 → 0.3.90
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 +85 -0
- package/README.md +630 -2
- package/RELEASES.md +733 -0
- package/assets/logo.svg +1 -0
- package/bin/quilt.js +816 -0
- package/desktop/main.js +315 -0
- package/package.json +163 -7
- package/src/account.js +563 -0
- package/src/admit-policy.js +32 -0
- package/src/agent-join.js +370 -0
- package/src/agent-task-workflow.js +176 -0
- package/src/agents/actions.js +85 -0
- package/src/agents/claude-code.js +180 -0
- package/src/agents/common.js +21 -0
- package/src/agents/cursor-transcripts.js +170 -0
- package/src/agents/cursor.js +404 -0
- package/src/agents/index.js +69 -0
- package/src/ai-vm-signin.js +98 -0
- package/src/ai-vm.js +380 -0
- package/src/api/access.js +75 -0
- package/src/api/activity.js +181 -0
- package/src/api/agent-auth.js +130 -0
- package/src/api/agent-placement.js +157 -0
- package/src/api/audit.js +83 -0
- package/src/api/auth.js +17 -0
- package/src/api/domains.js +30 -0
- package/src/api/file-paths.js +40 -0
- package/src/api/file-store.js +99 -0
- package/src/api/fly-machines.js +76 -0
- package/src/api/github-app.js +172 -0
- package/src/api/http.js +34 -0
- package/src/api/invite-email.js +53 -0
- package/src/api/issues.js +110 -0
- package/src/api/join-text.js +78 -0
- package/src/api/mailer.js +47 -0
- package/src/api/memory-store.js +1083 -0
- package/src/api/org-access.js +54 -0
- package/src/api/permissions.js +71 -0
- package/src/api/relay-cluster.js +251 -0
- package/src/api/relay-machines.js +60 -0
- package/src/api/routes/access-types.js +54 -0
- package/src/api/routes/agent-invites.js +115 -0
- package/src/api/routes/agents.js +157 -0
- package/src/api/routes/ai-vm.js +369 -0
- package/src/api/routes/github.js +380 -0
- package/src/api/routes/grants.js +70 -0
- package/src/api/routes/invites.js +188 -0
- package/src/api/routes/issues.js +47 -0
- package/src/api/routes/join.js +167 -0
- package/src/api/routes/members.js +110 -0
- package/src/api/routes/orgs.js +177 -0
- package/src/api/routes/relay-cluster.js +90 -0
- package/src/api/routes/relay.js +76 -0
- package/src/api/routes/session-invites.js +119 -0
- package/src/api/routes/sessions.js +92 -0
- package/src/api/routes/teams.js +132 -0
- package/src/api/routes/workspace-agents.js +332 -0
- package/src/api/routes/workspace-files.js +277 -0
- package/src/api/routes/workspace-invites.js +196 -0
- package/src/api/routes/workspaces.js +225 -0
- package/src/api/server.js +623 -0
- package/src/api/slugs.js +20 -0
- package/src/api/supabase-store.js +765 -0
- package/src/api/team-access.js +30 -0
- package/src/api/tokens.js +31 -0
- package/src/api/workspace-access.js +62 -0
- package/src/api/workspace-reach.js +60 -0
- package/src/autowork-tools.js +166 -0
- package/src/autowork.js +387 -0
- package/src/blobstore.js +99 -0
- package/src/boxes.js +37 -0
- package/src/branchdocs.js +235 -0
- package/src/branches.js +194 -0
- package/src/catchup.js +111 -0
- package/src/chat-archive.js +85 -0
- package/src/chat-links.js +923 -0
- package/src/clash.js +140 -0
- package/src/cloud-ai.js +50 -0
- package/src/commit-request.js +59 -0
- package/src/commit.js +213 -0
- package/src/connection.js +569 -0
- package/src/control.js +245 -0
- package/src/conversation.js +125 -0
- package/src/doctor.js +156 -0
- package/src/duties.js +303 -0
- package/src/editors.js +228 -0
- package/src/fsutil.js +87 -0
- package/src/git-hook.js +48 -0
- package/src/git.js +147 -0
- package/src/github-commit.js +320 -0
- package/src/gitignore.js +49 -0
- package/src/gitstate.js +900 -0
- package/src/history.js +307 -0
- package/src/hooks.js +371 -0
- package/src/hosted-agents.js +24 -0
- package/src/hosted-client.js +112 -0
- package/src/identity.js +99 -0
- package/src/inbox.js +237 -0
- package/src/integrations.js +221 -0
- package/src/key-cli.js +115 -0
- package/src/largefiles.js +91 -0
- package/src/legacy.js +25 -0
- package/src/mcp-proxy.js +40 -0
- package/src/mcp-room.js +32 -0
- package/src/mcp.js +1261 -0
- package/src/merge3.js +158 -0
- package/src/merges.js +148 -0
- package/src/pass-source.js +176 -0
- package/src/passes.js +64 -0
- package/src/pathrules.js +127 -0
- package/src/persona.js +101 -0
- package/src/presence.js +360 -0
- package/src/private-file.js +41 -0
- package/src/procs.js +70 -0
- package/src/project-rules.js +178 -0
- package/src/protocol.js +92 -0
- package/src/quiet-warnings.js +9 -0
- package/src/relay-ai-vm.js +82 -0
- package/src/relay-ask.js +141 -0
- package/src/relay-ci.js +111 -0
- package/src/relay-clash.js +597 -0
- package/src/relay-cluster.js +454 -0
- package/src/relay-commit.js +887 -0
- package/src/relay-filekey.js +46 -0
- package/src/relay-git.js +274 -0
- package/src/relay-githttp.js +137 -0
- package/src/relay-github.js +109 -0
- package/src/relay-lease.js +137 -0
- package/src/relay-lfs.js +37 -0
- package/src/relay-mcp.js +1679 -0
- package/src/relay-metrics.js +75 -0
- package/src/relay-outbound.js +1144 -0
- package/src/relay-sync.js +719 -0
- package/src/relay-turn-sync.js +307 -0
- package/src/relay-upstream.js +934 -0
- package/src/relay-webhooks.js +111 -0
- package/src/releases.js +131 -0
- package/src/report.js +129 -0
- package/src/runner.js +306 -0
- package/src/same-edit.js +18 -0
- package/src/server.js +4187 -0
- package/src/session-access.js +170 -0
- package/src/session-name.js +15 -0
- package/src/session.js +7361 -0
- package/src/settings.js +46 -0
- package/src/setup.js +184 -0
- package/src/shell-path.js +46 -0
- package/src/status.js +199 -0
- package/src/summarize.js +109 -0
- package/src/sync-words.js +42 -0
- package/src/task-comments.js +92 -0
- package/src/tasks.js +670 -0
- package/src/textdiff.js +20 -0
- package/src/turn-sync-plan.js +142 -0
- package/src/turn-sync.js +191 -0
- package/src/ui/access-form.js +37 -0
- package/src/ui/agent-env-key.js +76 -0
- package/src/ui/agent-guide.js +127 -0
- package/src/ui/agent-kinds.js +175 -0
- package/src/ui/agent-place.js +265 -0
- package/src/ui/app.css +2309 -0
- package/src/ui/app.js +542 -0
- package/src/ui/board.js +465 -0
- package/src/ui/boxes.js +76 -0
- package/src/ui/branches.js +111 -0
- package/src/ui/catchup.js +77 -0
- package/src/ui/changes.js +159 -0
- package/src/ui/chat.js +266 -0
- package/src/ui/cloud-ai-model.js +107 -0
- package/src/ui/cloud-ai.js +221 -0
- package/src/ui/common.js +431 -0
- package/src/ui/csv.js +21 -0
- package/src/ui/diffview.js +74 -0
- package/src/ui/feed-convs.js +39 -0
- package/src/ui/feed.js +174 -0
- package/src/ui/files.js +579 -0
- package/src/ui/fileview.js +123 -0
- package/src/ui/git-rule.js +60 -0
- package/src/ui/home.js +1323 -0
- package/src/ui/index.html +16 -0
- package/src/ui/invite.js +97 -0
- package/src/ui/invites.js +79 -0
- package/src/ui/loom-model.js +516 -0
- package/src/ui/loom.css +349 -0
- package/src/ui/loom.js +753 -0
- package/src/ui/mark.js +104 -0
- package/src/ui/merges.js +158 -0
- package/src/ui/rail-model.js +78 -0
- package/src/ui/rail.js +173 -0
- package/src/ui/releases.js +182 -0
- package/src/ui/schedule.js +156 -0
- package/src/ui/session.js +3394 -0
- package/src/ui/signin.js +75 -0
- package/src/ui/sounds.js +110 -0
- package/src/ui/tool-logo.js +86 -0
- package/src/ui/tree.js +276 -0
- package/src/ui/workspaces.js +542 -0
- package/src/ui-server.js +1272 -0
- package/src/update-check.js +67 -0
- package/src/upstream.js +155 -0
- package/src/webhooks.js +235 -0
- package/src/workspace-cli.js +62 -0
- package/src/workspace-tools.js +340 -0
- package/index.js +0 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// One live session per repository and branch (plan §8, addendum 16.6). The session that pushes
|
|
2
|
+
// repo@branch holds its lease in the accounts API (git_sync_leases), which works across relay
|
|
3
|
+
// machines; the relay renews every lease it holds every LEASE_HEARTBEAT_MS, for every session active
|
|
4
|
+
// in the last 24 hours (loaded or not), and keeps the list on its disk so a restart picks them up.
|
|
5
|
+
// A lease lapses LEASE_TTL_MS after its last heartbeat (two missed: a crashed relay's).
|
|
6
|
+
//
|
|
7
|
+
// Fencing: a holder publishes only while its last good heartbeat is younger than LEASE_TTL_MS minus
|
|
8
|
+
// PUSH_MARGIN_MS (a push's whole timeout, plus clock skew), and the API grants the lease to anyone
|
|
9
|
+
// else only once LEASE_TTL_MS has passed since that heartbeat: so a holder cut off from the API has
|
|
10
|
+
// stopped pushing before another can start. Every push is also a compare-and-swap on GitHub's ref
|
|
11
|
+
// (forward from the head the relay read), so two pushes can never both land on the same head, and a
|
|
12
|
+
// grant raises the generation, which the old holder learns at its next heartbeat.
|
|
13
|
+
//
|
|
14
|
+
// Without the API (a relay on its own) the leases live in this process, with the same rules.
|
|
15
|
+
import fs from 'node:fs'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
|
|
18
|
+
export const LEASE_TTL_MS = 2 * 60 * 1000
|
|
19
|
+
export const LEASE_HEARTBEAT_MS = 30 * 1000
|
|
20
|
+
export const LEASE_IDLE_MS = 24 * 60 * 60 * 1000
|
|
21
|
+
export const PUSH_MARGIN_MS = 70 * 1000
|
|
22
|
+
|
|
23
|
+
const local = new Map() // repo\0branch -> { room, generation, heartbeatAt } (no API: this process only)
|
|
24
|
+
export function resetLocalLeases () { local.clear() }
|
|
25
|
+
|
|
26
|
+
function localStore (now) {
|
|
27
|
+
return {
|
|
28
|
+
async take ({ repo, branch, room, ttlMs }) {
|
|
29
|
+
const k = `${repo}\0${branch}`
|
|
30
|
+
const cur = local.get(k)
|
|
31
|
+
const t = now()
|
|
32
|
+
if (!cur) { local.set(k, { room, generation: 1, heartbeatAt: t }); return { ok: true, generation: 1, room } }
|
|
33
|
+
const lapsed = cur.heartbeatAt < t - ttlMs
|
|
34
|
+
if (cur.room === room && !lapsed) { cur.heartbeatAt = t; return { ok: true, generation: cur.generation, room } }
|
|
35
|
+
if (!lapsed) return { ok: false, room: cur.room, generation: cur.generation }
|
|
36
|
+
local.set(k, { room, generation: cur.generation + 1, heartbeatAt: t })
|
|
37
|
+
return { ok: true, generation: cur.generation + 1, room }
|
|
38
|
+
},
|
|
39
|
+
async heartbeat ({ repo, branch, room, generation, ttlMs }) {
|
|
40
|
+
const cur = local.get(`${repo}\0${branch}`)
|
|
41
|
+
if (!cur || cur.room !== room || cur.generation !== generation) return { ok: false, lost: true, room: cur ? cur.room : '' }
|
|
42
|
+
if (cur.heartbeatAt < now() - ttlMs) return { ok: false, expired: true }
|
|
43
|
+
cur.heartbeatAt = now()
|
|
44
|
+
return { ok: true, generation }
|
|
45
|
+
},
|
|
46
|
+
async release ({ repo, branch, room, generation }) {
|
|
47
|
+
const k = `${repo}\0${branch}`
|
|
48
|
+
const cur = local.get(k)
|
|
49
|
+
if (cur && cur.room === room && cur.generation === generation) local.delete(k)
|
|
50
|
+
return { ok: true }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function apiStore ({ apiUrl, secret, fetch }) {
|
|
56
|
+
const call = async (what, body) => {
|
|
57
|
+
const res = await fetch(`${String(apiUrl).replace(/\/+$/, '')}/v1/relay/leases/${what}`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${secret}` }, body: JSON.stringify(body) })
|
|
58
|
+
if (!res.ok) throw new Error(`the accounts API answered ${res.status}`)
|
|
59
|
+
return res.json()
|
|
60
|
+
}
|
|
61
|
+
return { take: (b) => call('take', b), heartbeat: (b) => call('heartbeat', b), release: (b) => call('release', b) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The relay's leases. `ensure({ repo, branch, room })` resolves { ok, generation } (taking or
|
|
66
|
+
* renewing it), or { ok: false, room } naming the session that holds it, or { ok: false, error }
|
|
67
|
+
* when the API can't be reached (then nobody publishes, rather than two). `valid(...)` is the
|
|
68
|
+
* fencing check right before a push. `tick()` renews and lets go of idle ones.
|
|
69
|
+
*/
|
|
70
|
+
export function leaseKeeper ({ apiUrl = '', secret = '', fetch = globalThis.fetch, dataDir = null, log = () => {}, now = () => Date.now(), ttlMs = LEASE_TTL_MS, marginMs = PUSH_MARGIN_MS, idleMs = LEASE_IDLE_MS, holder = '' } = {}) {
|
|
71
|
+
const store = apiUrl && secret ? apiStore({ apiUrl, secret, fetch }) : localStore(now)
|
|
72
|
+
const file = dataDir ? path.join(dataDir, 'leases.json') : null
|
|
73
|
+
const held = new Map() // repo\0branch -> { repo, branch, room, generation, okAt, activeAt }
|
|
74
|
+
try { if (file) for (const e of JSON.parse(fs.readFileSync(file, 'utf8'))) if (e && e.repo && e.branch && e.room) held.set(`${e.repo}\0${e.branch}`, { ...e, okAt: 0 }) } catch {}
|
|
75
|
+
const save = () => { if (!file) return; try { fs.writeFileSync(`${file}.tmp`, JSON.stringify([...held.values()].map(({ okAt, ...e }) => e))); fs.renameSync(`${file}.tmp`, file) } catch {} }
|
|
76
|
+
const k = (repo, branch) => `${repo}\0${branch}`
|
|
77
|
+
|
|
78
|
+
const valid = ({ repo, branch, room }) => {
|
|
79
|
+
const e = held.get(k(repo, branch))
|
|
80
|
+
return !!(e && e.room === room && now() - e.okAt < ttlMs - marginMs)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ensure = async ({ repo, branch, room }) => {
|
|
84
|
+
const e = held.get(k(repo, branch))
|
|
85
|
+
if (e && e.room === room) e.activeAt = now()
|
|
86
|
+
if (valid({ repo, branch, room })) return { ok: true, generation: e.generation }
|
|
87
|
+
let r
|
|
88
|
+
try {
|
|
89
|
+
r = e && e.room === room && e.generation ? await store.heartbeat({ repo, branch, room, generation: e.generation, ttlMs }) : null
|
|
90
|
+
if (!r || !r.ok) r = await store.take({ repo, branch, room, holder, ttlMs })
|
|
91
|
+
} catch (err) { return { ok: false, error: err.message } }
|
|
92
|
+
if (!r.ok) {
|
|
93
|
+
if (e && e.room === room) { held.delete(k(repo, branch)); save() }
|
|
94
|
+
return { ok: false, room: r.room || '' }
|
|
95
|
+
}
|
|
96
|
+
held.set(k(repo, branch), { repo, branch, room, generation: r.generation, okAt: now(), activeAt: now() })
|
|
97
|
+
save()
|
|
98
|
+
return { ok: true, generation: r.generation }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const release = async ({ repo, branch, room }) => {
|
|
102
|
+
const e = held.get(k(repo, branch))
|
|
103
|
+
if (!e || e.room !== room) return false
|
|
104
|
+
held.delete(k(repo, branch))
|
|
105
|
+
save()
|
|
106
|
+
try { await store.release({ repo, branch, room, generation: e.generation }) } catch {}
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Renews every lease held for a session active in the last 24 hours; lets go of the rest. Returns those lost. */
|
|
111
|
+
const tick = async ({ ended = () => false } = {}) => {
|
|
112
|
+
const lost = []
|
|
113
|
+
for (const e of [...held.values()]) {
|
|
114
|
+
if (ended(e.room) || now() - (e.activeAt || 0) > idleMs) { await release(e); continue }
|
|
115
|
+
try {
|
|
116
|
+
const r = await store.heartbeat({ repo: e.repo, branch: e.branch, room: e.room, generation: e.generation, ttlMs })
|
|
117
|
+
if (r.ok) { e.okAt = now(); continue }
|
|
118
|
+
const again = r.expired ? await store.take({ repo: e.repo, branch: e.branch, room: e.room, holder, ttlMs }) : null
|
|
119
|
+
if (again && again.ok) { e.generation = again.generation; e.okAt = now(); save(); continue }
|
|
120
|
+
held.delete(k(e.repo, e.branch)); save()
|
|
121
|
+
lost.push({ ...e, to: (again && again.room) || r.room || '' })
|
|
122
|
+
log(`lease: ${e.repo} ${e.branch} is no longer ${e.room}'s (${(again && again.room) || r.room || 'lapsed'})`)
|
|
123
|
+
} catch (err) { log(`lease: could not renew ${e.repo} ${e.branch}: ${err.message}`) }
|
|
124
|
+
}
|
|
125
|
+
return lost
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The session moved to another relay machine, which renews its leases from now on (same session, same generation): this one stops. */
|
|
129
|
+
const forget = (room) => {
|
|
130
|
+
let n = 0
|
|
131
|
+
for (const [key, e] of held) if (e.room === room) { held.delete(key); n++ }
|
|
132
|
+
if (n) save()
|
|
133
|
+
return n
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { ensure, valid, release, tick, forget, held: () => [...held.values()], local: !(apiUrl && secret) }
|
|
137
|
+
}
|
package/src/relay-lfs.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Git LFS for the relay's commits (plan §10): a file the repository's .gitattributes puts through
|
|
2
|
+
// filter=lfs goes to GitHub's LFS store through the batch API, and the commit carries its pointer.
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
/** Git's LFS pointer for `bytes` (sha256 oid and size). */
|
|
6
|
+
export function lfsPointer (bytes) {
|
|
7
|
+
const oid = crypto.createHash('sha256').update(bytes).digest('hex')
|
|
8
|
+
return { oid, size: bytes.length, text: `version https://git-lfs.github.com/spec/v1\noid sha256:${oid}\nsize ${bytes.length}\n` }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Uploads `bytes` to the LFS store of the repository at `remoteUrl` (https://github.com/owner/name.git),
|
|
13
|
+
* signed in with `token`: the batch API says where (or that it is there already), then the upload and,
|
|
14
|
+
* when asked, the verify call. Resolves the pointer text to commit; throws with what went wrong.
|
|
15
|
+
*/
|
|
16
|
+
export async function lfsUpload ({ remoteUrl, token = null, bytes, fetch = globalThis.fetch }) {
|
|
17
|
+
const p = lfsPointer(bytes)
|
|
18
|
+
const base = String(remoteUrl).replace(/\/+$/, '').replace(/(\.git)?$/, '.git')
|
|
19
|
+
const auth = token ? { authorization: `Basic ${Buffer.from(`x-access-token:${token}`).toString('base64')}` } : {}
|
|
20
|
+
const json = { accept: 'application/vnd.git-lfs+json', 'content-type': 'application/vnd.git-lfs+json' }
|
|
21
|
+
const res = await fetch(`${base}/info/lfs/objects/batch`, { method: 'POST', headers: { ...json, ...auth }, body: JSON.stringify({ operation: 'upload', transfers: ['basic'], objects: [{ oid: p.oid, size: p.size }] }) })
|
|
22
|
+
if (!res.ok) throw new Error(`Git LFS answered ${res.status}${res.status === 404 ? ' (is LFS on for this repository?)' : ''}`)
|
|
23
|
+
const body = await res.json().catch(() => ({}))
|
|
24
|
+
const o = (Array.isArray(body.objects) ? body.objects : [])[0] || {}
|
|
25
|
+
if (o.error) throw new Error(`Git LFS: ${o.error.message || o.error.code}`)
|
|
26
|
+
const up = o.actions && o.actions.upload
|
|
27
|
+
if (up && up.href) {
|
|
28
|
+
const r = await fetch(up.href, { method: 'PUT', headers: { 'content-type': 'application/octet-stream', ...(up.header || {}) }, body: bytes })
|
|
29
|
+
if (!r.ok) throw new Error(`Git LFS upload answered ${r.status}`)
|
|
30
|
+
}
|
|
31
|
+
const verify = o.actions && o.actions.verify
|
|
32
|
+
if (verify && verify.href) {
|
|
33
|
+
const r = await fetch(verify.href, { method: 'POST', headers: { ...json, ...(verify.header || {}) }, body: JSON.stringify({ oid: p.oid, size: p.size }) })
|
|
34
|
+
if (!r.ok) throw new Error(`Git LFS verify answered ${r.status}`)
|
|
35
|
+
}
|
|
36
|
+
return p.text
|
|
37
|
+
}
|