@justyork/repo-mind 0.4.0 → 0.7.1

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 (99) hide show
  1. package/README.md +5 -2
  2. package/dist/ab-demo/compute-pass.d.ts +24 -0
  3. package/dist/ab-demo/compute-pass.js +80 -0
  4. package/dist/ab-demo/live-answer.d.ts +6 -0
  5. package/dist/ab-demo/live-answer.js +89 -0
  6. package/dist/ab-demo/live-eval.d.ts +19 -0
  7. package/dist/ab-demo/live-eval.js +192 -0
  8. package/dist/ab-demo/paths.d.ts +1 -0
  9. package/dist/ab-demo/paths.js +3 -0
  10. package/dist/ab-demo/record-transcript.d.ts +9 -0
  11. package/dist/ab-demo/record-transcript.js +97 -0
  12. package/dist/ab-demo/types.d.ts +57 -0
  13. package/dist/ab-demo/validate-questions.d.ts +5 -0
  14. package/dist/ab-demo/validate-questions.js +32 -0
  15. package/dist/agent-write-gate.d.ts +7 -0
  16. package/dist/agent-write-gate.js +33 -0
  17. package/dist/cli.js +102 -0
  18. package/dist/commands/ab-eval.d.ts +11 -0
  19. package/dist/commands/ab-eval.js +103 -0
  20. package/dist/commands/publish.d.ts +11 -0
  21. package/dist/commands/publish.js +91 -0
  22. package/dist/git/git-exec.d.ts +19 -0
  23. package/dist/git/git-exec.js +89 -0
  24. package/dist/git/publish-pr.d.ts +33 -0
  25. package/dist/git/publish-pr.js +121 -0
  26. package/dist/index/slug.d.ts +2 -0
  27. package/dist/index/slug.js +15 -0
  28. package/dist/mcp/server.js +40 -0
  29. package/dist/publish/batch-publish.d.ts +29 -0
  30. package/dist/publish/batch-publish.js +106 -0
  31. package/dist/tools/create-draft.d.ts +32 -0
  32. package/dist/tools/create-draft.js +163 -0
  33. package/dist/ui/draft-api.js +33 -1
  34. package/dist/ui/fs-operations.d.ts +13 -0
  35. package/dist/ui/fs-operations.js +139 -0
  36. package/dist/ui/fs-tree.d.ts +6 -1
  37. package/dist/ui/fs-tree.js +62 -36
  38. package/package.json +9 -1
  39. package/ui/dist/assets/{arc-C6B0IXf5.js → arc-B9D1Pvqi.js} +1 -1
  40. package/ui/dist/assets/{architectureDiagram-3BPJPVTR-BwcC0zwn.js → architectureDiagram-3BPJPVTR-Bvug0Ca5.js} +1 -1
  41. package/ui/dist/assets/{blockDiagram-GPEHLZMM-DIhdWMA6.js → blockDiagram-GPEHLZMM-Ckmilsu4.js} +1 -1
  42. package/ui/dist/assets/{c4Diagram-AAUBKEIU-DAe6bsUB.js → c4Diagram-AAUBKEIU-BT9pbmn0.js} +1 -1
  43. package/ui/dist/assets/channel-TAOLyHdK.js +1 -0
  44. package/ui/dist/assets/{chunk-2J33WTMH-Cy3md3pb.js → chunk-2J33WTMH-dgxq-Xfu.js} +1 -1
  45. package/ui/dist/assets/{chunk-4BX2VUAB-CzL8eUDD.js → chunk-4BX2VUAB-DmM9cIh_.js} +1 -1
  46. package/ui/dist/assets/{chunk-55IACEB6-CbthXzDA.js → chunk-55IACEB6-DsB88bPS.js} +1 -1
  47. package/ui/dist/assets/{chunk-727SXJPM-B7ZW-l9j.js → chunk-727SXJPM-BYuDClxC.js} +1 -1
  48. package/ui/dist/assets/{chunk-AQP2D5EJ-Vaux-7Ld.js → chunk-AQP2D5EJ-RO0Jvaof.js} +1 -1
  49. package/ui/dist/assets/{chunk-FMBD7UC4-C6P2YSWU.js → chunk-FMBD7UC4-DqY1unJF.js} +1 -1
  50. package/ui/dist/assets/{chunk-ND2GUHAM-gSNqdDAn.js → chunk-ND2GUHAM-BAURH6-3.js} +1 -1
  51. package/ui/dist/assets/{chunk-QZHKN3VN-DPPMoZqF.js → chunk-QZHKN3VN-BiCWufLo.js} +1 -1
  52. package/ui/dist/assets/classDiagram-4FO5ZUOK-0RJZpoIN.js +1 -0
  53. package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-0RJZpoIN.js +1 -0
  54. package/ui/dist/assets/{cose-bilkent-S5V4N54A-Ca4nXCOA.js → cose-bilkent-S5V4N54A-Bol8tJZ0.js} +1 -1
  55. package/ui/dist/assets/{dagre-BM42HDAG-C-4Y4Jyn.js → dagre-BM42HDAG-ziNCIqra.js} +1 -1
  56. package/ui/dist/assets/{diagram-2AECGRRQ-CNv_WVZk.js → diagram-2AECGRRQ-Dh2G8IOg.js} +1 -1
  57. package/ui/dist/assets/{diagram-5GNKFQAL-CNwYGvCw.js → diagram-5GNKFQAL-C_26Fq9O.js} +1 -1
  58. package/ui/dist/assets/{diagram-KO2AKTUF-CEULTL49.js → diagram-KO2AKTUF-CPS1B4bk.js} +1 -1
  59. package/ui/dist/assets/{diagram-LMA3HP47-sGQpPW5i.js → diagram-LMA3HP47-DoL5Nx28.js} +1 -1
  60. package/ui/dist/assets/{diagram-OG6HWLK6-BBEtEBkh.js → diagram-OG6HWLK6-DOy-TY5D.js} +1 -1
  61. package/ui/dist/assets/editor-CTtDusro.js +211 -0
  62. package/ui/dist/assets/{erDiagram-TEJ5UH35-B4mEdVPJ.js → erDiagram-TEJ5UH35-C2whsoWw.js} +1 -1
  63. package/ui/dist/assets/{flowDiagram-I6XJVG4X-7-OngwLn.js → flowDiagram-I6XJVG4X-D7-1XdVh.js} +1 -1
  64. package/ui/dist/assets/{ganttDiagram-6RSMTGT7-Bq3z_Nvr.js → ganttDiagram-6RSMTGT7-Dduumk4v.js} +1 -1
  65. package/ui/dist/assets/{gitGraphDiagram-PVQCEYII-BjMdjnGR.js → gitGraphDiagram-PVQCEYII-CJfZihUe.js} +1 -1
  66. package/ui/dist/assets/{graph-CwHQTpjf.js → graph-CFkZObJQ.js} +1 -1
  67. package/ui/dist/assets/{infoDiagram-5YYISTIA-ByORmROU.js → infoDiagram-5YYISTIA-B8IsGW31.js} +1 -1
  68. package/ui/dist/assets/{ishikawaDiagram-YF4QCWOH-B7yRHlcY.js → ishikawaDiagram-YF4QCWOH-CwaI1VjO.js} +1 -1
  69. package/ui/dist/assets/{journeyDiagram-JHISSGLW-J5mLr9YH.js → journeyDiagram-JHISSGLW-jo8e3Vri.js} +1 -1
  70. package/ui/dist/assets/{kanban-definition-UN3LZRKU-B3GyGD8X.js → kanban-definition-UN3LZRKU-DsFcvv5g.js} +1 -1
  71. package/ui/dist/assets/main-DEdNYNxf.js +274 -0
  72. package/ui/dist/assets/{mermaid.core-C57NVZG0.js → mermaid.core-CIyMeT5H.js} +4 -4
  73. package/ui/dist/assets/{mindmap-definition-RKZ34NQL-CkcXt9tU.js → mindmap-definition-RKZ34NQL-Bl3ML0fg.js} +1 -1
  74. package/ui/dist/assets/{pieDiagram-4H26LBE5-Y7B3kCyQ.js → pieDiagram-4H26LBE5-rCj5-rCc.js} +1 -1
  75. package/ui/dist/assets/{quadrantDiagram-W4KKPZXB-CXKUOw1G.js → quadrantDiagram-W4KKPZXB-tF5l8Pj1.js} +1 -1
  76. package/ui/dist/assets/{requirementDiagram-4Y6WPE33-DBmmmiXs.js → requirementDiagram-4Y6WPE33-CtAsbYzh.js} +1 -1
  77. package/ui/dist/assets/{sankeyDiagram-5OEKKPKP-BNADiaE4.js → sankeyDiagram-5OEKKPKP-CYgylTyv.js} +1 -1
  78. package/ui/dist/assets/{sequenceDiagram-3UESZ5HK-CctElOC5.js → sequenceDiagram-3UESZ5HK-D2FeAk8O.js} +1 -1
  79. package/ui/dist/assets/{stateDiagram-AJRCARHV-D40_7g7Y.js → stateDiagram-AJRCARHV-BhNt372S.js} +1 -1
  80. package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-hesflu-P.js +1 -0
  81. package/ui/dist/assets/theme-CgUWrIXo.css +1 -0
  82. package/ui/dist/assets/theme-DBoBdw62.js +1 -0
  83. package/ui/dist/assets/{timeline-definition-PNZ67QCA-DMVhUcek.js → timeline-definition-PNZ67QCA-DYrj-JqE.js} +1 -1
  84. package/ui/dist/assets/{vennDiagram-CIIHVFJN-Ci0RqPuV.js → vennDiagram-CIIHVFJN-DAdImqHv.js} +1 -1
  85. package/ui/dist/assets/visual-editor-BHZk1CI4.js +220 -0
  86. package/ui/dist/assets/{wardley-L42UT6IY-Chn0BKir.js → wardley-L42UT6IY-BfiflBRy.js} +1 -1
  87. package/ui/dist/assets/{wardleyDiagram-YWT4CUSO-BLdXlrC5.js → wardleyDiagram-YWT4CUSO-5HZM-go9.js} +1 -1
  88. package/ui/dist/assets/{xychartDiagram-2RQKCTM6-C4gOVVe1.js → xychartDiagram-2RQKCTM6-ozNCw5n2.js} +1 -1
  89. package/ui/dist/graph.html +3 -3
  90. package/ui/dist/index.html +4 -4
  91. package/ui/dist/assets/channel-C8Q7-wTd.js +0 -1
  92. package/ui/dist/assets/classDiagram-4FO5ZUOK-pYCdSDhT.js +0 -1
  93. package/ui/dist/assets/classDiagram-v2-Q7XG4LA2-pYCdSDhT.js +0 -1
  94. package/ui/dist/assets/editor-BcAuZsp8.js +0 -211
  95. package/ui/dist/assets/main-Du7fLv5Y.js +0 -244
  96. package/ui/dist/assets/stateDiagram-v2-BHNVJYJU-DSc6L6rE.js +0 -1
  97. package/ui/dist/assets/theme-BJgORXba.css +0 -1
  98. package/ui/dist/assets/theme-DxqwV6dp.js +0 -1
  99. package/ui/dist/assets/visual-editor-UWcMGp6p.js +0 -39
@@ -3,6 +3,21 @@ export const SLUG_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
3
3
  export function isValidSlug(slug) {
4
4
  return SLUG_PATTERN.test(slug);
5
5
  }
6
+ /** Derives a kebab-case slug from a human title. */
7
+ export function slugFromTitle(title) {
8
+ const normalized = title
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/-+/g, '-')
13
+ .replace(/^-|-$/g, '');
14
+ if (isValidSlug(normalized)) {
15
+ return normalized;
16
+ }
17
+ const fallback = normalized ? `draft-${normalized}` : 'draft';
18
+ const trimmed = fallback.slice(0, 80).replace(/-+$/g, '');
19
+ return isValidSlug(trimmed) ? trimmed : 'draft';
20
+ }
6
21
  /** Builds a stable slug from a path relative to the docs root. */
7
22
  export function slugFromRelativePath(relativePath) {
8
23
  const normalized = relativePath
@@ -5,6 +5,7 @@ import { getPackageVersion } from '../package-version.js';
5
5
  import { DocIndex } from '../index/doc-index.js';
6
6
  import { DOC_TYPES, DOC_STATUSES, DOC_DOMAINS } from '../index/types.js';
7
7
  import { exploreGraph } from '../tools/explore-graph.js';
8
+ import { createDraft } from '../tools/create-draft.js';
8
9
  import { getDoc } from '../tools/get-doc.js';
9
10
  import { getGlossaryTerm } from '../tools/get-glossary-term.js';
10
11
  import { listDocs } from '../tools/list-docs.js';
@@ -15,6 +16,7 @@ const TOOL_NAMES = [
15
16
  'get_doc',
16
17
  'get_glossary_term',
17
18
  'explore_graph',
19
+ 'create_draft',
18
20
  ];
19
21
  function isToolName(name) {
20
22
  return TOOL_NAMES.includes(name);
@@ -85,6 +87,23 @@ export async function startMcpServer() {
85
87
  required: ['slug'],
86
88
  },
87
89
  },
90
+ {
91
+ name: 'create_draft',
92
+ description: 'Create a SQLite-backed draft for human review in repo-mind UI (gated on ab-demo kill-switch or REPOMIND_AGENT_WRITE=1).',
93
+ inputSchema: {
94
+ type: 'object',
95
+ properties: {
96
+ type: { type: 'string', enum: [...DOC_TYPES] },
97
+ title: { type: 'string' },
98
+ body: { type: 'string' },
99
+ slug: { type: 'string' },
100
+ related: { type: 'array', items: { type: 'string' } },
101
+ tags: { type: 'array', items: { type: 'string' } },
102
+ forked_from: { type: 'string' },
103
+ },
104
+ required: ['type', 'title', 'body'],
105
+ },
106
+ },
88
107
  ],
89
108
  }));
90
109
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -125,6 +144,21 @@ export async function startMcpServer() {
125
144
  slug: typeof args.slug === 'string' ? args.slug : '',
126
145
  depth: typeof args.depth === 'number' ? args.depth : undefined,
127
146
  }));
147
+ case 'create_draft': {
148
+ const result = createDraft(index, {
149
+ type: typeof args.type === 'string' ? args.type : '',
150
+ title: typeof args.title === 'string' ? args.title : '',
151
+ body: typeof args.body === 'string' ? args.body : '',
152
+ slug: typeof args.slug === 'string' ? args.slug : undefined,
153
+ related: Array.isArray(args.related) ? args.related : undefined,
154
+ tags: Array.isArray(args.tags) ? args.tags : undefined,
155
+ forked_from: typeof args.forked_from === 'string' ? args.forked_from : undefined,
156
+ });
157
+ if (!result.ok) {
158
+ return toolError(result.error);
159
+ }
160
+ return toolResult(result.data);
161
+ }
128
162
  default: {
129
163
  const unknownTool = toolName;
130
164
  throw new Error(`Unknown tool: ${unknownTool}`);
@@ -153,3 +187,9 @@ function toolResult(payload) {
153
187
  content: [{ type: 'text', text: JSON.stringify(payload) }],
154
188
  };
155
189
  }
190
+ function toolError(error) {
191
+ return {
192
+ content: [{ type: 'text', text: JSON.stringify({ error }) }],
193
+ isError: true,
194
+ };
195
+ }
@@ -0,0 +1,29 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ import type { DraftsDb } from '../ui/db/drafts-db.js';
3
+ export interface PublishedDraft {
4
+ draftId: string;
5
+ slug: string;
6
+ title: string;
7
+ path: string;
8
+ }
9
+ export interface BatchPublishFailure {
10
+ draftId: string;
11
+ slug: string;
12
+ code: string;
13
+ message: string;
14
+ }
15
+ export interface BatchPublishResult {
16
+ published: PublishedDraft[];
17
+ failures: BatchPublishFailure[];
18
+ }
19
+ export interface BatchPublishOptions {
20
+ draftIds?: string[];
21
+ dryRun?: boolean;
22
+ }
23
+ export declare function batchPublishDrafts(index: DocIndex, db: DraftsDb, options?: BatchPublishOptions): BatchPublishResult;
24
+ export declare function defaultCommitMessage(published: PublishedDraft[]): string;
25
+ export declare function defaultPrTitle(published: PublishedDraft[]): string;
26
+ export declare function defaultPrBody(published: PublishedDraft[]): string;
27
+ export declare function buildPublishBranchName(published: PublishedDraft[]): string;
28
+ export declare function resolveUniqueBranchName(repoRoot: string, preferred: string): string;
29
+ export declare function relativeDocsPaths(repoRoot: string, absolutePaths: string[]): string[];
@@ -0,0 +1,106 @@
1
+ import path from 'node:path';
2
+ import { branchExists } from '../git/git-exec.js';
3
+ import { publishDraft, resolvePublishTargetPath, validateDraftForPublish } from '../ui/publish.js';
4
+ function selectDrafts(db, draftIds) {
5
+ const active = db.listActive();
6
+ if (!draftIds || draftIds.length === 0) {
7
+ return active;
8
+ }
9
+ const byId = new Map(active.map((draft) => [draft.id, draft]));
10
+ return draftIds.map((id) => byId.get(id) ?? null).filter((draft) => draft !== null);
11
+ }
12
+ export function batchPublishDrafts(index, db, options = {}) {
13
+ const drafts = selectDrafts(db, options.draftIds);
14
+ const failures = [];
15
+ const validated = [];
16
+ for (const draft of drafts) {
17
+ const validation = validateDraftForPublish(index, draft);
18
+ if (validation) {
19
+ failures.push({
20
+ draftId: draft.id,
21
+ slug: draft.slug,
22
+ code: validation.code,
23
+ message: validation.message,
24
+ });
25
+ continue;
26
+ }
27
+ validated.push(draft);
28
+ }
29
+ if (failures.length > 0) {
30
+ return { published: [], failures };
31
+ }
32
+ if (options.dryRun) {
33
+ return {
34
+ published: validated.map((draft) => ({
35
+ draftId: draft.id,
36
+ slug: draft.slug,
37
+ title: draft.title,
38
+ path: resolvePublishTargetPath(index, draft) ?? '(unresolved)',
39
+ })),
40
+ failures: [],
41
+ };
42
+ }
43
+ const published = [];
44
+ for (const draft of validated) {
45
+ const result = publishDraft(index, draft);
46
+ db.markPublished(draft.id, result.path);
47
+ published.push({
48
+ draftId: draft.id,
49
+ slug: draft.slug,
50
+ title: draft.title,
51
+ path: result.path,
52
+ });
53
+ }
54
+ return { published, failures: [] };
55
+ }
56
+ export function defaultCommitMessage(published) {
57
+ const lines = ['docs: publish via repo-mind', ''];
58
+ for (const item of published) {
59
+ lines.push(`- ${item.slug} (${item.title})`);
60
+ }
61
+ return lines.join('\n').trim();
62
+ }
63
+ export function defaultPrTitle(published) {
64
+ if (published.length === 1) {
65
+ return `docs: publish ${published[0].slug}`;
66
+ }
67
+ return `docs: publish ${published.length} pages via repo-mind`;
68
+ }
69
+ export function defaultPrBody(published) {
70
+ const lines = [
71
+ '## Summary',
72
+ '',
73
+ 'Published knowledge pages from repo-mind drafts:',
74
+ '',
75
+ ];
76
+ for (const item of published) {
77
+ lines.push(`- \`${item.slug}\` — ${item.title}`);
78
+ }
79
+ lines.push('', '## Test plan', '', '- [ ] `repo-mind check` passes', '- [ ] Pages render in repo-mind UI');
80
+ return lines.join('\n');
81
+ }
82
+ export function buildPublishBranchName(published) {
83
+ const slugPart = published
84
+ .map((item) => item.slug)
85
+ .slice(0, 2)
86
+ .join('-')
87
+ .slice(0, 40);
88
+ const stamp = new Date().toISOString().replace(/[-:]/g, '').slice(0, 13);
89
+ const suffix = slugPart ? `${slugPart}-${stamp}` : stamp;
90
+ return `repo-mind/publish/${suffix}`;
91
+ }
92
+ export function resolveUniqueBranchName(repoRoot, preferred) {
93
+ if (!branchExists(repoRoot, preferred)) {
94
+ return preferred;
95
+ }
96
+ for (let index = 2; index <= 9; index += 1) {
97
+ const candidate = `${preferred}-${index}`;
98
+ if (!branchExists(repoRoot, candidate)) {
99
+ return candidate;
100
+ }
101
+ }
102
+ return `${preferred}-${Date.now().toString(36)}`;
103
+ }
104
+ export function relativeDocsPaths(repoRoot, absolutePaths) {
105
+ return absolutePaths.map((absolutePath) => path.relative(repoRoot, absolutePath).replace(/\\/g, '/'));
106
+ }
@@ -0,0 +1,32 @@
1
+ import type { DocIndex } from '../index/doc-index.js';
2
+ export type CreateDraftErrorCode = 'AGENT_WRITE_DISABLED' | 'BAD_TYPE' | 'BAD_SLUG' | 'BAD_TITLE' | 'DRAFT_EXISTS' | 'DOC_NOT_FOUND' | 'NO_KNOWLEDGE_ROOT' | 'SLUG_CONFLICT' | 'WRITE_CONFLICT';
3
+ export interface CreateDraftError {
4
+ code: CreateDraftErrorCode;
5
+ message: string;
6
+ hint?: string;
7
+ }
8
+ export interface CreateDraftSuccess {
9
+ draftId: string;
10
+ slug: string;
11
+ title: string;
12
+ type: string;
13
+ editUrl: string;
14
+ editHint: string;
15
+ }
16
+ export type CreateDraftResult = {
17
+ ok: true;
18
+ data: CreateDraftSuccess;
19
+ } | {
20
+ ok: false;
21
+ error: CreateDraftError;
22
+ };
23
+ export interface CreateDraftInput {
24
+ type: string;
25
+ title: string;
26
+ body: string;
27
+ slug?: string;
28
+ related?: string[];
29
+ tags?: string[];
30
+ forked_from?: string;
31
+ }
32
+ export declare function createDraft(index: DocIndex, rawInput: CreateDraftInput, cwd?: string): CreateDraftResult;
@@ -0,0 +1,163 @@
1
+ import { isValidSlug, slugFromTitle } from '../index/slug.js';
2
+ import { DOC_TYPES, isDocType } from '../index/types.js';
3
+ import { getAgentWriteGate } from '../agent-write-gate.js';
4
+ import { getDoc } from './get-doc.js';
5
+ import { openDraftsDb } from '../ui/db/drafts-db.js';
6
+ function stringArray(value) {
7
+ if (!Array.isArray(value)) {
8
+ return [];
9
+ }
10
+ return value.filter((item) => typeof item === 'string');
11
+ }
12
+ function normalizeInput(raw) {
13
+ return {
14
+ type: raw.type.trim(),
15
+ title: raw.title.trim(),
16
+ body: raw.body,
17
+ slug: typeof raw.slug === 'string' && raw.slug.trim() ? raw.slug.trim() : undefined,
18
+ related: stringArray(raw.related),
19
+ tags: stringArray(raw.tags),
20
+ forked_from: typeof raw.forked_from === 'string' && raw.forked_from.trim()
21
+ ? raw.forked_from.trim()
22
+ : undefined,
23
+ };
24
+ }
25
+ function fail(code, message, hint) {
26
+ return { ok: false, error: { code, message, hint } };
27
+ }
28
+ function success(draft) {
29
+ const editUrl = `/?draft=${encodeURIComponent(draft.id)}`;
30
+ return {
31
+ ok: true,
32
+ data: {
33
+ draftId: draft.id,
34
+ slug: draft.slug,
35
+ title: draft.title,
36
+ type: draft.type,
37
+ editUrl,
38
+ editHint: `Run \`repo-mind ui\` and open draft "${draft.title}" in the sidebar (${editUrl}).`,
39
+ },
40
+ };
41
+ }
42
+ function slugTaken(index, slug, allowExistingDoc) {
43
+ if (index.getDocBySlug(slug) && !allowExistingDoc) {
44
+ return true;
45
+ }
46
+ return false;
47
+ }
48
+ function resolveAvailableSlug(index, db, base, options) {
49
+ if (!isValidSlug(base)) {
50
+ return { code: 'BAD_SLUG', message: `invalid slug: ${base}` };
51
+ }
52
+ const isFree = (slug) => !db.getActiveBySlug(slug) && !slugTaken(index, slug, options.allowExistingDoc);
53
+ if (isFree(base)) {
54
+ return base;
55
+ }
56
+ if (!options.autoSuffix) {
57
+ if (db.getActiveBySlug(base)) {
58
+ return {
59
+ code: 'DRAFT_EXISTS',
60
+ message: `active draft already exists for slug: ${base}`,
61
+ };
62
+ }
63
+ return {
64
+ code: 'SLUG_CONFLICT',
65
+ message: `published document already exists for slug: ${base}`,
66
+ hint: 'Use forked_from to edit an existing page, or omit slug to auto-assign a suffix.',
67
+ };
68
+ }
69
+ for (let suffix = 2; suffix <= 9; suffix += 1) {
70
+ const candidate = `${base}-${suffix}`;
71
+ if (isValidSlug(candidate) && isFree(candidate)) {
72
+ return candidate;
73
+ }
74
+ }
75
+ return {
76
+ code: 'WRITE_CONFLICT',
77
+ message: `could not allocate slug for base: ${base}`,
78
+ hint: 'All slug suffixes -2 through -9 are taken.',
79
+ };
80
+ }
81
+ export function createDraft(index, rawInput, cwd = process.cwd()) {
82
+ const gate = getAgentWriteGate(cwd);
83
+ if (!gate.enabled) {
84
+ return fail('AGENT_WRITE_DISABLED', gate.reason, 'Set REPOMIND_AGENT_WRITE=1 for local override.');
85
+ }
86
+ const knowledgeRoot = index.getKnowledgeRoot();
87
+ if (!knowledgeRoot) {
88
+ return fail('NO_KNOWLEDGE_ROOT', 'no docs/ knowledge root found');
89
+ }
90
+ const input = normalizeInput(rawInput);
91
+ const db = openDraftsDb(knowledgeRoot);
92
+ try {
93
+ if (input.forked_from) {
94
+ if (!isValidSlug(input.forked_from)) {
95
+ return fail('BAD_SLUG', `invalid forked_from slug: ${input.forked_from}`);
96
+ }
97
+ const existing = db.getActiveBySlug(input.forked_from);
98
+ if (existing) {
99
+ return fail('DRAFT_EXISTS', `active draft already exists for slug: ${input.forked_from}`);
100
+ }
101
+ const doc = getDoc(index, input.forked_from);
102
+ if (!doc.found || !doc.slug || !doc.frontmatter) {
103
+ return fail('DOC_NOT_FOUND', `document not found: ${input.forked_from}`);
104
+ }
105
+ const docRecord = index.getDocBySlug(input.forked_from);
106
+ try {
107
+ const draft = db.create({
108
+ slug: doc.slug,
109
+ type: doc.frontmatter.type,
110
+ title: input.title || doc.frontmatter.title || doc.slug,
111
+ body: input.body || doc.body || '',
112
+ tags: (input.tags ?? []).length > 0 ? input.tags ?? [] : (doc.frontmatter.tags ?? []),
113
+ related: (input.related ?? []).length > 0 ? input.related ?? [] : (doc.frontmatter.related ?? []),
114
+ forked_from: doc.slug,
115
+ target_path: docRecord?.relativePath ?? null,
116
+ });
117
+ return success(draft);
118
+ }
119
+ catch (error) {
120
+ const message = error instanceof Error ? error.message : String(error);
121
+ if (message.includes('already exists')) {
122
+ return fail('DRAFT_EXISTS', message);
123
+ }
124
+ throw error;
125
+ }
126
+ }
127
+ if (!input.title) {
128
+ return fail('BAD_TITLE', 'title is required');
129
+ }
130
+ if (!isDocType(input.type)) {
131
+ return fail('BAD_TYPE', `invalid type — expected one of: ${DOC_TYPES.join(', ')}`);
132
+ }
133
+ const baseSlug = input.slug ?? slugFromTitle(input.title);
134
+ const resolved = resolveAvailableSlug(index, db, baseSlug, {
135
+ autoSuffix: !input.slug,
136
+ allowExistingDoc: false,
137
+ });
138
+ if (typeof resolved !== 'string') {
139
+ return { ok: false, error: resolved };
140
+ }
141
+ try {
142
+ const draft = db.create({
143
+ slug: resolved,
144
+ type: input.type,
145
+ title: input.title,
146
+ body: input.body,
147
+ tags: input.tags,
148
+ related: input.related,
149
+ });
150
+ return success(draft);
151
+ }
152
+ catch (error) {
153
+ const message = error instanceof Error ? error.message : String(error);
154
+ if (message.includes('already exists')) {
155
+ return fail('DRAFT_EXISTS', message);
156
+ }
157
+ throw error;
158
+ }
159
+ }
160
+ finally {
161
+ db.close();
162
+ }
163
+ }
@@ -2,7 +2,7 @@ import { runExport } from '../commands/export.js';
2
2
  import { isValidSlug } from '../index/slug.js';
3
3
  import { DOC_TYPES, isDocStatus, isDocType } from '../index/types.js';
4
4
  import { writeCatalogEmoji } from './catalog-meta.js';
5
- import { createFolder, createPageFile, deleteFolder, deletePageFile, movePageFile, renamePageFile } from './fs-operations.js';
5
+ import { createFolder, createPageFile, deleteFolder, deletePageFile, moveFolder, movePageFile, promotePageToFolder, renamePageFile } from './fs-operations.js';
6
6
  import { prepareDocFile, prepareAllDocs } from '../prepare/prepare-docs.js';
7
7
  import { syncAllDocLinks } from '../prepare/auto-links.js';
8
8
  import { getDoc } from '../tools/get-doc.js';
@@ -102,6 +102,7 @@ export function handleDraftApi(index, db, method, pathname, bodyRaw) {
102
102
  body: doc.body ?? '',
103
103
  tags,
104
104
  related,
105
+ forked_from: page.slug,
105
106
  target_path: page.relativePath,
106
107
  });
107
108
  return { status: 201, body: { page, draft } };
@@ -111,6 +112,21 @@ export function handleDraftApi(index, db, method, pathname, bodyRaw) {
111
112
  return jsonError(400, message);
112
113
  }
113
114
  }
115
+ if (pathname === '/api/fs/promote-page' && method === 'POST') {
116
+ const body = parseJsonBody(bodyRaw);
117
+ if (body === null) {
118
+ return jsonError(400, 'invalid JSON body');
119
+ }
120
+ const pagePath = typeof body.pagePath === 'string' ? body.pagePath : '';
121
+ try {
122
+ const result = promotePageToFolder(index, pagePath);
123
+ return { status: 200, body: { result } };
124
+ }
125
+ catch (error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ return jsonError(400, message);
128
+ }
129
+ }
114
130
  if (pathname === '/api/fs/move' && method === 'POST') {
115
131
  const body = parseJsonBody(bodyRaw);
116
132
  if (body === null) {
@@ -127,6 +143,22 @@ export function handleDraftApi(index, db, method, pathname, bodyRaw) {
127
143
  return jsonError(400, message);
128
144
  }
129
145
  }
146
+ if (pathname === '/api/fs/move-folder' && method === 'POST') {
147
+ const body = parseJsonBody(bodyRaw);
148
+ if (body === null) {
149
+ return jsonError(400, 'invalid JSON body');
150
+ }
151
+ const fromPath = typeof body.fromPath === 'string' ? body.fromPath : '';
152
+ const toParentDir = typeof body.toParentDir === 'string' ? body.toParentDir : '';
153
+ try {
154
+ const result = moveFolder(index, fromPath, toParentDir);
155
+ return { status: 200, body: { result } };
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ return jsonError(400, message);
160
+ }
161
+ }
130
162
  if (pathname === '/api/fs/rename' && method === 'POST') {
131
163
  const body = parseJsonBody(bodyRaw);
132
164
  if (body === null) {
@@ -44,9 +44,22 @@ export interface CreatePageOptions {
44
44
  title?: string;
45
45
  templateId?: string;
46
46
  }
47
+ export interface PromotePageResult extends FsPageMutationResult {
48
+ folderPath: string;
49
+ }
47
50
  export declare function createFolder(index: DocIndex, parentPath: string, name: string): CreateFolderResult;
48
51
  export declare function createPageFile(index: DocIndex, parentPath: string, name: string, options?: CreatePageOptions): CreatePageResult;
52
+ /** Creates sibling folder `{parent}/{basename}/` for a leaf page (Confluence-style); page file stays in place. */
53
+ export declare function promotePageToFolder(index: DocIndex, pagePath: string): PromotePageResult;
49
54
  export declare function movePageFile(index: DocIndex, fromPath: string, toDir: string): FsPageMutationResult;
50
55
  export declare function renamePageFile(index: DocIndex, pagePath: string, newName: string): FsPageMutationResult;
56
+ export interface FsMoveFolderResult {
57
+ relativePath: string;
58
+ previousPath: string;
59
+ siblingPagePath?: string;
60
+ cascadeUpdated: string[];
61
+ }
62
+ /** Moves a folder and its Confluence sibling page file when present. */
63
+ export declare function moveFolder(index: DocIndex, fromPath: string, toParentDir: string): FsMoveFolderResult;
51
64
  export declare function deletePageFile(index: DocIndex, pagePath: string): FsDeletePageResult;
52
65
  export declare function deleteFolder(index: DocIndex, folderPath: string): FsDeleteFolderResult;
@@ -115,6 +115,58 @@ function rewritePageFrontmatter(absolutePath, newRelativePath) {
115
115
  fs.writeFileSync(absolutePath, matter.stringify(parsed.content, nextData), 'utf8');
116
116
  return { newSlug, newType };
117
117
  }
118
+ /** Creates sibling folder `{parent}/{basename}/` for a leaf page (Confluence-style); page file stays in place. */
119
+ export function promotePageToFolder(index, pagePath) {
120
+ const knowledgeRoot = index.getKnowledgeRoot();
121
+ if (!knowledgeRoot) {
122
+ throw new Error('no docs/ directory found');
123
+ }
124
+ const normalizedFrom = normalizeRelativePath(pagePath);
125
+ if (normalizedFrom === 'README.md') {
126
+ throw new Error('cannot promote docs root index');
127
+ }
128
+ const fileName = path.posix.basename(normalizedFrom);
129
+ if (fileName.toLowerCase() === 'readme.md') {
130
+ throw new Error('page is already inside a folder');
131
+ }
132
+ const sourceAbsolute = resolveRelativeMdPath(knowledgeRoot, normalizedFrom);
133
+ if (!sourceAbsolute || !fs.existsSync(sourceAbsolute)) {
134
+ throw new Error(`page not found: ${pagePath}`);
135
+ }
136
+ const parentPath = parentRelativePath(normalizedFrom);
137
+ const baseName = knowledgeFileDisplayName(fileName);
138
+ if (!isValidFsName(baseName)) {
139
+ throw new Error(`invalid page name: ${baseName}`);
140
+ }
141
+ const folderRelative = joinRelativePath(parentPath, baseName);
142
+ const folderAbsolute = resolveUnderKnowledgeRoot(knowledgeRoot, folderRelative);
143
+ if (!folderAbsolute) {
144
+ throw new Error('path escapes docs/');
145
+ }
146
+ if (fs.existsSync(folderAbsolute)) {
147
+ if (!fs.statSync(folderAbsolute).isDirectory()) {
148
+ throw new Error(`already exists: ${folderRelative}`);
149
+ }
150
+ }
151
+ else {
152
+ fs.mkdirSync(folderAbsolute, { recursive: true });
153
+ }
154
+ const previousSlug = readPageSlug(sourceAbsolute);
155
+ index.refresh();
156
+ return {
157
+ relativePath: normalizedFrom,
158
+ absolutePath: sourceAbsolute,
159
+ slug: previousSlug,
160
+ previousSlug,
161
+ slugChanged: false,
162
+ inboundWarnings: [],
163
+ cascadeUpdated: [],
164
+ folderPath: normalizeRelativePath(folderRelative),
165
+ };
166
+ }
167
+ function knowledgeFileDisplayName(fileName) {
168
+ return fileName.replace(/\.(md|ya?ml|json)$/i, '');
169
+ }
118
170
  export function movePageFile(index, fromPath, toDir) {
119
171
  const knowledgeRoot = index.getKnowledgeRoot();
120
172
  if (!knowledgeRoot) {
@@ -244,6 +296,93 @@ function mergeInboundWarnings(lists) {
244
296
  }
245
297
  return merged;
246
298
  }
299
+ function isDescendantOrEqual(ancestor, candidate) {
300
+ const a = normalizeRelativePath(ancestor);
301
+ const c = normalizeRelativePath(candidate);
302
+ if (!a) {
303
+ return true;
304
+ }
305
+ return c === a || c.startsWith(`${a}/`);
306
+ }
307
+ function absoluteToDocsRelative(knowledgeRoot, absolutePath) {
308
+ return normalizeRelativePath(path.relative(knowledgeRoot, absolutePath));
309
+ }
310
+ /** Moves a folder and its Confluence sibling page file when present. */
311
+ export function moveFolder(index, fromPath, toParentDir) {
312
+ const knowledgeRoot = index.getKnowledgeRoot();
313
+ if (!knowledgeRoot) {
314
+ throw new Error('no docs/ directory found');
315
+ }
316
+ const normalizedFrom = normalizeRelativePath(fromPath);
317
+ if (!normalizedFrom) {
318
+ throw new Error('cannot move docs root');
319
+ }
320
+ const sourceAbsolute = resolveUnderKnowledgeRoot(knowledgeRoot, normalizedFrom);
321
+ if (!sourceAbsolute || !fs.existsSync(sourceAbsolute)) {
322
+ throw new Error(`folder not found: ${fromPath}`);
323
+ }
324
+ if (!fs.statSync(sourceAbsolute).isDirectory()) {
325
+ throw new Error(`not a folder: ${fromPath}`);
326
+ }
327
+ const destParent = normalizeRelativePath(toParentDir);
328
+ if (isDescendantOrEqual(normalizedFrom, destParent)) {
329
+ throw new Error('cannot move folder into itself');
330
+ }
331
+ const folderName = path.posix.basename(normalizedFrom);
332
+ const destRelative = joinRelativePath(destParent, folderName);
333
+ const destAbsolute = resolveUnderKnowledgeRoot(knowledgeRoot, destRelative);
334
+ if (!destAbsolute) {
335
+ throw new Error('path escapes docs/');
336
+ }
337
+ if (fs.existsSync(destAbsolute)) {
338
+ throw new Error(`already exists: ${destRelative}`);
339
+ }
340
+ const fromParent = parentRelativePath(normalizedFrom);
341
+ const siblingRelative = joinRelativePath(fromParent, `${folderName}.md`);
342
+ const siblingAbsolute = resolveRelativeMdPath(knowledgeRoot, siblingRelative);
343
+ let siblingDestRelative;
344
+ if (siblingAbsolute && fs.existsSync(siblingAbsolute)) {
345
+ siblingDestRelative = joinRelativePath(destParent, `${folderName}.md`);
346
+ const siblingDestAbsolute = resolveRelativeMdPath(knowledgeRoot, siblingDestRelative);
347
+ if (!siblingDestAbsolute) {
348
+ throw new Error('path escapes docs/');
349
+ }
350
+ if (fs.existsSync(siblingDestAbsolute)) {
351
+ throw new Error(`already exists: ${siblingDestRelative}`);
352
+ }
353
+ fs.renameSync(siblingAbsolute, siblingDestAbsolute);
354
+ }
355
+ fs.renameSync(sourceAbsolute, destAbsolute);
356
+ const movedMarkdownAbsolutes = [];
357
+ if (siblingDestRelative) {
358
+ const siblingDestAbsolute = resolveRelativeMdPath(knowledgeRoot, siblingDestRelative);
359
+ if (siblingDestAbsolute) {
360
+ movedMarkdownAbsolutes.push(siblingDestAbsolute);
361
+ }
362
+ }
363
+ movedMarkdownAbsolutes.push(...listMarkdownFilesRecursive(destAbsolute));
364
+ const cascadeUpdated = new Set();
365
+ for (const absolutePath of movedMarkdownAbsolutes) {
366
+ const relativePath = absoluteToDocsRelative(knowledgeRoot, absolutePath);
367
+ const previousSlug = readPageSlug(absolutePath);
368
+ const { newSlug } = rewritePageFrontmatter(absolutePath, relativePath);
369
+ if (newSlug === previousSlug) {
370
+ continue;
371
+ }
372
+ for (const updatedPath of cascadeSlugRename(knowledgeRoot, previousSlug, newSlug, {
373
+ excludeAbsolutePath: absolutePath,
374
+ })) {
375
+ cascadeUpdated.add(updatedPath);
376
+ }
377
+ }
378
+ index.refresh();
379
+ return {
380
+ relativePath: destRelative,
381
+ previousPath: normalizedFrom,
382
+ siblingPagePath: siblingDestRelative,
383
+ cascadeUpdated: toRelativePaths(knowledgeRoot, [...cascadeUpdated]),
384
+ };
385
+ }
247
386
  export function deletePageFile(index, pagePath) {
248
387
  const knowledgeRoot = index.getKnowledgeRoot();
249
388
  if (!knowledgeRoot) {