@edspencer/paddock 0.60.0 → 0.61.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +44 -3
- package/package.json +3 -2
- package/packages/server/dist/adoptable.d.ts +36 -2
- package/packages/server/dist/adoptable.js +51 -11
- package/packages/server/dist/agent-errors.d.ts +41 -0
- package/packages/server/dist/agent-errors.js +127 -0
- package/packages/server/dist/app.js +36 -0
- package/packages/server/dist/claude-home.d.ts +192 -0
- package/packages/server/dist/claude-home.js +387 -0
- package/packages/server/dist/cli/args.d.ts +9 -1
- package/packages/server/dist/cli/args.js +21 -5
- package/packages/server/dist/cli/claude-home-choice.d.ts +44 -0
- package/packages/server/dist/cli/claude-home-choice.js +108 -0
- package/packages/server/dist/cli/import-chats.js +9 -6
- package/packages/server/dist/cli/paddock.js +54 -33
- package/packages/server/dist/config.d.ts +75 -12
- package/packages/server/dist/config.js +58 -11
- package/packages/server/dist/herdctl-agent-config.d.ts +15 -7
- package/packages/server/dist/herdctl-agent-config.js +21 -9
- package/packages/server/dist/herdctl.d.ts +15 -0
- package/packages/server/dist/herdctl.js +50 -6
- package/packages/server/dist/hook-config.d.ts +17 -15
- package/packages/server/dist/hook-config.js +11 -9
- package/packages/server/dist/routes/chats.js +33 -6
- package/packages/server/dist/sweep.js +13 -1
- package/packages/server/dist/transcripts.d.ts +32 -6
- package/packages/server/dist/transcripts.js +30 -24
- package/packages/server/dist/trigger-config.d.ts +18 -8
- package/packages/server/dist/trigger-config.js +18 -8
- package/packages/server/dist/ws.js +43 -11
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-xn9Rvoah.js → InstanceConfigPage-BgNXqAox.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DMONssKA.js → MarkdownRenderer-DfD_UtqU.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DHoWtvtY.js → ProjectRedirect-1EXdHXmO.js} +1 -1
- package/packages/web/dist/assets/ProjectView-DHb5vNUp.js +13 -0
- package/packages/web/dist/assets/{ProjectsGrid-CjfwpeFj.js → ProjectsGrid-Br-EhLZE.js} +1 -1
- package/packages/web/dist/assets/{arc-DdFdbD-c.js → arc-C2Jekujs.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-DC8UKtwV.js → architectureDiagram-3BPJPVTR-CflCA-em.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DqFFe_5c.js → blockDiagram-GPEHLZMM-Ds7GLNnC.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DbffFSM-.js → c4Diagram-AAUBKEIU-D4WyGO6I.js} +1 -1
- package/packages/web/dist/assets/channel-B1K_gwAH.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-8PP2RHft.js → chunk-2J33WTMH-tQunO_M5.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-Hag68B5N.js → chunk-4BX2VUAB-DT5dOWd2.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-BXp_gQIa.js → chunk-55IACEB6-CfTUqUEF.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-DlntjQux.js → chunk-727SXJPM-sK5bOAA0.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-BhI9KPrE.js → chunk-AQP2D5EJ-DcMgVe-P.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-WhfORs2z.js → chunk-FMBD7UC4-In3h_-7v.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-CIskmv20.js → chunk-ND2GUHAM-DuaIeRaC.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-CNOTpCW4.js → chunk-QZHKN3VN-DeT5mR5T.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +2 -0
- package/packages/web/dist/assets/{core-usOVRkLM.js → core-DgOgLvnM.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CniuMIIW.js → cose-bilkent-S5V4N54A-CyO-rznJ.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-5CteYfQY.js → dagre-BM42HDAG-B_9Ihzgn.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-BIQjD2LA.js → diagram-2AECGRRQ-z5mvZ5YT.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-1vy3ErTt.js → diagram-5GNKFQAL-Bsz0cgfn.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D45iqFk6.js → diagram-KO2AKTUF-B29k5f4q.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-DNktDktn.js → diagram-LMA3HP47-CFrwsHoI.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-BQ9JgV70.js → diagram-OG6HWLK6-D9Sjc2HR.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-oBasZ1uv.js → erDiagram-TEJ5UH35-C3vbkD7M.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DxnJHMBS.js → flowDiagram-I6XJVG4X-DkMIgntd.js} +1 -1
- package/packages/web/dist/assets/{format-CIAat6sU.js → format-BFDqyFuw.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CtJQ2m7z.js → ganttDiagram-6RSMTGT7-nvHanjpW.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-5RPscwD_.js → gitGraphDiagram-PVQCEYII-DbCNfOu7.js} +1 -1
- package/packages/web/dist/assets/index-DGCs1YIO.js +71 -0
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-zQu3xkW4.js → infoDiagram-5YYISTIA-aJjH9x3D.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-ChoEjcdw.js → ishikawaDiagram-YF4QCWOH-DoifwNnZ.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-DM2KWmDr.js → journeyDiagram-JHISSGLW-Cz9kG9HN.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DWzTpYgh.js → kanban-definition-UN3LZRKU-BoXnfr7A.js} +1 -1
- package/packages/web/dist/assets/{linear-Bpy9Aj-K.js → linear-BlTTO9yR.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-D43x5bFb.js → mermaid.core-YAG8eSPQ.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BhceB-cv.js → mindmap-definition-RKZ34NQL-DtUBKfkr.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BgcGlFFA.js → pieDiagram-4H26LBE5-B3Rwpmy0.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-ZRVaOoSp.js → quadrantDiagram-W4KKPZXB-B8eJtTyk.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-2hHcJHBt.js → requirementDiagram-4Y6WPE33-CpAluiY9.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-KPUQ9Mu5.js → sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-D4BXQSSM.js → sequenceDiagram-3UESZ5HK-CRpqKSGm.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DftS7dIo.js → stateDiagram-AJRCARHV-h3h8gXv_.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-C0OAxyWY.js → timeline-definition-PNZ67QCA-PVr5V_pG.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-C94fINN3.js → vennDiagram-CIIHVFJN-De-4NPAX.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-CbFrlwHE.js → wardley-L42UT6IY-Dtv10D2d.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CHG2nndd.js → wardleyDiagram-YWT4CUSO-p2fJuQ6C.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-U9ESc19z.js → xychartDiagram-2RQKCTM6-DyYF-k92.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/ProjectView-BmSdUwY2.js +0 -13
- package/packages/web/dist/assets/channel-QxsJGL6F.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CI7p84hR.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CI7p84hR.js +0 -2
- package/packages/web/dist/assets/index-DtRblEY_.js +0 -71
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CgBC1jpO.js +0 -2
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { PaddockConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Entries of a Claude home that are USER-LEVEL configuration rather than
|
|
4
|
+
* per-instance state, symlinked from the user's home into paddock's own when
|
|
5
|
+
* paddock's does not already have its own.
|
|
6
|
+
*
|
|
7
|
+
* Without this, relocating the home silently changes agent behaviour: the user's
|
|
8
|
+
* `~/.claude/CLAUDE.md` is auto-loaded into every session, `settings.json`
|
|
9
|
+
* carries permissions and hooks, and `agents/` `commands/` `plugins/` are
|
|
10
|
+
* capability. Dropping them is not "a clean home", it is a behaviour regression
|
|
11
|
+
* nobody asked for.
|
|
12
|
+
*
|
|
13
|
+
* `.credentials.json` is the one that matters most: on Linux (and in the Docker
|
|
14
|
+
* image) it is where `claude login` puts the OAuth token, so without the bridge
|
|
15
|
+
* every file-authenticated install would come back logged out. Symlinked rather
|
|
16
|
+
* than copied — a copy would duplicate a live secret onto disk AND diverge the
|
|
17
|
+
* moment Claude Code refreshed the token.
|
|
18
|
+
*
|
|
19
|
+
* Deliberately NOT bridged: `projects/` (that's the whole point), and everything
|
|
20
|
+
* that is per-instance runtime state — `todos/`, `shell-snapshots/`, `statsig/`,
|
|
21
|
+
* `sessions/`. Those are exactly what should now be per-instance.
|
|
22
|
+
*/
|
|
23
|
+
export declare const BRIDGED_ENTRIES: readonly [".credentials.json", "settings.json", "CLAUDE.md", "agents", "commands", "plugins"];
|
|
24
|
+
/**
|
|
25
|
+
* What a Keychain probe concluded.
|
|
26
|
+
*
|
|
27
|
+
* `unknown` is the important one and the default: not darwin, no `security`
|
|
28
|
+
* binary, the lookup errored, or we simply did not find an entry under a name we
|
|
29
|
+
* guessed. It must never be reported to the user as "you have no login".
|
|
30
|
+
*/
|
|
31
|
+
export type KeychainFinding = "found" | "unknown";
|
|
32
|
+
/** Probe for a macOS login paddock's own home cannot see. Injectable for tests. */
|
|
33
|
+
export type KeychainProbe = () => Promise<KeychainFinding>;
|
|
34
|
+
/**
|
|
35
|
+
* Ask the macOS Keychain whether a plain-name Claude Code login exists (#683).
|
|
36
|
+
*
|
|
37
|
+
* The whole value of this is turning "you appear to have no credentials" into
|
|
38
|
+
* "you ARE logged in; paddock cannot see it, and here is the one-liner" — a
|
|
39
|
+
* distinction paddock currently cannot make, and the difference between a
|
|
40
|
+
* message that describes a problem and one that fixes it.
|
|
41
|
+
*
|
|
42
|
+
* ## The asymmetry, which is deliberate
|
|
43
|
+
*
|
|
44
|
+
* A positive result is trustworthy: an entry under that service name is a login.
|
|
45
|
+
* A negative result is NOT — the service name is Claude Code's internal detail
|
|
46
|
+
* and can change, and `security` can fail for a dozen reasons that have nothing
|
|
47
|
+
* to do with the user. So a miss returns `unknown`, and the caller falls back to
|
|
48
|
+
* the existing, correct, generic warning. Guessing the name wrong therefore
|
|
49
|
+
* costs nothing beyond the improvement not happening.
|
|
50
|
+
*
|
|
51
|
+
* Never prompts and never reads a secret: `find-generic-password` WITHOUT `-w`
|
|
52
|
+
* reports existence only (attributes, which are discarded), so nothing sensitive
|
|
53
|
+
* reaches a log, a transcript, or this process's memory.
|
|
54
|
+
*/
|
|
55
|
+
export declare function probeMacosKeychainLogin(platform?: string): Promise<KeychainFinding>;
|
|
56
|
+
/** A message for the operator, at the level it should be logged at. */
|
|
57
|
+
export interface ClaudeHomeNotice {
|
|
58
|
+
level: "info" | "warn";
|
|
59
|
+
message: string;
|
|
60
|
+
}
|
|
61
|
+
export interface ClaudeHomeReport {
|
|
62
|
+
/** Entries symlinked in from the user's home this boot. */
|
|
63
|
+
bridged: string[];
|
|
64
|
+
/** Messages for `app.ts` to log — config resolution has no logger of its own. */
|
|
65
|
+
notices: ClaudeHomeNotice[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Create paddock's Claude home and bridge the user-level config into it.
|
|
69
|
+
*
|
|
70
|
+
* Idempotent and non-clobbering: an entry paddock's home already has (whether a
|
|
71
|
+
* real file the user put there or a symlink from a previous boot) is left
|
|
72
|
+
* completely alone, so this is safe to run on every start.
|
|
73
|
+
*
|
|
74
|
+
* Never throws. A home that cannot be created is not a reason to refuse to boot
|
|
75
|
+
* — Claude Code falls back to its own default and chat still works, which is the
|
|
76
|
+
* same failure mode `ensureProjectChats` has always had.
|
|
77
|
+
*/
|
|
78
|
+
export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "ownsClaudeHome">, env?: NodeJS.ProcessEnv, probeKeychain?: KeychainProbe): Promise<ClaudeHomeReport>;
|
|
79
|
+
/** One of the user's transcript folders, made readable through paddock's home. */
|
|
80
|
+
export interface LegacyMirror {
|
|
81
|
+
/** Folder name inside `<claudeHome>/projects/` — `encodePathForCli(engineCwd)`. */
|
|
82
|
+
name: string;
|
|
83
|
+
/**
|
|
84
|
+
* The working directory the ENGINE must be handed to read this folder.
|
|
85
|
+
*
|
|
86
|
+
* Not the real cwd the transcripts were recorded in — see
|
|
87
|
+
* {@link mirrorLegacyTranscriptFolders} for why it cannot be.
|
|
88
|
+
*/
|
|
89
|
+
engineCwd: string;
|
|
90
|
+
/** The user's real folder in `<legacyHome>/projects/`, which we only read. */
|
|
91
|
+
legacyDir: string;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The synthetic working directory a legacy transcript folder is offered to the
|
|
95
|
+
* engine under: the folder's own path inside the user's home.
|
|
96
|
+
*
|
|
97
|
+
* Any stable, per-folder, collision-free path would do. This one is the obvious
|
|
98
|
+
* choice because it already exists, is unique by construction, and is honest
|
|
99
|
+
* about where the transcripts actually are.
|
|
100
|
+
*/
|
|
101
|
+
export declare function legacySourcePath(legacyHome: string, folderName: string): string;
|
|
102
|
+
/**
|
|
103
|
+
* Make the user's own transcript folders visible through paddock's Claude home,
|
|
104
|
+
* so adoption (#588) still finds them.
|
|
105
|
+
*
|
|
106
|
+
* Adoption's whole premise is "you already have terminal `claude` history for
|
|
107
|
+
* this directory" — and that history is, by definition, in `~/.claude`. But the
|
|
108
|
+
* engine resolves every adoption path against the ONE home the FleetManager was
|
|
109
|
+
* built with, with no per-call override, so once paddock owns its home the
|
|
110
|
+
* user's folders are out of reach. Without this, #620 would silently gut #588.
|
|
111
|
+
*
|
|
112
|
+
* The fix is a read-only view: symlink each of the user's folders into
|
|
113
|
+
* `<claudeHome>/projects/`. The engine then reads their transcripts through it,
|
|
114
|
+
* and adoption's default `copy` mode writes the copy into the project's own
|
|
115
|
+
* folder (itself the symlink to `.chats/`) — originals read, never touched.
|
|
116
|
+
*
|
|
117
|
+
* ## Why the mirror is not named after the recorded cwd
|
|
118
|
+
*
|
|
119
|
+
* The obvious mirror name — the same encoded name the user's folder has — is
|
|
120
|
+
* wrong, and wrong in exactly the case adoption exists for. A folder's name is
|
|
121
|
+
* `encodePathForCli(cwd)`, so the user's history for `/code/thing` and a paddock
|
|
122
|
+
* project whose working directory IS `/code/thing` want the SAME name in the
|
|
123
|
+
* same directory. Paddock's `.chats/` symlink is already there and must stay, so
|
|
124
|
+
* the user's folder loses — and "I pointed a project at a directory I already
|
|
125
|
+
* have `claude` history for", the headline case, silently offers nothing.
|
|
126
|
+
*
|
|
127
|
+
* (Pre-#620 this case worked only because `ensureProjectChats` had already MOVED
|
|
128
|
+
* those transcripts into `.chats/` and deleted the originals. That destruction is
|
|
129
|
+
* the thing #620 removes, so the case now needs a real mechanism.)
|
|
130
|
+
*
|
|
131
|
+
* So each mirror is named for a SYNTHETIC working directory
|
|
132
|
+
* ({@link legacySourcePath}) that no agent will ever have, which makes
|
|
133
|
+
* collisions structurally impossible rather than merely unlikely. The caller
|
|
134
|
+
* keeps the folder's real recorded cwd for display and hands `engineCwd` to the
|
|
135
|
+
* engine — see `AdoptableSource.importFrom`.
|
|
136
|
+
*
|
|
137
|
+
* Entries in the legacy home that are themselves SYMLINKS are skipped: those are
|
|
138
|
+
* ones paddock planted under the old layout, pointing back at some `.chats/`, and
|
|
139
|
+
* mirroring them would re-offer paddock's own chats as "adoptable". That also
|
|
140
|
+
* makes the leftovers from before this change inert rather than confusing.
|
|
141
|
+
*
|
|
142
|
+
* Idempotent and never throws. Returns every mirror it maintains, not only the
|
|
143
|
+
* ones planted this call, so a caller can rely on the mapping after any call.
|
|
144
|
+
*/
|
|
145
|
+
export declare function mirrorLegacyTranscriptFolders(claudeHome: string, legacyHome: string): Promise<LegacyMirror[]>;
|
|
146
|
+
/**
|
|
147
|
+
* Count the transcript symlinks a paddock instance planted in the user's home
|
|
148
|
+
* under the pre-#620 layout that point back into THIS data dir.
|
|
149
|
+
*
|
|
150
|
+
* Purely informational. They are harmless — they still resolve, and nothing
|
|
151
|
+
* reads them any more — but they accumulate one per project per instance and go
|
|
152
|
+
* stale as soon as a data dir is deleted, so an operator is better off being
|
|
153
|
+
* told they exist than discovering a pile of dangling links months later.
|
|
154
|
+
* Paddock does not remove them itself: they live in `~/.claude`, and not writing
|
|
155
|
+
* there is the entire point.
|
|
156
|
+
*/
|
|
157
|
+
export declare function countLegacyTranscriptLinks(legacyHome: string, dataDir: string): Promise<number>;
|
|
158
|
+
/** A `~/.claude/projects/<enc>` symlink pointing at some `.chats/` store. */
|
|
159
|
+
export interface PlantedChatsLink {
|
|
160
|
+
/** The link itself, inside the user's home — the path to tell the user about. */
|
|
161
|
+
link: string;
|
|
162
|
+
/** What it points at, resolved. Always a `.chats` directory by construction. */
|
|
163
|
+
target: string;
|
|
164
|
+
/** Does the target still exist? A dangling link is the one that breaks tooling. */
|
|
165
|
+
dangling: boolean;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Find transcript symlinks planted in the user's home that point at a `.chats/`
|
|
169
|
+
* store OUTSIDE `dataDir` (#682).
|
|
170
|
+
*
|
|
171
|
+
* The instance's own leftovers — links into this data dir — are already reported
|
|
172
|
+
* by {@link countLegacyTranscriptLinks} as harmless pre-#620 residue, and they
|
|
173
|
+
* are: they were planted for a project whose transcripts paddock does own.
|
|
174
|
+
*
|
|
175
|
+
* The ones this finds are the poisoned kind. Under `--here` the workspace's
|
|
176
|
+
* `.chats/` sits at `<dir>/.chats` while the data dir is `<dir>/.paddock`, so a
|
|
177
|
+
* link planted by an affected build points somewhere no `dataDir` prefix match
|
|
178
|
+
* will ever catch. While it exists, every `claude` session the user starts in
|
|
179
|
+
* that directory writes into paddock's store instead of their own history, and
|
|
180
|
+
* anything that expects `~/.claude/projects/<enc>` to be a real directory
|
|
181
|
+
* (`unzip -d ~/.claude` being the observed one) fails with an error impossible
|
|
182
|
+
* to trace back here.
|
|
183
|
+
*
|
|
184
|
+
* Reported, never removed. Paddock not writing to `~/.claude` is the entire
|
|
185
|
+
* point of #620, and that has to include cleaning up after itself — a boot that
|
|
186
|
+
* silently deleted something under there would be the same class of surprise in
|
|
187
|
+
* the opposite direction. Naming the exact path is enough: `rm` is one command
|
|
188
|
+
* and the user can see what they are removing first.
|
|
189
|
+
*
|
|
190
|
+
* Never throws; an unreadable home just yields no findings.
|
|
191
|
+
*/
|
|
192
|
+
export declare function findPlantedChatsLinks(legacyHome: string, dataDir: string): Promise<PlantedChatsLink[]>;
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paddock's OWN Claude home (#620).
|
|
3
|
+
*
|
|
4
|
+
* Paddock already acts as the outer container for everything it owns: the
|
|
5
|
+
* projects tree, herdctl's entire state dir, the generated `herdctl.yaml`. Claude
|
|
6
|
+
* Code's transcripts were the one exception — they lived in the user's
|
|
7
|
+
* `~/.claude` and were reached by planting symlinks into it from outside. That
|
|
8
|
+
* was never a design choice. Claude Code resolves its home from
|
|
9
|
+
* `CLAUDE_CONFIG_DIR`, and until herdctl#423 nothing set it, so whatever home
|
|
10
|
+
* paddock configured the SDK still wrote to `~/.claude`. The symlink was the only
|
|
11
|
+
* lever there was.
|
|
12
|
+
*
|
|
13
|
+
* With `CLAUDE_CONFIG_DIR` now threaded (herdctl#423, `@herdctl/core@5.29.0`),
|
|
14
|
+
* paddock points Claude Code at `<dataDir>/claude-home` and `~/.claude` becomes a
|
|
15
|
+
* READ-ONLY source. This module owns everything that follows from that:
|
|
16
|
+
*
|
|
17
|
+
* - **{@link ensureClaudeHome}** — create the home, bridge the user-level config
|
|
18
|
+
* that lives in `~/.claude`, and report anything an operator needs to know.
|
|
19
|
+
* - **{@link mirrorLegacyTranscriptFolders}** — keep adoption (#588) able to see
|
|
20
|
+
* the user's own CLI history, which by definition is in `~/.claude`.
|
|
21
|
+
*
|
|
22
|
+
* ## The invariant
|
|
23
|
+
*
|
|
24
|
+
* **Paddock never moves, deletes or overwrites anything under `~/.claude`.**
|
|
25
|
+
*
|
|
26
|
+
* Stated precisely, because the bridge below does create symlinks POINTING at
|
|
27
|
+
* files in there, and Claude Code writing through one of those (refreshing an
|
|
28
|
+
* OAuth token, installing a plugin) does land in the user's home. That is Claude
|
|
29
|
+
* Code maintaining its own store in the place it has always kept it, which is
|
|
30
|
+
* what a user wants; it is not paddock relocating their data behind their back.
|
|
31
|
+
* The thing #620 exists to stop — `ensureProjectChats` copying a user's
|
|
32
|
+
* transcripts out and then `fs.rm`-ing the originals, unprompted, inside a bare
|
|
33
|
+
* `catch {}` — is gone, gated on `PaddockConfig.ownsClaudeHome`.
|
|
34
|
+
*/
|
|
35
|
+
import { promises as fs } from "node:fs";
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
import { encodePathForCli } from "@herdctl/core";
|
|
38
|
+
/**
|
|
39
|
+
* Entries of a Claude home that are USER-LEVEL configuration rather than
|
|
40
|
+
* per-instance state, symlinked from the user's home into paddock's own when
|
|
41
|
+
* paddock's does not already have its own.
|
|
42
|
+
*
|
|
43
|
+
* Without this, relocating the home silently changes agent behaviour: the user's
|
|
44
|
+
* `~/.claude/CLAUDE.md` is auto-loaded into every session, `settings.json`
|
|
45
|
+
* carries permissions and hooks, and `agents/` `commands/` `plugins/` are
|
|
46
|
+
* capability. Dropping them is not "a clean home", it is a behaviour regression
|
|
47
|
+
* nobody asked for.
|
|
48
|
+
*
|
|
49
|
+
* `.credentials.json` is the one that matters most: on Linux (and in the Docker
|
|
50
|
+
* image) it is where `claude login` puts the OAuth token, so without the bridge
|
|
51
|
+
* every file-authenticated install would come back logged out. Symlinked rather
|
|
52
|
+
* than copied — a copy would duplicate a live secret onto disk AND diverge the
|
|
53
|
+
* moment Claude Code refreshed the token.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately NOT bridged: `projects/` (that's the whole point), and everything
|
|
56
|
+
* that is per-instance runtime state — `todos/`, `shell-snapshots/`, `statsig/`,
|
|
57
|
+
* `sessions/`. Those are exactly what should now be per-instance.
|
|
58
|
+
*/
|
|
59
|
+
export const BRIDGED_ENTRIES = [
|
|
60
|
+
".credentials.json",
|
|
61
|
+
"settings.json",
|
|
62
|
+
"CLAUDE.md",
|
|
63
|
+
"agents",
|
|
64
|
+
"commands",
|
|
65
|
+
"plugins",
|
|
66
|
+
];
|
|
67
|
+
/** Env vars that authenticate Claude Code without any on-disk credential. */
|
|
68
|
+
const TOKEN_ENV_VARS = ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
69
|
+
/**
|
|
70
|
+
* Keychain service names Claude Code is known to store its login under.
|
|
71
|
+
*
|
|
72
|
+
* Only the unsuffixed form matters here: with `CLAUDE_CONFIG_DIR` set, Claude
|
|
73
|
+
* Code appends a path hash, and an entry under THAT name is one paddock's own
|
|
74
|
+
* home can already see. What we are looking for is the login made before paddock
|
|
75
|
+
* was involved.
|
|
76
|
+
*
|
|
77
|
+
* More than one candidate because the name is Claude Code's private detail, not
|
|
78
|
+
* a contract — see {@link probeMacosKeychainLogin} for why guessing wrong here
|
|
79
|
+
* is designed to be harmless.
|
|
80
|
+
*/
|
|
81
|
+
const KEYCHAIN_SERVICE_NAMES = ["Claude Code-credentials", "Claude Code"];
|
|
82
|
+
/**
|
|
83
|
+
* Ask the macOS Keychain whether a plain-name Claude Code login exists (#683).
|
|
84
|
+
*
|
|
85
|
+
* The whole value of this is turning "you appear to have no credentials" into
|
|
86
|
+
* "you ARE logged in; paddock cannot see it, and here is the one-liner" — a
|
|
87
|
+
* distinction paddock currently cannot make, and the difference between a
|
|
88
|
+
* message that describes a problem and one that fixes it.
|
|
89
|
+
*
|
|
90
|
+
* ## The asymmetry, which is deliberate
|
|
91
|
+
*
|
|
92
|
+
* A positive result is trustworthy: an entry under that service name is a login.
|
|
93
|
+
* A negative result is NOT — the service name is Claude Code's internal detail
|
|
94
|
+
* and can change, and `security` can fail for a dozen reasons that have nothing
|
|
95
|
+
* to do with the user. So a miss returns `unknown`, and the caller falls back to
|
|
96
|
+
* the existing, correct, generic warning. Guessing the name wrong therefore
|
|
97
|
+
* costs nothing beyond the improvement not happening.
|
|
98
|
+
*
|
|
99
|
+
* Never prompts and never reads a secret: `find-generic-password` WITHOUT `-w`
|
|
100
|
+
* reports existence only (attributes, which are discarded), so nothing sensitive
|
|
101
|
+
* reaches a log, a transcript, or this process's memory.
|
|
102
|
+
*/
|
|
103
|
+
export async function probeMacosKeychainLogin(platform = process.platform) {
|
|
104
|
+
if (platform !== "darwin")
|
|
105
|
+
return "unknown";
|
|
106
|
+
const { execFile } = await import("node:child_process");
|
|
107
|
+
const { promisify } = await import("node:util");
|
|
108
|
+
const run = promisify(execFile);
|
|
109
|
+
for (const service of KEYCHAIN_SERVICE_NAMES) {
|
|
110
|
+
// No `-w`: existence only. Output is discarded either way.
|
|
111
|
+
const found = await run("security", ["find-generic-password", "-s", service], {
|
|
112
|
+
timeout: 5_000,
|
|
113
|
+
}).then(() => true, () => false);
|
|
114
|
+
if (found)
|
|
115
|
+
return "found";
|
|
116
|
+
}
|
|
117
|
+
return "unknown";
|
|
118
|
+
}
|
|
119
|
+
/** Does `p` exist (following symlinks)? */
|
|
120
|
+
async function exists(p) {
|
|
121
|
+
return fs
|
|
122
|
+
.stat(p)
|
|
123
|
+
.then(() => true)
|
|
124
|
+
.catch(() => false);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Create paddock's Claude home and bridge the user-level config into it.
|
|
128
|
+
*
|
|
129
|
+
* Idempotent and non-clobbering: an entry paddock's home already has (whether a
|
|
130
|
+
* real file the user put there or a symlink from a previous boot) is left
|
|
131
|
+
* completely alone, so this is safe to run on every start.
|
|
132
|
+
*
|
|
133
|
+
* Never throws. A home that cannot be created is not a reason to refuse to boot
|
|
134
|
+
* — Claude Code falls back to its own default and chat still works, which is the
|
|
135
|
+
* same failure mode `ensureProjectChats` has always had.
|
|
136
|
+
*/
|
|
137
|
+
export async function ensureClaudeHome(cfg, env = process.env, probeKeychain = probeMacosKeychainLogin) {
|
|
138
|
+
const report = { bridged: [], notices: [] };
|
|
139
|
+
try {
|
|
140
|
+
await fs.mkdir(path.join(cfg.claudeHome, "projects"), { recursive: true });
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
report.notices.push({
|
|
144
|
+
level: "warn",
|
|
145
|
+
message: `could not create the Claude home at ${cfg.claudeHome} (${String(err)}) — ` +
|
|
146
|
+
`Claude Code will fall back to its own default and transcripts will not be ` +
|
|
147
|
+
`relocated into each project's .chats/`,
|
|
148
|
+
});
|
|
149
|
+
return report;
|
|
150
|
+
}
|
|
151
|
+
// Running in the user's own home is the pre-#620 layout: nothing to bridge
|
|
152
|
+
// (the files are already there) and nothing to warn about.
|
|
153
|
+
if (!cfg.ownsClaudeHome) {
|
|
154
|
+
report.notices.push({
|
|
155
|
+
level: "info",
|
|
156
|
+
message: `Claude home: ${cfg.claudeHome} (the user's own — paddock is not managing it)`,
|
|
157
|
+
});
|
|
158
|
+
return report;
|
|
159
|
+
}
|
|
160
|
+
for (const entry of BRIDGED_ENTRIES) {
|
|
161
|
+
const target = path.join(cfg.legacyClaudeHome, entry);
|
|
162
|
+
const link = path.join(cfg.claudeHome, entry);
|
|
163
|
+
// lstat, not stat: a bridge symlink planted on a previous boot whose target
|
|
164
|
+
// has since been removed still occupies the name, and re-linking it would be
|
|
165
|
+
// a no-op churn at best.
|
|
166
|
+
const occupied = await fs
|
|
167
|
+
.lstat(link)
|
|
168
|
+
.then(() => true)
|
|
169
|
+
.catch(() => false);
|
|
170
|
+
if (occupied)
|
|
171
|
+
continue;
|
|
172
|
+
if (!(await exists(target)))
|
|
173
|
+
continue;
|
|
174
|
+
try {
|
|
175
|
+
await fs.symlink(target, link);
|
|
176
|
+
report.bridged.push(entry);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
/* a race with another instance, or a read-only home — not fatal */
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
report.notices.push({
|
|
183
|
+
level: "info",
|
|
184
|
+
message: `Claude home: ${cfg.claudeHome} (paddock-owned; ~/.claude is read-only)` +
|
|
185
|
+
(report.bridged.length > 0 ? ` — bridged from ~/.claude: ${report.bridged.join(", ")}` : ""),
|
|
186
|
+
});
|
|
187
|
+
// The credential question (#620). Claude Code derives its secure-storage
|
|
188
|
+
// service name from whether `CLAUDE_CONFIG_DIR` is SET AT ALL — unset gets the
|
|
189
|
+
// plain name, set gets a path-hash suffix. Pointing paddock at its own home
|
|
190
|
+
// means it is always set, so a login held in the macOS Keychain under the plain
|
|
191
|
+
// name will not be found. A token in the environment is unaffected, and a
|
|
192
|
+
// file-based login is bridged above; if neither applies we cannot see the
|
|
193
|
+
// credential at all, so say so BEFORE the first turn fails with "Not logged in"
|
|
194
|
+
// rather than leaving the operator to work it out from a chat that never
|
|
195
|
+
// replies.
|
|
196
|
+
const hasTokenEnv = TOKEN_ENV_VARS.some((v) => (env[v] ?? "").trim().length > 0);
|
|
197
|
+
const hasCredentialFile = await exists(path.join(cfg.claudeHome, ".credentials.json"));
|
|
198
|
+
if (!hasTokenEnv && !hasCredentialFile) {
|
|
199
|
+
// #683: "you have no login" and "your login exists but I changed the service
|
|
200
|
+
// name it is filed under" are the same message today, and only one of them is
|
|
201
|
+
// the user's problem to solve. On darwin we can tell them apart, and a hit
|
|
202
|
+
// upgrades a paragraph of explanation into an instruction. A miss is
|
|
203
|
+
// inconclusive by design (see probeMacosKeychainLogin), so it falls through
|
|
204
|
+
// to the generic wording rather than asserting anything.
|
|
205
|
+
const keychain = await probeKeychain();
|
|
206
|
+
report.notices.push({
|
|
207
|
+
level: "warn",
|
|
208
|
+
message: keychain === "found"
|
|
209
|
+
? `you ARE logged in to Claude Code, but paddock cannot see it. The login is in the ` +
|
|
210
|
+
`macOS Keychain, and Claude Code files it under a service name derived from whether ` +
|
|
211
|
+
`CLAUDE_CONFIG_DIR is set — paddock sets it, to ${cfg.claudeHome}, so it looks under ` +
|
|
212
|
+
`a different name. A Keychain entry cannot be bridged the way ~/.claude files are. ` +
|
|
213
|
+
`Fix it either way: run \`CLAUDE_CONFIG_DIR=${cfg.claudeHome} claude login\` to log ` +
|
|
214
|
+
`in for this home too, or set CLAUDE_HOME=${cfg.legacyClaudeHome} (what \`paddock\` ` +
|
|
215
|
+
`does by default) to run against your own home and use the login you already have.`
|
|
216
|
+
: `no Claude credentials found for ${cfg.claudeHome}: no .credentials.json and no ` +
|
|
217
|
+
`${TOKEN_ENV_VARS.join("/")} in the environment. Claude Code scopes its credential ` +
|
|
218
|
+
`store to CLAUDE_CONFIG_DIR, so a login stored in the OS keychain against the ` +
|
|
219
|
+
`default home will NOT be found here. If turns fail with "Not logged in", either ` +
|
|
220
|
+
`re-run \`claude login\` with CLAUDE_CONFIG_DIR=${cfg.claudeHome}, or set ` +
|
|
221
|
+
`CLAUDE_HOME=${cfg.legacyClaudeHome} to keep using the previous layout.`,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return report;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The synthetic working directory a legacy transcript folder is offered to the
|
|
228
|
+
* engine under: the folder's own path inside the user's home.
|
|
229
|
+
*
|
|
230
|
+
* Any stable, per-folder, collision-free path would do. This one is the obvious
|
|
231
|
+
* choice because it already exists, is unique by construction, and is honest
|
|
232
|
+
* about where the transcripts actually are.
|
|
233
|
+
*/
|
|
234
|
+
export function legacySourcePath(legacyHome, folderName) {
|
|
235
|
+
return path.join(legacyHome, "projects", folderName);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Make the user's own transcript folders visible through paddock's Claude home,
|
|
239
|
+
* so adoption (#588) still finds them.
|
|
240
|
+
*
|
|
241
|
+
* Adoption's whole premise is "you already have terminal `claude` history for
|
|
242
|
+
* this directory" — and that history is, by definition, in `~/.claude`. But the
|
|
243
|
+
* engine resolves every adoption path against the ONE home the FleetManager was
|
|
244
|
+
* built with, with no per-call override, so once paddock owns its home the
|
|
245
|
+
* user's folders are out of reach. Without this, #620 would silently gut #588.
|
|
246
|
+
*
|
|
247
|
+
* The fix is a read-only view: symlink each of the user's folders into
|
|
248
|
+
* `<claudeHome>/projects/`. The engine then reads their transcripts through it,
|
|
249
|
+
* and adoption's default `copy` mode writes the copy into the project's own
|
|
250
|
+
* folder (itself the symlink to `.chats/`) — originals read, never touched.
|
|
251
|
+
*
|
|
252
|
+
* ## Why the mirror is not named after the recorded cwd
|
|
253
|
+
*
|
|
254
|
+
* The obvious mirror name — the same encoded name the user's folder has — is
|
|
255
|
+
* wrong, and wrong in exactly the case adoption exists for. A folder's name is
|
|
256
|
+
* `encodePathForCli(cwd)`, so the user's history for `/code/thing` and a paddock
|
|
257
|
+
* project whose working directory IS `/code/thing` want the SAME name in the
|
|
258
|
+
* same directory. Paddock's `.chats/` symlink is already there and must stay, so
|
|
259
|
+
* the user's folder loses — and "I pointed a project at a directory I already
|
|
260
|
+
* have `claude` history for", the headline case, silently offers nothing.
|
|
261
|
+
*
|
|
262
|
+
* (Pre-#620 this case worked only because `ensureProjectChats` had already MOVED
|
|
263
|
+
* those transcripts into `.chats/` and deleted the originals. That destruction is
|
|
264
|
+
* the thing #620 removes, so the case now needs a real mechanism.)
|
|
265
|
+
*
|
|
266
|
+
* So each mirror is named for a SYNTHETIC working directory
|
|
267
|
+
* ({@link legacySourcePath}) that no agent will ever have, which makes
|
|
268
|
+
* collisions structurally impossible rather than merely unlikely. The caller
|
|
269
|
+
* keeps the folder's real recorded cwd for display and hands `engineCwd` to the
|
|
270
|
+
* engine — see `AdoptableSource.importFrom`.
|
|
271
|
+
*
|
|
272
|
+
* Entries in the legacy home that are themselves SYMLINKS are skipped: those are
|
|
273
|
+
* ones paddock planted under the old layout, pointing back at some `.chats/`, and
|
|
274
|
+
* mirroring them would re-offer paddock's own chats as "adoptable". That also
|
|
275
|
+
* makes the leftovers from before this change inert rather than confusing.
|
|
276
|
+
*
|
|
277
|
+
* Idempotent and never throws. Returns every mirror it maintains, not only the
|
|
278
|
+
* ones planted this call, so a caller can rely on the mapping after any call.
|
|
279
|
+
*/
|
|
280
|
+
export async function mirrorLegacyTranscriptFolders(claudeHome, legacyHome) {
|
|
281
|
+
if (path.resolve(claudeHome) === path.resolve(legacyHome))
|
|
282
|
+
return [];
|
|
283
|
+
const from = path.join(legacyHome, "projects");
|
|
284
|
+
const to = path.join(claudeHome, "projects");
|
|
285
|
+
const mirrors = [];
|
|
286
|
+
try {
|
|
287
|
+
const names = await fs.readdir(from).catch(() => []);
|
|
288
|
+
if (names.length === 0)
|
|
289
|
+
return [];
|
|
290
|
+
await fs.mkdir(to, { recursive: true });
|
|
291
|
+
for (const folderName of names) {
|
|
292
|
+
const legacyDir = path.join(from, folderName);
|
|
293
|
+
const st = await fs.lstat(legacyDir).catch(() => null);
|
|
294
|
+
if (!st?.isDirectory())
|
|
295
|
+
continue; // skips files AND paddock's old symlinks
|
|
296
|
+
const engineCwd = legacySourcePath(legacyHome, folderName);
|
|
297
|
+
// encodePathForCli, not a local encoder: this name must be byte-identical
|
|
298
|
+
// to what the engine derives from `engineCwd`, including the truncate+hash
|
|
299
|
+
// branch it takes past 200 chars.
|
|
300
|
+
const name = encodePathForCli(engineCwd);
|
|
301
|
+
const link = path.join(to, name);
|
|
302
|
+
const occupied = await fs
|
|
303
|
+
.lstat(link)
|
|
304
|
+
.then(() => true)
|
|
305
|
+
.catch(() => false);
|
|
306
|
+
if (!occupied) {
|
|
307
|
+
const ok = await fs.symlink(legacyDir, link).then(() => true, () => false);
|
|
308
|
+
if (!ok)
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
mirrors.push({ name, engineCwd, legacyDir });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
/* non-fatal: adoption just sees fewer sources */
|
|
316
|
+
}
|
|
317
|
+
return mirrors;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Count the transcript symlinks a paddock instance planted in the user's home
|
|
321
|
+
* under the pre-#620 layout that point back into THIS data dir.
|
|
322
|
+
*
|
|
323
|
+
* Purely informational. They are harmless — they still resolve, and nothing
|
|
324
|
+
* reads them any more — but they accumulate one per project per instance and go
|
|
325
|
+
* stale as soon as a data dir is deleted, so an operator is better off being
|
|
326
|
+
* told they exist than discovering a pile of dangling links months later.
|
|
327
|
+
* Paddock does not remove them itself: they live in `~/.claude`, and not writing
|
|
328
|
+
* there is the entire point.
|
|
329
|
+
*/
|
|
330
|
+
export async function countLegacyTranscriptLinks(legacyHome, dataDir) {
|
|
331
|
+
const root = path.join(legacyHome, "projects");
|
|
332
|
+
const names = await fs.readdir(root).catch(() => []);
|
|
333
|
+
const prefix = path.resolve(dataDir) + path.sep;
|
|
334
|
+
let count = 0;
|
|
335
|
+
for (const name of names) {
|
|
336
|
+
const target = await fs.readlink(path.join(root, name)).catch(() => null);
|
|
337
|
+
if (target !== null && path.resolve(target).startsWith(prefix))
|
|
338
|
+
count++;
|
|
339
|
+
}
|
|
340
|
+
return count;
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Find transcript symlinks planted in the user's home that point at a `.chats/`
|
|
344
|
+
* store OUTSIDE `dataDir` (#682).
|
|
345
|
+
*
|
|
346
|
+
* The instance's own leftovers — links into this data dir — are already reported
|
|
347
|
+
* by {@link countLegacyTranscriptLinks} as harmless pre-#620 residue, and they
|
|
348
|
+
* are: they were planted for a project whose transcripts paddock does own.
|
|
349
|
+
*
|
|
350
|
+
* The ones this finds are the poisoned kind. Under `--here` the workspace's
|
|
351
|
+
* `.chats/` sits at `<dir>/.chats` while the data dir is `<dir>/.paddock`, so a
|
|
352
|
+
* link planted by an affected build points somewhere no `dataDir` prefix match
|
|
353
|
+
* will ever catch. While it exists, every `claude` session the user starts in
|
|
354
|
+
* that directory writes into paddock's store instead of their own history, and
|
|
355
|
+
* anything that expects `~/.claude/projects/<enc>` to be a real directory
|
|
356
|
+
* (`unzip -d ~/.claude` being the observed one) fails with an error impossible
|
|
357
|
+
* to trace back here.
|
|
358
|
+
*
|
|
359
|
+
* Reported, never removed. Paddock not writing to `~/.claude` is the entire
|
|
360
|
+
* point of #620, and that has to include cleaning up after itself — a boot that
|
|
361
|
+
* silently deleted something under there would be the same class of surprise in
|
|
362
|
+
* the opposite direction. Naming the exact path is enough: `rm` is one command
|
|
363
|
+
* and the user can see what they are removing first.
|
|
364
|
+
*
|
|
365
|
+
* Never throws; an unreadable home just yields no findings.
|
|
366
|
+
*/
|
|
367
|
+
export async function findPlantedChatsLinks(legacyHome, dataDir) {
|
|
368
|
+
const root = path.join(legacyHome, "projects");
|
|
369
|
+
const names = await fs.readdir(root).catch(() => []);
|
|
370
|
+
const ownPrefix = path.resolve(dataDir) + path.sep;
|
|
371
|
+
const found = [];
|
|
372
|
+
for (const name of names) {
|
|
373
|
+
const link = path.join(root, name);
|
|
374
|
+
const raw = await fs.readlink(link).catch(() => null);
|
|
375
|
+
if (raw === null)
|
|
376
|
+
continue;
|
|
377
|
+
const target = path.resolve(path.dirname(link), raw);
|
|
378
|
+
// `.chats` is paddock's store name and nothing else's — a precise enough
|
|
379
|
+
// signature that a link the user made themselves won't be misreported.
|
|
380
|
+
if (path.basename(target) !== ".chats")
|
|
381
|
+
continue;
|
|
382
|
+
if (target.startsWith(ownPrefix))
|
|
383
|
+
continue; // this instance's own residue
|
|
384
|
+
found.push({ link, target, dangling: !(await exists(target)) });
|
|
385
|
+
}
|
|
386
|
+
return found;
|
|
387
|
+
}
|
|
@@ -32,6 +32,14 @@ export interface CliOptions {
|
|
|
32
32
|
verbose: boolean;
|
|
33
33
|
help: boolean;
|
|
34
34
|
version: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Keep paddock's own Claude home instead of inheriting the user's (#683).
|
|
37
|
+
*
|
|
38
|
+
* The CLI defaults to running against `~/.claude` when there is no token and
|
|
39
|
+
* no explicit home, because that is the only way a macOS Keychain login is
|
|
40
|
+
* visible at all. This is the opt-out — see `cli/claude-home-choice.ts`.
|
|
41
|
+
*/
|
|
42
|
+
isolatedClaudeHome: boolean;
|
|
35
43
|
}
|
|
36
44
|
/** A usage error. Thrown rather than exiting, so parsing stays testable. */
|
|
37
45
|
export declare class CliError extends Error {
|
|
@@ -53,4 +61,4 @@ export declare function nodeVersionProblem(nodeVersion: string): string | undefi
|
|
|
53
61
|
* flag that fixes it.
|
|
54
62
|
*/
|
|
55
63
|
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 4000, 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
|
|
64
|
+
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 4000, 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 --isolated-claude-home\n Use Paddock's own Claude home instead of ~/.claude\n (see Credentials below)\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. With no\n CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment, it runs\n against your own ~/.claude and uses the Claude Code login already there \u2014\n including one held in the macOS Keychain, which cannot be shared any other\n way. Paddock creates nothing under ~/.claude; your transcripts just stay\n there rather than being relocated into .chats/, and existing sessions are\n still offered for import rather than opened for you.\n\n Pass --isolated-claude-home (or set CLAUDE_HOME) for a Claude home of its\n own; then a login must be a token in the environment, a ~/.claude\n .credentials.json, or `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`.\n With no login at all anywhere, run `claude setup-token`.\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";
|
|
@@ -34,6 +34,7 @@ export function parseArgs(argv) {
|
|
|
34
34
|
verbose: false,
|
|
35
35
|
help: false,
|
|
36
36
|
version: false,
|
|
37
|
+
isolatedClaudeHome: false,
|
|
37
38
|
};
|
|
38
39
|
for (let i = 0; i < argv.length; i++) {
|
|
39
40
|
const arg = argv[i];
|
|
@@ -65,6 +66,9 @@ export function parseArgs(argv) {
|
|
|
65
66
|
case "--verbose":
|
|
66
67
|
opts.verbose = true;
|
|
67
68
|
break;
|
|
69
|
+
case "--isolated-claude-home":
|
|
70
|
+
opts.isolatedClaudeHome = true;
|
|
71
|
+
break;
|
|
68
72
|
case "-h":
|
|
69
73
|
case "--help":
|
|
70
74
|
opts.help = true;
|
|
@@ -124,6 +128,9 @@ Options
|
|
|
124
128
|
--here Open the CURRENT directory as the workspace (see below)
|
|
125
129
|
-o, --open Open the app in your browser once it is listening
|
|
126
130
|
--verbose Show the server's own logs (quiet by default)
|
|
131
|
+
--isolated-claude-home
|
|
132
|
+
Use Paddock's own Claude home instead of ~/.claude
|
|
133
|
+
(see Credentials below)
|
|
127
134
|
-v, --version Print the Paddock version and exit
|
|
128
135
|
-h, --help Show this help
|
|
129
136
|
|
|
@@ -134,16 +141,25 @@ Opening a directory (--here)
|
|
|
134
141
|
|
|
135
142
|
· creates .paddock/ in the directory for this workspace's own state
|
|
136
143
|
· creates .chats/ for transcripts, and adds both to .gitignore
|
|
137
|
-
·
|
|
144
|
+
· offers your ~/.claude sessions for this directory for import — nothing
|
|
145
|
+
there is moved, copied or linked until you confirm
|
|
138
146
|
|
|
139
147
|
Once done, later runs in the same directory resume it — no flag needed.
|
|
140
148
|
Without --here, Paddock never touches the directory you ran it from.
|
|
141
149
|
|
|
142
150
|
Credentials
|
|
143
|
-
Paddock drives Claude Code, so it needs Claude credentials
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
Paddock drives Claude Code, so it needs Claude credentials. With no
|
|
152
|
+
CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment, it runs
|
|
153
|
+
against your own ~/.claude and uses the Claude Code login already there —
|
|
154
|
+
including one held in the macOS Keychain, which cannot be shared any other
|
|
155
|
+
way. Paddock creates nothing under ~/.claude; your transcripts just stay
|
|
156
|
+
there rather than being relocated into .chats/, and existing sessions are
|
|
157
|
+
still offered for import rather than opened for you.
|
|
158
|
+
|
|
159
|
+
Pass --isolated-claude-home (or set CLAUDE_HOME) for a Claude home of its
|
|
160
|
+
own; then a login must be a token in the environment, a ~/.claude
|
|
161
|
+
.credentials.json, or \`CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login\`.
|
|
162
|
+
With no login at all anywhere, run \`claude setup-token\`.
|
|
147
163
|
|
|
148
164
|
Your data
|
|
149
165
|
Everything lives in one directory — ~/.paddock unless you pass --data-dir.
|