@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.
- package/README.md +24 -30
- package/package.json +1 -1
- package/packages/server/dist/adoptable.d.ts +57 -0
- package/packages/server/dist/adoptable.js +26 -3
- package/packages/server/dist/agent-errors.js +1 -1
- package/packages/server/dist/claude-home.d.ts +4 -4
- package/packages/server/dist/claude-home.js +4 -4
- package/packages/server/dist/claude-mcp.d.ts +4 -2
- package/packages/server/dist/cli/args.d.ts +41 -2
- package/packages/server/dist/cli/args.js +119 -17
- package/packages/server/dist/cli/paddock.js +87 -96
- package/packages/server/dist/cli/service/backend.d.ts +38 -0
- package/packages/server/dist/cli/service/backend.js +19 -0
- package/packages/server/dist/cli/service/index.d.ts +28 -0
- package/packages/server/dist/cli/service/index.js +179 -0
- package/packages/server/dist/cli/service/launchd.d.ts +47 -0
- package/packages/server/dist/cli/service/launchd.js +206 -0
- package/packages/server/dist/cli/service/spec.d.ts +106 -0
- package/packages/server/dist/cli/service/spec.js +124 -0
- package/packages/server/dist/cli/service/systemd.d.ts +48 -0
- package/packages/server/dist/cli/service/systemd.js +155 -0
- package/packages/server/dist/discover.d.ts +170 -0
- package/packages/server/dist/discover.js +365 -0
- package/packages/server/dist/herdctl.d.ts +25 -2
- package/packages/server/dist/herdctl.js +30 -1
- package/packages/server/dist/routes/chats.js +14 -2
- package/packages/server/dist/routes/discover.d.ts +3 -0
- package/packages/server/dist/routes/discover.js +140 -0
- package/packages/server/dist/routes.d.ts +1 -0
- package/packages/server/dist/routes.js +2 -0
- package/packages/server/dist/session-hub.d.ts +10 -0
- package/packages/server/dist/session-hub.js +16 -2
- package/packages/server/dist/ws-protocol.d.ts +15 -0
- package/packages/server/dist/ws.js +1 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/Chip-DIdRqcqU.js +2 -0
- package/packages/web/dist/assets/DiscoverPage-BmCWnkjN.js +2 -0
- package/packages/web/dist/assets/DiscoverView-DwhtBdN2.js +2 -0
- package/packages/web/dist/assets/InstanceConfigPage-D01x1BUg.js +2 -0
- package/packages/web/dist/assets/{MarkdownRenderer-DDVO0zhl.js → MarkdownRenderer-BCheNET2.js} +2 -2
- package/packages/web/dist/assets/ProjectMenu-BqlUQuxM.js +2 -0
- package/packages/web/dist/assets/ProjectRedirect-D8QDxYLG.js +2 -0
- package/packages/web/dist/assets/ProjectView-cevSYdcl.js +13 -0
- package/packages/web/dist/assets/ProjectsGrid-yd18DXdV.js +2 -0
- package/packages/web/dist/assets/RootHome-DZEkOjko.js +2 -0
- package/packages/web/dist/assets/Surfaces-DvRztndg.js +2 -0
- package/packages/web/dist/assets/{arc-DXqVnx_r.js → arc-DFqBni1E.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-FgkfD1Bk.js → architectureDiagram-3BPJPVTR-DXFeq3pL.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-WwkIIJsI.js → blockDiagram-GPEHLZMM-DpeYsIwP.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-BIzkTibx.js → c4Diagram-AAUBKEIU-Bw5r8wjW.js} +1 -1
- package/packages/web/dist/assets/channel-DU94S7yB.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-C4TBH3R6.js → chunk-2J33WTMH-B0SAvGOr.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DTKJZlRM.js → chunk-4BX2VUAB-BaUGs1hD.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BC9_Qc8h.js → chunk-55IACEB6-BDHRluF5.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-D3gQ4rP3.js → chunk-727SXJPM-BX93DBZO.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-My2AXBvz.js → chunk-AQP2D5EJ-3RiQGxW2.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-D09zE6Ow.js → chunk-FMBD7UC4-DMkjv9SP.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CUSgeCLl.js → chunk-ND2GUHAM-CHiQfq4F.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-B6jVPYgy.js → chunk-QZHKN3VN-B1wCSHbM.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BcRv1OPp.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BcRv1OPp.js +2 -0
- package/packages/web/dist/assets/{core-DPY0seI-.js → core-C-49RP59.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BasCiARv.js → cose-bilkent-S5V4N54A-BIwwgSTZ.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-Cimn11Qe.js → dagre-BM42HDAG-BN45kshV.js} +1 -1
- package/packages/web/dist/assets/diagram-2AECGRRQ-C5NekbfR.js +44 -0
- package/packages/web/dist/assets/diagram-5GNKFQAL-De7wdssd.js +11 -0
- package/packages/web/dist/assets/{diagram-KO2AKTUF-dLORrSiu.js → diagram-KO2AKTUF-CgG0UA0n.js} +3 -3
- package/packages/web/dist/assets/{diagram-LMA3HP47-iUMeaR-J.js → diagram-LMA3HP47-DC-bDqxV.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-oEH6olHK.js → diagram-OG6HWLK6-XXfHDMhE.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-VtQqF9SY.js → erDiagram-TEJ5UH35-CHEd8JiS.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-wvF2EkJa.js → flowDiagram-I6XJVG4X-BbuYxd15.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-ng4qJ0E1.js → ganttDiagram-6RSMTGT7-BkF77xn5.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-BV4aWy8k.js → gitGraphDiagram-PVQCEYII-BdL5zVez.js} +2 -2
- package/packages/web/dist/assets/index-C3MdCCzF.js +71 -0
- package/packages/web/dist/assets/index-DYccdR9x.css +1 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BpGTxAjU.js → infoDiagram-5YYISTIA-vhTRATYg.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-Cj36LyoS.js → ishikawaDiagram-YF4QCWOH-CxjN-7YF.js} +2 -2
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-0vNlg3um.js → journeyDiagram-JHISSGLW-DJ414TaX.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-wijkEDaR.js → kanban-definition-UN3LZRKU-gaQeFRvZ.js} +1 -1
- package/packages/web/dist/assets/{linear-LqRymMD_.js → linear-CcvdFgxd.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-DPrhSs95.js → mermaid.core-Es6hedDB.js} +5 -5
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-en-uGqgC.js → mindmap-definition-RKZ34NQL-Dk7qf3FR.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-DKBZ2Vnx.js → pieDiagram-4H26LBE5-CSWXwU6d.js} +3 -3
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB--g-WtxyL.js → quadrantDiagram-W4KKPZXB-CBVdURbr.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-Dl4eYxDI.js → requirementDiagram-4Y6WPE33-CbU18IZA.js} +3 -3
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-7lt4gqSw.js → sankeyDiagram-5OEKKPKP-BeAlNgjE.js} +4 -4
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-COgq-dXg.js → sequenceDiagram-3UESZ5HK-CKVKzxTs.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CL6XC79S.js → stateDiagram-AJRCARHV-B-aSaO4W.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-TptFh32C.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C5egKeGT.js → timeline-definition-PNZ67QCA-CP9JHEBr.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C_UhtC8u.js → vennDiagram-CIIHVFJN-Dbtwwvvd.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Bj9chUqz.js → wardley-L42UT6IY-NonvEhoT.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-D7VnZoRp.js → wardleyDiagram-YWT4CUSO-CDnpv9Ws.js} +2 -2
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-VrWLDoi4.js → xychartDiagram-2RQKCTM6-C1i-Wk0H.js} +1 -1
- package/packages/web/dist/fonts/alegreya-latin-italic.woff2 +0 -0
- package/packages/web/dist/fonts/alegreya-latin.woff2 +0 -0
- package/packages/web/dist/fonts/alegreya-sc-latin-500.woff2 +0 -0
- package/packages/web/dist/fonts/alegreya-sc-latin-700.woff2 +0 -0
- package/packages/web/dist/fonts/courier-prime-latin-400.woff2 +0 -0
- package/packages/web/dist/fonts/courier-prime-latin-700.woff2 +0 -0
- package/packages/web/dist/fonts/courier-prime-latin-italic.woff2 +0 -0
- package/packages/web/dist/fonts/iosevka-terminal-400.woff2 +0 -0
- package/packages/web/dist/fonts/iosevka-terminal-700.woff2 +0 -0
- package/packages/web/dist/fonts/martian-mono-latin.woff2 +0 -0
- package/packages/web/dist/fonts/saira-latin.woff2 +0 -0
- package/packages/web/dist/index.html +61 -12
- package/packages/web/dist/sw.js +1 -1
- package/packages/server/dist/cli/here.d.ts +0 -25
- package/packages/server/dist/cli/here.js +0 -104
- package/packages/web/dist/assets/InstanceConfigPage-DKzAHOhx.js +0 -2
- package/packages/web/dist/assets/ProjectRedirect-WczXTV0_.js +0 -2
- package/packages/web/dist/assets/ProjectView-Dd_myWpV.js +0 -13
- package/packages/web/dist/assets/ProjectsGrid-BeJj0q6Y.js +0 -2
- package/packages/web/dist/assets/channel-DUsOl5_p.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CIdDKEqH.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CIdDKEqH.js +0 -2
- package/packages/web/dist/assets/diagram-2AECGRRQ-fe9cDHyn.js +0 -44
- package/packages/web/dist/assets/diagram-5GNKFQAL-T0tQlfbl.js +0 -11
- package/packages/web/dist/assets/format-CvRqOKZU.js +0 -2
- package/packages/web/dist/assets/index-DGNSeqL-.js +0 -71
- package/packages/web/dist/assets/index-DMRbU1Jl.css +0 -1
- 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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
**
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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
|
@@ -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
|
-
/**
|
|
158
|
-
|
|
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
|
|
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`
|
|
248
|
-
*
|
|
249
|
-
* link planted by an affected build
|
|
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`
|
|
699
|
-
*
|
|
700
|
-
* link planted by an affected build
|
|
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
|
|
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
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
141
|
-
|
|
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`;
|