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,91 @@
1
+ // Encryption for large files stored outside the session document. Each session
2
+ // has file keys; a file key is stored in the document only wrapped (encrypted)
3
+ // with keys derived from the session's secrets, so only members can open it.
4
+ import crypto from 'node:crypto'
5
+
6
+ const MAGIC = Buffer.from('QF1')
7
+ const NONCE = 12
8
+ const TAG = 16
9
+
10
+ /** The key that wraps file keys for whoever holds `secret` in `room`. */
11
+ export function deriveWrapKey (secret, room) {
12
+ return Buffer.from(crypto.hkdfSync('sha256', String(secret), String(room), 'quilt-file-key-wrap-v1', 32))
13
+ }
14
+
15
+ export function newFileKey () {
16
+ return crypto.randomBytes(32)
17
+ }
18
+
19
+ function seal (plain, key) {
20
+ const nonce = crypto.randomBytes(NONCE)
21
+ const c = crypto.createCipheriv('aes-256-gcm', key, nonce)
22
+ const body = Buffer.concat([c.update(plain), c.final()])
23
+ return Buffer.concat([nonce, body, c.getAuthTag()])
24
+ }
25
+
26
+ function open (sealed, key) {
27
+ if (sealed.length < NONCE + TAG) throw new Error('too short')
28
+ const d = crypto.createDecipheriv('aes-256-gcm', key, sealed.subarray(0, NONCE))
29
+ d.setAuthTag(sealed.subarray(sealed.length - TAG))
30
+ return Buffer.concat([d.update(sealed.subarray(NONCE, sealed.length - TAG)), d.final()])
31
+ }
32
+
33
+ export function wrapKey (fileKey, wrap) {
34
+ return seal(fileKey, wrap).toString('base64url')
35
+ }
36
+
37
+ /** The file key, or null if this wrap key doesn't open it. */
38
+ export function unwrapKey (wrapped, wrap) {
39
+ try {
40
+ const key = open(Buffer.from(String(wrapped), 'base64url'), wrap)
41
+ return key.length === 32 ? key : null
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ export function encryptBlob (plain, fileKey) {
48
+ return Buffer.concat([MAGIC, seal(plain, fileKey)])
49
+ }
50
+
51
+ export function decryptBlob (sealed, fileKey) {
52
+ try {
53
+ if (!sealed.subarray(0, MAGIC.length).equals(MAGIC)) throw new Error('not a quilt file')
54
+ return open(sealed.subarray(MAGIC.length), fileKey)
55
+ } catch {
56
+ throw new Error('this file could not be decrypted')
57
+ }
58
+ }
59
+
60
+ /** Where a file is stored: the same for the same key and content, meaningless to anyone else. */
61
+ export function blobId (fileKey, sha1Hex) {
62
+ return crypto.createHmac('sha256', fileKey).update(`id:${sha1Hex}`).digest('hex').slice(0, 32)
63
+ }
64
+
65
+ // GitHub as a node (plan §10): on a branch that syncs with GitHub, once the session owner agrees,
66
+ // each app also wraps that branch's file keys for the relay's own key (X25519), so the relay can read
67
+ // large files to commit them. Branches that don't sync with GitHub stay end-to-end encrypted.
68
+
69
+ /** A short id for a relay's public key (spki DER, base64url), so a wrap says which key it is for. */
70
+ export const relayKeyId = (relayPub) => crypto.createHash('sha256').update(String(relayPub)).digest('hex').slice(0, 16)
71
+
72
+ const relayWrapKey = (shared, epk) => Buffer.from(crypto.hkdfSync('sha256', shared, Buffer.from(String(epk)), 'quilt-file-key-relay-v1', 32))
73
+
74
+ /** `fileKey` wrapped for the relay's X25519 public key `relayPub`: { to, epk, wrapped }. */
75
+ export function wrapForRelay (fileKey, relayPub) {
76
+ const pub = crypto.createPublicKey({ key: Buffer.from(String(relayPub), 'base64url'), format: 'der', type: 'spki' })
77
+ const eph = crypto.generateKeyPairSync('x25519')
78
+ const epk = eph.publicKey.export({ format: 'der', type: 'spki' }).toString('base64url')
79
+ const shared = crypto.diffieHellman({ privateKey: eph.privateKey, publicKey: pub })
80
+ return { to: relayKeyId(relayPub), epk, wrapped: wrapKey(fileKey, relayWrapKey(shared, epk)) }
81
+ }
82
+
83
+ /** The file key in a wrap made by wrapForRelay, with the relay's private key; null if it isn't for this key. */
84
+ export function unwrapAsRelay (entry, privateKey) {
85
+ try {
86
+ if (!entry || typeof entry.epk !== 'string' || typeof entry.wrapped !== 'string') return null
87
+ const epk = crypto.createPublicKey({ key: Buffer.from(entry.epk, 'base64url'), format: 'der', type: 'spki' })
88
+ const shared = crypto.diffieHellman({ privateKey, publicKey: epk })
89
+ return unwrapKey(entry.wrapped, relayWrapKey(shared, entry.epk))
90
+ } catch { return null }
91
+ }
package/src/legacy.js ADDED
@@ -0,0 +1,25 @@
1
+ // Quilt was called cowove until 2026-09-28. Existing installs keep working:
2
+ // ~/.cowove and a project's .cowove folder are moved to .quilt the first time
3
+ // they're needed, and COWOVE_* settings count as QUILT_* ones.
4
+ import fs from 'node:fs'
5
+ import os from 'node:os'
6
+ import path from 'node:path'
7
+
8
+ /** Moves <parent>/.cowove to <parent>/.quilt if only the old one exists. */
9
+ export function migrateDir (parent) {
10
+ const cur = path.join(parent, '.quilt')
11
+ const old = path.join(parent, '.cowove')
12
+ try { if (!fs.existsSync(cur) && fs.existsSync(old)) fs.renameSync(old, cur) } catch {}
13
+ return cur
14
+ }
15
+
16
+ /** ~/.quilt: settings, identity, recent sessions, running processes. */
17
+ export const quiltHome = () => migrateDir(os.homedir())
18
+
19
+ /** Copies COWOVE_* variables to their QUILT_* names unless those are set. */
20
+ export function adoptLegacyEnv (env = process.env) {
21
+ for (const [k, v] of Object.entries(env)) {
22
+ if (k.startsWith('COWOVE_') && env[`QUILT_${k.slice(7)}`] === undefined) env[`QUILT_${k.slice(7)}`] = v
23
+ }
24
+ return env
25
+ }
@@ -0,0 +1,40 @@
1
+ // `quilt mcp` in a sandbox with a QUILT_AGENT_KEY and no `quilt join` running: a stdio MCP server
2
+ // that hands each request to the hosted MCP (the accounts API's /mcp) as the key's agent, with the
3
+ // key's session filled in. Any AI tool that runs stdio MCP servers gets the same quilt_* tools as
4
+ // everywhere else, with nothing installed but `npx heyquilt mcp`.
5
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js'
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
7
+ import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'
8
+ import { hostedClient } from './hosted-client.js'
9
+
10
+ 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).'
12
+
13
+ /** The tools as the hosted MCP lists them, without `session` (the proxy fills it in). */
14
+ export function withoutSession (tools) {
15
+ return (tools || []).map((t) => {
16
+ const schema = t.inputSchema && t.inputSchema.properties && t.inputSchema.properties.session
17
+ ? { ...t.inputSchema, properties: Object.fromEntries(Object.entries(t.inputSchema.properties).filter(([k]) => k !== 'session')), ...(Array.isArray(t.inputSchema.required) ? { required: t.inputSchema.required.filter((k) => k !== 'session') } : {}) }
18
+ : t.inputSchema
19
+ return { ...t, inputSchema: schema }
20
+ })
21
+ }
22
+
23
+ export async function runMcpProxy ({ key, client = hostedClient({ key }), transport = new StdioServerTransport() }) {
24
+ let instructions = ''
25
+ try {
26
+ const init = await client.rpc('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'quilt-mcp-proxy', version: '1' } })
27
+ instructions = (init && init.instructions) || ''
28
+ } catch {} // the first tool call says what is wrong (a revoked key, no network)
29
+ const server = new Server({ name: 'quilt', version: '0.2.0' }, { capabilities: { tools: {} }, instructions: instructions + NOTE(key.room) })
30
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: withoutSession((await client.rpc('tools/list', {})).tools) }))
31
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
32
+ try {
33
+ return await client.callRaw(req.params.name, req.params.arguments || {})
34
+ } catch (e) {
35
+ return { content: [{ type: 'text', text: e.message }], isError: true }
36
+ }
37
+ })
38
+ await server.connect(transport)
39
+ return server
40
+ }
@@ -0,0 +1,32 @@
1
+ // Which room a hosted agent's MCP request (a JSON-RPC body) is about: the invite a
2
+ // quilt_join_session call joins, or the `session` a tool call names. The accounts API uses it to
3
+ // mint the pass for that room (src/api/server.js); the relay, to send the request to the machine
4
+ // that holds the room (src/relay-cluster.js).
5
+ import { parseInvite } from './ui/invite.js'
6
+
7
+ const ROOM = /^[A-Za-z0-9_-]{1,64}$/
8
+
9
+ /** The room a quilt_join_session call in this MCP request joins, or '' (any other request). */
10
+ export function joiningRoom (body) {
11
+ if (!body || !body.length) return ''
12
+ try {
13
+ const msg = JSON.parse(body.toString('utf8'))
14
+ for (const m of Array.isArray(msg) ? msg : [msg]) {
15
+ if (m?.method === 'tools/call' && m.params?.name === 'quilt_join_session') return parseInvite(String(m.params.arguments?.invite || ''), { allowRelay: () => true }).room
16
+ }
17
+ } catch {}
18
+ return ''
19
+ }
20
+
21
+ /** The `session` a tool call names (an agent in several sessions says which each call is about), or ''. */
22
+ export function sessionArg (body) {
23
+ if (!body || !body.length) return ''
24
+ try {
25
+ const msg = JSON.parse(body.toString('utf8'))
26
+ for (const m of Array.isArray(msg) ? msg : [msg]) {
27
+ const s = m?.method === 'tools/call' ? m.params?.arguments?.session : undefined
28
+ if (typeof s === 'string' && ROOM.test(s.trim())) return s.trim()
29
+ }
30
+ } catch {}
31
+ return ''
32
+ }