heyquilt 0.0.1 → 0.3.106
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 +633 -2
- package/RELEASES.md +862 -0
- package/assets/logo.svg +1 -0
- package/bin/quilt.js +849 -0
- package/desktop/main.js +315 -0
- package/package.json +163 -7
- package/src/account.js +591 -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 +187 -0
- package/src/agents/common.js +21 -0
- package/src/agents/cursor-transcripts.js +177 -0
- package/src/agents/cursor.js +404 -0
- package/src/agents/index.js +69 -0
- package/src/ai-vm-signin.js +110 -0
- package/src/ai-vm.js +522 -0
- package/src/api/access.js +75 -0
- package/src/api/activity.js +181 -0
- package/src/api/agent-auth.js +136 -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 +133 -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 +1219 -0
- package/src/api/org-access.js +54 -0
- package/src/api/permissions.js +71 -0
- package/src/api/relay-cluster.js +545 -0
- package/src/api/relay-machines.js +92 -0
- package/src/api/releases.js +87 -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 +252 -0
- package/src/api/routes/ai-vm.js +495 -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 +179 -0
- package/src/api/routes/relay-cluster.js +90 -0
- package/src/api/routes/relay.js +76 -0
- package/src/api/routes/secrets.js +448 -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 +232 -0
- package/src/api/sandbox-keys.js +57 -0
- package/src/api/secrets-access.js +93 -0
- package/src/api/server.js +654 -0
- package/src/api/slugs.js +20 -0
- package/src/api/supabase-store.js +875 -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 +375 -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 +128 -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 +248 -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 +97 -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 +924 -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 +45 -0
- package/src/mcp-room.js +32 -0
- package/src/mcp.js +1258 -0
- package/src/merge3.js +240 -0
- package/src/merges.js +148 -0
- package/src/pass-source.js +183 -0
- package/src/passes.js +64 -0
- package/src/pathrules.js +134 -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 +214 -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 +166 -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 +1691 -0
- package/src/relay-metrics.js +140 -0
- package/src/relay-outbound.js +1273 -0
- package/src/relay-sync.js +716 -0
- package/src/relay-turn-sync.js +506 -0
- package/src/relay-upstream.js +804 -0
- package/src/relay-webhooks.js +111 -0
- package/src/releases.js +138 -0
- package/src/report.js +129 -0
- package/src/runner.js +307 -0
- package/src/same-edit.js +18 -0
- 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 +4214 -0
- package/src/session-access.js +170 -0
- package/src/session-name.js +15 -0
- package/src/session.js +7446 -0
- package/src/settings.js +46 -0
- package/src/setup.js +184 -0
- package/src/shell-path.js +46 -0
- package/src/status.js +197 -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 +145 -0
- package/src/turn-sync.js +212 -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 +2494 -0
- package/src/ui/app.js +546 -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 +108 -0
- package/src/ui/cloud-ai.js +221 -0
- package/src/ui/common.js +433 -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 +150 -0
- package/src/ui/git-rule.js +60 -0
- package/src/ui/home.js +1322 -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 +365 -0
- package/src/ui/loom.js +786 -0
- package/src/ui/mark.js +104 -0
- package/src/ui/rail-model.js +75 -0
- package/src/ui/rail.js +173 -0
- package/src/ui/releases.js +182 -0
- package/src/ui/sandbox-keys-model.js +53 -0
- package/src/ui/sandbox-keys.js +132 -0
- package/src/ui/schedule.js +156 -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 +3292 -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 +281 -0
- package/src/ui/workspaces.js +547 -0
- package/src/ui-server.js +1381 -0
- package/src/update-check.js +67 -0
- package/src/upstream.js +192 -0
- 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/webhooks.js +235 -0
- package/src/workspace-cli.js +62 -0
- package/src/workspace-tools.js +340 -0
- package/index.js +0 -2
package/bin/quilt.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import '../src/quiet-warnings.js'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import { parseArgs } from 'node:util'
|
|
7
|
+
import { adoptLegacyEnv } from '../src/legacy.js'
|
|
8
|
+
|
|
9
|
+
adoptLegacyEnv()
|
|
10
|
+
|
|
11
|
+
const HELP = `quilt: real-time pair vibe coding with any AI tool
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
quilt ui Open the app in your browser (start, join, chat)
|
|
15
|
+
quilt login [--no-browser] Sign in to your heyquilt.com account
|
|
16
|
+
quilt logout Sign this computer out
|
|
17
|
+
quilt whoami Show which account this computer is signed in to
|
|
18
|
+
quilt serve [--port 4321] [--data ./quilt-data] Run a relay server (see docs/hosting.md)
|
|
19
|
+
quilt api [--port 8787] [--memory] Run the accounts API (needs SUPABASE_URL etc.; --memory for local testing)
|
|
20
|
+
quilt agent join <link> --name <name> Join Quilt as an agent with an invite link from the website
|
|
21
|
+
[--agent-id <id>] (an agent that joined before comes back as itself)
|
|
22
|
+
[--here] (keeps its keys in this project's .quilt/: a sandbox that wipes home)
|
|
23
|
+
[--session <invite> --print-env] (keeps nothing: prints the sandbox's QUILT_AGENT_KEY instead)
|
|
24
|
+
quilt agent env --name <name> --session <invite> A new QUILT_AGENT_KEY for a sandbox, for an agent joined on this computer
|
|
25
|
+
quilt agent whoami --name <name> Show who a joined agent is
|
|
26
|
+
quilt join Rejoin this folder's last session, or start a new one
|
|
27
|
+
quilt join <invite-link> Join a partner's session in this folder
|
|
28
|
+
quilt sync [--dir <folder>] Agents in a sandbox: sync this folder with the session once and exit
|
|
29
|
+
(needs QUILT_AGENT_KEY; run it at the start and end of every turn).
|
|
30
|
+
With the key, status, messages, say, tasks, task, history, claim,
|
|
31
|
+
release, focus, commits, commit-request, tool and mcp work too
|
|
32
|
+
quilt setup Connect Claude Code / Cursor / others via MCP
|
|
33
|
+
quilt status Show collaborators, claims, activity, chat
|
|
34
|
+
quilt history [path] [--by name] [--since 2h] [--task id] [--diff] [-n 30]
|
|
35
|
+
Who changed what, when, and for which task
|
|
36
|
+
quilt chat Live chat (messages, DMs, files) in this terminal
|
|
37
|
+
quilt say [@name] <message> Message everyone, or one person with @name
|
|
38
|
+
quilt send <file> [@name] [message] Send a file (not added to the project)
|
|
39
|
+
quilt messages [--all] [--with name] [--grep text] [--before id] [-n 30]
|
|
40
|
+
Show unread messages, or read back: the conversation
|
|
41
|
+
with one person, a search, earlier messages
|
|
42
|
+
quilt get <message-id> [dest] Download a shared file again
|
|
43
|
+
quilt run -- <command> [args...] Run a command with the session's secrets: it gets NAME=quilt and
|
|
44
|
+
NAME's base URL (STRIPE_BASE_URL), and its requests go through
|
|
45
|
+
Quilt's vault; it never sees a value
|
|
46
|
+
quilt commit-request <message> [--files a,b] [--task id]
|
|
47
|
+
Ask a person to commit for you (when agents may not push here)
|
|
48
|
+
quilt commits Open commit requests and recent commits
|
|
49
|
+
quilt github Whether agents can push here through GitHub, and the link that connects it
|
|
50
|
+
quilt focus <what you're doing> Tell collaborators what you're working on
|
|
51
|
+
quilt claim <path|glob> [reason] Mark files as yours for now
|
|
52
|
+
quilt release <path|glob|*> Release a claim
|
|
53
|
+
quilt chat-link [name] [--minutes N] Owner: a link for a chat-only AI (ChatGPT, claude.ai, Grok); 10 minutes
|
|
54
|
+
quilt chat-link extend <name> <minutes> Owner: keep a chat link working for that long from now
|
|
55
|
+
quilt github-token [--clear] Owner: a read-only GitHub token (read from stdin) so the relay brings in commits while everyone's offline
|
|
56
|
+
quilt invite Print this session's invite code
|
|
57
|
+
quilt workspace list|files|get|put|write|mkdir|mv|rm The workspace library (quilt workspace for help)
|
|
58
|
+
quilt stop Shut down everything quilt is running (relay, app, syncs)
|
|
59
|
+
quilt doctor [folder] [--watch 30] Check what quilt can see of your Claude Code / Cursor chats
|
|
60
|
+
quilt mcp Run the MCP server (used by AI tools)
|
|
61
|
+
quilt ai-vm Run a person's AI on its own cloud machine (Quilt starts it there)
|
|
62
|
+
quilt hook Claude Code hook (installed by quilt setup; reads the event on stdin)
|
|
63
|
+
|
|
64
|
+
Join options:
|
|
65
|
+
--agent <name> Join as a Quilt agent saved with \`quilt agent join\` (default: your account)
|
|
66
|
+
--tool <tool> What you're coding with, e.g. claude, cursor (shown to others)
|
|
67
|
+
--dir <folder> Project folder (default: current folder)
|
|
68
|
+
--room <name> --secret <secret> Join/create a specific room instead of using an invite
|
|
69
|
+
--prefer local On first join, keep your local version of files that differ
|
|
70
|
+
(default: take the session's version and back yours up)
|
|
71
|
+
`
|
|
72
|
+
|
|
73
|
+
const cmd = process.argv[2]
|
|
74
|
+
const argv = process.argv.slice(3)
|
|
75
|
+
|
|
76
|
+
async function main () {
|
|
77
|
+
if (await withAgentKey()) return
|
|
78
|
+
switch (cmd) {
|
|
79
|
+
case 'serve': return serve()
|
|
80
|
+
case 'api': return apiCmd()
|
|
81
|
+
case 'agent': return agentCmd()
|
|
82
|
+
case 'ui': return ui()
|
|
83
|
+
case 'login': return login()
|
|
84
|
+
case 'logout': return logout()
|
|
85
|
+
case 'whoami': return whoami()
|
|
86
|
+
case 'join': return join()
|
|
87
|
+
case 'sync': return syncCmd()
|
|
88
|
+
case 'setup': return doSetup()
|
|
89
|
+
case 'mcp': return (await import('../src/mcp.js')).runMcp()
|
|
90
|
+
case 'ai-vm': try { await (await import('../src/ai-vm.js')).runAiVm() } catch (err) { fail(err.message) } return
|
|
91
|
+
case 'hook': process.exitCode = await (await import('../src/hooks.js')).runHook(); return
|
|
92
|
+
case 'status': return status()
|
|
93
|
+
case 'history': return history()
|
|
94
|
+
case 'say': return say()
|
|
95
|
+
case 'send': return sendFile()
|
|
96
|
+
case 'messages': case 'inbox': return messages()
|
|
97
|
+
case 'get': return getFile()
|
|
98
|
+
case 'commit-request': return commitRequest()
|
|
99
|
+
case 'commits': return commits()
|
|
100
|
+
case 'github': return githubStatusCmd()
|
|
101
|
+
case 'git-token': return gitToken()
|
|
102
|
+
case 'run': return runCmd()
|
|
103
|
+
case 'git-credential': return gitCredential()
|
|
104
|
+
case 'git-pre-push': return gitPrePush()
|
|
105
|
+
case 'chat': return chat()
|
|
106
|
+
case 'focus': return simple('/focus', { text: argv.join(' ') }, () => 'focus updated')
|
|
107
|
+
case 'claim': return simple('/claim', { pattern: argv[0], note: argv.slice(1).join(' ') }, (r) =>
|
|
108
|
+
`claimed ${argv[0]}` + (r.overlapping?.length ? `\nwarning: overlaps ${r.overlapping.map((c) => `${c.by}'s ${c.pattern}`).join(', ')}` : ''))
|
|
109
|
+
case 'release': return simple('/release', { pattern: argv[0] || '*' }, (r) => `released ${r.released} claim(s)`)
|
|
110
|
+
case 'chat-link': {
|
|
111
|
+
if (argv[0] === 'extend') {
|
|
112
|
+
if (!argv[1] || !Number(argv[2])) fail('usage: quilt chat-link extend <name> <minutes>')
|
|
113
|
+
return simple('/chat-link/extend', { who: argv[1], minutes: Number(argv[2]) }, (r) => `${r.name}'s chat link now works until ${new Date(r.expiresAt).toLocaleString()}.`)
|
|
114
|
+
}
|
|
115
|
+
const i = argv.indexOf('--minutes')
|
|
116
|
+
const minutes = i >= 0 ? Number(argv[i + 1]) : undefined
|
|
117
|
+
const name = argv.filter((a, k) => !(i >= 0 && (k === i || k === i + 1))).join(' ')
|
|
118
|
+
return simple('/chat-link', { name, minutes }, (r) => `Chat link for ${r.name}, until ${new Date(r.expiresAt).toLocaleTimeString()}:\n\n ${r.url}\n\nPaste it into ChatGPT, claude.ai or Grok with "Open this link and follow it to join our Quilt session."\nAnyone with it can act as ${r.name}. Extend it with \`quilt chat-link extend ${r.name} <minutes>\`; once it runs out, make a new one.`)
|
|
119
|
+
}
|
|
120
|
+
case 'github-token': {
|
|
121
|
+
// Read from stdin, never the command line (where shell history and ps would keep it).
|
|
122
|
+
if (argv[0] === '--clear') return simple('/github-token', { token: '' }, () => 'The relay no longer has a GitHub token for this session.')
|
|
123
|
+
if (argv.length) fail('usage: quilt github-token [--clear] (the token is read from stdin: pbpaste | quilt github-token)')
|
|
124
|
+
if (process.stdin.isTTY) process.stderr.write('Paste a read-only GitHub token (contents: read) and press Enter: ')
|
|
125
|
+
const token = await new Promise((resolve) => {
|
|
126
|
+
let buf = ''
|
|
127
|
+
process.stdin.setEncoding('utf8')
|
|
128
|
+
process.stdin.on('data', (d) => { buf += d; if (process.stdin.isTTY && buf.includes('\n')) { process.stdin.pause(); resolve(buf) } })
|
|
129
|
+
process.stdin.on('end', () => resolve(buf))
|
|
130
|
+
})
|
|
131
|
+
if (!token.trim()) fail('no token given')
|
|
132
|
+
return simple('/github-token', { token: token.trim() }, () => 'The relay has the token: it brings in commits from your private repository while nobody\'s folder is online.')
|
|
133
|
+
}
|
|
134
|
+
case 'invite': return invite()
|
|
135
|
+
case 'workspace': case 'workspaces': return workspaceCmd()
|
|
136
|
+
case 'stop': return stopAll()
|
|
137
|
+
case 'doctor': {
|
|
138
|
+
const i = argv.indexOf('--watch')
|
|
139
|
+
const secs = i >= 0 ? Number(argv[i + 1]) || 30 : 0
|
|
140
|
+
const dir = argv.find((a, k) => !a.startsWith('--') && !(i >= 0 && k === i + 1))
|
|
141
|
+
return (await import('../src/doctor.js')).doctor({ dir, watchSeconds: secs })
|
|
142
|
+
}
|
|
143
|
+
case '-v': case '--version': case 'version':
|
|
144
|
+
console.log(`quilt ${(await import('../src/releases.js')).currentVersion()}`); return
|
|
145
|
+
case undefined: case '-h': case '--help': case 'help':
|
|
146
|
+
process.stdout.write(HELP); return
|
|
147
|
+
default:
|
|
148
|
+
console.error(`unknown command: ${cmd}\n`); process.stdout.write(HELP); process.exit(1)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function serve () {
|
|
153
|
+
const { values } = parseArgs({ args: argv, options: { port: { type: 'string' }, host: { type: 'string' }, data: { type: 'string' }, key: { type: 'string' } } })
|
|
154
|
+
const { startServer } = await import('../src/server.js')
|
|
155
|
+
const port = Number(values.port || process.env.PORT || 4321)
|
|
156
|
+
const dataDir = path.resolve(values.data || process.env.QUILT_DATA || './quilt-data')
|
|
157
|
+
const srv = await startServer({ port, host: values.host || process.env.QUILT_HOST || '0.0.0.0', dataDir, ...(values.key ? { relayKey: values.key } : {}) })
|
|
158
|
+
// One bad request must never take every session down with it.
|
|
159
|
+
process.on('uncaughtException', (err) => console.error('relay: unexpected error, carrying on:', err))
|
|
160
|
+
process.on('unhandledRejection', (err) => console.error('relay: unexpected rejection, carrying on:', err))
|
|
161
|
+
const c = srv.config
|
|
162
|
+
console.log(`quilt relay listening on :${srv.port} (data: ${dataDir})`)
|
|
163
|
+
console.log(` sign-in: ${c.passPublicKey ? 'a pass from the accounts API is required; new sessions are limited per account' : 'off (set QUILT_PASS_PUBLIC_KEY to require it)'}`)
|
|
164
|
+
console.log(` dashboard: ${c.apiUrl && c.relayApiSecret ? `reports who is in which session to ${c.apiUrl}` : 'off (set QUILT_API_URL and RELAY_API_SECRET to report sessions to the dashboard)'}`)
|
|
165
|
+
if (!c.passPublicKey) console.log(` new sessions: ${c.relayKey ? 'need the relay key' : 'open to anyone who can reach this relay (set QUILT_RELAY_KEY to restrict)'}`)
|
|
166
|
+
console.log(` limits: ${Math.round(c.maxRoomBytes / 1048576)} MB per session, ${Math.round(c.maxRoomFileBytes / 1048576)} MB of shared files, ${c.maxConnsPerIp} connections per address, idle sessions removed after ${c.roomTtlDays} days`)
|
|
167
|
+
console.log(`for development, point Quilt at it with QUILT_SERVER=ws://<this-host>:${srv.port}`)
|
|
168
|
+
const { registerProcess } = await import('../src/procs.js')
|
|
169
|
+
registerProcess('relay', { port: srv.port, dataDir })
|
|
170
|
+
const shutdown = async () => { await srv.close(); process.exit(0) }
|
|
171
|
+
process.on('SIGINT', shutdown)
|
|
172
|
+
process.on('SIGTERM', shutdown)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function apiCmd () {
|
|
176
|
+
const { values } = parseArgs({ args: argv, options: { port: { type: 'string' }, host: { type: 'string' }, memory: { type: 'boolean' } } })
|
|
177
|
+
const { startApi } = await import('../src/api/server.js')
|
|
178
|
+
const { makeFileStore } = await import('../src/api/file-store.js')
|
|
179
|
+
const env = process.env
|
|
180
|
+
let store, verifyUser, mailer
|
|
181
|
+
if (values.memory) {
|
|
182
|
+
const { createMemoryStore } = await import('../src/api/memory-store.js')
|
|
183
|
+
store = createMemoryStore(); store.addUser('local', { name: 'Local user' })
|
|
184
|
+
verifyUser = async (t) => (t === 'local' ? { userId: 'local', email: '' } : null)
|
|
185
|
+
console.log('in-memory mode: use "Authorization: Bearer local" as the signed-in user')
|
|
186
|
+
const { createConsoleMailer } = await import('../src/api/mailer.js')
|
|
187
|
+
mailer = createConsoleMailer(console.log)
|
|
188
|
+
} else {
|
|
189
|
+
for (const k of ['SUPABASE_URL', 'SUPABASE_SERVICE_ROLE_KEY', 'QUILT_SITE_URL', 'SMTP_URL', 'SMTP_FROM', 'PASS_SIGNING_KEY']) if (!env[k]) fail(`${k} is not set`)
|
|
190
|
+
const { createSupabaseStore } = await import('../src/api/supabase-store.js')
|
|
191
|
+
const { createUserVerifier } = await import('../src/api/auth.js')
|
|
192
|
+
store = createSupabaseStore({ url: env.SUPABASE_URL, serviceKey: env.SUPABASE_SERVICE_ROLE_KEY })
|
|
193
|
+
verifyUser = createUserVerifier({ supabaseUrl: env.SUPABASE_URL })
|
|
194
|
+
const { createSmtpMailer } = await import('../src/api/mailer.js')
|
|
195
|
+
mailer = createSmtpMailer({ url: env.SMTP_URL, from: env.SMTP_FROM })
|
|
196
|
+
}
|
|
197
|
+
let passKey = env.PASS_SIGNING_KEY || ''
|
|
198
|
+
if (values.memory && !passKey) {
|
|
199
|
+
const { newPassKeys } = await import('../src/passes.js')
|
|
200
|
+
const keys = newPassKeys()
|
|
201
|
+
passKey = keys.privateKey
|
|
202
|
+
console.log(`pass signing key made for this run; start a local relay with QUILT_PASS_PUBLIC_KEY=${keys.publicKey}`)
|
|
203
|
+
}
|
|
204
|
+
// In memory anyone may use "Bearer local", so only listen on this machine unless asked.
|
|
205
|
+
const host = values.host || (values.memory ? '127.0.0.1' : '0.0.0.0')
|
|
206
|
+
const port = Number(values.port || env.PORT || 8787)
|
|
207
|
+
const api = await startApi({
|
|
208
|
+
port, host, store, verifyUser, mailer, passKey,
|
|
209
|
+
reportKey: env.QUILT_REPORT_KEY || '',
|
|
210
|
+
// The relay signs its presence reports with this (scripts/relay-api-secret.mjs).
|
|
211
|
+
relaySecret: env.RELAY_API_SECRET || '',
|
|
212
|
+
// Quilt's GitHub App (GITHUB_APP_*), for agents' commits; off when not set.
|
|
213
|
+
github: (await import('../src/api/github-app.js')).appConfig(env),
|
|
214
|
+
// Fly's Machines API for the relay (RELAY_FLY_API_TOKEN, QUILT_RELAY_APP): heavy sessions get a relay machine of their own.
|
|
215
|
+
relayFly: (await import('../src/api/relay-machines.js')).relayFlyFromEnv(env),
|
|
216
|
+
// The load each shared relay machine stays under, and when the cluster scales down (QUILT_RELAY_TARGET_*, QUILT_RELAY_*; relay-cluster.js TARGETS).
|
|
217
|
+
relayTargets: (await import('../src/api/relay-cluster.js')).relayTargetsFromEnv(env),
|
|
218
|
+
// Where agents reach this API (invite links point here).
|
|
219
|
+
apiUrl: env.QUILT_API_PUBLIC_URL || (values.memory ? `http://${host}:${port}` : 'https://api.heyquilt.com'),
|
|
220
|
+
siteUrl: env.QUILT_SITE_URL || 'http://localhost:3000',
|
|
221
|
+
trustProxy: /^(1|true|yes)$/i.test(env.QUILT_TRUST_PROXY || ''), log: console.log,
|
|
222
|
+
workspaces: env.QUILT_WORKSPACES === '1' || env.QUILT_WORKSPACES === 'true',
|
|
223
|
+
// Secrets: the vault (quilt-vault) and the secret it calls this API with; off when not set.
|
|
224
|
+
vaultSecret: env.QUILT_VAULT_SECRET || '',
|
|
225
|
+
vaultUrl: env.QUILT_VAULT_URL || 'https://vault.heyquilt.com',
|
|
226
|
+
// People's cloud AI machines (FLY_API_TOKEN, AI_VM_APP, AI_VM_IMAGE, AI_VM_REGION); off when not set.
|
|
227
|
+
fly: (await import('../src/api/fly-machines.js')).flyFromEnv(env),
|
|
228
|
+
fileStore: makeFileStore(
|
|
229
|
+
{ storageUrl: env.QUILT_STORAGE_URL, storageKey: env.QUILT_STORAGE_KEY, storageBucket: env.QUILT_STORAGE_WS_BUCKET },
|
|
230
|
+
values.memory ? fs.mkdtempSync(path.join(os.tmpdir(), 'quilt-api-files-')) : path.resolve(env.QUILT_API_DATA || './quilt-api-data')
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
console.log(`quilt accounts API listening on ${api.url}`)
|
|
234
|
+
if (!env.RELAY_API_SECRET) console.log('RELAY_API_SECRET is not set: the relay cannot report sessions for the dashboard')
|
|
235
|
+
if (!env.FLY_API_TOKEN || !env.AI_VM_APP || !env.AI_VM_IMAGE) console.log('FLY_API_TOKEN, AI_VM_APP or AI_VM_IMAGE is not set: people cannot connect a cloud AI')
|
|
236
|
+
if (!env.QUILT_VAULT_SECRET) console.log('QUILT_VAULT_SECRET is not set: secrets are off')
|
|
237
|
+
const shutdown = async () => { await api.close(); process.exit(0) }
|
|
238
|
+
process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function agentCmd () {
|
|
242
|
+
const usage = 'usage: quilt agent join <link> --name <name> [--agent-id <id>] [--provider <p>] [--type <t>] [--description <d>] [--here]\n' +
|
|
243
|
+
' quilt agent join <link> --name <name> --session <session invite link> --print-env\n' +
|
|
244
|
+
' quilt agent env --name <name> --session <session invite link> [--key-name <label>]\n' +
|
|
245
|
+
' quilt agent whoami --name <name>'
|
|
246
|
+
const [sub, ...rest] = argv
|
|
247
|
+
let parsed = { values: {}, positionals: [] }
|
|
248
|
+
try {
|
|
249
|
+
parsed = parseArgs({ args: rest, allowPositionals: true, options: { name: { type: 'string' }, 'agent-id': { type: 'string' }, provider: { type: 'string' }, type: { type: 'string' }, description: { type: 'string' }, here: { type: 'boolean' }, 'print-env': { type: 'boolean' }, session: { type: 'string' }, 'key-name': { type: 'string' } } })
|
|
250
|
+
} catch { fail(usage) }
|
|
251
|
+
const { values, positionals } = parsed
|
|
252
|
+
if (!values.name || !((sub === 'join' && positionals[0]) || sub === 'whoami' || sub === 'env')) fail(usage)
|
|
253
|
+
const { agentJoin, agentJoinUnsaved, makeEnvKey, savedAgentEnvKey, agentWhoami, describeAgent } = await import('../src/agent-join.js')
|
|
254
|
+
// A sandbox's QUILT_AGENT_KEY goes to stdout alone (pipe it where it belongs); what to do with it, to stderr.
|
|
255
|
+
const printEnv = (envKey) => {
|
|
256
|
+
console.log(`QUILT_AGENT_KEY=${envKey}`)
|
|
257
|
+
console.error(`Put it in your sandbox's environment or secrets settings as QUILT_AGENT_KEY (once). The agent then runs \`npx heyquilt sync\` at the start and end of every turn. It works until it is revoked on heyquilt.com (Agents, ${values.name}'s keys). Keep it secret: it signs in as ${values.name}.`)
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
if (sub === 'join' && values['print-env']) {
|
|
261
|
+
if (!values.session) fail('--print-env needs --session <session invite link>: the key says which session the sandbox works in.')
|
|
262
|
+
if (values.here) fail('--print-env keeps nothing on disk; leave out --here.')
|
|
263
|
+
const joined = await agentJoinUnsaved({ link: positionals[0], name: values.name, agentId: values['agent-id'], provider: values.provider, type: values.type, description: values.description })
|
|
264
|
+
console.error(`${joined.rejoined ? 'Joined Quilt again as' : 'Joined Quilt as'} ${values.name} (agent id ${joined.agentId}). Nothing was saved on this computer.`)
|
|
265
|
+
printEnv(await makeEnvKey({ api: joined.api, accessKey: joined.accessKey, session: values.session, keyName: values['key-name'] }))
|
|
266
|
+
} else if (sub === 'join') await agentJoin({ link: positionals[0], name: values.name, agentId: values['agent-id'], provider: values.provider, type: values.type, description: values.description, here: !!values.here })
|
|
267
|
+
else if (sub === 'env') printEnv(await savedAgentEnvKey({ name: values.name, session: values.session, keyName: values['key-name'] }))
|
|
268
|
+
else console.log(describeAgent(await agentWhoami({ name: values.name })))
|
|
269
|
+
} catch (err) {
|
|
270
|
+
fail(err.message)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function join () {
|
|
275
|
+
const { values, positionals } = parseArgs({
|
|
276
|
+
args: argv,
|
|
277
|
+
allowPositionals: true,
|
|
278
|
+
options: {
|
|
279
|
+
room: { type: 'string' }, secret: { type: 'string' },
|
|
280
|
+
tool: { type: 'string' }, dir: { type: 'string' }, prefer: { type: 'string' }, agent: { type: 'string' }
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
const { runSession, decodeInvite, newConn, readConfig, personsFolder, agentCopyFolder } = await import('../src/runner.js')
|
|
284
|
+
const { sessionPasses } = await import('../src/pass-source.js')
|
|
285
|
+
const { clearAccount, readAccount, resumeAccount } = await import('../src/account.js')
|
|
286
|
+
// Every session signs in: as this computer's account, or as a saved agent. A computer
|
|
287
|
+
// linked before signs itself back in when its sign-in was lost.
|
|
288
|
+
if (!values.agent && !readAccount()) await resumeAccount().catch(() => null)
|
|
289
|
+
let auth
|
|
290
|
+
try { auth = sessionPasses({ agent: values.agent || null }) } catch (err) { fail(err.message) }
|
|
291
|
+
const whyStopped = (err) => {
|
|
292
|
+
if (!err.signedOut || values.agent) return err.message
|
|
293
|
+
clearAccount()
|
|
294
|
+
return 'This computer was signed out. Run quilt login again.'
|
|
295
|
+
}
|
|
296
|
+
let dir = path.resolve(values.dir || '.')
|
|
297
|
+
const saved = readConfig(dir) || {}
|
|
298
|
+
|
|
299
|
+
const { unsupportedRelay } = await import('../src/settings.js')
|
|
300
|
+
let conn
|
|
301
|
+
if (positionals[0]) {
|
|
302
|
+
try { conn = decodeInvite(positionals[0]) } catch (err) { fail(err.message) }
|
|
303
|
+
} else if (values.room) {
|
|
304
|
+
conn = newConn()
|
|
305
|
+
conn.room = values.room
|
|
306
|
+
if (values.secret || process.env.QUILT_SECRET) conn.secret = values.secret || process.env.QUILT_SECRET
|
|
307
|
+
} else if (saved.server && !unsupportedRelay(saved.server)) {
|
|
308
|
+
conn = { server: saved.server, room: saved.room, secret: saved.secret, ...(saved.viewSecret ? { viewSecret: saved.viewSecret } : {}) }
|
|
309
|
+
} else {
|
|
310
|
+
if (saved.server) console.log("This folder's last session ran on your computer's own relay, which Quilt no longer supports. Starting a new session.")
|
|
311
|
+
conn = newConn()
|
|
312
|
+
console.log('starting a new session')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// An agent never takes over a folder a person synced from this computer (they'd lose it
|
|
316
|
+
// from the app's Recent list and couldn't get back in): it keeps its own copy of the room.
|
|
317
|
+
if (auth.kind === 'agent' && personsFolder(dir, auth.name)) {
|
|
318
|
+
const copy = agentCopyFolder(conn.room, auth.name)
|
|
319
|
+
console.log(`${dir} is a person's own copy of a session on this computer and stays theirs: syncing ${copy} instead.`)
|
|
320
|
+
dir = copy
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const stamp = () => new Date().toLocaleTimeString()
|
|
324
|
+
console.log(`quilt: syncing ${dir}`)
|
|
325
|
+
let run
|
|
326
|
+
try {
|
|
327
|
+
let tool = values.tool || saved.tool
|
|
328
|
+
if (!tool && auth.kind === 'agent') {
|
|
329
|
+
try {
|
|
330
|
+
const { agentWhoami } = await import('../src/agent-join.js')
|
|
331
|
+
const { toolLabel } = await import('../src/agents/common.js')
|
|
332
|
+
const me = await agentWhoami({ name: auth.name })
|
|
333
|
+
const provider = me?.agent?.provider
|
|
334
|
+
if (provider) {
|
|
335
|
+
const labeled = toolLabel(provider)
|
|
336
|
+
const known = new Set(['Claude Code', 'Cursor', 'Codex', 'xAI', 'Windsurf', 'Zed', 'GitHub Copilot', 'Aider'])
|
|
337
|
+
if (known.has(labeled)) tool = labeled
|
|
338
|
+
else tool = labeled || tool
|
|
339
|
+
}
|
|
340
|
+
} catch {}
|
|
341
|
+
}
|
|
342
|
+
run = await runSession({
|
|
343
|
+
dir,
|
|
344
|
+
conn,
|
|
345
|
+
name: auth.name,
|
|
346
|
+
tool,
|
|
347
|
+
prefer: values.prefer === 'local' ? 'local' : 'remote',
|
|
348
|
+
kind: auth.kind,
|
|
349
|
+
passes: auth.passes,
|
|
350
|
+
apiAuth: auth.apiAuth,
|
|
351
|
+
identity: auth.identity,
|
|
352
|
+
inviteServer: saved.room === conn.room ? saved.inviteServer : undefined,
|
|
353
|
+
onLog: (m) => console.log(`[${stamp()}] ${m}`),
|
|
354
|
+
onDebug: process.env.QUILT_DEBUG ? (m) => console.log(`[${stamp()}] debug: ${m}`) : undefined,
|
|
355
|
+
onFatal: (err) => fail(whyStopped(err))
|
|
356
|
+
})
|
|
357
|
+
} catch (err) {
|
|
358
|
+
fail(whyStopped(err))
|
|
359
|
+
}
|
|
360
|
+
console.log(` room ${conn.room} on ${conn.server} as "${run.session.name}"`)
|
|
361
|
+
|
|
362
|
+
const { registerProcess } = await import('../src/procs.js')
|
|
363
|
+
registerProcess('sync', { dir })
|
|
364
|
+
console.log(`\nInvite your partner by sending them this link:\n\n ${run.invite}\n\nThey paste it into quilt (Join a session), or run \`quilt join <link>\` in an empty (or matching) folder.\n`)
|
|
365
|
+
console.log('Tip: run `quilt setup` once so your AI tools can see each other. Ctrl+C to stop.\n')
|
|
366
|
+
|
|
367
|
+
const stop = async () => {
|
|
368
|
+
console.log('\nstopping…')
|
|
369
|
+
await run.stop()
|
|
370
|
+
process.exit(0)
|
|
371
|
+
}
|
|
372
|
+
process.on('SIGINT', stop)
|
|
373
|
+
process.on('SIGTERM', stop)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* A sandbox with a QUILT_AGENT_KEY and no quilt join running: the session's commands, and
|
|
378
|
+
* `quilt mcp`, go to the hosted tools as the key's agent (src/key-cli.js, src/mcp-proxy.js).
|
|
379
|
+
* Returns whether the command was run here.
|
|
380
|
+
*/
|
|
381
|
+
async function withAgentKey () {
|
|
382
|
+
const { KEY_COMMANDS, KEY_USAGE, runKeyCommand } = await import('../src/key-cli.js')
|
|
383
|
+
if (!KEY_COMMANDS.has(cmd) && cmd !== 'mcp') return false
|
|
384
|
+
const keyOnly = ['tasks', 'task', 'tool'].includes(cmd)
|
|
385
|
+
const { findDaemon } = await import('../src/control.js')
|
|
386
|
+
if (findDaemon()) {
|
|
387
|
+
if (keyOnly) fail(`quilt ${cmd} is for an agent in a sandbox (QUILT_AGENT_KEY). With quilt join running here, your AI has the quilt_* tools: run \`quilt setup\` once.`)
|
|
388
|
+
return false
|
|
389
|
+
}
|
|
390
|
+
const key = await envKey()
|
|
391
|
+
if (!key) {
|
|
392
|
+
if (keyOnly) fail(`quilt ${cmd} needs QUILT_AGENT_KEY, the key for this agent set in your sandbox's environment or secrets settings (heyquilt.com: Agents, then Sandbox key).\n\n${KEY_USAGE}`)
|
|
393
|
+
return false
|
|
394
|
+
}
|
|
395
|
+
const { hostedClient } = await import('../src/hosted-client.js')
|
|
396
|
+
const client = hostedClient({ key })
|
|
397
|
+
if (cmd === 'mcp') {
|
|
398
|
+
await (await import('../src/mcp-proxy.js')).runMcpProxy({ key, client })
|
|
399
|
+
return true
|
|
400
|
+
}
|
|
401
|
+
const stdin = cmd === 'tool' && argv[1] === '-' ? await readStdin() : ''
|
|
402
|
+
process.exitCode = await runKeyCommand(cmd, argv, { client, stdin })
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** The sandbox's QUILT_AGENT_KEY (src/ui/agent-env-key.js), or null; a malformed one stops here. */
|
|
407
|
+
async function envKey () {
|
|
408
|
+
const { agentEnvKeyFromEnv } = await import('../src/ui/agent-env-key.js')
|
|
409
|
+
try { return agentEnvKeyFromEnv(process.env) } catch (err) { fail(err.message) }
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** `quilt sync`: one shot, for an agent in a sandbox (src/turn-sync.js). */
|
|
413
|
+
async function syncCmd () {
|
|
414
|
+
let values
|
|
415
|
+
try { ({ values } = parseArgs({ args: argv, options: { dir: { type: 'string' } } })) } catch { fail('usage: quilt sync [--dir <folder>]') }
|
|
416
|
+
const dir = path.resolve(values.dir || '.')
|
|
417
|
+
const key = await envKey()
|
|
418
|
+
if (!key) fail('quilt sync needs QUILT_AGENT_KEY: the key for this agent, set once in your sandbox\'s environment or secrets settings. The person you work for makes it on heyquilt.com (Agents, then Sandbox key on your agent), or runs `quilt agent env --name <agent> --session <invite link>`. On a computer of your own, run `quilt join` instead: it syncs live.')
|
|
419
|
+
const { findDaemon } = await import('../src/control.js')
|
|
420
|
+
const live = findDaemon(dir)
|
|
421
|
+
if (live) fail(`quilt join is already syncing ${live.dir} live, so there is nothing for quilt sync to do here.`)
|
|
422
|
+
const { turnSync, describeSync } = await import('../src/turn-sync.js')
|
|
423
|
+
const { hostedClient } = await import('../src/hosted-client.js')
|
|
424
|
+
const client = hostedClient({ key })
|
|
425
|
+
try {
|
|
426
|
+
console.log(describeSync(await turnSync({ dir, key, client, log: (m) => console.log(m) }), { name: key.name }))
|
|
427
|
+
} catch (err) { fail(err.message) }
|
|
428
|
+
// What waits for the agent: said right away, so a turn starts with it.
|
|
429
|
+
try {
|
|
430
|
+
const inbox = await client.call('quilt_inbox', {})
|
|
431
|
+
if (inbox && !/^Nothing new for you/.test(inbox)) console.log(`\n${inbox}`)
|
|
432
|
+
} catch {}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function ui () {
|
|
436
|
+
const { values } = parseArgs({ args: argv, options: { port: { type: 'string' }, 'no-open': { type: 'boolean' }, preview: { type: 'boolean' } } })
|
|
437
|
+
const { startUi } = await import('../src/ui-server.js')
|
|
438
|
+
const { registerProcess, stopProcesses } = await import('../src/procs.js')
|
|
439
|
+
;(await import('../src/integrations.js')).registerOnStart((line) => console.log(line))
|
|
440
|
+
const app = await startUi({
|
|
441
|
+
port: Number(values.port || 7420),
|
|
442
|
+
preview: !!values.preview,
|
|
443
|
+
// The app's "Shut down" button: stop every other quilt process, then this one.
|
|
444
|
+
onShutdown: async () => {
|
|
445
|
+
console.log('\nshutting down everything…')
|
|
446
|
+
await stopProcesses()
|
|
447
|
+
await app.close()
|
|
448
|
+
process.exit(0)
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
registerProcess('app', { port: app.port, url: app.url })
|
|
452
|
+
console.log(`quilt is running at:\n\n ${app.url}\n`)
|
|
453
|
+
console.log('Keep this terminal open while you work. Ctrl+C to stop, or `quilt stop` to shut everything down.')
|
|
454
|
+
if (!values['no-open']) openBrowser(app.url)
|
|
455
|
+
const stop = async () => { console.log('\nstopping…'); await app.close(); process.exit(0) }
|
|
456
|
+
process.on('SIGINT', stop)
|
|
457
|
+
process.on('SIGTERM', stop)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function login () {
|
|
461
|
+
const { values } = parseArgs({ args: argv, options: { 'no-browser': { type: 'boolean' } } })
|
|
462
|
+
const { readAccount, saveAccount, startLink, waitForLink, accountFromProfile, resumeAccount } = await import('../src/account.js')
|
|
463
|
+
const { loadIdentity } = await import('../src/identity.js')
|
|
464
|
+
const current = readAccount()
|
|
465
|
+
if (current) return console.log(`Already signed in as ${current.account.name} (${current.account.email}). Run quilt logout first to switch accounts.`)
|
|
466
|
+
const identity = loadIdentity()
|
|
467
|
+
// Linked before: signs straight back in, no browser.
|
|
468
|
+
const back = await resumeAccount({ identity, asked: true }).catch(() => null)
|
|
469
|
+
if (back) return console.log(`Signed in as ${back.account.name} (${back.account.email}).`)
|
|
470
|
+
let link
|
|
471
|
+
try { link = await startLink({ identity }) } catch (err) { fail(err.message) }
|
|
472
|
+
console.log(`To sign in, open this page and approve this computer:\n\n ${link.verificationUrl}\n\nCheck it shows the code ${link.userCode}. Waiting…`)
|
|
473
|
+
if (!values['no-browser']) openBrowser(link.verificationUrl)
|
|
474
|
+
let r
|
|
475
|
+
try { r = await waitForLink({ identity, link }) } catch (err) { fail(err.expired ? 'The code expired. Run quilt login again.' : err.message) }
|
|
476
|
+
const account = accountFromProfile(r.profile)
|
|
477
|
+
saveAccount({ token: r.token, account, signedInAt: Date.now() })
|
|
478
|
+
console.log(`Signed in as ${account.name} (${account.email}).`)
|
|
479
|
+
;(await import('../src/integrations.js')).registerOnStart((line) => console.log(line))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async function logout () {
|
|
483
|
+
const { readAccount, signOut } = await import('../src/account.js')
|
|
484
|
+
const current = readAccount()
|
|
485
|
+
if (!current) return console.log('Not signed in')
|
|
486
|
+
await signOut({ token: current.token })
|
|
487
|
+
console.log(`Signed out of ${current.account.email}.`)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function whoami () {
|
|
491
|
+
const { readAccount } = await import('../src/account.js')
|
|
492
|
+
const current = readAccount()
|
|
493
|
+
if (!current) {
|
|
494
|
+
console.log('Not signed in')
|
|
495
|
+
process.exitCode = 1
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
console.log(`${current.account.name} (${current.account.email})`)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
async function openBrowser (url) {
|
|
502
|
+
const { spawn } = await import('node:child_process')
|
|
503
|
+
const [cmd, args] = process.platform === 'darwin' ? ['open', [url]]
|
|
504
|
+
: process.platform === 'win32' ? ['cmd', ['/c', 'start', '', url]]
|
|
505
|
+
: ['xdg-open', [url]]
|
|
506
|
+
try { spawn(cmd, args, { stdio: 'ignore', detached: true }).on('error', () => {}).unref() } catch {}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function doSetup () {
|
|
510
|
+
const { setup } = await import('../src/setup.js')
|
|
511
|
+
const changed = setup(process.cwd())
|
|
512
|
+
if (!changed.length) return console.log('already set up')
|
|
513
|
+
console.log('updated:\n' + changed.map((c) => ` - ${c}`).join('\n'))
|
|
514
|
+
console.log('\nAn AI tool that was already open picks up the "quilt" MCP server when it restarts.')
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function daemonOrFail () {
|
|
518
|
+
const { findDaemon, call } = await import('../src/control.js')
|
|
519
|
+
const d = findDaemon()
|
|
520
|
+
if (!d) fail('quilt is not running here. Start it with `quilt join` in the project folder.')
|
|
521
|
+
return { d, call }
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function status () {
|
|
525
|
+
const { d, call } = await daemonOrFail()
|
|
526
|
+
console.log((await call(d, 'GET', '/status')).markdown)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async function history () {
|
|
530
|
+
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { by: { type: 'string' }, since: { type: 'string' }, task: { type: 'string' }, diff: { type: 'boolean' }, n: { type: 'string', short: 'n' } } })
|
|
531
|
+
const { formatHistory } = await import('../src/history.js')
|
|
532
|
+
await simple('/history', { path: positionals[0], by: values.by, since: values.since, task: values.task, limit: Number(values.n || 30) }, (r) => formatHistory(r.entries, { withDiff: !!values.diff }))
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async function simple (route, body, format) {
|
|
536
|
+
const { d, call } = await daemonOrFail()
|
|
537
|
+
try { console.log(format(await call(d, 'POST', route, body))) } catch (err) { fail(err.message) }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/** Splits "@bob rest of message" into { to: 'bob', rest: [...] }. */
|
|
541
|
+
function splitRecipient (args) {
|
|
542
|
+
const i = args.indexOf('--to')
|
|
543
|
+
if (i !== -1) return { to: args[i + 1], rest: [...args.slice(0, i), ...args.slice(i + 2)] }
|
|
544
|
+
if (args[0] && args[0].startsWith('@') && args[0].length > 1) return { to: args[0].slice(1), rest: args.slice(1) }
|
|
545
|
+
return { to: undefined, rest: args }
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async function say () {
|
|
549
|
+
const flag = (f) => { const i = argv.indexOf(f); if (i === -1) return false; argv.splice(i, 1); return true }
|
|
550
|
+
const everyone = flag('--everyone')
|
|
551
|
+
const also = flag('--also')
|
|
552
|
+
const { to, rest } = splitRecipient(argv)
|
|
553
|
+
if (!rest.length) fail('usage: quilt say [@name] <message> (a message to everyone names who it is for with @Name, or add --everyone)')
|
|
554
|
+
await simple('/say', { text: rest.join(' '), to, everyone, also }, (r) =>
|
|
555
|
+
to ? `sent to ${to}${r.recipientOnline ? '' : ' (offline, they will see it when they reconnect)'}` : 'sent')
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
async function workspaceCmd () {
|
|
559
|
+
const { WORKSPACE_USAGE, runWorkspaceCommand } = await import('../src/workspace-cli.js')
|
|
560
|
+
const [sub, ...rest] = argv
|
|
561
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') return console.log(WORKSPACE_USAGE)
|
|
562
|
+
const { localWorkspaceAccess } = await import('../src/mcp.js')
|
|
563
|
+
const { findDaemon } = await import('../src/control.js')
|
|
564
|
+
const here = process.env.QUILT_DIR || process.cwd()
|
|
565
|
+
// Whoever this folder's session is (an agent's copy names its agent), from anywhere inside it.
|
|
566
|
+
const projectDirs = () => [here, findDaemon(here)?.dir].filter((d) => d && d !== os.homedir())
|
|
567
|
+
let r
|
|
568
|
+
try {
|
|
569
|
+
r = await runWorkspaceCommand(sub, rest, localWorkspaceAccess({ projectDirs }))
|
|
570
|
+
} catch (err) { fail(err.message) }
|
|
571
|
+
if (!r.ok) fail(r.text.replace(/^Error: /, ''))
|
|
572
|
+
console.log(r.text)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function sendFile () {
|
|
576
|
+
const [file, ...more] = argv
|
|
577
|
+
if (!file) fail('usage: quilt send <file> [@name] [message]')
|
|
578
|
+
if (!fs.existsSync(file)) fail(`no such file: ${file}`)
|
|
579
|
+
const { to, rest } = splitRecipient(more)
|
|
580
|
+
await simple('/send', { path: path.resolve(file), to, text: rest.join(' ') }, (r) =>
|
|
581
|
+
`sent ${r.file.name}${to ? ` to ${to}` : ''}`)
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
async function messages () {
|
|
585
|
+
const { values } = parseArgs({ args: argv, options: { all: { type: 'boolean' }, with: { type: 'string' }, grep: { type: 'string' }, before: { type: 'string' }, n: { type: 'string', short: 'n' } } })
|
|
586
|
+
const { d, call } = await daemonOrFail()
|
|
587
|
+
if (values.with || values.grep || values.before) {
|
|
588
|
+
// Reading back (conversation.js): the kept chat, past the room's newest 500, with message ids.
|
|
589
|
+
const { renderConversation } = await import('../src/conversation.js')
|
|
590
|
+
const r = await call(d, 'POST', '/conversation', { with: values.with, q: values.grep, before: values.before, limit: Number(values.n || 30) })
|
|
591
|
+
return console.log(renderConversation(r, { me: r.me, with: values.with, q: values.grep }).replace(/quilt_conversation|call again with before: "([^"]+)"/g, (m, id) => id ? `run again with --before ${id}` : m))
|
|
592
|
+
}
|
|
593
|
+
const { renderMessage } = await import('../src/status.js')
|
|
594
|
+
const all = values.all
|
|
595
|
+
const { messages } = await call(d, 'POST', '/messages', { unreadOnly: !all, with: values.with, limit: Number(values.n || 50) })
|
|
596
|
+
const me = (await call(d, 'GET', '/status')).me.name
|
|
597
|
+
if (!messages.length) return console.log(all ? 'no messages yet' : 'no unread messages (use --all to see history)')
|
|
598
|
+
for (const m of messages) console.log(plain(renderMessage(m, me)))
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function commitRequest () {
|
|
602
|
+
const { values, positionals } = parseArgs({ args: argv, allowPositionals: true, options: { files: { type: 'string' }, task: { type: 'string' } } })
|
|
603
|
+
const files = values.files ? values.files.split(',').map((f) => f.trim()).filter(Boolean) : undefined
|
|
604
|
+
await simple('/commit-request', { message: positionals.join(' '), files, task: values.task }, (r) =>
|
|
605
|
+
`asked for a commit [${r.id}] of ${r.files.length} file${r.files.length === 1 ? '' : 's'}: ${r.files.slice(0, 12).join(', ')}${r.files.length > 12 ? ', …' : ''}\n${r.committer}`)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// git in an agent's folder runs these (installGitHelpers, gitstate.js); `--dir` is the folder, whose
|
|
609
|
+
// running session answers. They never print anything but what git expects, and never fail a git
|
|
610
|
+
// command when Quilt isn't running: git then behaves as it would without Quilt. Nor do they make git
|
|
611
|
+
// wait: a session that doesn't answer within HELPER_WAIT is taken as no session, and the whole helper
|
|
612
|
+
// exits by HELPER_LIMIT whatever is still pending (the env overrides are for tests).
|
|
613
|
+
const HELPER_WAIT = Number(process.env.QUILT_HELPER_WAIT_MS) || 4000
|
|
614
|
+
const HELPER_LIMIT = Number(process.env.QUILT_HELPER_LIMIT_MS) || 10000
|
|
615
|
+
function helperDeadline () { setTimeout(() => process.exit(0), HELPER_LIMIT).unref() }
|
|
616
|
+
function helperDir () {
|
|
617
|
+
const i = argv.indexOf('--dir')
|
|
618
|
+
const dir = i >= 0 ? argv[i + 1] : process.cwd()
|
|
619
|
+
if (i >= 0) argv.splice(i, 2)
|
|
620
|
+
return dir
|
|
621
|
+
}
|
|
622
|
+
async function helperDaemon (dir) {
|
|
623
|
+
const { findDaemon, call } = await import('../src/control.js')
|
|
624
|
+
const d = findDaemon(dir)
|
|
625
|
+
return d ? { d, call } : null
|
|
626
|
+
}
|
|
627
|
+
// git closes stdin after what it sends; one that stays open is read for HELPER_WAIT at most.
|
|
628
|
+
const readStdin = () => new Promise((resolve) => {
|
|
629
|
+
let s = ''
|
|
630
|
+
const done = () => { clearTimeout(t); process.stdin.pause(); resolve(s) }
|
|
631
|
+
const t = setTimeout(done, HELPER_WAIT)
|
|
632
|
+
process.stdin.setEncoding('utf8'); process.stdin.on('data', (c) => { s += c }); process.stdin.on('end', done); process.stdin.on('error', done)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* git's credential helper protocol: `get` answers with the session's token for the host; store and erase
|
|
637
|
+
* are nothing to do. `--fallback` (a person's folder, git for everyone): when no Quilt session answers
|
|
638
|
+
* (the app is closed) or it has no token, the person's own helpers are asked instead, as git would have
|
|
639
|
+
* without Quilt: `git credential fill` from outside the folder, so only their global and system config count.
|
|
640
|
+
*/
|
|
641
|
+
async function gitCredential () {
|
|
642
|
+
const dir = helperDir()
|
|
643
|
+
const fallback = argv.includes('--fallback')
|
|
644
|
+
if (fallback) argv.splice(argv.indexOf('--fallback'), 1)
|
|
645
|
+
if (argv[0] !== 'get') return
|
|
646
|
+
helperDeadline()
|
|
647
|
+
const input = await readStdin()
|
|
648
|
+
const fields = Object.fromEntries(input.split('\n').filter((l) => l.includes('=')).map((l) => { const i = l.indexOf('='); return [l.slice(0, i), l.slice(i + 1)] }))
|
|
649
|
+
try {
|
|
650
|
+
const h = await helperDaemon(dir)
|
|
651
|
+
if (h) {
|
|
652
|
+
const r = await h.call(h.d, 'POST', '/git/credential', { host: fields.host || '', protocol: fields.protocol || '' }, { timeoutMs: HELPER_WAIT })
|
|
653
|
+
if (r && r.username && r.password) return process.stdout.write(`username=${r.username}\npassword=${r.password}\n`)
|
|
654
|
+
}
|
|
655
|
+
} catch {}
|
|
656
|
+
if (fallback) await ownCredential(input)
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/** The person's own credentials for what git asked (`input`, git's credential fields), or nothing: never a prompt. */
|
|
660
|
+
async function ownCredential (input) {
|
|
661
|
+
const os = await import('node:os')
|
|
662
|
+
const { execFile } = await import('node:child_process')
|
|
663
|
+
const away = os.tmpdir()
|
|
664
|
+
const out = await new Promise((resolve) => {
|
|
665
|
+
const c = execFile('git', ['credential', 'fill'], { cwd: away, encoding: 'utf8', timeout: Math.max(1000, HELPER_LIMIT - HELPER_WAIT - 1000), killSignal: 'SIGKILL', env: { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: '', SSH_ASKPASS: '', GIT_CEILING_DIRECTORIES: path.dirname(away) } }, (err, stdout) => resolve(err ? '' : stdout))
|
|
666
|
+
c.stdin.end(input.endsWith('\n\n') ? input : `${input.replace(/\n*$/, '')}\n\n`)
|
|
667
|
+
})
|
|
668
|
+
const lines = out.split('\n').filter((l) => /^(username|password)=/.test(l))
|
|
669
|
+
if (lines.length === 2) process.stdout.write(`${lines.join('\n')}\n`)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** The pre-push hook: refs on stdin (local ref, local sha, remote ref, remote sha), the remote's name and URL as arguments. Exits 1 with the reason when the push may not go. */
|
|
673
|
+
async function gitPrePush () {
|
|
674
|
+
const dir = helperDir()
|
|
675
|
+
const [remote = 'origin'] = argv
|
|
676
|
+
helperDeadline()
|
|
677
|
+
const input = await readStdin()
|
|
678
|
+
const refs = input.split('\n').filter(Boolean).map((l) => { const [localRef, localSha, remoteRef, remoteSha] = l.trim().split(/\s+/); return { localRef, localSha, remoteRef, remoteSha } })
|
|
679
|
+
try {
|
|
680
|
+
const h = await helperDaemon(dir)
|
|
681
|
+
if (!h) return
|
|
682
|
+
const r = await h.call(h.d, 'POST', '/git/push-check', { remote, refs }, { timeoutMs: HELPER_WAIT })
|
|
683
|
+
if (r && r.ok === false) { process.stderr.write(`Quilt: ${r.why || 'this push is not allowed here'}\n`); process.exitCode = 1 }
|
|
684
|
+
} catch {}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/** A short-lived token for this folder's repository (agents), for gh or GitHub's API. */
|
|
688
|
+
/** `quilt git-token` no longer prints a token (a value in a terminal is a value in an AI's context). */
|
|
689
|
+
async function gitToken () {
|
|
690
|
+
const { GIT_TOKEN_GONE } = await import('../src/secret-run.js')
|
|
691
|
+
fail(GIT_TOKEN_GONE)
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/** `quilt run -- <command>`: the command gets its secrets' stand-ins and gateways (src/secret-run.js). */
|
|
695
|
+
async function runCmd () {
|
|
696
|
+
const { RUN_USAGE, runSource, runWithSecrets } = await import('../src/secret-run.js')
|
|
697
|
+
const command = argv[0] === '--' ? argv.slice(1) : argv
|
|
698
|
+
if (!command.length || (argv[0] !== '--' && ['-h', '--help', 'help'].includes(command[0]))) return console.log(RUN_USAGE)
|
|
699
|
+
const { findDaemon, call } = await import('../src/control.js')
|
|
700
|
+
// The session syncing this folder first (its person or agent), else a sandbox's key.
|
|
701
|
+
const daemon = findDaemon()
|
|
702
|
+
const source = runSource({ key: daemon ? null : await envKey(), daemon, call })
|
|
703
|
+
if (!source) fail('quilt run needs a session: run it in a folder Quilt is syncing (quilt join, or the app), or in a sandbox with QUILT_AGENT_KEY.')
|
|
704
|
+
try { process.exitCode = await runWithSecrets({ command, source }) } catch (err) { fail(err.message) }
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
async function githubStatusCmd () {
|
|
708
|
+
const { d, call } = await daemonOrFail()
|
|
709
|
+
const st = await call(d, 'GET', '/status')
|
|
710
|
+
const mine = (st.branches || []).find((b) => st.git && b.name === st.git.key) || (st.branches || []).find((b) => b.default)
|
|
711
|
+
const g = mine && mine.relay && mine.relay.github
|
|
712
|
+
const repo = mine && mine.relay && mine.relay.repo
|
|
713
|
+
if (!repo) return console.log('This session does not follow a GitHub repository (no member\'s folder is a clone of one), so there is nothing to connect.')
|
|
714
|
+
const words = { ok: `Git is on for everyone here, through @${g && g.login}'s connection: people and agents push with git, nothing to set up`, 'not-connected': 'not connected: nobody in the session has connected GitHub yet', 'not-installed': 'Quilt is not installed on this repository on GitHub yet', 'no-access': `no connected GitHub account (@${g && g.login}) may write to it`, 'not-github': 'not on GitHub' }
|
|
715
|
+
console.log(`${repo}: ${g ? words[g.state] || g.state : 'not checked yet'}`)
|
|
716
|
+
if (g && g.state === 'ok') return
|
|
717
|
+
const { readAccount } = await import('../src/account.js')
|
|
718
|
+
const token = readAccount()?.token
|
|
719
|
+
if (!token) return console.log('Sign in (quilt login), then run this again for the link that connects GitHub.')
|
|
720
|
+
const { githubConnectUrl } = await import('../src/account.js')
|
|
721
|
+
try { console.log(`Connect it (one click, once, by someone whose GitHub account can write to the repository):\n ${await githubConnectUrl({ token, repo })}`) } catch (err) { console.log(`Could not get the link: ${err.message}`) }
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
async function commits () {
|
|
725
|
+
const { d, call } = await daemonOrFail()
|
|
726
|
+
const { describeCommitRequests } = await import('../src/commit.js')
|
|
727
|
+
const c = await call(d, 'GET', '/commits')
|
|
728
|
+
console.log(plain(describeCommitRequests(c, { canCommit: !!c.canCommit })))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
async function getFile () {
|
|
732
|
+
if (!argv[0]) fail('usage: quilt get <message-id> [dest]')
|
|
733
|
+
await simple('/get', { id: argv[0], dest: argv[1] ? path.resolve(argv[1]) : undefined }, (r) => `saved to ${r.path}`)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const plain = (md) => md.replace(/\*\*/g, '').replace(/`/g, '')
|
|
737
|
+
|
|
738
|
+
async function chat () {
|
|
739
|
+
const { d, call } = await daemonOrFail()
|
|
740
|
+
const { renderMessage } = await import('../src/status.js')
|
|
741
|
+
const readline = await import('node:readline')
|
|
742
|
+
const st = await call(d, 'GET', '/status')
|
|
743
|
+
const me = st.me.name
|
|
744
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' })
|
|
745
|
+
const print = (line) => {
|
|
746
|
+
readline.clearLine(process.stdout, 0)
|
|
747
|
+
readline.cursorTo(process.stdout, 0)
|
|
748
|
+
console.log(line)
|
|
749
|
+
rl.prompt(true)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
console.log(`chatting in room ${st.room} as ${me}. Online: ${st.peers.map((p) => p.name).join(', ') || 'nobody else yet'}`)
|
|
753
|
+
console.log('type a message, "@name msg" for a direct message, "/send <file> [@name] [msg]", "/get <id> [dest]", "/who", "/quit"\n')
|
|
754
|
+
const { messages: backlog } = await call(d, 'POST', '/messages', { limit: 20 })
|
|
755
|
+
for (const m of backlog) console.log(plain(renderMessage(m, me)))
|
|
756
|
+
|
|
757
|
+
// Live messages over server-sent events.
|
|
758
|
+
const ctrl = new AbortController()
|
|
759
|
+
;(async () => {
|
|
760
|
+
try {
|
|
761
|
+
const res = await fetch(`http://127.0.0.1:${d.port}/events`, { headers: { authorization: `Bearer ${d.token}` }, signal: ctrl.signal })
|
|
762
|
+
const decoder = new TextDecoder()
|
|
763
|
+
let buf = ''
|
|
764
|
+
for await (const chunk of res.body) {
|
|
765
|
+
buf += decoder.decode(chunk, { stream: true })
|
|
766
|
+
let idx
|
|
767
|
+
while ((idx = buf.indexOf('\n\n')) !== -1) {
|
|
768
|
+
const block = buf.slice(0, idx)
|
|
769
|
+
buf = buf.slice(idx + 2)
|
|
770
|
+
const event = (block.match(/^event: (.*)$/m) || [])[1]
|
|
771
|
+
const data = (block.match(/^data: (.*)$/m) || [])[1]
|
|
772
|
+
if (!event || !data) continue
|
|
773
|
+
const payload = JSON.parse(data)
|
|
774
|
+
if (event === 'message' && payload.by !== me) print(plain(renderMessage({ ...payload, unread: false }, me)))
|
|
775
|
+
else if (event === 'log' && !payload.startsWith('💬')) print(` · ${payload}`)
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (!ctrl.signal.aborted) { print('lost connection to quilt'); process.exit(1) }
|
|
779
|
+
} catch (err) {
|
|
780
|
+
if (!ctrl.signal.aborted) { print(`lost connection to quilt: ${err.message}`); process.exit(1) }
|
|
781
|
+
}
|
|
782
|
+
})()
|
|
783
|
+
|
|
784
|
+
rl.prompt()
|
|
785
|
+
rl.on('line', async (line) => {
|
|
786
|
+
line = line.trim()
|
|
787
|
+
try {
|
|
788
|
+
if (!line) {
|
|
789
|
+
// nothing
|
|
790
|
+
} else if (line === '/quit' || line === '/exit') {
|
|
791
|
+
return rl.close()
|
|
792
|
+
} else if (line === '/who') {
|
|
793
|
+
const s = await call(d, 'GET', '/status')
|
|
794
|
+
print(s.peers.length ? s.peers.map((p) => ` ${p.name} (${p.tool})${p.focus ? `: ${p.focus}` : ''}`).join('\n') : ' nobody else is online')
|
|
795
|
+
} else if (line.startsWith('/send ')) {
|
|
796
|
+
const [file, ...more] = line.slice(6).trim().split(/\s+/)
|
|
797
|
+
const { to, rest } = splitRecipient(more)
|
|
798
|
+
const r = await call(d, 'POST', '/send', { path: path.resolve(file), to, text: rest.join(' ') })
|
|
799
|
+
print(` sent ${r.file.name}${to ? ` to ${to}` : ''}`)
|
|
800
|
+
} else if (line.startsWith('/get ')) {
|
|
801
|
+
const [id, dest] = line.slice(5).trim().split(/\s+/)
|
|
802
|
+
const r = await call(d, 'POST', '/get', { id, dest: dest ? path.resolve(dest) : undefined })
|
|
803
|
+
print(` saved to ${r.path}`)
|
|
804
|
+
} else if (line.startsWith('/')) {
|
|
805
|
+
print(' unknown command')
|
|
806
|
+
} else {
|
|
807
|
+
const { to, rest } = splitRecipient(line.split(' '))
|
|
808
|
+
// A person typing here, not an AI: what they send needs no @Name.
|
|
809
|
+
const r = await call(d, 'POST', '/say', { text: rest.join(' '), to, everyone: true })
|
|
810
|
+
if (to && !r.recipientOnline) print(` (${to} is offline; they'll see it when they reconnect)`)
|
|
811
|
+
}
|
|
812
|
+
} catch (err) {
|
|
813
|
+
print(` error: ${err.message}`)
|
|
814
|
+
}
|
|
815
|
+
rl.prompt()
|
|
816
|
+
})
|
|
817
|
+
rl.on('close', () => { ctrl.abort(); process.exit(0) })
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function invite () {
|
|
821
|
+
const { encodeInvite } = await import('../src/runner.js')
|
|
822
|
+
const { unsupportedRelay } = await import('../src/settings.js')
|
|
823
|
+
let dir = process.cwd()
|
|
824
|
+
while (true) {
|
|
825
|
+
const f = path.join(dir, '.quilt', 'config.json')
|
|
826
|
+
if (fs.existsSync(f)) {
|
|
827
|
+
const c = JSON.parse(fs.readFileSync(f, 'utf8'))
|
|
828
|
+
// Never hand out an invite to a relay Quilt won't connect to (or let anyone else connect to).
|
|
829
|
+
if (unsupportedRelay(c.server)) fail("This folder's last session ran on your computer's own relay, which Quilt no longer supports. Run quilt join here to start a new session.")
|
|
830
|
+
return console.log(encodeInvite({ ...c, server: c.inviteServer && !unsupportedRelay(c.inviteServer) ? c.inviteServer : c.server }))
|
|
831
|
+
}
|
|
832
|
+
if (path.dirname(dir) === dir) fail('no session configured in this folder')
|
|
833
|
+
dir = path.dirname(dir)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
async function stopAll () {
|
|
838
|
+
const { stopProcesses, describeProcess } = await import('../src/procs.js')
|
|
839
|
+
const stopped = await stopProcesses()
|
|
840
|
+
if (!stopped.length) return console.log('nothing to stop: quilt is not running')
|
|
841
|
+
console.log('stopped:\n' + stopped.map((p) => ` - ${describeProcess(p)}`).join('\n'))
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function fail (msg) {
|
|
845
|
+
console.error(msg)
|
|
846
|
+
process.exit(1)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
main().catch((err) => fail(err.stack || err.message))
|