@edspencer/paddock 0.0.1 → 0.56.0
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 +69 -0
- package/install-notice.mjs +29 -0
- package/package.json +58 -5
- package/packages/server/dist/adoptable.d.ts +127 -0
- package/packages/server/dist/adoptable.js +315 -0
- package/packages/server/dist/app.d.ts +67 -0
- package/packages/server/dist/app.js +325 -0
- package/packages/server/dist/archive.d.ts +42 -0
- package/packages/server/dist/archive.js +125 -0
- package/packages/server/dist/attachments-config.d.ts +114 -0
- package/packages/server/dist/attachments-config.js +165 -0
- package/packages/server/dist/attachments-hint.d.ts +53 -0
- package/packages/server/dist/attachments-hint.js +140 -0
- package/packages/server/dist/attachments.d.ts +33 -0
- package/packages/server/dist/attachments.js +117 -0
- package/packages/server/dist/auth.d.ts +84 -0
- package/packages/server/dist/auth.js +247 -0
- package/packages/server/dist/background.d.ts +36 -0
- package/packages/server/dist/background.js +137 -0
- package/packages/server/dist/bind-safety.d.ts +56 -0
- package/packages/server/dist/bind-safety.js +51 -0
- package/packages/server/dist/brand.d.ts +44 -0
- package/packages/server/dist/brand.js +88 -0
- package/packages/server/dist/chat-dto.d.ts +210 -0
- package/packages/server/dist/chat-dto.js +308 -0
- package/packages/server/dist/cli/import-chats.d.ts +21 -0
- package/packages/server/dist/cli/import-chats.js +197 -0
- package/packages/server/dist/cli/paddock.d.ts +2 -0
- package/packages/server/dist/cli/paddock.js +247 -0
- package/packages/server/dist/concurrency.d.ts +57 -0
- package/packages/server/dist/concurrency.js +71 -0
- package/packages/server/dist/config.d.ts +512 -0
- package/packages/server/dist/config.js +593 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/event-bus.d.ts +87 -0
- package/packages/server/dist/event-bus.js +48 -0
- package/packages/server/dist/git.d.ts +128 -0
- package/packages/server/dist/git.js +446 -0
- package/packages/server/dist/github-auth.d.ts +44 -0
- package/packages/server/dist/github-auth.js +169 -0
- package/packages/server/dist/herdctl-agent-config.d.ts +98 -0
- package/packages/server/dist/herdctl-agent-config.js +331 -0
- package/packages/server/dist/herdctl-agent-names.d.ts +172 -0
- package/packages/server/dist/herdctl-agent-names.js +228 -0
- package/packages/server/dist/herdctl-jobs-index.d.ts +65 -0
- package/packages/server/dist/herdctl-jobs-index.js +225 -0
- package/packages/server/dist/herdctl-jobs.d.ts +98 -0
- package/packages/server/dist/herdctl-jobs.js +225 -0
- package/packages/server/dist/herdctl.d.ts +572 -0
- package/packages/server/dist/herdctl.js +1218 -0
- package/packages/server/dist/hook-config.d.ts +217 -0
- package/packages/server/dist/hook-config.js +224 -0
- package/packages/server/dist/http-bytes.d.ts +24 -0
- package/packages/server/dist/http-bytes.js +52 -0
- package/packages/server/dist/index.d.ts +1 -0
- package/packages/server/dist/index.js +14 -0
- package/packages/server/dist/instance-config.d.ts +135 -0
- package/packages/server/dist/instance-config.js +345 -0
- package/packages/server/dist/localcommand.d.ts +23 -0
- package/packages/server/dist/localcommand.js +140 -0
- package/packages/server/dist/management-auth.d.ts +99 -0
- package/packages/server/dist/management-auth.js +165 -0
- package/packages/server/dist/management-config.d.ts +164 -0
- package/packages/server/dist/management-config.js +286 -0
- package/packages/server/dist/management-mcp-server.d.ts +20 -0
- package/packages/server/dist/management-mcp-server.js +95 -0
- package/packages/server/dist/management-metadata.d.ts +67 -0
- package/packages/server/dist/management-metadata.js +56 -0
- package/packages/server/dist/management-ops.d.ts +110 -0
- package/packages/server/dist/management-ops.js +449 -0
- package/packages/server/dist/management-policy.d.ts +172 -0
- package/packages/server/dist/management-policy.js +259 -0
- package/packages/server/dist/message-provenance.d.ts +112 -0
- package/packages/server/dist/message-provenance.js +260 -0
- package/packages/server/dist/models.d.ts +164 -0
- package/packages/server/dist/models.js +208 -0
- package/packages/server/dist/openapi.d.ts +66 -0
- package/packages/server/dist/openapi.js +183 -0
- package/packages/server/dist/parent-detach.d.ts +30 -0
- package/packages/server/dist/parent-detach.js +109 -0
- package/packages/server/dist/preload.d.ts +43 -0
- package/packages/server/dist/preload.js +55 -0
- package/packages/server/dist/project-files.d.ts +76 -0
- package/packages/server/dist/project-files.js +191 -0
- package/packages/server/dist/project-mime.d.ts +35 -0
- package/packages/server/dist/project-mime.js +55 -0
- package/packages/server/dist/project-paths.d.ts +88 -0
- package/packages/server/dist/project-paths.js +126 -0
- package/packages/server/dist/project-types.d.ts +316 -0
- package/packages/server/dist/project-types.js +48 -0
- package/packages/server/dist/projects.d.ts +202 -0
- package/packages/server/dist/projects.js +894 -0
- package/packages/server/dist/queued-message.d.ts +35 -0
- package/packages/server/dist/queued-message.js +116 -0
- package/packages/server/dist/read-state.d.ts +46 -0
- package/packages/server/dist/read-state.js +139 -0
- package/packages/server/dist/recovery-config.d.ts +111 -0
- package/packages/server/dist/recovery-config.js +105 -0
- package/packages/server/dist/recovery.d.ts +262 -0
- package/packages/server/dist/recovery.js +490 -0
- package/packages/server/dist/resume-drain.d.ts +122 -0
- package/packages/server/dist/resume-drain.js +158 -0
- package/packages/server/dist/route-context.d.ts +168 -0
- package/packages/server/dist/route-context.js +39 -0
- package/packages/server/dist/route-errors.d.ts +8 -0
- package/packages/server/dist/route-errors.js +13 -0
- package/packages/server/dist/routes/chats.d.ts +12 -0
- package/packages/server/dist/routes/chats.js +1406 -0
- package/packages/server/dist/routes/git.d.ts +13 -0
- package/packages/server/dist/routes/git.js +236 -0
- package/packages/server/dist/routes/mcp.d.ts +83 -0
- package/packages/server/dist/routes/mcp.js +250 -0
- package/packages/server/dist/routes/meta.d.ts +15 -0
- package/packages/server/dist/routes/meta.js +447 -0
- package/packages/server/dist/routes/projects.d.ts +25 -0
- package/packages/server/dist/routes/projects.js +736 -0
- package/packages/server/dist/routes/triggers.d.ts +21 -0
- package/packages/server/dist/routes/triggers.js +324 -0
- package/packages/server/dist/routes/workspace-mount.d.ts +48 -0
- package/packages/server/dist/routes/workspace-mount.js +32 -0
- package/packages/server/dist/routes.d.ts +26 -0
- package/packages/server/dist/routes.js +29 -0
- package/packages/server/dist/run-provenance.d.ts +109 -0
- package/packages/server/dist/run-provenance.js +206 -0
- package/packages/server/dist/runs.d.ts +92 -0
- package/packages/server/dist/runs.js +72 -0
- package/packages/server/dist/schedule-config.d.ts +68 -0
- package/packages/server/dist/schedule-config.js +75 -0
- package/packages/server/dist/schedule-session.d.ts +34 -0
- package/packages/server/dist/schedule-session.js +120 -0
- package/packages/server/dist/self-mcp-descriptions.d.ts +20 -0
- package/packages/server/dist/self-mcp-descriptions.js +145 -0
- package/packages/server/dist/self-mcp-read.d.ts +15 -0
- package/packages/server/dist/self-mcp-read.js +97 -0
- package/packages/server/dist/self-mcp-triggers.d.ts +30 -0
- package/packages/server/dist/self-mcp-triggers.js +144 -0
- package/packages/server/dist/self-mcp-types.d.ts +248 -0
- package/packages/server/dist/self-mcp-types.js +10 -0
- package/packages/server/dist/self-mcp-util.d.ts +73 -0
- package/packages/server/dist/self-mcp-util.js +157 -0
- package/packages/server/dist/self-mcp-write.d.ts +35 -0
- package/packages/server/dist/self-mcp-write.js +215 -0
- package/packages/server/dist/self-mcp.d.ts +95 -0
- package/packages/server/dist/self-mcp.js +448 -0
- package/packages/server/dist/send-file-mcp.d.ts +46 -0
- package/packages/server/dist/send-file-mcp.js +281 -0
- package/packages/server/dist/session-hub.d.ts +186 -0
- package/packages/server/dist/session-hub.js +291 -0
- package/packages/server/dist/spawn-capability.d.ts +99 -0
- package/packages/server/dist/spawn-capability.js +106 -0
- package/packages/server/dist/star.d.ts +26 -0
- package/packages/server/dist/star.js +94 -0
- package/packages/server/dist/start.d.ts +1 -0
- package/packages/server/dist/start.js +21 -0
- package/packages/server/dist/subagents.d.ts +337 -0
- package/packages/server/dist/subagents.js +626 -0
- package/packages/server/dist/sweep.d.ts +122 -0
- package/packages/server/dist/sweep.js +696 -0
- package/packages/server/dist/tooldetails.d.ts +33 -0
- package/packages/server/dist/tooldetails.js +466 -0
- package/packages/server/dist/transcribe.d.ts +69 -0
- package/packages/server/dist/transcribe.js +119 -0
- package/packages/server/dist/transcripts.d.ts +42 -0
- package/packages/server/dist/transcripts.js +164 -0
- package/packages/server/dist/trigger-config.d.ts +278 -0
- package/packages/server/dist/trigger-config.js +417 -0
- package/packages/server/dist/trigger-runtime.d.ts +94 -0
- package/packages/server/dist/trigger-runtime.js +82 -0
- package/packages/server/dist/trigger-session.d.ts +34 -0
- package/packages/server/dist/trigger-session.js +122 -0
- package/packages/server/dist/triggers.d.ts +83 -0
- package/packages/server/dist/triggers.js +124 -0
- package/packages/server/dist/trusted-proxy.d.ts +57 -0
- package/packages/server/dist/trusted-proxy.js +157 -0
- package/packages/server/dist/turn-notice.d.ts +154 -0
- package/packages/server/dist/turn-notice.js +271 -0
- package/packages/server/dist/unread.d.ts +36 -0
- package/packages/server/dist/unread.js +123 -0
- package/packages/server/dist/usage.d.ts +66 -0
- package/packages/server/dist/usage.js +243 -0
- package/packages/server/dist/wake-injection.d.ts +156 -0
- package/packages/server/dist/wake-injection.js +136 -0
- package/packages/server/dist/whisper-local.d.ts +9 -0
- package/packages/server/dist/whisper-local.js +122 -0
- package/packages/server/dist/ws-context.d.ts +158 -0
- package/packages/server/dist/ws-context.js +1 -0
- package/packages/server/dist/ws-protocol.d.ts +423 -0
- package/packages/server/dist/ws-protocol.js +251 -0
- package/packages/server/dist/ws-self-mcp.d.ts +52 -0
- package/packages/server/dist/ws-self-mcp.js +28 -0
- package/packages/server/dist/ws-triggers.d.ts +29 -0
- package/packages/server/dist/ws-triggers.js +261 -0
- package/packages/server/dist/ws-turn.d.ts +86 -0
- package/packages/server/dist/ws-turn.js +751 -0
- package/packages/server/dist/ws.d.ts +62 -0
- package/packages/server/dist/ws.js +1024 -0
- package/packages/server/package.json +6 -0
- package/packages/web/dist/assets/InstanceConfigPage-4cQGTj9Y.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-CJZFLj_8.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-BHwVbbqB.js +2 -0
- package/packages/web/dist/assets/ProjectView-BOvuAITg.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-BCojZOGY.js +2 -0
- package/packages/web/dist/assets/arc-BVyxvnkl.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-Cd9Puqg1.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-B_AQt_Rx.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-7e0TQtXB.js +11 -0
- package/packages/web/dist/assets/channel-C_1aISNn.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-Dz4WZmUZ.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-DVWYldo6.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-BIMrizQd.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-CL2q9fF1.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-BSq3HlQ9.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-Bgt64pLE.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-C8Pr2A38.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-BAOqh-MN.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-B8JE7cHJ.js +2 -0
- package/packages/web/dist/assets/core-lJeGACQg.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-BF4tameA.js +2 -0
- package/packages/web/dist/assets/cpp-Bm3ii6kY.js +2 -0
- package/packages/web/dist/assets/csharp-BNpuYZMG.js +2 -0
- package/packages/web/dist/assets/css-DazXZka4.js +2 -0
- package/packages/web/dist/assets/cytoscape.esm-CUqq0XTU.js +332 -0
- package/packages/web/dist/assets/dagre-BM42HDAG-DdsLM6ql.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-BQ78S_RM.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-CAKE6nHE.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-fEvHRXgr.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-CtWsgeSm.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-Buog8k7_.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-CWvVVPZS.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-DsCGXtp4.js +163 -0
- package/packages/web/dist/assets/format-DamJQ9LO.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-CsdwIvvC.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-hZvrX1sD.js +107 -0
- package/packages/web/dist/assets/go-BPx_y7j8.js +2 -0
- package/packages/web/dist/assets/graph-BlwPajkw.js +2 -0
- package/packages/web/dist/assets/index-CGbZdrm8.js +71 -0
- package/packages/web/dist/assets/index-DJDQ210-.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-nE_Vtz_l.js +3 -0
- package/packages/web/dist/assets/ini-B3BVgEsG.js +2 -0
- package/packages/web/dist/assets/init-Gi6I4Gst.js +2 -0
- package/packages/web/dist/assets/ishikawaDiagram-YF4QCWOH-2Nlqs0oH.js +71 -0
- package/packages/web/dist/assets/java-B5VKqtH2.js +2 -0
- package/packages/web/dist/assets/javascript-BKRaQes9.js +2 -0
- package/packages/web/dist/assets/journeyDiagram-JHISSGLW-mjU1fMc4.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-C_9xNaa3.js +90 -0
- package/packages/web/dist/assets/katex-C5jXJg4s.js +258 -0
- package/packages/web/dist/assets/kotlin-Cw-eF_bm.js +3 -0
- package/packages/web/dist/assets/lastTab-CU1zsF0j.js +2 -0
- package/packages/web/dist/assets/layout-Boy8vhSP.js +2 -0
- package/packages/web/dist/assets/linear-BxKd91ho.js +2 -0
- package/packages/web/dist/assets/mermaid.core-C-CuhD8I.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bs6yfcZv.js +97 -0
- package/packages/web/dist/assets/ordinal-Cboi1Yqb.js +2 -0
- package/packages/web/dist/assets/php-CRdd4dfS.js +3 -0
- package/packages/web/dist/assets/pieDiagram-4H26LBE5-DNdqX7O5.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-ZMJl9OdZ.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-RmxMdm2q.js +85 -0
- package/packages/web/dist/assets/ruby-7T4ktxyv.js +2 -0
- package/packages/web/dist/assets/rust-HC3MBUhY.js +2 -0
- package/packages/web/dist/assets/sankeyDiagram-5OEKKPKP-OAAhWcfK.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-BWF7rCyN.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-M3b_zqqo.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CI7saFDa.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-BpOCh63N.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-D_iPH45Q.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-DVD9I2Om.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-Bn1IKwjn.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-DwtI0bkR.js +8 -0
- package/packages/web/dist/assets/yaml-drsDnIKT.js +2 -0
- package/packages/web/dist/favicon.ico +0 -0
- package/packages/web/dist/fonts/inter-latin.woff2 +0 -0
- package/packages/web/dist/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/packages/web/dist/icons/apple-touch-icon.png +0 -0
- package/packages/web/dist/icons/favicon-16.png +0 -0
- package/packages/web/dist/icons/favicon-32.png +0 -0
- package/packages/web/dist/icons/icon-192.png +0 -0
- package/packages/web/dist/icons/icon-512.png +0 -0
- package/packages/web/dist/icons/maskable-512.png +0 -0
- package/packages/web/dist/index.html +56 -0
- package/packages/web/dist/manifest.webmanifest +32 -0
- package/packages/web/dist/sw.js +139 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Paddock
|
|
2
|
+
|
|
3
|
+
**Project-first launchpad for Claude Code** — server-hosted, persistent, resumable
|
|
4
|
+
sessions organized by project, in a web UI.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
npx @edspencer/paddock
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Then open <http://127.0.0.1:4000>.
|
|
11
|
+
|
|
12
|
+
> **First run downloads ~250 MB.** Paddock drives Claude Code, and the Claude
|
|
13
|
+
> Agent SDK ships a per-platform binary of that size. It cannot be skipped —
|
|
14
|
+
> installing with `--omit=optional` produces a Paddock whose chats all fail.
|
|
15
|
+
> Later runs reuse the npm cache and start immediately. For repeated use,
|
|
16
|
+
> `npm i -g @edspencer/paddock` is friendlier than bare `npx`.
|
|
17
|
+
|
|
18
|
+
## Credentials
|
|
19
|
+
|
|
20
|
+
Paddock runs Claude Code on your behalf, so it needs Claude credentials:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
claude setup-token # Claude Max/Pro
|
|
24
|
+
export ANTHROPIC_API_KEY=sk-ant-… # or API billing
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
If you already use Claude Code on this machine, your existing login is picked up
|
|
28
|
+
automatically and there is nothing to do.
|
|
29
|
+
|
|
30
|
+
## Options
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
-p, --port <port> HTTP/WS port (default 4000)
|
|
34
|
+
--host <host> Bind address (default 127.0.0.1)
|
|
35
|
+
-d, --data-dir <path> Projects + state (default ~/.paddock)
|
|
36
|
+
-v, --version Print the version
|
|
37
|
+
-h, --help Show help
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Your projects, chats and settings live in `~/.paddock` and persist between runs.
|
|
41
|
+
|
|
42
|
+
## Security
|
|
43
|
+
|
|
44
|
+
Paddock binds **loopback only** with authentication disabled, which is the right
|
|
45
|
+
default for a laptop. It **refuses to start** if you bind a routable interface
|
|
46
|
+
while auth is off — to expose it on a network, configure `PADDOCK_AUTH_MODE`
|
|
47
|
+
first. See [AUTH.md](https://github.com/edspencer/paddock/blob/main/AUTH.md).
|
|
48
|
+
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
Node.js 22 or newer.
|
|
52
|
+
|
|
53
|
+
## Other ways to run it
|
|
54
|
+
|
|
55
|
+
A multi-arch Docker image is published alongside this package, and is the better
|
|
56
|
+
fit for a server deployment:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
docker run -d -p 4000:4000 -v /srv/paddock-data:/data \
|
|
60
|
+
-e CLAUDE_CODE_OAUTH_TOKEN=… ghcr.io/edspencer/paddock:latest
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Links
|
|
64
|
+
|
|
65
|
+
- [Documentation](https://github.com/edspencer/paddock#readme)
|
|
66
|
+
- [Issues](https://github.com/edspencer/paddock/issues)
|
|
67
|
+
- Built on [herdctl](https://github.com/edspencer/herdctl)
|
|
68
|
+
|
|
69
|
+
MIT © Ed Spencer
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Printed as a `preinstall` script of the PUBLISHED package, before npm starts
|
|
4
|
+
* fetching the dependency tree.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: Paddock depends on the Claude Agent SDK, whose per-platform
|
|
7
|
+
* binary is ~250 MB. That download dominates a first `npx @edspencer/paddock`
|
|
8
|
+
* and is long enough that, unannounced, it reads as a hang. It also cannot be
|
|
9
|
+
* trimmed — the binary arrives via a platform optionalDependency, and
|
|
10
|
+
* installing with `--omit=optional` produces a Paddock whose chats all fail.
|
|
11
|
+
*
|
|
12
|
+
* NOT wired into packages/server/package.json in the repo: a `preinstall` there
|
|
13
|
+
* would fire on every workspace `npm install` a contributor runs. It belongs to
|
|
14
|
+
* the synthesized publish manifest — see the npm packaging issue.
|
|
15
|
+
*
|
|
16
|
+
* npm may suppress this under `--silent` / non-TTY / some CI log levels, so it
|
|
17
|
+
* is a courtesy, not a guarantee. The same warning belongs in the README, where
|
|
18
|
+
* people actually look before running an unfamiliar command.
|
|
19
|
+
*/
|
|
20
|
+
if (!process.env.PADDOCK_QUIET_INSTALL) {
|
|
21
|
+
process.stdout.write(
|
|
22
|
+
[
|
|
23
|
+
"",
|
|
24
|
+
" Paddock bundles the Claude Code runtime — about 250 MB on first install.",
|
|
25
|
+
" Later runs reuse the cache and start immediately.",
|
|
26
|
+
"",
|
|
27
|
+
].join("\n") + "\n",
|
|
28
|
+
);
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@edspencer/paddock",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.56.0",
|
|
4
|
+
"description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"anthropic",
|
|
9
|
+
"ai",
|
|
10
|
+
"agents",
|
|
11
|
+
"paddock"
|
|
12
|
+
],
|
|
7
13
|
"homepage": "https://github.com/edspencer/paddock#readme",
|
|
8
|
-
"
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/edspencer/paddock/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/edspencer/paddock.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Ed Spencer",
|
|
23
|
+
"type": "module",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"paddock": "packages/server/dist/cli/paddock.js"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"preinstall": "node install-notice.mjs"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@fastify/multipart": "8.3.1",
|
|
35
|
+
"@fastify/static": "7.0.4",
|
|
36
|
+
"@fastify/swagger": "8.15.0",
|
|
37
|
+
"@fastify/swagger-ui": "4.2.0",
|
|
38
|
+
"@fastify/websocket": "10.0.1",
|
|
39
|
+
"@herdctl/chat": "0.8.0",
|
|
40
|
+
"@herdctl/core": "5.29.1",
|
|
41
|
+
"@modelcontextprotocol/sdk": "1.29.0",
|
|
42
|
+
"fastify": "4.28.1",
|
|
43
|
+
"jose": "6.2.3",
|
|
44
|
+
"proxy-addr": "2.0.7",
|
|
45
|
+
"yaml": "2.5.1",
|
|
46
|
+
"zod": "4.4.3"
|
|
47
|
+
},
|
|
48
|
+
"optionalDependencies": {
|
|
49
|
+
"nodejs-whisper": "0.2.9"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"packages/server/dist",
|
|
53
|
+
"packages/server/package.json",
|
|
54
|
+
"packages/web/dist",
|
|
55
|
+
"install-notice.mjs",
|
|
56
|
+
"README.md"
|
|
57
|
+
],
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public",
|
|
60
|
+
"provenance": true
|
|
61
|
+
}
|
|
9
62
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type AdoptableSession } from "@herdctl/core";
|
|
2
|
+
import type { Project } from "./projects.js";
|
|
3
|
+
/** One working directory that has adoptable sessions, and which ones. */
|
|
4
|
+
export interface AdoptableSource {
|
|
5
|
+
/** The working directory whose transcript folder holds these sessions. */
|
|
6
|
+
sourceCwd: string;
|
|
7
|
+
/** Adoptable session ids, newest transcript first. */
|
|
8
|
+
sessionIds: string[];
|
|
9
|
+
}
|
|
10
|
+
/** A candidate that detection deliberately did NOT offer, and why. */
|
|
11
|
+
export interface FilteredSession {
|
|
12
|
+
sessionId: string;
|
|
13
|
+
sourceCwd: string;
|
|
14
|
+
reason: FilterReason;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Why a candidate was withheld. Both values are OURS (paddock-side noise
|
|
18
|
+
* filtering); the engine's own exclusions (sidechain / already-adopted /
|
|
19
|
+
* attributed-to-run) never reach us as candidates at all.
|
|
20
|
+
*/
|
|
21
|
+
export type FilterReason = "too-small" | "slash-command-only";
|
|
22
|
+
/** What a project could import right now. */
|
|
23
|
+
export interface AdoptableSummary {
|
|
24
|
+
/** Total adoptable sessions across every source, after de-dup and filtering. */
|
|
25
|
+
count: number;
|
|
26
|
+
sources: AdoptableSource[];
|
|
27
|
+
/**
|
|
28
|
+
* Candidates withheld by the noise filter. Reported rather than dropped
|
|
29
|
+
* silently so "why is the count 5 and not 7?" always has an answer.
|
|
30
|
+
*/
|
|
31
|
+
filtered: FilteredSession[];
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Minimum transcript size to be worth offering, in bytes.
|
|
35
|
+
*
|
|
36
|
+
* Calibrated against real transcripts: ONE complete Claude Code user record
|
|
37
|
+
* (uuid, parentUuid, sessionId, cwd, version, gitBranch, timestamp, message …)
|
|
38
|
+
* measures ~350-450 bytes on its own. A file under 256 bytes therefore cannot
|
|
39
|
+
* hold even a single complete exchange — it is a zero-byte stub or a truncated
|
|
40
|
+
* fragment. The shortest genuine session in the QA corpus (one user message, no
|
|
41
|
+
* reply — an aborted chat, which IS worth importing) is 396 bytes, so this sits
|
|
42
|
+
* comfortably below the real floor rather than near it.
|
|
43
|
+
*/
|
|
44
|
+
export declare const MIN_TRANSCRIPT_BYTES = 256;
|
|
45
|
+
/**
|
|
46
|
+
* Size ceiling under which a session that OPENS with a slash command is treated
|
|
47
|
+
* as noise.
|
|
48
|
+
*
|
|
49
|
+
* `/mcp`, `/status`, `/cost` and friends open a session that is nothing but the
|
|
50
|
+
* command and its output — real files on disk, but not conversations. The
|
|
51
|
+
* preview alone cannot settle it: the engine truncates a preview at 100 chars,
|
|
52
|
+
* and a session may legitimately open with `/review …` and then run for pages.
|
|
53
|
+
* So the preview says "this began as a command" and the size says "and it never
|
|
54
|
+
* went anywhere". 4 kB is roughly ten complete transcript records — comfortably
|
|
55
|
+
* more than a command plus its output, comfortably less than a conversation.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SLASH_COMMAND_MAX_BYTES = 4096;
|
|
58
|
+
/** The engine surface this module needs — narrowed so tests can fake it. */
|
|
59
|
+
export interface AdoptableFleet {
|
|
60
|
+
listAdoptableSessions(agentName: string, fromWorkingDir?: string): Promise<AdoptableSession[]>;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Per-instance adoptable-session detector with an mtime-keyed cache.
|
|
64
|
+
*
|
|
65
|
+
* ## What is cached, and why the key is sound
|
|
66
|
+
*
|
|
67
|
+
* The expensive work is (a) reading one transcript head per transcript folder on
|
|
68
|
+
* the machine to recover its cwd, and (b) an engine scan per matched source. The
|
|
69
|
+
* cheap work — one `readdir` plus a `stat` per folder — always runs, and
|
|
70
|
+
* produces the key.
|
|
71
|
+
*
|
|
72
|
+
* A cached answer is reused only while ALL of these are unchanged:
|
|
73
|
+
*
|
|
74
|
+
* - `<claudeHome>/projects` mtime+size — a transcript folder appeared or went.
|
|
75
|
+
* - each MATCHED folder's mtime+size — a transcript appeared or went inside it.
|
|
76
|
+
* - `<stateDir>/adopted-sessions` mtime+size — something was adopted/released,
|
|
77
|
+
* which is exactly what removes a session from the adoptable set.
|
|
78
|
+
* - `<stateDir>/jobs` mtime+size — a run completed, and a run record is what
|
|
79
|
+
* flips a native session to `attributed-to-run` (also excluded).
|
|
80
|
+
*
|
|
81
|
+
* Directory mtime changes when an entry is added or removed, NOT when an
|
|
82
|
+
* existing file is appended to — and appending to a transcript cannot change
|
|
83
|
+
* whether it is adoptable, so that granularity is precisely right rather than
|
|
84
|
+
* merely cheap. The one residual is an in-memory platform binding taken without
|
|
85
|
+
* touching either state dir; {@link invalidate} is called after every import for
|
|
86
|
+
* the case that actually matters.
|
|
87
|
+
*/
|
|
88
|
+
export declare class AdoptableIndex {
|
|
89
|
+
private readonly claudeHomePath;
|
|
90
|
+
private readonly stateDir;
|
|
91
|
+
/** folder name → last-seen entry, reused while its `key` is unchanged. */
|
|
92
|
+
private readonly folders;
|
|
93
|
+
/** project key → cached summary + the composite key it was computed under. */
|
|
94
|
+
private readonly summaries;
|
|
95
|
+
constructor(claudeHomePath: string, stateDir: string);
|
|
96
|
+
/**
|
|
97
|
+
* Drop a project's cached summary (or every project's, with no argument).
|
|
98
|
+
* Called after an import, which changes the adoptable set in a way the caller
|
|
99
|
+
* already knows about and shouldn't have to wait for a directory mtime to
|
|
100
|
+
* reveal.
|
|
101
|
+
*
|
|
102
|
+
* NOTE the workspace key may be `""` (the root workspace) — this takes a
|
|
103
|
+
* string and tests `undefined` explicitly, never truthiness.
|
|
104
|
+
*/
|
|
105
|
+
invalidate(projectKey?: string): void;
|
|
106
|
+
/**
|
|
107
|
+
* Enumerate `<claudeHome>/projects/*`, refreshing the recorded cwd only for
|
|
108
|
+
* folders whose file set changed since last time.
|
|
109
|
+
*/
|
|
110
|
+
private scanFolders;
|
|
111
|
+
/**
|
|
112
|
+
* The candidate source working directories for a project, de-duplicated by the
|
|
113
|
+
* transcript folder they resolve to.
|
|
114
|
+
*
|
|
115
|
+
* ORIGIN-FIRST ordering (see {@link adoptableFor}'s session de-dup): matched
|
|
116
|
+
* external checkouts come first, the project's own working directory last.
|
|
117
|
+
*/
|
|
118
|
+
private candidateSources;
|
|
119
|
+
/**
|
|
120
|
+
* What `project` could import, as served by the `adoptable-chats` endpoint.
|
|
121
|
+
*
|
|
122
|
+
* `agentName` is the adopting agent (the project's keeper); it is the metadata
|
|
123
|
+
* cache key the engine warms titles/previews under, so detection and the later
|
|
124
|
+
* adoption agree.
|
|
125
|
+
*/
|
|
126
|
+
adoptableFor(fleet: AdoptableFleet, project: Project, agentName: string): Promise<AdoptableSummary>;
|
|
127
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detection of a project's ADOPTABLE native Claude Code chats (#588).
|
|
3
|
+
*
|
|
4
|
+
* A project is backed by a working directory, and a user very often already has
|
|
5
|
+
* terminal `claude` history for that same directory — or for their own checkout
|
|
6
|
+
* of the same repo, somewhere else entirely on the disk. Those transcripts live
|
|
7
|
+
* in `<claudeHome>/projects/<encoded-cwd>/*.jsonl` and are invisible to paddock
|
|
8
|
+
* until they are adopted. This module answers "what could this project import?".
|
|
9
|
+
*
|
|
10
|
+
* The engine already knows how to decide whether ONE working directory's
|
|
11
|
+
* transcripts are adoptable (`fleet.listAdoptableSessions` — it excludes
|
|
12
|
+
* sidechains, already-adopted sessions and anything a real run is attributed
|
|
13
|
+
* to). What it cannot know is which working directories belong to a paddock
|
|
14
|
+
* project. That is this module's job, and it has three parts:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Assemble candidate sources** — the project's own `workingDir`, plus any
|
|
17
|
+
* `<claudeHome>/projects/*` folder whose RECORDED cwd matches the project.
|
|
18
|
+
* 2. **Filter low-value noise** — a zero-byte transcript or a session that is
|
|
19
|
+
* nothing but a `/mcp` slash command is not a chat anyone wants imported.
|
|
20
|
+
* 3. **Cache** — the scan is O(all transcript folders on the machine), so it is
|
|
21
|
+
* keyed on directory mtimes and only redone when something could have moved.
|
|
22
|
+
*
|
|
23
|
+
* ## Why the recorded cwd, never the folder name
|
|
24
|
+
*
|
|
25
|
+
* `encodePathForCli` replaces every non-alphanumeric character with `-` and
|
|
26
|
+
* truncates+hashes past 200 chars. It is lossy and NOT invertible: `/a/b-c`,
|
|
27
|
+
* `/a-b/c` and `/a/b/c` all encode to `-a-b-c`. Deriving a cwd from a folder
|
|
28
|
+
* name is therefore impossible in general and silently wrong in particular. Each
|
|
29
|
+
* folder's real cwd is read out of the head of one of its transcripts
|
|
30
|
+
* (`readSessionCwd`), which is the authoritative, non-lossy source.
|
|
31
|
+
*
|
|
32
|
+
* A direct consequence: two DIFFERENT candidate cwds can name the SAME physical
|
|
33
|
+
* folder, which would offer every session in it twice. Sources are therefore
|
|
34
|
+
* de-duplicated by the folder's resolved real path, and sessions by id.
|
|
35
|
+
*
|
|
36
|
+
* ## The matching heuristic, and what it cannot do
|
|
37
|
+
*
|
|
38
|
+
* For a REPO-BACKED project a folder matches when its recorded cwd's BASENAME
|
|
39
|
+
* equals the project's checkout name (`repoCheckoutName(repo)`) — i.e. "the user
|
|
40
|
+
* has their own clone of this repo elsewhere". This is deliberately simple and
|
|
41
|
+
* is NOT identity-precise: two unrelated checkouts that happen to share a
|
|
42
|
+
* basename (`~/work/api` and `~/oss/api`) both match, and a clone the user
|
|
43
|
+
* renamed on disk does not match at all. Verifying the checkout's git remote
|
|
44
|
+
* would be exact; it also means running git in every candidate directory, which
|
|
45
|
+
* is the wrong cost for a count shown in a header. Noted as a known limit
|
|
46
|
+
* (#588 gotcha 6) rather than pretended away — the import is per-source, so a
|
|
47
|
+
* user who sees a source they don't recognise can decline it.
|
|
48
|
+
*
|
|
49
|
+
* For a NOTEBOOK project the match is exact cwd equality: its working directory
|
|
50
|
+
* IS the project directory, and nothing else is plausibly "the same project".
|
|
51
|
+
*/
|
|
52
|
+
import { promises as fs } from "node:fs";
|
|
53
|
+
import path from "node:path";
|
|
54
|
+
import { encodePathForCli, readSessionCwd } from "@herdctl/core";
|
|
55
|
+
import { repoCheckoutName } from "./project-paths.js";
|
|
56
|
+
/**
|
|
57
|
+
* Minimum transcript size to be worth offering, in bytes.
|
|
58
|
+
*
|
|
59
|
+
* Calibrated against real transcripts: ONE complete Claude Code user record
|
|
60
|
+
* (uuid, parentUuid, sessionId, cwd, version, gitBranch, timestamp, message …)
|
|
61
|
+
* measures ~350-450 bytes on its own. A file under 256 bytes therefore cannot
|
|
62
|
+
* hold even a single complete exchange — it is a zero-byte stub or a truncated
|
|
63
|
+
* fragment. The shortest genuine session in the QA corpus (one user message, no
|
|
64
|
+
* reply — an aborted chat, which IS worth importing) is 396 bytes, so this sits
|
|
65
|
+
* comfortably below the real floor rather than near it.
|
|
66
|
+
*/
|
|
67
|
+
export const MIN_TRANSCRIPT_BYTES = 256;
|
|
68
|
+
/**
|
|
69
|
+
* Size ceiling under which a session that OPENS with a slash command is treated
|
|
70
|
+
* as noise.
|
|
71
|
+
*
|
|
72
|
+
* `/mcp`, `/status`, `/cost` and friends open a session that is nothing but the
|
|
73
|
+
* command and its output — real files on disk, but not conversations. The
|
|
74
|
+
* preview alone cannot settle it: the engine truncates a preview at 100 chars,
|
|
75
|
+
* and a session may legitimately open with `/review …` and then run for pages.
|
|
76
|
+
* So the preview says "this began as a command" and the size says "and it never
|
|
77
|
+
* went anywhere". 4 kB is roughly ten complete transcript records — comfortably
|
|
78
|
+
* more than a command plus its output, comfortably less than a conversation.
|
|
79
|
+
*/
|
|
80
|
+
export const SLASH_COMMAND_MAX_BYTES = 4096;
|
|
81
|
+
/**
|
|
82
|
+
* A preview that OPENS with a slash-command invocation.
|
|
83
|
+
*
|
|
84
|
+
* Claude Code writes an invocation either as bare text (`/mcp`) or inside the
|
|
85
|
+
* `<command-name>` envelope it uses for expanded commands; both are matched.
|
|
86
|
+
*
|
|
87
|
+
* The trailing lookahead is what keeps an absolute PATH from reading as a
|
|
88
|
+
* command: the command word must end at whitespace, at `<`, or at end-of-string,
|
|
89
|
+
* so a prompt beginning `/data/projects/foo …` does not match (the `/` after
|
|
90
|
+
* `/data` disqualifies it) while `/mcp`, `/mcp</command-name>` and
|
|
91
|
+
* `/review the auth refactor` all do.
|
|
92
|
+
*/
|
|
93
|
+
const SLASH_COMMAND_ONLY_RE = /^\s*(?:<command-name>\s*)?\/[a-z][a-z0-9:_-]*(?![^\s<])/i;
|
|
94
|
+
/** Classify a candidate as noise, or `null` to keep it. */
|
|
95
|
+
function filterReasonFor(session) {
|
|
96
|
+
if (session.sizeBytes < MIN_TRANSCRIPT_BYTES)
|
|
97
|
+
return "too-small";
|
|
98
|
+
if (session.sizeBytes < SLASH_COMMAND_MAX_BYTES &&
|
|
99
|
+
session.preview !== undefined &&
|
|
100
|
+
SLASH_COMMAND_ONLY_RE.test(session.preview)) {
|
|
101
|
+
return "slash-command-only";
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
/** A directory's cache identity: its mtime + size, or `null` when absent. */
|
|
106
|
+
async function dirKey(dir) {
|
|
107
|
+
const st = await fs.stat(dir).catch(() => null);
|
|
108
|
+
return st ? `${st.mtimeMs}:${st.size}` : "-";
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Per-instance adoptable-session detector with an mtime-keyed cache.
|
|
112
|
+
*
|
|
113
|
+
* ## What is cached, and why the key is sound
|
|
114
|
+
*
|
|
115
|
+
* The expensive work is (a) reading one transcript head per transcript folder on
|
|
116
|
+
* the machine to recover its cwd, and (b) an engine scan per matched source. The
|
|
117
|
+
* cheap work — one `readdir` plus a `stat` per folder — always runs, and
|
|
118
|
+
* produces the key.
|
|
119
|
+
*
|
|
120
|
+
* A cached answer is reused only while ALL of these are unchanged:
|
|
121
|
+
*
|
|
122
|
+
* - `<claudeHome>/projects` mtime+size — a transcript folder appeared or went.
|
|
123
|
+
* - each MATCHED folder's mtime+size — a transcript appeared or went inside it.
|
|
124
|
+
* - `<stateDir>/adopted-sessions` mtime+size — something was adopted/released,
|
|
125
|
+
* which is exactly what removes a session from the adoptable set.
|
|
126
|
+
* - `<stateDir>/jobs` mtime+size — a run completed, and a run record is what
|
|
127
|
+
* flips a native session to `attributed-to-run` (also excluded).
|
|
128
|
+
*
|
|
129
|
+
* Directory mtime changes when an entry is added or removed, NOT when an
|
|
130
|
+
* existing file is appended to — and appending to a transcript cannot change
|
|
131
|
+
* whether it is adoptable, so that granularity is precisely right rather than
|
|
132
|
+
* merely cheap. The one residual is an in-memory platform binding taken without
|
|
133
|
+
* touching either state dir; {@link invalidate} is called after every import for
|
|
134
|
+
* the case that actually matters.
|
|
135
|
+
*/
|
|
136
|
+
export class AdoptableIndex {
|
|
137
|
+
claudeHomePath;
|
|
138
|
+
stateDir;
|
|
139
|
+
/** folder name → last-seen entry, reused while its `key` is unchanged. */
|
|
140
|
+
folders = new Map();
|
|
141
|
+
/** project key → cached summary + the composite key it was computed under. */
|
|
142
|
+
summaries = new Map();
|
|
143
|
+
constructor(claudeHomePath, stateDir) {
|
|
144
|
+
this.claudeHomePath = claudeHomePath;
|
|
145
|
+
this.stateDir = stateDir;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Drop a project's cached summary (or every project's, with no argument).
|
|
149
|
+
* Called after an import, which changes the adoptable set in a way the caller
|
|
150
|
+
* already knows about and shouldn't have to wait for a directory mtime to
|
|
151
|
+
* reveal.
|
|
152
|
+
*
|
|
153
|
+
* NOTE the workspace key may be `""` (the root workspace) — this takes a
|
|
154
|
+
* string and tests `undefined` explicitly, never truthiness.
|
|
155
|
+
*/
|
|
156
|
+
invalidate(projectKey) {
|
|
157
|
+
if (projectKey === undefined)
|
|
158
|
+
this.summaries.clear();
|
|
159
|
+
else
|
|
160
|
+
this.summaries.delete(projectKey);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Enumerate `<claudeHome>/projects/*`, refreshing the recorded cwd only for
|
|
164
|
+
* folders whose file set changed since last time.
|
|
165
|
+
*/
|
|
166
|
+
async scanFolders() {
|
|
167
|
+
const root = path.join(this.claudeHomePath, "projects");
|
|
168
|
+
const names = await fs.readdir(root).catch(() => []);
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const name of names) {
|
|
171
|
+
const dir = path.join(root, name);
|
|
172
|
+
const st = await fs.stat(dir).catch(() => null);
|
|
173
|
+
if (!st?.isDirectory())
|
|
174
|
+
continue;
|
|
175
|
+
const key = `${st.mtimeMs}:${st.size}`;
|
|
176
|
+
const cached = this.folders.get(name);
|
|
177
|
+
if (cached && cached.key === key) {
|
|
178
|
+
out.push(cached);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
const entry = {
|
|
182
|
+
name,
|
|
183
|
+
realPath: await fs.realpath(dir).catch(() => dir),
|
|
184
|
+
key,
|
|
185
|
+
cwd: await recordedCwd(dir),
|
|
186
|
+
};
|
|
187
|
+
this.folders.set(name, entry);
|
|
188
|
+
out.push(entry);
|
|
189
|
+
}
|
|
190
|
+
// Forget folders that no longer exist, so the map can't grow without bound.
|
|
191
|
+
const live = new Set(out.map((e) => e.name));
|
|
192
|
+
for (const name of [...this.folders.keys()])
|
|
193
|
+
if (!live.has(name))
|
|
194
|
+
this.folders.delete(name);
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The candidate source working directories for a project, de-duplicated by the
|
|
199
|
+
* transcript folder they resolve to.
|
|
200
|
+
*
|
|
201
|
+
* ORIGIN-FIRST ordering (see {@link adoptableFor}'s session de-dup): matched
|
|
202
|
+
* external checkouts come first, the project's own working directory last.
|
|
203
|
+
*/
|
|
204
|
+
async candidateSources(project, folders) {
|
|
205
|
+
const own = project.workingDir;
|
|
206
|
+
const ownFolder = path.join(this.claudeHomePath, "projects", encodePathForCli(own));
|
|
207
|
+
const ownReal = await fs.realpath(ownFolder).catch(() => ownFolder);
|
|
208
|
+
const matches = (cwd) => {
|
|
209
|
+
if (cwd === own)
|
|
210
|
+
return true;
|
|
211
|
+
// Repo-backed: "the user's own clone of the same repo", matched by the
|
|
212
|
+
// checkout basename. Deliberately loose — see the module header.
|
|
213
|
+
if (project.repo !== undefined && project.repo !== "") {
|
|
214
|
+
return path.basename(cwd) === repoCheckoutName(project.repo);
|
|
215
|
+
}
|
|
216
|
+
// Notebook: exact cwd equality only.
|
|
217
|
+
return false;
|
|
218
|
+
};
|
|
219
|
+
const seen = new Set([ownReal]);
|
|
220
|
+
const sources = [];
|
|
221
|
+
for (const folder of folders) {
|
|
222
|
+
if (folder.cwd === null || !matches(folder.cwd))
|
|
223
|
+
continue;
|
|
224
|
+
if (seen.has(folder.realPath))
|
|
225
|
+
continue;
|
|
226
|
+
seen.add(folder.realPath);
|
|
227
|
+
sources.push(folder.cwd);
|
|
228
|
+
}
|
|
229
|
+
// The project's own working directory goes LAST so that a session offered by
|
|
230
|
+
// both it and an external checkout is attributed to the external one — the
|
|
231
|
+
// origin, where the transcript actually lives, so a copy has something to
|
|
232
|
+
// copy (#588 gotcha 2).
|
|
233
|
+
sources.push(own);
|
|
234
|
+
return sources;
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* What `project` could import, as served by the `adoptable-chats` endpoint.
|
|
238
|
+
*
|
|
239
|
+
* `agentName` is the adopting agent (the project's keeper); it is the metadata
|
|
240
|
+
* cache key the engine warms titles/previews under, so detection and the later
|
|
241
|
+
* adoption agree.
|
|
242
|
+
*/
|
|
243
|
+
async adoptableFor(fleet, project, agentName) {
|
|
244
|
+
const folders = await this.scanFolders();
|
|
245
|
+
const sources = await this.candidateSources(project, folders);
|
|
246
|
+
// Sentinel for "this folder recorded no cwd": it must be a string no real
|
|
247
|
+
// source can equal, and `""` will not do — the root workspace's key is `""`,
|
|
248
|
+
// so an empty cwd is a value that genuinely occurs here. Written as the
|
|
249
|
+
// escape, never a raw NUL byte: a raw NUL makes ripgrep classify the whole
|
|
250
|
+
// file as binary and skip it, so the file vanishes from code search (#570).
|
|
251
|
+
const matchedKeys = folders
|
|
252
|
+
.filter((f) => sources.includes(f.cwd ?? "\u0000"))
|
|
253
|
+
.map((f) => `${f.name}=${f.key}`)
|
|
254
|
+
.sort();
|
|
255
|
+
const key = [
|
|
256
|
+
await dirKey(path.join(this.claudeHomePath, "projects")),
|
|
257
|
+
await dirKey(path.join(this.stateDir, "adopted-sessions")),
|
|
258
|
+
await dirKey(path.join(this.stateDir, "jobs")),
|
|
259
|
+
...matchedKeys,
|
|
260
|
+
].join("|");
|
|
261
|
+
const cached = this.summaries.get(project.slug);
|
|
262
|
+
if (cached && cached.key === key)
|
|
263
|
+
return cached.summary;
|
|
264
|
+
const seen = new Set();
|
|
265
|
+
const out = [];
|
|
266
|
+
const filtered = [];
|
|
267
|
+
let count = 0;
|
|
268
|
+
for (const sourceCwd of sources) {
|
|
269
|
+
// Fault-isolated per source: an unreadable folder costs its own sessions,
|
|
270
|
+
// not the whole count.
|
|
271
|
+
const sessions = await fleet.listAdoptableSessions(agentName, sourceCwd).catch(() => []);
|
|
272
|
+
const ids = [];
|
|
273
|
+
for (const session of sessions) {
|
|
274
|
+
// De-dup by session id. Two candidate cwds can collide onto one folder
|
|
275
|
+
// under the lossy encoding, and a transcript may have been copied; either
|
|
276
|
+
// way one session must be offered once.
|
|
277
|
+
if (seen.has(session.sessionId))
|
|
278
|
+
continue;
|
|
279
|
+
seen.add(session.sessionId);
|
|
280
|
+
const reason = filterReasonFor(session);
|
|
281
|
+
if (reason !== null) {
|
|
282
|
+
filtered.push({ sessionId: session.sessionId, sourceCwd, reason });
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
ids.push(session.sessionId);
|
|
286
|
+
}
|
|
287
|
+
if (ids.length > 0) {
|
|
288
|
+
out.push({ sourceCwd, sessionIds: ids });
|
|
289
|
+
count += ids.length;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const summary = { count, sources: out, filtered };
|
|
293
|
+
this.summaries.set(project.slug, { key, summary });
|
|
294
|
+
return summary;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The cwd recorded inside a transcript folder, by reading the head of its
|
|
299
|
+
* newest-looking `.jsonl`.
|
|
300
|
+
*
|
|
301
|
+
* Every session in a folder shares the encoded cwd, so ONE read identifies the
|
|
302
|
+
* folder. Files are tried in readdir order until one yields a cwd, because a
|
|
303
|
+
* zero-byte or truncated transcript records none (the QA corpus has both).
|
|
304
|
+
*/
|
|
305
|
+
async function recordedCwd(dir) {
|
|
306
|
+
const entries = await fs.readdir(dir).catch(() => []);
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (!entry.endsWith(".jsonl"))
|
|
309
|
+
continue;
|
|
310
|
+
const cwd = await readSessionCwd(path.join(dir, entry)).catch(() => null);
|
|
311
|
+
if (cwd !== null && cwd !== "")
|
|
312
|
+
return cwd;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App factory — builds a fully-wired Fastify instance (REST + WS + optional
|
|
3
|
+
* static SPA) around a ProjectStore + HerdctlService + GitService + GithubAuth.
|
|
4
|
+
*
|
|
5
|
+
* Extracted from index.ts so tests can boot the REAL app in-process against a
|
|
6
|
+
* temp data dir (with a fake `claude` on PATH) without binding a port or
|
|
7
|
+
* registering signal handlers. `index.ts` calls `buildApp()` then `app.listen`.
|
|
8
|
+
*
|
|
9
|
+
* This is a testability seam only — the wiring is identical to the prior inline
|
|
10
|
+
* bootstrap; no behavior changed.
|
|
11
|
+
*/
|
|
12
|
+
import { type FastifyInstance } from "fastify";
|
|
13
|
+
import { type PaddockConfig } from "./config.js";
|
|
14
|
+
import { ProjectStore } from "./projects.js";
|
|
15
|
+
import { HerdctlService } from "./herdctl.js";
|
|
16
|
+
import { GitService } from "./git.js";
|
|
17
|
+
import { GithubAuth } from "./github-auth.js";
|
|
18
|
+
import { type Transcriber } from "./transcribe.js";
|
|
19
|
+
import { SweepService } from "./sweep.js";
|
|
20
|
+
import { ArchiveStore } from "./archive.js";
|
|
21
|
+
import { StarStore } from "./star.js";
|
|
22
|
+
import { ReadStateStore } from "./read-state.js";
|
|
23
|
+
import { UnreadStore } from "./unread.js";
|
|
24
|
+
import { ParentDetachStore } from "./parent-detach.js";
|
|
25
|
+
import { QueuedMessageStore } from "./queued-message.js";
|
|
26
|
+
import { RunProvenanceStore } from "./run-provenance.js";
|
|
27
|
+
import { PaddockEventBus } from "./event-bus.js";
|
|
28
|
+
import { TriggerService } from "./triggers.js";
|
|
29
|
+
export interface BuiltApp {
|
|
30
|
+
app: FastifyInstance;
|
|
31
|
+
cfg: PaddockConfig;
|
|
32
|
+
projects: ProjectStore;
|
|
33
|
+
herdctl: HerdctlService;
|
|
34
|
+
git: GitService;
|
|
35
|
+
githubAuth: GithubAuth;
|
|
36
|
+
sweep: SweepService;
|
|
37
|
+
archive: ArchiveStore;
|
|
38
|
+
star: StarStore;
|
|
39
|
+
readState: ReadStateStore;
|
|
40
|
+
/** Per-user manual "unread" override sidecar (#458). */
|
|
41
|
+
unread: UnreadStore;
|
|
42
|
+
/** Explicit "detached from its parent" sidecar (#508) — beats both parent tiers. */
|
|
43
|
+
parentDetach: ParentDetachStore;
|
|
44
|
+
/** Per-chat creation provenance (#261), incl. the recorded parent edge (#485). */
|
|
45
|
+
runProvenance: RunProvenanceStore;
|
|
46
|
+
queuedMessage: QueuedMessageStore;
|
|
47
|
+
transcriber: Transcriber;
|
|
48
|
+
/** In-process lifecycle event bus (Epic T) — commit sites emit lifecycle events. */
|
|
49
|
+
events: PaddockEventBus;
|
|
50
|
+
/** Unified trigger registry (Epic T / T1) — the sole trigger CRUD surface. */
|
|
51
|
+
triggers: TriggerService;
|
|
52
|
+
/** Tear down the fleet + close the server (no process.exit, for tests). */
|
|
53
|
+
close: () => Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
export interface BuildAppOptions {
|
|
56
|
+
/** Override the resolved config (tests pass a temp-dir config). */
|
|
57
|
+
config?: PaddockConfig;
|
|
58
|
+
/** Skip serving the built web SPA even if a dist exists (API-only). */
|
|
59
|
+
serveStatic?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Construct and fully register the paddock app. Does NOT listen on a port and
|
|
63
|
+
* installs NO signal handlers — the caller (index.ts in prod, tests in CI)
|
|
64
|
+
* owns the lifecycle. The herdctl fleet is initialized + started; a failure
|
|
65
|
+
* there is logged and swallowed (project CRUD still works), matching prod.
|
|
66
|
+
*/
|
|
67
|
+
export declare function buildApp(opts?: BuildAppOptions): Promise<BuiltApp>;
|