@edspencer/paddock 0.66.2 → 0.68.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.
Files changed (122) hide show
  1. package/README.md +24 -30
  2. package/package.json +1 -1
  3. package/packages/server/dist/adoptable.d.ts +57 -0
  4. package/packages/server/dist/adoptable.js +26 -3
  5. package/packages/server/dist/agent-errors.js +1 -1
  6. package/packages/server/dist/claude-home.d.ts +4 -4
  7. package/packages/server/dist/claude-home.js +4 -4
  8. package/packages/server/dist/claude-mcp.d.ts +4 -2
  9. package/packages/server/dist/cli/args.d.ts +41 -2
  10. package/packages/server/dist/cli/args.js +119 -17
  11. package/packages/server/dist/cli/paddock.js +87 -96
  12. package/packages/server/dist/cli/service/backend.d.ts +38 -0
  13. package/packages/server/dist/cli/service/backend.js +19 -0
  14. package/packages/server/dist/cli/service/index.d.ts +28 -0
  15. package/packages/server/dist/cli/service/index.js +179 -0
  16. package/packages/server/dist/cli/service/launchd.d.ts +47 -0
  17. package/packages/server/dist/cli/service/launchd.js +206 -0
  18. package/packages/server/dist/cli/service/spec.d.ts +106 -0
  19. package/packages/server/dist/cli/service/spec.js +124 -0
  20. package/packages/server/dist/cli/service/systemd.d.ts +48 -0
  21. package/packages/server/dist/cli/service/systemd.js +155 -0
  22. package/packages/server/dist/discover.d.ts +170 -0
  23. package/packages/server/dist/discover.js +365 -0
  24. package/packages/server/dist/herdctl.d.ts +25 -2
  25. package/packages/server/dist/herdctl.js +30 -1
  26. package/packages/server/dist/routes/chats.js +14 -2
  27. package/packages/server/dist/routes/discover.d.ts +3 -0
  28. package/packages/server/dist/routes/discover.js +140 -0
  29. package/packages/server/dist/routes.d.ts +1 -0
  30. package/packages/server/dist/routes.js +2 -0
  31. package/packages/server/dist/session-hub.d.ts +10 -0
  32. package/packages/server/dist/session-hub.js +16 -2
  33. package/packages/server/dist/ws-protocol.d.ts +15 -0
  34. package/packages/server/dist/ws.js +1 -0
  35. package/packages/server/package.json +1 -1
  36. package/packages/web/dist/assets/Chip-DIdRqcqU.js +2 -0
  37. package/packages/web/dist/assets/DiscoverPage-BmCWnkjN.js +2 -0
  38. package/packages/web/dist/assets/DiscoverView-DwhtBdN2.js +2 -0
  39. package/packages/web/dist/assets/InstanceConfigPage-D01x1BUg.js +2 -0
  40. package/packages/web/dist/assets/{MarkdownRenderer-DDVO0zhl.js → MarkdownRenderer-BCheNET2.js} +2 -2
  41. package/packages/web/dist/assets/ProjectMenu-BqlUQuxM.js +2 -0
  42. package/packages/web/dist/assets/ProjectRedirect-D8QDxYLG.js +2 -0
  43. package/packages/web/dist/assets/ProjectView-cevSYdcl.js +13 -0
  44. package/packages/web/dist/assets/ProjectsGrid-yd18DXdV.js +2 -0
  45. package/packages/web/dist/assets/RootHome-DZEkOjko.js +2 -0
  46. package/packages/web/dist/assets/Surfaces-DvRztndg.js +2 -0
  47. package/packages/web/dist/assets/{arc-DXqVnx_r.js → arc-DFqBni1E.js} +1 -1
  48. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-FgkfD1Bk.js → architectureDiagram-3BPJPVTR-DXFeq3pL.js} +1 -1
  49. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-WwkIIJsI.js → blockDiagram-GPEHLZMM-DpeYsIwP.js} +1 -1
  50. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BIzkTibx.js → c4Diagram-AAUBKEIU-Bw5r8wjW.js} +1 -1
  51. package/packages/web/dist/assets/channel-DU94S7yB.js +2 -0
  52. package/packages/web/dist/assets/{chunk-2J33WTMH-C4TBH3R6.js → chunk-2J33WTMH-B0SAvGOr.js} +1 -1
  53. package/packages/web/dist/assets/{chunk-4BX2VUAB-DTKJZlRM.js → chunk-4BX2VUAB-BaUGs1hD.js} +1 -1
  54. package/packages/web/dist/assets/{chunk-55IACEB6-BC9_Qc8h.js → chunk-55IACEB6-BDHRluF5.js} +1 -1
  55. package/packages/web/dist/assets/{chunk-727SXJPM-D3gQ4rP3.js → chunk-727SXJPM-BX93DBZO.js} +1 -1
  56. package/packages/web/dist/assets/{chunk-AQP2D5EJ-My2AXBvz.js → chunk-AQP2D5EJ-3RiQGxW2.js} +1 -1
  57. package/packages/web/dist/assets/{chunk-FMBD7UC4-D09zE6Ow.js → chunk-FMBD7UC4-DMkjv9SP.js} +1 -1
  58. package/packages/web/dist/assets/{chunk-ND2GUHAM-CUSgeCLl.js → chunk-ND2GUHAM-CHiQfq4F.js} +1 -1
  59. package/packages/web/dist/assets/{chunk-QZHKN3VN-B6jVPYgy.js → chunk-QZHKN3VN-B1wCSHbM.js} +1 -1
  60. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BcRv1OPp.js +2 -0
  61. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BcRv1OPp.js +2 -0
  62. package/packages/web/dist/assets/{core-DPY0seI-.js → core-C-49RP59.js} +1 -1
  63. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BasCiARv.js → cose-bilkent-S5V4N54A-BIwwgSTZ.js} +1 -1
  64. package/packages/web/dist/assets/{dagre-BM42HDAG-Cimn11Qe.js → dagre-BM42HDAG-BN45kshV.js} +1 -1
  65. package/packages/web/dist/assets/diagram-2AECGRRQ-C5NekbfR.js +44 -0
  66. package/packages/web/dist/assets/diagram-5GNKFQAL-De7wdssd.js +11 -0
  67. package/packages/web/dist/assets/{diagram-KO2AKTUF-dLORrSiu.js → diagram-KO2AKTUF-CgG0UA0n.js} +3 -3
  68. package/packages/web/dist/assets/{diagram-LMA3HP47-iUMeaR-J.js → diagram-LMA3HP47-DC-bDqxV.js} +1 -1
  69. package/packages/web/dist/assets/{diagram-OG6HWLK6-oEH6olHK.js → diagram-OG6HWLK6-XXfHDMhE.js} +1 -1
  70. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-VtQqF9SY.js → erDiagram-TEJ5UH35-CHEd8JiS.js} +1 -1
  71. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-wvF2EkJa.js → flowDiagram-I6XJVG4X-BbuYxd15.js} +1 -1
  72. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-ng4qJ0E1.js → ganttDiagram-6RSMTGT7-BkF77xn5.js} +1 -1
  73. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-BV4aWy8k.js → gitGraphDiagram-PVQCEYII-BdL5zVez.js} +2 -2
  74. package/packages/web/dist/assets/index-C3MdCCzF.js +71 -0
  75. package/packages/web/dist/assets/index-DYccdR9x.css +1 -0
  76. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BpGTxAjU.js → infoDiagram-5YYISTIA-vhTRATYg.js} +1 -1
  77. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-Cj36LyoS.js → ishikawaDiagram-YF4QCWOH-CxjN-7YF.js} +2 -2
  78. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-0vNlg3um.js → journeyDiagram-JHISSGLW-DJ414TaX.js} +1 -1
  79. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-wijkEDaR.js → kanban-definition-UN3LZRKU-gaQeFRvZ.js} +1 -1
  80. package/packages/web/dist/assets/{linear-LqRymMD_.js → linear-CcvdFgxd.js} +1 -1
  81. package/packages/web/dist/assets/{mermaid.core-DPrhSs95.js → mermaid.core-Es6hedDB.js} +5 -5
  82. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-en-uGqgC.js → mindmap-definition-RKZ34NQL-Dk7qf3FR.js} +1 -1
  83. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DKBZ2Vnx.js → pieDiagram-4H26LBE5-CSWXwU6d.js} +3 -3
  84. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB--g-WtxyL.js → quadrantDiagram-W4KKPZXB-CBVdURbr.js} +1 -1
  85. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-Dl4eYxDI.js → requirementDiagram-4Y6WPE33-CbU18IZA.js} +3 -3
  86. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-7lt4gqSw.js → sankeyDiagram-5OEKKPKP-BeAlNgjE.js} +4 -4
  87. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-COgq-dXg.js → sequenceDiagram-3UESZ5HK-CKVKzxTs.js} +1 -1
  88. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CL6XC79S.js → stateDiagram-AJRCARHV-B-aSaO4W.js} +1 -1
  89. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-TptFh32C.js +2 -0
  90. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C5egKeGT.js → timeline-definition-PNZ67QCA-CP9JHEBr.js} +1 -1
  91. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C_UhtC8u.js → vennDiagram-CIIHVFJN-Dbtwwvvd.js} +1 -1
  92. package/packages/web/dist/assets/{wardley-L42UT6IY-Bj9chUqz.js → wardley-L42UT6IY-NonvEhoT.js} +1 -1
  93. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-D7VnZoRp.js → wardleyDiagram-YWT4CUSO-CDnpv9Ws.js} +2 -2
  94. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-VrWLDoi4.js → xychartDiagram-2RQKCTM6-C1i-Wk0H.js} +1 -1
  95. package/packages/web/dist/fonts/alegreya-latin-italic.woff2 +0 -0
  96. package/packages/web/dist/fonts/alegreya-latin.woff2 +0 -0
  97. package/packages/web/dist/fonts/alegreya-sc-latin-500.woff2 +0 -0
  98. package/packages/web/dist/fonts/alegreya-sc-latin-700.woff2 +0 -0
  99. package/packages/web/dist/fonts/courier-prime-latin-400.woff2 +0 -0
  100. package/packages/web/dist/fonts/courier-prime-latin-700.woff2 +0 -0
  101. package/packages/web/dist/fonts/courier-prime-latin-italic.woff2 +0 -0
  102. package/packages/web/dist/fonts/iosevka-terminal-400.woff2 +0 -0
  103. package/packages/web/dist/fonts/iosevka-terminal-700.woff2 +0 -0
  104. package/packages/web/dist/fonts/martian-mono-latin.woff2 +0 -0
  105. package/packages/web/dist/fonts/saira-latin.woff2 +0 -0
  106. package/packages/web/dist/index.html +61 -12
  107. package/packages/web/dist/sw.js +1 -1
  108. package/packages/server/dist/cli/here.d.ts +0 -25
  109. package/packages/server/dist/cli/here.js +0 -104
  110. package/packages/web/dist/assets/InstanceConfigPage-DKzAHOhx.js +0 -2
  111. package/packages/web/dist/assets/ProjectRedirect-WczXTV0_.js +0 -2
  112. package/packages/web/dist/assets/ProjectView-Dd_myWpV.js +0 -13
  113. package/packages/web/dist/assets/ProjectsGrid-BeJj0q6Y.js +0 -2
  114. package/packages/web/dist/assets/channel-DUsOl5_p.js +0 -2
  115. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CIdDKEqH.js +0 -2
  116. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CIdDKEqH.js +0 -2
  117. package/packages/web/dist/assets/diagram-2AECGRRQ-fe9cDHyn.js +0 -44
  118. package/packages/web/dist/assets/diagram-5GNKFQAL-T0tQlfbl.js +0 -11
  119. package/packages/web/dist/assets/format-CvRqOKZU.js +0 -2
  120. package/packages/web/dist/assets/index-DGNSeqL-.js +0 -71
  121. package/packages/web/dist/assets/index-DMRbU1Jl.css +0 -1
  122. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BDlRyphY.js +0 -2
package/README.md CHANGED
@@ -75,51 +75,45 @@ mcpServers:
75
75
  NOTION_TOKEN: env:NOTION_TOKEN
76
76
  ```
77
77
 
78
- ## Open your own project
79
-
80
- ```sh
81
- cd ~/code/some-project # somewhere you've used Claude Code
82
- npx @edspencer/paddock --here
83
- ```
84
-
85
- `--here` opens **that directory** as Paddock's workspace: Claude works in your files,
86
- and any Claude Code sessions you already have for the directory are offered for
87
- import. It's the fastest way to see what Paddock does with real history instead of an
88
- empty instance. Later runs in the same directory resume it — no flag needed.
89
-
90
- The flag is the consent, so here is everything it does:
91
-
92
- - creates **`.paddock/`** for the workspace's state and **`.chats/`** for transcripts
93
- - appends those two entries to **`.gitignore`**
94
-
95
- **Nothing is written into your `~/.claude`** — no file, no symlink. Paddock uses a
96
- Claude home of its own under `.paddock/` and relocates transcripts into `.chats/`.
97
- Sessions found in `~/.claude` are *offered* for import: nothing is moved, copied or
98
- linked until you confirm, and your terminal `claude` keeps working exactly as before.
99
- To share one set of transcripts between Paddock and your terminal instead, set
78
+ ## Open your own projects
79
+
80
+ A brand-new instance is empty, so it opens on **Discover**. That reads your Claude
81
+ Code history, works out which directories on this machine you have actually been
82
+ using `claude` in, and offers them as projects — with conversation counts, last-used
83
+ dates and git remotes, so you can tell them apart.
84
+
85
+ Tick the ones you want and press Import. Each becomes a project pointing at that
86
+ directory, with its conversations copied in as chats you can resume — so instead of
87
+ an empty instance you are looking at your own work. Expand a row first if you would
88
+ rather pick individual conversations than take the lot. Discover stays in the sidebar
89
+ afterwards; it is not only a first-run screen.
90
+
91
+ **Nothing is written into the directories you import.** No `.paddock/`, no `.chats/`,
92
+ no `.gitignore` edit, no `CLAUDE.md`. The project record and the copied transcripts
93
+ both live in the data dir, and the project simply points at the path.
94
+
95
+ **Your `~/.claude` is not touched either.** Transcripts are *copied*, with their
96
+ timestamps preserved — the originals are never moved or deleted, and your terminal
97
+ `claude` keeps working exactly as before. To share **one** set of transcripts between
98
+ Paddock and your terminal rather than keeping a copy, set
100
99
  `claude: { transcripts: host }` in `<data-dir>/paddock.config.yaml`. (Your *login* is
101
100
  already shared, which is why there is nothing to log into — see Credentials above.)
102
101
 
103
- To undo it: `rm -rf .paddock .chats` and drop the two `.gitignore` lines.
104
-
105
- Without `--here`, Paddock never touches the directory you ran it from.
106
-
107
102
  ## Options
108
103
 
109
104
  ```
110
105
  -p, --port <port> HTTP/WS port (default 7233)
111
106
  --host <host> Bind address (default 127.0.0.1)
112
107
  -d, --data-dir <path> Projects + state (default ~/.paddock)
113
- --here Open the CURRENT directory as the workspace
114
108
  -o, --open Open the app in your browser once it is listening
115
109
  --verbose Show the server's own logs (quiet by default)
116
110
  -v, --version Print the version
117
111
  -h, --help Show help
118
112
  ```
119
113
 
120
- Your projects, chats and settings persist between runs in `~/.paddock` — or in
121
- `<dir>/.paddock` when you use `--here`. Either way it's one directory: move it to
122
- move your instance, delete it to start over.
114
+ Your projects, chats and settings persist between runs in `~/.paddock`, or wherever
115
+ `--data-dir` points. It is one directory: move it to move your instance, delete it to
116
+ start over. Where you run `paddock` from has no effect on which instance you get.
123
117
 
124
118
  ## Security
125
119
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.66.2",
3
+ "version": "0.68.0",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -104,6 +104,14 @@ export declare const SLASH_COMMAND_MAX_BYTES = 4096;
104
104
  * it pasted into a test here would go stale silently and take the filter with it.
105
105
  */
106
106
  export declare function isSweeperPrompt(preview: string): boolean;
107
+ /**
108
+ * Classify a candidate as noise, or `null` to keep it.
109
+ *
110
+ * Exported for `discover.ts` (#745), which counts a directory's NON-NOISE
111
+ * sessions to rank it. The count a Discover row shows and the offer the import
112
+ * dialog then makes have to be the same number, so both go through this.
113
+ */
114
+ export declare function filterReasonFor(session: AdoptableSession): FilterReason | null;
107
115
  /**
108
116
  * Reduce a git remote URL to a comparable `host/path` identity (#659).
109
117
  *
@@ -145,6 +153,31 @@ export declare function gitRemotes(cwd: string): Promise<string[]>;
145
153
  export interface AdoptableFleet {
146
154
  listAdoptableSessions(agentName: string, fromWorkingDir?: string): Promise<AdoptableSession[]>;
147
155
  }
156
+ /**
157
+ * One `<claudeHome>/projects/*` folder, with its recorded cwd recovered.
158
+ *
159
+ * Exported for `discover.ts` (#745): instance-level discovery asks the SAME
160
+ * question this module's cache already answers — "which directories on this
161
+ * machine have transcript folders, and what cwd does each really record" — and
162
+ * re-scanning would both duplicate the folder→cwd read and miss the legacy
163
+ * mirroring {@link AdoptableIndex.scanFolders} performs on the way past.
164
+ */
165
+ export interface TranscriptFolder {
166
+ /** Folder name under `<claudeHome>/projects/`. */
167
+ name: string;
168
+ /** Absolute path, symlinks resolved — the de-dup identity for a source. */
169
+ realPath: string;
170
+ /** `mtimeMs:size` of the folder, i.e. "has its file SET changed". */
171
+ key: string;
172
+ /** The cwd recorded inside one of its transcripts, or `null` if unknowable. */
173
+ cwd: string | null;
174
+ /**
175
+ * The path to hand the engine to read THIS folder. Equal to {@link cwd} for an
176
+ * ordinary folder (whose name IS the encoded cwd); the synthetic mirror path
177
+ * for one of the user's own folders (#620).
178
+ */
179
+ engineCwd: string | null;
180
+ }
148
181
  /**
149
182
  * Per-instance adoptable-session detector with an mtime-keyed cache.
150
183
  *
@@ -216,6 +249,18 @@ export declare class AdoptableIndex {
216
249
  * string and tests `undefined` explicitly, never truthiness.
217
250
  */
218
251
  invalidate(projectKey?: string): void;
252
+ /**
253
+ * Every transcript folder this instance can see, with its recorded cwd — the
254
+ * raw material instance-level Discovery (#745) filters into candidate project
255
+ * directories.
256
+ *
257
+ * Deliberately the same call `adoptableFor` makes, cache and legacy mirroring
258
+ * included, rather than a second scan: the mirroring is what puts the user's
259
+ * OWN `~/.claude` history in view (#620), and Discovery finding nothing on a
260
+ * laptop because it skipped that step would be the whole feature failing
261
+ * silently.
262
+ */
263
+ transcriptFolders(): Promise<TranscriptFolder[]>;
219
264
  /**
220
265
  * Enumerate `<claudeHome>/projects/*`, refreshing the recorded cwd only for
221
266
  * folders whose file set changed since last time.
@@ -246,3 +291,15 @@ export declare class AdoptableIndex {
246
291
  */
247
292
  adoptableFor(fleet: AdoptableFleet, project: Project, agentName: string): Promise<AdoptableSummary>;
248
293
  }
294
+ /**
295
+ * The cwd recorded inside a transcript folder, by reading the head of its
296
+ * newest-looking `.jsonl`.
297
+ *
298
+ * Every session in a folder shares the encoded cwd, so ONE read identifies the
299
+ * folder. Files are tried in readdir order until one yields a cwd, because a
300
+ * zero-byte or truncated transcript records none (the QA corpus has both).
301
+ *
302
+ * Exported so nothing else is ever tempted to invert `encodePathForCli` — the
303
+ * encoding is lossy and the folder name is NOT a cwd (see this module's header).
304
+ */
305
+ export declare function recordedCwd(dir: string): Promise<string | null>;
@@ -154,8 +154,14 @@ const SWEEPER_PROMPT_RE = /^Project: .*\(slug: [^)]*\)\s+(?:Summary:.*\s+)?You a
154
154
  export function isSweeperPrompt(preview) {
155
155
  return SWEEPER_PROMPT_RE.test(preview);
156
156
  }
157
- /** Classify a candidate as noise, or `null` to keep it. */
158
- function filterReasonFor(session) {
157
+ /**
158
+ * Classify a candidate as noise, or `null` to keep it.
159
+ *
160
+ * Exported for `discover.ts` (#745), which counts a directory's NON-NOISE
161
+ * sessions to rank it. The count a Discover row shows and the offer the import
162
+ * dialog then makes have to be the same number, so both go through this.
163
+ */
164
+ export function filterReasonFor(session) {
159
165
  if (session.sizeBytes < MIN_TRANSCRIPT_BYTES)
160
166
  return "too-small";
161
167
  if (session.preview !== undefined && isSweeperPrompt(session.preview)) {
@@ -333,6 +339,20 @@ export class AdoptableIndex {
333
339
  else
334
340
  this.summaries.delete(projectKey);
335
341
  }
342
+ /**
343
+ * Every transcript folder this instance can see, with its recorded cwd — the
344
+ * raw material instance-level Discovery (#745) filters into candidate project
345
+ * directories.
346
+ *
347
+ * Deliberately the same call `adoptableFor` makes, cache and legacy mirroring
348
+ * included, rather than a second scan: the mirroring is what puts the user's
349
+ * OWN `~/.claude` history in view (#620), and Discovery finding nothing on a
350
+ * laptop because it skipped that step would be the whole feature failing
351
+ * silently.
352
+ */
353
+ async transcriptFolders() {
354
+ return this.scanFolders();
355
+ }
336
356
  /**
337
357
  * Enumerate `<claudeHome>/projects/*`, refreshing the recorded cwd only for
338
358
  * folders whose file set changed since last time.
@@ -547,8 +567,11 @@ export class AdoptableIndex {
547
567
  * Every session in a folder shares the encoded cwd, so ONE read identifies the
548
568
  * folder. Files are tried in readdir order until one yields a cwd, because a
549
569
  * zero-byte or truncated transcript records none (the QA corpus has both).
570
+ *
571
+ * Exported so nothing else is ever tempted to invert `encodePathForCli` — the
572
+ * encoding is lossy and the folder name is NOT a cwd (see this module's header).
550
573
  */
551
- async function recordedCwd(dir) {
574
+ export async function recordedCwd(dir) {
552
575
  const entries = await fs.readdir(dir).catch(() => []);
553
576
  for (const entry of entries) {
554
577
  if (!entry.endsWith(".jsonl"))
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Making a failed agent run readable (#684).
3
3
  *
4
- * A credential-less `npx @edspencer/paddock --here` — the path the README, the
4
+ * A credential-less `npx @edspencer/paddock` — the path the README, the
5
5
  * docs site and the front page all point new users at — greeted them with a
6
6
  * multi-screen stack trace containing the entire sweeper system prompt, four
7
7
  * times over: once in the `ExecaError`, once in the `[fleet-manager]` line, once
@@ -244,10 +244,10 @@ export interface PlantedChatsLink {
244
244
  * by {@link countLegacyTranscriptLinks} as harmless pre-#620 residue, and they
245
245
  * are: they were planted for a project whose transcripts paddock does own.
246
246
  *
247
- * The ones this finds are the poisoned kind. Under `--here` the workspace's
248
- * `.chats/` sits at `<dir>/.chats` while the data dir is `<dir>/.paddock`, so a
249
- * link planted by an affected build points somewhere no `dataDir` prefix match
250
- * will ever catch. While it exists, every `claude` session the user starts in
247
+ * The ones this finds are the poisoned kind. Under `--here` — removed in #798,
248
+ * but the residue outlives it — the workspace's `.chats/` sat at `<dir>/.chats`
249
+ * while the data dir was `<dir>/.paddock`, so a link planted by an affected build
250
+ * points somewhere no `dataDir` prefix match will ever catch. While it exists, every `claude` session the user starts in
251
251
  * that directory writes into paddock's store instead of their own history, and
252
252
  * anything that expects `~/.claude/projects/<enc>` to be a real directory
253
253
  * (`unzip -d ~/.claude` being the observed one) fails with an error impossible
@@ -695,10 +695,10 @@ export async function countLegacyTranscriptLinks(legacyHome, dataDir) {
695
695
  * by {@link countLegacyTranscriptLinks} as harmless pre-#620 residue, and they
696
696
  * are: they were planted for a project whose transcripts paddock does own.
697
697
  *
698
- * The ones this finds are the poisoned kind. Under `--here` the workspace's
699
- * `.chats/` sits at `<dir>/.chats` while the data dir is `<dir>/.paddock`, so a
700
- * link planted by an affected build points somewhere no `dataDir` prefix match
701
- * will ever catch. While it exists, every `claude` session the user starts in
698
+ * The ones this finds are the poisoned kind. Under `--here` — removed in #798,
699
+ * but the residue outlives it — the workspace's `.chats/` sat at `<dir>/.chats`
700
+ * while the data dir was `<dir>/.paddock`, so a link planted by an affected build
701
+ * points somewhere no `dataDir` prefix match will ever catch. While it exists, every `claude` session the user starts in
702
702
  * that directory writes into paddock's store instead of their own history, and
703
703
  * anything that expects `~/.claude/projects/<enc>` to be a real directory
704
704
  * (`unzip -d ~/.claude` being the observed one) fails with an error impossible
@@ -70,8 +70,10 @@ export interface HostMcpCaveat {
70
70
  * top-level `mcpServers` that applies everywhere, and a
71
71
  * `projects[<absolute dir>].mcpServers` that applies only in that directory. The
72
72
  * per-directory one is keyed by the LITERAL absolute path — not the `-`-encoded
73
- * form the transcript folders use — and it is the scope a `--here` workspace
74
- * hits, because `claude mcp add` without `--scope user` writes there.
73
+ * form the transcript folders use — and it is the scope a project linked at
74
+ * that path hits, because `claude mcp add` without `--scope user` writes there.
75
+ * (It used to say "a `--here` workspace"; that flag is gone as of #798, but the
76
+ * point survives it — a `path:` project reaches exactly the same entry.)
75
77
  *
76
78
  * {@link declared} is the third contributor and the one that is not the host's at
77
79
  * all: paddock's own top-level `mcpServers:` config block (#691 step 6), resolved
@@ -27,7 +27,6 @@ export interface CliOptions {
27
27
  port?: string;
28
28
  host?: string;
29
29
  dataDir?: string;
30
- here: boolean;
31
30
  open: boolean;
32
31
  verbose: boolean;
33
32
  help: boolean;
@@ -37,6 +36,45 @@ export interface CliOptions {
37
36
  export declare class CliError extends Error {
38
37
  }
39
38
  export declare const MIN_NODE_MAJOR = 22;
39
+ /** What `paddock service` can be asked to do. */
40
+ export declare const SERVICE_ACTIONS: readonly ["install", "uninstall", "status"];
41
+ export type ServiceAction = (typeof SERVICE_ACTIONS)[number];
42
+ /** The leading words {@link parseCommand} recognises. Anything else is a flag. */
43
+ export declare const VERBS: readonly ["start", "service"];
44
+ /**
45
+ * A parsed invocation: which verb, plus the flags that followed it.
46
+ *
47
+ * `action` is optional on `service` for exactly one reason — `paddock service
48
+ * --help`, where there is no action to name and printing usage is the whole
49
+ * request. It is `undefined` only when `opts.help` is true.
50
+ */
51
+ export type Command = {
52
+ verb: "start";
53
+ opts: CliOptions;
54
+ } | {
55
+ verb: "service";
56
+ action: ServiceAction | undefined;
57
+ opts: CliOptions;
58
+ };
59
+ /**
60
+ * Split a leading verb off the argv, then parse the rest as flags.
61
+ *
62
+ * The dispatch is deliberately a check on `argv[0]` alone rather than a scan for
63
+ * the first non-flag token, and it happens BEFORE the flag loop. Two properties
64
+ * fall out of that, both of which matter more than the flexibility given up:
65
+ *
66
+ * - **Bare `paddock` is untouched.** No verb means the whole argv goes to
67
+ * {@link parseArgs} exactly as before, so the demo path cannot change
68
+ * behaviour, and an unrecognised leading token still produces `unknown
69
+ * option:` from the flag loop rather than a new and different error.
70
+ * - **Flags parse after a verb**, so `paddock start --port 7299` and
71
+ * `paddock service install --port 7299` both work, and the flag grammar is
72
+ * the same one in every position.
73
+ *
74
+ * A verb is only a verb in first position: `paddock --port start` is still a
75
+ * missing-value error, and `paddock start start` is still `unknown option`.
76
+ */
77
+ export declare function parseCommand(argv: string[]): Command;
40
78
  export declare function parseArgs(argv: string[]): CliOptions;
41
79
  /**
42
80
  * Why this Node is unusable, or `undefined` if it is fine.
@@ -53,4 +91,5 @@ export declare function nodeVersionProblem(nodeVersion: string): string | undefi
53
91
  * names neither the port nor the flag that fixes it.
54
92
  */
55
93
  export declare function explainListenError(err: unknown, host: string, port: string): string;
56
- export declare const USAGE = "paddock \u2014 run a Paddock instance locally\n\nUsage\n npx @edspencer/paddock [options]\n\nOptions\n -p, --port <port> HTTP/WS port (default 7233, or $PORT)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)\n --here Open the CURRENT directory as the workspace (see below)\n -o, --open Open the app in your browser once it is listening\n --verbose Show the server's own logs (quiet by default)\n -v, --version Print the Paddock version and exit\n -h, --help Show this help\n\nOpening a directory (--here)\n Run `paddock --here` inside a project and Paddock opens THAT directory as its\n workspace: Claude works in your files, and any Claude Code sessions you already\n have for the directory are offered for import. It:\n\n \u00B7 creates .paddock/ in the directory for this workspace's own state\n \u00B7 creates .chats/ for transcripts, and adds both to .gitignore\n \u00B7 offers your ~/.claude sessions for this directory for import \u2014 nothing\n there is moved, copied or linked until you confirm\n\n Once done, later runs in the same directory resume it \u2014 no flag needed.\n Without --here, Paddock never touches the directory you ran it from.\n\nCredentials\n Paddock drives Claude Code, so it needs Claude credentials \u2014 and if you\n already use Claude Code on this machine, it uses the login you already have.\n That is a macOS Keychain entry on a Mac, or your ~/.claude/.credentials.json\n elsewhere (symlinked in, never copied). Reading a login writes nothing.\n\n Otherwise: a CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment,\n or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. With no\n login at all anywhere, run `claude setup-token`. Paddock says at startup when\n it can find none.\n\nSharing your Claude Code state\n Apart from that login, Paddock writes nothing outside its data dir by\n default: transcripts go to each project's .chats/, and your ~/.claude is read\n for config only. Each thing it can share is one key in\n <data-dir>/paddock.config.yaml:\n\n claude:\n transcripts: host # own | host, default own\n credentials: host # own | host, default host\n instructions: host # own | host, default own\n hooks: host # own | host, default own\n mcpServers: host # own | host, default own\n\n transcripts: host makes a chat and a `claude --resume` in the same directory\n the same file, live in both directions; deleting such a chat in Paddock\n releases it rather than removing it, because it is your history rather than\n Paddock's copy. credentials: own is the opt-out from sharing the login above.\n\n instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and\n plugins/. Off by default, which is a change: your curated CLAUDE.md does not\n reach Paddock's agents unless you say so. Each project's own CLAUDE.md always\n does.\n\n hooks: host runs the shell commands your ~/.claude/settings.json binds to\n tool use. Off by default \u2014 inheriting someone's hooks is not something to\n discover after the fact. Its other keys (permissions, model, statusline)\n apply either way: under hooks: own Paddock writes its own settings.json\n carrying them with hooks dropped, regenerated at startup.\n\n mcpServers: host attaches the MCP servers declared in your ~/.claude.json \u2014\n the top-level ones, plus a project's own when its directory matches. To give\n this instance a server your machine doesn't have, declare it instead in a\n sibling mcpServers: block of the same file; use env:VAR_NAME anywhere a\n string goes so tokens stay out of it. That keeps a token out of the file, not\n out of `ps`: leave driveMode on its default (session) for a server holding\n one, since batch passes the definition to claude as a command-line argument.\n\nYour data\n Everything lives in one directory \u2014 ~/.paddock unless you pass --data-dir.\n Projects, chat transcripts and settings all persist there between runs. Move\n that directory to move your instance; delete it to start over. Nothing is\n stored anywhere else.\n\nNotes\n Binds loopback with authentication disabled, which is safe for a laptop. To\n expose it on a network, set PADDOCK_AUTH_MODE first \u2014 Paddock refuses to bind\n a routable interface wide open. See AUTH.md.\n\nDocs: https://github.com/edspencer/paddock";
94
+ export declare const USAGE = "paddock \u2014 run a Paddock instance locally\n\nUsage\n npx @edspencer/paddock [options] start the server (the default)\n paddock start [options] the same thing, said out loud\n paddock service <install|uninstall|status>\n run it in the background from login\n (`paddock service --help`)\n\nOptions\n -p, --port <port> HTTP/WS port (default 7233, or $PORT)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)\n -o, --open Open the app in your browser once it is listening\n --verbose Show the server's own logs (quiet by default)\n -v, --version Print the Paddock version and exit\n -h, --help Show this help\n\nOpening your own directories\n Where you run this from does not matter \u2014 the instance is decided by its data\n dir alone. Directories are added inside the app, on the Discover screen: it\n reads your Claude Code history, lists the directories you have worked in, and\n links the ones you tick as projects. A new instance opens on it.\n\n Importing a directory does not write into it. No .paddock/, no .chats/, no\n .gitignore edits, no CLAUDE.md \u2014 the project record and the copied transcripts\n both live in the data dir, and the project just points at the path. Your own\n ~/.claude transcripts are copied, never moved or deleted, so your terminal\n `claude` keeps working exactly as before.\n\nCredentials\n Paddock drives Claude Code, so it needs Claude credentials \u2014 and if you\n already use Claude Code on this machine, it uses the login you already have.\n That is a macOS Keychain entry on a Mac, or your ~/.claude/.credentials.json\n elsewhere (symlinked in, never copied). Reading a login writes nothing.\n\n Otherwise: a CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment,\n or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. With no\n login at all anywhere, run `claude setup-token`. Paddock says at startup when\n it can find none.\n\nSharing your Claude Code state\n Apart from that login, Paddock writes nothing outside its data dir by\n default: transcripts go to each project's .chats/, and your ~/.claude is read\n for config only. Each thing it can share is one key in\n <data-dir>/paddock.config.yaml:\n\n claude:\n transcripts: host # own | host, default own\n credentials: host # own | host, default host\n instructions: host # own | host, default own\n hooks: host # own | host, default own\n mcpServers: host # own | host, default own\n\n transcripts: host makes a chat and a `claude --resume` in the same directory\n the same file, live in both directions; deleting such a chat in Paddock\n releases it rather than removing it, because it is your history rather than\n Paddock's copy. credentials: own is the opt-out from sharing the login above.\n\n instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and\n plugins/. Off by default, which is a change: your curated CLAUDE.md does not\n reach Paddock's agents unless you say so. Each project's own CLAUDE.md always\n does.\n\n hooks: host runs the shell commands your ~/.claude/settings.json binds to\n tool use. Off by default \u2014 inheriting someone's hooks is not something to\n discover after the fact. Its other keys (permissions, model, statusline)\n apply either way: under hooks: own Paddock writes its own settings.json\n carrying them with hooks dropped, regenerated at startup.\n\n mcpServers: host attaches the MCP servers declared in your ~/.claude.json \u2014\n the top-level ones, plus a project's own when its directory matches. To give\n this instance a server your machine doesn't have, declare it instead in a\n sibling mcpServers: block of the same file; use env:VAR_NAME anywhere a\n string goes so tokens stay out of it. That keeps a token out of the file, not\n out of `ps`: leave driveMode on its default (session) for a server holding\n one, since batch passes the definition to claude as a command-line argument.\n\nYour data\n Everything lives in one directory \u2014 ~/.paddock unless you pass --data-dir.\n Projects, chat transcripts and settings all persist there between runs. Move\n that directory to move your instance; delete it to start over. Nothing is\n stored anywhere else.\n\nNotes\n Binds loopback with authentication disabled, which is safe for a laptop. To\n expose it on a network, set PADDOCK_AUTH_MODE first \u2014 Paddock refuses to bind\n a routable interface wide open. See AUTH.md.\n\nDocs: https://github.com/edspencer/paddock";
95
+ export declare const SERVICE_USAGE = "paddock service \u2014 keep Paddock running in the background\n\nUsage\n paddock service install [options] register it and start it now\n paddock service uninstall stop it and deregister it\n paddock service status is it registered, is it running, where are the logs\n\nOptions (install only \u2014 recorded in the generated unit)\n -p, --port <port> HTTP/WS port (default 7233)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Only if you want an instance SEPARATE from your\n terminal one. Omitted by default on purpose, so\n `paddock service` and a bare `paddock` are the same\n ~/.paddock instance reached two ways.\n --verbose Record the server's own logs, not just warnings\n\nAt login, not at boot\n This registers a per-USER service \u2014 a launchd LaunchAgent on macOS, a\n `systemd --user` unit on Linux \u2014 so it runs as you, with your own Claude\n login. That is not incidental: on macOS your Claude login is a Keychain item\n that only a logged-in user session can read. A boot-time system daemon has no\n such session and could not use it.\n\n So Paddock starts when you LOG IN, not when the machine boots. After a\n restart that nobody logs into, Paddock is not running. That is the design, not\n a fault.\n\n On Linux, a user service is also stopped when you log out. To keep it up:\n\n loginctl enable-linger $USER\n\nWhere it lives\n macOS ~/Library/LaunchAgents/net.edspencer.paddock.plist\n logs in <data-dir>/service/\n Linux ~/.config/systemd/user/paddock.service\n logs via journalctl --user -u paddock -f\n\nInstalled from npx?\n `service install` refuses. An npx cache path is hash-keyed and npm may prune\n it, so the unit would work until it silently didn't, at some future login.\n Install properly first:\n\n npm i -g @edspencer/paddock && paddock service install\n\nA note on access\n Paddock binds loopback with authentication off, which is right for a laptop.\n A service is up for as long as you are logged in rather than as long as a\n terminal tab, so that window is longer \u2014 but it is not wider: any local\n process that could reach the port could already read the same Claude login as\n you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.\n\nDocs: https://github.com/edspencer/paddock";
@@ -27,9 +27,60 @@
27
27
  export class CliError extends Error {
28
28
  }
29
29
  export const MIN_NODE_MAJOR = 22;
30
+ /** What `paddock service` can be asked to do. */
31
+ export const SERVICE_ACTIONS = ["install", "uninstall", "status"];
32
+ /** The leading words {@link parseCommand} recognises. Anything else is a flag. */
33
+ export const VERBS = ["start", "service"];
34
+ function isServiceAction(token) {
35
+ return SERVICE_ACTIONS.includes(token);
36
+ }
37
+ /**
38
+ * Split a leading verb off the argv, then parse the rest as flags.
39
+ *
40
+ * The dispatch is deliberately a check on `argv[0]` alone rather than a scan for
41
+ * the first non-flag token, and it happens BEFORE the flag loop. Two properties
42
+ * fall out of that, both of which matter more than the flexibility given up:
43
+ *
44
+ * - **Bare `paddock` is untouched.** No verb means the whole argv goes to
45
+ * {@link parseArgs} exactly as before, so the demo path cannot change
46
+ * behaviour, and an unrecognised leading token still produces `unknown
47
+ * option:` from the flag loop rather than a new and different error.
48
+ * - **Flags parse after a verb**, so `paddock start --port 7299` and
49
+ * `paddock service install --port 7299` both work, and the flag grammar is
50
+ * the same one in every position.
51
+ *
52
+ * A verb is only a verb in first position: `paddock --port start` is still a
53
+ * missing-value error, and `paddock start start` is still `unknown option`.
54
+ */
55
+ export function parseCommand(argv) {
56
+ const [first, ...rest] = argv;
57
+ if (first === "service") {
58
+ const head = rest[0];
59
+ let action;
60
+ if (head !== undefined && !head.startsWith("-")) {
61
+ // Catch a misspelled action here rather than letting the flag loop call
62
+ // it an "unknown option", which sends the reader looking for a flag.
63
+ if (!isServiceAction(head)) {
64
+ throw new CliError(`unknown service action: ${head}\n` +
65
+ `Expected one of: ${SERVICE_ACTIONS.join(", ")}.\n` +
66
+ "Run `paddock service --help` for usage.");
67
+ }
68
+ action = head;
69
+ }
70
+ const opts = parseArgs(action === undefined ? rest : rest.slice(1));
71
+ // `--help` wins over a missing action: asking for usage is not a usage error.
72
+ if (action === undefined && !opts.help) {
73
+ throw new CliError(`\`paddock service\` needs an action: ${SERVICE_ACTIONS.join(", ")}.\n` +
74
+ "Run `paddock service --help` for usage.");
75
+ }
76
+ return { verb: "service", action, opts };
77
+ }
78
+ if (first === "start")
79
+ return { verb: "start", opts: parseArgs(rest) };
80
+ return { verb: "start", opts: parseArgs(argv) };
81
+ }
30
82
  export function parseArgs(argv) {
31
83
  const opts = {
32
- here: false,
33
84
  open: false,
34
85
  verbose: false,
35
86
  help: false,
@@ -55,9 +106,6 @@ export function parseArgs(argv) {
55
106
  case "--data-dir":
56
107
  opts.dataDir = next();
57
108
  break;
58
- case "--here":
59
- opts.here = true;
60
- break;
61
109
  case "-o":
62
110
  case "--open":
63
111
  opts.open = true;
@@ -115,30 +163,32 @@ export function explainListenError(err, host, port) {
115
163
  export const USAGE = `paddock — run a Paddock instance locally
116
164
 
117
165
  Usage
118
- npx @edspencer/paddock [options]
166
+ npx @edspencer/paddock [options] start the server (the default)
167
+ paddock start [options] the same thing, said out loud
168
+ paddock service <install|uninstall|status>
169
+ run it in the background from login
170
+ (\`paddock service --help\`)
119
171
 
120
172
  Options
121
173
  -p, --port <port> HTTP/WS port (default 7233, or $PORT)
122
174
  --host <host> Bind address (default 127.0.0.1)
123
175
  -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)
124
- --here Open the CURRENT directory as the workspace (see below)
125
176
  -o, --open Open the app in your browser once it is listening
126
177
  --verbose Show the server's own logs (quiet by default)
127
178
  -v, --version Print the Paddock version and exit
128
179
  -h, --help Show this help
129
180
 
130
- Opening a directory (--here)
131
- Run \`paddock --here\` inside a project and Paddock opens THAT directory as its
132
- workspace: Claude works in your files, and any Claude Code sessions you already
133
- have for the directory are offered for import. It:
134
-
135
- · creates .paddock/ in the directory for this workspace's own state
136
- · creates .chats/ for transcripts, and adds both to .gitignore
137
- · offers your ~/.claude sessions for this directory for import — nothing
138
- there is moved, copied or linked until you confirm
181
+ Opening your own directories
182
+ Where you run this from does not matter — the instance is decided by its data
183
+ dir alone. Directories are added inside the app, on the Discover screen: it
184
+ reads your Claude Code history, lists the directories you have worked in, and
185
+ links the ones you tick as projects. A new instance opens on it.
139
186
 
140
- Once done, later runs in the same directory resume it — no flag needed.
141
- Without --here, Paddock never touches the directory you ran it from.
187
+ Importing a directory does not write into it. No .paddock/, no .chats/, no
188
+ .gitignore edits, no CLAUDE.md — the project record and the copied transcripts
189
+ both live in the data dir, and the project just points at the path. Your own
190
+ ~/.claude transcripts are copied, never moved or deleted, so your terminal
191
+ \`claude\` keeps working exactly as before.
142
192
 
143
193
  Credentials
144
194
  Paddock drives Claude Code, so it needs Claude credentials — and if you
@@ -200,3 +250,55 @@ Notes
200
250
  a routable interface wide open. See AUTH.md.
201
251
 
202
252
  Docs: https://github.com/edspencer/paddock`;
253
+ export const SERVICE_USAGE = `paddock service — keep Paddock running in the background
254
+
255
+ Usage
256
+ paddock service install [options] register it and start it now
257
+ paddock service uninstall stop it and deregister it
258
+ paddock service status is it registered, is it running, where are the logs
259
+
260
+ Options (install only — recorded in the generated unit)
261
+ -p, --port <port> HTTP/WS port (default 7233)
262
+ --host <host> Bind address (default 127.0.0.1)
263
+ -d, --data-dir <path> Only if you want an instance SEPARATE from your
264
+ terminal one. Omitted by default on purpose, so
265
+ \`paddock service\` and a bare \`paddock\` are the same
266
+ ~/.paddock instance reached two ways.
267
+ --verbose Record the server's own logs, not just warnings
268
+
269
+ At login, not at boot
270
+ This registers a per-USER service — a launchd LaunchAgent on macOS, a
271
+ \`systemd --user\` unit on Linux — so it runs as you, with your own Claude
272
+ login. That is not incidental: on macOS your Claude login is a Keychain item
273
+ that only a logged-in user session can read. A boot-time system daemon has no
274
+ such session and could not use it.
275
+
276
+ So Paddock starts when you LOG IN, not when the machine boots. After a
277
+ restart that nobody logs into, Paddock is not running. That is the design, not
278
+ a fault.
279
+
280
+ On Linux, a user service is also stopped when you log out. To keep it up:
281
+
282
+ loginctl enable-linger $USER
283
+
284
+ Where it lives
285
+ macOS ~/Library/LaunchAgents/net.edspencer.paddock.plist
286
+ logs in <data-dir>/service/
287
+ Linux ~/.config/systemd/user/paddock.service
288
+ logs via journalctl --user -u paddock -f
289
+
290
+ Installed from npx?
291
+ \`service install\` refuses. An npx cache path is hash-keyed and npm may prune
292
+ it, so the unit would work until it silently didn't, at some future login.
293
+ Install properly first:
294
+
295
+ npm i -g @edspencer/paddock && paddock service install
296
+
297
+ A note on access
298
+ Paddock binds loopback with authentication off, which is right for a laptop.
299
+ A service is up for as long as you are logged in rather than as long as a
300
+ terminal tab, so that window is longer — but it is not wider: any local
301
+ process that could reach the port could already read the same Claude login as
302
+ you. Set PADDOCK_AUTH_MODE if you want a credential on it anyway.
303
+
304
+ Docs: https://github.com/edspencer/paddock`;