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.
Files changed (114) hide show
  1. package/CHANGELOG.md +706 -1
  2. package/README.md +35 -5
  3. package/config.default.yaml +30 -2
  4. package/dist/auth.d.ts +29 -0
  5. package/dist/auth.d.ts.map +1 -1
  6. package/dist/auth.js +58 -5
  7. package/dist/auth.js.map +1 -1
  8. package/dist/bin.d.ts.map +1 -1
  9. package/dist/bin.js +449 -21
  10. package/dist/bin.js.map +1 -1
  11. package/dist/breaker-store.d.ts +25 -1
  12. package/dist/breaker-store.d.ts.map +1 -1
  13. package/dist/breaker-store.js +193 -25
  14. package/dist/breaker-store.js.map +1 -1
  15. package/dist/client-register.d.ts +147 -0
  16. package/dist/client-register.d.ts.map +1 -0
  17. package/dist/client-register.js +380 -0
  18. package/dist/client-register.js.map +1 -0
  19. package/dist/config/validation.d.ts +29 -0
  20. package/dist/config/validation.d.ts.map +1 -1
  21. package/dist/config/validation.js +88 -0
  22. package/dist/config/validation.js.map +1 -1
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +55 -2
  25. package/dist/config.js.map +1 -1
  26. package/dist/dispatch-log.d.ts +14 -0
  27. package/dist/dispatch-log.d.ts.map +1 -1
  28. package/dist/dispatch-log.js +3 -0
  29. package/dist/dispatch-log.js.map +1 -1
  30. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  31. package/dist/dispatchers/generic-cli.js +35 -0
  32. package/dist/dispatchers/generic-cli.js.map +1 -1
  33. package/dist/dispatchers/openai-compatible.d.ts +20 -4
  34. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  35. package/dist/dispatchers/openai-compatible.js +145 -17
  36. package/dist/dispatchers/openai-compatible.js.map +1 -1
  37. package/dist/dispatchers/shared/rate-limit-headers.d.ts +4 -2
  38. package/dist/dispatchers/shared/rate-limit-headers.d.ts.map +1 -1
  39. package/dist/dispatchers/shared/rate-limit-headers.js +4 -2
  40. package/dist/dispatchers/shared/rate-limit-headers.js.map +1 -1
  41. package/dist/http/answer-stream.d.ts +52 -0
  42. package/dist/http/answer-stream.d.ts.map +1 -0
  43. package/dist/http/answer-stream.js +47 -0
  44. package/dist/http/answer-stream.js.map +1 -0
  45. package/dist/http/parse.d.ts.map +1 -1
  46. package/dist/http/parse.js +41 -0
  47. package/dist/http/parse.js.map +1 -1
  48. package/dist/http/server.d.ts.map +1 -1
  49. package/dist/http/server.js +102 -9
  50. package/dist/http/server.js.map +1 -1
  51. package/dist/jobs/store.d.ts.map +1 -1
  52. package/dist/jobs/store.js +45 -4
  53. package/dist/jobs/store.js.map +1 -1
  54. package/dist/jobs.d.ts +24 -0
  55. package/dist/jobs.d.ts.map +1 -1
  56. package/dist/jobs.js +101 -1
  57. package/dist/jobs.js.map +1 -1
  58. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  59. package/dist/mcp/config-hot-reload.js +10 -0
  60. package/dist/mcp/config-hot-reload.js.map +1 -1
  61. package/dist/mcp/near-miss-guard.d.ts +45 -0
  62. package/dist/mcp/near-miss-guard.d.ts.map +1 -0
  63. package/dist/mcp/near-miss-guard.js +98 -0
  64. package/dist/mcp/near-miss-guard.js.map +1 -0
  65. package/dist/mcp/server.d.ts.map +1 -1
  66. package/dist/mcp/server.js +36 -0
  67. package/dist/mcp/server.js.map +1 -1
  68. package/dist/mcp/tool-schemas.d.ts.map +1 -1
  69. package/dist/mcp/tool-schemas.js +21 -4
  70. package/dist/mcp/tool-schemas.js.map +1 -1
  71. package/dist/mcp/tools.d.ts +32 -0
  72. package/dist/mcp/tools.d.ts.map +1 -1
  73. package/dist/mcp/tools.js +41 -0
  74. package/dist/mcp/tools.js.map +1 -1
  75. package/dist/mcp-clients.d.ts +63 -0
  76. package/dist/mcp-clients.d.ts.map +1 -0
  77. package/dist/mcp-clients.js +117 -0
  78. package/dist/mcp-clients.js.map +1 -0
  79. package/dist/near-miss.d.ts +63 -0
  80. package/dist/near-miss.d.ts.map +1 -0
  81. package/dist/near-miss.js +132 -0
  82. package/dist/near-miss.js.map +1 -0
  83. package/dist/quota.d.ts +31 -1
  84. package/dist/quota.d.ts.map +1 -1
  85. package/dist/quota.js +68 -6
  86. package/dist/quota.js.map +1 -1
  87. package/dist/route-policy.d.ts +10 -0
  88. package/dist/route-policy.d.ts.map +1 -1
  89. package/dist/route-policy.js +20 -2
  90. package/dist/route-policy.js.map +1 -1
  91. package/dist/router.d.ts +61 -2
  92. package/dist/router.d.ts.map +1 -1
  93. package/dist/router.js +165 -29
  94. package/dist/router.js.map +1 -1
  95. package/dist/safety.d.ts.map +1 -1
  96. package/dist/safety.js +31 -1
  97. package/dist/safety.js.map +1 -1
  98. package/dist/status.d.ts +30 -21
  99. package/dist/status.d.ts.map +1 -1
  100. package/dist/status.js +152 -8
  101. package/dist/status.js.map +1 -1
  102. package/dist/types.d.ts +43 -1
  103. package/dist/types.d.ts.map +1 -1
  104. package/dist/working-dir.d.ts.map +1 -1
  105. package/dist/working-dir.js +16 -0
  106. package/dist/working-dir.js.map +1 -1
  107. package/dist/workspace-resolve.d.ts.map +1 -1
  108. package/dist/workspace-resolve.js +119 -15
  109. package/dist/workspace-resolve.js.map +1 -1
  110. package/dist/workspaces.d.ts +83 -0
  111. package/dist/workspaces.d.ts.map +1 -1
  112. package/dist/workspaces.js +288 -15
  113. package/dist/workspaces.js.map +1 -1
  114. 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,CAQN;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"}
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"}