harness-dispatch 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +706 -1
- package/README.md +35 -5
- package/config.default.yaml +30 -2
- package/dist/auth.d.ts +29 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +58 -5
- package/dist/auth.js.map +1 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +449 -21
- package/dist/bin.js.map +1 -1
- package/dist/breaker-store.d.ts +25 -1
- package/dist/breaker-store.d.ts.map +1 -1
- package/dist/breaker-store.js +193 -25
- package/dist/breaker-store.js.map +1 -1
- package/dist/client-register.d.ts +147 -0
- package/dist/client-register.d.ts.map +1 -0
- package/dist/client-register.js +380 -0
- package/dist/client-register.js.map +1 -0
- package/dist/config/validation.d.ts +29 -0
- package/dist/config/validation.d.ts.map +1 -1
- package/dist/config/validation.js +88 -0
- package/dist/config/validation.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -2
- package/dist/config.js.map +1 -1
- package/dist/dispatch-log.d.ts +14 -0
- package/dist/dispatch-log.d.ts.map +1 -1
- package/dist/dispatch-log.js +3 -0
- package/dist/dispatch-log.js.map +1 -1
- package/dist/dispatchers/generic-cli.d.ts.map +1 -1
- package/dist/dispatchers/generic-cli.js +35 -0
- package/dist/dispatchers/generic-cli.js.map +1 -1
- package/dist/dispatchers/openai-compatible.d.ts +20 -4
- package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
- package/dist/dispatchers/openai-compatible.js +145 -17
- package/dist/dispatchers/openai-compatible.js.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.d.ts +4 -2
- package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
- package/dist/dispatchers/shared/rate-limit-headers.js +4 -2
- package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
- package/dist/http/answer-stream.d.ts +52 -0
- package/dist/http/answer-stream.d.ts.map +1 -0
- package/dist/http/answer-stream.js +47 -0
- package/dist/http/answer-stream.js.map +1 -0
- package/dist/http/parse.d.ts.map +1 -1
- package/dist/http/parse.js +41 -0
- package/dist/http/parse.js.map +1 -1
- package/dist/http/server.d.ts.map +1 -1
- package/dist/http/server.js +102 -9
- package/dist/http/server.js.map +1 -1
- package/dist/jobs/store.d.ts.map +1 -1
- package/dist/jobs/store.js +45 -4
- package/dist/jobs/store.js.map +1 -1
- package/dist/jobs.d.ts +24 -0
- package/dist/jobs.d.ts.map +1 -1
- package/dist/jobs.js +101 -1
- package/dist/jobs.js.map +1 -1
- package/dist/mcp/config-hot-reload.d.ts.map +1 -1
- package/dist/mcp/config-hot-reload.js +10 -0
- package/dist/mcp/config-hot-reload.js.map +1 -1
- package/dist/mcp/near-miss-guard.d.ts +45 -0
- package/dist/mcp/near-miss-guard.d.ts.map +1 -0
- package/dist/mcp/near-miss-guard.js +98 -0
- package/dist/mcp/near-miss-guard.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +36 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tool-schemas.d.ts.map +1 -1
- package/dist/mcp/tool-schemas.js +21 -4
- package/dist/mcp/tool-schemas.js.map +1 -1
- package/dist/mcp/tools.d.ts +32 -0
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +41 -0
- package/dist/mcp/tools.js.map +1 -1
- package/dist/mcp-clients.d.ts +63 -0
- package/dist/mcp-clients.d.ts.map +1 -0
- package/dist/mcp-clients.js +117 -0
- package/dist/mcp-clients.js.map +1 -0
- package/dist/near-miss.d.ts +63 -0
- package/dist/near-miss.d.ts.map +1 -0
- package/dist/near-miss.js +132 -0
- package/dist/near-miss.js.map +1 -0
- package/dist/quota.d.ts +31 -1
- package/dist/quota.d.ts.map +1 -1
- package/dist/quota.js +68 -6
- package/dist/quota.js.map +1 -1
- package/dist/route-policy.d.ts +10 -0
- package/dist/route-policy.d.ts.map +1 -1
- package/dist/route-policy.js +20 -2
- package/dist/route-policy.js.map +1 -1
- package/dist/router.d.ts +61 -2
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +165 -29
- package/dist/router.js.map +1 -1
- package/dist/safety.d.ts.map +1 -1
- package/dist/safety.js +31 -1
- package/dist/safety.js.map +1 -1
- package/dist/status.d.ts +30 -21
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +152 -8
- package/dist/status.js.map +1 -1
- package/dist/types.d.ts +43 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/working-dir.d.ts.map +1 -1
- package/dist/working-dir.js +16 -0
- package/dist/working-dir.js.map +1 -1
- package/dist/workspace-resolve.d.ts.map +1 -1
- package/dist/workspace-resolve.js +119 -15
- package/dist/workspace-resolve.js.map +1 -1
- package/dist/workspaces.d.ts +83 -0
- package/dist/workspaces.d.ts.map +1 -1
- package/dist/workspaces.js +288 -15
- package/dist/workspaces.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register this server with the MCP clients installed on this machine.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. Setup was a copy-paste job: `configure` printed a JSON
|
|
5
|
+
* snippet and the user pasted it somewhere. Nobody owned the result, and on
|
|
6
|
+
* the maintainer's machine that produced, at the same time — a Claude Code
|
|
7
|
+
* entry launching a directory renamed away months earlier, a session hook
|
|
8
|
+
* pointing at the same dead path, and a working Cursor entry with no
|
|
9
|
+
* `--config`, so the two clients disagreed about which routes existed while
|
|
10
|
+
* both appeared to work.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT IS PARANOID. This project has failed at exactly this before. v0.1.0
|
|
13
|
+
* shipped a setup command that wrote `~/.claude/CLAUDE.md` and a hooks entry;
|
|
14
|
+
* v0.2.0 removed the command and neither artifact was ever cleaned up. Both
|
|
15
|
+
* were still on the machine seven minor versions later, the hook failing
|
|
16
|
+
* silently every session. So:
|
|
17
|
+
*
|
|
18
|
+
* - only the two config shapes actually opened on a real machine are written;
|
|
19
|
+
* - every write is backed up, merged, and swapped in atomically;
|
|
20
|
+
* - an entry that already exists and differs is REPORTED, never overwritten
|
|
21
|
+
* without consent — that Cursor entry was good, and a blind writer would
|
|
22
|
+
* have destroyed a working setup in the name of fixing it;
|
|
23
|
+
* - removal ships in the same change as writing, so the ENTRY is never left
|
|
24
|
+
* orphaned when someone changes their mind. Backups are bounded rather than
|
|
25
|
+
* removed with it — see pruneOwnBackups for why undoing a registration is
|
|
26
|
+
* the worst moment to delete the record of what it replaced.
|
|
27
|
+
*
|
|
28
|
+
* It writes MCP server registration and nothing else — no instructions, no
|
|
29
|
+
* hooks, no behavioural configuration. That is the part that rotted last time.
|
|
30
|
+
*/
|
|
31
|
+
/** The key this server is registered under, and the only one we ever touch. */
|
|
32
|
+
export declare const ENTRY_KEY = "harness-dispatch";
|
|
33
|
+
export interface ServerEntry {
|
|
34
|
+
command: string;
|
|
35
|
+
args: string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* What a client's config says about us right now.
|
|
39
|
+
*
|
|
40
|
+
* `absent` is the client not being installed — not a fault, and not something
|
|
41
|
+
* to offer. `unreadable` covers a config that will not parse: that is the
|
|
42
|
+
* other application's problem, and the one thing we must not do is rewrite a
|
|
43
|
+
* file we could not understand.
|
|
44
|
+
*/
|
|
45
|
+
export type ClientState = "absent" | "unreadable" | "missing-entry" | "matches" | "differs";
|
|
46
|
+
export interface ClientPlan {
|
|
47
|
+
id: string;
|
|
48
|
+
client: string;
|
|
49
|
+
file: string;
|
|
50
|
+
/** Carried from the client table so writers never re-derive it. */
|
|
51
|
+
serversKey: string;
|
|
52
|
+
state: ClientState;
|
|
53
|
+
/** The entry currently registered, when there is one. */
|
|
54
|
+
current?: unknown;
|
|
55
|
+
/** What we would write. */
|
|
56
|
+
desired: ServerEntry;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The entry to write.
|
|
60
|
+
*
|
|
61
|
+
* The `--config` path is absolute on purpose. A relative path — or none, which
|
|
62
|
+
* is what the machine's Cursor entry had — silently falls back to the shipped
|
|
63
|
+
* defaults, so the client quietly ignores every edit made to the config file
|
|
64
|
+
* and disagrees with the client next to it about which routes exist. Both look
|
|
65
|
+
* like they work, which is what made it survive.
|
|
66
|
+
*/
|
|
67
|
+
export declare function desiredEntry(configPath: string, command: string[]): ServerEntry;
|
|
68
|
+
/**
|
|
69
|
+
* How to launch us, in the way most likely to still work next month.
|
|
70
|
+
*
|
|
71
|
+
* A global install is preferred because it needs no network and starts faster.
|
|
72
|
+
* `npx -y harness-dispatch` is the fallback, not an absolute path into a
|
|
73
|
+
* checkout: an absolute path is precisely what broke here, and the failure was
|
|
74
|
+
* invisible for months.
|
|
75
|
+
*
|
|
76
|
+
* This resolves PATH, which the read-only inspector deliberately refuses to do
|
|
77
|
+
* — but the trade is opposite. There, a wrong guess reports a healthy install
|
|
78
|
+
* as broken. Here, guessing "harness-dispatch" when nothing by that name is
|
|
79
|
+
* installed writes an entry that can never spawn, which is the exact silent
|
|
80
|
+
* breakage this whole feature exists to end.
|
|
81
|
+
*/
|
|
82
|
+
export declare function launchCommand(env?: NodeJS.ProcessEnv): string[];
|
|
83
|
+
/**
|
|
84
|
+
* Launch the build you are running RIGHT NOW, by absolute path.
|
|
85
|
+
*
|
|
86
|
+
* This is the one form the rest of this module argues against, and it exists
|
|
87
|
+
* because refusing to write it did not stop anyone needing it. On a
|
|
88
|
+
* development checkout the installed-package form is actively wrong: with
|
|
89
|
+
* nothing installed globally, `npx -y harness-dispatch` fetches the published
|
|
90
|
+
* version, so registering it would silently swap a checkout that is commits
|
|
91
|
+
* ahead for an older release — a downgrade that looks like a successful setup.
|
|
92
|
+
* That is exactly what `connect` found on the maintainer's machine, where the
|
|
93
|
+
* hand-written absolute entry was the correct one.
|
|
94
|
+
*
|
|
95
|
+
* The trade is real and unchanged: an absolute path stops working the moment
|
|
96
|
+
* the directory is renamed or deleted, silently, which is the failure that
|
|
97
|
+
* started all of this. It is opt-in per run, the path is printed before it is
|
|
98
|
+
* written, and `doctor` fails on a client entry naming a path that has gone —
|
|
99
|
+
* so the failure mode this reintroduces is the one thing already checked for.
|
|
100
|
+
*/
|
|
101
|
+
export declare function devLaunchCommand(entryPath: string): string[];
|
|
102
|
+
/**
|
|
103
|
+
* What would change, per client, if we registered right now.
|
|
104
|
+
*
|
|
105
|
+
* Reports only. Nothing here writes, so a caller can show this and ask — which
|
|
106
|
+
* is the whole point, given one of the entries this was written for was
|
|
107
|
+
* already correct.
|
|
108
|
+
*/
|
|
109
|
+
export declare function planClientWrites(configPath: string, opts?: {
|
|
110
|
+
home?: string;
|
|
111
|
+
command?: string[];
|
|
112
|
+
}): ClientPlan[];
|
|
113
|
+
export interface WriteOutcome {
|
|
114
|
+
id: string;
|
|
115
|
+
client: string;
|
|
116
|
+
file: string;
|
|
117
|
+
action: "written" | "unchanged" | "skipped";
|
|
118
|
+
backupPath?: string;
|
|
119
|
+
reason?: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Register (or re-register) this server in one client's config.
|
|
123
|
+
*
|
|
124
|
+
* Merges: every other server, and every field of an existing entry we do not
|
|
125
|
+
* set, is preserved. `env` in particular holds live API keys on real machines
|
|
126
|
+
* — dropping it while "fixing" an entry would break unrelated servers and leak
|
|
127
|
+
* nothing but the user's afternoon.
|
|
128
|
+
*/
|
|
129
|
+
export declare function writeClientEntry(plan: ClientPlan, opts: {
|
|
130
|
+
stamp: string;
|
|
131
|
+
consented?: boolean;
|
|
132
|
+
}): Promise<WriteOutcome>;
|
|
133
|
+
/**
|
|
134
|
+
* Take our entry back out.
|
|
135
|
+
*
|
|
136
|
+
* Ships with the writer, not later: entries this tool created outliving the
|
|
137
|
+
* feature that created them is the documented way this project has already
|
|
138
|
+
* gone wrong once. An entry that no longer looks like ours is reported and
|
|
139
|
+
* LEFT ALONE — someone edited it deliberately, and deleting a hand-tuned entry
|
|
140
|
+
* because it no longer matches our template would be its own version of the
|
|
141
|
+
* same mistake.
|
|
142
|
+
*/
|
|
143
|
+
export declare function removeClientEntry(plan: ClientPlan, opts: {
|
|
144
|
+
stamp: string;
|
|
145
|
+
force?: boolean;
|
|
146
|
+
}): Promise<WriteOutcome>;
|
|
147
|
+
//# sourceMappingURL=client-register.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-register.d.ts","sourceRoot":"","sources":["../src/client-register.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAQH,+EAA+E;AAC/E,eAAO,MAAM,SAAS,qBAAqB,CAAC;AAE5C,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,YAAY,GAAG,eAAe,GAAG,SAAS,GAAG,SAAS,CAAC;AAE5F,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,WAAW,CAAC;IACnB,yDAAyD;IACzD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2BAA2B;IAC3B,OAAO,EAAE,WAAW,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,WAAW,CAG/E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,EAAE,CAI5E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAE5D;AAoCD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAC/C,UAAU,EAAE,CAoBd;AA4HD,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,GAC3C,OAAO,CAAC,YAAY,CAAC,CAmDvB;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACvC,OAAO,CAAC,YAAY,CAAC,CAqCvB"}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register this server with the MCP clients installed on this machine.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. Setup was a copy-paste job: `configure` printed a JSON
|
|
5
|
+
* snippet and the user pasted it somewhere. Nobody owned the result, and on
|
|
6
|
+
* the maintainer's machine that produced, at the same time — a Claude Code
|
|
7
|
+
* entry launching a directory renamed away months earlier, a session hook
|
|
8
|
+
* pointing at the same dead path, and a working Cursor entry with no
|
|
9
|
+
* `--config`, so the two clients disagreed about which routes existed while
|
|
10
|
+
* both appeared to work.
|
|
11
|
+
*
|
|
12
|
+
* WHY IT IS PARANOID. This project has failed at exactly this before. v0.1.0
|
|
13
|
+
* shipped a setup command that wrote `~/.claude/CLAUDE.md` and a hooks entry;
|
|
14
|
+
* v0.2.0 removed the command and neither artifact was ever cleaned up. Both
|
|
15
|
+
* were still on the machine seven minor versions later, the hook failing
|
|
16
|
+
* silently every session. So:
|
|
17
|
+
*
|
|
18
|
+
* - only the two config shapes actually opened on a real machine are written;
|
|
19
|
+
* - every write is backed up, merged, and swapped in atomically;
|
|
20
|
+
* - an entry that already exists and differs is REPORTED, never overwritten
|
|
21
|
+
* without consent — that Cursor entry was good, and a blind writer would
|
|
22
|
+
* have destroyed a working setup in the name of fixing it;
|
|
23
|
+
* - removal ships in the same change as writing, so the ENTRY is never left
|
|
24
|
+
* orphaned when someone changes their mind. Backups are bounded rather than
|
|
25
|
+
* removed with it — see pruneOwnBackups for why undoing a registration is
|
|
26
|
+
* the worst moment to delete the record of what it replaced.
|
|
27
|
+
*
|
|
28
|
+
* It writes MCP server registration and nothing else — no instructions, no
|
|
29
|
+
* hooks, no behavioural configuration. That is the part that rotted last time.
|
|
30
|
+
*/
|
|
31
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
32
|
+
import { copyFile, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import { clientConfigLocations } from "./mcp-clients.js";
|
|
35
|
+
/** The key this server is registered under, and the only one we ever touch. */
|
|
36
|
+
export const ENTRY_KEY = "harness-dispatch";
|
|
37
|
+
/**
|
|
38
|
+
* The entry to write.
|
|
39
|
+
*
|
|
40
|
+
* The `--config` path is absolute on purpose. A relative path — or none, which
|
|
41
|
+
* is what the machine's Cursor entry had — silently falls back to the shipped
|
|
42
|
+
* defaults, so the client quietly ignores every edit made to the config file
|
|
43
|
+
* and disagrees with the client next to it about which routes exist. Both look
|
|
44
|
+
* like they work, which is what made it survive.
|
|
45
|
+
*/
|
|
46
|
+
export function desiredEntry(configPath, command) {
|
|
47
|
+
const [cmd, ...prefix] = command;
|
|
48
|
+
return { command: cmd ?? "harness-dispatch", args: [...prefix, "--config", path.resolve(configPath)] };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* How to launch us, in the way most likely to still work next month.
|
|
52
|
+
*
|
|
53
|
+
* A global install is preferred because it needs no network and starts faster.
|
|
54
|
+
* `npx -y harness-dispatch` is the fallback, not an absolute path into a
|
|
55
|
+
* checkout: an absolute path is precisely what broke here, and the failure was
|
|
56
|
+
* invisible for months.
|
|
57
|
+
*
|
|
58
|
+
* This resolves PATH, which the read-only inspector deliberately refuses to do
|
|
59
|
+
* — but the trade is opposite. There, a wrong guess reports a healthy install
|
|
60
|
+
* as broken. Here, guessing "harness-dispatch" when nothing by that name is
|
|
61
|
+
* installed writes an entry that can never spawn, which is the exact silent
|
|
62
|
+
* breakage this whole feature exists to end.
|
|
63
|
+
*/
|
|
64
|
+
export function launchCommand(env = process.env) {
|
|
65
|
+
return resolvesOnPath("harness-dispatch", env)
|
|
66
|
+
? ["harness-dispatch"]
|
|
67
|
+
: ["npx", "-y", "harness-dispatch"];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Launch the build you are running RIGHT NOW, by absolute path.
|
|
71
|
+
*
|
|
72
|
+
* This is the one form the rest of this module argues against, and it exists
|
|
73
|
+
* because refusing to write it did not stop anyone needing it. On a
|
|
74
|
+
* development checkout the installed-package form is actively wrong: with
|
|
75
|
+
* nothing installed globally, `npx -y harness-dispatch` fetches the published
|
|
76
|
+
* version, so registering it would silently swap a checkout that is commits
|
|
77
|
+
* ahead for an older release — a downgrade that looks like a successful setup.
|
|
78
|
+
* That is exactly what `connect` found on the maintainer's machine, where the
|
|
79
|
+
* hand-written absolute entry was the correct one.
|
|
80
|
+
*
|
|
81
|
+
* The trade is real and unchanged: an absolute path stops working the moment
|
|
82
|
+
* the directory is renamed or deleted, silently, which is the failure that
|
|
83
|
+
* started all of this. It is opt-in per run, the path is printed before it is
|
|
84
|
+
* written, and `doctor` fails on a client entry naming a path that has gone —
|
|
85
|
+
* so the failure mode this reintroduces is the one thing already checked for.
|
|
86
|
+
*/
|
|
87
|
+
export function devLaunchCommand(entryPath) {
|
|
88
|
+
return ["node", path.resolve(entryPath)];
|
|
89
|
+
}
|
|
90
|
+
function resolvesOnPath(cmd, env) {
|
|
91
|
+
const dirs = (env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean);
|
|
92
|
+
// On Windows the executable is `harness-dispatch.cmd`/`.exe`, never the bare
|
|
93
|
+
// name — checking only the bare name would answer "not installed" on every
|
|
94
|
+
// Windows machine and send everyone to the slower npx form.
|
|
95
|
+
const exts = process.platform === "win32"
|
|
96
|
+
? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
97
|
+
: [""];
|
|
98
|
+
for (const dir of dirs) {
|
|
99
|
+
for (const ext of exts) {
|
|
100
|
+
if (existsSync(path.join(dir, cmd + ext.toLowerCase())))
|
|
101
|
+
return true;
|
|
102
|
+
if (existsSync(path.join(dir, cmd + ext)))
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
function sameEntry(a, b) {
|
|
109
|
+
const e = a;
|
|
110
|
+
if (!e || typeof e !== "object")
|
|
111
|
+
return false;
|
|
112
|
+
if (e.command !== b.command)
|
|
113
|
+
return false;
|
|
114
|
+
if (!Array.isArray(e.args) || e.args.length !== b.args.length)
|
|
115
|
+
return false;
|
|
116
|
+
return e.args.every((v, i) => v === b.args[i]);
|
|
117
|
+
}
|
|
118
|
+
function readJsonFile(file) {
|
|
119
|
+
try {
|
|
120
|
+
return { ok: true, value: JSON.parse(readFileSync(file, "utf8")) };
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return { ok: false };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* What would change, per client, if we registered right now.
|
|
128
|
+
*
|
|
129
|
+
* Reports only. Nothing here writes, so a caller can show this and ask — which
|
|
130
|
+
* is the whole point, given one of the entries this was written for was
|
|
131
|
+
* already correct.
|
|
132
|
+
*/
|
|
133
|
+
export function planClientWrites(configPath, opts = {}) {
|
|
134
|
+
const desired = desiredEntry(configPath, opts.command ?? launchCommand());
|
|
135
|
+
const locations = clientConfigLocations(opts.home);
|
|
136
|
+
return locations.map(({ id, client, file, serversKey }) => {
|
|
137
|
+
const base = { id, client, file, serversKey, desired };
|
|
138
|
+
if (!existsSync(file))
|
|
139
|
+
return { ...base, state: "absent" };
|
|
140
|
+
const parsed = readJsonFile(file);
|
|
141
|
+
if (!parsed.ok)
|
|
142
|
+
return { ...base, state: "unreadable" };
|
|
143
|
+
const servers = parsed.value?.[serversKey];
|
|
144
|
+
const current = servers && typeof servers === "object"
|
|
145
|
+
? servers[ENTRY_KEY]
|
|
146
|
+
: undefined;
|
|
147
|
+
if (current === undefined)
|
|
148
|
+
return { ...base, state: "missing-entry" };
|
|
149
|
+
return {
|
|
150
|
+
...base,
|
|
151
|
+
state: sameEntry(current, desired) ? "matches" : "differs",
|
|
152
|
+
current,
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Copy the file next to itself before touching it.
|
|
158
|
+
*
|
|
159
|
+
* Same directory, so it inherits that directory's permissions rather than
|
|
160
|
+
* landing somewhere world-readable: `~/.claude.json` holds live API keys on
|
|
161
|
+
* this machine, and a backup is a copy of those keys.
|
|
162
|
+
*/
|
|
163
|
+
async function backup(file, stamp) {
|
|
164
|
+
const dest = `${file}.harness-dispatch-backup-${stamp}`;
|
|
165
|
+
await copyFile(file, dest);
|
|
166
|
+
await pruneOwnBackups(file);
|
|
167
|
+
return dest;
|
|
168
|
+
}
|
|
169
|
+
/** How many of our own backups of one file to keep. */
|
|
170
|
+
const KEEP_BACKUPS = 3;
|
|
171
|
+
const NOT_AN_OBJECT = "its config file is not a JSON object, so there is nothing to merge our entry into";
|
|
172
|
+
/**
|
|
173
|
+
* The parsed file as something safe to spread into, or undefined if it is not.
|
|
174
|
+
*
|
|
175
|
+
* "Does it parse" and "is it the shape I am about to merge into" are different
|
|
176
|
+
* questions, and only the first was being asked. An ARRAY parses fine and then
|
|
177
|
+
* gets spread into an object: an array-rooted file came back as
|
|
178
|
+
* `{"0":…,"1":…,"mcpServers":{…}}` and `connect` reported success. A backup is
|
|
179
|
+
* always taken so it was recoverable — but this module edits OTHER
|
|
180
|
+
* applications' config files, which is the highest-consequence thing this
|
|
181
|
+
* product does, and it must not rewrite a shape it does not understand.
|
|
182
|
+
*
|
|
183
|
+
* `null`/absent stays mergeable: that is an empty file, which is the ordinary
|
|
184
|
+
* first-run case.
|
|
185
|
+
*
|
|
186
|
+
* Applied at EVERY level this spreads, not just the root. The first version
|
|
187
|
+
* guarded the root only, so `{"mcpServers": "oops"}` still had its string
|
|
188
|
+
* rekeyed into `{"0":"o","1":"o",…}` and reported success — the same defect
|
|
189
|
+
* one level down, found by the very next acceptance pass. Anywhere a spread
|
|
190
|
+
* happens, this question has to be asked first.
|
|
191
|
+
*/
|
|
192
|
+
function mergeableRoot(value) {
|
|
193
|
+
if (value === null || value === undefined)
|
|
194
|
+
return {};
|
|
195
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
196
|
+
return undefined;
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Keep the last few backups of a file, not all of them ever taken.
|
|
201
|
+
*
|
|
202
|
+
* Every write AND every removal takes one, so a few rounds of connect/--remove
|
|
203
|
+
* left a pile of copies of `~/.claude.json` — a file this module's own comment
|
|
204
|
+
* describes as holding live API keys — with nothing to prune them, no flag to
|
|
205
|
+
* decline them, and no command to list them. An acceptance pass found one
|
|
206
|
+
* already sitting on the maintainer's machine, and the module header's claim
|
|
207
|
+
* that "nothing this creates is left orphaned" was true of the entry and false
|
|
208
|
+
* of these.
|
|
209
|
+
*
|
|
210
|
+
* Bounded rather than removed entirely, and NOT cleared by `--remove`: the
|
|
211
|
+
* moment someone undoes a registration is the worst possible moment to destroy
|
|
212
|
+
* the copy of what it looked like before. Only files matching the name this
|
|
213
|
+
* function itself writes are touched, so a backup anyone else made is not ours
|
|
214
|
+
* to delete.
|
|
215
|
+
*/
|
|
216
|
+
async function pruneOwnBackups(file) {
|
|
217
|
+
const dir = path.dirname(file);
|
|
218
|
+
const prefix = `${path.basename(file)}.harness-dispatch-backup-`;
|
|
219
|
+
try {
|
|
220
|
+
const mine = (await readdir(dir))
|
|
221
|
+
.filter((name) => name.startsWith(prefix))
|
|
222
|
+
// The stamp is an ISO timestamp with `:` and `.` replaced, so it sorts
|
|
223
|
+
// lexicographically in chronological order.
|
|
224
|
+
.sort();
|
|
225
|
+
for (const name of mine.slice(0, Math.max(0, mine.length - KEEP_BACKUPS))) {
|
|
226
|
+
await rm(path.join(dir, name), { force: true });
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// Best effort: failing to tidy up must never fail the registration itself.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Replace a JSON file without ever leaving it half-written.
|
|
235
|
+
*
|
|
236
|
+
* Write a sibling temp file, parse it BACK, and only then rename over the
|
|
237
|
+
* original — rename within a directory is atomic, so a reader sees the old
|
|
238
|
+
* file or the new one and never a truncated one. A half-written
|
|
239
|
+
* `~/.claude.json` costs someone their entire Claude Code configuration, which
|
|
240
|
+
* is a far worse outcome than failing to register.
|
|
241
|
+
*/
|
|
242
|
+
async function writeJsonAtomic(file, value) {
|
|
243
|
+
const tmp = `${file}.harness-dispatch-tmp-${process.pid}`;
|
|
244
|
+
const text = `${JSON.stringify(value, null, 2)}\n`;
|
|
245
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
246
|
+
// Carry the original's permissions onto the replacement.
|
|
247
|
+
//
|
|
248
|
+
// `rename` swaps the INODE, so the file that survives has the temp file's
|
|
249
|
+
// mode — 0644 under a typical umask — not its own. A `~/.claude.json` that
|
|
250
|
+
// Claude Code created 0600 therefore came back group- and world-readable,
|
|
251
|
+
// and that file holds live API keys: registering a server would have
|
|
252
|
+
// silently widened access to somebody's credentials.
|
|
253
|
+
//
|
|
254
|
+
// The same module's backup() uses copyFile, which preserves mode, so the two
|
|
255
|
+
// halves of one write path disagreed about whether the mode mattered. Found
|
|
256
|
+
// by reading during an acceptance pass; POSIX-only, and unreproducible on
|
|
257
|
+
// the Windows machine it was found on, which is exactly why it survived.
|
|
258
|
+
const mode = await stat(file)
|
|
259
|
+
.then((s) => s.mode & 0o777)
|
|
260
|
+
.catch(() => undefined);
|
|
261
|
+
await writeFile(tmp, text, mode === undefined ? "utf8" : { encoding: "utf8", mode });
|
|
262
|
+
try {
|
|
263
|
+
JSON.parse(await readFile(tmp, "utf8"));
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
await rm(tmp, { force: true });
|
|
267
|
+
throw new Error(`Refusing to write ${file}: the replacement did not parse back as JSON (${String(err)}). ` +
|
|
268
|
+
"The original file is untouched.");
|
|
269
|
+
}
|
|
270
|
+
await rename(tmp, file);
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Register (or re-register) this server in one client's config.
|
|
274
|
+
*
|
|
275
|
+
* Merges: every other server, and every field of an existing entry we do not
|
|
276
|
+
* set, is preserved. `env` in particular holds live API keys on real machines
|
|
277
|
+
* — dropping it while "fixing" an entry would break unrelated servers and leak
|
|
278
|
+
* nothing but the user's afternoon.
|
|
279
|
+
*/
|
|
280
|
+
export async function writeClientEntry(plan, opts) {
|
|
281
|
+
const base = { id: plan.id, client: plan.client, file: plan.file };
|
|
282
|
+
if (plan.state === "absent") {
|
|
283
|
+
return { ...base, action: "skipped", reason: "no config file for this client" };
|
|
284
|
+
}
|
|
285
|
+
if (plan.state === "unreadable") {
|
|
286
|
+
return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
|
|
287
|
+
}
|
|
288
|
+
if (plan.state === "matches")
|
|
289
|
+
return { ...base, action: "unchanged" };
|
|
290
|
+
// An entry someone edited by hand is not ours to replace unasked.
|
|
291
|
+
//
|
|
292
|
+
// `removeClientEntry` has always refused this and this function did not, so
|
|
293
|
+
// the protection existed on the half where the cost is lower. An acceptance
|
|
294
|
+
// pass found OPERATIONS.md promising it for both and measured `connect
|
|
295
|
+
// --clients claude-code` printing the hand-written entry it was about to
|
|
296
|
+
// destroy and then destroying it, exit 0.
|
|
297
|
+
//
|
|
298
|
+
// `consented` is what the interactive path passes after showing the diff and
|
|
299
|
+
// getting a yes — that prompt IS the consent, so nothing changes for someone
|
|
300
|
+
// watching it happen. Naming a client on the command line is not the same
|
|
301
|
+
// thing: it says which client, not "and overwrite whatever I put there".
|
|
302
|
+
if (plan.state === "differs" && opts.consented !== true) {
|
|
303
|
+
return {
|
|
304
|
+
...base,
|
|
305
|
+
action: "skipped",
|
|
306
|
+
reason: "its entry has been edited by hand — re-run interactively (no --clients, " +
|
|
307
|
+
"no --yes) to see the difference and confirm, or pass --force to overwrite it",
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const parsed = readJsonFile(plan.file);
|
|
311
|
+
if (!parsed.ok) {
|
|
312
|
+
return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
|
|
313
|
+
}
|
|
314
|
+
const root = mergeableRoot(parsed.value);
|
|
315
|
+
if (root === undefined)
|
|
316
|
+
return { ...base, action: "skipped", reason: NOT_AN_OBJECT };
|
|
317
|
+
const serversValue = mergeableRoot(root[plan.serversKey]);
|
|
318
|
+
if (serversValue === undefined) {
|
|
319
|
+
return { ...base, action: "skipped", reason: `its \`${plan.serversKey}\` is not a JSON object` };
|
|
320
|
+
}
|
|
321
|
+
const servers = { ...serversValue };
|
|
322
|
+
const existing = mergeableRoot(servers[ENTRY_KEY]);
|
|
323
|
+
if (existing === undefined) {
|
|
324
|
+
return { ...base, action: "skipped", reason: `its existing \`${ENTRY_KEY}\` entry is not a JSON object` };
|
|
325
|
+
}
|
|
326
|
+
servers[ENTRY_KEY] = { ...existing, ...plan.desired };
|
|
327
|
+
const backupPath = await backup(plan.file, opts.stamp);
|
|
328
|
+
await writeJsonAtomic(plan.file, { ...root, [plan.serversKey]: servers });
|
|
329
|
+
return { ...base, action: "written", backupPath };
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Take our entry back out.
|
|
333
|
+
*
|
|
334
|
+
* Ships with the writer, not later: entries this tool created outliving the
|
|
335
|
+
* feature that created them is the documented way this project has already
|
|
336
|
+
* gone wrong once. An entry that no longer looks like ours is reported and
|
|
337
|
+
* LEFT ALONE — someone edited it deliberately, and deleting a hand-tuned entry
|
|
338
|
+
* because it no longer matches our template would be its own version of the
|
|
339
|
+
* same mistake.
|
|
340
|
+
*/
|
|
341
|
+
export async function removeClientEntry(plan, opts) {
|
|
342
|
+
const base = { id: plan.id, client: plan.client, file: plan.file };
|
|
343
|
+
if (plan.state === "absent" || plan.state === "missing-entry") {
|
|
344
|
+
return { ...base, action: "unchanged" };
|
|
345
|
+
}
|
|
346
|
+
if (plan.state === "unreadable") {
|
|
347
|
+
return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
|
|
348
|
+
}
|
|
349
|
+
if (plan.state === "differs" && opts.force !== true) {
|
|
350
|
+
return {
|
|
351
|
+
...base,
|
|
352
|
+
action: "skipped",
|
|
353
|
+
reason: "its entry has been edited by hand — remove it yourself, or pass --force",
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
const parsed = readJsonFile(plan.file);
|
|
357
|
+
if (!parsed.ok) {
|
|
358
|
+
return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
|
|
359
|
+
}
|
|
360
|
+
const root = mergeableRoot(parsed.value);
|
|
361
|
+
// Removal was already safe without this: our entry cannot be present in a
|
|
362
|
+
// file this shape, so the `ENTRY_KEY in servers` check below returns
|
|
363
|
+
// "unchanged" before anything is written. Kept anyway so both writers ask
|
|
364
|
+
// the same question of the same file, rather than one of them being correct
|
|
365
|
+
// by accident of a later check. A test for it was written and then deleted —
|
|
366
|
+
// it passed with the guard removed, so it was evidence of nothing.
|
|
367
|
+
if (root === undefined)
|
|
368
|
+
return { ...base, action: "unchanged" };
|
|
369
|
+
const serversValue = mergeableRoot(root[plan.serversKey]);
|
|
370
|
+
if (serversValue === undefined)
|
|
371
|
+
return { ...base, action: "unchanged" };
|
|
372
|
+
const servers = { ...serversValue };
|
|
373
|
+
if (!(ENTRY_KEY in servers))
|
|
374
|
+
return { ...base, action: "unchanged" };
|
|
375
|
+
delete servers[ENTRY_KEY];
|
|
376
|
+
const backupPath = await backup(plan.file, opts.stamp);
|
|
377
|
+
await writeJsonAtomic(plan.file, { ...root, [plan.serversKey]: servers });
|
|
378
|
+
return { ...base, action: "written", backupPath };
|
|
379
|
+
}
|
|
380
|
+
//# sourceMappingURL=client-register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-register.js","sourceRoot":"","sources":["../src/client-register.ts"],"names":[],"mappings":"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"}
|
|
@@ -67,6 +67,35 @@ export declare const KNOWN_TOP_LEVEL_KEYS: Set<string>;
|
|
|
67
67
|
*/
|
|
68
68
|
export declare const KNOWN_ROUTE_KEYS: Set<string>;
|
|
69
69
|
export declare function warnUnknownRouteKeys(entry: Record<string, unknown>, label: string, warnings: string[]): void;
|
|
70
|
+
/**
|
|
71
|
+
* A RECOGNISED key carrying the wrong TYPE of value.
|
|
72
|
+
*
|
|
73
|
+
* The unknown-key warner above covers a misspelled key. It does not cover a
|
|
74
|
+
* correctly-spelled one whose value cannot be read: the coercions in
|
|
75
|
+
* coercions.ts drop on mismatch and the caller supplies a default, silently.
|
|
76
|
+
* That file's own header names this gap and points at the unknown-key warning
|
|
77
|
+
* as the mitigation — which does not cover it, because the key is not unknown.
|
|
78
|
+
*
|
|
79
|
+
* Found live on the maintainer's machine by an acceptance pass: four routes
|
|
80
|
+
* carrying `tier: metered`, which is not a number, silently running at the
|
|
81
|
+
* default tier 3. Nothing had ever said so. `weight: very-high` becomes 1.0 the
|
|
82
|
+
* same way, and both feed routing decisions.
|
|
83
|
+
*
|
|
84
|
+
* Reports rather than rejects, like every other warning here: the config still
|
|
85
|
+
* loads, and `doctor` exits non-zero so the signal is not merely decorative.
|
|
86
|
+
*/
|
|
87
|
+
export declare function warnMistypedRouteValues(entry: Record<string, unknown>, label: string, warnings: string[]): void;
|
|
88
|
+
/**
|
|
89
|
+
* Two route entries sharing one name.
|
|
90
|
+
*
|
|
91
|
+
* The later entry wins outright, so everything the earlier one declared is
|
|
92
|
+
* gone. An acceptance pass measured the consequence: a first entry setting
|
|
93
|
+
* `safety_profile: read_only` and `workspace_policy: copy` was replaced by a
|
|
94
|
+
* second with neither, and the surviving route ran `workspace_edit` /
|
|
95
|
+
* `shared_locked` — silently LESS restrictive than what was written, with no
|
|
96
|
+
* warning on any surface.
|
|
97
|
+
*/
|
|
98
|
+
export declare function warnDuplicateRouteNames(names: Array<string | undefined>, block: string, warnings: string[]): void;
|
|
70
99
|
export declare function warnUnknownTopLevelKeys(raw: Record<string, unknown>, warnings: string[]): void;
|
|
71
100
|
export declare function warnUnknownSafetyEnums(node: unknown, warnings: string[], where?: string): void;
|
|
72
101
|
//# sourceMappingURL=validation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/config/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAsB7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAiB/B,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB,aAU3B,CAAC;AAEH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,IAAI,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../../src/config/validation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAKH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAsB7D,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,aAiB/B,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,gBAAgB,aAU3B,CAAC;AAEH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,IAAI,CASN;AA0BD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,IAAI,CAoBN;AAED;;;;;;;;;GASG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,EAChC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,IAAI,CAkBN;AAeD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAgB9F;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,KAAK,SAAK,GAAG,IAAI,CAqC1F"}
|