heyquilt 0.0.1 → 0.3.90
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +85 -0
- package/README.md +630 -2
- package/RELEASES.md +733 -0
- package/assets/logo.svg +1 -0
- package/bin/quilt.js +816 -0
- package/desktop/main.js +315 -0
- package/package.json +163 -7
- package/src/account.js +563 -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 +180 -0
- package/src/agents/common.js +21 -0
- package/src/agents/cursor-transcripts.js +170 -0
- package/src/agents/cursor.js +404 -0
- package/src/agents/index.js +69 -0
- package/src/ai-vm-signin.js +98 -0
- package/src/ai-vm.js +380 -0
- package/src/api/access.js +75 -0
- package/src/api/activity.js +181 -0
- package/src/api/agent-auth.js +130 -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 +76 -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 +1083 -0
- package/src/api/org-access.js +54 -0
- package/src/api/permissions.js +71 -0
- package/src/api/relay-cluster.js +251 -0
- package/src/api/relay-machines.js +60 -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 +157 -0
- package/src/api/routes/ai-vm.js +369 -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 +177 -0
- package/src/api/routes/relay-cluster.js +90 -0
- package/src/api/routes/relay.js +76 -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 +225 -0
- package/src/api/server.js +623 -0
- package/src/api/slugs.js +20 -0
- package/src/api/supabase-store.js +765 -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 +387 -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 +140 -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 +245 -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 +87 -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 +900 -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 +40 -0
- package/src/mcp-room.js +32 -0
- package/src/mcp.js +1261 -0
- package/src/merge3.js +158 -0
- package/src/merges.js +148 -0
- package/src/pass-source.js +176 -0
- package/src/passes.js +64 -0
- package/src/pathrules.js +127 -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 +597 -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 +137 -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 +1679 -0
- package/src/relay-metrics.js +75 -0
- package/src/relay-outbound.js +1144 -0
- package/src/relay-sync.js +719 -0
- package/src/relay-turn-sync.js +307 -0
- package/src/relay-upstream.js +934 -0
- package/src/relay-webhooks.js +111 -0
- package/src/releases.js +131 -0
- package/src/report.js +129 -0
- package/src/runner.js +306 -0
- package/src/same-edit.js +18 -0
- package/src/server.js +4187 -0
- package/src/session-access.js +170 -0
- package/src/session-name.js +15 -0
- package/src/session.js +7361 -0
- package/src/settings.js +46 -0
- package/src/setup.js +184 -0
- package/src/shell-path.js +46 -0
- package/src/status.js +199 -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 +142 -0
- package/src/turn-sync.js +191 -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 +2309 -0
- package/src/ui/app.js +542 -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 +107 -0
- package/src/ui/cloud-ai.js +221 -0
- package/src/ui/common.js +431 -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 +123 -0
- package/src/ui/git-rule.js +60 -0
- package/src/ui/home.js +1323 -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 +349 -0
- package/src/ui/loom.js +753 -0
- package/src/ui/mark.js +104 -0
- package/src/ui/merges.js +158 -0
- package/src/ui/rail-model.js +78 -0
- package/src/ui/rail.js +173 -0
- package/src/ui/releases.js +182 -0
- package/src/ui/schedule.js +156 -0
- package/src/ui/session.js +3394 -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 +276 -0
- package/src/ui/workspaces.js +542 -0
- package/src/ui-server.js +1272 -0
- package/src/update-check.js +67 -0
- package/src/upstream.js +155 -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/src/runner.js
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Starting and stopping a session for a folder. Shared by `quilt join` and
|
|
2
|
+
// `quilt ui` so both behave identically (config, STATUS.md, control API).
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import os from 'node:os'
|
|
5
|
+
import path from 'node:path'
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
import { Session } from './session.js'
|
|
8
|
+
import { startControl } from './control.js'
|
|
9
|
+
import { renderStatus } from './status.js'
|
|
10
|
+
import { startAgentReaders } from './agents/index.js'
|
|
11
|
+
import { relayUrl, isHostedRelay, getSettings } from './settings.js'
|
|
12
|
+
import { createSummarizer, createTaskTitler } from './summarize.js'
|
|
13
|
+
import { quiltHome, migrateDir } from './legacy.js'
|
|
14
|
+
import { writePrivateJson } from './private-file.js'
|
|
15
|
+
import { listProcesses } from './procs.js'
|
|
16
|
+
import { JOIN_HOST, INVALID_INVITE, buildInvite, parseInvite } from './ui/invite.js'
|
|
17
|
+
import { installHooks, HOOKS_FILE } from './setup.js'
|
|
18
|
+
import { releaseLeftoverHookClaims } from './hooks.js'
|
|
19
|
+
import { registerOnStart } from './integrations.js'
|
|
20
|
+
import { autowork } from './autowork.js'
|
|
21
|
+
import { keptInProject } from './agent-join.js'
|
|
22
|
+
|
|
23
|
+
export { JOIN_HOST }
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An invite link: https://join.heyquilt.com/<room>#<secret> for sessions on Quilt's relay,
|
|
27
|
+
* or https://<relay>/join/<room>#<secret> for any other relay (development relays).
|
|
28
|
+
*/
|
|
29
|
+
export function encodeInvite (c) {
|
|
30
|
+
return buildInvite(c, isHostedRelay)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Reads an invite link (or an older base64 code), with or without "quilt join" or "quilt:" in front.
|
|
35
|
+
* A link naming its own relay is accepted only for Quilt's relay or the one this computer already
|
|
36
|
+
* uses (QUILT_SERVER), so a crafted link can't hand this computer's pass and files to another relay.
|
|
37
|
+
*/
|
|
38
|
+
export function decodeInvite (code) {
|
|
39
|
+
const r = parseInvite(code, { allowRelay: (s) => isHostedRelay(s) || s === relayUrl() })
|
|
40
|
+
return { server: r.relay || relayUrl(), room: r.room, secret: r.secret }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** A new room on Quilt's relay: `secret` invites people to edit, `viewSecret` to only watch. */
|
|
44
|
+
export function newConn (server = relayUrl()) {
|
|
45
|
+
return {
|
|
46
|
+
server,
|
|
47
|
+
// 64 random bits: a new session never draws the name of one already on the relay.
|
|
48
|
+
room: `room-${crypto.randomBytes(8).toString('hex')}`,
|
|
49
|
+
secret: crypto.randomBytes(18).toString('base64url'),
|
|
50
|
+
viewSecret: crypto.randomBytes(18).toString('base64url')
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function readConfig (dir) {
|
|
55
|
+
try { return JSON.parse(fs.readFileSync(path.join(migrateDir(dir), 'config.json'), 'utf8')) } catch { return null }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The other process syncing this exact folder ({ pid, port, token } from its daemon.json), or null. */
|
|
59
|
+
function otherSync (dir) {
|
|
60
|
+
try {
|
|
61
|
+
const info = JSON.parse(fs.readFileSync(path.join(dir, '.quilt', 'daemon.json'), 'utf8'))
|
|
62
|
+
if (info.pid === process.pid) return null
|
|
63
|
+
process.kill(info.pid, 0)
|
|
64
|
+
return info
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True if another process is already syncing this exact folder. */
|
|
71
|
+
export function runningElsewhere (dir) {
|
|
72
|
+
return !!otherSync(dir)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Says what is already syncing `dir`, so the person can stop it rather than guess: an AI
|
|
77
|
+
* agent that joined from its tool's quilt MCP server, a `quilt join` in a terminal, another
|
|
78
|
+
* copy of the app. Null when nothing else is.
|
|
79
|
+
*/
|
|
80
|
+
export async function describeOtherSync (dir) {
|
|
81
|
+
const other = otherSync(dir)
|
|
82
|
+
if (!other) return null
|
|
83
|
+
let info = null
|
|
84
|
+
try {
|
|
85
|
+
const res = await fetch(`http://127.0.0.1:${other.port}/info`, { headers: { authorization: `Bearer ${other.token}` }, signal: AbortSignal.timeout(1500) })
|
|
86
|
+
if (res.ok) info = await res.json()
|
|
87
|
+
} catch {}
|
|
88
|
+
const proc = listProcesses().find((p) => p.pid === other.pid)
|
|
89
|
+
const who = info && info.kind === 'agent'
|
|
90
|
+
? `your AI agent "${info.name}" joined it from its tool's quilt MCP server (process ${other.pid}). Ask the agent to leave with quilt_leave_session, or close that tool`
|
|
91
|
+
: proc && proc.kind === 'app'
|
|
92
|
+
? `another copy of the Quilt app (process ${other.pid}) has it open. Leave it there, or quit that app`
|
|
93
|
+
: proc && proc.kind === 'sync'
|
|
94
|
+
? `a \`quilt join\` in a terminal (process ${other.pid}) is syncing it${info ? ` as "${info.name}"` : ''}. Stop that one (Ctrl-C there, or \`quilt stop\`)`
|
|
95
|
+
: `another quilt process (${other.pid}) is syncing it${info ? ` as "${info.name}"` : ''}. Stop that one first`
|
|
96
|
+
return `This folder is already being synced by another quilt process: ${who}, then rejoin here.`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Starts syncing `dir`. `conn` is { server, room, secret }; `inviteServer`
|
|
101
|
+
* optionally overrides the relay address given out in invites (e.g. a public
|
|
102
|
+
* tunnel URL when the relay runs on this machine).
|
|
103
|
+
*/
|
|
104
|
+
export async function runSession ({ dir, conn, name, tool, color = null, shareByDefault = true, summarizeByDefault = false, joined = false, prefer = 'remote', inviteServer, onLog, onFatal, onDebug, kind = 'human', agentFeed = true, readerOptions = {}, passes = null, identity = null, startName = '', startAgentCommits = '', workspace = '' }) {
|
|
105
|
+
dir = path.resolve(dir)
|
|
106
|
+
if (!/^wss?:\/\//.test(conn.server)) throw new Error('The relay address must start with ws:// or wss://')
|
|
107
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true })
|
|
108
|
+
if (!fs.statSync(dir).isDirectory()) throw new Error(`${dir} is not a folder`)
|
|
109
|
+
const busy = await describeOtherSync(dir)
|
|
110
|
+
if (busy) throw new Error(busy)
|
|
111
|
+
|
|
112
|
+
// With passes, the relay knows you by your account (or agent): its name and agent
|
|
113
|
+
// badge come from the pass. The session starts with the name saved on this computer
|
|
114
|
+
// (or one already fetched) and takes the pass's once it arrives, without waiting for
|
|
115
|
+
// one here, so being offline doesn't stop a session from starting.
|
|
116
|
+
const p = passes && passes.payload
|
|
117
|
+
if (p && p.name) name = p.name
|
|
118
|
+
if (p && p.kind === 'agent') kind = 'agent'
|
|
119
|
+
name = (name || os.userInfo().username).trim()
|
|
120
|
+
tool = tool || 'unknown'
|
|
121
|
+
const invite = encodeInvite({ ...conn, server: inviteServer || conn.server })
|
|
122
|
+
// Only the person who made the room has the view-only secret.
|
|
123
|
+
const viewInvite = conn.viewSecret ? encodeInvite({ ...conn, secret: conn.viewSecret, server: inviteServer || conn.server }) : null
|
|
124
|
+
const previous = readConfig(dir)
|
|
125
|
+
// Sharing your AI chat follows your setting; a pause or resume is remembered for this folder.
|
|
126
|
+
const shareAgent = previous && previous.room === conn.room && typeof previous.shareAgent === 'boolean' ? previous.shareAgent : shareByDefault !== false
|
|
127
|
+
const summarize = previous && previous.room === conn.room && typeof previous.summarize === 'boolean' ? previous.summarize : !!summarizeByDefault
|
|
128
|
+
// The workspace carries over only for the same room: a new room in this folder starts outside any.
|
|
129
|
+
const savedWorkspace = workspace || (previous && previous.room === conn.room ? previous.workspace : '') || ''
|
|
130
|
+
fs.mkdirSync(path.join(dir, '.quilt'), { recursive: true })
|
|
131
|
+
const configFile = path.join(dir, '.quilt', 'config.json')
|
|
132
|
+
// It holds the room secret: written privately and atomically (see private-file.js).
|
|
133
|
+
writePrivateJson(configFile, { ...conn, name, tool, kind, inviteServer: inviteServer || undefined, shareAgent, summarize, workspace: savedWorkspace || undefined })
|
|
134
|
+
// Keeps the saved name in step with the pass's (the rest of the file may have changed since).
|
|
135
|
+
const saveName = (name) => {
|
|
136
|
+
try { writePrivateJson(configFile, { ...JSON.parse(fs.readFileSync(configFile, 'utf8')), name }) } catch {}
|
|
137
|
+
}
|
|
138
|
+
ensureGitExclude(dir)
|
|
139
|
+
|
|
140
|
+
// An agent whose keys are kept in its project (quilt agent join --here) is in a sandbox that is cleaned up after every run: it works in turns.
|
|
141
|
+
const turns = kind === 'agent' && keptInProject(name)
|
|
142
|
+
const session = new Session({ dir, ...conn, name, tool, color, prefer, kind, shareAgent, identity, passes, startName, startAgentCommits, workspace: savedWorkspace, turns })
|
|
143
|
+
const summarizer = () => createSummarizer({ onWarn: (msg) => session.log(`✂️ ${msg}`) })
|
|
144
|
+
if (summarize) session.summarizer = summarizer()
|
|
145
|
+
session.taskTitler = createTaskTitler({ onWarn: (msg) => session.log(`📋 ${msg}`) })
|
|
146
|
+
if (onLog) session.on('log', onLog)
|
|
147
|
+
if (onDebug) session.on('debug', onDebug)
|
|
148
|
+
session.on('fatal', (err) => onFatal && onFatal(err))
|
|
149
|
+
const statusFile = path.join(dir, '.quilt', 'STATUS.md')
|
|
150
|
+
session.on('status-changed', () => {
|
|
151
|
+
// Read config fresh: the workspace can change after the session started (the app's
|
|
152
|
+
// "move to workspace", or a later quilt_start_session call writing it here).
|
|
153
|
+
try { fs.writeFileSync(statusFile, renderStatus({ ...session.status(), workspace: readConfig(dir)?.workspace })) } catch {}
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
await session.start({ waitTimeoutMs: 15000 })
|
|
158
|
+
} catch (err) {
|
|
159
|
+
await session.stop().catch(() => {})
|
|
160
|
+
throw err
|
|
161
|
+
}
|
|
162
|
+
// The pass may have named us differently from the name we started with.
|
|
163
|
+
if (session.name !== name) saveName(session.name)
|
|
164
|
+
session.on('identity', ({ name }) => saveName(name))
|
|
165
|
+
const control = await startControl(session, { invite, viewInvite, joined })
|
|
166
|
+
// Work for this person's AI can run on its own here, when they turned that on (autowork.js).
|
|
167
|
+
autowork().attach(session)
|
|
168
|
+
remember({ dir, room: conn.room, server: conn.server, name: session.name, tool, kind, workspace: savedWorkspace })
|
|
169
|
+
|
|
170
|
+
// Every AI tool on this computer can reach Quilt (its MCP server, by absolute path), with nothing to set up.
|
|
171
|
+
registerOnStart((line) => session.log(line))
|
|
172
|
+
// Claude Code also applies Quilt's rules by itself through hooks (src/hooks.js), in this person's own
|
|
173
|
+
// settings file; everyone's session writes its own, so the rule holds for everyone in the room.
|
|
174
|
+
try { if (installHooks(dir)) session.log(`🪝 added Quilt's Claude Code hooks to ${HOOKS_FILE}: files are checked before Claude edits them`) } catch {}
|
|
175
|
+
releaseLeftoverHookClaims(session).then((n) => { if (n) session.log(`🔓 released ${n} claim(s) left by an earlier AI session`) }).catch(() => {})
|
|
176
|
+
|
|
177
|
+
// Share this person's AI chat (Claude Code, Cursor) with the room.
|
|
178
|
+
const readers = agentFeed
|
|
179
|
+
? startAgentReaders({
|
|
180
|
+
dir,
|
|
181
|
+
...readerOptions,
|
|
182
|
+
onEntries: (entries) => session.pushAgentEntries(entries),
|
|
183
|
+
onState: (state) => session.setAgentState(state),
|
|
184
|
+
onLog: (line) => onLog && onLog(line)
|
|
185
|
+
})
|
|
186
|
+
: null
|
|
187
|
+
|
|
188
|
+
let stopped = false
|
|
189
|
+
return {
|
|
190
|
+
session,
|
|
191
|
+
invite,
|
|
192
|
+
viewInvite,
|
|
193
|
+
summarizer,
|
|
194
|
+
dir,
|
|
195
|
+
stop: async () => {
|
|
196
|
+
if (stopped) return
|
|
197
|
+
stopped = true
|
|
198
|
+
if (readers) readers.stop()
|
|
199
|
+
await control.close()
|
|
200
|
+
await session.stop()
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Keep .quilt/ and this person's hook settings out of git without editing the (synced) .gitignore. */
|
|
206
|
+
function ensureGitExclude (dir) {
|
|
207
|
+
const exclude = path.join(dir, '.git', 'info', 'exclude')
|
|
208
|
+
try {
|
|
209
|
+
if (!fs.existsSync(path.join(dir, '.git'))) return
|
|
210
|
+
let text = fs.existsSync(exclude) ? fs.readFileSync(exclude, 'utf8') : ''
|
|
211
|
+
for (const line of ['.quilt/', HOOKS_FILE]) {
|
|
212
|
+
if (text.split('\n').includes(line)) continue
|
|
213
|
+
fs.mkdirSync(path.dirname(exclude), { recursive: true })
|
|
214
|
+
text = `${text && !text.endsWith('\n') ? `${text}\n` : text}${line}\n`
|
|
215
|
+
fs.writeFileSync(exclude, text)
|
|
216
|
+
}
|
|
217
|
+
} catch {}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ------------------------------------------------- whose folder is it? --
|
|
221
|
+
// A folder is a person's unless it is an agent's own copy of a room, and that is known
|
|
222
|
+
// by its name: "quilt-<room>-<agent>" (or "quilt-<room>" from older versions), which is
|
|
223
|
+
// how agents' copies are always made. Who ran it last says nothing: an agent run in a
|
|
224
|
+
// person's folder (an older version, a `quilt join --agent` there) must not make it the
|
|
225
|
+
// agent's, or the person loses it from Recent and can't get back in.
|
|
226
|
+
|
|
227
|
+
const expandHome = (p) => p === '~' ? os.homedir() : String(p).startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p
|
|
228
|
+
const slug = (name) => String(name || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'agent'
|
|
229
|
+
|
|
230
|
+
/** True if `dir` is an agent's own copy of `room`. */
|
|
231
|
+
export function isAgentCopy (dir, room) {
|
|
232
|
+
const base = path.basename(path.resolve(dir))
|
|
233
|
+
return base === `quilt-${room}` || base.startsWith(`quilt-${room}-`)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* A folder a person synced from this computer (the app, or `quilt join`): any synced folder that
|
|
238
|
+
* isn't an agent's copy, or (for `agent`) the agent's own folder: one it synced itself, like a
|
|
239
|
+
* sandbox's project that it rejoins every turn.
|
|
240
|
+
*/
|
|
241
|
+
export function personsFolder (dir, agent = null) {
|
|
242
|
+
const saved = readConfig(dir)
|
|
243
|
+
if (!saved || isAgentCopy(dir, saved.room)) return false
|
|
244
|
+
return !(agent && saved.kind === 'agent' && saved.name === agent)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Where an agent keeps its own copy of a room: "quilt-<room>-<agent>" under this computer's
|
|
249
|
+
* join folder (the app's "Join into" setting, ~/quilt by default), so it never lands inside
|
|
250
|
+
* a person's project, and two agents on one computer never share a copy.
|
|
251
|
+
*/
|
|
252
|
+
export function agentCopyFolder (room, agent) {
|
|
253
|
+
const root = path.resolve(expandHome(getSettings().joinDir || '~/quilt'))
|
|
254
|
+
const dir = path.resolve(root, `quilt-${room}-${slug(agent)}`)
|
|
255
|
+
if (path.dirname(dir) !== root) throw new Error(INVALID_INVITE)
|
|
256
|
+
return dir
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Recently used folders, for the UI's "rejoin" list.
|
|
260
|
+
const recentFile = () => path.join(quiltHome(), 'recent.json')
|
|
261
|
+
|
|
262
|
+
/** A person's recent folders. An agent's own copies of rooms are its to rejoin, not the app's. */
|
|
263
|
+
export function recentSessions () {
|
|
264
|
+
try {
|
|
265
|
+
return JSON.parse(fs.readFileSync(recentFile(), 'utf8')).filter((r) => !isAgentCopy(r.dir, r.room) && fs.existsSync(path.join(r.dir, '.quilt', 'config.json')))
|
|
266
|
+
} catch {
|
|
267
|
+
return []
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Drops a folder from the recent list (its files and settings stay). */
|
|
272
|
+
export function forgetRecent (dir) {
|
|
273
|
+
try {
|
|
274
|
+
const list = JSON.parse(fs.readFileSync(recentFile(), 'utf8')).filter((r) => r.dir !== dir)
|
|
275
|
+
fs.writeFileSync(recentFile(), JSON.stringify(list, null, 2))
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Takes a folder's session out of its workspace on this computer: its config and the recent list. */
|
|
280
|
+
export function forgetWorkspace (dir) {
|
|
281
|
+
const saved = readConfig(dir)
|
|
282
|
+
if (saved && saved.workspace) {
|
|
283
|
+
const { workspace, ...rest } = saved
|
|
284
|
+
try { writePrivateJson(path.join(dir, '.quilt', 'config.json'), rest) } catch {}
|
|
285
|
+
}
|
|
286
|
+
rememberWorkspace(dir, '')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Records which workspace a remembered folder's session is in. */
|
|
290
|
+
export function rememberWorkspace (dir, workspace) {
|
|
291
|
+
try {
|
|
292
|
+
const list = JSON.parse(fs.readFileSync(recentFile(), 'utf8')).map((r) => (r.dir === dir ? { ...r, workspace } : r))
|
|
293
|
+
fs.writeFileSync(recentFile(), JSON.stringify(list, null, 2))
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function remember (entry) {
|
|
298
|
+
// An agent's run in a person's folder isn't theirs to put on the list, or to rename there.
|
|
299
|
+
if (entry.kind === 'agent' && !isAgentCopy(entry.dir, entry.room)) return
|
|
300
|
+
try {
|
|
301
|
+
const list = recentSessions().filter((r) => r.dir !== entry.dir)
|
|
302
|
+
list.unshift({ ...entry, lastUsed: Date.now() })
|
|
303
|
+
fs.mkdirSync(path.dirname(recentFile()), { recursive: true })
|
|
304
|
+
fs.writeFileSync(recentFile(), JSON.stringify(list.slice(0, 12), null, 2))
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
package/src/same-edit.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// A change two folders may make at the same moment, such as the same commits
|
|
2
|
+
// pulled into two folders on one branch, is written under an id taken from the
|
|
3
|
+
// change itself: both folders then make the very same edit, and the shared
|
|
4
|
+
// copy keeps it once instead of twice.
|
|
5
|
+
import crypto from 'node:crypto'
|
|
6
|
+
|
|
7
|
+
/** The Yjs client id for one change: the same path, before and after give the same id. */
|
|
8
|
+
export function sameEditClient (...parts) {
|
|
9
|
+
const id = crypto.createHash('sha1').update(parts.map((p) => p ?? '\u0001').join('\u0000')).digest().readUInt32BE(0)
|
|
10
|
+
return id || 1
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Runs fn with the doc's new items made by `client`, then puts the doc's own id back. */
|
|
14
|
+
export function asClient (doc, client, fn) {
|
|
15
|
+
const own = doc.clientID
|
|
16
|
+
doc.clientID = client
|
|
17
|
+
try { return fn() } finally { doc.clientID = own }
|
|
18
|
+
}
|