heyquilt 0.3.90 → 0.3.107
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/README.md +14 -11
- package/RELEASES.md +135 -2
- package/bin/quilt.js +45 -12
- package/package.json +3 -3
- package/src/account.js +28 -0
- package/src/agents/claude-code.js +11 -4
- package/src/agents/cursor-transcripts.js +11 -4
- package/src/ai-vm-signin.js +14 -2
- package/src/ai-vm.js +150 -8
- package/src/api/agent-auth.js +9 -3
- package/src/api/fly-machines.js +72 -15
- package/src/api/memory-store.js +142 -6
- package/src/api/relay-cluster.js +337 -43
- package/src/api/relay-machines.js +35 -3
- package/src/api/releases.js +87 -0
- package/src/api/routes/agents.js +111 -16
- package/src/api/routes/ai-vm.js +146 -19
- package/src/api/routes/orgs.js +3 -1
- package/src/api/routes/secrets.js +448 -0
- package/src/api/routes/workspaces.js +8 -1
- package/src/api/sandbox-keys.js +57 -0
- package/src/api/secrets-access.js +93 -0
- package/src/api/server.js +40 -9
- package/src/api/supabase-store.js +121 -11
- package/src/autowork.js +3 -15
- package/src/catchup.js +1 -1
- package/src/clash.js +10 -22
- package/src/control.js +18 -15
- package/src/fsutil.js +11 -1
- package/src/gitstate.js +29 -5
- package/src/hooks.js +1 -1
- package/src/mcp-proxy.js +7 -2
- package/src/mcp.js +22 -25
- package/src/merge3.js +94 -12
- package/src/pass-source.js +9 -2
- package/src/pathrules.js +7 -0
- package/src/relay-clash.js +101 -484
- package/src/relay-githttp.js +36 -7
- package/src/relay-mcp.js +19 -7
- package/src/relay-metrics.js +66 -1
- package/src/relay-outbound.js +154 -25
- package/src/relay-sync.js +133 -136
- package/src/relay-turn-sync.js +250 -51
- package/src/relay-upstream.js +49 -179
- package/src/releases.js +16 -9
- package/src/runner.js +3 -2
- 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 +71 -44
- package/src/session.js +347 -262
- package/src/status.js +3 -5
- package/src/sync-words.js +1 -1
- package/src/turn-sync-plan.js +13 -10
- package/src/turn-sync.js +32 -11
- package/src/ui/agent-guide.js +4 -4
- package/src/ui/app.css +208 -23
- package/src/ui/app.js +4 -0
- package/src/ui/cloud-ai-model.js +3 -2
- package/src/ui/common.js +2 -0
- package/src/ui/fileview.js +28 -1
- package/src/ui/home.js +32 -33
- package/src/ui/loom.css +18 -2
- package/src/ui/loom.js +41 -8
- package/src/ui/rail-model.js +6 -9
- package/src/ui/releases.js +1 -1
- package/src/ui/sandbox-keys-model.js +53 -0
- package/src/ui/sandbox-keys.js +132 -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 +47 -149
- package/src/ui/tree.js +23 -18
- package/src/ui/workspaces.js +5 -0
- package/src/ui-server.js +140 -28
- package/src/update-check.js +1 -1
- package/src/upstream.js +75 -38
- 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/ui/merges.js +0 -158
package/README.md
CHANGED
|
@@ -32,8 +32,9 @@ within milliseconds. Your agents can also see what the other agents are doing.
|
|
|
32
32
|
WebSockets, and only lets in people signed in to heyquilt.com. If your
|
|
33
33
|
connection drops, keep working: Quilt keeps a local copy of the shared state
|
|
34
34
|
and merges your offline edits line by line when you reconnect, like git.
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Nobody is ever asked to merge: where two people changed the same lines,
|
|
36
|
+
Quilt keeps the newer edit, keeps the other in the file's history, and tells
|
|
37
|
+
whoever wrote it. The same goes for commits pulled or pushed to GitHub.
|
|
37
38
|
- **Watch each other's AI, live.** The app shows your partner's AI conversation
|
|
38
39
|
as it happens: their prompts, the AI's replies, and one-line actions like
|
|
39
40
|
"Edited src/app.ts" or "Ran npm test". Quilt reads Claude Code's and Cursor's
|
|
@@ -70,11 +71,11 @@ within milliseconds. Your agents can also see what the other agents are doing.
|
|
|
70
71
|
|
|
71
72
|
### Download the app
|
|
72
73
|
|
|
73
|
-
Get Quilt for [Mac (Apple silicon)](https://
|
|
74
|
-
[Mac (Intel)](https://
|
|
75
|
-
[Windows](https://
|
|
76
|
-
Linux ([x86_64](https://
|
|
77
|
-
[ARM](https://
|
|
74
|
+
Get Quilt for [Mac (Apple silicon)](https://heyquilt.com/download/quilt-mac-arm64.dmg),
|
|
75
|
+
[Mac (Intel)](https://heyquilt.com/download/quilt-mac-x64.dmg),
|
|
76
|
+
[Windows](https://heyquilt.com/download/quilt-windows-x64.exe) or
|
|
77
|
+
Linux ([x86_64](https://heyquilt.com/download/quilt-linux-x86_64.AppImage),
|
|
78
|
+
[ARM](https://heyquilt.com/download/quilt-linux-arm64.AppImage): an
|
|
78
79
|
AppImage, `chmod +x` it), and open it.
|
|
79
80
|
Everything happens in the app: start a session, send the invite link, and
|
|
80
81
|
partners click it to join. Closing the window keeps your sessions syncing from
|
|
@@ -97,7 +98,7 @@ A Linux machine with no desktop (a server, a cloud VM, a container, an agent's s
|
|
|
97
98
|
gets the command line with its own Node.js, so nothing else needs installing:
|
|
98
99
|
|
|
99
100
|
```bash
|
|
100
|
-
curl -fsSL https://
|
|
101
|
+
curl -fsSL https://heyquilt.com/install.sh | sh
|
|
101
102
|
quilt login
|
|
102
103
|
```
|
|
103
104
|
|
|
@@ -514,9 +515,11 @@ person with `@name`. Messages are kept in the room, so anyone offline sees
|
|
|
514
515
|
them when they reconnect, and `quilt status` shows your unread count.
|
|
515
516
|
|
|
516
517
|
Files you **send** (screenshots, logs, exports, a PDF spec) go through the relay
|
|
517
|
-
as attachments. They are *not* added to the shared project
|
|
518
|
-
|
|
519
|
-
|
|
518
|
+
as attachments. They are *not* added to the shared project. Everyone who can
|
|
519
|
+
see the message gets the file in the `Chat files` folder of their session
|
|
520
|
+
folder, including files sent while they were offline or before they joined.
|
|
521
|
+
Quilt never syncs that folder and git ignores it, so it never reaches GitHub:
|
|
522
|
+
only the people in the session have it. The limit is 100 MB per file.
|
|
520
523
|
|
|
521
524
|
Direct messages and files are only shown to the sender and recipient, but they
|
|
522
525
|
travel through the shared room, so they're private from other collaborators'
|
package/RELEASES.md
CHANGED
|
@@ -11,6 +11,139 @@ Format: `## <version> — <YYYY-MM-DD>`, an optional one-line summary, then bull
|
|
|
11
11
|
Lead each bullet with a short bold phrase. Inline `code` and **bold** are rendered;
|
|
12
12
|
nothing else is.
|
|
13
13
|
|
|
14
|
+
## 0.3.107 — 2026-10-19
|
|
15
|
+
|
|
16
|
+
- **Your cloud AI's sessions stay listed.** Settings → Your AI shows the sessions your cloud AI is in while its machine sleeps and after Quilt's servers restart, instead of "In no session yet". A session nobody named shows as your folder's name, as the rest of the app shows it.
|
|
17
|
+
|
|
18
|
+
## 0.3.106 — 2026-10-19
|
|
19
|
+
|
|
20
|
+
0.3.105 didn't publish (GitHub didn't run its builds), so it comes with this one.
|
|
21
|
+
|
|
22
|
+
- **Agents' folders are git checkouts again.** The relay's newer git no longer came with the part that serves git to agents (`git http-backend`), so agents' and cloud AIs' folders synced without git. It's back.
|
|
23
|
+
- **A relay that can't serve git says so.** If its git ever lacks that part again, the relay logs it at start and agents' folders use GitHub's git instead of failing; a failed git request through the relay is logged with what git said.
|
|
24
|
+
- **An agent's main is the session's main.** When several branches in a session started from main, the relay could give agents' folders an old main from one of them. Now they get the session's own main.
|
|
25
|
+
- **Your cloud AI joins the sessions you already have open.** A cloud AI connected while sessions were open now joins them too, instead of only the sessions you start afterwards, so it can hear and answer there.
|
|
26
|
+
|
|
27
|
+
## 0.3.105 — 2026-10-19
|
|
28
|
+
|
|
29
|
+
Your cloud AI's sign-in shows its link in seconds.
|
|
30
|
+
|
|
31
|
+
- **Every AI tool is ready before you connect.** The spare cloud machine now reads Claude Code, Codex and Cursor into memory before anyone connects, then sleeps. Connecting wakes it as yours, so the sign-in link shows in a few seconds instead of sitting at "Starting Claude's own sign-in…" while the machine read the tool off its disk.
|
|
32
|
+
- **Switching tools signs the new one in at once.** Changing Claude Code to Codex or Cursor no longer shows the old tool's link, and the new tool's sign-in starts straight away instead of after about 25 seconds.
|
|
33
|
+
- **Even right after someone else connects.** A spare that is still getting its tools ready when you connect stops and starts your sign-in at once.
|
|
34
|
+
- **A code Claude doesn't take shows a new link.** Pasting a wrong or expired code used to leave "Checking the code" up forever. Now Claude's sign-in starts over by itself and shows its new link, with a note that the code wasn't taken.
|
|
35
|
+
- **`npx heyquilt` is always current.** The npm package is published with every release, so agents in a sandbox running `npx heyquilt sync` get the same version as the app.
|
|
36
|
+
|
|
37
|
+
## 0.3.104 — 2026-10-19
|
|
38
|
+
|
|
39
|
+
0.3.102 and 0.3.103 didn't publish (a test of the release itself failed), so they come with this one.
|
|
40
|
+
|
|
41
|
+
- **Downloads and updates come from heyquilt.com (0.3.102).** Installers, the Linux one-liner (`curl -fsSL https://heyquilt.com/install.sh | sh`) and the app's update check no longer depend on the GitHub repository, which is private.
|
|
42
|
+
- **git never waits on Quilt (0.3.103).** `git fetch`, `git push` and `git ls-remote` in a folder Quilt signs in to GitHub no longer hang when Quilt is slow to answer.
|
|
43
|
+
|
|
44
|
+
## 0.3.103 — 2026-10-19
|
|
45
|
+
|
|
46
|
+
- **git never waits on Quilt.** In a folder where Quilt signs git in to GitHub, `git fetch`, `git push` and `git ls-remote` could hang for minutes when Quilt was slow to answer. Quilt now answers within a few seconds or steps aside, and your own sign-in (a keychain, `gh`) is used instead.
|
|
47
|
+
|
|
48
|
+
## 0.3.102 — 2026-10-19
|
|
49
|
+
|
|
50
|
+
Downloads and updates come from heyquilt.com.
|
|
51
|
+
|
|
52
|
+
- **Downloads work with Quilt's code kept private.** The installers, the Linux one-liner (`curl -fsSL https://heyquilt.com/install.sh | sh`) and the app's update check now go through heyquilt.com instead of the GitHub repository, which is private. A pinned version still works: `heyquilt.com/download/v0.3.102/<file>`.
|
|
53
|
+
- **The update check asks Quilt, not GitHub.** Your app learns about new versions from Quilt's own servers, and "Update Quilt" downloads from heyquilt.com.
|
|
54
|
+
|
|
55
|
+
## 0.3.101 — 2026-10-18
|
|
56
|
+
|
|
57
|
+
Three updates in one: 0.3.98, 0.3.99 and 0.3.100 were built but couldn't be published, so they come with this one.
|
|
58
|
+
|
|
59
|
+
- **Nobody is asked to merge any more (0.3.99).** When your offline edits, a `git pull`, a push to GitHub or an AI's sandbox meet the session's work, Quilt combines them itself. Where both changed the same lines the newer edit stays, and whoever's lines were replaced gets a message listing them; they're kept in the file's history. The Merges bar, Keep mine / Keep theirs and merge tasks are gone.
|
|
60
|
+
- **Your cloud AI is ready in seconds (0.3.100).** A spare machine waits for the next person, so connecting takes about 4 seconds, and Claude's sign-in link shows again.
|
|
61
|
+
- **An AI's edits stay in their box (0.3.98)**, even with several of its conversations going at once.
|
|
62
|
+
- **Releases publish reliably.** Builds now go straight onto the release instead of through GitHub's build storage, which had filled up and stopped the last three.
|
|
63
|
+
|
|
64
|
+
## 0.3.100 — 2026-10-18
|
|
65
|
+
|
|
66
|
+
Your cloud AI is ready in seconds.
|
|
67
|
+
|
|
68
|
+
- **A cloud machine waits ready for you.** Quilt keeps a spare machine made and stopped ahead of time (a stopped machine costs only its disk). Connecting a cloud AI gives you that one, running in about 4 seconds instead of the 15 to 20 a new machine took, and another spare is made for the next person.
|
|
69
|
+
- **Claude's sign-in shows its link again.** Claude Code now prints its sign-in link on claude.com, which the cloud machine didn't recognise, so the sign-in sat at "Starting Claude's own sign-in…". It shows the link and takes your code again.
|
|
70
|
+
|
|
71
|
+
## 0.3.99 — 2026-10-18
|
|
72
|
+
|
|
73
|
+
Nobody is asked to merge any more.
|
|
74
|
+
|
|
75
|
+
- **Quilt merges on its own, every time.** When two versions of a file meet (you worked offline, `git pull` brought commits in, someone pushed to GitHub, or an AI's sandbox synced), Quilt combines them itself. Changes to different lines both go in, including changes on neighbouring lines. Where both sides changed the same lines, the newer edit keeps them; a pulled commit counts as new as when it was made, not when git wrote it. An edit always beats a deletion. Nobody is asked to pick a side, and nobody's AI is handed a merge to do.
|
|
76
|
+
- **Nothing is lost, and whoever lost lines is told.** The merge is in the file's history like any edit. The version that lost is kept, and its writer gets one message listing the lines that were replaced, which they can put back with an ordinary edit if they want them.
|
|
77
|
+
- **A file someone holds still waits for them.** Your change to a file another person or AI is working on goes in, merged, the moment they let go of it.
|
|
78
|
+
- **GitHub too.** Commits pushed to GitHub outside Quilt come into the session at once, combined with the session's work the same way (a commit counts as new as when it was made); there are no clash tasks, no merger and reviewer, and no file held for a merge or for failing checks. A local commit the relay pushes for you is combined with GitHub's newer commits the same way, and conflict markers a git merge left in a file are settled (both sides' lines kept) before it goes to GitHub. `quilt_clash` stays for older guides and says there is nothing to do.
|
|
79
|
+
- **Gone: the Merges bar and the "to merge" pill**, Keep mine / Keep theirs / Edit by hand / Send to, conflict markers in shared files, merge tasks and their merger and reviewer, and handing a merge to your AI. `quilt_merges` now says what Quilt merged; `quilt_resolve_merge` answers that there is nothing to settle.
|
|
80
|
+
|
|
81
|
+
## 0.3.98 — 2026-10-18
|
|
82
|
+
|
|
83
|
+
- **A new heyquilt.com.** The website is dark by default, like the app, with a light mode a click away. The homepage shows Quilt working instead of describing it: a live Loom where three people's AIs work at once, wait for a file and hand it over with notes; side by side, what happens to two AIs on one file with and without Quilt; then the steps, what Quilt does, how agents join, what stays yours, and answers to the usual questions. It's drawn in the page rather than from screenshots, so it's sharp at any size and quick to load, and it moves only while you're looking, and not at all with reduced motion.
|
|
84
|
+
- **Fixed: an AI's edits could escape their box.** With several Claude Code or Cursor conversations going at once, one finishing its turn made Quilt think your AI had stopped, so another conversation's edits counted as yours and weren't boxed: half a change could reach everyone's folder before it was committed. Quilt now counts your AI as working while any of its conversations is mid-turn.
|
|
85
|
+
|
|
86
|
+
## 0.3.97 — 2026-10-18
|
|
87
|
+
|
|
88
|
+
Files sent in chat get a folder of their own.
|
|
89
|
+
|
|
90
|
+
- **A Chat files folder only the session has.** Files sent in a session's chat now land in a `Chat files` folder in your session folder, instead of hidden in `.quilt/inbox/`. Quilt never syncs it and git ignores it, so it never reaches GitHub or the workspace: everyone who can see the message gets the file, and a file sent to one person goes only to them. Files you send are kept there too, and files sent before you joined or while you were away come in when you open the session. A file you delete from the folder stays deleted; open it in chat to get it back.
|
|
91
|
+
- **See them in the file tree.** The app's file tree shows the Chat files folder, marked session only, with who sent each file. Open one to read it, see a picture, or download it, and AIs see them in `quilt_list_files`.
|
|
92
|
+
- **git no longer hangs on Quilt's sign-in.** In the desktop app, the helper Quilt gives git to sign in to GitHub, and its pre-push hook, could open as a second copy of the app instead of answering, so `git fetch`, `git pull` and `git push` waited for ever. They answer at once now.
|
|
93
|
+
- **A file you had open is there after a reload.** A file tab put back when the app reloads now shows the file, instead of "Loading…" until you clicked it again.
|
|
94
|
+
- **A stopped session lets go of its folder.** If git's `index.lock` came and went just as a session stopped, Quilt could keep watching the folder, and `quilt` stayed running after you left. It now stops watching for good.
|
|
95
|
+
|
|
96
|
+
## 0.3.96 — 2026-10-18
|
|
97
|
+
|
|
98
|
+
- **Your commits can't delete what someone just pushed.** When Quilt put your local commits on GitHub and someone pushed again a moment later, your folder could jump to their newer commit before its files arrived, so a `git commit -a` made right then recorded those files as deleted. Your folder now follows the commit Quilt pushed, and their newer work comes in the usual way, files first.
|
|
99
|
+
|
|
100
|
+
## 0.3.95 — 2026-10-18
|
|
101
|
+
|
|
102
|
+
The relay scales on real load, both ways.
|
|
103
|
+
|
|
104
|
+
- **Sessions go where there's room.** Each relay machine reports its memory, event-loop lag, CPU and traffic, and new sessions go to the least loaded machine under its target. The number of sessions is now only a hard ceiling.
|
|
105
|
+
- **Busy machines get relief.** A relay machine over its load target for a while moves its lightest sessions to a quieter machine, or to a new one. It never moves a session during a git push or commit, and never moves the same session twice within 30 minutes.
|
|
106
|
+
- **Idle machines go away.** Relay machines left empty for 30 minutes, including a heavy session's own machine after that session ends, are stopped and removed. Two lightly loaded machines merge into one, and at least one machine always stays.
|
|
107
|
+
|
|
108
|
+
## 0.3.94 — 2026-10-18
|
|
109
|
+
|
|
110
|
+
Secrets your team can use but nobody can read.
|
|
111
|
+
|
|
112
|
+
- **Team, workspace and session secrets.** Add API keys in session settings, on the workspace page, or for the whole team on heyquilt.com (your org's Secrets page). Once saved a value is never shown again, to anyone: replace or delete it. Each secret goes to one service only (for example `api.stripe.com`), and each shows when it was last used.
|
|
113
|
+
- **`quilt run -- <command>`.** Runs your project with its secrets without ever handing them over: the command gets `STRIPE_KEY=quilt` and `STRIPE_BASE_URL` pointing at Quilt's vault, which adds the real key on the way to Stripe. Point an SDK at the base URL (OpenAI and Anthropic's are set for you).
|
|
114
|
+
- **People-only secrets ask you first.** Mark a secret People only and every run that needs it asks you to allow it in the app; agents never get it.
|
|
115
|
+
- **Turn inherited secrets off.** A session owner can switch workspace and team secrets off for their session, and a workspace can switch team secrets off, for a leak or when they aren't needed. Choose which agents may use secrets at all.
|
|
116
|
+
- **Import a `.env` file** straight into the vault, then delete it.
|
|
117
|
+
- **For agents:** a new `quilt_secret_names` tool lists names and where they go, never values. `quilt git-token` no longer prints a token.
|
|
118
|
+
|
|
119
|
+
## 0.3.93 — 2026-10-18
|
|
120
|
+
|
|
121
|
+
Sandbox keys you can see and manage.
|
|
122
|
+
|
|
123
|
+
- **An agent's keys in one table.** In the app (Settings → Agents, open an agent) and on heyquilt.com, each agent lists its sandbox keys: which session, when it was last used, its use over the last seven days, and whether it is in use, idle or revoked.
|
|
124
|
+
- **New key, Rotate, Revoke.** Make a key for a session in place; it is shown once, with Copy. Rotate swaps it for a new one with the same name and session; Revoke stops it at once; Remove clears a revoked one.
|
|
125
|
+
|
|
126
|
+
## 0.3.92 — 2026-10-18
|
|
127
|
+
|
|
128
|
+
Everything from 0.3.88 to 0.3.91, which did not publish: git for everyone, boxes, your AI on a cloud machine, and the relay on many machines.
|
|
129
|
+
|
|
130
|
+
- **A new session follows its workspace's git rule at once.** A session made in a workspace, or moved into or out of one, picks up the workspace's, team's or org's git rule right away instead of up to ten minutes later. Deleting a workspace lifts its rule from its sessions at once too.
|
|
131
|
+
- **Your AI on a cloud machine.** Settings → Your AI: connect Claude Code, Codex or Cursor and it runs on a cloud machine of your own, so it keeps taking tasks, answering @mentions and messages while your computer is closed. Sign the tool in right in the card with its own login: Quilt never sees it. See its status, its sessions and today's use against its daily cap; change its model, instructions and cap; wake it or disconnect it. Included in every plan for now.
|
|
132
|
+
- **See who has unfinished work.** In the people panel, each person's boxed files are listed under them with what changed, opened in place. Ask for someone's boxed file there, or unpack your own to put it in everyone's folder. Boxed files have an amber mark in the file tree.
|
|
133
|
+
- **One control for git, everywhere.** Off for everyone, on with agents pushing their own branches, or on with agents pushing any branch: the same three-way control in session, workspace, org and default settings, and for teams and orgs on heyquilt.com. Choices a higher rule doesn't allow are greyed, with who set the rule.
|
|
134
|
+
- **Your AI's unfinished work stays in a box.** When an AI edits a file in a git folder, the change is boxed: everyone sees it, but it is only in its owner's folder, so nobody else's AI can commit it. It reaches everyone when its owner commits it, puts the file back, unpacks it (`quilt_unpack`) or hands the file off. `quilt_status` lists boxed work, and a box whose owner has been away a day gets one reminder a day.
|
|
135
|
+
- **Hosted agents' writes box too**, until they commit with `quilt_git_commit`.
|
|
136
|
+
- **The relay runs on many machines.** Each session lives on one machine, and new sessions go to the least busy. A heavy session (big or busy) gets a machine of its own and moves there; people reconnect by themselves in a few seconds.
|
|
137
|
+
- **Fixed: hosted agents' and branch leases' files could be removed** by the relay's sweep of idle sessions.
|
|
138
|
+
- **Everyone pushes once one person connects GitHub.** When the session owner (or a workspace, team or org rule) has git on and someone has connected GitHub, everyone in the session, people and agents alike, can `git push` with nothing to set up. Each person gets a one-hour token for that repository only, taken back when they leave or git is turned off. Your own git settings and keychain are never touched.
|
|
139
|
+
- **Off means off for everyone.** Turning git off now stops people's pushes too, not just agents'. Commits from the app's commit panel still go.
|
|
140
|
+
- **Team git rules.** A team can set git for the sessions its members own. Org, team and workspace rules are ceilings: a session owner can tighten them, never loosen them, and is told who set the rule.
|
|
141
|
+
- **No more `gh auth login`.** Quilt no longer tells you to sign in to GitHub in another tool; it says whose connection git goes through.
|
|
142
|
+
|
|
143
|
+
## 0.3.91 — 2026-10-18
|
|
144
|
+
|
|
145
|
+
- **A new session follows its workspace's git rule at once.** A session made in a workspace, or moved into or out of one, picks up the workspace's, team's or org's git rule right away instead of up to ten minutes later. Deleting a workspace lifts its rule from its sessions at once too.
|
|
146
|
+
|
|
14
147
|
## 0.3.90 — 2026-10-18
|
|
15
148
|
|
|
16
149
|
Your AI on a cloud machine, boxes you can see, and one control for git.
|
|
@@ -48,9 +181,9 @@ A new look for Quilt: the Patch Q mark, and the app and heyquilt.com in ink, ver
|
|
|
48
181
|
- **A cleaner light mode.** Light mode drops the cream for neutral greys with a hint of ink. The top bar, Files, chat and the sidebar are white, and the work (the Loom, the board, a file, Home, Settings) sits on a cool grey, so its white cards and the Loom's lane heads stand out with firm lines and a soft shadow. The task board sits on white, with pale columns and borderless cards; column titles are quiet, with a coloured dot for the status; the open tab is underlined in vermilion; and type is a step lighter. Every card, Loom cards included, lifts the same way. Edits, locks and In progress are now blue, so vermilion only marks the open file, what needs you and what's live. Errors are crimson, so they no longer look like links. Small grey text, warnings and edits are easier to read, and every text colour now passes WCAG AA. Workspace covers lose their gloss, and the workspace swatches in the sidebar have an edge so you can see them. One person colour moved from rust to indigo, so nobody's lane looks like an alert. In dark mode, counts on vermilion badges are readable now.
|
|
49
182
|
- **Code in the file view is no longer ruled into boxes.** Every line of a file or a merge was drawn as a little card, with a border and a shadow, because it shared a class name with the Loom's cards.
|
|
50
183
|
- **Every wait shows the mark piecing itself together.** Loading files, people, invites, repositories, changes, agents and the rules check, signing in, and updating Quilt now show the mark being pieced together, patch by patch, instead of plain "Loading…" text. With reduced motion turned on, it fades gently instead.
|
|
51
|
-
- **Tell your conversations apart in the Loom.** When someone has several AI conversations going at once, each gets its own piece of the Q, shown on its cards and in a single row in their lane's header. One working right now has its piece run a lap of the Q and come home. Names come from the AI tool itself (the thread names in Claude Code, the chat names in Cursor), and you can rename one with the pencil (on this computer). Click a conversation to follow just it: its name
|
|
184
|
+
- **Tell your conversations apart in the Loom.** When someone has several AI conversations going at once, each gets its own piece of the Q, shown on its cards and in a single row in their lane's header. One working right now has its piece run a lap of the Q and come home. Names come from the AI tool itself (the thread names in Claude Code, the chat names in Cursor), and you can rename one with the pencil (on this computer). Click a conversation to follow just it: it moves to the front with its name readable, the others shrink to small marks beside it (or a "+N" in a narrow lane), the Loom shows its latest card and everything else steps back. Click again or press Esc to see everything.
|
|
52
185
|
- **A working AI pieces its lane's quilt.** In the Loom, the needle under a working AI's lane is now a patch being pieced on the lane's thread and sewn down into a Q, slot by slot in that person's colour, the vermilion corner last. Each piece hangs from the end of the thread before it drops in, so the thread no longer runs through the pieces, the quilt or its label. With reduced motion turned on, it shows the finished Q.
|
|
53
|
-
- **Back to the latest in one click.** Scroll up the Loom to read back and a Latest button appears at the bottom; click it to glide back down to what's happening now. If anything new comes in while you're up there, it says New activity instead.
|
|
186
|
+
- **Back to the latest in one click.** Scroll up the Loom to read back and a Latest button appears at the bottom; click it to glide back down to what's happening now. If anything new comes in while you're up there, it says New activity instead. The New activity button in an AI's chat also takes a click anywhere on it now: pressing it slid it sideways, so a click on its arrow missed.
|
|
54
187
|
- **Something live glows like the mark's corner.** The dot that pulses while an AI is working is now the mark's vermilion half-square, and the update bar's dot, the release notes' versions and their bullets use it too.
|
|
55
188
|
- **Crisp at the smallest sizes.** The favicon, menu-bar icon and smallest app icons are drawn pixel by pixel at 16, 24 and 32 px, so they stay sharp instead of blurring. The full design spec is in `brand/SPEC.md`.
|
|
56
189
|
- **heyquilt.com, re-branded to match.** The website moves to ink, vermilion and linen too, in light and dark. The patchwork behind its screenshots is now an ink quilt on the mark's own grid, finished with the vermilion corner, and the screenshots, blog pictures, favicons and link previews all show the new look. Buttons that are working and pages that are loading show the mark piecing itself together, and a missing page shows the mark with a piece gone.
|
package/bin/quilt.js
CHANGED
|
@@ -40,9 +40,9 @@ Usage:
|
|
|
40
40
|
Show unread messages, or read back: the conversation
|
|
41
41
|
with one person, a search, earlier messages
|
|
42
42
|
quilt get <message-id> [dest] Download a shared file again
|
|
43
|
-
quilt
|
|
44
|
-
|
|
45
|
-
|
|
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
46
|
quilt commit-request <message> [--files a,b] [--task id]
|
|
47
47
|
Ask a person to commit for you (when agents may not push here)
|
|
48
48
|
quilt commits Open commit requests and recent commits
|
|
@@ -99,6 +99,7 @@ async function main () {
|
|
|
99
99
|
case 'commits': return commits()
|
|
100
100
|
case 'github': return githubStatusCmd()
|
|
101
101
|
case 'git-token': return gitToken()
|
|
102
|
+
case 'run': return runCmd()
|
|
102
103
|
case 'git-credential': return gitCredential()
|
|
103
104
|
case 'git-pre-push': return gitPrePush()
|
|
104
105
|
case 'chat': return chat()
|
|
@@ -212,11 +213,16 @@ async function apiCmd () {
|
|
|
212
213
|
github: (await import('../src/api/github-app.js')).appConfig(env),
|
|
213
214
|
// Fly's Machines API for the relay (RELAY_FLY_API_TOKEN, QUILT_RELAY_APP): heavy sessions get a relay machine of their own.
|
|
214
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),
|
|
215
218
|
// Where agents reach this API (invite links point here).
|
|
216
219
|
apiUrl: env.QUILT_API_PUBLIC_URL || (values.memory ? `http://${host}:${port}` : 'https://api.heyquilt.com'),
|
|
217
220
|
siteUrl: env.QUILT_SITE_URL || 'http://localhost:3000',
|
|
218
221
|
trustProxy: /^(1|true|yes)$/i.test(env.QUILT_TRUST_PROXY || ''), log: console.log,
|
|
219
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',
|
|
220
226
|
// People's cloud AI machines (FLY_API_TOKEN, AI_VM_APP, AI_VM_IMAGE, AI_VM_REGION); off when not set.
|
|
221
227
|
fly: (await import('../src/api/fly-machines.js')).flyFromEnv(env),
|
|
222
228
|
fileStore: makeFileStore(
|
|
@@ -227,6 +233,7 @@ async function apiCmd () {
|
|
|
227
233
|
console.log(`quilt accounts API listening on ${api.url}`)
|
|
228
234
|
if (!env.RELAY_API_SECRET) console.log('RELAY_API_SECRET is not set: the relay cannot report sessions for the dashboard')
|
|
229
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')
|
|
230
237
|
const shutdown = async () => { await api.close(); process.exit(0) }
|
|
231
238
|
process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown)
|
|
232
239
|
}
|
|
@@ -340,6 +347,7 @@ async function join () {
|
|
|
340
347
|
prefer: values.prefer === 'local' ? 'local' : 'remote',
|
|
341
348
|
kind: auth.kind,
|
|
342
349
|
passes: auth.passes,
|
|
350
|
+
apiAuth: auth.apiAuth,
|
|
343
351
|
identity: auth.identity,
|
|
344
352
|
inviteServer: saved.room === conn.room ? saved.inviteServer : undefined,
|
|
345
353
|
onLog: (m) => console.log(`[${stamp()}] ${m}`),
|
|
@@ -599,7 +607,12 @@ async function commitRequest () {
|
|
|
599
607
|
|
|
600
608
|
// git in an agent's folder runs these (installGitHelpers, gitstate.js); `--dir` is the folder, whose
|
|
601
609
|
// running session answers. They never print anything but what git expects, and never fail a git
|
|
602
|
-
// command when Quilt isn't running: git then behaves as it would without Quilt.
|
|
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() }
|
|
603
616
|
function helperDir () {
|
|
604
617
|
const i = argv.indexOf('--dir')
|
|
605
618
|
const dir = i >= 0 ? argv[i + 1] : process.cwd()
|
|
@@ -611,7 +624,13 @@ async function helperDaemon (dir) {
|
|
|
611
624
|
const d = findDaemon(dir)
|
|
612
625
|
return d ? { d, call } : null
|
|
613
626
|
}
|
|
614
|
-
|
|
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
|
+
})
|
|
615
634
|
|
|
616
635
|
/**
|
|
617
636
|
* git's credential helper protocol: `get` answers with the session's token for the host; store and erase
|
|
@@ -624,12 +643,13 @@ async function gitCredential () {
|
|
|
624
643
|
const fallback = argv.includes('--fallback')
|
|
625
644
|
if (fallback) argv.splice(argv.indexOf('--fallback'), 1)
|
|
626
645
|
if (argv[0] !== 'get') return
|
|
646
|
+
helperDeadline()
|
|
627
647
|
const input = await readStdin()
|
|
628
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)] }))
|
|
629
649
|
try {
|
|
630
650
|
const h = await helperDaemon(dir)
|
|
631
651
|
if (h) {
|
|
632
|
-
const r = await h.call(h.d, 'POST', '/git/credential', { host: fields.host || '', protocol: fields.protocol || '' })
|
|
652
|
+
const r = await h.call(h.d, 'POST', '/git/credential', { host: fields.host || '', protocol: fields.protocol || '' }, { timeoutMs: HELPER_WAIT })
|
|
633
653
|
if (r && r.username && r.password) return process.stdout.write(`username=${r.username}\npassword=${r.password}\n`)
|
|
634
654
|
}
|
|
635
655
|
} catch {}
|
|
@@ -642,7 +662,7 @@ async function ownCredential (input) {
|
|
|
642
662
|
const { execFile } = await import('node:child_process')
|
|
643
663
|
const away = os.tmpdir()
|
|
644
664
|
const out = await new Promise((resolve) => {
|
|
645
|
-
const c = execFile('git', ['credential', 'fill'], { cwd: away, encoding: 'utf8', timeout:
|
|
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))
|
|
646
666
|
c.stdin.end(input.endsWith('\n\n') ? input : `${input.replace(/\n*$/, '')}\n\n`)
|
|
647
667
|
})
|
|
648
668
|
const lines = out.split('\n').filter((l) => /^(username|password)=/.test(l))
|
|
@@ -653,22 +673,35 @@ async function ownCredential (input) {
|
|
|
653
673
|
async function gitPrePush () {
|
|
654
674
|
const dir = helperDir()
|
|
655
675
|
const [remote = 'origin'] = argv
|
|
676
|
+
helperDeadline()
|
|
656
677
|
const input = await readStdin()
|
|
657
678
|
const refs = input.split('\n').filter(Boolean).map((l) => { const [localRef, localSha, remoteRef, remoteSha] = l.trim().split(/\s+/); return { localRef, localSha, remoteRef, remoteSha } })
|
|
658
679
|
try {
|
|
659
680
|
const h = await helperDaemon(dir)
|
|
660
681
|
if (!h) return
|
|
661
|
-
const r = await h.call(h.d, 'POST', '/git/push-check', { remote, refs })
|
|
682
|
+
const r = await h.call(h.d, 'POST', '/git/push-check', { remote, refs }, { timeoutMs: HELPER_WAIT })
|
|
662
683
|
if (r && r.ok === false) { process.stderr.write(`Quilt: ${r.why || 'this push is not allowed here'}\n`); process.exitCode = 1 }
|
|
663
684
|
} catch {}
|
|
664
685
|
}
|
|
665
686
|
|
|
666
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). */
|
|
667
689
|
async function gitToken () {
|
|
668
|
-
const {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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) }
|
|
672
705
|
}
|
|
673
706
|
|
|
674
707
|
async function githubStatusCmd () {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "heyquilt",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.107",
|
|
4
4
|
"description": "Real-time, tool-agnostic pair vibe coding: sync a project folder live between collaborators using any AI coding tool.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Daniel Carmichael",
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
"cowove": "bin/quilt.js"
|
|
14
14
|
},
|
|
15
15
|
"scripts": {
|
|
16
|
-
"test": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test test/unit/*.test.js test/integration/*.test.js test/e2e/*.test.js",
|
|
16
|
+
"test": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test --test-timeout=600000 test/unit/*.test.js test/integration/*.test.js test/e2e/*.test.js",
|
|
17
17
|
"test:unit": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test test/unit/*.test.js",
|
|
18
18
|
"test:integration": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test test/integration/*.test.js",
|
|
19
|
-
"test:e2e": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test test/e2e/*.test.js",
|
|
19
|
+
"test:e2e": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test --test-timeout=600000 test/e2e/*.test.js",
|
|
20
20
|
"test:stress": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test --test-concurrency=1 test/stress/*.test.js",
|
|
21
21
|
"test:file": "node --disable-warning=ExperimentalWarning --import ./test/helpers/setup.js --test",
|
|
22
22
|
"test:coverage": "node scripts/coverage.mjs",
|
package/src/account.js
CHANGED
|
@@ -186,6 +186,34 @@ export async function listAgents ({ token, api = apiUrl(), fetch: fetchImpl = gl
|
|
|
186
186
|
return r.agents
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
// Sandbox keys (QUILT_AGENT_KEY) on one of your agents: its Keys table in Settings › Agents.
|
|
190
|
+
const sandbox$ = (id, rest = '') => `/v1/agents/${encodeURIComponent(id)}/sandbox-keys${rest}`
|
|
191
|
+
|
|
192
|
+
/** An agent's sandbox keys ({ keys, sessions }): never the keys themselves. */
|
|
193
|
+
export async function listSandboxKeys ({ token, id, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
194
|
+
const r = await call(fetchImpl, api, 'GET', sandbox$(id), null, token)
|
|
195
|
+
if (!Array.isArray(r.keys) || !Array.isArray(r.sessions)) throw new Error(BAD_REPLY)
|
|
196
|
+
return r
|
|
197
|
+
}
|
|
198
|
+
/** A new sandbox key for a session (`session`, its invite link, or `room`, one a key already has): { envKey, key }, shown once. */
|
|
199
|
+
export async function createSandboxKey ({ token, id, name, session, room, sessionName, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
200
|
+
const r = await call(fetchImpl, api, 'POST', `/v1/agents/${encodeURIComponent(id)}/env-key`, { name, ...(session ? { session } : { room }), ...(sessionName ? { sessionName } : {}) }, token)
|
|
201
|
+
if (!r.envKey) throw new Error(BAD_REPLY)
|
|
202
|
+
return r
|
|
203
|
+
}
|
|
204
|
+
/** Rotate: the same key made again (shown once), the old one revoked. */
|
|
205
|
+
export async function rotateSandboxKey ({ token, id, keyId, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
206
|
+
const r = await call(fetchImpl, api, 'POST', sandbox$(id, `/${encodeURIComponent(keyId)}/rotate`), {}, token)
|
|
207
|
+
if (!r.envKey) throw new Error(BAD_REPLY)
|
|
208
|
+
return r
|
|
209
|
+
}
|
|
210
|
+
export async function revokeSandboxKey ({ token, id, keyId, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
211
|
+
return call(fetchImpl, api, 'POST', sandbox$(id, `/${encodeURIComponent(keyId)}/revoke`), {}, token)
|
|
212
|
+
}
|
|
213
|
+
export async function removeSandboxKey ({ token, id, keyId, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
214
|
+
return call(fetchImpl, api, 'DELETE', sandbox$(id, `/${encodeURIComponent(keyId)}`), null, token)
|
|
215
|
+
}
|
|
216
|
+
|
|
189
217
|
/** This account's GitHub connection (Quilt's GitHub App): { available, connected, login? }. */
|
|
190
218
|
export async function githubStatus ({ token, api = apiUrl(), fetch: fetchImpl = globalThis.fetch }) {
|
|
191
219
|
return call(fetchImpl, api, 'GET', '/v1/me/github', null, token)
|
|
@@ -36,7 +36,11 @@ export function startClaudeCodeReader ({ dir, chatDir, onEntries, onState, onLog
|
|
|
36
36
|
const slug = slugFor(chatRoot)
|
|
37
37
|
const files = new Map() // path -> { offset, partial, seen: Set<path> }
|
|
38
38
|
let status = null
|
|
39
|
-
|
|
39
|
+
// Each conversation mid-turn, by its transcript file, with when it last wrote: the tool is working
|
|
40
|
+
// while any of them is, so one conversation ending its turn never makes another's edits look like a
|
|
41
|
+
// person's (they'd go unboxed). A conversation silent for IDLE_AFTER_MS counts as done.
|
|
42
|
+
const midTurn = new Map()
|
|
43
|
+
const settle = () => setStatus(midTurn.size ? 'working' : 'idle')
|
|
40
44
|
let stopped = false
|
|
41
45
|
let failed = false
|
|
42
46
|
let first = true
|
|
@@ -127,8 +131,10 @@ export function startClaudeCodeReader ({ dir, chatDir, onEntries, onState, onLog
|
|
|
127
131
|
if (!r) continue
|
|
128
132
|
entries.push(...r.entries)
|
|
129
133
|
if (r.relevant) {
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
if (!backfill) {
|
|
135
|
+
if (r.endTurn) midTurn.delete(file); else midTurn.set(file, now())
|
|
136
|
+
settle()
|
|
137
|
+
}
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
return backfill ? entries.slice(-BACKFILL_ENTRIES) : entries
|
|
@@ -161,7 +167,8 @@ export function startClaudeCodeReader ({ dir, chatDir, onEntries, onState, onLog
|
|
|
161
167
|
}
|
|
162
168
|
}
|
|
163
169
|
first = false
|
|
164
|
-
|
|
170
|
+
for (const [f, at] of midTurn) if (now() - at > IDLE_AFTER_MS) midTurn.delete(f)
|
|
171
|
+
settle()
|
|
165
172
|
if (out.length) onEntries(out.sort((a, b) => a.ts - b.ts))
|
|
166
173
|
} catch (err) {
|
|
167
174
|
if (!failed) { failed = true; onLog(`Claude Code feed error: ${err.message}`) }
|
|
@@ -48,7 +48,11 @@ export function startCursorTranscriptReader ({ dir, onEntries, onState, onLog =
|
|
|
48
48
|
const root = projectsDir || cursorProjectsDir()
|
|
49
49
|
const files = new Map() // path -> { offset, partial, lineNo }
|
|
50
50
|
let status = null
|
|
51
|
-
|
|
51
|
+
// Each conversation mid-turn, by its transcript file, with when it last wrote: the tool is working
|
|
52
|
+
// while any of them is, so one conversation ending its turn never makes another's edits look like a
|
|
53
|
+
// person's (they'd go unboxed). A conversation silent for IDLE_AFTER_MS counts as done.
|
|
54
|
+
const midTurn = new Map()
|
|
55
|
+
const settle = () => setStatus(midTurn.size ? 'working' : 'idle')
|
|
52
56
|
let stopped = false
|
|
53
57
|
let failed = false
|
|
54
58
|
let first = true
|
|
@@ -112,8 +116,10 @@ export function startCursorTranscriptReader ({ dir, onEntries, onState, onLog =
|
|
|
112
116
|
if (!r) continue
|
|
113
117
|
entries.push(...r.entries)
|
|
114
118
|
if (r.relevant) {
|
|
115
|
-
|
|
116
|
-
|
|
119
|
+
{
|
|
120
|
+
if (r.endTurn) midTurn.delete(file); else midTurn.set(file, now())
|
|
121
|
+
settle()
|
|
122
|
+
}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
return backfill ? entries.slice(-BACKFILL_ENTRIES) : entries
|
|
@@ -157,7 +163,8 @@ export function startCursorTranscriptReader ({ dir, onEntries, onState, onLog =
|
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
first = false
|
|
160
|
-
|
|
166
|
+
for (const [f, at] of midTurn) if (now() - at > IDLE_AFTER_MS) midTurn.delete(f)
|
|
167
|
+
settle()
|
|
161
168
|
if (out.length) onEntries(out)
|
|
162
169
|
} catch (err) {
|
|
163
170
|
if (!failed) { failed = true; onLog(`Cursor feed error: ${err.message}`) }
|
package/src/ai-vm-signin.js
CHANGED
|
@@ -16,9 +16,14 @@ export const SIGN_IN = {
|
|
|
16
16
|
exe: ['claude'],
|
|
17
17
|
args: ['setup-token'],
|
|
18
18
|
env: {},
|
|
19
|
-
|
|
19
|
+
// Claude's sign-in page has moved between hosts (claude.ai, then claude.com/cai/oauth).
|
|
20
|
+
urlHost: /^((www\.)?claude\.(ai|com)|console\.anthropic\.com)$/,
|
|
20
21
|
needsCode: true,
|
|
21
22
|
token: /sk-ant-oat01-[A-Za-z0-9_-]{20,}/,
|
|
23
|
+
// A code it can't use: "OAuth error: … Press Enter to retry", and Enter shows a new link.
|
|
24
|
+
// Its screen is redrawn a few letters at a time, so only fragments are looked for.
|
|
25
|
+
refused: /rror|status code|retry/i,
|
|
26
|
+
retry: '\r',
|
|
22
27
|
status: { file: 'claude-token' }
|
|
23
28
|
},
|
|
24
29
|
Codex: {
|
|
@@ -55,10 +60,11 @@ export function readSignIn (tool, text) {
|
|
|
55
60
|
const out = { url: '', code: '', needsCode: false, token: '' }
|
|
56
61
|
if (!t) return out
|
|
57
62
|
const s = plain(text)
|
|
63
|
+
// The latest link: a sign-in that starts over prints a new one.
|
|
58
64
|
for (const m of s.matchAll(/https:\/\/[^\s"'<>]+/g)) {
|
|
59
65
|
try {
|
|
60
66
|
const u = new URL(m[0].replace(/[).,;]+$/, ''))
|
|
61
|
-
if (t.urlHost.test(u.hostname))
|
|
67
|
+
if (t.urlHost.test(u.hostname)) out.url = u.href
|
|
62
68
|
} catch {}
|
|
63
69
|
}
|
|
64
70
|
if (t.code) {
|
|
@@ -71,6 +77,12 @@ export function readSignIn (tool, text) {
|
|
|
71
77
|
return out
|
|
72
78
|
}
|
|
73
79
|
|
|
80
|
+
/** Whether what a tool printed after a pasted code says it didn't take it. */
|
|
81
|
+
export function codeRefused (tool, text) {
|
|
82
|
+
const t = SIGN_IN[tool]
|
|
83
|
+
return !!(t && t.refused && t.refused.test(plain(text)))
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
/** Where a tool's token is kept on the machine (only Claude Code hands one over). */
|
|
75
87
|
export const tokenFile = (home, tool) => (SIGN_IN[tool]?.status?.file ? path.join(home, '.quilt', 'ai-vm', SIGN_IN[tool].status.file) : null)
|
|
76
88
|
|