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.
Files changed (204) hide show
  1. package/LICENSE +85 -0
  2. package/README.md +630 -2
  3. package/RELEASES.md +733 -0
  4. package/assets/logo.svg +1 -0
  5. package/bin/quilt.js +816 -0
  6. package/desktop/main.js +315 -0
  7. package/package.json +163 -7
  8. package/src/account.js +563 -0
  9. package/src/admit-policy.js +32 -0
  10. package/src/agent-join.js +370 -0
  11. package/src/agent-task-workflow.js +176 -0
  12. package/src/agents/actions.js +85 -0
  13. package/src/agents/claude-code.js +180 -0
  14. package/src/agents/common.js +21 -0
  15. package/src/agents/cursor-transcripts.js +170 -0
  16. package/src/agents/cursor.js +404 -0
  17. package/src/agents/index.js +69 -0
  18. package/src/ai-vm-signin.js +98 -0
  19. package/src/ai-vm.js +380 -0
  20. package/src/api/access.js +75 -0
  21. package/src/api/activity.js +181 -0
  22. package/src/api/agent-auth.js +130 -0
  23. package/src/api/agent-placement.js +157 -0
  24. package/src/api/audit.js +83 -0
  25. package/src/api/auth.js +17 -0
  26. package/src/api/domains.js +30 -0
  27. package/src/api/file-paths.js +40 -0
  28. package/src/api/file-store.js +99 -0
  29. package/src/api/fly-machines.js +76 -0
  30. package/src/api/github-app.js +172 -0
  31. package/src/api/http.js +34 -0
  32. package/src/api/invite-email.js +53 -0
  33. package/src/api/issues.js +110 -0
  34. package/src/api/join-text.js +78 -0
  35. package/src/api/mailer.js +47 -0
  36. package/src/api/memory-store.js +1083 -0
  37. package/src/api/org-access.js +54 -0
  38. package/src/api/permissions.js +71 -0
  39. package/src/api/relay-cluster.js +251 -0
  40. package/src/api/relay-machines.js +60 -0
  41. package/src/api/routes/access-types.js +54 -0
  42. package/src/api/routes/agent-invites.js +115 -0
  43. package/src/api/routes/agents.js +157 -0
  44. package/src/api/routes/ai-vm.js +369 -0
  45. package/src/api/routes/github.js +380 -0
  46. package/src/api/routes/grants.js +70 -0
  47. package/src/api/routes/invites.js +188 -0
  48. package/src/api/routes/issues.js +47 -0
  49. package/src/api/routes/join.js +167 -0
  50. package/src/api/routes/members.js +110 -0
  51. package/src/api/routes/orgs.js +177 -0
  52. package/src/api/routes/relay-cluster.js +90 -0
  53. package/src/api/routes/relay.js +76 -0
  54. package/src/api/routes/session-invites.js +119 -0
  55. package/src/api/routes/sessions.js +92 -0
  56. package/src/api/routes/teams.js +132 -0
  57. package/src/api/routes/workspace-agents.js +332 -0
  58. package/src/api/routes/workspace-files.js +277 -0
  59. package/src/api/routes/workspace-invites.js +196 -0
  60. package/src/api/routes/workspaces.js +225 -0
  61. package/src/api/server.js +623 -0
  62. package/src/api/slugs.js +20 -0
  63. package/src/api/supabase-store.js +765 -0
  64. package/src/api/team-access.js +30 -0
  65. package/src/api/tokens.js +31 -0
  66. package/src/api/workspace-access.js +62 -0
  67. package/src/api/workspace-reach.js +60 -0
  68. package/src/autowork-tools.js +166 -0
  69. package/src/autowork.js +387 -0
  70. package/src/blobstore.js +99 -0
  71. package/src/boxes.js +37 -0
  72. package/src/branchdocs.js +235 -0
  73. package/src/branches.js +194 -0
  74. package/src/catchup.js +111 -0
  75. package/src/chat-archive.js +85 -0
  76. package/src/chat-links.js +923 -0
  77. package/src/clash.js +140 -0
  78. package/src/cloud-ai.js +50 -0
  79. package/src/commit-request.js +59 -0
  80. package/src/commit.js +213 -0
  81. package/src/connection.js +569 -0
  82. package/src/control.js +245 -0
  83. package/src/conversation.js +125 -0
  84. package/src/doctor.js +156 -0
  85. package/src/duties.js +303 -0
  86. package/src/editors.js +228 -0
  87. package/src/fsutil.js +87 -0
  88. package/src/git-hook.js +48 -0
  89. package/src/git.js +147 -0
  90. package/src/github-commit.js +320 -0
  91. package/src/gitignore.js +49 -0
  92. package/src/gitstate.js +900 -0
  93. package/src/history.js +307 -0
  94. package/src/hooks.js +371 -0
  95. package/src/hosted-agents.js +24 -0
  96. package/src/hosted-client.js +112 -0
  97. package/src/identity.js +99 -0
  98. package/src/inbox.js +237 -0
  99. package/src/integrations.js +221 -0
  100. package/src/key-cli.js +115 -0
  101. package/src/largefiles.js +91 -0
  102. package/src/legacy.js +25 -0
  103. package/src/mcp-proxy.js +40 -0
  104. package/src/mcp-room.js +32 -0
  105. package/src/mcp.js +1261 -0
  106. package/src/merge3.js +158 -0
  107. package/src/merges.js +148 -0
  108. package/src/pass-source.js +176 -0
  109. package/src/passes.js +64 -0
  110. package/src/pathrules.js +127 -0
  111. package/src/persona.js +101 -0
  112. package/src/presence.js +360 -0
  113. package/src/private-file.js +41 -0
  114. package/src/procs.js +70 -0
  115. package/src/project-rules.js +178 -0
  116. package/src/protocol.js +92 -0
  117. package/src/quiet-warnings.js +9 -0
  118. package/src/relay-ai-vm.js +82 -0
  119. package/src/relay-ask.js +141 -0
  120. package/src/relay-ci.js +111 -0
  121. package/src/relay-clash.js +597 -0
  122. package/src/relay-cluster.js +454 -0
  123. package/src/relay-commit.js +887 -0
  124. package/src/relay-filekey.js +46 -0
  125. package/src/relay-git.js +274 -0
  126. package/src/relay-githttp.js +137 -0
  127. package/src/relay-github.js +109 -0
  128. package/src/relay-lease.js +137 -0
  129. package/src/relay-lfs.js +37 -0
  130. package/src/relay-mcp.js +1679 -0
  131. package/src/relay-metrics.js +75 -0
  132. package/src/relay-outbound.js +1144 -0
  133. package/src/relay-sync.js +719 -0
  134. package/src/relay-turn-sync.js +307 -0
  135. package/src/relay-upstream.js +934 -0
  136. package/src/relay-webhooks.js +111 -0
  137. package/src/releases.js +131 -0
  138. package/src/report.js +129 -0
  139. package/src/runner.js +306 -0
  140. package/src/same-edit.js +18 -0
  141. package/src/server.js +4187 -0
  142. package/src/session-access.js +170 -0
  143. package/src/session-name.js +15 -0
  144. package/src/session.js +7361 -0
  145. package/src/settings.js +46 -0
  146. package/src/setup.js +184 -0
  147. package/src/shell-path.js +46 -0
  148. package/src/status.js +199 -0
  149. package/src/summarize.js +109 -0
  150. package/src/sync-words.js +42 -0
  151. package/src/task-comments.js +92 -0
  152. package/src/tasks.js +670 -0
  153. package/src/textdiff.js +20 -0
  154. package/src/turn-sync-plan.js +142 -0
  155. package/src/turn-sync.js +191 -0
  156. package/src/ui/access-form.js +37 -0
  157. package/src/ui/agent-env-key.js +76 -0
  158. package/src/ui/agent-guide.js +127 -0
  159. package/src/ui/agent-kinds.js +175 -0
  160. package/src/ui/agent-place.js +265 -0
  161. package/src/ui/app.css +2309 -0
  162. package/src/ui/app.js +542 -0
  163. package/src/ui/board.js +465 -0
  164. package/src/ui/boxes.js +76 -0
  165. package/src/ui/branches.js +111 -0
  166. package/src/ui/catchup.js +77 -0
  167. package/src/ui/changes.js +159 -0
  168. package/src/ui/chat.js +266 -0
  169. package/src/ui/cloud-ai-model.js +107 -0
  170. package/src/ui/cloud-ai.js +221 -0
  171. package/src/ui/common.js +431 -0
  172. package/src/ui/csv.js +21 -0
  173. package/src/ui/diffview.js +74 -0
  174. package/src/ui/feed-convs.js +39 -0
  175. package/src/ui/feed.js +174 -0
  176. package/src/ui/files.js +579 -0
  177. package/src/ui/fileview.js +123 -0
  178. package/src/ui/git-rule.js +60 -0
  179. package/src/ui/home.js +1323 -0
  180. package/src/ui/index.html +16 -0
  181. package/src/ui/invite.js +97 -0
  182. package/src/ui/invites.js +79 -0
  183. package/src/ui/loom-model.js +516 -0
  184. package/src/ui/loom.css +349 -0
  185. package/src/ui/loom.js +753 -0
  186. package/src/ui/mark.js +104 -0
  187. package/src/ui/merges.js +158 -0
  188. package/src/ui/rail-model.js +78 -0
  189. package/src/ui/rail.js +173 -0
  190. package/src/ui/releases.js +182 -0
  191. package/src/ui/schedule.js +156 -0
  192. package/src/ui/session.js +3394 -0
  193. package/src/ui/signin.js +75 -0
  194. package/src/ui/sounds.js +110 -0
  195. package/src/ui/tool-logo.js +86 -0
  196. package/src/ui/tree.js +276 -0
  197. package/src/ui/workspaces.js +542 -0
  198. package/src/ui-server.js +1272 -0
  199. package/src/update-check.js +67 -0
  200. package/src/upstream.js +155 -0
  201. package/src/webhooks.js +235 -0
  202. package/src/workspace-cli.js +62 -0
  203. package/src/workspace-tools.js +340 -0
  204. 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
+ }
@@ -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
+ }