harness-dispatch 0.7.9 → 0.9.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 (153) hide show
  1. package/CHANGELOG.md +1170 -1
  2. package/README.md +52 -6
  3. package/config.default.yaml +49 -5
  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/billing.d.ts +24 -0
  9. package/dist/billing.d.ts.map +1 -1
  10. package/dist/billing.js +24 -0
  11. package/dist/billing.js.map +1 -1
  12. package/dist/bin.d.ts.map +1 -1
  13. package/dist/bin.js +529 -31
  14. package/dist/bin.js.map +1 -1
  15. package/dist/breaker-store.d.ts +25 -1
  16. package/dist/breaker-store.d.ts.map +1 -1
  17. package/dist/breaker-store.js +193 -25
  18. package/dist/breaker-store.js.map +1 -1
  19. package/dist/circuit-breaker.d.ts +9 -1
  20. package/dist/circuit-breaker.d.ts.map +1 -1
  21. package/dist/circuit-breaker.js +15 -1
  22. package/dist/circuit-breaker.js.map +1 -1
  23. package/dist/client-register.d.ts +157 -0
  24. package/dist/client-register.d.ts.map +1 -0
  25. package/dist/client-register.js +389 -0
  26. package/dist/client-register.js.map +1 -0
  27. package/dist/config/validation.d.ts +41 -0
  28. package/dist/config/validation.d.ts.map +1 -1
  29. package/dist/config/validation.js +215 -1
  30. package/dist/config/validation.js.map +1 -1
  31. package/dist/config.d.ts +4 -2
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js +143 -6
  34. package/dist/config.js.map +1 -1
  35. package/dist/configure-yaml.d.ts +2 -0
  36. package/dist/configure-yaml.d.ts.map +1 -1
  37. package/dist/configure-yaml.js +69 -3
  38. package/dist/configure-yaml.js.map +1 -1
  39. package/dist/dispatch-log.d.ts +14 -0
  40. package/dist/dispatch-log.d.ts.map +1 -1
  41. package/dist/dispatch-log.js +3 -0
  42. package/dist/dispatch-log.js.map +1 -1
  43. package/dist/dispatchers/generic-cli.d.ts.map +1 -1
  44. package/dist/dispatchers/generic-cli.js +92 -15
  45. package/dist/dispatchers/generic-cli.js.map +1 -1
  46. package/dist/dispatchers/openai-compatible.d.ts +20 -4
  47. package/dist/dispatchers/openai-compatible.d.ts.map +1 -1
  48. package/dist/dispatchers/openai-compatible.js +167 -19
  49. package/dist/dispatchers/openai-compatible.js.map +1 -1
  50. package/dist/dispatchers/shared/harness-login.d.ts +25 -0
  51. package/dist/dispatchers/shared/harness-login.d.ts.map +1 -0
  52. package/dist/dispatchers/shared/harness-login.js +68 -0
  53. package/dist/dispatchers/shared/harness-login.js.map +1 -0
  54. package/dist/dispatchers/shared/stream-subprocess.d.ts.map +1 -1
  55. package/dist/dispatchers/shared/stream-subprocess.js +70 -4
  56. package/dist/dispatchers/shared/stream-subprocess.js.map +1 -1
  57. package/dist/file-lock.d.ts +29 -1
  58. package/dist/file-lock.d.ts.map +1 -1
  59. package/dist/file-lock.js +22 -1
  60. package/dist/file-lock.js.map +1 -1
  61. package/dist/http/answer-stream.d.ts +52 -0
  62. package/dist/http/answer-stream.d.ts.map +1 -0
  63. package/dist/http/answer-stream.js +47 -0
  64. package/dist/http/answer-stream.js.map +1 -0
  65. package/dist/http/parse.d.ts.map +1 -1
  66. package/dist/http/parse.js +41 -0
  67. package/dist/http/parse.js.map +1 -1
  68. package/dist/http/server.d.ts +2 -0
  69. package/dist/http/server.d.ts.map +1 -1
  70. package/dist/http/server.js +281 -23
  71. package/dist/http/server.js.map +1 -1
  72. package/dist/jobs/context.d.ts +0 -7
  73. package/dist/jobs/context.d.ts.map +1 -1
  74. package/dist/jobs/context.js +95 -7
  75. package/dist/jobs/context.js.map +1 -1
  76. package/dist/jobs/store.d.ts.map +1 -1
  77. package/dist/jobs/store.js +45 -4
  78. package/dist/jobs/store.js.map +1 -1
  79. package/dist/jobs/types.d.ts +8 -1
  80. package/dist/jobs/types.d.ts.map +1 -1
  81. package/dist/jobs.d.ts +39 -0
  82. package/dist/jobs.d.ts.map +1 -1
  83. package/dist/jobs.js +207 -15
  84. package/dist/jobs.js.map +1 -1
  85. package/dist/leaderboard.d.ts.map +1 -1
  86. package/dist/leaderboard.js +39 -27
  87. package/dist/leaderboard.js.map +1 -1
  88. package/dist/mcp/config-hot-reload.d.ts +14 -3
  89. package/dist/mcp/config-hot-reload.d.ts.map +1 -1
  90. package/dist/mcp/config-hot-reload.js +33 -5
  91. package/dist/mcp/config-hot-reload.js.map +1 -1
  92. package/dist/mcp/dispatcher-factory.d.ts.map +1 -1
  93. package/dist/mcp/dispatcher-factory.js +14 -3
  94. package/dist/mcp/dispatcher-factory.js.map +1 -1
  95. package/dist/mcp/near-miss-guard.d.ts +45 -0
  96. package/dist/mcp/near-miss-guard.d.ts.map +1 -0
  97. package/dist/mcp/near-miss-guard.js +98 -0
  98. package/dist/mcp/near-miss-guard.js.map +1 -0
  99. package/dist/mcp/server.d.ts.map +1 -1
  100. package/dist/mcp/server.js +36 -0
  101. package/dist/mcp/server.js.map +1 -1
  102. package/dist/mcp/tool-schemas.d.ts +1 -1
  103. package/dist/mcp/tool-schemas.d.ts.map +1 -1
  104. package/dist/mcp/tool-schemas.js +27 -7
  105. package/dist/mcp/tool-schemas.js.map +1 -1
  106. package/dist/mcp/tools.d.ts +33 -4
  107. package/dist/mcp/tools.d.ts.map +1 -1
  108. package/dist/mcp/tools.js +85 -8
  109. package/dist/mcp/tools.js.map +1 -1
  110. package/dist/mcp-clients.d.ts +72 -0
  111. package/dist/mcp-clients.d.ts.map +1 -0
  112. package/dist/mcp-clients.js +121 -0
  113. package/dist/mcp-clients.js.map +1 -0
  114. package/dist/near-miss.d.ts +63 -0
  115. package/dist/near-miss.d.ts.map +1 -0
  116. package/dist/near-miss.js +132 -0
  117. package/dist/near-miss.js.map +1 -0
  118. package/dist/quota.d.ts +31 -1
  119. package/dist/quota.d.ts.map +1 -1
  120. package/dist/quota.js +84 -8
  121. package/dist/quota.js.map +1 -1
  122. package/dist/route-policy.d.ts +12 -1
  123. package/dist/route-policy.d.ts.map +1 -1
  124. package/dist/route-policy.js +74 -2
  125. package/dist/route-policy.js.map +1 -1
  126. package/dist/router.d.ts +61 -2
  127. package/dist/router.d.ts.map +1 -1
  128. package/dist/router.js +160 -29
  129. package/dist/router.js.map +1 -1
  130. package/dist/safety.d.ts.map +1 -1
  131. package/dist/safety.js +37 -1
  132. package/dist/safety.js.map +1 -1
  133. package/dist/state-dir.d.ts +10 -0
  134. package/dist/state-dir.d.ts.map +1 -1
  135. package/dist/state-dir.js +12 -0
  136. package/dist/state-dir.js.map +1 -1
  137. package/dist/status.d.ts +30 -21
  138. package/dist/status.d.ts.map +1 -1
  139. package/dist/status.js +152 -8
  140. package/dist/status.js.map +1 -1
  141. package/dist/types.d.ts +49 -2
  142. package/dist/types.d.ts.map +1 -1
  143. package/dist/working-dir.d.ts.map +1 -1
  144. package/dist/working-dir.js +16 -0
  145. package/dist/working-dir.js.map +1 -1
  146. package/dist/workspace-resolve.d.ts.map +1 -1
  147. package/dist/workspace-resolve.js +196 -15
  148. package/dist/workspace-resolve.js.map +1 -1
  149. package/dist/workspaces.d.ts +98 -0
  150. package/dist/workspaces.d.ts.map +1 -1
  151. package/dist/workspaces.js +609 -20
  152. package/dist/workspaces.js.map +1 -1
  153. package/package.json +1 -1
@@ -0,0 +1,157 @@
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
+ /**
46
+ * `absent`: neither the config file nor the client's command exists — not
47
+ * installed. `missing-file`: the command is on PATH but the file is not there
48
+ * yet (Claude Code writes it on first interactive launch) — the file will be
49
+ * created with just our entry, which the client accepts as a user-scope
50
+ * registration (checked against Claude Code 2.1.258: `claude mcp list` shows
51
+ * the server from a file holding only `mcpServers`).
52
+ */
53
+ export type ClientState = "absent" | "missing-file" | "unreadable" | "missing-entry" | "matches" | "differs";
54
+ export interface ClientPlan {
55
+ id: string;
56
+ client: string;
57
+ file: string;
58
+ /** Carried from the client table so writers never re-derive it. */
59
+ serversKey: string;
60
+ state: ClientState;
61
+ /** The entry currently registered, when there is one. */
62
+ current?: unknown;
63
+ /** What we would write. */
64
+ desired: ServerEntry;
65
+ }
66
+ /**
67
+ * The entry to write.
68
+ *
69
+ * The `--config` path is absolute on purpose. A relative path — or none, which
70
+ * is what the machine's Cursor entry had — silently falls back to the shipped
71
+ * defaults, so the client quietly ignores every edit made to the config file
72
+ * and disagrees with the client next to it about which routes exist. Both look
73
+ * like they work, which is what made it survive.
74
+ */
75
+ export declare function desiredEntry(configPath: string, command: string[]): ServerEntry;
76
+ /**
77
+ * How to launch us, in the way most likely to still work next month.
78
+ *
79
+ * A global install is preferred because it needs no network and starts faster.
80
+ * `npx -y harness-dispatch` is the fallback, not an absolute path into a
81
+ * checkout: an absolute path is precisely what broke here, and the failure was
82
+ * invisible for months.
83
+ *
84
+ * This resolves PATH, which the read-only inspector deliberately refuses to do
85
+ * — but the trade is opposite. There, a wrong guess reports a healthy install
86
+ * as broken. Here, guessing "harness-dispatch" when nothing by that name is
87
+ * installed writes an entry that can never spawn, which is the exact silent
88
+ * breakage this whole feature exists to end.
89
+ */
90
+ export declare function launchCommand(env?: NodeJS.ProcessEnv): string[];
91
+ /**
92
+ * Launch the build you are running RIGHT NOW, by absolute path.
93
+ *
94
+ * This is the one form the rest of this module argues against, and it exists
95
+ * because refusing to write it did not stop anyone needing it. On a
96
+ * development checkout the installed-package form is actively wrong: with
97
+ * nothing installed globally, `npx -y harness-dispatch` fetches the published
98
+ * version, so registering it would silently swap a checkout that is commits
99
+ * ahead for an older release — a downgrade that looks like a successful setup.
100
+ * That is exactly what `connect` found on the maintainer's machine, where the
101
+ * hand-written absolute entry was the correct one.
102
+ *
103
+ * The trade is real and unchanged: an absolute path stops working the moment
104
+ * the directory is renamed or deleted, silently, which is the failure that
105
+ * started all of this. It is opt-in per run, the path is printed before it is
106
+ * written, and `doctor` fails on a client entry naming a path that has gone —
107
+ * so the failure mode this reintroduces is the one thing already checked for.
108
+ */
109
+ export declare function devLaunchCommand(entryPath: string): string[];
110
+ /**
111
+ * What would change, per client, if we registered right now.
112
+ *
113
+ * Reports only. Nothing here writes, so a caller can show this and ask — which
114
+ * is the whole point, given one of the entries this was written for was
115
+ * already correct.
116
+ */
117
+ export declare function planClientWrites(configPath: string, opts?: {
118
+ home?: string;
119
+ command?: string[];
120
+ /** Is any of these commands on PATH? Injectable so tests never depend on what is installed. */
121
+ installed?: (commands: string[]) => boolean;
122
+ }): ClientPlan[];
123
+ export interface WriteOutcome {
124
+ id: string;
125
+ client: string;
126
+ file: string;
127
+ action: "written" | "unchanged" | "skipped";
128
+ backupPath?: string;
129
+ reason?: string;
130
+ }
131
+ /**
132
+ * Register (or re-register) this server in one client's config.
133
+ *
134
+ * Merges: every other server, and every field of an existing entry we do not
135
+ * set, is preserved. `env` in particular holds live API keys on real machines
136
+ * — dropping it while "fixing" an entry would break unrelated servers and leak
137
+ * nothing but the user's afternoon.
138
+ */
139
+ export declare function writeClientEntry(plan: ClientPlan, opts: {
140
+ stamp: string;
141
+ consented?: boolean;
142
+ }): Promise<WriteOutcome>;
143
+ /**
144
+ * Take our entry back out.
145
+ *
146
+ * Ships with the writer, not later: entries this tool created outliving the
147
+ * feature that created them is the documented way this project has already
148
+ * gone wrong once. An entry that no longer looks like ours is reported and
149
+ * LEFT ALONE — someone edited it deliberately, and deleting a hand-tuned entry
150
+ * because it no longer matches our template would be its own version of the
151
+ * same mistake.
152
+ */
153
+ export declare function removeClientEntry(plan: ClientPlan, opts: {
154
+ stamp: string;
155
+ force?: boolean;
156
+ }): Promise<WriteOutcome>;
157
+ //# 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;AASH,+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;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GACnB,QAAQ,GACR,cAAc,GACd,YAAY,GACZ,eAAe,GACf,SAAS,GACT,SAAS,CAAC;AAEd,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;IACJ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;CACxC,GACL,UAAU,EAAE,CAuBd;AAgID,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,CA6DvB;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,389 @@
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 { commandAvailable } from "./dispatchers/shared/which-available.js";
35
+ import { clientConfigLocations } from "./mcp-clients.js";
36
+ /** The key this server is registered under, and the only one we ever touch. */
37
+ export const ENTRY_KEY = "harness-dispatch";
38
+ /**
39
+ * The entry to write.
40
+ *
41
+ * The `--config` path is absolute on purpose. A relative path — or none, which
42
+ * is what the machine's Cursor entry had — silently falls back to the shipped
43
+ * defaults, so the client quietly ignores every edit made to the config file
44
+ * and disagrees with the client next to it about which routes exist. Both look
45
+ * like they work, which is what made it survive.
46
+ */
47
+ export function desiredEntry(configPath, command) {
48
+ const [cmd, ...prefix] = command;
49
+ return { command: cmd ?? "harness-dispatch", args: [...prefix, "--config", path.resolve(configPath)] };
50
+ }
51
+ /**
52
+ * How to launch us, in the way most likely to still work next month.
53
+ *
54
+ * A global install is preferred because it needs no network and starts faster.
55
+ * `npx -y harness-dispatch` is the fallback, not an absolute path into a
56
+ * checkout: an absolute path is precisely what broke here, and the failure was
57
+ * invisible for months.
58
+ *
59
+ * This resolves PATH, which the read-only inspector deliberately refuses to do
60
+ * — but the trade is opposite. There, a wrong guess reports a healthy install
61
+ * as broken. Here, guessing "harness-dispatch" when nothing by that name is
62
+ * installed writes an entry that can never spawn, which is the exact silent
63
+ * breakage this whole feature exists to end.
64
+ */
65
+ export function launchCommand(env = process.env) {
66
+ return resolvesOnPath("harness-dispatch", env)
67
+ ? ["harness-dispatch"]
68
+ : ["npx", "-y", "harness-dispatch"];
69
+ }
70
+ /**
71
+ * Launch the build you are running RIGHT NOW, by absolute path.
72
+ *
73
+ * This is the one form the rest of this module argues against, and it exists
74
+ * because refusing to write it did not stop anyone needing it. On a
75
+ * development checkout the installed-package form is actively wrong: with
76
+ * nothing installed globally, `npx -y harness-dispatch` fetches the published
77
+ * version, so registering it would silently swap a checkout that is commits
78
+ * ahead for an older release — a downgrade that looks like a successful setup.
79
+ * That is exactly what `connect` found on the maintainer's machine, where the
80
+ * hand-written absolute entry was the correct one.
81
+ *
82
+ * The trade is real and unchanged: an absolute path stops working the moment
83
+ * the directory is renamed or deleted, silently, which is the failure that
84
+ * started all of this. It is opt-in per run, the path is printed before it is
85
+ * written, and `doctor` fails on a client entry naming a path that has gone —
86
+ * so the failure mode this reintroduces is the one thing already checked for.
87
+ */
88
+ export function devLaunchCommand(entryPath) {
89
+ return ["node", path.resolve(entryPath)];
90
+ }
91
+ function resolvesOnPath(cmd, env) {
92
+ const dirs = (env.PATH ?? env.Path ?? "").split(path.delimiter).filter(Boolean);
93
+ // On Windows the executable is `harness-dispatch.cmd`/`.exe`, never the bare
94
+ // name — checking only the bare name would answer "not installed" on every
95
+ // Windows machine and send everyone to the slower npx form.
96
+ const exts = process.platform === "win32"
97
+ ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
98
+ : [""];
99
+ for (const dir of dirs) {
100
+ for (const ext of exts) {
101
+ if (existsSync(path.join(dir, cmd + ext.toLowerCase())))
102
+ return true;
103
+ if (existsSync(path.join(dir, cmd + ext)))
104
+ return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ function sameEntry(a, b) {
110
+ const e = a;
111
+ if (!e || typeof e !== "object")
112
+ return false;
113
+ if (e.command !== b.command)
114
+ return false;
115
+ if (!Array.isArray(e.args) || e.args.length !== b.args.length)
116
+ return false;
117
+ return e.args.every((v, i) => v === b.args[i]);
118
+ }
119
+ function readJsonFile(file) {
120
+ try {
121
+ return { ok: true, value: JSON.parse(readFileSync(file, "utf8")) };
122
+ }
123
+ catch {
124
+ return { ok: false };
125
+ }
126
+ }
127
+ /**
128
+ * What would change, per client, if we registered right now.
129
+ *
130
+ * Reports only. Nothing here writes, so a caller can show this and ask — which
131
+ * is the whole point, given one of the entries this was written for was
132
+ * already correct.
133
+ */
134
+ export function planClientWrites(configPath, opts = {}) {
135
+ const desired = desiredEntry(configPath, opts.command ?? launchCommand());
136
+ const installed = opts.installed ?? ((commands) => commands.some((c) => commandAvailable(c)));
137
+ const locations = clientConfigLocations(opts.home);
138
+ return locations.map(({ id, client, file, serversKey, commands }) => {
139
+ const base = { id, client, file, serversKey, desired };
140
+ if (!existsSync(file)) {
141
+ return { ...base, state: installed(commands) ? "missing-file" : "absent" };
142
+ }
143
+ const parsed = readJsonFile(file);
144
+ if (!parsed.ok)
145
+ return { ...base, state: "unreadable" };
146
+ const servers = parsed.value?.[serversKey];
147
+ const current = servers && typeof servers === "object"
148
+ ? servers[ENTRY_KEY]
149
+ : undefined;
150
+ if (current === undefined)
151
+ return { ...base, state: "missing-entry" };
152
+ return {
153
+ ...base,
154
+ state: sameEntry(current, desired) ? "matches" : "differs",
155
+ current,
156
+ };
157
+ });
158
+ }
159
+ /**
160
+ * Copy the file next to itself before touching it.
161
+ *
162
+ * Same directory, so it inherits that directory's permissions rather than
163
+ * landing somewhere world-readable: `~/.claude.json` holds live API keys on
164
+ * this machine, and a backup is a copy of those keys.
165
+ */
166
+ async function backup(file, stamp) {
167
+ const dest = `${file}.harness-dispatch-backup-${stamp}`;
168
+ await copyFile(file, dest);
169
+ await pruneOwnBackups(file);
170
+ return dest;
171
+ }
172
+ /** How many of our own backups of one file to keep. */
173
+ const KEEP_BACKUPS = 3;
174
+ const NOT_AN_OBJECT = "its config file is not a JSON object, so there is nothing to merge our entry into";
175
+ /**
176
+ * The parsed file as something safe to spread into, or undefined if it is not.
177
+ *
178
+ * "Does it parse" and "is it the shape I am about to merge into" are different
179
+ * questions, and only the first was being asked. An ARRAY parses fine and then
180
+ * gets spread into an object: an array-rooted file came back as
181
+ * `{"0":…,"1":…,"mcpServers":{…}}` and `connect` reported success. A backup is
182
+ * always taken so it was recoverable — but this module edits OTHER
183
+ * applications' config files, which is the highest-consequence thing this
184
+ * product does, and it must not rewrite a shape it does not understand.
185
+ *
186
+ * `null`/absent stays mergeable: that is an empty file, which is the ordinary
187
+ * first-run case.
188
+ *
189
+ * Applied at EVERY level this spreads, not just the root. The first version
190
+ * guarded the root only, so `{"mcpServers": "oops"}` still had its string
191
+ * rekeyed into `{"0":"o","1":"o",…}` and reported success — the same defect
192
+ * one level down, found by the very next acceptance pass. Anywhere a spread
193
+ * happens, this question has to be asked first.
194
+ */
195
+ function mergeableRoot(value) {
196
+ if (value === null || value === undefined)
197
+ return {};
198
+ if (typeof value !== "object" || Array.isArray(value))
199
+ return undefined;
200
+ return value;
201
+ }
202
+ /**
203
+ * Keep the last few backups of a file, not all of them ever taken.
204
+ *
205
+ * Every write AND every removal takes one, so a few rounds of connect/--remove
206
+ * left a pile of copies of `~/.claude.json` — a file this module's own comment
207
+ * describes as holding live API keys — with nothing to prune them, no flag to
208
+ * decline them, and no command to list them. An acceptance pass found one
209
+ * already sitting on the maintainer's machine, and the module header's claim
210
+ * that "nothing this creates is left orphaned" was true of the entry and false
211
+ * of these.
212
+ *
213
+ * Bounded rather than removed entirely, and NOT cleared by `--remove`: the
214
+ * moment someone undoes a registration is the worst possible moment to destroy
215
+ * the copy of what it looked like before. Only files matching the name this
216
+ * function itself writes are touched, so a backup anyone else made is not ours
217
+ * to delete.
218
+ */
219
+ async function pruneOwnBackups(file) {
220
+ const dir = path.dirname(file);
221
+ const prefix = `${path.basename(file)}.harness-dispatch-backup-`;
222
+ try {
223
+ const mine = (await readdir(dir))
224
+ .filter((name) => name.startsWith(prefix))
225
+ // The stamp is an ISO timestamp with `:` and `.` replaced, so it sorts
226
+ // lexicographically in chronological order.
227
+ .sort();
228
+ for (const name of mine.slice(0, Math.max(0, mine.length - KEEP_BACKUPS))) {
229
+ await rm(path.join(dir, name), { force: true });
230
+ }
231
+ }
232
+ catch {
233
+ // Best effort: failing to tidy up must never fail the registration itself.
234
+ }
235
+ }
236
+ /**
237
+ * Replace a JSON file without ever leaving it half-written.
238
+ *
239
+ * Write a sibling temp file, parse it BACK, and only then rename over the
240
+ * original — rename within a directory is atomic, so a reader sees the old
241
+ * file or the new one and never a truncated one. A half-written
242
+ * `~/.claude.json` costs someone their entire Claude Code configuration, which
243
+ * is a far worse outcome than failing to register.
244
+ */
245
+ async function writeJsonAtomic(file, value, opts = {}) {
246
+ const tmp = `${file}.harness-dispatch-tmp-${process.pid}`;
247
+ const text = `${JSON.stringify(value, null, 2)}\n`;
248
+ await mkdir(path.dirname(file), { recursive: true });
249
+ // Carry the original's permissions onto the replacement.
250
+ //
251
+ // `rename` swaps the INODE, so the file that survives has the temp file's
252
+ // mode — 0644 under a typical umask — not its own. A `~/.claude.json` that
253
+ // Claude Code created 0600 therefore came back group- and world-readable,
254
+ // and that file holds live API keys: registering a server would have
255
+ // silently widened access to somebody's credentials.
256
+ //
257
+ // The same module's backup() uses copyFile, which preserves mode, so the two
258
+ // halves of one write path disagreed about whether the mode mattered. Found
259
+ // by reading during an acceptance pass; POSIX-only, and unreproducible on
260
+ // the Windows machine it was found on, which is exactly why it survived.
261
+ const mode = await stat(file)
262
+ .then((s) => s.mode & 0o777)
263
+ .catch(() => opts.createMode);
264
+ await writeFile(tmp, text, mode === undefined ? "utf8" : { encoding: "utf8", mode });
265
+ try {
266
+ JSON.parse(await readFile(tmp, "utf8"));
267
+ }
268
+ catch (err) {
269
+ await rm(tmp, { force: true });
270
+ throw new Error(`Refusing to write ${file}: the replacement did not parse back as JSON (${String(err)}). ` +
271
+ "The original file is untouched.");
272
+ }
273
+ await rename(tmp, file);
274
+ }
275
+ /**
276
+ * Register (or re-register) this server in one client's config.
277
+ *
278
+ * Merges: every other server, and every field of an existing entry we do not
279
+ * set, is preserved. `env` in particular holds live API keys on real machines
280
+ * — dropping it while "fixing" an entry would break unrelated servers and leak
281
+ * nothing but the user's afternoon.
282
+ */
283
+ export async function writeClientEntry(plan, opts) {
284
+ const base = { id: plan.id, client: plan.client, file: plan.file };
285
+ if (plan.state === "absent") {
286
+ return { ...base, action: "skipped", reason: "no config file for this client" };
287
+ }
288
+ if (plan.state === "unreadable") {
289
+ return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
290
+ }
291
+ if (plan.state === "matches")
292
+ return { ...base, action: "unchanged" };
293
+ if (plan.state === "missing-file") {
294
+ // Nothing to merge and nothing to back up. 0600 because this file is
295
+ // where the client will later keep its own credentials.
296
+ await writeJsonAtomic(plan.file, { [plan.serversKey]: { [ENTRY_KEY]: plan.desired } }, { createMode: 0o600 });
297
+ return { ...base, action: "written" };
298
+ }
299
+ // An entry someone edited by hand is not ours to replace unasked.
300
+ //
301
+ // `removeClientEntry` has always refused this and this function did not, so
302
+ // the protection existed on the half where the cost is lower. An acceptance
303
+ // pass found OPERATIONS.md promising it for both and measured `connect
304
+ // --clients claude-code` printing the hand-written entry it was about to
305
+ // destroy and then destroying it, exit 0.
306
+ //
307
+ // `consented` is what the interactive path passes after showing the diff and
308
+ // getting a yes — that prompt IS the consent, so nothing changes for someone
309
+ // watching it happen. Naming a client on the command line is not the same
310
+ // thing: it says which client, not "and overwrite whatever I put there".
311
+ if (plan.state === "differs" && opts.consented !== true) {
312
+ return {
313
+ ...base,
314
+ action: "skipped",
315
+ reason: "its entry has been edited by hand — re-run interactively (no --clients, " +
316
+ "no --yes) to see the difference and confirm, or pass --force to overwrite it",
317
+ };
318
+ }
319
+ const parsed = readJsonFile(plan.file);
320
+ if (!parsed.ok) {
321
+ return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
322
+ }
323
+ const root = mergeableRoot(parsed.value);
324
+ if (root === undefined)
325
+ return { ...base, action: "skipped", reason: NOT_AN_OBJECT };
326
+ const serversValue = mergeableRoot(root[plan.serversKey]);
327
+ if (serversValue === undefined) {
328
+ return { ...base, action: "skipped", reason: `its \`${plan.serversKey}\` is not a JSON object` };
329
+ }
330
+ const servers = { ...serversValue };
331
+ const existing = mergeableRoot(servers[ENTRY_KEY]);
332
+ if (existing === undefined) {
333
+ return { ...base, action: "skipped", reason: `its existing \`${ENTRY_KEY}\` entry is not a JSON object` };
334
+ }
335
+ servers[ENTRY_KEY] = { ...existing, ...plan.desired };
336
+ const backupPath = await backup(plan.file, opts.stamp);
337
+ await writeJsonAtomic(plan.file, { ...root, [plan.serversKey]: servers });
338
+ return { ...base, action: "written", backupPath };
339
+ }
340
+ /**
341
+ * Take our entry back out.
342
+ *
343
+ * Ships with the writer, not later: entries this tool created outliving the
344
+ * feature that created them is the documented way this project has already
345
+ * gone wrong once. An entry that no longer looks like ours is reported and
346
+ * LEFT ALONE — someone edited it deliberately, and deleting a hand-tuned entry
347
+ * because it no longer matches our template would be its own version of the
348
+ * same mistake.
349
+ */
350
+ export async function removeClientEntry(plan, opts) {
351
+ const base = { id: plan.id, client: plan.client, file: plan.file };
352
+ if (plan.state === "absent" || plan.state === "missing-file" || plan.state === "missing-entry") {
353
+ return { ...base, action: "unchanged" };
354
+ }
355
+ if (plan.state === "unreadable") {
356
+ return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
357
+ }
358
+ if (plan.state === "differs" && opts.force !== true) {
359
+ return {
360
+ ...base,
361
+ action: "skipped",
362
+ reason: "its entry has been edited by hand — remove it yourself, or pass --force",
363
+ };
364
+ }
365
+ const parsed = readJsonFile(plan.file);
366
+ if (!parsed.ok) {
367
+ return { ...base, action: "skipped", reason: "its config file does not parse as JSON" };
368
+ }
369
+ const root = mergeableRoot(parsed.value);
370
+ // Removal was already safe without this: our entry cannot be present in a
371
+ // file this shape, so the `ENTRY_KEY in servers` check below returns
372
+ // "unchanged" before anything is written. Kept anyway so both writers ask
373
+ // the same question of the same file, rather than one of them being correct
374
+ // by accident of a later check. A test for it was written and then deleted —
375
+ // it passed with the guard removed, so it was evidence of nothing.
376
+ if (root === undefined)
377
+ return { ...base, action: "unchanged" };
378
+ const serversValue = mergeableRoot(root[plan.serversKey]);
379
+ if (serversValue === undefined)
380
+ return { ...base, action: "unchanged" };
381
+ const servers = { ...serversValue };
382
+ if (!(ENTRY_KEY in servers))
383
+ return { ...base, action: "unchanged" };
384
+ delete servers[ENTRY_KEY];
385
+ const backupPath = await backup(plan.file, opts.stamp);
386
+ await writeJsonAtomic(plan.file, { ...root, [plan.serversKey]: servers });
387
+ return { ...base, action: "written", backupPath };
388
+ }
389
+ //# 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,47 @@ 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
+ * The one thing it does beyond reporting is DELETE an out-of-range numeric
88
+ * value from the entry, so the built-in default applies. That is not a second
89
+ * behaviour bolted on: a non-numeric `tier: metered` already ends up at the
90
+ * default, because `num()` cannot read it. `tier: -5` is different only in
91
+ * that the coercion CAN read it, which is exactly why it is dangerous — it
92
+ * reaches routing and wins. Deleting the key makes the two unusable cases
93
+ * behave the same way, which is what an operator reading either warning
94
+ * ("IGNORED, and the built-in default applies instead") is being told.
95
+ *
96
+ * Every caller warns before it parses the same object, so the deletion is
97
+ * visible to the parse that follows.
98
+ */
99
+ export declare function warnMistypedRouteValues(entry: Record<string, unknown>, label: string, warnings: string[]): void;
100
+ /**
101
+ * Two route entries sharing one name.
102
+ *
103
+ * The later entry wins outright, so everything the earlier one declared is
104
+ * gone. An acceptance pass measured the consequence: a first entry setting
105
+ * `safety_profile: read_only` and `workspace_policy: copy` was replaced by a
106
+ * second with neither, and the surviving route ran `workspace_edit` /
107
+ * `shared_locked` — silently LESS restrictive than what was written, with no
108
+ * warning on any surface.
109
+ */
110
+ export declare function warnDuplicateRouteNames(names: Array<string | undefined>, block: string, warnings: string[]): void;
70
111
  export declare function warnUnknownTopLevelKeys(raw: Record<string, unknown>, warnings: string[]): void;
71
112
  export declare function warnUnknownSafetyEnums(node: unknown, warnings: string[], where?: string): void;
72
113
  //# 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,aAoB/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;AAyFD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAAE,GACjB,IAAI,CAyDN;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;AA6BD,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"}