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,340 @@
1
+ // The workspace library as MCP tools for agents: list workspaces and their files, read and
2
+ // write files, make folders, move and delete them, and the agent's webhook for workspace events. Shared by
3
+ // the local MCP (`quilt mcp`, which can also read and save files on this computer) and the
4
+ // hosted MCP on the relay. Each host passes how to reach the API:
5
+ // call(method, path, body) -> parsed JSON, or throws an Error with .status and the API's message
6
+ // fetchBytes(url, maxBytes) -> Buffer, refusing a body over maxBytes (bytesFetcher makes one)
7
+ // put(url, bytes, headers) -> HTTP status
8
+ import crypto from 'node:crypto'
9
+ import fs from 'node:fs'
10
+ import path from 'node:path'
11
+ import { z } from 'zod'
12
+ import { cleanFilePath, isTextual } from './api/file-paths.js'
13
+
14
+ /** One paragraph for the MCP instructions, added only where these tools are offered. */
15
+ export const WORKSPACE_GUIDE =
16
+ 'Files that are not code (images, video, documents, data) live in the workspace library: read them with ' +
17
+ 'quilt_workspace_read_file and put what you make there with quilt_workspace_write_file and a short note, not in chat. ' +
18
+ 'Subscribe with quilt_workspace_webhook to be told when a session starts in your workspace; join it with the link the event carries, ' +
19
+ 'and wait: the session\'s owner lets you in.'
20
+
21
+ /** The same, for hosted agents: their one webhook (quilt_webhook_subscribe) carries session.started too. */
22
+ export const WORKSPACE_GUIDE_HOSTED = WORKSPACE_GUIDE.replace('Subscribe with quilt_workspace_webhook to be told', 'Your webhook (quilt_webhook_subscribe) also tells you')
23
+
24
+ // The API's own limit on one file; fromPath may send up to this.
25
+ const MAX_FROM_PATH = 500 * 1024 * 1024
26
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
27
+ const BASE64 = /^[A-Za-z0-9+/]*={0,2}$/
28
+
29
+ /** Whether `target` is strictly inside `root` (both absolute). A name like "..notes" is inside. */
30
+ export function isInside (root, target) {
31
+ const rel = path.relative(root, target)
32
+ return rel !== '' && rel !== '..' && !rel.startsWith('..' + path.sep) && !path.isAbsolute(rel)
33
+ }
34
+
35
+ /**
36
+ * A fetchBytes for registerWorkspaceTools: GETs `url` and answers its bytes, giving up (and
37
+ * dropping the connection) as soon as the body is larger than `maxBytes`, so a link never
38
+ * makes the host hold more than the tool asked for.
39
+ */
40
+ export function bytesFetcher (fetchImpl = globalThis.fetch, { timeoutMs = 10 * 60 * 1000 } = {}) {
41
+ return async (url, maxBytes = Infinity) => {
42
+ const ac = new AbortController()
43
+ const timer = setTimeout(() => ac.abort(new Error('the download took too long')), timeoutMs)
44
+ timer.unref?.()
45
+ const tooBig = () => new Error(`the download is larger than expected (over ${formatBytes(maxBytes)})`)
46
+ try {
47
+ const res = await fetchImpl(url, { signal: ac.signal })
48
+ if (!res.ok) { ac.abort(); throw new Error(`the download failed (${res.status})`) }
49
+ if (Number(res.headers.get('content-length')) > maxBytes) { ac.abort(); throw tooBig() }
50
+ if (!res.body) return Buffer.alloc(0)
51
+ const chunks = []
52
+ let n = 0
53
+ for await (const chunk of res.body) {
54
+ n += chunk.length
55
+ if (n > maxBytes) { ac.abort(); throw tooBig() }
56
+ chunks.push(chunk)
57
+ }
58
+ return Buffer.concat(chunks.map((c) => Buffer.from(c)))
59
+ } finally {
60
+ clearTimeout(timer)
61
+ }
62
+ }
63
+ }
64
+
65
+ /** Whether `p` matches `glob`: `*` is any run of characters inside one folder, `**` crosses folders. */
66
+ export function globMatch (glob, p) {
67
+ let re = ''
68
+ const g = String(glob)
69
+ for (let i = 0; i < g.length; i++) {
70
+ if (g.startsWith('**/', i)) { re += '(?:.*/)?'; i += 2 } else if (g.startsWith('**', i)) { re += '.*'; i += 1 } else if (g[i] === '*') re += '[^/]*'
71
+ else re += g[i].replace(/[.+?^${}()|[\]\\]/g, '\\$&')
72
+ }
73
+ return new RegExp(`^${re}$`).test(p)
74
+ }
75
+
76
+ export function formatBytes (n) {
77
+ if (n < 1024) return `${n} B`
78
+ const units = ['KB', 'MB', 'GB', 'TB']
79
+ let v = n / 1024; let u = 0
80
+ while (v >= 1024 && u < units.length - 1) { v /= 1024; u++ }
81
+ return `${v < 10 ? v.toFixed(1) : Math.round(v)} ${units[u]}`
82
+ }
83
+
84
+ function ago (ms, now = Date.now()) {
85
+ if (!ms) return ''
86
+ const s = Math.max(0, Math.round((now - ms) / 1000))
87
+ if (s < 60) return `${s}s ago`
88
+ if (s < 3600) return `${Math.round(s / 60)}m ago`
89
+ if (s < 86400) return `${Math.round(s / 3600)}h ago`
90
+ return `${Math.round(s / 86400)}d ago`
91
+ }
92
+
93
+ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`
94
+ const ws$ = (id) => `/v1/workspaces/${encodeURIComponent(id)}`
95
+ const file$ = (wsId, fileId) => `${ws$(wsId)}/files/${encodeURIComponent(fileId)}`
96
+
97
+ /**
98
+ * Registers the library tools on an MCP server. `saveDir` (local only) is where binary or
99
+ * large reads are saved; `readLocal(path) -> Buffer` (local only) lets writes take a file
100
+ * from this computer (fromPath). `maxInline` caps text read back in an answer and text or
101
+ * base64 sent in a call. `guide` (local MCP) puts WORKSPACE_GUIDE in quilt_workspaces' description
102
+ * and answer, since the local MCP adds these tools after its instructions are sent.
103
+ */
104
+ export function registerWorkspaceTools (server, { call, fetchBytes, put, saveDir = null, maxInline = 2 * 1024 * 1024, readLocal = null, guide = false, webhook = true }) {
105
+ const answer = (text) => ({ content: [{ type: 'text', text }] })
106
+ const tool = (fn) => async (args = {}) => {
107
+ try {
108
+ return answer(await fn(args))
109
+ } catch (err) {
110
+ return { content: [{ type: 'text', text: `Error: ${err.message}` }], isError: true }
111
+ }
112
+ }
113
+
114
+ /** The workspace the agent means, by id or by name (any case): { id, name, ... }. */
115
+ async function workspaceOf (ref) {
116
+ const want = String(ref ?? '').trim()
117
+ if (!want) throw new Error('Say which workspace (its name or id). quilt_workspaces lists yours.')
118
+ const { workspaces } = await call('GET', '/v1/me/workspaces')
119
+ const byId = workspaces.find((w) => w.id === want)
120
+ if (byId) return byId
121
+ const named = workspaces.filter((w) => w.name.toLowerCase() === want.toLowerCase())
122
+ if (named.length === 1) return named[0]
123
+ if (named.length > 1) throw new Error(`Several workspaces are called ${want}: ${named.map((w) => w.id).join(', ')}. Use the id.`)
124
+ // An id it isn't listed under: the API has the last word (and says not found).
125
+ if (UUID.test(want)) return { id: want, name: want }
126
+ throw new Error(`No workspace called ${want}. quilt_workspaces lists the ones you can reach.`)
127
+ }
128
+
129
+ /** The library row at `p` (a file or a folder). */
130
+ async function entryAt (ws, p) {
131
+ const want = cleanFilePath(p)
132
+ const { files } = await call('GET', `${ws$(ws.id)}/files`)
133
+ const f = files.find((x) => x.path === want)
134
+ if (!f) throw new Error(`No file at ${want} in ${ws.name}. quilt_workspace_files lists what is there.`)
135
+ return f
136
+ }
137
+
138
+ /** account -> name, for who uploaded what. Best effort: an empty map if the API says no. */
139
+ async function namesIn (ws) {
140
+ const names = new Map()
141
+ try {
142
+ const d = await call('GET', ws$(ws.id))
143
+ for (const m of d.members || []) if (m.name) names.set(m.account, m.name)
144
+ for (const a of d.agents || []) if (a.name) names.set(a.account, a.name)
145
+ if (d.owner?.account && d.owner.name) names.set(d.owner.account, d.owner.name)
146
+ } catch {}
147
+ return names
148
+ }
149
+
150
+ /** Where a read is saved on this computer: inside saveDir/<workspace id>, never outside it. */
151
+ function savePath (ws, p) {
152
+ const root = path.resolve(saveDir, ws.id)
153
+ const file = path.resolve(root, p)
154
+ if (!isInside(root, file)) throw new Error('that path cannot be saved here')
155
+ return file
156
+ }
157
+
158
+ // With `guide`, the guide comes with quilt_workspaces (its description and the top of its
159
+ // answer), for a host whose instructions went out before these tools were added.
160
+ const lead = guide ? `${WORKSPACE_GUIDE}\n\n` : ''
161
+ server.registerTool('quilt_workspaces', {
162
+ description: 'List the Quilt workspaces you can reach: name, id, your access (edit or view), open sessions, files and storage used. Workspace tools take a workspace by name or id.' + (guide ? ` ${WORKSPACE_GUIDE}` : ''),
163
+ inputSchema: {}
164
+ }, tool(async () => lead + await listWorkspaces()))
165
+
166
+ async function listWorkspaces () {
167
+ const { workspaces } = await call('GET', '/v1/me/workspaces')
168
+ if (!workspaces.length) return 'You are not in any workspace yet. A person adds you to one in the Quilt app or on heyquilt.com.'
169
+ const usage = await Promise.all(workspaces.map((w) => call('GET', ws$(w.id)).then((d) => d.usage || null, () => null)))
170
+ return workspaces.map((w, i) => {
171
+ const c = w.counts || {}
172
+ const u = usage[i]
173
+ const parts = [`${w.access} access`, `${plural(c.open ?? 0, 'open session')}`, plural(c.files ?? u?.fileCount ?? 0, 'file')]
174
+ if (u) parts.push(`${formatBytes(u.usedBytes)} of ${formatBytes(u.quotaBytes)} used`)
175
+ if (w.archivedAt) parts.push('archived')
176
+ return `- ${w.name} (id ${w.id}): ${parts.join(', ')}`
177
+ }).join('\n')
178
+ }
179
+
180
+ server.registerTool('quilt_workspace_files', {
181
+ description: 'List the files in a workspace library: path, kind, size, version, who uploaded it and when, and its note. Narrow with folder (just that folder) or glob (like "**/*.png"; * stays in a folder, ** crosses folders).',
182
+ inputSchema: {
183
+ workspace: z.string().describe('The workspace name or id'),
184
+ folder: z.string().optional().describe('Only this folder, like "cuts" ("" for the top level)'),
185
+ glob: z.string().optional().describe('Only paths matching this, like "cuts/*.mp4" or "**/*.md"')
186
+ }
187
+ }, tool(async ({ workspace, folder, glob }) => {
188
+ const ws = await workspaceOf(workspace)
189
+ const q = folder !== undefined ? `?folder=${encodeURIComponent(folder)}` : ''
190
+ const [{ files }, names] = await Promise.all([call('GET', `${ws$(ws.id)}/files${q}`), namesIn(ws)])
191
+ const shown = glob ? files.filter((f) => globMatch(glob, f.path)) : files
192
+ if (!shown.length) return files.length ? `Nothing in ${ws.name} matches ${glob}.` : `No files in ${ws.name}${folder ? ` under ${folder}` : ''} yet.`
193
+ const now = Date.now()
194
+ return shown.map((f) => {
195
+ if (f.kind === 'folder') return `${f.path}/ folder`
196
+ const by = names.get(f.uploadedBy) || f.uploadedBy || ''
197
+ const bits = [formatBytes(f.size || 0), `v${f.version}`, by && `by ${by}`, ago(f.uploadedAt, now)].filter(Boolean)
198
+ return `${f.path} ${bits.join(', ')}${f.note ? ` "${f.note}"` : ''}`
199
+ }).join('\n')
200
+ }))
201
+
202
+ server.registerTool('quilt_workspace_read_file', {
203
+ description: `Read a file from a workspace library. Text up to ${formatBytes(maxInline)} comes back in the answer; anything else comes back as a download link${saveDir ? ' and is saved on this computer (the answer says where, as savedTo)' : ''}. Give version for an earlier version.`,
204
+ inputSchema: {
205
+ workspace: z.string().describe('The workspace name or id'),
206
+ path: z.string().describe('The file\'s path in the library, like "cuts/teaser.mp4"'),
207
+ version: z.number().int().positive().optional().describe('An earlier version number (default: the current one)')
208
+ }
209
+ }, tool(async ({ workspace, path: p, version }) => {
210
+ const ws = await workspaceOf(workspace)
211
+ const f = await entryAt(ws, p)
212
+ if (f.kind !== 'file') throw new Error(`${f.path} is a folder. quilt_workspace_files with folder lists what is in it.`)
213
+ const dl = await call('GET', `${file$(ws.id, f.id)}/download${version ? `?version=${version}` : ''}`)
214
+ const v = version || f.version
215
+ if (isTextual(dl.mime) && dl.size <= maxInline) {
216
+ const bytes = await fetchBytes(dl.url, maxInline)
217
+ return `${f.path} (version ${v}, ${formatBytes(bytes.length)}):\n\n${bytes.toString('utf8')}`
218
+ }
219
+ const lines = [`${f.path} (version ${v}) is ${dl.mime || 'a file'}, ${formatBytes(dl.size)}.`, `Download link (expires ${new Date(dl.expiresAt).toISOString()}): ${dl.url}`]
220
+ if (saveDir) {
221
+ try {
222
+ const file = savePath(ws, f.path)
223
+ const bytes = await fetchBytes(dl.url, Number.isFinite(dl.size) ? dl.size : MAX_FROM_PATH)
224
+ await fs.promises.mkdir(path.dirname(file), { recursive: true })
225
+ await fs.promises.writeFile(file, bytes)
226
+ lines.push(`savedTo: ${file}`)
227
+ } catch (err) {
228
+ lines.push(`Could not save a copy on this computer: ${err.message}`)
229
+ }
230
+ }
231
+ return lines.join('\n')
232
+ }))
233
+
234
+ const sources = readLocal ? 'text, base64 or fromPath' : 'text or base64'
235
+ const writeSchema = {
236
+ workspace: z.string().describe('The workspace name or id'),
237
+ path: z.string().describe('Where it goes in the library, like "drafts/brief.md". Missing folders are made; an existing file gets a new version.'),
238
+ text: z.string().optional().describe(`The file's contents as text (up to ${formatBytes(maxInline)})`),
239
+ base64: z.string().optional().describe(`The file's bytes as base64 (up to ${formatBytes(maxInline)})`),
240
+ ...(readLocal ? { fromPath: z.string().optional().describe(`A file in this project folder (or the temp folder) to upload, up to ${formatBytes(MAX_FROM_PATH)}`) } : {}),
241
+ note: z.string().optional().describe('A short note on what this is or what changed (under 300 characters)')
242
+ }
243
+ server.registerTool('quilt_workspace_write_file', {
244
+ description: `Put a file in a workspace library, from exactly one of ${sources}. Writing to an existing path adds a new version. Add a short note saying what it is. Needs edit access.`,
245
+ inputSchema: writeSchema
246
+ }, tool(async ({ workspace, path: p, text, base64, fromPath, note }) => {
247
+ const given = [text, base64, fromPath].filter((v) => v !== undefined && v !== null)
248
+ if (given.length !== 1) throw new Error(`Give exactly one of ${sources}.`)
249
+ if (fromPath !== undefined && !readLocal) throw new Error('fromPath works only with the Quilt MCP on your own computer. Send text or base64.')
250
+ const tooBig = (n) => new Error(`That is too large to send in a call (${formatBytes(n)}; at most ${formatBytes(maxInline)}).${readLocal ? ' Save it to a file and use fromPath.' : ' A person can upload it in the Quilt app.'}`)
251
+ let bytes
252
+ if (text !== undefined) {
253
+ bytes = Buffer.from(String(text), 'utf8')
254
+ if (bytes.length > maxInline) throw tooBig(bytes.length)
255
+ } else if (base64 !== undefined) {
256
+ // Whitespace (line breaks) first, then the size before decoding, so a huge string is
257
+ // refused without a huge buffer; the pattern has no nested repeats, so it is linear.
258
+ const b = String(base64).replace(/\s+/g, '')
259
+ if (Math.floor(b.length * 3 / 4) > maxInline + 3) throw tooBig(Math.floor(b.length * 3 / 4))
260
+ if (!BASE64.test(b)) throw new Error('base64 has characters base64 cannot have.')
261
+ bytes = Buffer.from(b, 'base64')
262
+ if (bytes.length > maxInline) throw tooBig(bytes.length)
263
+ } else {
264
+ bytes = await readLocal(String(fromPath))
265
+ if (bytes.length > MAX_FROM_PATH) throw new Error(`That file is too large (${formatBytes(bytes.length)}; at most ${formatBytes(MAX_FROM_PATH)}).`)
266
+ }
267
+ if (!bytes.length) throw new Error('That file is empty; the library keeps files with something in them.')
268
+ const ws = await workspaceOf(workspace)
269
+ const sha256 = crypto.createHash('sha256').update(bytes).digest('hex')
270
+ const started = await call('POST', `${ws$(ws.id)}/files`, { path: p, size: bytes.length, sha256, note: note || '' })
271
+ const { file, upload } = started
272
+ const status = await put(upload.url, bytes, { 'content-type': file.mime || 'application/octet-stream', ...(upload.headers || {}) })
273
+ if (status < 200 || status >= 300) throw new Error(`The upload did not go through (storage answered ${status}). Try again.`)
274
+ const done = await call('POST', `${file$(ws.id, file.id)}/done`, {})
275
+ return `Saved ${done.file.path} in ${ws.name} (version ${done.file.version}, ${formatBytes(done.file.size)}).`
276
+ }))
277
+
278
+ server.registerTool('quilt_workspace_make_folder', {
279
+ description: 'Make a folder in a workspace library, with any folders above it ("cuts/raw" makes both). Writing a file to a path makes its folders too. Needs edit access.',
280
+ inputSchema: {
281
+ workspace: z.string().describe('The workspace name or id'),
282
+ path: z.string().describe('The folder, like "cuts/raw"')
283
+ }
284
+ }, tool(async ({ workspace, path: p }) => {
285
+ const ws = await workspaceOf(workspace)
286
+ const { file } = await call('POST', `${ws$(ws.id)}/folders`, { path: p })
287
+ return `Made the folder ${file.path}/ in ${ws.name}.`
288
+ }))
289
+
290
+ server.registerTool('quilt_workspace_move_file', {
291
+ description: 'Rename or move a file or folder in a workspace library (a folder takes everything in it). Needs edit access.',
292
+ inputSchema: {
293
+ workspace: z.string().describe('The workspace name or id'),
294
+ path: z.string().describe('Its path now'),
295
+ to: z.string().describe('Its new path, like "final/teaser.mp4"')
296
+ }
297
+ }, tool(async ({ workspace, path: p, to }) => {
298
+ const ws = await workspaceOf(workspace)
299
+ const f = await entryAt(ws, p)
300
+ const { file } = await call('PATCH', file$(ws.id, f.id), { path: to })
301
+ return `Moved ${f.path} to ${file.path} in ${ws.name}.`
302
+ }))
303
+
304
+ server.registerTool('quilt_workspace_delete_file', {
305
+ description: 'Delete a file or folder (with everything in it) from a workspace library. Needs edit access.',
306
+ inputSchema: {
307
+ workspace: z.string().describe('The workspace name or id'),
308
+ path: z.string().describe('The path to delete')
309
+ }
310
+ }, tool(async ({ workspace, path: p }) => {
311
+ const ws = await workspaceOf(workspace)
312
+ const f = await entryAt(ws, p)
313
+ await call('DELETE', file$(ws.id, f.id))
314
+ return `Deleted ${f.path}${f.kind === 'folder' ? ' and everything in it' : ''} from ${ws.name}.`
315
+ }))
316
+
317
+ if (!webhook) return // hosted agents: quilt_webhook_subscribe is their one webhook, session.started included
318
+ server.registerTool('quilt_workspace_webhook', {
319
+ description: 'Have Quilt POST workspace events to a URL of yours: session.started when a session starts in a workspace that invites you to its sessions, with its join link. Answers the signing secret once. Setting it again replaces the URL and the secret.',
320
+ inputSchema: {
321
+ url: z.string().describe('An https URL that takes POSTs')
322
+ }
323
+ }, tool(async ({ url }) => {
324
+ const r = await call('PUT', '/v1/agents/me/webhook', { url })
325
+ return [
326
+ `Workspace events (${(r.events || []).join(', ')}) now go to ${r.url}.`,
327
+ `Secret (shown once; check x-quilt-signature with it): ${r.secret}`,
328
+ 'Each POST is JSON ({ event, id, ts, workspace, room, name, link, by, via }) with x-quilt-event, x-quilt-delivery, x-quilt-timestamp and ' +
329
+ 'x-quilt-signature: sha256=HMAC-SHA256(secret, "<timestamp>.<body>"). On session.started, join with quilt_join_session and the link; you wait until the session\'s owner lets you in.'
330
+ ].join('\n')
331
+ }))
332
+
333
+ server.registerTool('quilt_workspace_webhook_off', {
334
+ description: 'Stop sending workspace events to your webhook.',
335
+ inputSchema: {}
336
+ }, tool(async () => {
337
+ await call('DELETE', '/v1/agents/me/webhook')
338
+ return 'Workspace events are no longer sent to a webhook.'
339
+ }))
340
+ }
package/index.js DELETED
@@ -1,2 +0,0 @@
1
- // Quilt is coming soon: https://heyquilt.com
2
- module.exports = {}