@edspencer/paddock 0.79.0 → 0.80.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 (96) hide show
  1. package/package.json +2 -2
  2. package/packages/server/dist/app.js +18 -14
  3. package/packages/server/dist/cli/args.d.ts +47 -7
  4. package/packages/server/dist/cli/args.js +112 -18
  5. package/packages/server/dist/cli/exit-codes.d.ts +49 -0
  6. package/packages/server/dist/cli/exit-codes.js +48 -0
  7. package/packages/server/dist/cli/paddock.js +84 -37
  8. package/packages/server/dist/cli/service/backend.d.ts +48 -4
  9. package/packages/server/dist/cli/service/index.d.ts +56 -2
  10. package/packages/server/dist/cli/service/index.js +283 -56
  11. package/packages/server/dist/cli/service/launchd.d.ts +8 -0
  12. package/packages/server/dist/cli/service/launchd.js +29 -7
  13. package/packages/server/dist/cli/service/logs.d.ts +39 -0
  14. package/packages/server/dist/cli/service/logs.js +115 -0
  15. package/packages/server/dist/cli/service/path-env.d.ts +27 -0
  16. package/packages/server/dist/cli/service/path-env.js +120 -0
  17. package/packages/server/dist/cli/service/ready.d.ts +22 -0
  18. package/packages/server/dist/cli/service/ready.js +30 -0
  19. package/packages/server/dist/cli/service/spec.d.ts +45 -6
  20. package/packages/server/dist/cli/service/spec.js +34 -7
  21. package/packages/server/dist/cli/service/systemd.d.ts +47 -4
  22. package/packages/server/dist/cli/service/systemd.js +127 -22
  23. package/packages/server/dist/management-mcp-server.d.ts +21 -3
  24. package/packages/server/dist/management-mcp-server.js +4 -13
  25. package/packages/server/dist/routes/meta.js +11 -3
  26. package/packages/server/dist/service-log.d.ts +29 -0
  27. package/packages/server/dist/service-log.js +104 -0
  28. package/packages/server/dist/start.js +11 -0
  29. package/packages/server/dist/version.d.ts +3 -0
  30. package/packages/server/dist/version.js +32 -0
  31. package/packages/server/package.json +1 -1
  32. package/packages/web/dist/assets/{DeleteProjectDialog-DE5KhPD_.js → DeleteProjectDialog-DHczLhwQ.js} +1 -1
  33. package/packages/web/dist/assets/DiscoverPage-BSGSzTEs.js +2 -0
  34. package/packages/web/dist/assets/{DiscoverView-DCEp5YDh.js → DiscoverView-DtTOpTji.js} +1 -1
  35. package/packages/web/dist/assets/{InstanceConfigPage-D09mz7nR.js → InstanceConfigPage-BRoBh1C1.js} +1 -1
  36. package/packages/web/dist/assets/{MarkdownRenderer-CujBOaSt.js → MarkdownRenderer-DJf8IJuk.js} +1 -1
  37. package/packages/web/dist/assets/{ProjectRedirect-DaOQz7Bx.js → ProjectRedirect-EioguUKW.js} +1 -1
  38. package/packages/web/dist/assets/{ProjectView-BxwVtKmQ.js → ProjectView-BiKnGfk6.js} +3 -3
  39. package/packages/web/dist/assets/{ProjectsGrid-BkULnAQF.js → ProjectsGrid-D6kfaIal.js} +1 -1
  40. package/packages/web/dist/assets/{RootHome-C7PQt4Mc.js → RootHome-DpcyuV6e.js} +1 -1
  41. package/packages/web/dist/assets/{Surfaces-XlC0TXmU.js → Surfaces-CHM29PhP.js} +1 -1
  42. package/packages/web/dist/assets/{arc-C1crrky_.js → arc-fiqD05Hu.js} +1 -1
  43. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR--3BQUIXc.js → architectureDiagram-3BPJPVTR-BuuB2Nkf.js} +1 -1
  44. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-EisqobqG.js → blockDiagram-GPEHLZMM-DQMBuzfi.js} +1 -1
  45. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-e6-wiFTa.js → c4Diagram-AAUBKEIU-BKN0UxCH.js} +1 -1
  46. package/packages/web/dist/assets/channel-B7yh6Su_.js +2 -0
  47. package/packages/web/dist/assets/{chunk-2J33WTMH-ToP1Bx9O.js → chunk-2J33WTMH-DW37EupX.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-4BX2VUAB-BD2icQlZ.js → chunk-4BX2VUAB-Dyv7zV_c.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-55IACEB6-D803_50z.js → chunk-55IACEB6-DzMR0dOk.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-727SXJPM-CiJOKx_U.js → chunk-727SXJPM-BdZ3Sor8.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BYni6r_j.js → chunk-AQP2D5EJ-B4i7_og0.js} +1 -1
  52. package/packages/web/dist/assets/{chunk-FMBD7UC4-D6QbCpWh.js → chunk-FMBD7UC4-jcTr85it.js} +1 -1
  53. package/packages/web/dist/assets/{chunk-ND2GUHAM-CvJSg6SC.js → chunk-ND2GUHAM-fgMsadqB.js} +1 -1
  54. package/packages/web/dist/assets/{chunk-QZHKN3VN-Cz0BtHbV.js → chunk-QZHKN3VN-bJNHYS1W.js} +1 -1
  55. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DIlhMPgi.js +2 -0
  56. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DIlhMPgi.js +2 -0
  57. package/packages/web/dist/assets/{core-BaJJFoBn.js → core-Bk4KFrmN.js} +1 -1
  58. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CzAP4gqt.js → cose-bilkent-S5V4N54A-BIqCSlQy.js} +1 -1
  59. package/packages/web/dist/assets/{dagre-BM42HDAG-CwIEzm8u.js → dagre-BM42HDAG-6nyhtVQz.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-2AECGRRQ-BEvdaFOg.js → diagram-2AECGRRQ-CjizfipR.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-5GNKFQAL-mk5qWzPB.js → diagram-5GNKFQAL-DlgB5vWb.js} +1 -1
  62. package/packages/web/dist/assets/{diagram-KO2AKTUF-C4lD_Kit.js → diagram-KO2AKTUF-CNC9qanV.js} +1 -1
  63. package/packages/web/dist/assets/{diagram-LMA3HP47-BmduqHdC.js → diagram-LMA3HP47-DSJ-s9lh.js} +1 -1
  64. package/packages/web/dist/assets/{diagram-OG6HWLK6-CzbvsaAK.js → diagram-OG6HWLK6-C9FoHBDg.js} +1 -1
  65. package/packages/web/dist/assets/{documentTitle-DvZmF8pw.js → documentTitle-Cj_AY97j.js} +1 -1
  66. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-B3Nm1nfI.js → erDiagram-TEJ5UH35-z6gku4Yt.js} +1 -1
  67. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DTAf1VHv.js → flowDiagram-I6XJVG4X-Dl5c_FgU.js} +1 -1
  68. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-BOeIF25v.js → ganttDiagram-6RSMTGT7-CIscJ-Dt.js} +1 -1
  69. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DR0ETY9X.js → gitGraphDiagram-PVQCEYII-CvttOXM_.js} +1 -1
  70. package/packages/web/dist/assets/{index-BqXu07yh.js → index-DN82do6Z.js} +2 -2
  71. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-iFsN2k7Z.js → infoDiagram-5YYISTIA-dzWYdDJj.js} +1 -1
  72. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-D0aKZ4O0.js → ishikawaDiagram-YF4QCWOH-8yX9k4bo.js} +1 -1
  73. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Dzt-NowV.js → journeyDiagram-JHISSGLW-BmitzJDR.js} +1 -1
  74. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-EurYbfWQ.js → kanban-definition-UN3LZRKU-BZmIkfkW.js} +1 -1
  75. package/packages/web/dist/assets/{linear-DjReSGMv.js → linear-BBLXE4o7.js} +1 -1
  76. package/packages/web/dist/assets/{mermaid.core-CPXZwBWr.js → mermaid.core-mSKff4yu.js} +4 -4
  77. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-BZKr8aHp.js → mindmap-definition-RKZ34NQL-BWAUa4CP.js} +1 -1
  78. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-CGIjsXTW.js → pieDiagram-4H26LBE5-VmVdiL4C.js} +1 -1
  79. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-CMk1ecJ2.js → quadrantDiagram-W4KKPZXB-Dybfv6NU.js} +1 -1
  80. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-DR2dI24k.js → requirementDiagram-4Y6WPE33-BPnOvB7w.js} +1 -1
  81. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-BP6UMMTr.js → sankeyDiagram-5OEKKPKP-YXIEmKw7.js} +1 -1
  82. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-4jrB80y7.js → sequenceDiagram-3UESZ5HK-DintIRUe.js} +1 -1
  83. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-yAWdzPCD.js → stateDiagram-AJRCARHV-eMsjahWp.js} +1 -1
  84. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CpAOGzbO.js +2 -0
  85. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-MFxU8aFE.js → timeline-definition-PNZ67QCA-as4zRCPn.js} +1 -1
  86. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-DptEBZ6E.js → vennDiagram-CIIHVFJN-CPzvUkM0.js} +1 -1
  87. package/packages/web/dist/assets/{wardley-L42UT6IY-Dk-HljGs.js → wardley-L42UT6IY-q6moXj5g.js} +1 -1
  88. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DhIz3dBE.js → wardleyDiagram-YWT4CUSO-BsEEb9Gm.js} +1 -1
  89. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-ytw3T4h8.js → xychartDiagram-2RQKCTM6-DSgZlcZW.js} +1 -1
  90. package/packages/web/dist/index.html +1 -1
  91. package/packages/web/dist/sw.js +1 -1
  92. package/packages/web/dist/assets/DiscoverPage-vaaC-Dji.js +0 -2
  93. package/packages/web/dist/assets/channel-D4JZjkXm.js +0 -2
  94. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BQVXohi4.js +0 -2
  95. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BQVXohi4.js +0 -2
  96. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-D0dmQsgw.js +0 -2
@@ -59,6 +59,39 @@ export function unitPath(homeDir, xdgConfigHome) {
59
59
  : (process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config")));
60
60
  return path.join(base, "systemd", "user", SYSTEMD_UNIT);
61
61
  }
62
+ /**
63
+ * Unit-file escaping, ported from Local Review's `server/service/systemd.ts`
64
+ * (`pct`, `cEscape`, `cUnescape`, `execWord`, `execWords`) @ cli-contract spec
65
+ * 0.1; local changes: doc comments only. Wave C4 moves these to
66
+ * `reference/service/` and re-syncs.
67
+ *
68
+ * `%` starts a specifier EVERYWHERE in a unit file, so it is doubled; `$`
69
+ * starts a variable in `ExecStart`, so it is doubled there too.
70
+ */
71
+ const pct = (s) => s.replace(/%/g, "%%");
72
+ const cEscape = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\t/g, "\\t");
73
+ const cUnescape = (s) => s.replace(/\\(.)/g, (_, c) => (c === "n" ? "\n" : c === "t" ? "\t" : c));
74
+ /** One `ExecStart` word: specifiers and variables escaped, quoted when it has to be. */
75
+ export function execWord(a) {
76
+ const s = pct(a).replace(/\$/g, "$$$$");
77
+ return s === "" || /[\s"'\\;]/.test(s) ? `"${cEscape(s)}"` : s;
78
+ }
79
+ /**
80
+ * The other way: the words of an `ExecStart` line.
81
+ *
82
+ * Reads both shapes on purpose. A unit written before #796's quoting is a bare
83
+ * whitespace-separated line, and its owner is exactly the person whose `status`
84
+ * has to keep working — so unquoted words pass through unchanged, and quoted
85
+ * ones are unescaped.
86
+ */
87
+ export function execWords(line) {
88
+ const out = [];
89
+ for (const m of line.matchAll(/"((?:[^"\\]|\\.)*)"|(\S+)/g)) {
90
+ const w = m[1] !== undefined ? cUnescape(m[1]) : m[2];
91
+ out.push(w.replace(/\$\$/g, "$").replace(/%%/g, "%"));
92
+ }
93
+ return out;
94
+ }
62
95
  /**
63
96
  * Render the unit.
64
97
  *
@@ -78,21 +111,44 @@ export function unitPath(homeDir, xdgConfigHome) {
78
111
  * "start at login" for a user unit; `multi-user.target` is the system-unit
79
112
  * spelling and does nothing here.
80
113
  *
81
- * No `StandardOutput=` — a user unit's output goes to the journal, which is
82
- * where a Linux user will look, and duplicating it into a file would give two
83
- * answers to "where are the logs".
114
+ * ## Logs go to a file, not the journal (SPEC §7)
115
+ *
116
+ * `StandardOutput=append:` / `StandardError=append:` name one file, the same
117
+ * one launchd's two keys name. The journal was the obvious choice and the wrong
118
+ * one for three reasons: `journalctl --user` shows nothing at all on a distro
119
+ * whose journal is volatile (the output is gone after a reboot, which is
120
+ * exactly when someone looks); it makes "where are the logs" have a different
121
+ * answer on each platform, so `paddock service logs` would be two
122
+ * implementations; and it rules out Windows, which the contract requires
123
+ * nothing rule out.
124
+ *
125
+ * The directory has to exist first — systemd creates the file, not its parent —
126
+ * which `install` below does.
127
+ *
128
+ * ## Every ExecStart word is quoted
129
+ *
130
+ * `%` starts a specifier anywhere in a unit file and `$` starts a variable in
131
+ * `ExecStart`, so both are escaped; a word containing whitespace, a quote or a
132
+ * backslash is quoted. Paths with a space in them are the common case
133
+ * (`/Users/Ed Spencer/.paddock`), and the old unqualified `join(" ")` wrote a
134
+ * unit systemd then split into the wrong arguments — a service that fails at
135
+ * login having printed nothing.
84
136
  */
85
137
  export function renderUnit(spec) {
86
- const exec = [spec.nodePath, spec.scriptPath, ...spec.args].join(" ");
87
- return `[Unit]
138
+ const exec = [spec.nodePath, spec.scriptPath, ...spec.args].map(execWord).join(" ");
139
+ return `# Generated by Paddock — \`paddock service install\` rewrites this file.
140
+ # Node: ${spec.nodePath}
141
+ [Unit]
88
142
  Description=Paddock — persistent, resumable Claude Code sessions, by project
89
143
  Documentation=https://paddock.edspencer.net
90
144
 
91
145
  [Service]
92
146
  Type=simple
93
147
  ExecStart=${exec}
94
- WorkingDirectory=${spec.workingDirectory}
95
- Environment=PATH=${spec.pathEnv}
148
+ WorkingDirectory=${pct(spec.workingDirectory)}
149
+ Environment="PATH=${cEscape(pct(spec.pathEnv))}"
150
+ StandardOutput=append:${pct(spec.logPath)}
151
+ StandardError=append:${pct(spec.logPath)}
96
152
  Restart=always
97
153
  RestartSec=10
98
154
 
@@ -100,6 +156,11 @@ RestartSec=10
100
156
  WantedBy=default.target
101
157
  `;
102
158
  }
159
+ /** The log file an installed unit appends to, or undefined if it names none. */
160
+ export function parseUnitLogPath(text) {
161
+ const m = /^StandardOutput=append:(.*)$/m.exec(text);
162
+ return m === null ? undefined : m[1].trim().replace(/%%/g, "%");
163
+ }
103
164
  /**
104
165
  * Does an installed unit carry `Restart=always` (#872)?
105
166
  *
@@ -117,15 +178,40 @@ export function parseUnitArgv(text) {
117
178
  const m = /^ExecStart=(.*)$/m.exec(text);
118
179
  if (m === null)
119
180
  return [];
120
- return m[1].trim().split(/\s+/).filter(Boolean);
181
+ return execWords(m[1].trim());
121
182
  }
122
- /** Run a `systemctl` and turn a non-zero exit into the manager's own words. */
183
+ /**
184
+ * What a `systemctl` failure means, in the manager's own words where it has any.
185
+ *
186
+ * The `status === null` branch is the one QA found: `spawnSync` reports a
187
+ * missing binary that way, with no output at all, so the message was
188
+ * `systemctl --user enable --now failed (exit null).` and then a blank line —
189
+ * which tells someone on a container with no systemd nothing whatsoever. That
190
+ * is also the single likeliest way this command fails on a machine it was never
191
+ * going to work on.
192
+ */
193
+ export function systemctlFailure(args, r) {
194
+ if (r.status === null || r.status === 127) {
195
+ return ("systemctl is not available, so Paddock cannot install a service here.\n" +
196
+ "It needs systemd's per-user instance (`systemctl --user`) — a container " +
197
+ "or a non-systemd distribution has none.\n" +
198
+ "Run `paddock serve` in a terminal, or keep it up with whatever this " +
199
+ "machine uses.");
200
+ }
201
+ if (/Failed to connect to (user scope )?bus|DBUS_SESSION_BUS_ADDRESS|XDG_RUNTIME_DIR/.test(r.stderr)) {
202
+ return (`cannot reach systemd's per-user instance (${r.stderr.trim()}).\n` +
203
+ "Run this from a login session, and check that $XDG_RUNTIME_DIR is set " +
204
+ "(usually /run/user/$(id -u)).");
205
+ }
206
+ return (`systemctl ${args.join(" ")} failed (exit ${String(r.status)}).\n` +
207
+ `${r.stderr || r.stdout}`.trim());
208
+ }
209
+ /** Run a `systemctl` and turn a non-zero exit into something actionable. */
123
210
  function systemctlOrThrow(run, args) {
124
211
  const r = run("systemctl", args);
125
212
  if (r.status === 0)
126
213
  return;
127
- throw new Error(`systemctl ${args.join(" ")} failed (exit ${String(r.status)}).\n` +
128
- `${r.stderr || r.stdout}`.trim());
214
+ throw new Error(systemctlFailure(args, r));
129
215
  }
130
216
  export function createSystemdBackend(run, homeDir, xdgConfigHome) {
131
217
  // Both forwarded, neither defaulted here: `unitPath` owns the precedence, and
@@ -140,15 +226,19 @@ export function createSystemdBackend(run, homeDir, xdgConfigHome) {
140
226
  install(spec) {
141
227
  fs.mkdirSync(path.dirname(file), { recursive: true });
142
228
  fs.mkdirSync(spec.workingDirectory, { recursive: true });
229
+ // systemd creates the log FILE for `append:` but not its directory, and a
230
+ // missing directory fails the unit at start with the server never having
231
+ // run — no output anywhere, because the thing that would have written it
232
+ // is what failed.
233
+ fs.mkdirSync(path.dirname(spec.logPath), { recursive: true });
143
234
  fs.writeFileSync(file, renderUnit(spec), "utf8");
144
235
  // Without this the manager keeps serving the previous unit from cache and
145
236
  // an "upgrade" silently changes nothing.
146
237
  run("systemctl", ["--user", "daemon-reload"]);
147
- const enabled = run("systemctl", ["--user", "enable", "--now", SYSTEMD_UNIT]);
148
- if (enabled.status !== 0) {
149
- throw new Error(`systemctl --user enable --now failed (exit ${String(enabled.status)}).\n` +
150
- `${enabled.stderr || enabled.stdout}`.trim());
151
- }
238
+ const args = ["--user", "enable", "--now", SYSTEMD_UNIT];
239
+ const enabled = run("systemctl", args);
240
+ if (enabled.status !== 0)
241
+ throw new Error(systemctlFailure(args, enabled));
152
242
  },
153
243
  uninstall() {
154
244
  const existed = fs.existsSync(file);
@@ -188,21 +278,36 @@ export function createSystemdBackend(run, homeDir, xdgConfigHome) {
188
278
  const active = run("systemctl", ["--user", "is-active", SYSTEMD_UNIT]);
189
279
  const pid = run("systemctl", ["--user", "show", "-p", "MainPID", "--value", SYSTEMD_UNIT]);
190
280
  const mainPid = pid.stdout.trim();
281
+ const logPath = parseUnitLogPath(text);
191
282
  return {
192
283
  registered: true,
193
284
  running: active.stdout.trim() === "active",
194
285
  ...(mainPid !== "" && mainPid !== "0" ? { pid: mainPid } : {}),
195
286
  argv: argv.slice(2),
287
+ ...(argv[0] !== undefined ? { nodePath: argv[0] } : {}),
288
+ ...(argv[1] !== undefined ? { cliPath: argv[1] } : {}),
289
+ ...(logPath !== undefined ? { logPath } : {}),
196
290
  keepsAlive: unitKeepsAlive(text),
197
291
  };
198
292
  },
199
- logsHint() {
200
- return ` Logs: journalctl --user -u ${SYSTEMD_UNIT} -f`;
201
- },
202
- lingerNote() {
293
+ /**
294
+ * `loginctl show-user <user> -p Linger` (SPEC §7).
295
+ *
296
+ * Parsed rather than trusted to exit codes: `loginctl` is absent in plenty
297
+ * of containers and exits non-zero for a user logind has no record of, and
298
+ * neither of those is "linger is off". An unparseable answer is reported as
299
+ * unknown, which prints nothing — the alternative is handing a `sudo`
300
+ * command to someone whose machine has no `loginctl` to run it with.
301
+ */
302
+ linger() {
203
303
  const user = os.userInfo().username;
204
- return (" A user service stops when you log out. To keep Paddock up:\n" +
205
- ` loginctl enable-linger ${user}`);
304
+ const r = run("loginctl", ["show-user", user, "-p", "Linger"]);
305
+ const m = /Linger=(yes|no)/i.exec(`${r.stdout}${r.stderr}`);
306
+ return {
307
+ applies: true,
308
+ user,
309
+ ...(m !== null ? { enabled: m[1].toLowerCase() === "yes" } : {}),
310
+ };
206
311
  },
207
312
  };
208
313
  }
@@ -1,3 +1,22 @@
1
+ /**
2
+ * The MCP server for external Management API callers (issue #312 M2).
3
+ *
4
+ * Adapts the SAME tool definitions the in-process keeper gets — an
5
+ * {@link InjectedMcpServerDef} produced by `selfMcpServerDefForOps` — onto the
6
+ * official MCP SDK, so an external client sees exactly the toolset a keeper
7
+ * sees, minus whatever its scope hides. Nothing is redefined here: if a tool is
8
+ * added to the self-MCP, it appears over `/mcp` with no further work, which is
9
+ * the "upgrade the server, every client's toolset upgrades" property the epic
10
+ * calls out.
11
+ *
12
+ * ── Why the LOW-LEVEL `Server`, not `McpServer` ────────────────────────────
13
+ * The high-level `McpServer.registerTool` wants Zod schemas and converts them to
14
+ * JSON Schema for the wire. Our tools already carry hand-written JSON Schema
15
+ * (shared with herdctl's injected-MCP bridge), so going through Zod would mean
16
+ * translating JSON Schema → Zod → JSON Schema and hoping the round trip is
17
+ * faithful. Implementing `tools/list` + `tools/call` directly passes our schemas
18
+ * through verbatim and keeps ONE source of truth for the tool surface.
19
+ */
1
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
21
  import type { InjectedMcpServerDef } from "@herdctl/core";
3
22
  import { type ManagementPrincipal } from "./management-policy.js";
@@ -5,9 +24,8 @@ import { type ManagementPrincipal } from "./management-policy.js";
5
24
  export declare const MANAGEMENT_MCP_SERVER_NAME = "paddock";
6
25
  /**
7
26
  * Package version, advertised to the client so it can report which Paddock it
8
- * is talking to. Resolved at runtime (dist/management-mcp-server.js →
9
- * ../package.json), the same way app.ts resolves it for the OpenAPI document,
10
- * so it tracks releases without a build step.
27
+ * is talking to. One resolution, shared with the OpenAPI document and
28
+ * `/api/health` (version.ts), so the three cannot disagree.
11
29
  */
12
30
  export declare const MANAGEMENT_MCP_SERVER_VERSION: string;
13
31
  /**
@@ -17,27 +17,18 @@
17
17
  * faithful. Implementing `tools/list` + `tools/call` directly passes our schemas
18
18
  * through verbatim and keeps ONE source of truth for the tool surface.
19
19
  */
20
- import { createRequire } from "node:module";
21
20
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
22
21
  import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
23
22
  import { ManagementDeniedError } from "./management-policy.js";
23
+ import { PADDOCK_VERSION } from "./version.js";
24
24
  /** Server identity advertised in the MCP `initialize` handshake. */
25
25
  export const MANAGEMENT_MCP_SERVER_NAME = "paddock";
26
26
  /**
27
27
  * Package version, advertised to the client so it can report which Paddock it
28
- * is talking to. Resolved at runtime (dist/management-mcp-server.js →
29
- * ../package.json), the same way app.ts resolves it for the OpenAPI document,
30
- * so it tracks releases without a build step.
28
+ * is talking to. One resolution, shared with the OpenAPI document and
29
+ * `/api/health` (version.ts), so the three cannot disagree.
31
30
  */
32
- export const MANAGEMENT_MCP_SERVER_VERSION = (() => {
33
- try {
34
- const require = createRequire(import.meta.url);
35
- return require("../package.json").version ?? "0.0.0";
36
- }
37
- catch {
38
- return "0.0.0";
39
- }
40
- })();
31
+ export const MANAGEMENT_MCP_SERVER_VERSION = PADDOCK_VERSION;
41
32
  /**
42
33
  * Build an MCP server exposing `def`'s tools to `principal`.
43
34
  *
@@ -5,6 +5,7 @@ import { TranscriptionError } from "../transcribe.js";
5
5
  import { resolveModels, resolveDefaultModel } from "../models.js";
6
6
  import { sendProjectError } from "../route-errors.js";
7
7
  import { cspFor, parseRangeHeader } from "../http-bytes.js";
8
+ import { PADDOCK_VERSION } from "../version.js";
8
9
  export function registerMetaRoutes(app, ctx) {
9
10
  const { herdctl, transcriber, attachments, cfg } = ctx;
10
11
  // --- voice dictation (#voice): capability probe + transcription -------
@@ -106,20 +107,27 @@ export function registerMetaRoutes(app, ctx) {
106
107
  },
107
108
  },
108
109
  }, async (req) => req.user);
110
+ // The CLI contract's one required server endpoint (SPEC §8): `{ ok, version }`,
111
+ // no auth. The version is load-bearing rather than decorative — it is how
112
+ // `paddock service status` can say that the version RUNNING differs from the
113
+ // one installed (an upgrade that never restarted), and how wave C2's
114
+ // `paddock update` will confirm the service came back as the version it just
115
+ // installed rather than merely coming back. `ALWAYS_PUBLIC_PATHS` in auth.ts
116
+ // keeps it answerable whatever `PADDOCK_AUTH_MODE` is set to.
109
117
  app.get("/api/health", {
110
118
  schema: {
111
119
  tags: ["System"],
112
120
  summary: "Health check",
113
- description: "Liveness probe. Returns a JSON object `{ ok: true }`.",
121
+ description: "Liveness probe. Returns a JSON object `{ ok: true, version: \"<semver>\" }`. Never requires authentication.",
114
122
  response: {
115
123
  200: {
116
- description: "Service is up.",
124
+ description: "Service is up, and the version it is running.",
117
125
  type: "object",
118
126
  additionalProperties: true,
119
127
  },
120
128
  },
121
129
  },
122
- }, async () => ({ ok: true }));
130
+ }, async () => ({ ok: true, version: PADDOCK_VERSION }));
123
131
  // --- instance-wide settings (issue #385) --------------------------------
124
132
  // A top-level admin screen over the frozen instance config. GET reports every
125
133
  // surfaced field (value / default / editable / sensitive / env-shadow); PUT
@@ -0,0 +1,29 @@
1
+ /** Rotate once the log passes this. */
2
+ export declare const ROTATE_AT_BYTES: number;
3
+ /** How many rotated generations to keep (`service.log.1` … `.3`). */
4
+ export declare const KEEP_GENERATIONS = 3;
5
+ /** How often to look. */
6
+ export declare const ROTATE_CHECK_MS: number;
7
+ export interface RotateOptions {
8
+ maxBytes?: number;
9
+ keep?: number;
10
+ /** Where a warning goes. Failing to rotate is never fatal. */
11
+ warn?: (message: string) => void;
12
+ }
13
+ /** `<dataDir>/logs/service.log` — the one file both streams append to. */
14
+ export declare function serviceLogFile(dataDir: string): string;
15
+ /**
16
+ * Rotate `<dataDir>/logs/service.log` if it has grown past the limit.
17
+ *
18
+ * Returns true if it rotated. Never throws: a log directory that has become
19
+ * read-only is worth a warning, not a server that will not start.
20
+ */
21
+ export declare function rotateServiceLog(dataDir: string, opts?: RotateOptions): boolean;
22
+ /**
23
+ * Rotate now, then every {@link ROTATE_CHECK_MS}. Returns a stop function.
24
+ *
25
+ * The timer is unref'd, so this cannot hold the process open — which matters
26
+ * because the one thing worse than an unrotated log is a `paddock serve` that
27
+ * does not exit on Ctrl-C.
28
+ */
29
+ export declare function startServiceLogRotation(dataDir: string, opts?: RotateOptions): () => void;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Rotating the service log (SPEC §7: "the tool rotates its own log").
3
+ *
4
+ * The service's stdout and stderr are appended to `<dataDir>/logs/service.log`
5
+ * by the supervisor itself — launchd's `StandardOutPath`, systemd's
6
+ * `StandardOutput=append:`. Nothing else rotates it: a `systemd --user` unit
7
+ * gets no journal rotation because it is not using the journal, and macOS has
8
+ * never had anything that would. Left alone, an instance that has been up for
9
+ * months has a log file nobody can open.
10
+ *
11
+ * ## Why copy-and-truncate, and not rename
12
+ *
13
+ * The usual rotation is `rename(log, log.1)` then create a new `log`. That is
14
+ * exactly wrong here, because **we are not the writer**. The supervisor holds
15
+ * an open append-mode descriptor on the inode; renaming the path does not move
16
+ * the descriptor, so every subsequent line goes to `service.log.1` and the
17
+ * fresh `service.log` stays empty for ever. `logrotate`'s `copytruncate` exists
18
+ * for this case and this is it: copy the bytes out, then `truncate()` the
19
+ * original to zero. The descriptor's next write — `O_APPEND`, so the offset is
20
+ * re-derived from the file length — lands at 0.
21
+ *
22
+ * The cost is honest and small: a line written between the copy and the
23
+ * truncate is lost. The alternative costs every line after the first rotation.
24
+ *
25
+ * ## When it runs
26
+ *
27
+ * At boot, and then on an interval. Not on every write, because the writes do
28
+ * not come through this process's own logger — they come through a file
29
+ * descriptor the supervisor opened — so there is no write to hook. The timer is
30
+ * `unref`'d: a rotation pending must never be the reason the process will not
31
+ * exit.
32
+ *
33
+ * It is a no-op anywhere there is no service log, which is every foreground
34
+ * `paddock serve`: the file does not exist, so nothing is copied and nothing is
35
+ * truncated. No caller has to know whether it is running under a supervisor.
36
+ */
37
+ import fs from "node:fs";
38
+ import path from "node:path";
39
+ /** Rotate once the log passes this. */
40
+ export const ROTATE_AT_BYTES = 8 * 1024 * 1024;
41
+ /** How many rotated generations to keep (`service.log.1` … `.3`). */
42
+ export const KEEP_GENERATIONS = 3;
43
+ /** How often to look. */
44
+ export const ROTATE_CHECK_MS = 15 * 60 * 1000;
45
+ /** `<dataDir>/logs/service.log` — the one file both streams append to. */
46
+ export function serviceLogFile(dataDir) {
47
+ return path.join(dataDir, "logs", "service.log");
48
+ }
49
+ /**
50
+ * Rotate `<dataDir>/logs/service.log` if it has grown past the limit.
51
+ *
52
+ * Returns true if it rotated. Never throws: a log directory that has become
53
+ * read-only is worth a warning, not a server that will not start.
54
+ */
55
+ export function rotateServiceLog(dataDir, opts = {}) {
56
+ const maxBytes = opts.maxBytes ?? ROTATE_AT_BYTES;
57
+ const keep = opts.keep ?? KEEP_GENERATIONS;
58
+ const file = serviceLogFile(dataDir);
59
+ try {
60
+ const size = fs.statSync(file).size;
61
+ if (size <= maxBytes)
62
+ return false;
63
+ }
64
+ catch {
65
+ // No log file: a foreground run, or a service that has not written yet.
66
+ return false;
67
+ }
68
+ try {
69
+ // Shuffle the generations down, oldest first, so nothing is overwritten
70
+ // before it has been moved. The oldest is dropped.
71
+ const gen = (n) => `${file}.${String(n)}`;
72
+ fs.rmSync(gen(keep), { force: true });
73
+ for (let n = keep - 1; n >= 1; n--) {
74
+ if (fs.existsSync(gen(n)))
75
+ fs.renameSync(gen(n), gen(n + 1));
76
+ }
77
+ fs.copyFileSync(file, gen(1));
78
+ // The truncate, not an unlink: see the header. The supervisor's descriptor
79
+ // has to keep working, and it only does if the inode survives.
80
+ fs.truncateSync(file, 0);
81
+ return true;
82
+ }
83
+ catch (err) {
84
+ opts.warn?.(`could not rotate ${file}: ${err.message}`);
85
+ return false;
86
+ }
87
+ }
88
+ /**
89
+ * Rotate now, then every {@link ROTATE_CHECK_MS}. Returns a stop function.
90
+ *
91
+ * The timer is unref'd, so this cannot hold the process open — which matters
92
+ * because the one thing worse than an unrotated log is a `paddock serve` that
93
+ * does not exit on Ctrl-C.
94
+ */
95
+ export function startServiceLogRotation(dataDir, opts = {}) {
96
+ rotateServiceLog(dataDir, opts);
97
+ const timer = setInterval(() => {
98
+ rotateServiceLog(dataDir, opts);
99
+ }, ROTATE_CHECK_MS);
100
+ timer.unref();
101
+ return () => {
102
+ clearInterval(timer);
103
+ };
104
+ }
@@ -8,9 +8,20 @@
8
8
  * depends on that to apply its own defaults (e.g. a data dir under $HOME).
9
9
  */
10
10
  import { buildApp } from "./app.js";
11
+ import { startServiceLogRotation } from "./service-log.js";
11
12
  export async function start() {
12
13
  const { app, cfg, close } = await buildApp();
14
+ // Nothing else rotates the service log: the supervisor appends to it through
15
+ // its own descriptor, a `systemd --user` unit writing to a file gets no
16
+ // journal rotation, and macOS has nothing that would (SPEC §7). A no-op on a
17
+ // foreground run, where that file does not exist — see service-log.ts.
18
+ const stopRotation = startServiceLogRotation(cfg.dataDir, {
19
+ warn: (message) => {
20
+ app.log.warn(message);
21
+ },
22
+ });
13
23
  const shutdown = async () => {
24
+ stopRotation();
14
25
  await close();
15
26
  process.exit(0);
16
27
  };
@@ -0,0 +1,3 @@
1
+ export declare const PADDOCK_VERSION: string;
2
+ /** What the product is called in prose, for `serve`'s banner line (SPEC §3). */
3
+ export declare const PADDOCK_NAME = "Paddock";
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The running Paddock version, resolved once.
3
+ *
4
+ * Three surfaces have to agree on this string and now do, because they read it
5
+ * from one place: the OpenAPI document's `info.version` (`app.ts`), the
6
+ * management MCP server's advertised version, and — the reason this module
7
+ * exists — `GET /api/health`, which the CLI contract requires to carry the
8
+ * version so that `paddock service status` can compare the version that is
9
+ * RUNNING with the version that is installed (SPEC §8).
10
+ *
11
+ * `createRequire(import.meta.url)` resolves against this module's own path, so
12
+ * `../package.json` is `packages/server/package.json` from `dist/version.js` in
13
+ * both layouts the package ships in (the repo, and the synthesized npm tarball
14
+ * — see `scripts/make-npm-package.mjs`, which keeps that manifest precisely so
15
+ * a version is readable at runtime).
16
+ *
17
+ * Never throws. A missing or unreadable manifest degrades to `"0.0.0"`: a
18
+ * health endpoint that 500s because it could not find its own version would be
19
+ * a worse failure than one that answers with a version nobody shipped.
20
+ */
21
+ import { createRequire } from "node:module";
22
+ export const PADDOCK_VERSION = (() => {
23
+ try {
24
+ const require = createRequire(import.meta.url);
25
+ return require("../package.json").version ?? "0.0.0";
26
+ }
27
+ catch {
28
+ return "0.0.0";
29
+ }
30
+ })();
31
+ /** What the product is called in prose, for `serve`'s banner line (SPEC §3). */
32
+ export const PADDOCK_NAME = "Paddock";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,2 +1,2 @@
1
- import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-BqXu07yh.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
1
+ import{r as l,e as k,j as s,A as y,d as p,a as w}from"./index-DN82do6Z.js";import{c as N}from"./lastTab-CU1zsF0j.js";function v({open:e,title:t,message:c,confirmLabel:n="Delete",cancelLabel:o="Cancel",danger:a=!0,wide:h=!1,dismissOnBackdrop:b=!0,onConfirm:g,onClose:d}){const[r,m]=l.useState(!1),[f,u]=l.useState(null);if(l.useEffect(()=>{e?u(null):m(!1)},[e]),k(e&&!r,d),!e)return null;const j=async()=>{m(!0),u(null);try{await g()}catch(i){u(i instanceof Error?i.message:"Action failed"),m(!1)}};return s.jsx("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-overlay p-4 backdrop-blur-sm",onClick:()=>b&&!r&&d(),children:s.jsxs("div",{className:`w-full ${h?"max-w-md":"max-w-sm"} animate-scale-in rounded-2xl border border-edge bg-surface-raised p-6 shadow-2xl`,onClick:i=>i.stopPropagation(),role:"alertdialog","aria-modal":"true",children:[s.jsxs("div",{className:"flex items-start gap-3",children:[a&&s.jsx("span",{className:"mt-0.5 flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-danger-soft text-danger",children:s.jsx(y,{width:18,height:18})}),s.jsxs("div",{className:"min-w-0",children:[s.jsx("h2",{className:"text-base font-semibold",children:t}),s.jsx("div",{className:"mt-1.5 text-sm text-fg-muted",children:c})]})]}),f&&s.jsx("p",{className:"mt-4 rounded-lg bg-danger-soft px-3 py-2 text-sm text-danger",children:f}),s.jsxs("div",{className:"mt-5 flex justify-end gap-2",children:[s.jsx("button",{type:"button",className:"btn-ghost",onClick:d,disabled:r,children:o}),s.jsx("button",{type:"button",className:a?"btn btn-danger":"btn-primary",onClick:j,disabled:r,children:r?"Working…":n})]})]})})}function P(e,t){return e===t?!0:e.startsWith(t.endsWith("/")?t:`${t}/`)}function x(e){return!P(e.workingDir,e.dir)}function C({project:e}){const t=s.jsx("span",{className:"font-medium text-fg",children:e.name});return x(e)?s.jsxs(s.Fragment,{children:[t," will be unlinked. Paddock’s notes, settings and chats for it are permanently removed, and this cannot be undone — but the directory it works in is left alone:",s.jsx("span",{className:"mt-1.5 block break-all font-mono text-xs text-fg",children:e.workingDir}),s.jsx("span",{className:"mt-1.5 block",children:"Nothing in there is deleted."})]}):s.jsxs(s.Fragment,{children:[t," and all its chats and files will be permanently removed. This cannot be undone."]})}function D(){const{remove:e}=p();return l.useCallback(async t=>{await w.deleteProject(t),e(t),N(t)},[e])}function W({project:e,open:t,onClose:c,onDeleted:n}){const o=D(),a=x(e);return s.jsx(v,{open:t,title:"Delete project?",message:s.jsx(C,{project:e}),confirmLabel:"Delete project",wide:a,dismissOnBackdrop:!a,onConfirm:async()=>{await o(e.slug),n==null||n()},onClose:c})}export{v as C,W as D,x as d};
2
2
 
@@ -0,0 +1,2 @@
1
+ import{j as o}from"./index-DN82do6Z.js";import{D as r}from"./DiscoverView-DtTOpTji.js";import{u as e}from"./documentTitle-Cj_AY97j.js";import"./Surfaces-CHM29PhP.js";function p(){return e(["Discover"]),o.jsx(r,{})}export{p as DiscoverPage};
2
+
@@ -1,2 +1,2 @@
1
- import{h as oe,d as ie,a1 as de,r as u,a as A,j as s,W as Q,A as X,a5 as Y,J as $,a2 as ce,U as le,i as he,a6 as ue,a7 as J,K as T,c as U,a8 as pe,v as xe,Z as V}from"./index-BqXu07yh.js";import{E as R,C as me}from"./Surfaces-XlC0TXmU.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:Z(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:Z(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function W(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=W(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=W(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function Z(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,K=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(Q,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?K:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:K})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:U("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",q(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(Q,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:U("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":_(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:_(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[q(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function _(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function q(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
1
+ import{h as oe,d as ie,a1 as de,r as u,a as A,j as s,W as Q,A as X,a5 as Y,J as $,a2 as ce,U as le,i as he,a6 as ue,a7 as J,K as T,c as U,a8 as pe,v as xe,Z as V}from"./index-DN82do6Z.js";import{E as R,C as me}from"./Surfaces-CHM29PhP.js";function ge(e){const t={};for(const n of e)t[n.path]={checked:!0,expanded:!1,loading:!1,error:null};return t}function fe(e){if(!e.sessions||!e.selected)return e.checked?"on":"off";const t=e.sessions.sessions.length,n=e.selected.size;return t===0||n===0?"off":n===t?"on":"mixed"}function C(e,t){return e.sessions&&e.selected?e.selected.size:e.checked?t.sessionCount:0}function je(e,t){return t.reduce((n,r)=>n+C(e[r.path]??O(),r),0)}function be(e,t){return t.filter(n=>C(e[n.path]??O(),n)>0)}function we(e,t,n){const r=e[t];if(!r)return e;const a={...r,checked:n};return r.sessions&&(a.selected=n?new Set(r.sessions.sessions.map(c=>c.sessionId)):new Set),{...e,[t]:a}}function ye(e,t,n){const r=e[t];if(!r||!r.sessions)return e;const a=new Set(r.selected??[]);return a.delete(n)||a.add(n),{...e,[t]:{...r,selected:a,checked:a.size>0}}}function ve(e,t,n){const r=e[t];if(!r)return e;const a=r.checked?new Set(n.sessions.map(c=>c.sessionId)):new Set;return{...e,[t]:{...r,sessions:n,selected:a,loading:!1,error:null}}}function D(e,t,n){const r=e[t];return r?{...e,[t]:{...r,...n}}:e}function Ne(e,t){const n=e??O();return C(n,t)===0?null:n.sessions&&n.selected?{sessionIds:[...n.selected]}:{}}function O(){return{checked:!1,expanded:!1,loading:!1,error:null}}function ke(e){return e.error!==void 0?e.stage==="create"?"danger":"warn":e.adopted>0?"success":"warn"}async function Se(e,t,n){let r;try{r=await e.createProject({name:t.name,slug:t.suggestedSlug,path:t.path,managed:!1})}catch(a){return{stage:"create",adopted:0,skipped:[],error:Z(a)}}try{const a=await e.adoptChats(r.slug,{sourceCwd:r.workingDir,...n.sessionIds?{sessionIds:n.sessionIds}:{}});return{project:r,adopted:a.adopted.length,skipped:a.skipped}}catch(a){return{stage:"import",project:r,adopted:0,skipped:[],error:Z(a)}}}async function $e(e,t,n){const r=[];for(const a of t){n(a.candidate.path,"running");const c=await Se(e,a.candidate,a.plan);r.push(c),n(a.candidate.path,"done",c)}return r}const Ce={sidechain:"sub-agent transcript","already-adopted":"already adopted","destination-exists":"a chat with that id already exists here","attributed-to-run":"belongs to a Paddock run",unreadable:"transcript unreadable","placement-failed":"copy failed","record-failed":"could not be recorded"};function W(e){if(e.length===0)return"";const t=[...new Set(e.map(n=>Ce[n.reason]??n.reason))];return`${e.length} skipped (${t.join(", ")})`}function Ie(e,t){var l;if(e.stage==="create")return`Couldn't create the project: ${e.error}`;const n=((l=e.project)==null?void 0:l.slug)??t.suggestedSlug;if(e.stage==="import")return`Project “${n}” was created, but importing its chats failed: ${e.error}. It is there and empty — you can import them from the project itself.`;if(e.adopted===0){const m=W(e.skipped);return t.recordedPath!==void 0?`Project “${n}” was created but no chats came with it. Its transcripts record ${t.recordedPath}, a different spelling of ${t.path}, and an import matches on the exact path.`:m?`Project “${n}” was created but no chats came with it — ${m}.`:`Project “${n}” was created but no chats came with it.`}const r=e.adopted,a=W(e.skipped),c=`Adopted ${r} chat${r===1?"":"s"} into “${n}”`;return a?`${c} — ${a}.`:`${c}.`}function Z(e){return e instanceof Error?e.message:String(e)}function Ge({firstRun:e=!1,embedded:t=!1,onLeave:n,onStartChat:r}){const a=oe(),{refresh:c}=ie(),l=de(),[m,d]=u.useState(!1),[j,I]=u.useState(!1),[h,x]=u.useState(null),[y,z]=u.useState(null),[F,P]=u.useState(!0),[f,v]=u.useState({}),[k,G]=u.useState(!1),[L,H]=u.useState({}),[E,B]=u.useState({}),[S,ee]=u.useState(!1);u.useEffect(()=>{let o=!0;return P(!0),z(null),A.discover({includeNonGit:m,includeOutsideHome:j}).then(i=>{o&&(x(i),v(ge(i.candidates)),P(!1))}).catch(i=>{o&&(z(i instanceof Error?i.message:"Discovery failed"),P(!1))}),()=>{o=!1}},[m,j]);const w=(h==null?void 0:h.candidates)??[],M=u.useMemo(()=>je(f,w),[f,w]),b=u.useMemo(()=>be(f,w),[f,w]),se=u.useCallback(o=>{v(p=>{const g=p[o.path];if(!g)return p;const N=D(p,o.path,{expanded:!g.expanded});return g.expanded||g.sessions||g.loading?N:D(N,o.path,{loading:!0,error:null})});const i=f[o.path];!i||i.expanded||i.sessions||i.loading||A.discoverSessions(o.path).then(p=>v(g=>ve(g,o.path,p))).catch(p=>v(g=>D(g,o.path,{loading:!1,error:p instanceof Error?p.message:"Could not read this directory's sessions"})))},[f]),te=u.useCallback(async()=>{if(k)return;G(!0),H(Object.fromEntries(b.map(i=>[i.path,"pending"]))),B({});const o=b.map(i=>({candidate:i,plan:Ne(f[i.path],i)??{}}));await $e(A,o,(i,p,g)=>{H(N=>({...N,[i]:p})),g&&B(N=>({...N,[i]:g}))}),G(!1),ee(!0),await c()},[b,f,k,c]),ne=u.useCallback(async()=>{await c(),n==null||n(),a("/")},[a,c,n]),ae=Object.values(E).reduce((o,i)=>o+i.adopted,0),re=Object.values(E).filter(o=>o.project).length,K=s.jsxs(s.Fragment,{children:[s.jsx("p",{className:"mb-5 text-sm text-fg-muted",children:e?"If you have used Claude Code in a terminal on this machine, Paddock can adopt that history: each directory becomes a project with its conversations already in it.":"Directories on this machine with existing Claude Code history. Adopting one links it as a project and brings its conversations in; your own history is never moved or deleted."}),S&&s.jsx(Te,{imported:ae,projects:re,onGetStarted:ne}),y!==null&&s.jsx(Q,{tone:"danger",icon:s.jsx(X,{width:16,height:16}),children:y}),F&&s.jsx(De,{}),!F&&y===null&&h!==null&&s.jsxs(s.Fragment,{children:[h.scanned===0?s.jsx(Ae,{claudeHome:h.claudeHome,onStartChat:r}):w.length===0?s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:"Nothing to adopt",body:"Paddock found Claude Code history on this machine, but none of it is available to adopt right now. The tally below says what was skipped, and the two toggles relax the rules that are safe to relax."}):s.jsx("ul",{className:"space-y-2",children:w.map(o=>s.jsx(Pe,{candidate:o,row:f[o.path],status:L[o.path],outcome:E[o.path],locked:k||S,onToggleRow:i=>v(p=>we(p,o.path,i)),onToggleSession:i=>v(p=>ye(p,o.path,i)),onToggleExpanded:()=>se(o)},o.path))}),!S&&h.scanned>0&&s.jsx(Ee,{result:h,includeNonGit:m,includeOutsideHome:j,disabled:k,onIncludeNonGit:d,onIncludeOutsideHome:I}),w.length>0&&!S&&s.jsxs("div",{className:"mt-5 flex items-center justify-between gap-3 border-t border-edge pt-4",children:[s.jsxs("span",{className:"text-sm text-fg-muted",children:[M," conversation",M===1?"":"s"," from ",b.length," director",b.length===1?"y":"ies"]}),s.jsxs($,{variant:"primary",onClick:()=>void te(),disabled:b.length===0,loading:k,loadingLabel:"Adopting…",children:["Adopt ",b.length," project",b.length===1?"":"s"]})]})]})]});return t?K:s.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[s.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-edge px-3 pb-2.5 sm:px-6 lg:py-4",children:[s.jsx("button",{type:"button",onClick:()=>l==null?void 0:l.openNav(),"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:s.jsx(ce,{width:20,height:20})}),s.jsx(le,{width:18,height:18,className:"shrink-0 text-fg-subtle"}),s.jsx("h1",{className:"text-md font-semibold tracking-tight",children:"Discover"})]}),s.jsx("div",{className:"flex-1 overflow-y-auto overscroll-contain",children:s.jsx("div",{className:"mx-auto max-w-3xl px-6 py-6",children:K})})]})}function Pe({candidate:e,row:t,status:n,outcome:r,locked:a,onToggleRow:c,onToggleSession:l,onToggleExpanded:m}){const d=t??{checked:!1,expanded:!1,loading:!1,error:null},j=fe(d),I=C(d,e),h=r?ke(r):null;return s.jsx("li",{children:s.jsxs(me,{flush:!0,className:U("overflow-hidden",h==="success"&&"border-success-edge bg-success-soft",h==="warn"&&"border-warn-edge bg-warn-soft",h==="danger"&&"border-danger-edge bg-danger-soft",n==="running"&&"border-accent-edge"),"data-testid":`discover-row-${e.suggestedSlug}`,"data-tone":h??void 0,children:[s.jsxs("div",{className:"flex items-start gap-3 px-3 py-2.5",children:[s.jsx(J,{className:"mt-1",checked:j==="on",indeterminate:j==="mixed",disabled:a,"aria-label":`Adopt ${e.path}`,onChange:x=>c(x.target.checked)}),s.jsxs("div",{className:"min-w-0 flex-1",children:[s.jsx("code",{className:"block break-all text-sm text-fg",children:e.path}),s.jsxs("div",{className:"mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-fg-muted",children:[s.jsxs("span",{className:"tabular",children:[e.sessionCount," conversation",e.sessionCount===1?"":"s"]}),e.filteredCount>0&&s.jsxs("span",{className:"tabular",title:"Empty, slash-command-only or sweeper transcripts, withheld as noise",children:["· ",e.filteredCount," filtered out"]}),e.lastSessionAt!==void 0&&s.jsxs("span",{className:"tabular",children:["· last ",q(e.lastSessionAt)]}),e.gitRemote!==void 0&&s.jsx(T,{tone:"lineage",size:"sm",children:e.gitRemote}),!e.hasGit&&s.jsx(T,{tone:"neutral",size:"sm",children:"no git"}),!e.insideHome&&s.jsx(T,{tone:"neutral",size:"sm",children:"outside home"})]}),e.recordedPath!==void 0&&s.jsx(Q,{tone:"warn",className:"mt-2",icon:s.jsx(X,{width:14,height:14}),children:s.jsxs("span",{className:"text-xs",children:["These transcripts record"," ",s.jsx("code",{className:"break-all",children:e.recordedPath})," — a different spelling of this path. The import matches on the exact path, so it may bring nothing in."]})}),r&&s.jsx("p",{className:U("mt-2 text-xs",h==="success"&&"text-success",h==="warn"&&"text-warn",h==="danger"&&"text-danger"),children:Ie(r,e)}),n==="running"&&!r&&s.jsx("p",{className:"mt-2 text-xs text-fg-muted",children:"Adopting…"})]}),s.jsxs("span",{className:"flex shrink-0 items-center gap-1",children:[s.jsx("span",{className:"tabular text-xs text-fg-subtle",children:I}),s.jsx($,{size:"icon-sm",variant:"subtle","aria-label":d.expanded?"Hide conversations":"Show conversations","aria-expanded":d.expanded,onClick:m,children:d.expanded?s.jsx(pe,{width:14,height:14}):s.jsx(xe,{width:14,height:14})})]})]}),d.expanded&&s.jsxs("div",{className:"border-t border-edge px-3 py-2",children:[d.loading&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Reading transcripts…"}),d.error!==null&&s.jsx("p",{className:"py-2 text-xs text-danger",children:d.error}),d.sessions&&d.sessions.sessions.length===0&&s.jsx("p",{className:"py-2 text-xs text-fg-muted",children:"Nothing to adopt here any more — it may have been adopted since the scan."}),d.sessions&&d.sessions.sessions.length>0&&s.jsx("ul",{className:"space-y-1",children:d.sessions.sessions.map(x=>{var y;return s.jsx("li",{children:s.jsxs("label",{className:"flex cursor-pointer items-start gap-2 rounded-lg px-2 py-1.5 transition-colors hover:bg-surface-hover",children:[s.jsx(J,{checked:((y=d.selected)==null?void 0:y.has(x.sessionId))??!1,disabled:a,"aria-label":_(x),onChange:()=>l(x.sessionId)}),s.jsxs("span",{className:"min-w-0 flex-1",children:[s.jsx("span",{className:"block truncate text-xs text-fg",children:_(x)}),s.jsxs("span",{className:"tabular mt-0.5 block text-3xs text-fg-muted",children:[q(x.mtime)," · ",Oe(x.sizeBytes)]})]})]})},x.sessionId)})}),d.sessions&&d.sessions.filtered.length>0&&s.jsxs("p",{className:"mt-2 px-2 text-3xs text-fg-subtle",children:[d.sessions.filtered.length," more withheld as noise (",[...new Set(d.sessions.filtered.map(x=>x.reason))].join(", "),")."]})]})]})})}function Ee({result:e,includeNonGit:t,includeOutsideHome:n,disabled:r,onIncludeNonGit:a,onIncludeOutsideHome:c}){const l=e.excluded["no-git"]??0,m=e.excluded["outside-home"]??0,d=l>0||t,j=m>0||n;return e.scanned===0?null:s.jsxs("div",{className:"mt-5 space-y-2 border-t border-edge pt-4",children:[s.jsx("p",{className:"text-xs text-fg-muted",children:Re(e)}),d&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:t,disabled:r,onChange:a,label:"Also offer directories without a git repository"}),s.jsxs("span",{children:["Also offer directories without a git repository",l>0&&` (${l} hidden)`]})]}),j&&s.jsxs("div",{className:"flex items-center gap-2 text-xs text-fg-muted",children:[s.jsx(V,{checked:n,disabled:r,onChange:c,label:"Also offer directories outside your home directory"}),s.jsxs("span",{children:["Also offer directories outside ",s.jsx("code",{children:e.homeDir}),m>0&&` (${m} hidden)`]})]})]})}function Ae({claudeHome:e,onStartChat:t}){return s.jsx(R,{variant:"panel",icon:s.jsx(ue,{width:22,height:22}),title:"Nothing to adopt yet",body:s.jsxs(s.Fragment,{children:["No Claude Code history has been recorded under"," ",s.jsx("code",{className:"break-all",children:e}),". That is normal on a new install, and on a container whose Claude home is its own rather than yours — nothing is wrong. Start a chat here and Paddock writes its own history; or bind-mount an existing one there and re-open this page, or import it headlessly with ",s.jsx("code",{children:"npm run import-chats"}),"."]}),action:t&&s.jsx($,{variant:"primary",icon:s.jsx(he,{width:14,height:14}),onClick:t,children:"Start a chat"})})}function Te({imported:e,projects:t,onGetStarted:n}){return s.jsx("div",{className:"mb-6",children:s.jsx(R,{variant:"panel",icon:s.jsx(Y,{width:22,height:22}),title:t===0?"Nothing was adopted":`${t} project${t===1?"":"s"}, ${e} conversation${e===1?"":"s"}`,body:t===0?"Every row failed. Each one says why below.":"They are in the sidebar now. Anything that did not land says why below.",action:s.jsx($,{variant:"primary",onClick:()=>void n(),children:"Get started"})})})}function De(){return s.jsx("div",{className:"space-y-2","aria-label":"Scanning",role:"status",children:[0,1,2].map(e=>s.jsx("div",{className:"h-16 animate-pulse rounded-2xl bg-surface-active"},e))})}function Re(e){const t=[],n=[["temp-root",a=>`${a} in temp directories`],["no-git",a=>`${a} without a git repository`],["outside-home",a=>`${a} outside your home directory`],["already-managed",a=>`${a} already a project`],["no-sessions",a=>`${a} with nothing to adopt`],["no-recorded-cwd",a=>`${a} with no recorded directory`],["missing",a=>`${a} no longer on disk`],["home-root",a=>`${a} your home directory itself`],["system-path",a=>`${a} system directories`],["paddock-internal",a=>`${a} inside Paddock's own data`]];for(const[a,c]of n){const l=e.excluded[a];l!==void 0&&l>0&&t.push(c(l))}const r=`${e.scanned} transcript folder${e.scanned===1?"":"s"} scanned`;return t.length===0?`${r}.`:`${r}; skipped ${t.join(", ")}.`}function _(e){var r,a;const t=(r=e.autoName)==null?void 0:r.trim();if(t!==void 0&&t!=="")return t;const n=(a=e.preview)==null?void 0:a.trim();return n!==void 0&&n!==""?n:e.sessionId}function q(e){const t=new Date(e);return Number.isNaN(t.getTime())?"unknown date":t.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function Oe(e){return e<1024?`${e} B`:e<1024*1024?`${Math.round(e/1024)} kB`:`${(e/(1024*1024)).toFixed(1)} MB`}export{Ge as D};
2
2