@edspencer/paddock 0.64.0 → 0.65.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 (73) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/management-ops.js +40 -1
  3. package/packages/server/dist/management-policy.d.ts +4 -3
  4. package/packages/server/dist/management-policy.js +5 -2
  5. package/packages/server/dist/projects.d.ts +12 -3
  6. package/packages/server/dist/projects.js +14 -5
  7. package/packages/server/dist/self-mcp-descriptions.d.ts +1 -0
  8. package/packages/server/dist/self-mcp-descriptions.js +14 -1
  9. package/packages/server/dist/self-mcp-types.d.ts +26 -4
  10. package/packages/server/dist/self-mcp-write.d.ts +21 -0
  11. package/packages/server/dist/self-mcp-write.js +44 -0
  12. package/packages/server/dist/self-mcp.d.ts +7 -6
  13. package/packages/server/dist/self-mcp.js +36 -10
  14. package/packages/server/package.json +1 -1
  15. package/packages/web/dist/assets/{InstanceConfigPage-Blr1a-hx.js → InstanceConfigPage-DcprTH2W.js} +1 -1
  16. package/packages/web/dist/assets/{MarkdownRenderer-R6YzfI9Y.js → MarkdownRenderer-Ri1LwAUN.js} +1 -1
  17. package/packages/web/dist/assets/{ProjectRedirect-Cy-7Thc-.js → ProjectRedirect-Dze5H8Xy.js} +1 -1
  18. package/packages/web/dist/assets/{ProjectView-bMX4GEau.js → ProjectView-PrA8purE.js} +3 -3
  19. package/packages/web/dist/assets/{ProjectsGrid-C9XHf4eS.js → ProjectsGrid-BidsM7ah.js} +1 -1
  20. package/packages/web/dist/assets/{arc-B_42F7Yk.js → arc-eQ3ryE9c.js} +1 -1
  21. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-BAXzSOyw.js → architectureDiagram-3BPJPVTR-Dc6Tj2Ai.js} +1 -1
  22. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-BdYEdA-3.js → blockDiagram-GPEHLZMM-DBX5pma_.js} +1 -1
  23. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-Cc4o15R5.js → c4Diagram-AAUBKEIU-CnWrtTpa.js} +1 -1
  24. package/packages/web/dist/assets/channel-B4sED4OP.js +2 -0
  25. package/packages/web/dist/assets/{chunk-2J33WTMH-CeuxHjIm.js → chunk-2J33WTMH-2qmLvtsr.js} +1 -1
  26. package/packages/web/dist/assets/{chunk-4BX2VUAB-DnZfrAaV.js → chunk-4BX2VUAB-Dn-LUDMV.js} +1 -1
  27. package/packages/web/dist/assets/{chunk-55IACEB6-DFE6UYOT.js → chunk-55IACEB6-DB0augUY.js} +1 -1
  28. package/packages/web/dist/assets/{chunk-727SXJPM-YVHQ5cHc.js → chunk-727SXJPM-BPQQM0i8.js} +1 -1
  29. package/packages/web/dist/assets/{chunk-AQP2D5EJ-5BYSbg-F.js → chunk-AQP2D5EJ-DOhfwA-x.js} +1 -1
  30. package/packages/web/dist/assets/{chunk-FMBD7UC4-HBcelndR.js → chunk-FMBD7UC4-CatHHdFD.js} +1 -1
  31. package/packages/web/dist/assets/{chunk-ND2GUHAM-2AwEsdxJ.js → chunk-ND2GUHAM-B645aAlG.js} +1 -1
  32. package/packages/web/dist/assets/{chunk-QZHKN3VN-BPpaZ6ct.js → chunk-QZHKN3VN-D1fAaPSZ.js} +1 -1
  33. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BlVeHFir.js +2 -0
  34. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BlVeHFir.js +2 -0
  35. package/packages/web/dist/assets/{core-Cu_4vw1U.js → core-B4t7D8Om.js} +1 -1
  36. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-DWkF0NrF.js → cose-bilkent-S5V4N54A-BLdKDAwa.js} +1 -1
  37. package/packages/web/dist/assets/{dagre-BM42HDAG-DTgNmO9I.js → dagre-BM42HDAG-CbTbTVKv.js} +1 -1
  38. package/packages/web/dist/assets/{diagram-2AECGRRQ-DC28b7YS.js → diagram-2AECGRRQ-CorSS29_.js} +1 -1
  39. package/packages/web/dist/assets/{diagram-5GNKFQAL-_WvuLno6.js → diagram-5GNKFQAL-PLKivts0.js} +1 -1
  40. package/packages/web/dist/assets/{diagram-KO2AKTUF-BEYxPfWq.js → diagram-KO2AKTUF-CUSIBacZ.js} +1 -1
  41. package/packages/web/dist/assets/{diagram-LMA3HP47-Biyev7x0.js → diagram-LMA3HP47-CZ69mAZs.js} +1 -1
  42. package/packages/web/dist/assets/{diagram-OG6HWLK6-u47NLW3S.js → diagram-OG6HWLK6-Cmf042Rh.js} +1 -1
  43. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-Bud2ISlK.js → erDiagram-TEJ5UH35-DyIKGj6e.js} +1 -1
  44. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-D-18vLQ1.js → flowDiagram-I6XJVG4X-B79pLdLD.js} +1 -1
  45. package/packages/web/dist/assets/{format-CQ2BDlby.js → format-C5nhFL8q.js} +1 -1
  46. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-CzyRX39N.js → ganttDiagram-6RSMTGT7-BFTR_oNA.js} +1 -1
  47. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DLogfrjg.js → gitGraphDiagram-PVQCEYII-DdGUlJxS.js} +1 -1
  48. package/packages/web/dist/assets/{index-B0gJ0qWe.js → index-DVSsta3B.js} +2 -2
  49. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-OgNPt6RG.js → infoDiagram-5YYISTIA-B80FyUOy.js} +1 -1
  50. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-CrM0t0_X.js → ishikawaDiagram-YF4QCWOH-_wRlphn9.js} +1 -1
  51. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-xbxfHfwR.js → journeyDiagram-JHISSGLW-qu4yvjW8.js} +1 -1
  52. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-DpszBaZb.js → kanban-definition-UN3LZRKU-D2DEUp2-.js} +1 -1
  53. package/packages/web/dist/assets/{linear-BLujdbjQ.js → linear-D-CLRUEW.js} +1 -1
  54. package/packages/web/dist/assets/{mermaid.core-DzEhy9Qq.js → mermaid.core-DdihBJl4.js} +4 -4
  55. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DaH3Lmoo.js → mindmap-definition-RKZ34NQL-BEh_TDBf.js} +1 -1
  56. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-BxeW1kuf.js → pieDiagram-4H26LBE5-BbrmLVaZ.js} +1 -1
  57. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-BKs2CT0r.js → quadrantDiagram-W4KKPZXB-DfVuygAv.js} +1 -1
  58. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CVdO-hXD.js → requirementDiagram-4Y6WPE33-BHp2QpZt.js} +1 -1
  59. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-yusj_kJC.js → sankeyDiagram-5OEKKPKP-D2G6lRMY.js} +1 -1
  60. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C0h-9c8Y.js → sequenceDiagram-3UESZ5HK-Dj-jzR-c.js} +1 -1
  61. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-BSBY8Psb.js → stateDiagram-AJRCARHV-C5D6bT13.js} +1 -1
  62. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-C8NDOqTq.js +2 -0
  63. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-DEFUdNb8.js → timeline-definition-PNZ67QCA-DE4gGjhP.js} +1 -1
  64. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-DunhMqxK.js → vennDiagram-CIIHVFJN-C9QNrl92.js} +1 -1
  65. package/packages/web/dist/assets/{wardley-L42UT6IY-CXJsBGKl.js → wardley-L42UT6IY-U2fSGy-d.js} +1 -1
  66. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DfT_-dqX.js → wardleyDiagram-YWT4CUSO-B9RhObdH.js} +1 -1
  67. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-BysTtULW.js → xychartDiagram-2RQKCTM6-BnlPrtQ9.js} +1 -1
  68. package/packages/web/dist/index.html +1 -1
  69. package/packages/web/dist/sw.js +1 -1
  70. package/packages/web/dist/assets/channel-BZnRb_8C.js +0 -2
  71. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BGCpL0-K.js +0 -2
  72. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BGCpL0-K.js +0 -2
  73. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-4DGE0Z1g.js +0 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -314,6 +314,37 @@ export function buildManagementOps(ctx, params) {
314
314
  agentRegistered,
315
315
  };
316
316
  },
317
+ // Promotion (#470), sharing create's gate. Same discipline: the SAME two calls,
318
+ // in the same order, that `POST /api/projects/:slug/promote` makes — store
319
+ // promote, then agent re-registration. Every guard (already unmanaged, the
320
+ // root workspace, an existing checkout dir, a bad URL) and the rollback to an
321
+ // untouched notebook on clone failure live in `ProjectStore.promote` (#213).
322
+ promoteProject: async (projectSlug, repo) => {
323
+ const project = await deps.projects.promote(projectSlug, repo);
324
+ // Non-fatal, as in the route — but it matters more than it does for create:
325
+ // re-registering is what re-symlinks the NEW working dir at the project's
326
+ // existing `.chats/` store, which is what keeps its chats listed + resumable.
327
+ let agentRegistered = true;
328
+ try {
329
+ await deps.herdctl.ensureProjectAgent(project);
330
+ }
331
+ catch {
332
+ agentRegistered = false;
333
+ }
334
+ return {
335
+ slug: project.slug,
336
+ name: project.name,
337
+ dir: project.dir,
338
+ workingDir: project.workingDir,
339
+ // #206 replaced `repoBacked` with the `managed` axis. Promotion always
340
+ // lands on `managed: false` — that IS what promoting means now — but read
341
+ // it off the project rather than hard-coding, so this reports what the
342
+ // store actually did.
343
+ managed: project.managed,
344
+ ...(project.repo ? { repo: project.repo } : {}),
345
+ agentRegistered,
346
+ };
347
+ },
317
348
  triggersMcpEnabled: includeTriggers,
318
349
  listTriggers: async (projectSlug) => {
319
350
  if (!deps.triggers)
@@ -405,12 +436,20 @@ export function enforceManagementPolicy(ops, principal) {
405
436
  const write = {
406
437
  currentProjectSlug: w.currentProjectSlug,
407
438
  currentSessionId: w.currentSessionId,
439
+ // The project block is offered when EITHER of its verbs is granted; the
440
+ // per-tool filter (`managementToolFilter`) is what hides the other one, so a
441
+ // client scoped to just one of them isn't denied both.
442
+ projectsMcpEnabled: w.projectsMcpEnabled &&
443
+ (isOperationAllowed(scope, "create_project") || isOperationAllowed(scope, "promote_project")),
408
444
  // Instance-level, so there is no project to scope-check — only the verb.
409
- projectsMcpEnabled: w.projectsMcpEnabled && isOperationAllowed(scope, "create_project"),
410
445
  createProject: (input) => {
411
446
  assertOperation(principal, "create_project");
412
447
  return w.createProject(input);
413
448
  },
449
+ // Promotion DOES name an existing project, so unlike create it is scope-checked
450
+ // against `projects` too — a client confined to one project must not restructure
451
+ // another.
452
+ promoteProject: (projectSlug, repo) => guard("promote_project", projectSlug, () => w.promoteProject(projectSlug, repo)),
414
453
  createChat: (projectSlug, prompt, o) => guard("create_chat", projectSlug, () => w.createChat(projectSlug, prompt, o)),
415
454
  // NOTE: `fork_chat_batch` is a convenience wrapper that calls this same
416
455
  // callback N times, so it is gated on `fork_chat` here AND on
@@ -66,7 +66,7 @@ export interface ManagementPrincipal {
66
66
  /** Read-only operations: no state change, no turn started. */
67
67
  export declare const READ_OPERATIONS: readonly ["list_projects", "list_chats", "read_chat"];
68
68
  /** Chat write operations. The first four START TURNS (see the RCE note above). */
69
- export declare const WRITE_OPERATIONS: readonly ["create_project", "create_chat", "fork_chat", "send_message", "fork_chat_batch", "archive_chat", "unarchive_chat"];
69
+ export declare const WRITE_OPERATIONS: readonly ["create_project", "promote_project", "create_chat", "fork_chat", "send_message", "fork_chat_batch", "archive_chat", "unarchive_chat"];
70
70
  /** Unified trigger operations (Epic T / T3). `list_triggers` is the read one. */
71
71
  export declare const TRIGGER_OPERATIONS: readonly ["list_triggers", "set_trigger", "remove_trigger", "run_trigger"];
72
72
  /** Every operation the management surface knows about. */
@@ -81,8 +81,9 @@ export declare const ALL_OPERATIONS: readonly string[];
81
81
  /**
82
82
  * Operations that grant code execution on the host, and so define the risk class
83
83
  * a write scope really is. A superset of {@link TURN_SPAWNING_OPERATIONS}:
84
- * `create_project` starts no turn, but it clones a CALLER-SUPPLIED git URL and
85
- * creates instance-level state, which belongs in the same warning.
84
+ * `create_project`/`promote_project` start no turn, but they clone a
85
+ * CALLER-SUPPLIED git URL and create (or restructure) instance-level state, which
86
+ * belongs in the same warning.
86
87
  */
87
88
  export declare const HIGH_RISK_OPERATIONS: readonly string[];
88
89
  export declare const TURN_SPAWNING_OPERATIONS: readonly string[];
@@ -34,6 +34,7 @@ export const READ_OPERATIONS = ["list_projects", "list_chats", "read_chat"];
34
34
  /** Chat write operations. The first four START TURNS (see the RCE note above). */
35
35
  export const WRITE_OPERATIONS = [
36
36
  "create_project",
37
+ "promote_project",
37
38
  "create_chat",
38
39
  "fork_chat",
39
40
  "send_message",
@@ -64,11 +65,13 @@ export const ALL_OPERATIONS = [
64
65
  /**
65
66
  * Operations that grant code execution on the host, and so define the risk class
66
67
  * a write scope really is. A superset of {@link TURN_SPAWNING_OPERATIONS}:
67
- * `create_project` starts no turn, but it clones a CALLER-SUPPLIED git URL and
68
- * creates instance-level state, which belongs in the same warning.
68
+ * `create_project`/`promote_project` start no turn, but they clone a
69
+ * CALLER-SUPPLIED git URL and create (or restructure) instance-level state, which
70
+ * belongs in the same warning.
69
71
  */
70
72
  export const HIGH_RISK_OPERATIONS = [
71
73
  "create_project",
74
+ "promote_project",
72
75
  "create_chat",
73
76
  "fork_chat",
74
77
  "fork_chat_batch",
@@ -67,9 +67,18 @@ export declare class ProjectStore {
67
67
  * root, and that implicit safety is gone.
68
68
  *
69
69
  * So the check is made explicit and centralised here rather than re-derived at
70
- * each call site: an `fs.rm` that escapes throws instead of deleting. Both
71
- * current callers already only pass in-root paths — this exists so that stays
72
- * true when someone later adds a third one.
70
+ * each call site: an `fs.rm` that escapes throws instead of deleting. Every
71
+ * caller already only passes in-root paths — this exists so that stays true
72
+ * when someone later adds another one.
73
+ *
74
+ * `promote()`'s two rollbacks route through here as well. Their target is
75
+ * `<projectsRoot>/<slug>/<checkoutName>`, which is in-root by construction, so
76
+ * the guard is inert there today — that is the point. This is the choke point
77
+ * the comment above promised, and a recursive delete that skips it is exactly
78
+ * the drift it exists to prevent. {@link rollback} is the deliberate exception:
79
+ * it deletes paths that may legitimately sit OUTSIDE the root (a user-nominated
80
+ * clone target), and is safe by a different mechanism — it only ever removes
81
+ * directories this operation created.
73
82
  */
74
83
  private rmInsideRoot;
75
84
  /**
@@ -202,9 +202,18 @@ export class ProjectStore {
202
202
  * root, and that implicit safety is gone.
203
203
  *
204
204
  * So the check is made explicit and centralised here rather than re-derived at
205
- * each call site: an `fs.rm` that escapes throws instead of deleting. Both
206
- * current callers already only pass in-root paths — this exists so that stays
207
- * true when someone later adds a third one.
205
+ * each call site: an `fs.rm` that escapes throws instead of deleting. Every
206
+ * caller already only passes in-root paths — this exists so that stays true
207
+ * when someone later adds another one.
208
+ *
209
+ * `promote()`'s two rollbacks route through here as well. Their target is
210
+ * `<projectsRoot>/<slug>/<checkoutName>`, which is in-root by construction, so
211
+ * the guard is inert there today — that is the point. This is the choke point
212
+ * the comment above promised, and a recursive delete that skips it is exactly
213
+ * the drift it exists to prevent. {@link rollback} is the deliberate exception:
214
+ * it deletes paths that may legitimately sit OUTSIDE the root (a user-nominated
215
+ * clone target), and is safe by a different mechanism — it only ever removes
216
+ * directories this operation created.
208
217
  */
209
218
  async rmInsideRoot(target) {
210
219
  const resolved = path.resolve(target);
@@ -641,7 +650,7 @@ export class ProjectStore {
641
650
  await cloneRepo(repo, checkoutDir);
642
651
  }
643
652
  catch (err) {
644
- await fs.rm(checkoutDir, { recursive: true, force: true }).catch(() => undefined);
653
+ await this.rmInsideRoot(checkoutDir).catch(() => undefined);
645
654
  throw new ProjectError(err instanceof Error ? err.message : `Failed to clone ${repo}`, "invalid");
646
655
  }
647
656
  // `writeYaml` is the atomic COMMIT point: until it succeeds the notebook is
@@ -666,7 +675,7 @@ export class ProjectStore {
666
675
  await this.writeYaml(slug, next);
667
676
  }
668
677
  catch (err) {
669
- await fs.rm(checkoutDir, { recursive: true, force: true }).catch(() => undefined);
678
+ await this.rmInsideRoot(checkoutDir).catch(() => undefined);
670
679
  throw err;
671
680
  }
672
681
  // Post-commit finalization — the project is now validly repo-backed; these
@@ -14,6 +14,7 @@ export declare const ARCHIVE_CHAT_DESC: string;
14
14
  export declare const UNARCHIVE_CHAT_DESC: string;
15
15
  export declare const FORK_CHAT_BATCH_DESC: string;
16
16
  export declare const CREATE_PROJECT_DESC: string;
17
+ export declare const PROMOTE_PROJECT_DESC: string;
17
18
  export declare const SET_TRIGGER_DESC: string;
18
19
  export declare const REMOVE_TRIGGER_DESC: string;
19
20
  export declare const RUN_TRIGGER_DESC: string;
@@ -96,7 +96,7 @@ export const FORK_CHAT_BATCH_DESC = "FAN-OUT primitive: fork ONE source chat int
96
96
  `"<name_prefix> <i>" (1-based). With \`model\`, every fork's kickoff turn runs on ` +
97
97
  `that model. Up to ${FORK_BATCH_MAX} forks per call; they run ` +
98
98
  "concurrently. Returns the source and every new child's sessionId.";
99
- // ── Project tools (issue #467) ──────────────────────────────────────────────
99
+ // ── Project tools (issues #467, #470) ───────────────────────────────────────
100
100
  export const CREATE_PROJECT_DESC = "Create a BRAND-NEW Paddock project — the workspace a keeper agent lives in — and " +
101
101
  "register its keeper. `name` is the only required argument; the kebab-case `slug` " +
102
102
  "is derived from it unless you pass one. Two kinds: pass a `repo` git URL for a " +
@@ -109,6 +109,19 @@ export const CREATE_PROJECT_DESC = "Create a BRAND-NEW Paddock project — the w
109
109
  "pass that slug to `create_chat` to start working in it. A bad or unreachable " +
110
110
  "repo URL fails cleanly and leaves NO half-created project behind, so it is safe " +
111
111
  "to retry with a corrected URL.";
112
+ export const PROMOTE_PROJECT_DESC = "Turn an EXISTING notebook project into a REPO-BACKED one, in place — use this " +
113
+ "when a notes-only project has grown into (or was always meant to be) a codebase, " +
114
+ "instead of creating a second project and abandoning the first. `repo` (a git URL) " +
115
+ "is required; `project` defaults to the CURRENT project, so an agent can promote " +
116
+ "the workspace it is running in. The repo is cloned into a nested checkout inside " +
117
+ "the project, that checkout becomes the agent's working directory, and the agent is " +
118
+ "re-registered there. Everything already in the project is KEPT — its existing " +
119
+ "chats stay listed and resumable, and its OVERVIEW/CHANGELOG/settings are " +
120
+ "untouched. Not reversible through this MCP, and only ever notebook → repo-backed: " +
121
+ "a project that is ALREADY repo-backed is refused, as is the root workspace. A bad " +
122
+ "or unreachable repo URL fails cleanly, leaving the notebook exactly as it was, so " +
123
+ "it is safe to retry with a corrected URL. Returns the project's new working " +
124
+ "directory and whether its agent re-registered.";
112
125
  // ── Unified trigger tools (Epic T / T3) ─────────────────────────────────────
113
126
  export const SET_TRIGGER_DESC = "Create or update a unified project TRIGGER — a standing rule that runs an agent " +
114
127
  "turn when something happens, keyed by `name`. Enable/disable is just this call " +
@@ -54,6 +54,13 @@ export interface SelfMcpCreatedProject {
54
54
  */
55
55
  agentRegistered: boolean;
56
56
  }
57
+ /**
58
+ * A promoted project as surfaced back to the agent (issue #470). Declared as an
59
+ * ALIAS rather than a second copy of the same fields: promotion's whole point is
60
+ * that the project ends up in exactly the state a repo-backed `create_project`
61
+ * would have produced, so the two reports must not be able to drift.
62
+ */
63
+ export type SelfMcpPromotedProject = SelfMcpCreatedProject;
57
64
  /** A chat as surfaced to the agent. */
58
65
  export interface SelfMcpChat {
59
66
  /** Owning project slug. */
@@ -192,10 +199,11 @@ export interface SelfMcpWriteContext {
192
199
  /** Set (or clear) a chat's archived flag (presentational metadata only). */
193
200
  setArchived: (projectSlug: string, sessionId: string, archived: boolean) => Promise<void>;
194
201
  /**
195
- * Whether the PROJECT tools (`create_project`, issue #467) are enabled for this
196
- * instance — the `selfMcpProjectsEnabled` gate, resolved by the caller. When false
197
- * the tool is NOT injected at all (absent, not present-but-refusing), the same
198
- * binary discipline as {@link triggersMcpEnabled}.
202
+ * Whether the PROJECT tools (`create_project`, issue #467; `promote_project`,
203
+ * issue #470) are enabled for this instance — the `selfMcpProjectsEnabled` gate,
204
+ * resolved by the caller. When false the tools are NOT injected at all (absent,
205
+ * not present-but-refusing), the same binary discipline as
206
+ * {@link triggersMcpEnabled}.
199
207
  */
200
208
  projectsMcpEnabled: boolean;
201
209
  /**
@@ -208,6 +216,20 @@ export interface SelfMcpWriteContext {
208
216
  * non-fatal (mirroring the route) and reported via `agentRegistered`.
209
217
  */
210
218
  createProject: (input: SelfMcpCreateProjectInput) => Promise<SelfMcpCreatedProject>;
219
+ /**
220
+ * Promote an EXISTING notebook project to repo-backed in place, and re-register
221
+ * its agent — the SAME two steps `POST /api/projects/:slug/promote` performs, in
222
+ * the same order (`ProjectStore.promote` then `ensureProjectAgent`), so the REST
223
+ * and MCP paths cannot drift (issue #470/#213). All the real work (the already-
224
+ * repo-backed / root-workspace / bad-URL guards, the clone into the nested
225
+ * checkout, the `writeYaml` commit point and its rollback to an UNTOUCHED
226
+ * notebook on failure) stays in the store. Throws on a promote failure; a
227
+ * re-registration failure is non-fatal (mirroring the route) and reported via
228
+ * `agentRegistered` — though it matters more here than for create, since
229
+ * re-registration is what re-symlinks the new working dir at the project's
230
+ * existing `.chats/` store so its chats stay listed and resumable.
231
+ */
232
+ promoteProject: (projectSlug: string, repo: string) => Promise<SelfMcpPromotedProject>;
211
233
  /**
212
234
  * Whether the unified trigger-management MCP is enabled for THIS turn's project
213
235
  * (Epic T / T3 — the successor to the G5 hook-MCP gate, resolved from the SAME
@@ -32,4 +32,25 @@ export declare function archiveChatHandler(write: SelfMcpWriteContext, archived:
32
32
  * an unclonable repo, and still rolls the whole project dir back on a clone failure.
33
33
  */
34
34
  export declare function createProjectHandler(write: SelfMcpWriteContext): (args: Record<string, unknown>) => Promise<McpToolCallResult>;
35
+ /**
36
+ * `promote_project` (issue #470) — turn an EXISTING notebook project into a
37
+ * repo-backed one in place, over the same `ProjectStore.promote` (#213) that
38
+ * `POST /api/projects/:slug/promote` uses. Shares `create_project`'s
39
+ * `projectsMcpEnabled` gate: same blast-radius class (it clones a caller-supplied
40
+ * URL and restructures a project), and this one MUTATES existing state.
41
+ *
42
+ * Pure, like its siblings: it resolves the target (defaulting to the current
43
+ * project, as every other project-targeting write tool does), shape-checks the URL
44
+ * with the very same `isValidRepoUrl` the store uses, and delegates. Everything
45
+ * that makes promotion safe — the already-repo-backed / root-workspace / existing-
46
+ * checkout-dir guards, and the rollback that leaves an untouched notebook when the
47
+ * clone fails — stays in the store; nothing is re-implemented here.
48
+ *
49
+ * Deliberately NO up-front `SLUG_RE` check on the target, unlike `create_project`:
50
+ * there the slug names a project being INVENTED, here it names one that must
51
+ * already exist, so the store's own `not_found` is both the real check and the
52
+ * better message — and `SLUG_RE` would reject the root workspace's legitimate ""
53
+ * key with a misleading "invalid slug" instead of the store's explicit refusal.
54
+ */
55
+ export declare function promoteProjectHandler(write: SelfMcpWriteContext): (args: Record<string, unknown>) => Promise<McpToolCallResult>;
35
56
  export declare function forkChatBatchHandler(write: SelfMcpWriteContext): (args: Record<string, unknown>) => Promise<McpToolCallResult>;
@@ -171,6 +171,50 @@ export function createProjectHandler(write) {
171
171
  }
172
172
  };
173
173
  }
174
+ /**
175
+ * `promote_project` (issue #470) — turn an EXISTING notebook project into a
176
+ * repo-backed one in place, over the same `ProjectStore.promote` (#213) that
177
+ * `POST /api/projects/:slug/promote` uses. Shares `create_project`'s
178
+ * `projectsMcpEnabled` gate: same blast-radius class (it clones a caller-supplied
179
+ * URL and restructures a project), and this one MUTATES existing state.
180
+ *
181
+ * Pure, like its siblings: it resolves the target (defaulting to the current
182
+ * project, as every other project-targeting write tool does), shape-checks the URL
183
+ * with the very same `isValidRepoUrl` the store uses, and delegates. Everything
184
+ * that makes promotion safe — the already-repo-backed / root-workspace / existing-
185
+ * checkout-dir guards, and the rollback that leaves an untouched notebook when the
186
+ * clone fails — stays in the store; nothing is re-implemented here.
187
+ *
188
+ * Deliberately NO up-front `SLUG_RE` check on the target, unlike `create_project`:
189
+ * there the slug names a project being INVENTED, here it names one that must
190
+ * already exist, so the store's own `not_found` is both the real check and the
191
+ * better message — and `SLUG_RE` would reject the root workspace's legitimate ""
192
+ * key with a misleading "invalid slug" instead of the store's explicit refusal.
193
+ */
194
+ export function promoteProjectHandler(write) {
195
+ return async (args) => {
196
+ try {
197
+ const projectArg = typeof args.project === "string" ? args.project.trim() : "";
198
+ const projectSlug = projectArg.length > 0 ? projectArg : write.currentProjectSlug;
199
+ const repo = typeof args.repo === "string" ? args.repo.trim() : "";
200
+ if (!repo) {
201
+ return fail("Error: `repo` is required (the git URL to back the project with).");
202
+ }
203
+ if (!isValidRepoUrl(repo)) {
204
+ return fail(`Error: invalid repo URL "${repo}" — expected an https://, git://, ssh:// or ` +
205
+ "git@host:owner/repo url.");
206
+ }
207
+ const project = await write.promoteProject(projectSlug, repo);
208
+ return ok({ promoted: true, ...project });
209
+ }
210
+ catch (error) {
211
+ // Same hygiene as create_project: a clone failure arrives as git's whole argv
212
+ // (`Command failed: git clone -- <url> <dest>`), and the agent needs the repo
213
+ // URL + git's reason, not our on-disk layout.
214
+ return fail(`Error promoting project: ${truncateText(redactPaths(errText(error)))}`);
215
+ }
216
+ };
217
+ }
174
218
  export function forkChatBatchHandler(write) {
175
219
  return async (args) => {
176
220
  try {
@@ -12,7 +12,7 @@
12
12
  * - `self-mcp-util.ts` — result envelopes, clamps, arg coercion + caps
13
13
  * - `self-mcp-descriptions.ts` — the agent-facing `*_DESC` prose
14
14
  * - `self-mcp-read.ts` — list_projects / list_chats / read_chat handlers
15
- * - `self-mcp-write.ts` — create/fork/send/archive/fork-batch + create_project handlers
15
+ * - `self-mcp-write.ts` — create/fork/send/archive/fork-batch + project handlers
16
16
  * - `self-mcp-triggers.ts` — list/set/remove/run trigger handlers
17
17
  *
18
18
  * ── How it reaches the keeper ───────────────────────────────────────────────
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import type { InjectedMcpServerDef } from "@herdctl/core";
34
34
  import type { SelfMcpContext, SelfMcpWriteContext } from "./self-mcp-types.js";
35
- export type { SelfMcpProject, SelfMcpChat, SelfMcpMessage, SelfMcpTrigger, SelfMcpContext, SelfMcpWriteContext, SelfMcpCreateProjectInput, SelfMcpCreatedProject, } from "./self-mcp-types.js";
35
+ export type { SelfMcpProject, SelfMcpChat, SelfMcpMessage, SelfMcpTrigger, SelfMcpContext, SelfMcpWriteContext, SelfMcpCreateProjectInput, SelfMcpCreatedProject, SelfMcpPromotedProject, } from "./self-mcp-types.js";
36
36
  export { FORK_BATCH_MAX, redactPaths, READ_CHAT_DEFAULT_LIMIT, READ_CHAT_MAX_LIMIT, READ_CHAT_MAX_TEXT, clampLimit, truncateText, resolveModelArg, coercePrompts, coerceToolList, coerceBoolean, } from "./self-mcp-util.js";
37
37
  /** Assembly-time options for {@link selfMcpServerDef}. */
38
38
  export interface SelfMcpServerDefOptions {
@@ -50,8 +50,8 @@ export interface SelfMcpServerDefOptions {
50
50
  * write flag is on), the WRITE tools (create_chat/fork_chat/send_message/
51
51
  * archive_chat/unarchive_chat/fork_chat_batch) are appended too; omit it for
52
52
  * unchanged read-only behavior. Two further blocks hang off independent gates ON
53
- * that write context: {@link SelfMcpWriteContext.projectsMcpEnabled} (issue #467)
54
- * appends the project tool (create_project), and
53
+ * that write context: {@link SelfMcpWriteContext.projectsMcpEnabled} (issues #467,
54
+ * #470) appends the project tools (create_project/promote_project), and
55
55
  * {@link SelfMcpWriteContext.triggersMcpEnabled} (the per-project trigger-MCP
56
56
  * opt-in, Epic T / T3) appends the unified trigger-management tools (list_triggers/
57
57
  * set_trigger/remove_trigger/run_trigger). Inject under
@@ -75,11 +75,12 @@ export declare const SELF_MCP_WRITE_TOOL_NAMES: {
75
75
  readonly forkChatBatch: "mcp__paddock_manage__fork_chat_batch";
76
76
  };
77
77
  /**
78
- * The fully-qualified name of the PROJECT tool (issue #467), present only when a
79
- * write context has {@link SelfMcpWriteContext.projectsMcpEnabled} on.
78
+ * The fully-qualified names of the PROJECT tools (issues #467, #470), present only
79
+ * when a write context has {@link SelfMcpWriteContext.projectsMcpEnabled} on.
80
80
  */
81
81
  export declare const SELF_MCP_PROJECT_TOOL_NAMES: {
82
82
  readonly createProject: "mcp__paddock_manage__create_project";
83
+ readonly promoteProject: "mcp__paddock_manage__promote_project";
83
84
  };
84
85
  /**
85
86
  * The fully-qualified names of the Epic T / T3 unified trigger-management tools
@@ -1,7 +1,7 @@
1
1
  import { FORK_BATCH_MAX, READ_CHAT_DEFAULT_LIMIT, READ_CHAT_MAX_LIMIT, } from "./self-mcp-util.js";
2
- import { MODEL_ARG_DESC, LIST_PROJECTS_DESC, LIST_CHATS_DESC, READ_CHAT_DESC, CREATE_CHAT_DESC, FORK_CHAT_DESC, SEND_MESSAGE_DESC, ARCHIVE_CHAT_DESC, UNARCHIVE_CHAT_DESC, FORK_CHAT_BATCH_DESC, CREATE_PROJECT_DESC, SET_TRIGGER_DESC, REMOVE_TRIGGER_DESC, RUN_TRIGGER_DESC, LIST_TRIGGERS_DESC, } from "./self-mcp-descriptions.js";
2
+ import { MODEL_ARG_DESC, LIST_PROJECTS_DESC, LIST_CHATS_DESC, READ_CHAT_DESC, CREATE_CHAT_DESC, FORK_CHAT_DESC, SEND_MESSAGE_DESC, ARCHIVE_CHAT_DESC, UNARCHIVE_CHAT_DESC, FORK_CHAT_BATCH_DESC, CREATE_PROJECT_DESC, PROMOTE_PROJECT_DESC, SET_TRIGGER_DESC, REMOVE_TRIGGER_DESC, RUN_TRIGGER_DESC, LIST_TRIGGERS_DESC, } from "./self-mcp-descriptions.js";
3
3
  import { listProjectsHandler, listChatsHandler, readChatHandler } from "./self-mcp-read.js";
4
- import { createChatHandler, forkChatHandler, sendMessageHandler, archiveChatHandler, forkChatBatchHandler, createProjectHandler, } from "./self-mcp-write.js";
4
+ import { createChatHandler, forkChatHandler, sendMessageHandler, archiveChatHandler, forkChatBatchHandler, createProjectHandler, promoteProjectHandler, } from "./self-mcp-write.js";
5
5
  import { PROJECT_STATUSES } from "./project-types.js";
6
6
  import { listTriggersHandler, setTriggerHandler, removeTriggerHandler, runTriggerHandler, } from "./self-mcp-triggers.js";
7
7
  export { FORK_BATCH_MAX, redactPaths, READ_CHAT_DEFAULT_LIMIT, READ_CHAT_MAX_LIMIT, READ_CHAT_MAX_TEXT, clampLimit, truncateText, resolveModelArg, coercePrompts, coerceToolList, coerceBoolean, } from "./self-mcp-util.js";
@@ -204,11 +204,15 @@ function writeTools(write) {
204
204
  ];
205
205
  }
206
206
  /**
207
- * The PROJECT tools (issue #467 — `create_project`). Appended only when
207
+ * The PROJECT tools (issue #467 — `create_project`; issue #470 —
208
+ * `promote_project`). Appended only when
208
209
  * {@link SelfMcpWriteContext.projectsMcpEnabled} is on: the same coarse binary gate
209
- * as the trigger tools (when off the tool is ABSENT, not present-but-refusing). Its
210
- * own flag because — unlike every other write tool — this one provisions
211
- * instance-level state and clones a caller-supplied git URL.
210
+ * as the trigger tools (when off the tools are ABSENT, not present-but-refusing).
211
+ * Their own flag because — unlike every other write tool — these provision or
212
+ * restructure instance-level state and clone a caller-supplied git URL. Promotion
213
+ * shares the flag rather than getting a third one: it is the same blast-radius
214
+ * class, and an operator who has granted "this agent may provision projects" has
215
+ * already made the decision promotion asks for.
212
216
  */
213
217
  function projectTools(write) {
214
218
  return [
@@ -244,6 +248,27 @@ function projectTools(write) {
244
248
  },
245
249
  handler: createProjectHandler(write),
246
250
  },
251
+ {
252
+ name: "promote_project",
253
+ description: PROMOTE_PROJECT_DESC,
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ project: {
258
+ type: "string",
259
+ description: "Slug of the NOTEBOOK project to promote. Omit to promote the current project.",
260
+ },
261
+ repo: {
262
+ type: "string",
263
+ description: "Git URL to back the project with (https://, git://, ssh:// or " +
264
+ "git@host:owner/repo). Cloned into a nested checkout that becomes the " +
265
+ "agent's working directory. Required.",
266
+ },
267
+ },
268
+ required: ["repo"],
269
+ },
270
+ handler: promoteProjectHandler(write),
271
+ },
247
272
  ];
248
273
  }
249
274
  /**
@@ -386,8 +411,8 @@ function triggerTools(write) {
386
411
  * write flag is on), the WRITE tools (create_chat/fork_chat/send_message/
387
412
  * archive_chat/unarchive_chat/fork_chat_batch) are appended too; omit it for
388
413
  * unchanged read-only behavior. Two further blocks hang off independent gates ON
389
- * that write context: {@link SelfMcpWriteContext.projectsMcpEnabled} (issue #467)
390
- * appends the project tool (create_project), and
414
+ * that write context: {@link SelfMcpWriteContext.projectsMcpEnabled} (issues #467,
415
+ * #470) appends the project tools (create_project/promote_project), and
391
416
  * {@link SelfMcpWriteContext.triggersMcpEnabled} (the per-project trigger-MCP
392
417
  * opt-in, Epic T / T3) appends the unified trigger-management tools (list_triggers/
393
418
  * set_trigger/remove_trigger/run_trigger). Inject under
@@ -428,11 +453,12 @@ export const SELF_MCP_WRITE_TOOL_NAMES = {
428
453
  forkChatBatch: `mcp__${SERVER_NAME}__fork_chat_batch`,
429
454
  };
430
455
  /**
431
- * The fully-qualified name of the PROJECT tool (issue #467), present only when a
432
- * write context has {@link SelfMcpWriteContext.projectsMcpEnabled} on.
456
+ * The fully-qualified names of the PROJECT tools (issues #467, #470), present only
457
+ * when a write context has {@link SelfMcpWriteContext.projectsMcpEnabled} on.
433
458
  */
434
459
  export const SELF_MCP_PROJECT_TOOL_NAMES = {
435
460
  createProject: `mcp__${SERVER_NAME}__create_project`,
461
+ promoteProject: `mcp__${SERVER_NAME}__promote_project`,
436
462
  };
437
463
  /**
438
464
  * The fully-qualified names of the Epic T / T3 unified trigger-management tools
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,2 +1,2 @@
1
- import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-B0gJ0qWe.js";function $(){const[t,s]=o.useState(null),[l,i]=o.useState(!0),[r,c]=o.useState(null),[d,m]=o.useState({}),[h,k]=o.useState(!1),[j,p]=o.useState(null),[N,u]=o.useState(!1);o.useEffect(()=>{let a=!0;return i(!0),f.getInstanceConfig().then(n=>{a&&(s(n),c(null))}).catch(n=>a&&c(n instanceof Error?n.message:String(n))).finally(()=>a&&i(!1)),()=>{a=!1}},[]);const v=(a,n)=>{u(!1),p(null),m(g=>({...g,[a]:n}))},S=a=>Object.prototype.hasOwnProperty.call(d,a.key)?d[a.key]:a.value,y=o.useMemo(()=>t?t.groups.flatMap(a=>a.fields):[],[t]),x=o.useMemo(()=>y.filter(a=>a.editable&&!a.envOverridden).filter(a=>Object.prototype.hasOwnProperty.call(d,a.key)).filter(a=>!B(d[a.key],a.value)).map(a=>a.key),[y,d]),w=async()=>{if(x.length===0)return;const a={};for(const n of x)a[n]=d[n];k(!0),p(null);try{await f.updateInstanceConfig(a);const n=await f.getInstanceConfig();s(n),m({}),u(!0)}catch(n){p(n instanceof A||n instanceof Error?n.message:String(n))}finally{k(!1)}},C=()=>{m({}),p(null),u(!1)};return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:"mx-auto max-w-2xl",children:[e.jsx(L,{saved:N,configPath:t==null?void 0:t.configPath}),l&&e.jsx("p",{className:"text-sm text-paddock-500",children:"Loading…"}),r&&e.jsxs("p",{className:"text-sm text-red-600 dark:text-red-400",role:"alert",children:["Failed to load settings: ",r]}),t&&t.groups.map(a=>e.jsx(M,{title:a.label,description:a.description,children:e.jsx("div",{className:"grid grid-cols-1 gap-4",children:a.fields.map(n=>e.jsx(P,{field:n,value:S(n),onChange:g=>v(n.key,g)},n.key))})},a.id))]})}),t&&e.jsxs("footer",{className:"flex items-center gap-3 border-t border-paddock-200 px-4 py-3 dark:border-paddock-800 sm:px-6",children:[j&&e.jsxs("span",{className:"flex items-center gap-1.5 text-[13px] text-red-600 dark:text-red-400",role:"alert",children:[e.jsx(b,{width:14,height:14,className:"shrink-0"}),j]}),e.jsx("span",{className:"ml-auto text-[12px] text-paddock-400",children:x.length>0?`${x.length} unsaved change${x.length===1?"":"s"}`:"No changes"}),e.jsx("button",{type:"button",className:"btn-subtle",onClick:C,disabled:x.length===0||h,children:"Reset"}),e.jsx("button",{type:"button",className:"btn-primary",onClick:w,disabled:x.length===0||h,children:h?"Saving…":"Save changes"})]})]})}function L({saved:t,configPath:s}){return e.jsxs("div",{role:"status",className:`mb-6 flex items-start gap-2 rounded-lg border px-3 py-2.5 text-[13px] leading-snug ${t?"border-emerald-300 bg-emerald-50 text-emerald-800 dark:border-emerald-700/60 dark:bg-emerald-900/20 dark:text-emerald-300":"border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-300"}`,children:[t?e.jsx(E,{width:15,height:15,className:"mt-0.5 shrink-0"}):e.jsx(b,{width:15,height:15,className:"mt-0.5 shrink-0"}),e.jsx("span",{children:t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):e.jsxs(e.Fragment,{children:["Changes here are written to ",e.jsx("code",{className:"font-mono text-[12px]",children:T(s)})," ","and take effect only after the server restarts — the running instance keeps its current config until then."]})})]})}function M({title:t,description:s,children:l}){return e.jsxs("section",{className:"mb-6",children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wide text-paddock-500",children:t}),s&&e.jsx("p",{className:"mb-3 mt-0.5 text-[13px] text-paddock-500",children:s}),e.jsx("div",{className:`card ${s?"":"mt-2"}`,children:l})]})}function P({field:t,value:s,onChange:l}){const i=!t.editable||t.envOverridden,r=`cfg-${t.key}`;return e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("label",{htmlFor:i?void 0:r,className:"text-sm font-medium",children:[t.label,t.sensitive&&e.jsx("span",{className:"ml-1.5 align-middle text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400",children:"sensitive"})]}),t.type==="boolean"&&!i&&e.jsx("input",{id:r,type:"checkbox",className:"h-4 w-4 accent-accent",checked:!!s,onChange:c=>l(c.target.checked)})]}),t.help&&e.jsx("p",{className:"mt-0.5 text-[12px] leading-snug text-paddock-500",children:t.help}),i?e.jsx(V,{field:t,value:s}):t.type!=="boolean"&&e.jsx(R,{field:t,value:s,onChange:l,inputId:r}),t.envOverridden&&e.jsxs("p",{className:"mt-1 flex items-start gap-1.5 text-[12px] leading-snug text-amber-600 dark:text-amber-400",children:[e.jsx(b,{width:13,height:13,className:"mt-0.5 shrink-0"}),e.jsxs("span",{children:["Overridden by environment variable ",e.jsx("code",{className:"font-mono",children:t.envVar})," — edit that env var (and restart) to change it."]})]})]})}function R({field:t,value:s,onChange:l,inputId:i}){if(t.type==="enum")return e.jsx("select",{id:i,className:"input mt-1.5",value:String(s??""),onChange:r=>l(r.target.value),children:(t.enumValues??[]).map(r=>e.jsx("option",{value:r,children:r},r))});if(t.type==="number")return e.jsx("input",{id:i,type:"number",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"default":String(t.default),onChange:r=>l(r.target.value===""?null:Number(r.target.value))});if(t.type==="text"){const r=s==null||s===t.default,c=s==null?String(t.default??""):String(s),d=typeof t.default=="string"&&!r;return e.jsxs("div",{className:"mt-1.5",children:[e.jsx("textarea",{id:i,className:"input min-h-[9rem] resize-y font-mono text-[12px] leading-relaxed",rows:10,spellCheck:!1,value:c,onChange:m=>l(m.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-[12px] text-paddock-500",children:[e.jsx("span",{children:r?"Using the built-in default.":s===""?"Empty — nothing will be appended.":`${c.length.toLocaleString()} characters.`}),d&&e.jsx("button",{type:"button",className:"shrink-0 underline underline-offset-2 hover:text-paddock-700 dark:hover:text-paddock-300",onClick:()=>l(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const r=Array.isArray(s)?s.join(", "):String(s??"");return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:r,onChange:c=>l(c.target.value.split(",").map(d=>d.trim()).filter(Boolean))})}return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"":String(t.default),onChange:r=>l(r.target.value)})}function V({field:t,value:s}){return e.jsx("div",{className:"mt-1.5 rounded-md border border-dashed border-paddock-300 bg-paddock-50 px-2.5 py-1.5 text-[13px] text-paddock-600 dark:border-paddock-700 dark:bg-paddock-900/40 dark:text-paddock-300",children:t.type==="boolean"?e.jsx("span",{className:"font-mono",children:s?"true":"false"}):s==null||s===""?e.jsx("span",{className:"italic text-paddock-400",children:t.type==="text"&&s===""?"(empty — nothing appended)":"(not set)"}):t.type==="text"?e.jsx("span",{className:"block max-h-64 overflow-y-auto whitespace-pre-wrap font-mono text-[12px] leading-relaxed",children:String(s)}):e.jsx("span",{className:"break-all font-mono",children:Array.isArray(s)?s.join(", "):String(s)})})}function B(t,s){return Array.isArray(t)&&Array.isArray(s)?t.length===s.length&&t.every((l,i)=>l===s[i]):t===s}function T(t){if(!t)return"paddock.config.yaml";const s=t.split("/");return s[s.length-1]||t}function z(){const t=O(),s=(t==null?void 0:t.openNav)??(()=>{});return e.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[e.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-paddock-200 px-3 pb-2.5 dark:border-paddock-800 sm:px-6 lg:py-4",children:[e.jsx("button",{type:"button",onClick:s,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(F,{width:20,height:20})}),e.jsx(I,{width:18,height:18,className:"shrink-0 text-paddock-400"}),e.jsx("h1",{className:"text-[15px] font-semibold tracking-tight",children:"Config"})]}),e.jsx($,{})]})}export{z as InstanceConfigPage};
1
+ import{r as o,b as f,j as e,A as b,k as E,l as A,m as O,M as F,n as I}from"./index-DVSsta3B.js";function $(){const[t,s]=o.useState(null),[l,i]=o.useState(!0),[r,c]=o.useState(null),[d,m]=o.useState({}),[h,k]=o.useState(!1),[j,p]=o.useState(null),[N,u]=o.useState(!1);o.useEffect(()=>{let a=!0;return i(!0),f.getInstanceConfig().then(n=>{a&&(s(n),c(null))}).catch(n=>a&&c(n instanceof Error?n.message:String(n))).finally(()=>a&&i(!1)),()=>{a=!1}},[]);const v=(a,n)=>{u(!1),p(null),m(g=>({...g,[a]:n}))},S=a=>Object.prototype.hasOwnProperty.call(d,a.key)?d[a.key]:a.value,y=o.useMemo(()=>t?t.groups.flatMap(a=>a.fields):[],[t]),x=o.useMemo(()=>y.filter(a=>a.editable&&!a.envOverridden).filter(a=>Object.prototype.hasOwnProperty.call(d,a.key)).filter(a=>!B(d[a.key],a.value)).map(a=>a.key),[y,d]),w=async()=>{if(x.length===0)return;const a={};for(const n of x)a[n]=d[n];k(!0),p(null);try{await f.updateInstanceConfig(a);const n=await f.getInstanceConfig();s(n),m({}),u(!0)}catch(n){p(n instanceof A||n instanceof Error?n.message:String(n))}finally{k(!1)}},C=()=>{m({}),p(null),u(!1)};return e.jsxs(e.Fragment,{children:[e.jsx("div",{className:"min-h-0 flex-1 overflow-y-auto px-4 py-5 sm:px-6",children:e.jsxs("div",{className:"mx-auto max-w-2xl",children:[e.jsx(L,{saved:N,configPath:t==null?void 0:t.configPath}),l&&e.jsx("p",{className:"text-sm text-paddock-500",children:"Loading…"}),r&&e.jsxs("p",{className:"text-sm text-red-600 dark:text-red-400",role:"alert",children:["Failed to load settings: ",r]}),t&&t.groups.map(a=>e.jsx(M,{title:a.label,description:a.description,children:e.jsx("div",{className:"grid grid-cols-1 gap-4",children:a.fields.map(n=>e.jsx(P,{field:n,value:S(n),onChange:g=>v(n.key,g)},n.key))})},a.id))]})}),t&&e.jsxs("footer",{className:"flex items-center gap-3 border-t border-paddock-200 px-4 py-3 dark:border-paddock-800 sm:px-6",children:[j&&e.jsxs("span",{className:"flex items-center gap-1.5 text-[13px] text-red-600 dark:text-red-400",role:"alert",children:[e.jsx(b,{width:14,height:14,className:"shrink-0"}),j]}),e.jsx("span",{className:"ml-auto text-[12px] text-paddock-400",children:x.length>0?`${x.length} unsaved change${x.length===1?"":"s"}`:"No changes"}),e.jsx("button",{type:"button",className:"btn-subtle",onClick:C,disabled:x.length===0||h,children:"Reset"}),e.jsx("button",{type:"button",className:"btn-primary",onClick:w,disabled:x.length===0||h,children:h?"Saving…":"Save changes"})]})]})}function L({saved:t,configPath:s}){return e.jsxs("div",{role:"status",className:`mb-6 flex items-start gap-2 rounded-lg border px-3 py-2.5 text-[13px] leading-snug ${t?"border-emerald-300 bg-emerald-50 text-emerald-800 dark:border-emerald-700/60 dark:bg-emerald-900/20 dark:text-emerald-300":"border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700/60 dark:bg-amber-900/20 dark:text-amber-300"}`,children:[t?e.jsx(E,{width:15,height:15,className:"mt-0.5 shrink-0"}):e.jsx(b,{width:15,height:15,className:"mt-0.5 shrink-0"}),e.jsx("span",{children:t?e.jsxs(e.Fragment,{children:[e.jsx("strong",{children:"Saved to disk."})," These changes take effect only after the server restarts."]}):e.jsxs(e.Fragment,{children:["Changes here are written to ",e.jsx("code",{className:"font-mono text-[12px]",children:T(s)})," ","and take effect only after the server restarts — the running instance keeps its current config until then."]})})]})}function M({title:t,description:s,children:l}){return e.jsxs("section",{className:"mb-6",children:[e.jsx("h2",{className:"text-sm font-semibold uppercase tracking-wide text-paddock-500",children:t}),s&&e.jsx("p",{className:"mb-3 mt-0.5 text-[13px] text-paddock-500",children:s}),e.jsx("div",{className:`card ${s?"":"mt-2"}`,children:l})]})}function P({field:t,value:s,onChange:l}){const i=!t.editable||t.envOverridden,r=`cfg-${t.key}`;return e.jsxs("div",{children:[e.jsxs("div",{className:"flex items-center justify-between gap-3",children:[e.jsxs("label",{htmlFor:i?void 0:r,className:"text-sm font-medium",children:[t.label,t.sensitive&&e.jsx("span",{className:"ml-1.5 align-middle text-[10px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400",children:"sensitive"})]}),t.type==="boolean"&&!i&&e.jsx("input",{id:r,type:"checkbox",className:"h-4 w-4 accent-accent",checked:!!s,onChange:c=>l(c.target.checked)})]}),t.help&&e.jsx("p",{className:"mt-0.5 text-[12px] leading-snug text-paddock-500",children:t.help}),i?e.jsx(V,{field:t,value:s}):t.type!=="boolean"&&e.jsx(R,{field:t,value:s,onChange:l,inputId:r}),t.envOverridden&&e.jsxs("p",{className:"mt-1 flex items-start gap-1.5 text-[12px] leading-snug text-amber-600 dark:text-amber-400",children:[e.jsx(b,{width:13,height:13,className:"mt-0.5 shrink-0"}),e.jsxs("span",{children:["Overridden by environment variable ",e.jsx("code",{className:"font-mono",children:t.envVar})," — edit that env var (and restart) to change it."]})]})]})}function R({field:t,value:s,onChange:l,inputId:i}){if(t.type==="enum")return e.jsx("select",{id:i,className:"input mt-1.5",value:String(s??""),onChange:r=>l(r.target.value),children:(t.enumValues??[]).map(r=>e.jsx("option",{value:r,children:r},r))});if(t.type==="number")return e.jsx("input",{id:i,type:"number",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"default":String(t.default),onChange:r=>l(r.target.value===""?null:Number(r.target.value))});if(t.type==="text"){const r=s==null||s===t.default,c=s==null?String(t.default??""):String(s),d=typeof t.default=="string"&&!r;return e.jsxs("div",{className:"mt-1.5",children:[e.jsx("textarea",{id:i,className:"input min-h-[9rem] resize-y font-mono text-[12px] leading-relaxed",rows:10,spellCheck:!1,value:c,onChange:m=>l(m.target.value)}),e.jsxs("div",{className:"mt-1 flex items-center justify-between gap-3 text-[12px] text-paddock-500",children:[e.jsx("span",{children:r?"Using the built-in default.":s===""?"Empty — nothing will be appended.":`${c.length.toLocaleString()} characters.`}),d&&e.jsx("button",{type:"button",className:"shrink-0 underline underline-offset-2 hover:text-paddock-700 dark:hover:text-paddock-300",onClick:()=>l(null),children:"Restore default"})]})]})}if(t.type==="string-list"){const r=Array.isArray(s)?s.join(", "):String(s??"");return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:r,onChange:c=>l(c.target.value.split(",").map(d=>d.trim()).filter(Boolean))})}return e.jsx("input",{id:i,type:"text",className:"input mt-1.5",value:s==null?"":String(s),placeholder:t.default===null?"":String(t.default),onChange:r=>l(r.target.value)})}function V({field:t,value:s}){return e.jsx("div",{className:"mt-1.5 rounded-md border border-dashed border-paddock-300 bg-paddock-50 px-2.5 py-1.5 text-[13px] text-paddock-600 dark:border-paddock-700 dark:bg-paddock-900/40 dark:text-paddock-300",children:t.type==="boolean"?e.jsx("span",{className:"font-mono",children:s?"true":"false"}):s==null||s===""?e.jsx("span",{className:"italic text-paddock-400",children:t.type==="text"&&s===""?"(empty — nothing appended)":"(not set)"}):t.type==="text"?e.jsx("span",{className:"block max-h-64 overflow-y-auto whitespace-pre-wrap font-mono text-[12px] leading-relaxed",children:String(s)}):e.jsx("span",{className:"break-all font-mono",children:Array.isArray(s)?s.join(", "):String(s)})})}function B(t,s){return Array.isArray(t)&&Array.isArray(s)?t.length===s.length&&t.every((l,i)=>l===s[i]):t===s}function T(t){if(!t)return"paddock.config.yaml";const s=t.split("/");return s[s.length-1]||t}function z(){const t=O(),s=(t==null?void 0:t.openNav)??(()=>{});return e.jsxs("div",{className:"flex h-full min-h-0 flex-col",children:[e.jsxs("header",{className:"pt-safe flex items-center gap-2 border-b border-paddock-200 px-3 pb-2.5 dark:border-paddock-800 sm:px-6 lg:py-4",children:[e.jsx("button",{type:"button",onClick:s,"aria-label":"Open menu",className:"btn-subtle -ml-1 shrink-0 px-2 py-1.5 lg:hidden",children:e.jsx(F,{width:20,height:20})}),e.jsx(I,{width:18,height:18,className:"shrink-0 text-paddock-400"}),e.jsx("h1",{className:"text-[15px] font-semibold tracking-tight",children:"Config"})]}),e.jsx($,{})]})}export{z as InstanceConfigPage};
2
2