@edspencer/paddock 0.63.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 (86) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/adoptable.d.ts +18 -0
  3. package/packages/server/dist/adoptable.js +1 -1
  4. package/packages/server/dist/app.js +3 -1
  5. package/packages/server/dist/claude-home.js +9 -1
  6. package/packages/server/dist/cli/import-chats.js +1 -1
  7. package/packages/server/dist/git.d.ts +44 -1
  8. package/packages/server/dist/git.js +79 -11
  9. package/packages/server/dist/management-ops.js +42 -2
  10. package/packages/server/dist/management-policy.d.ts +4 -3
  11. package/packages/server/dist/management-policy.js +5 -2
  12. package/packages/server/dist/project-paths.d.ts +72 -4
  13. package/packages/server/dist/project-paths.js +76 -4
  14. package/packages/server/dist/project-types.d.ts +109 -11
  15. package/packages/server/dist/projects.d.ts +170 -3
  16. package/packages/server/dist/projects.js +477 -66
  17. package/packages/server/dist/routes/git.js +6 -3
  18. package/packages/server/dist/routes/projects.js +16 -2
  19. package/packages/server/dist/self-mcp-descriptions.d.ts +1 -0
  20. package/packages/server/dist/self-mcp-descriptions.js +14 -1
  21. package/packages/server/dist/self-mcp-types.d.ts +32 -8
  22. package/packages/server/dist/self-mcp-write.d.ts +21 -0
  23. package/packages/server/dist/self-mcp-write.js +44 -0
  24. package/packages/server/dist/self-mcp.d.ts +7 -6
  25. package/packages/server/dist/self-mcp.js +36 -10
  26. package/packages/server/dist/sweep.js +10 -5
  27. package/packages/server/package.json +1 -1
  28. package/packages/web/dist/assets/{InstanceConfigPage-CCk2hVT9.js → InstanceConfigPage-DcprTH2W.js} +1 -1
  29. package/packages/web/dist/assets/{MarkdownRenderer-B6gST_0U.js → MarkdownRenderer-Ri1LwAUN.js} +1 -1
  30. package/packages/web/dist/assets/{ProjectRedirect-BCmKT4mf.js → ProjectRedirect-Dze5H8Xy.js} +1 -1
  31. package/packages/web/dist/assets/{ProjectView-CCN98MWu.js → ProjectView-PrA8purE.js} +9 -9
  32. package/packages/web/dist/assets/{ProjectsGrid-CYzuN_yD.js → ProjectsGrid-BidsM7ah.js} +1 -1
  33. package/packages/web/dist/assets/{arc-BxvtMCZQ.js → arc-eQ3ryE9c.js} +1 -1
  34. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-Bp6ffJ_u.js → architectureDiagram-3BPJPVTR-Dc6Tj2Ai.js} +1 -1
  35. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-BIY2X8nW.js → blockDiagram-GPEHLZMM-DBX5pma_.js} +1 -1
  36. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-Br9stEJS.js → c4Diagram-AAUBKEIU-CnWrtTpa.js} +1 -1
  37. package/packages/web/dist/assets/channel-B4sED4OP.js +2 -0
  38. package/packages/web/dist/assets/{chunk-2J33WTMH-Djc7uqMV.js → chunk-2J33WTMH-2qmLvtsr.js} +1 -1
  39. package/packages/web/dist/assets/{chunk-4BX2VUAB-yEVMLksS.js → chunk-4BX2VUAB-Dn-LUDMV.js} +1 -1
  40. package/packages/web/dist/assets/{chunk-55IACEB6-tJaEdPZg.js → chunk-55IACEB6-DB0augUY.js} +1 -1
  41. package/packages/web/dist/assets/{chunk-727SXJPM-bWEqh672.js → chunk-727SXJPM-BPQQM0i8.js} +1 -1
  42. package/packages/web/dist/assets/{chunk-AQP2D5EJ-B8U9fB-T.js → chunk-AQP2D5EJ-DOhfwA-x.js} +1 -1
  43. package/packages/web/dist/assets/{chunk-FMBD7UC4-BeEITYig.js → chunk-FMBD7UC4-CatHHdFD.js} +1 -1
  44. package/packages/web/dist/assets/{chunk-ND2GUHAM-L2rpLaoK.js → chunk-ND2GUHAM-B645aAlG.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-QZHKN3VN-qqzPMZL_.js → chunk-QZHKN3VN-D1fAaPSZ.js} +1 -1
  46. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-BlVeHFir.js +2 -0
  47. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-BlVeHFir.js +2 -0
  48. package/packages/web/dist/assets/{core-CSaNbmLO.js → core-B4t7D8Om.js} +1 -1
  49. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-BRF7Nech.js → cose-bilkent-S5V4N54A-BLdKDAwa.js} +1 -1
  50. package/packages/web/dist/assets/{dagre-BM42HDAG-D10rJWOR.js → dagre-BM42HDAG-CbTbTVKv.js} +1 -1
  51. package/packages/web/dist/assets/{diagram-2AECGRRQ-DGjT5mAO.js → diagram-2AECGRRQ-CorSS29_.js} +1 -1
  52. package/packages/web/dist/assets/{diagram-5GNKFQAL-DaH3bxHh.js → diagram-5GNKFQAL-PLKivts0.js} +1 -1
  53. package/packages/web/dist/assets/{diagram-KO2AKTUF-QrLW9Wn6.js → diagram-KO2AKTUF-CUSIBacZ.js} +1 -1
  54. package/packages/web/dist/assets/{diagram-LMA3HP47-BX15wgYN.js → diagram-LMA3HP47-CZ69mAZs.js} +1 -1
  55. package/packages/web/dist/assets/{diagram-OG6HWLK6-DXQ_p63s.js → diagram-OG6HWLK6-Cmf042Rh.js} +1 -1
  56. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C_ioRruE.js → erDiagram-TEJ5UH35-DyIKGj6e.js} +1 -1
  57. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-7d-xLXLM.js → flowDiagram-I6XJVG4X-B79pLdLD.js} +1 -1
  58. package/packages/web/dist/assets/{format-BSDZezwD.js → format-C5nhFL8q.js} +1 -1
  59. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DZYx5xcj.js → ganttDiagram-6RSMTGT7-BFTR_oNA.js} +1 -1
  60. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-dimo-osm.js → gitGraphDiagram-PVQCEYII-DdGUlJxS.js} +1 -1
  61. package/packages/web/dist/assets/{index-CUnFKMga.js → index-DVSsta3B.js} +14 -14
  62. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BzApY25x.js → infoDiagram-5YYISTIA-B80FyUOy.js} +1 -1
  63. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DtX47_9p.js → ishikawaDiagram-YF4QCWOH-_wRlphn9.js} +1 -1
  64. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-C7fITKg1.js → journeyDiagram-JHISSGLW-qu4yvjW8.js} +1 -1
  65. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BOrWLf_W.js → kanban-definition-UN3LZRKU-D2DEUp2-.js} +1 -1
  66. package/packages/web/dist/assets/{linear-DuyAaBu6.js → linear-D-CLRUEW.js} +1 -1
  67. package/packages/web/dist/assets/{mermaid.core-BQlzuVvO.js → mermaid.core-DdihBJl4.js} +4 -4
  68. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CmP748B5.js → mindmap-definition-RKZ34NQL-BEh_TDBf.js} +1 -1
  69. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-CRd5Pdfq.js → pieDiagram-4H26LBE5-BbrmLVaZ.js} +1 -1
  70. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-DpJm0Yw_.js → quadrantDiagram-W4KKPZXB-DfVuygAv.js} +1 -1
  71. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-B0o2PRMC.js → requirementDiagram-4Y6WPE33-BHp2QpZt.js} +1 -1
  72. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-CBi3Ek3v.js → sankeyDiagram-5OEKKPKP-D2G6lRMY.js} +1 -1
  73. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-C1stxfQg.js → sequenceDiagram-3UESZ5HK-Dj-jzR-c.js} +1 -1
  74. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-lCkiETWK.js → stateDiagram-AJRCARHV-C5D6bT13.js} +1 -1
  75. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-C8NDOqTq.js +2 -0
  76. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-D3DkWJMC.js → timeline-definition-PNZ67QCA-DE4gGjhP.js} +1 -1
  77. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-CO2-yC7K.js → vennDiagram-CIIHVFJN-C9QNrl92.js} +1 -1
  78. package/packages/web/dist/assets/{wardley-L42UT6IY-Dfjp0kqX.js → wardley-L42UT6IY-U2fSGy-d.js} +1 -1
  79. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-CIl0OPwS.js → wardleyDiagram-YWT4CUSO-B9RhObdH.js} +1 -1
  80. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-CuvlQuup.js → xychartDiagram-2RQKCTM6-BnlPrtQ9.js} +1 -1
  81. package/packages/web/dist/index.html +1 -1
  82. package/packages/web/dist/sw.js +1 -1
  83. package/packages/web/dist/assets/channel-CMdgNeFH.js +0 -2
  84. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +0 -2
  85. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +0 -2
  86. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +0 -2
@@ -146,7 +146,10 @@ export function registerGitWorkspaceRoutes(app, ctx) {
146
146
  }, async (req, reply) => {
147
147
  try {
148
148
  const project = await projects.get(req.params.slug);
149
- return await git.projectStatus(project.dir);
149
+ // The project's WORKING directory, not its metadata dir (#597): for a
150
+ // repo-backed or linked project those differ, and `dir` showed the notes
151
+ // folder rather than the code the project is about.
152
+ return await git.projectStatus(project.workingDir);
150
153
  }
151
154
  catch (err) {
152
155
  return sendProjectError(reply, err);
@@ -181,7 +184,7 @@ export function registerGitWorkspaceRoutes(app, ctx) {
181
184
  }, async (req, reply) => {
182
185
  try {
183
186
  const project = await projects.get(req.params.slug);
184
- const diff = await git.projectDiff(project.dir, req.query.file);
187
+ const diff = await git.projectDiff(project.workingDir, req.query.file);
185
188
  reply.header("content-type", "text/plain; charset=utf-8");
186
189
  return diff;
187
190
  }
@@ -227,7 +230,7 @@ export function registerGitWorkspaceRoutes(app, ctx) {
227
230
  // Optional `files` (project-relative) commits only those changes (#258);
228
231
  // omitted ⇒ commit the whole subtree (legacy behavior).
229
232
  const files = Array.isArray(req.body?.files) ? req.body?.files : undefined;
230
- return await git.commitProject(project.dir, message, files);
233
+ return await git.commitProject(project.workingDir, message, files);
231
234
  }
232
235
  catch (err) {
233
236
  return sendProjectError(reply, err);
@@ -79,7 +79,15 @@ export function registerProjectRoutes(app, ctx) {
79
79
  const projectsOut = await Promise.all(list.map(async (p) => ({
80
80
  ...p,
81
81
  chatTurns: await buildChatTurns(p.slug, turnsByProject, user, readState, unread),
82
- dirty: dirty[p.slug] ?? 0,
82
+ // The cheap store-wide sweep above buckets by path segment under
83
+ // `projectsRoot`, so it can only speak for projects whose working dir is
84
+ // the metadata dir. For one pointing somewhere else (a linked checkout,
85
+ // issue #206) ask its own repo directly — a subprocess, but only for the
86
+ // projects the sweep structurally cannot cover, and only for as many of
87
+ // those as actually exist.
88
+ dirty: p.workingDir === p.dir
89
+ ? (dirty[p.slug] ?? 0)
90
+ : await git.dirtyCountAt(p.workingDir).catch(() => 0),
83
91
  })));
84
92
  // No `dirty` for the root: `dirtyCounts()` buckets by first path segment, so
85
93
  // a file directly in the projects root has no bucket at all. Reporting 0
@@ -93,7 +101,7 @@ export function registerProjectRoutes(app, ctx) {
93
101
  schema: {
94
102
  tags: ["Projects"],
95
103
  summary: "Create a project",
96
- description: "Creates a project from `CreateProjectInput` and registers its agent and its sweeper. Set `repo` to create a repo-backed project (the cloned checkout becomes the agent's working directory); omit it for a notebook project. Responds 201 with `{ project }`.",
104
+ description: "Creates a project from `CreateProjectInput` and registers its agent and its sweeper. Set `repo` to create a repo-backed project (the cloned checkout becomes the agent's working directory), or `path` to LINK an existing on-box git checkout used in place with no copy (that directory becomes the working directory and Paddock writes nothing into it); omit both for a notebook project. Responds 201 with `{ project }`.",
97
105
  body: {
98
106
  type: ["object", "null"],
99
107
  additionalProperties: true,
@@ -113,6 +121,12 @@ export function registerProjectRoutes(app, ctx) {
113
121
  repo: {
114
122
  description: "External git repo URL to back this project (repo-backed if set).",
115
123
  },
124
+ path: {
125
+ description: "Absolute path to the directory this project's content lives in (issue #206). For an unmanaged project this LINKS an existing on-box checkout, used in place with no copy. If it does not exist, Paddock clones `repo` into it when one is given, or creates it for a managed project. Must lie outside the projects root, the data dir, and every other project's working directory. No git repository is required. Immutable once set.",
126
+ },
127
+ managed: {
128
+ description: "Whether Paddock curates this project's own CLAUDE.md/OVERVIEW.md/CHANGELOG.md (issue #206). Omit to derive it: a create naming `repo` or `path` is unmanaged, a plain one is managed. `managed: true` together with `repo` is rejected.",
129
+ },
116
130
  },
117
131
  required: [],
118
132
  },
@@ -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 " +
@@ -39,12 +39,14 @@ export interface SelfMcpCreatedProject {
39
39
  name: string;
40
40
  /** The project's metadata dir (OVERVIEW/CHANGELOG/`.chats` live here). */
41
41
  dir: string;
42
- /** The keeper's cwd — the nested checkout when repo-backed, else `dir`. */
42
+ /** The keeper's cwd — `path`, else the nested checkout, else `dir` (#206). */
43
43
  workingDir: string;
44
- /** Whether the project is backed by a cloned external repo (#187). */
45
- repoBacked: boolean;
46
- /** The repo URL when repo-backed, else undefined. */
44
+ /** Whether Paddock curates this project's own files (#206, replaced `repoBacked`). */
45
+ managed: boolean;
46
+ /** The repo URL when one is recorded, else undefined. */
47
47
  repo?: string;
48
+ /** The nominated content directory when one is set, else undefined (#206). */
49
+ path?: string;
48
50
  /**
49
51
  * Whether the keeper agent registered. The REST create path treats a registration
50
52
  * failure as non-fatal (the project IS created), so this mirrors it — but reports
@@ -52,6 +54,13 @@ export interface SelfMcpCreatedProject {
52
54
  */
53
55
  agentRegistered: boolean;
54
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;
55
64
  /** A chat as surfaced to the agent. */
56
65
  export interface SelfMcpChat {
57
66
  /** Owning project slug. */
@@ -190,10 +199,11 @@ export interface SelfMcpWriteContext {
190
199
  /** Set (or clear) a chat's archived flag (presentational metadata only). */
191
200
  setArchived: (projectSlug: string, sessionId: string, archived: boolean) => Promise<void>;
192
201
  /**
193
- * Whether the PROJECT tools (`create_project`, issue #467) are enabled for this
194
- * instance — the `selfMcpProjectsEnabled` gate, resolved by the caller. When false
195
- * the tool is NOT injected at all (absent, not present-but-refusing), the same
196
- * 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}.
197
207
  */
198
208
  projectsMcpEnabled: boolean;
199
209
  /**
@@ -206,6 +216,20 @@ export interface SelfMcpWriteContext {
206
216
  * non-fatal (mirroring the route) and reported via `agentRegistered`.
207
217
  */
208
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>;
209
233
  /**
210
234
  * Whether the unified trigger-management MCP is enabled for THIS turn's project
211
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
@@ -285,11 +285,16 @@ export class SweepService {
285
285
  // NOCHANGE leaves it untouched. A write failure is non-fatal (OVERVIEW/
286
286
  // CHANGELOG already landed; the watermark should still advance) → warn.
287
287
  //
288
- // REPO-BACKED projects (issue #187): the CLAUDE.md is the external repo's
289
- // OWN, upstream-owned file — the sweeper must NEVER write it (that would
290
- // dirty the checkout and, if pushed, leak curation upstream). OVERVIEW.md +
291
- // CHANGELOG.md are still curated (sidecarred), just not CLAUDE.md.
292
- if (parsed.claude !== null && !project.repoBacked) {
288
+ // UNMANAGED projects (issue #206, formerly keyed on `repoBacked`): the
289
+ // CLAUDE.md belongs to the working directory's own source control — the
290
+ // sweeper must NEVER write it (that would dirty the checkout and, if pushed,
291
+ // leak curation upstream). OVERVIEW.md + CHANGELOG.md are still curated
292
+ // (sidecarred into the metadata dir), just not CLAUDE.md.
293
+ //
294
+ // `managed` is the precise fact here, where `repoBacked` was a proxy for it:
295
+ // what decides this is whether Paddock looks after the project's files, not
296
+ // whether a git repo happens to sit behind them.
297
+ if (parsed.claude !== null && project.managed) {
293
298
  const bounded = enforceHeadBudget(parsed.claude, budgetChars(budget, "claudeMaxTokens"));
294
299
  await this.projects.writeClaudeCurated(project.slug, bounded).catch((err) => {
295
300
  this.log.warn({ err, slug: project.slug }, "sweep: CLAUDE.md write failed (non-fatal)");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paddock/server",
3
- "version": "0.63.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-CUnFKMga.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