@edspencer/paddock 0.0.1 → 0.57.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 +538 -0
- package/packages/server/dist/config.js +625 -0
- package/packages/server/dist/curation-config.d.ts +55 -0
- package/packages/server/dist/curation-config.js +64 -0
- package/packages/server/dist/environment-prompt.d.ts +52 -0
- package/packages/server/dist/environment-prompt.js +61 -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 +623 -0
- package/packages/server/dist/herdctl.js +1279 -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 +143 -0
- package/packages/server/dist/instance-config.js +385 -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-Bypd_quL.js +2 -0
- package/packages/web/dist/assets/MarkdownRenderer-Co8w9iuX.js +30 -0
- package/packages/web/dist/assets/ProjectRedirect-DXrA24Cy.js +2 -0
- package/packages/web/dist/assets/ProjectView-B9opP7Sv.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-D2cwb2jY.js +2 -0
- package/packages/web/dist/assets/arc-B--FIRB8.js +2 -0
- package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-B6dRWZZ6.js +37 -0
- package/packages/web/dist/assets/bash-I8pq0VWm.js +2 -0
- package/packages/web/dist/assets/blockDiagram-GPEHLZMM-DLxFAvvf.js +133 -0
- package/packages/web/dist/assets/c-D2F1sQUL.js +2 -0
- package/packages/web/dist/assets/c4Diagram-AAUBKEIU-CNee5ks4.js +11 -0
- package/packages/web/dist/assets/channel-Bw0A52CB.js +2 -0
- package/packages/web/dist/assets/chunk-2J33WTMH-DObw03Xl.js +2 -0
- package/packages/web/dist/assets/chunk-4BX2VUAB-CH2SR6nN.js +2 -0
- package/packages/web/dist/assets/chunk-55IACEB6-WlVpegT3.js +2 -0
- package/packages/web/dist/assets/chunk-727SXJPM-g8GT9Rgd.js +207 -0
- package/packages/web/dist/assets/chunk-AQP2D5EJ-DbN4PsDF.js +232 -0
- package/packages/web/dist/assets/chunk-FMBD7UC4-DSN3BjFG.js +16 -0
- package/packages/web/dist/assets/chunk-ND2GUHAM-D9R4le0z.js +2 -0
- package/packages/web/dist/assets/chunk-QZHKN3VN-D4YgRaKH.js +2 -0
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-itf_kt-q.js +2 -0
- package/packages/web/dist/assets/core-CysJ2gLj.js +4 -0
- package/packages/web/dist/assets/cose-bilkent-S5V4N54A-D1PB0KNu.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-C6Uuq-dq.js +5 -0
- package/packages/web/dist/assets/defaultLocale-DX6XiGOO.js +2 -0
- package/packages/web/dist/assets/diagram-2AECGRRQ-C9JvQ425.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-Cu6pdh2d.js +11 -0
- package/packages/web/dist/assets/diagram-KO2AKTUF-PmXcUQhY.js +4 -0
- package/packages/web/dist/assets/diagram-LMA3HP47-9ww6_9Iq.js +25 -0
- package/packages/web/dist/assets/diagram-OG6HWLK6-DWQVjv_n.js +25 -0
- package/packages/web/dist/assets/erDiagram-TEJ5UH35-6F7xTuff.js +86 -0
- package/packages/web/dist/assets/flowDiagram-I6XJVG4X-SCiGu7m6.js +163 -0
- package/packages/web/dist/assets/format-yf7Vf9kR.js +2 -0
- package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BNpPwxkL.js +293 -0
- package/packages/web/dist/assets/gitGraphDiagram-PVQCEYII-C0gyAJjW.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-B-YuzkF6.js +71 -0
- package/packages/web/dist/assets/index-CvGiQddO.css +1 -0
- package/packages/web/dist/assets/infoDiagram-5YYISTIA-CJ2CFPfy.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-Cs0D32Ry.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-Bc9fuI2i.js +140 -0
- package/packages/web/dist/assets/json-DIYVocXf.js +2 -0
- package/packages/web/dist/assets/kanban-definition-UN3LZRKU-D_tC7QfC.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-BzGHkF9K.js +2 -0
- package/packages/web/dist/assets/mermaid.core-DlbsYzE-.js +310 -0
- package/packages/web/dist/assets/mindmap-definition-RKZ34NQL-Bxeh-ToD.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-CDmilo8S.js +31 -0
- package/packages/web/dist/assets/python-kSlZHhJb.js +2 -0
- package/packages/web/dist/assets/quadrantDiagram-W4KKPZXB-D14-JyE9.js +8 -0
- package/packages/web/dist/assets/queued-LamEU0GH.js +2 -0
- package/packages/web/dist/assets/requirementDiagram-4Y6WPE33-CJ1PUGu-.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-BkUK73W0.js +41 -0
- package/packages/web/dist/assets/scss-DZLA7YXz.js +2 -0
- package/packages/web/dist/assets/sequenceDiagram-3UESZ5HK-sbcvsuLp.js +163 -0
- package/packages/web/dist/assets/sql-BC88IN-V.js +2 -0
- package/packages/web/dist/assets/stateDiagram-AJRCARHV-DV-7BdHK.js +2 -0
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BlkGdXKW.js +2 -0
- package/packages/web/dist/assets/swift-Dn8kKO_1.js +2 -0
- package/packages/web/dist/assets/timeline-definition-PNZ67QCA-C5su21G_.js +121 -0
- package/packages/web/dist/assets/typescript-C2FFdlUC.js +2 -0
- package/packages/web/dist/assets/vennDiagram-CIIHVFJN-JNkIJAaQ.js +35 -0
- package/packages/web/dist/assets/wardley-L42UT6IY-qcz6t2vR.js +174 -0
- package/packages/web/dist/assets/wardleyDiagram-YWT4CUSO-CMrOIZ-c.js +79 -0
- package/packages/web/dist/assets/xml-BXBhIUeX.js +2 -0
- package/packages/web/dist/assets/xychartDiagram-2RQKCTM6-C9q6Oy9u.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
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot importer for native Claude Code chats (#588) — the headless twin of
|
|
3
|
+
* `POST …/adopt-chats`.
|
|
4
|
+
*
|
|
5
|
+
* ## Why it lives here and not in `scripts/`
|
|
6
|
+
*
|
|
7
|
+
* It needs `loadPaddockConfig`, `ProjectStore` and `HerdctlService`, which are
|
|
8
|
+
* TypeScript modules in this package. `scripts/` is pinned to CommonJS by its
|
|
9
|
+
* own `package.json` (for the extensionless `pm` wrapper) and holds only plain
|
|
10
|
+
* `.mjs`, so a script there could not import them without a build step or a
|
|
11
|
+
* duplicated config parser — and a duplicated parser is exactly how the CLI and
|
|
12
|
+
* the server would come to disagree about which data dir or Claude home they are
|
|
13
|
+
* working on. Here it inherits this package's `"type": "module"`, its tsconfig
|
|
14
|
+
* and its `tsx`, and runs the SAME code the server runs.
|
|
15
|
+
*
|
|
16
|
+
* ## Deployment note
|
|
17
|
+
*
|
|
18
|
+
* The import needs READ access to the user's transcripts. In a container that
|
|
19
|
+
* usually means running this on the HOST against the data dir (`--data-dir`),
|
|
20
|
+
* or mounting `~/.claude` in and pointing `CLAUDE_HOME` at the mount. Detection
|
|
21
|
+
* silently finds nothing if the transcripts are not visible — which is why
|
|
22
|
+
* `--dry-run` prints the sources it found rather than only a count.
|
|
23
|
+
*
|
|
24
|
+
* ## Usage
|
|
25
|
+
*
|
|
26
|
+
* npm run import-chats -w @paddock/server -- --project <slug> [options]
|
|
27
|
+
*
|
|
28
|
+
* --project <slug> Workspace to import into. Required. Use "" (or --root)
|
|
29
|
+
* for the ROOT workspace, whose key IS the empty string.
|
|
30
|
+
* --root Sugar for `--project ""`.
|
|
31
|
+
* --from <dir> Import only this source working directory. Default: every
|
|
32
|
+
* source detection offers.
|
|
33
|
+
* --move Move transcripts instead of copying them. DESTRUCTIVE:
|
|
34
|
+
* the sessions disappear from the user's own history.
|
|
35
|
+
* --dry-run Report what would happen and write nothing at all.
|
|
36
|
+
* --data-dir <dir> Paddock data dir (same meaning as PADDOCK_DATA_DIR).
|
|
37
|
+
* Applied to the environment BEFORE the config is read.
|
|
38
|
+
* --json Emit the raw result as JSON instead of prose.
|
|
39
|
+
*/
|
|
40
|
+
import { loadPaddockConfig } from "../config.js";
|
|
41
|
+
import { ProjectStore, ROOT_KEY } from "../projects.js";
|
|
42
|
+
import { HerdctlService } from "../herdctl.js";
|
|
43
|
+
import { RunProvenanceStore, ADOPTED_ROOT } from "../run-provenance.js";
|
|
44
|
+
/**
|
|
45
|
+
* Parse argv. Hand-rolled because this is the repo's FIRST script taking flags
|
|
46
|
+
* (`pm` parses its own, in CommonJS) — there is no convention to follow and no
|
|
47
|
+
* argument parser in the dependency tree, and adding one for six flags would be
|
|
48
|
+
* the larger change.
|
|
49
|
+
*
|
|
50
|
+
* `--project ""` must survive: the root workspace's key is the empty string, so
|
|
51
|
+
* a "missing value" check written as truthiness would reject the one workspace
|
|
52
|
+
* that always exists.
|
|
53
|
+
*/
|
|
54
|
+
export function parseArgs(argv) {
|
|
55
|
+
const args = { move: false, dryRun: false, json: false, help: false };
|
|
56
|
+
for (let i = 0; i < argv.length; i++) {
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
const value = () => {
|
|
59
|
+
const next = argv[i + 1];
|
|
60
|
+
if (next === undefined || (next.startsWith("--") && next.length > 2)) {
|
|
61
|
+
throw new Error(`${arg} needs a value`);
|
|
62
|
+
}
|
|
63
|
+
i++;
|
|
64
|
+
return next;
|
|
65
|
+
};
|
|
66
|
+
switch (arg) {
|
|
67
|
+
case "--project":
|
|
68
|
+
args.project = value();
|
|
69
|
+
break;
|
|
70
|
+
case "--root":
|
|
71
|
+
args.project = ROOT_KEY;
|
|
72
|
+
break;
|
|
73
|
+
case "--from":
|
|
74
|
+
args.from = value();
|
|
75
|
+
break;
|
|
76
|
+
case "--move":
|
|
77
|
+
args.move = true;
|
|
78
|
+
break;
|
|
79
|
+
case "--dry-run":
|
|
80
|
+
args.dryRun = true;
|
|
81
|
+
break;
|
|
82
|
+
case "--data-dir":
|
|
83
|
+
args.dataDir = value();
|
|
84
|
+
break;
|
|
85
|
+
case "--json":
|
|
86
|
+
args.json = true;
|
|
87
|
+
break;
|
|
88
|
+
case "--help":
|
|
89
|
+
case "-h":
|
|
90
|
+
args.help = true;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return args;
|
|
97
|
+
}
|
|
98
|
+
const USAGE = `Import existing Claude Code CLI chats into a Paddock workspace.
|
|
99
|
+
|
|
100
|
+
npm run import-chats -w @paddock/server -- --project <slug> [options]
|
|
101
|
+
|
|
102
|
+
--project <slug> Workspace to import into (required). "" = the root workspace.
|
|
103
|
+
--root Sugar for --project ""
|
|
104
|
+
--from <dir> Import only this source working directory
|
|
105
|
+
--move Move transcripts instead of copying (DESTRUCTIVE)
|
|
106
|
+
--dry-run Report what would happen; write nothing
|
|
107
|
+
--data-dir <dir> Paddock data dir (same as PADDOCK_DATA_DIR)
|
|
108
|
+
--json Emit raw JSON
|
|
109
|
+
`;
|
|
110
|
+
async function main() {
|
|
111
|
+
const args = parseArgs(process.argv.slice(2));
|
|
112
|
+
if (args.help) {
|
|
113
|
+
process.stdout.write(USAGE);
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
// `=== undefined`, not falsiness: "" is the root workspace, a real target.
|
|
117
|
+
if (args.project === undefined) {
|
|
118
|
+
process.stderr.write("--project is required (use --root for the root workspace)\n\n" + USAGE);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
// Must land in the environment BEFORE the config is read — every other path
|
|
122
|
+
// (projects root, state dir, herdctl config) is derived from it.
|
|
123
|
+
if (args.dataDir !== undefined)
|
|
124
|
+
process.env.PADDOCK_DATA_DIR = args.dataDir;
|
|
125
|
+
const cfg = loadPaddockConfig();
|
|
126
|
+
const projects = new ProjectStore(cfg.projectsRoot);
|
|
127
|
+
const herdctl = new HerdctlService(cfg);
|
|
128
|
+
const project = await projects.get(args.project);
|
|
129
|
+
// Register the keepers so the agent this imports INTO exists and its `.chats/`
|
|
130
|
+
// symlink is in place. `init` is the same call the server makes at boot; the
|
|
131
|
+
// scheduler is never started, so nothing fires while we run.
|
|
132
|
+
const rootWorkspace = await projects.get(ROOT_KEY);
|
|
133
|
+
await herdctl.init([...(await projects.list()), rootWorkspace]);
|
|
134
|
+
try {
|
|
135
|
+
const summary = await herdctl.listAdoptable(project);
|
|
136
|
+
if (summary.count === 0 && summary.filtered.length === 0) {
|
|
137
|
+
process.stdout.write(args.json
|
|
138
|
+
? JSON.stringify({ adopted: [], skipped: [] }) + "\n"
|
|
139
|
+
: `Nothing to import for "${project.slug || "(root)"}".\n` +
|
|
140
|
+
`Working directory: ${project.workingDir}\n` +
|
|
141
|
+
`Claude home: ${cfg.claudeHome}\n` +
|
|
142
|
+
`If you expected chats here, check that this process can READ the transcripts\n` +
|
|
143
|
+
`(a container only sees what is mounted) and that CLAUDE_HOME points at them.\n`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
const result = await herdctl.adoptChats(project, {
|
|
147
|
+
sourceCwd: args.from,
|
|
148
|
+
mode: args.move ? "move" : "copy",
|
|
149
|
+
dryRun: args.dryRun,
|
|
150
|
+
});
|
|
151
|
+
// Provenance is a server-side sidecar, so the CLI must stamp it too or a
|
|
152
|
+
// chat imported headlessly would badge differently from one imported in the
|
|
153
|
+
// UI. Skipped under --dry-run: it is a write.
|
|
154
|
+
if (!args.dryRun) {
|
|
155
|
+
const runProvenance = new RunProvenanceStore(cfg.dataDir);
|
|
156
|
+
for (const sessionId of result.adopted) {
|
|
157
|
+
await runProvenance.stampIfAbsent(sessionId, ADOPTED_ROOT).catch(() => undefined);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (args.json) {
|
|
161
|
+
process.stdout.write(JSON.stringify({ ...result, sources: summary.sources }, null, 2) + "\n");
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
const verb = args.dryRun ? "Would import" : "Imported";
|
|
165
|
+
const lines = [];
|
|
166
|
+
lines.push(`${verb} ${result.adopted.length} chat(s) into "${project.slug || "(root)"}".`);
|
|
167
|
+
for (const source of summary.sources) {
|
|
168
|
+
lines.push(` from ${source.sourceCwd} (${source.sessionIds.length} offered)`);
|
|
169
|
+
}
|
|
170
|
+
if (summary.filtered.length > 0) {
|
|
171
|
+
lines.push(` withheld as noise: ${summary.filtered.length}`);
|
|
172
|
+
for (const f of summary.filtered)
|
|
173
|
+
lines.push(` ${f.sessionId} ${f.reason}`);
|
|
174
|
+
}
|
|
175
|
+
if (result.skipped.length > 0) {
|
|
176
|
+
lines.push(` skipped: ${result.skipped.length}`);
|
|
177
|
+
for (const s of result.skipped)
|
|
178
|
+
lines.push(` ${s.sessionId} ${s.reason}`);
|
|
179
|
+
}
|
|
180
|
+
if (args.dryRun)
|
|
181
|
+
lines.push("Dry run — nothing was written.");
|
|
182
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await herdctl.stop().catch(() => undefined);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Only run when invoked directly, so the arg parser stays unit-testable.
|
|
190
|
+
if (process.argv[1] !== undefined && import.meta.url === `file://${process.argv[1]}`) {
|
|
191
|
+
main()
|
|
192
|
+
.then((code) => process.exit(code))
|
|
193
|
+
.catch((err) => {
|
|
194
|
+
process.stderr.write(`${err.message ?? err}\n`);
|
|
195
|
+
process.exit(1);
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `paddock` — the npx / global-install entrypoint (#TBD).
|
|
4
|
+
*
|
|
5
|
+
* PROTOTYPE. This is the user-facing front door for `npx @edspencer/paddock`:
|
|
6
|
+
* someone who has never seen this repo runs one command and gets a working
|
|
7
|
+
* instance on localhost. Everything here is preflight and defaults — the server
|
|
8
|
+
* itself is unchanged, and `start()` is the same lifecycle `node dist/index.js`
|
|
9
|
+
* uses.
|
|
10
|
+
*
|
|
11
|
+
* The design constraint that shapes this file: the server is already
|
|
12
|
+
* *tolerant* — every setting has a default, an empty data dir self-initializes,
|
|
13
|
+
* and auth defaults to `none` on a loopback bind. So a bare `start()` would in
|
|
14
|
+
* fact boot. What it would NOT do is tell a first-time user why their chats
|
|
15
|
+
* fail, or where their data went. Hence:
|
|
16
|
+
*
|
|
17
|
+
* 1. Node version — checked explicitly, because the `engines` field only
|
|
18
|
+
* warns and the eventual failure is an opaque syntax error.
|
|
19
|
+
* 2. Data dir — defaults to `~/.paddock`, NOT the server's `./data`, which is
|
|
20
|
+
* relative to cwd and would litter whatever directory you happened to run
|
|
21
|
+
* npx from (and silently give you a different instance next time).
|
|
22
|
+
* 3. `claude` on PATH — prepended from our own node_modules/.bin. Chats do not
|
|
23
|
+
* need it (the SDK runtime resolves its own bundled binary), but the
|
|
24
|
+
* sweeper and triggers shell out to `claude` by name.
|
|
25
|
+
* 4. Credentials — a warning, never a hard failure: we cannot reliably detect
|
|
26
|
+
* a macOS Keychain login, so refusing to start would lock out valid users.
|
|
27
|
+
*/
|
|
28
|
+
import fs from "node:fs";
|
|
29
|
+
import os from "node:os";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
const MIN_NODE_MAJOR = 22;
|
|
33
|
+
/** This module's own directory: `<…>/packages/server/dist/cli`. */
|
|
34
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
/**
|
|
36
|
+
* Walk up from this module looking for `rel`, returning the containing dir.
|
|
37
|
+
*
|
|
38
|
+
* Two layouts have to work and they nest differently:
|
|
39
|
+
*
|
|
40
|
+
* repo `<repo>/packages/server/dist/cli/paddock.js`
|
|
41
|
+
* deps at `<repo>/packages/server/node_modules` AND `<repo>/node_modules`
|
|
42
|
+
* published `<pkg>/packages/server/dist/cli/paddock.js`
|
|
43
|
+
* deps at `<pkg>/node_modules` only — two levels FURTHER up
|
|
44
|
+
*
|
|
45
|
+
* A fixed `../..` hop is right for the repo and wrong for the published tarball,
|
|
46
|
+
* where it lands on `packages/server` and finds no `node_modules` at all. Walking
|
|
47
|
+
* up is correct for both without branching on which one we are in.
|
|
48
|
+
*/
|
|
49
|
+
function findUp(rel) {
|
|
50
|
+
let dir = moduleDir;
|
|
51
|
+
for (;;) {
|
|
52
|
+
if (fs.existsSync(path.join(dir, rel)))
|
|
53
|
+
return dir;
|
|
54
|
+
const parent = path.dirname(dir);
|
|
55
|
+
if (parent === dir)
|
|
56
|
+
return undefined;
|
|
57
|
+
dir = parent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Nearest ancestor holding a `package.json` — `packages/server` in both layouts. */
|
|
61
|
+
const packageRoot = findUp("package.json") ?? path.resolve(moduleDir, "../..");
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const opts = { help: false, version: false };
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const arg = argv[i];
|
|
66
|
+
const next = () => {
|
|
67
|
+
const v = argv[++i];
|
|
68
|
+
if (v === undefined)
|
|
69
|
+
fail(`${arg} needs a value`);
|
|
70
|
+
return v;
|
|
71
|
+
};
|
|
72
|
+
switch (arg) {
|
|
73
|
+
case "-p":
|
|
74
|
+
case "--port":
|
|
75
|
+
opts.port = next();
|
|
76
|
+
break;
|
|
77
|
+
case "--host":
|
|
78
|
+
opts.host = next();
|
|
79
|
+
break;
|
|
80
|
+
case "-d":
|
|
81
|
+
case "--data-dir":
|
|
82
|
+
opts.dataDir = next();
|
|
83
|
+
break;
|
|
84
|
+
case "-h":
|
|
85
|
+
case "--help":
|
|
86
|
+
opts.help = true;
|
|
87
|
+
break;
|
|
88
|
+
case "-v":
|
|
89
|
+
case "--version":
|
|
90
|
+
opts.version = true;
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
fail(`unknown option: ${arg}\nRun \`paddock --help\` for usage.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return opts;
|
|
97
|
+
}
|
|
98
|
+
function fail(message) {
|
|
99
|
+
console.error(`paddock: ${message}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
function readVersion() {
|
|
103
|
+
try {
|
|
104
|
+
const raw = fs.readFileSync(path.join(packageRoot, "package.json"), "utf8");
|
|
105
|
+
return JSON.parse(raw).version ?? "unknown";
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return "unknown";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const USAGE = `paddock — run a Paddock instance locally
|
|
112
|
+
|
|
113
|
+
Usage
|
|
114
|
+
npx @edspencer/paddock [options]
|
|
115
|
+
|
|
116
|
+
Options
|
|
117
|
+
-p, --port <port> HTTP/WS port (default 4000, or $PORT)
|
|
118
|
+
--host <host> Bind address (default 127.0.0.1)
|
|
119
|
+
-d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)
|
|
120
|
+
-v, --version Print the Paddock version and exit
|
|
121
|
+
-h, --help Show this help
|
|
122
|
+
|
|
123
|
+
Credentials
|
|
124
|
+
Paddock drives Claude Code, so it needs Claude credentials in the
|
|
125
|
+
environment: CLAUDE_CODE_OAUTH_TOKEN (Max/Pro) or ANTHROPIC_API_KEY (API
|
|
126
|
+
billing). If you already use Claude Code on this machine, an existing login
|
|
127
|
+
under ~/.claude is picked up automatically. Otherwise run \`claude setup-token\`.
|
|
128
|
+
|
|
129
|
+
Notes
|
|
130
|
+
Binds loopback with authentication disabled, which is safe for a laptop. To
|
|
131
|
+
expose it on a network, set PADDOCK_AUTH_MODE first — Paddock refuses to bind
|
|
132
|
+
a routable interface wide open. See AUTH.md.
|
|
133
|
+
|
|
134
|
+
Docs: https://github.com/edspencer/paddock`;
|
|
135
|
+
/** Node's own version gate. `engines` only warns by default, and the failure it
|
|
136
|
+
* eventually produces is an unexplained syntax error deep in a dependency. */
|
|
137
|
+
function checkNodeVersion() {
|
|
138
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
139
|
+
if (Number.isFinite(major) && major < MIN_NODE_MAJOR) {
|
|
140
|
+
fail(`Node ${process.versions.node} is too old — Paddock needs Node ${MIN_NODE_MAJOR}+.\n` +
|
|
141
|
+
`Upgrade Node, then re-run. (nodejs.org, or \`nvm install ${MIN_NODE_MAJOR}\`)`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Put our bundled `claude` on PATH for child processes.
|
|
146
|
+
*
|
|
147
|
+
* Chat turns do NOT need this: they run herdctl's SDK runtime, which resolves
|
|
148
|
+
* the binary from the Claude Agent SDK's own platform package via
|
|
149
|
+
* `require.resolve` and never consults PATH. But the post-turn sweeper and all
|
|
150
|
+
* triggers are one-shot `trigger()` calls on the CLI runtime, which does
|
|
151
|
+
* `execa("claude", …)` — by name, without execa's `preferLocal`, so a local
|
|
152
|
+
* install is invisible to it unless PATH says otherwise.
|
|
153
|
+
*
|
|
154
|
+
* Without this, chats work and every sweep/trigger fails (logged, non-fatal).
|
|
155
|
+
*/
|
|
156
|
+
function addBundledBinsToPath() {
|
|
157
|
+
const owner = findUp(path.join("node_modules", ".bin"));
|
|
158
|
+
if (owner === undefined)
|
|
159
|
+
return;
|
|
160
|
+
const binDir = path.join(owner, "node_modules", ".bin");
|
|
161
|
+
const sep = process.platform === "win32" ? ";" : ":";
|
|
162
|
+
const current = process.env.PATH ?? "";
|
|
163
|
+
if (current.split(sep).includes(binDir))
|
|
164
|
+
return;
|
|
165
|
+
process.env.PATH = current ? `${binDir}${sep}${current}` : binDir;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Best-effort credential check — WARNS, never blocks.
|
|
169
|
+
*
|
|
170
|
+
* Claude Code stores credentials differently per platform (a file under
|
|
171
|
+
* ~/.claude on Linux, the Keychain on macOS), so absence of a file does not
|
|
172
|
+
* prove absence of a login. Refusing to start on a false negative would be
|
|
173
|
+
* worse than a chat that fails with the runtime's own error, so this only
|
|
174
|
+
* prints guidance.
|
|
175
|
+
*/
|
|
176
|
+
function warnIfNoCredentials(claudeHome) {
|
|
177
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN || process.env.ANTHROPIC_API_KEY)
|
|
178
|
+
return;
|
|
179
|
+
const looksLoggedIn = fs.existsSync(path.join(claudeHome, ".credentials.json")) ||
|
|
180
|
+
fs.existsSync(path.join(os.homedir(), ".claude.json"));
|
|
181
|
+
if (looksLoggedIn)
|
|
182
|
+
return;
|
|
183
|
+
console.warn([
|
|
184
|
+
"",
|
|
185
|
+
" ⚠ No Claude credentials found.",
|
|
186
|
+
"",
|
|
187
|
+
" Paddock will start, but chats will fail until it has credentials.",
|
|
188
|
+
" Fix with either:",
|
|
189
|
+
"",
|
|
190
|
+
" claude setup-token # Claude Max/Pro",
|
|
191
|
+
" export ANTHROPIC_API_KEY=sk-ant-… # API billing",
|
|
192
|
+
"",
|
|
193
|
+
].join("\n"));
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Report where the data lives, the first time we create it.
|
|
197
|
+
*
|
|
198
|
+
* `npx` is stateless enough that people reasonably assume the whole thing is
|
|
199
|
+
* ephemeral. It is not — projects, chat transcripts, and settings all persist
|
|
200
|
+
* here across runs, so say so once, at the moment the directory appears.
|
|
201
|
+
*/
|
|
202
|
+
function noteFirstRun(dataDir) {
|
|
203
|
+
if (fs.existsSync(dataDir))
|
|
204
|
+
return;
|
|
205
|
+
console.log([
|
|
206
|
+
"",
|
|
207
|
+
` Welcome to Paddock. Creating a new instance in ${dataDir}`,
|
|
208
|
+
" Projects, chats and settings persist there between runs.",
|
|
209
|
+
"",
|
|
210
|
+
].join("\n"));
|
|
211
|
+
}
|
|
212
|
+
async function main() {
|
|
213
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
214
|
+
if (opts.help) {
|
|
215
|
+
console.log(USAGE);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (opts.version) {
|
|
219
|
+
console.log(readVersion());
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
checkNodeVersion();
|
|
223
|
+
addBundledBinsToPath();
|
|
224
|
+
// Apply CLI defaults as env vars. This is the whole integration surface: the
|
|
225
|
+
// server resolves config inside `buildApp()`, so anything set before the
|
|
226
|
+
// dynamic import below is picked up with no special-casing in config.ts.
|
|
227
|
+
// Precedence is explicit flag > existing env > our default.
|
|
228
|
+
const dataDir = path.resolve(opts.dataDir ?? process.env.PADDOCK_DATA_DIR ?? path.join(os.homedir(), ".paddock"));
|
|
229
|
+
noteFirstRun(dataDir);
|
|
230
|
+
process.env.PADDOCK_DATA_DIR = dataDir;
|
|
231
|
+
if (opts.port)
|
|
232
|
+
process.env.PORT = opts.port;
|
|
233
|
+
if (opts.host)
|
|
234
|
+
process.env.HOST = opts.host;
|
|
235
|
+
warnIfNoCredentials(process.env.CLAUDE_HOME ?? path.join(os.homedir(), ".claude"));
|
|
236
|
+
const port = process.env.PORT ?? "4000";
|
|
237
|
+
const host = process.env.HOST ?? "127.0.0.1";
|
|
238
|
+
// Imported dynamically, AFTER the env above is set: a static import would
|
|
239
|
+
// pull in app.ts -> config.ts and resolve config against the wrong data dir.
|
|
240
|
+
const { start } = await import("../start.js");
|
|
241
|
+
await start();
|
|
242
|
+
console.log(`\n Paddock is running at http://${host}:${port}\n Press Ctrl-C to stop.\n`);
|
|
243
|
+
}
|
|
244
|
+
main().catch((err) => {
|
|
245
|
+
console.error("fatal:", err);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* concurrency — bounded fan-out for the routes that touch one file per chat.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Several endpoints map over every session in a project and open a transcript
|
|
7
|
+
* per session. Written as `Promise.all(sessions.map(...))` that starts EVERY
|
|
8
|
+
* read at once: on a real instance that is 1,515 concurrent read streams, each
|
|
9
|
+
* buffering a transcript that averages ~350 KB. The work is identical either
|
|
10
|
+
* way — the reads just all land in memory at the same moment instead of a few
|
|
11
|
+
* at a time.
|
|
12
|
+
*
|
|
13
|
+
* Measured on that corpus (`GET /chats/usage`, issue #544):
|
|
14
|
+
*
|
|
15
|
+
* | concurrency | wall time | peak RSS |
|
|
16
|
+
* |---|---|---|
|
|
17
|
+
* | unbounded (1,515) | 4,510 ms | 706 MB |
|
|
18
|
+
* | 64 | 4,477 ms | 280 MB |
|
|
19
|
+
* | 16 | 4,329 ms | 178 MB |
|
|
20
|
+
*
|
|
21
|
+
* Bounding it is not a latency trade: the bounded runs were marginally FASTER,
|
|
22
|
+
* because 1,515 interleaved streams cost more in scheduling and GC pressure than
|
|
23
|
+
* they buy in overlap. The disk is not the bottleneck at this size — the parse
|
|
24
|
+
* is, and the parse is single-threaded regardless.
|
|
25
|
+
*
|
|
26
|
+
* ## Why a local copy
|
|
27
|
+
*
|
|
28
|
+
* `@herdctl/core` has this function twice (`state/job-index.ts` and
|
|
29
|
+
* `state/utils/concurrency.ts`) and exports NEITHER from the package root, so it
|
|
30
|
+
* is not reachable from here today. Consolidating and exporting the survivor is
|
|
31
|
+
* filed upstream as **herdctl#421**; when that lands, this module should be
|
|
32
|
+
* deleted and the import repointed at core. The clamping below matches the
|
|
33
|
+
* `state/utils` variant deliberately — the `job-index` copy silently spawns zero
|
|
34
|
+
* workers (and so resolves having done nothing) when `limit <= 0`.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Default bound for per-chat file fan-out.
|
|
38
|
+
*
|
|
39
|
+
* 16 measured *faster* than unbounded on a 1,515-chat project while cutting peak
|
|
40
|
+
* RSS ~4× (706 MB → 178 MB), so there is headroom on both sides of this number;
|
|
41
|
+
* it is not a knife-edge. Kept as a named constant because the next caller — the
|
|
42
|
+
* boot-warm sweep — must use the same bound to get the same guarantee.
|
|
43
|
+
*/
|
|
44
|
+
export declare const FILE_FANOUT_CONCURRENCY = 16;
|
|
45
|
+
/**
|
|
46
|
+
* Map over `items` with at most `limit` calls to `fn` in flight, preserving
|
|
47
|
+
* input order in the result.
|
|
48
|
+
*
|
|
49
|
+
* Order-preserving is load-bearing: callers index the result positionally
|
|
50
|
+
* against the input array, so this must behave exactly like `Promise.all(
|
|
51
|
+
* items.map(fn))` in everything except how many calls are live at once.
|
|
52
|
+
*
|
|
53
|
+
* `limit` is clamped to at least 1, so a mis-computed or zero bound degrades to
|
|
54
|
+
* serial execution rather than silently completing without running `fn` at all.
|
|
55
|
+
* Rejections propagate as they would from `Promise.all`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* concurrency — bounded fan-out for the routes that touch one file per chat.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Several endpoints map over every session in a project and open a transcript
|
|
7
|
+
* per session. Written as `Promise.all(sessions.map(...))` that starts EVERY
|
|
8
|
+
* read at once: on a real instance that is 1,515 concurrent read streams, each
|
|
9
|
+
* buffering a transcript that averages ~350 KB. The work is identical either
|
|
10
|
+
* way — the reads just all land in memory at the same moment instead of a few
|
|
11
|
+
* at a time.
|
|
12
|
+
*
|
|
13
|
+
* Measured on that corpus (`GET /chats/usage`, issue #544):
|
|
14
|
+
*
|
|
15
|
+
* | concurrency | wall time | peak RSS |
|
|
16
|
+
* |---|---|---|
|
|
17
|
+
* | unbounded (1,515) | 4,510 ms | 706 MB |
|
|
18
|
+
* | 64 | 4,477 ms | 280 MB |
|
|
19
|
+
* | 16 | 4,329 ms | 178 MB |
|
|
20
|
+
*
|
|
21
|
+
* Bounding it is not a latency trade: the bounded runs were marginally FASTER,
|
|
22
|
+
* because 1,515 interleaved streams cost more in scheduling and GC pressure than
|
|
23
|
+
* they buy in overlap. The disk is not the bottleneck at this size — the parse
|
|
24
|
+
* is, and the parse is single-threaded regardless.
|
|
25
|
+
*
|
|
26
|
+
* ## Why a local copy
|
|
27
|
+
*
|
|
28
|
+
* `@herdctl/core` has this function twice (`state/job-index.ts` and
|
|
29
|
+
* `state/utils/concurrency.ts`) and exports NEITHER from the package root, so it
|
|
30
|
+
* is not reachable from here today. Consolidating and exporting the survivor is
|
|
31
|
+
* filed upstream as **herdctl#421**; when that lands, this module should be
|
|
32
|
+
* deleted and the import repointed at core. The clamping below matches the
|
|
33
|
+
* `state/utils` variant deliberately — the `job-index` copy silently spawns zero
|
|
34
|
+
* workers (and so resolves having done nothing) when `limit <= 0`.
|
|
35
|
+
*/
|
|
36
|
+
/**
|
|
37
|
+
* Default bound for per-chat file fan-out.
|
|
38
|
+
*
|
|
39
|
+
* 16 measured *faster* than unbounded on a 1,515-chat project while cutting peak
|
|
40
|
+
* RSS ~4× (706 MB → 178 MB), so there is headroom on both sides of this number;
|
|
41
|
+
* it is not a knife-edge. Kept as a named constant because the next caller — the
|
|
42
|
+
* boot-warm sweep — must use the same bound to get the same guarantee.
|
|
43
|
+
*/
|
|
44
|
+
export const FILE_FANOUT_CONCURRENCY = 16;
|
|
45
|
+
/**
|
|
46
|
+
* Map over `items` with at most `limit` calls to `fn` in flight, preserving
|
|
47
|
+
* input order in the result.
|
|
48
|
+
*
|
|
49
|
+
* Order-preserving is load-bearing: callers index the result positionally
|
|
50
|
+
* against the input array, so this must behave exactly like `Promise.all(
|
|
51
|
+
* items.map(fn))` in everything except how many calls are live at once.
|
|
52
|
+
*
|
|
53
|
+
* `limit` is clamped to at least 1, so a mis-computed or zero bound degrades to
|
|
54
|
+
* serial execution rather than silently completing without running `fn` at all.
|
|
55
|
+
* Rejections propagate as they would from `Promise.all`.
|
|
56
|
+
*/
|
|
57
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
58
|
+
const results = new Array(items.length);
|
|
59
|
+
const workers = Math.max(1, Math.min(Math.floor(limit) || 1, items.length));
|
|
60
|
+
let next = 0;
|
|
61
|
+
const worker = async () => {
|
|
62
|
+
for (;;) {
|
|
63
|
+
const index = next++;
|
|
64
|
+
if (index >= items.length)
|
|
65
|
+
return;
|
|
66
|
+
results[index] = await fn(items[index], index);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
await Promise.all(Array.from({ length: workers }, worker));
|
|
70
|
+
return results;
|
|
71
|
+
}
|