@edspencer/paddock 0.61.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/README.md +15 -4
- package/package.json +1 -1
- 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 +22 -1
- package/packages/server/dist/claude-home.d.ts +68 -1
- package/packages/server/dist/claude-home.js +118 -7
- 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/paddock.js +51 -8
- package/packages/server/dist/sweep.js +13 -1
- package/packages/server/dist/transcripts.d.ts +15 -8
- package/packages/server/dist/transcripts.js +18 -0
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CzMqTdUU.js → InstanceConfigPage-BgNXqAox.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-YSEZHWDe.js → MarkdownRenderer-DfD_UtqU.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DLWxZbqK.js → ProjectRedirect-1EXdHXmO.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BFX8ff2n.js → ProjectView-DHb5vNUp.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-DLOeY_ww.js → ProjectsGrid-Br-EhLZE.js} +1 -1
- package/packages/web/dist/assets/{arc-BwC1ik-b.js → arc-C2Jekujs.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CsVvXQWw.js → architectureDiagram-3BPJPVTR-CflCA-em.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DXQk2bIa.js → blockDiagram-GPEHLZMM-Ds7GLNnC.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DXiA26It.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-Dt5nC02A.js → chunk-2J33WTMH-tQunO_M5.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DcjnWvl6.js → chunk-4BX2VUAB-DT5dOWd2.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CRa3LkU4.js → chunk-55IACEB6-CfTUqUEF.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-H3V-11qu.js → chunk-727SXJPM-sK5bOAA0.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DPCrENYV.js → chunk-AQP2D5EJ-DcMgVe-P.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-C4UO0ZTc.js → chunk-FMBD7UC4-In3h_-7v.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-BtI37CnI.js → chunk-ND2GUHAM-DuaIeRaC.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-C5bTaK5j.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-DB3cpm5N.js → core-DgOgLvnM.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-q-cjTZO4.js → cose-bilkent-S5V4N54A-CyO-rznJ.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DYEV5HFo.js → dagre-BM42HDAG-B_9Ihzgn.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-D16XCw78.js → diagram-2AECGRRQ-z5mvZ5YT.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-CeztryYE.js → diagram-5GNKFQAL-Bsz0cgfn.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D5SLIMDS.js → diagram-KO2AKTUF-B29k5f4q.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-Bw-sVPxM.js → diagram-LMA3HP47-CFrwsHoI.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DsRMITgx.js → diagram-OG6HWLK6-D9Sjc2HR.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-T8Qqc_ku.js → erDiagram-TEJ5UH35-C3vbkD7M.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Cyx47UWU.js → flowDiagram-I6XJVG4X-DkMIgntd.js} +1 -1
- package/packages/web/dist/assets/{format-D2I0duZa.js → format-BFDqyFuw.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DxYJmG9o.js → ganttDiagram-6RSMTGT7-nvHanjpW.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-COK5YOTD.js → gitGraphDiagram-PVQCEYII-DbCNfOu7.js} +1 -1
- package/packages/web/dist/assets/{index-wBXQMkGI.js → index-DGCs1YIO.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BamRN_Nn.js → infoDiagram-5YYISTIA-aJjH9x3D.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DuOHdpEH.js → ishikawaDiagram-YF4QCWOH-DoifwNnZ.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Bri7GKTW.js → journeyDiagram-JHISSGLW-Cz9kG9HN.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BdkgUnzI.js → kanban-definition-UN3LZRKU-BoXnfr7A.js} +1 -1
- package/packages/web/dist/assets/{linear-DOcNPCpB.js → linear-BlTTO9yR.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-D9SbVBpN.js → mermaid.core-YAG8eSPQ.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CVmLm7zn.js → mindmap-definition-RKZ34NQL-DtUBKfkr.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-D0N10eVU.js → pieDiagram-4H26LBE5-B3Rwpmy0.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B5gnYdTa.js → quadrantDiagram-W4KKPZXB-B8eJtTyk.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CwtxezNZ.js → requirementDiagram-4Y6WPE33-CpAluiY9.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-D79O3cux.js → sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-xhKcI6tI.js → sequenceDiagram-3UESZ5HK-CRpqKSGm.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DBYMAjcX.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-e-MYF4lm.js → timeline-definition-PNZ67QCA-PVr5V_pG.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-B8UsvRDM.js → vennDiagram-CIIHVFJN-De-4NPAX.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-1ucACjTl.js → wardley-L42UT6IY-Dtv10D2d.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DJufLd_u.js → wardleyDiagram-YWT4CUSO-p2fJuQ6C.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-qqaZNlu9.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/channel-DDgTXrGP.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU1kd_Vu.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU1kd_Vu.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CZtr9Jvq.js +0 -2
package/README.md
CHANGED
|
@@ -26,8 +26,11 @@ claude setup-token # Claude Max/Pro
|
|
|
26
26
|
export ANTHROPIC_API_KEY=sk-ant-… # or API billing
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
If you already use Claude Code on this machine,
|
|
30
|
-
|
|
29
|
+
If you already use Claude Code on this machine, there is nothing to do: with no token
|
|
30
|
+
in the environment, Paddock runs against your own `~/.claude` and uses the login
|
|
31
|
+
already there — including one held in the **macOS Keychain**, which cannot be shared
|
|
32
|
+
any other way. Pass `--isolated-claude-home` to give Paddock a Claude home of its own
|
|
33
|
+
instead.
|
|
31
34
|
|
|
32
35
|
## Open your own project
|
|
33
36
|
|
|
@@ -45,8 +48,14 @@ The flag is the consent, so here is everything it does:
|
|
|
45
48
|
|
|
46
49
|
- creates **`.paddock/`** for the workspace's state and **`.chats/`** for transcripts
|
|
47
50
|
- appends those two entries to **`.gitignore`**
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
|
|
52
|
+
**Nothing is written into your `~/.claude`, ever** — no file, no symlink, in any
|
|
53
|
+
configuration. Sessions found there are *offered* for import: nothing is moved, copied
|
|
54
|
+
or linked until you confirm, and your terminal `claude` keeps working exactly as
|
|
55
|
+
before. What varies is which Claude home Paddock runs against, and it says which on
|
|
56
|
+
startup — with a token in the environment (or `--isolated-claude-home`) it uses its own
|
|
57
|
+
under `.paddock/` and relocates transcripts into `.chats/`; without one it uses your
|
|
58
|
+
`~/.claude`, and transcripts stay there instead.
|
|
50
59
|
|
|
51
60
|
To undo it: `rm -rf .paddock .chats` and drop the two `.gitignore` lines.
|
|
52
61
|
|
|
@@ -61,6 +70,8 @@ Without `--here`, Paddock never touches the directory you ran it from.
|
|
|
61
70
|
--here Open the CURRENT directory as the workspace
|
|
62
71
|
-o, --open Open the app in your browser once it is listening
|
|
63
72
|
--verbose Show the server's own logs (quiet by default)
|
|
73
|
+
--isolated-claude-home
|
|
74
|
+
Use Paddock's own Claude home instead of ~/.claude
|
|
64
75
|
-v, --version Print the version
|
|
65
76
|
-h, --help Show help
|
|
66
77
|
```
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one-line, actionable reason this failed, if it is one we recognise.
|
|
3
|
+
*
|
|
4
|
+
* `undefined` means "no idea" — and an unknown failure keeps its full detail,
|
|
5
|
+
* because that is exactly the case where somebody needs it.
|
|
6
|
+
*/
|
|
7
|
+
export declare function classifyAgentError(err: unknown): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* The same error with the reconstructed `claude` argv cut out.
|
|
10
|
+
*
|
|
11
|
+
* execa's message quotes the entire command it ran, which for the sweeper is
|
|
12
|
+
* ~2 KB of system prompt followed by the full `--disallowedTools` list. It is
|
|
13
|
+
* noise to an end user and it buries the signal; it also means the curator
|
|
14
|
+
* prompt is copied into any log or paste of the failure. Everything before the
|
|
15
|
+
* argv (`Command failed with exit code 1`) is kept, because the exit status is
|
|
16
|
+
* the part that is actually diagnostic.
|
|
17
|
+
*
|
|
18
|
+
* Full text is still one env var away — see {@link installHerdctlLogBridge}.
|
|
19
|
+
*/
|
|
20
|
+
export declare function summariseAgentError(message: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Route `@herdctl/core`'s logging through a handler that shapes agent failures.
|
|
23
|
+
*
|
|
24
|
+
* Two behaviours, and they are separate on purpose:
|
|
25
|
+
*
|
|
26
|
+
* - **Always** summarise the argv. The system prompt in a `claude -p` command
|
|
27
|
+
* line is never what somebody is reading the log for, on a laptop or a
|
|
28
|
+
* server. `HERDCTL_LOG_LEVEL=debug` turns it back on — at debug you have
|
|
29
|
+
* asked for everything, and the argv is genuinely useful there.
|
|
30
|
+
* - **In quiet mode** (the CLI's default), collapse a recognised failure to its
|
|
31
|
+
* cause. `LOG_LEVEL=warn` was never enough for this: core logs these at
|
|
32
|
+
* `error`, which passes every level filter there is, and one of them is a
|
|
33
|
+
* bare `console.error`. Quiet-by-default meant "quiet when nothing goes
|
|
34
|
+
* wrong", which is the easy half.
|
|
35
|
+
*
|
|
36
|
+
* Idempotent; installing again replaces the handler. Call `setLogHandler(null)`
|
|
37
|
+
* to restore core's own output.
|
|
38
|
+
*/
|
|
39
|
+
export declare function installHerdctlLogBridge(options: {
|
|
40
|
+
quiet: boolean;
|
|
41
|
+
}): void;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making a failed agent run readable (#684).
|
|
3
|
+
*
|
|
4
|
+
* A credential-less `npx @edspencer/paddock --here` — the path the README, the
|
|
5
|
+
* docs site and the front page all point new users at — greeted them with a
|
|
6
|
+
* multi-screen stack trace containing the entire sweeper system prompt, four
|
|
7
|
+
* times over: once in the `ExecaError`, once in the `[fleet-manager]` line, once
|
|
8
|
+
* in the serialised `err.message` and once inside `err.stack`. The one useful
|
|
9
|
+
* string, `Not logged in · Please run /login`, was on line 40 of the dump. The
|
|
10
|
+
* server was fine and still serving; nothing about the output said so.
|
|
11
|
+
*
|
|
12
|
+
* Three separate things produce that wall, and only one of them is ours:
|
|
13
|
+
*
|
|
14
|
+
* 1. `sweep.ts` logging `{ err }` at pino level 50 — ours, fixed there.
|
|
15
|
+
* 2. `[CLIRuntime] Process failed: …` — `@herdctl/core`.
|
|
16
|
+
* 3. `[fleet-manager] Job … error: …` — `@herdctl/core`.
|
|
17
|
+
*
|
|
18
|
+
* We cannot edit core from here, but we do not have to: `setLogHandler` is an
|
|
19
|
+
* exported hook that replaces the `console.*` output of EVERY `createLogger` in
|
|
20
|
+
* the engine. {@link installHerdctlLogBridge} uses it to shape both of core's
|
|
21
|
+
* lines on the way past. That is a better fix than patching core anyway — it
|
|
22
|
+
* keeps the decision about how much noise a paddock instance emits inside
|
|
23
|
+
* paddock, where the CLI's quiet mode lives.
|
|
24
|
+
*/
|
|
25
|
+
import { setLogHandler, getLogLevel, LOG_LEVEL_ORDER } from "@herdctl/core";
|
|
26
|
+
/**
|
|
27
|
+
* Failures that are USER STATE, not exceptions — a known cause with a known
|
|
28
|
+
* remedy, where a stack trace tells the reader nothing they can act on.
|
|
29
|
+
*
|
|
30
|
+
* Matched against the whole error text because the signal arrives in the failed
|
|
31
|
+
* command's own output, not in the message execa constructs.
|
|
32
|
+
*/
|
|
33
|
+
const KNOWN_CAUSES = [
|
|
34
|
+
{
|
|
35
|
+
pattern: /Not logged in|Please run \/login|Invalid API key|OAuth token has expired/i,
|
|
36
|
+
cause: "Claude Code is not logged in — run `claude setup-token`, " +
|
|
37
|
+
"or set CLAUDE_CODE_OAUTH_TOKEN",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
pattern: /Credit balance is too low|insufficient[_ ]quota/i,
|
|
41
|
+
cause: "the Claude account is out of credit",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// Anchored to `claude` specifically. A bare /ENOENT/ would claim a missing
|
|
45
|
+
// CLI for any file the engine failed to open, which is a confident wrong
|
|
46
|
+
// answer — worse than the raw error it replaces.
|
|
47
|
+
pattern: /ENOENT[^\n]{0,40}claude|claude: (command )?not found/i,
|
|
48
|
+
cause: "the `claude` CLI is not on PATH (needed by the sweeper and triggers)",
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
/**
|
|
52
|
+
* The one-line, actionable reason this failed, if it is one we recognise.
|
|
53
|
+
*
|
|
54
|
+
* `undefined` means "no idea" — and an unknown failure keeps its full detail,
|
|
55
|
+
* because that is exactly the case where somebody needs it.
|
|
56
|
+
*/
|
|
57
|
+
export function classifyAgentError(err) {
|
|
58
|
+
const text = agentErrorText(err);
|
|
59
|
+
return KNOWN_CAUSES.find((k) => k.pattern.test(text))?.cause;
|
|
60
|
+
}
|
|
61
|
+
/** Everything an error carries as text — message, and stdio execa attached. */
|
|
62
|
+
function agentErrorText(err) {
|
|
63
|
+
if (typeof err === "string")
|
|
64
|
+
return err;
|
|
65
|
+
const e = err;
|
|
66
|
+
return [e?.message, e?.stdout, e?.stderr].filter((v) => typeof v === "string").join("\n");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The same error with the reconstructed `claude` argv cut out.
|
|
70
|
+
*
|
|
71
|
+
* execa's message quotes the entire command it ran, which for the sweeper is
|
|
72
|
+
* ~2 KB of system prompt followed by the full `--disallowedTools` list. It is
|
|
73
|
+
* noise to an end user and it buries the signal; it also means the curator
|
|
74
|
+
* prompt is copied into any log or paste of the failure. Everything before the
|
|
75
|
+
* argv (`Command failed with exit code 1`) is kept, because the exit status is
|
|
76
|
+
* the part that is actually diagnostic.
|
|
77
|
+
*
|
|
78
|
+
* Full text is still one env var away — see {@link installHerdctlLogBridge}.
|
|
79
|
+
*/
|
|
80
|
+
export function summariseAgentError(message) {
|
|
81
|
+
return message.replace(
|
|
82
|
+
// Up to and including "claude", then a flag, then everything after it.
|
|
83
|
+
/(:\s*)claude(\.exe)?\s+-{1,2}[\s\S]*/, "$1claude … (argv omitted — set HERDCTL_LOG_LEVEL=debug for the full command)");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Route `@herdctl/core`'s logging through a handler that shapes agent failures.
|
|
87
|
+
*
|
|
88
|
+
* Two behaviours, and they are separate on purpose:
|
|
89
|
+
*
|
|
90
|
+
* - **Always** summarise the argv. The system prompt in a `claude -p` command
|
|
91
|
+
* line is never what somebody is reading the log for, on a laptop or a
|
|
92
|
+
* server. `HERDCTL_LOG_LEVEL=debug` turns it back on — at debug you have
|
|
93
|
+
* asked for everything, and the argv is genuinely useful there.
|
|
94
|
+
* - **In quiet mode** (the CLI's default), collapse a recognised failure to its
|
|
95
|
+
* cause. `LOG_LEVEL=warn` was never enough for this: core logs these at
|
|
96
|
+
* `error`, which passes every level filter there is, and one of them is a
|
|
97
|
+
* bare `console.error`. Quiet-by-default meant "quiet when nothing goes
|
|
98
|
+
* wrong", which is the easy half.
|
|
99
|
+
*
|
|
100
|
+
* Idempotent; installing again replaces the handler. Call `setLogHandler(null)`
|
|
101
|
+
* to restore core's own output.
|
|
102
|
+
*/
|
|
103
|
+
export function installHerdctlLogBridge(options) {
|
|
104
|
+
const debug = LOG_LEVEL_ORDER[getLogLevel()] <= LOG_LEVEL_ORDER.debug;
|
|
105
|
+
setLogHandler((level, prefix, message, data) => {
|
|
106
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[getLogLevel()])
|
|
107
|
+
return;
|
|
108
|
+
let text = debug ? message : summariseAgentError(message);
|
|
109
|
+
if (options.quiet && level === "error") {
|
|
110
|
+
const cause = classifyAgentError(message);
|
|
111
|
+
// A known cause replaces the message rather than annotating it: the
|
|
112
|
+
// reader does not need the exit code as well as the reason, and the
|
|
113
|
+
// failure is non-fatal — the server is still up and still serving.
|
|
114
|
+
if (cause !== undefined)
|
|
115
|
+
text = `${cause} — run with --verbose for the full error`;
|
|
116
|
+
}
|
|
117
|
+
const line = data === undefined ? `[${prefix}] ${text}` : `[${prefix}] ${text} ${JSON.stringify(data)}`;
|
|
118
|
+
if (level === "error")
|
|
119
|
+
console.error(line);
|
|
120
|
+
else if (level === "warn")
|
|
121
|
+
console.warn(line);
|
|
122
|
+
else if (level === "info")
|
|
123
|
+
console.info(line);
|
|
124
|
+
else
|
|
125
|
+
console.debug(line);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
@@ -19,7 +19,8 @@ import { createRequire } from "node:module";
|
|
|
19
19
|
import { promises as fs } from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { loadPaddockConfig } from "./config.js";
|
|
22
|
-
import { ensureClaudeHome, countLegacyTranscriptLinks } from "./claude-home.js";
|
|
22
|
+
import { ensureClaudeHome, countLegacyTranscriptLinks, findPlantedChatsLinks, } from "./claude-home.js";
|
|
23
|
+
import { installHerdctlLogBridge } from "./agent-errors.js";
|
|
23
24
|
import { ProjectStore, ROOT_KEY } from "./projects.js";
|
|
24
25
|
import { AttachmentStore } from "./attachments.js";
|
|
25
26
|
import { HerdctlService } from "./herdctl.js";
|
|
@@ -74,6 +75,11 @@ function hasFileExtension(pathname) {
|
|
|
74
75
|
*/
|
|
75
76
|
export async function buildApp(opts = {}) {
|
|
76
77
|
const cfg = opts.config ?? loadPaddockConfig();
|
|
78
|
+
// Shape the engine's own logging before anything can start a job (#684). Two
|
|
79
|
+
// of the three sources of the credential-failure stack-trace wall are inside
|
|
80
|
+
// `@herdctl/core`, and `setLogHandler` is the supported way to reach them from
|
|
81
|
+
// out here. `PADDOCK_QUIET` is set by `cli/paddock.ts` unless `--verbose`.
|
|
82
|
+
installHerdctlLogBridge({ quiet: (process.env.PADDOCK_QUIET ?? "") !== "" });
|
|
77
83
|
const app = Fastify({
|
|
78
84
|
logger: { level: cfg.logLevel },
|
|
79
85
|
});
|
|
@@ -165,6 +171,21 @@ export async function buildApp(opts = {}) {
|
|
|
165
171
|
`paddock leaves them alone because it does not write to ~/.claude. Safe to delete.`);
|
|
166
172
|
}
|
|
167
173
|
}
|
|
174
|
+
// #682: links an affected build planted at a path the user had not used yet.
|
|
175
|
+
// Unlike the residue above these are still LIVE — they redirect the user's own
|
|
176
|
+
// future `claude` sessions — so they warn, and name every path. Paddock does
|
|
177
|
+
// not remove them; it does not write to `~/.claude`.
|
|
178
|
+
const poisoned = await findPlantedChatsLinks(cfg.legacyClaudeHome, cfg.dataDir);
|
|
179
|
+
if (poisoned.length > 0) {
|
|
180
|
+
app.log.warn(`${poisoned.length} symlink(s) in ${path.join(cfg.legacyClaudeHome, "projects")} redirect ` +
|
|
181
|
+
`Claude Code's transcripts into a paddock .chats/ store (#682). While they exist, ` +
|
|
182
|
+
`\`claude\` sessions you start in those directories are written to paddock's store, and ` +
|
|
183
|
+
`deleting that store destroys them. Paddock does not remove anything under ~/.claude — ` +
|
|
184
|
+
`delete them yourself if you did not intend this:\n` +
|
|
185
|
+
poisoned
|
|
186
|
+
.map((p) => ` ${p.link} -> ${p.target}${p.dangling ? " (target is missing)" : ""}`)
|
|
187
|
+
.join("\n"));
|
|
188
|
+
}
|
|
168
189
|
const rootWorkspace = await projects.get(ROOT_KEY);
|
|
169
190
|
const initialProjects = [...(await projects.list()), rootWorkspace];
|
|
170
191
|
try {
|
|
@@ -21,6 +21,38 @@ import type { PaddockConfig } from "./config.js";
|
|
|
21
21
|
* `sessions/`. Those are exactly what should now be per-instance.
|
|
22
22
|
*/
|
|
23
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>;
|
|
24
56
|
/** A message for the operator, at the level it should be logged at. */
|
|
25
57
|
export interface ClaudeHomeNotice {
|
|
26
58
|
level: "info" | "warn";
|
|
@@ -43,7 +75,7 @@ export interface ClaudeHomeReport {
|
|
|
43
75
|
* — Claude Code falls back to its own default and chat still works, which is the
|
|
44
76
|
* same failure mode `ensureProjectChats` has always had.
|
|
45
77
|
*/
|
|
46
|
-
export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "ownsClaudeHome">, env?: NodeJS.ProcessEnv): Promise<ClaudeHomeReport>;
|
|
78
|
+
export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "ownsClaudeHome">, env?: NodeJS.ProcessEnv, probeKeychain?: KeychainProbe): Promise<ClaudeHomeReport>;
|
|
47
79
|
/** One of the user's transcript folders, made readable through paddock's home. */
|
|
48
80
|
export interface LegacyMirror {
|
|
49
81
|
/** Folder name inside `<claudeHome>/projects/` — `encodePathForCli(engineCwd)`. */
|
|
@@ -123,3 +155,38 @@ export declare function mirrorLegacyTranscriptFolders(claudeHome: string, legacy
|
|
|
123
155
|
* there is the entire point.
|
|
124
156
|
*/
|
|
125
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[]>;
|
|
@@ -66,6 +66,56 @@ export const BRIDGED_ENTRIES = [
|
|
|
66
66
|
];
|
|
67
67
|
/** Env vars that authenticate Claude Code without any on-disk credential. */
|
|
68
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
|
+
}
|
|
69
119
|
/** Does `p` exist (following symlinks)? */
|
|
70
120
|
async function exists(p) {
|
|
71
121
|
return fs
|
|
@@ -84,7 +134,7 @@ async function exists(p) {
|
|
|
84
134
|
* — Claude Code falls back to its own default and chat still works, which is the
|
|
85
135
|
* same failure mode `ensureProjectChats` has always had.
|
|
86
136
|
*/
|
|
87
|
-
export async function ensureClaudeHome(cfg, env = process.env) {
|
|
137
|
+
export async function ensureClaudeHome(cfg, env = process.env, probeKeychain = probeMacosKeychainLogin) {
|
|
88
138
|
const report = { bridged: [], notices: [] };
|
|
89
139
|
try {
|
|
90
140
|
await fs.mkdir(path.join(cfg.claudeHome, "projects"), { recursive: true });
|
|
@@ -146,14 +196,29 @@ export async function ensureClaudeHome(cfg, env = process.env) {
|
|
|
146
196
|
const hasTokenEnv = TOKEN_ENV_VARS.some((v) => (env[v] ?? "").trim().length > 0);
|
|
147
197
|
const hasCredentialFile = await exists(path.join(cfg.claudeHome, ".credentials.json"));
|
|
148
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();
|
|
149
206
|
report.notices.push({
|
|
150
207
|
level: "warn",
|
|
151
|
-
message:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.`,
|
|
157
222
|
});
|
|
158
223
|
}
|
|
159
224
|
return report;
|
|
@@ -274,3 +339,49 @@ export async function countLegacyTranscriptLinks(legacyHome, dataDir) {
|
|
|
274
339
|
}
|
|
275
340
|
return count;
|
|
276
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.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export interface ClaudeHomeChoiceInput {
|
|
2
|
+
/** The process environment to read. */
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
/** The user's own Claude home — `os.homedir()/.claude`. */
|
|
5
|
+
userClaudeHome: string;
|
|
6
|
+
/** Does that directory exist? Nothing to be continuous with if it does not. */
|
|
7
|
+
userHomeExists: boolean;
|
|
8
|
+
/** Did the user ask for isolation explicitly (`--isolated-claude-home`)? */
|
|
9
|
+
forceIsolated: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface ClaudeHomeChoice {
|
|
12
|
+
/**
|
|
13
|
+
* The value to put in `CLAUDE_HOME`, or `undefined` to leave the default
|
|
14
|
+
* (`<dataDir>/claude-home`) alone.
|
|
15
|
+
*/
|
|
16
|
+
claudeHome?: string;
|
|
17
|
+
/** Short, user-facing reason — printed when continuity is chosen. */
|
|
18
|
+
reason: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decide whether the interactive CLI should run against the user's own Claude
|
|
22
|
+
* home for credential continuity.
|
|
23
|
+
*
|
|
24
|
+
* Pure, so the whole decision table is testable on a platform that has no
|
|
25
|
+
* Keychain. Continuity is chosen only when all four hold:
|
|
26
|
+
*
|
|
27
|
+
* 1. **No explicit home in the environment.** `CLAUDE_HOME` or
|
|
28
|
+
* `CLAUDE_CONFIG_DIR` is somebody deciding this already; deferring to them is
|
|
29
|
+
* the same rule herdctl#423 follows.
|
|
30
|
+
* 2. **No token in the environment.** A token authenticates regardless of home,
|
|
31
|
+
* so the isolated home works and is strictly better — this is the shape every
|
|
32
|
+
* CI, container and server run has.
|
|
33
|
+
* 3. **`--isolated-claude-home` not passed.** The explicit opt-out.
|
|
34
|
+
* 4. **`~/.claude` actually exists.** With no user home there is no login to be
|
|
35
|
+
* continuous with, and the owned home self-initializes cleanly.
|
|
36
|
+
*
|
|
37
|
+
* Note what is NOT a condition: the platform. The isolation/continuity trade is
|
|
38
|
+
* the same argument on Linux, where it additionally makes the `.credentials.json`
|
|
39
|
+
* bridge unnecessary rather than merely insufficient. Branching on `darwin` would
|
|
40
|
+
* mean two behaviours to reason about and one of them untested on most machines.
|
|
41
|
+
*/
|
|
42
|
+
export declare function chooseClaudeHome(input: ClaudeHomeChoiceInput): ClaudeHomeChoice;
|
|
43
|
+
/** `~/.claude`, given a home directory. Kept here so callers do not re-spell it. */
|
|
44
|
+
export declare function userClaudeHomePath(homedir: string): string;
|