@aprovan/hardcopy 0.1.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 (53) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/publish.yml +41 -0
  3. package/.prettierignore +17 -0
  4. package/LICENSE +21 -0
  5. package/README.md +183 -0
  6. package/dist/cli.d.ts +1 -0
  7. package/dist/cli.js +2950 -0
  8. package/dist/index.d.ts +406 -0
  9. package/dist/index.js +2737 -0
  10. package/dist/mcp-server.d.ts +7 -0
  11. package/dist/mcp-server.js +2665 -0
  12. package/docs/research/crdt.md +777 -0
  13. package/docs/research/github-issues.md +684 -0
  14. package/docs/research/gql.md +876 -0
  15. package/docs/research/index.md +19 -0
  16. package/docs/specs/conflict-resolution.md +1254 -0
  17. package/docs/specs/hardcopy.md +742 -0
  18. package/docs/specs/patchwork-integration.md +227 -0
  19. package/docs/specs/plugin-architecture.md +747 -0
  20. package/mcp.json +8 -0
  21. package/package.json +64 -0
  22. package/scripts/install-graphqlite.ts +156 -0
  23. package/src/cli.ts +356 -0
  24. package/src/config.ts +104 -0
  25. package/src/conflict-store.ts +136 -0
  26. package/src/conflict.ts +147 -0
  27. package/src/crdt.ts +100 -0
  28. package/src/db.ts +600 -0
  29. package/src/env.ts +34 -0
  30. package/src/format.ts +72 -0
  31. package/src/formats/github-issue.ts +55 -0
  32. package/src/hardcopy/core.ts +78 -0
  33. package/src/hardcopy/diff.ts +188 -0
  34. package/src/hardcopy/index.ts +67 -0
  35. package/src/hardcopy/init.ts +24 -0
  36. package/src/hardcopy/push.ts +444 -0
  37. package/src/hardcopy/sync.ts +37 -0
  38. package/src/hardcopy/types.ts +49 -0
  39. package/src/hardcopy/views.ts +199 -0
  40. package/src/hardcopy.ts +1 -0
  41. package/src/index.ts +13 -0
  42. package/src/llm-merge.ts +109 -0
  43. package/src/mcp-server.ts +388 -0
  44. package/src/merge.ts +75 -0
  45. package/src/provider.ts +40 -0
  46. package/src/providers/a2a/index.ts +166 -0
  47. package/src/providers/git/index.ts +212 -0
  48. package/src/providers/github/index.ts +236 -0
  49. package/src/providers/github/issues.ts +66 -0
  50. package/src/providers.ts +7 -0
  51. package/src/types.ts +101 -0
  52. package/tsconfig.json +21 -0
  53. package/tsup.config.ts +10 -0
package/dist/index.js ADDED
@@ -0,0 +1,2737 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res) => function __init() {
4
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
5
+ };
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+
11
+ // node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js
12
+ import path from "path";
13
+ import { fileURLToPath } from "url";
14
+ var init_esm_shims = __esm({
15
+ "node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_tsx@4.21.0_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/esm_shims.js"() {
16
+ "use strict";
17
+ }
18
+ });
19
+
20
+ // src/format.ts
21
+ import matter from "gray-matter";
22
+ function registerFormat(handler) {
23
+ handlers.set(handler.type, handler);
24
+ }
25
+ function getFormat(type) {
26
+ return handlers.get(type);
27
+ }
28
+ function listFormats() {
29
+ return Array.from(handlers.keys());
30
+ }
31
+ function renderNode(node, template) {
32
+ if (template) {
33
+ return renderTemplate(template, node);
34
+ }
35
+ const handler = handlers.get(node.type);
36
+ if (!handler) {
37
+ throw new Error(`No format handler for type: ${node.type}`);
38
+ }
39
+ return handler.render(node);
40
+ }
41
+ function parseFile(content, type) {
42
+ const handler = handlers.get(type);
43
+ if (!handler) {
44
+ return parseGeneric(content);
45
+ }
46
+ return handler.parse(content);
47
+ }
48
+ function parseGeneric(content) {
49
+ const { data, content: body } = matter(content);
50
+ return { attrs: data, body: body.trim() };
51
+ }
52
+ function renderTemplate(template, node) {
53
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, path2) => {
54
+ const value = resolvePath(
55
+ node,
56
+ path2.trim()
57
+ );
58
+ return value?.toString() ?? "";
59
+ });
60
+ }
61
+ function resolvePath(obj, path2) {
62
+ const parts = path2.split(".");
63
+ let current = obj;
64
+ for (const part of parts) {
65
+ if (current === null || current === void 0) return void 0;
66
+ current = current[part];
67
+ }
68
+ return current;
69
+ }
70
+ var handlers;
71
+ var init_format = __esm({
72
+ "src/format.ts"() {
73
+ "use strict";
74
+ init_esm_shims();
75
+ handlers = /* @__PURE__ */ new Map();
76
+ }
77
+ });
78
+
79
+ // src/crdt.ts
80
+ import { LoroDoc } from "loro-crdt";
81
+ import { readFile as readFile2, writeFile, mkdir, access, rm } from "fs/promises";
82
+ import { dirname as dirname2, join as join2 } from "path";
83
+ function setDocContent(doc, content) {
84
+ const text = doc.getText("body");
85
+ const current = text.toString();
86
+ if (current !== content) {
87
+ text.delete(0, current.length);
88
+ text.insert(0, content);
89
+ }
90
+ }
91
+ function getDocContent(doc) {
92
+ return doc.getText("body").toString();
93
+ }
94
+ function setDocAttrs(doc, attrs) {
95
+ const map = doc.getMap("attrs");
96
+ for (const [key, value] of Object.entries(attrs)) {
97
+ map.set(key, value);
98
+ }
99
+ }
100
+ function getDocAttrs(doc) {
101
+ const map = doc.getMap("attrs");
102
+ const result = {};
103
+ for (const key of map.keys()) {
104
+ result[key] = map.get(key);
105
+ }
106
+ return result;
107
+ }
108
+ var CRDTStore;
109
+ var init_crdt = __esm({
110
+ "src/crdt.ts"() {
111
+ "use strict";
112
+ init_esm_shims();
113
+ CRDTStore = class {
114
+ basePath;
115
+ constructor(basePath) {
116
+ this.basePath = basePath;
117
+ }
118
+ getPath(nodeId) {
119
+ const encoded = encodeURIComponent(nodeId);
120
+ return join2(this.basePath, `${encoded}.loro`);
121
+ }
122
+ async exists(nodeId) {
123
+ try {
124
+ await access(this.getPath(nodeId));
125
+ return true;
126
+ } catch {
127
+ return false;
128
+ }
129
+ }
130
+ async load(nodeId) {
131
+ const path2 = this.getPath(nodeId);
132
+ try {
133
+ const data = await readFile2(path2);
134
+ const doc = new LoroDoc();
135
+ doc.import(new Uint8Array(data));
136
+ return doc;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+ async save(nodeId, doc) {
142
+ const path2 = this.getPath(nodeId);
143
+ await mkdir(dirname2(path2), { recursive: true });
144
+ const snapshot = doc.export({ mode: "snapshot" });
145
+ await writeFile(path2, Buffer.from(snapshot));
146
+ }
147
+ async create(nodeId) {
148
+ const doc = new LoroDoc();
149
+ await this.save(nodeId, doc);
150
+ return doc;
151
+ }
152
+ async loadOrCreate(nodeId) {
153
+ const existing = await this.load(nodeId);
154
+ if (existing) return existing;
155
+ return this.create(nodeId);
156
+ }
157
+ async delete(nodeId) {
158
+ try {
159
+ await rm(this.getPath(nodeId));
160
+ } catch {
161
+ }
162
+ }
163
+ async merge(nodeId, remote) {
164
+ const local = await this.loadOrCreate(nodeId);
165
+ local.import(remote.export({ mode: "update" }));
166
+ await this.save(nodeId, local);
167
+ return local;
168
+ }
169
+ };
170
+ }
171
+ });
172
+
173
+ // src/hardcopy/views.ts
174
+ import { join as join6 } from "path";
175
+ import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile6, rm as rm3, readdir as readdir2, stat } from "fs/promises";
176
+ async function getViews() {
177
+ const config = await this.loadConfig();
178
+ return config.views.map((v) => v.path);
179
+ }
180
+ async function refreshView(viewPath, options = {}) {
181
+ const config = await this.loadConfig();
182
+ const view = config.views.find((v) => v.path === viewPath);
183
+ if (!view) throw new Error(`View not found: ${viewPath}`);
184
+ const viewDir = join6(this.root, view.path);
185
+ await mkdir4(viewDir, { recursive: true });
186
+ const db = this.getDatabase();
187
+ const params = {};
188
+ const me = process.env["HARDCOPY_ME"] ?? process.env["GITHUB_USER"];
189
+ if (me) params["me"] = me;
190
+ const nodes = await db.queryViewNodes(
191
+ view.query,
192
+ Object.keys(params).length ? params : void 0
193
+ );
194
+ const indexState = {
195
+ loaded: nodes.length,
196
+ pageSize: 10,
197
+ lastFetch: (/* @__PURE__ */ new Date()).toISOString(),
198
+ ttl: 300
199
+ };
200
+ await writeFile3(
201
+ join6(viewDir, ".index"),
202
+ JSON.stringify(indexState, null, 2)
203
+ );
204
+ const expectedFiles = /* @__PURE__ */ new Set();
205
+ for (const node of nodes) {
206
+ const renderedPaths = await renderNodeToFile.call(this, node, view, viewDir);
207
+ for (const p of renderedPaths) {
208
+ expectedFiles.add(p);
209
+ }
210
+ }
211
+ const existingFiles = await listViewFiles(viewDir);
212
+ const orphanedFiles = existingFiles.filter((f) => !expectedFiles.has(f));
213
+ if (options.clean && orphanedFiles.length > 0) {
214
+ await cleanupOrphanedFiles.call(this, viewDir, orphanedFiles);
215
+ }
216
+ return {
217
+ rendered: expectedFiles.size,
218
+ orphaned: orphanedFiles,
219
+ cleaned: options.clean ?? false
220
+ };
221
+ }
222
+ async function renderNodeToFile(node, view, viewDir) {
223
+ const renderedPaths = [];
224
+ const crdt = this.getCRDTStore();
225
+ const db = this.getDatabase();
226
+ for (const renderConfig of view.render) {
227
+ const filePath = resolveRenderPath(renderConfig.path, node);
228
+ const fullPath = join6(viewDir, filePath);
229
+ await mkdir4(join6(fullPath, ".."), { recursive: true });
230
+ let content;
231
+ if (renderConfig.template) {
232
+ content = renderNode(node, renderConfig.template);
233
+ } else if (renderConfig.type) {
234
+ content = renderNode({ ...node, type: renderConfig.type });
235
+ } else {
236
+ content = renderNode(node);
237
+ }
238
+ const doc = await crdt.loadOrCreate(node.id);
239
+ const body = node.attrs["body"] ?? "";
240
+ setDocContent(doc, body);
241
+ setDocAttrs(doc, node.attrs);
242
+ await crdt.save(node.id, doc);
243
+ await writeFile3(fullPath, content);
244
+ const fileStat = await stat(fullPath);
245
+ await db.upsertNode({ ...node, syncedAt: fileStat.mtimeMs });
246
+ renderedPaths.push(filePath);
247
+ }
248
+ return renderedPaths;
249
+ }
250
+ function resolveRenderPath(template, node) {
251
+ return template.replace(/\{\{([^}]+)\}\}/g, (_, path2) => {
252
+ const parts = path2.trim().split(".");
253
+ let current = { ...node, ...node.attrs };
254
+ for (const part of parts) {
255
+ if (current === null || current === void 0) return "";
256
+ current = current[part];
257
+ }
258
+ return String(current ?? "");
259
+ });
260
+ }
261
+ async function listViewFiles(viewDir) {
262
+ const files = [];
263
+ async function walk(dir, base) {
264
+ let entries;
265
+ try {
266
+ entries = await readdir2(dir, { withFileTypes: true });
267
+ } catch {
268
+ return;
269
+ }
270
+ for (const entry of entries) {
271
+ const relPath = base ? `${base}/${entry.name}` : entry.name;
272
+ if (entry.name.startsWith(".")) continue;
273
+ if (entry.isDirectory()) {
274
+ await walk(join6(dir, entry.name), relPath);
275
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
276
+ files.push(relPath);
277
+ }
278
+ }
279
+ }
280
+ await walk(viewDir, "");
281
+ return files;
282
+ }
283
+ async function cleanupOrphanedFiles(viewDir, orphanedFiles) {
284
+ for (const relPath of orphanedFiles) {
285
+ const fullPath = join6(viewDir, relPath);
286
+ await syncFileBeforeDelete.call(this, fullPath);
287
+ try {
288
+ await rm3(fullPath);
289
+ console.log(`Deleted orphaned file: ${relPath}`);
290
+ } catch (err) {
291
+ console.error(`Failed to delete ${relPath}: ${err}`);
292
+ }
293
+ }
294
+ }
295
+ async function syncFileBeforeDelete(fullPath) {
296
+ try {
297
+ const content = await readFile6(fullPath, "utf-8");
298
+ const parsed = parseFile(content, "generic");
299
+ const nodeId = parsed.attrs._id ?? parsed.attrs.id;
300
+ if (!nodeId) return;
301
+ const crdt = this.getCRDTStore();
302
+ const doc = await crdt.load(nodeId);
303
+ if (!doc) return;
304
+ const crdtContent = getDocContent(doc);
305
+ if (parsed.body !== crdtContent) {
306
+ console.warn(
307
+ `Warning: File for ${nodeId} has local changes that may be lost. Run 'hardcopy push' first to preserve changes.`
308
+ );
309
+ }
310
+ await crdt.delete(nodeId);
311
+ } catch {
312
+ }
313
+ }
314
+ var init_views = __esm({
315
+ "src/hardcopy/views.ts"() {
316
+ "use strict";
317
+ init_esm_shims();
318
+ init_crdt();
319
+ init_format();
320
+ }
321
+ });
322
+
323
+ // src/hardcopy/diff.ts
324
+ var diff_exports = {};
325
+ __export(diff_exports, {
326
+ detectChanges: () => detectChanges,
327
+ diff: () => diff,
328
+ getChangedFiles: () => getChangedFiles
329
+ });
330
+ import { join as join7 } from "path";
331
+ import { readFile as readFile7, stat as stat2 } from "fs/promises";
332
+ import { minimatch } from "minimatch";
333
+ async function getChangedFiles(pattern) {
334
+ const config = await this.loadConfig();
335
+ const db = this.getDatabase();
336
+ const changedFiles = [];
337
+ for (const view of config.views) {
338
+ const viewDir = join7(this.root, view.path);
339
+ const files = await listViewFiles(viewDir);
340
+ for (const relPath of files) {
341
+ const fullPath = join7(viewDir, relPath);
342
+ const viewRelPath = join7(view.path, relPath);
343
+ if (pattern && !minimatch(viewRelPath, pattern) && !viewRelPath.startsWith(pattern)) {
344
+ continue;
345
+ }
346
+ const fileStat = await stat2(fullPath).catch(() => null);
347
+ if (!fileStat) continue;
348
+ const content = await readFile7(fullPath, "utf-8");
349
+ const parsed = parseFile(content, "generic");
350
+ const nodeId = parsed.attrs._id ?? parsed.attrs.id;
351
+ if (!nodeId) continue;
352
+ const dbNode = await db.getNode(nodeId);
353
+ const fileMtime = fileStat.mtimeMs;
354
+ const syncedAt = dbNode?.syncedAt ?? 0;
355
+ if (fileMtime > syncedAt) {
356
+ changedFiles.push({
357
+ path: viewRelPath,
358
+ fullPath,
359
+ nodeId,
360
+ nodeType: dbNode?.type ?? parsed.attrs._type ?? "unknown",
361
+ status: dbNode ? "modified" : "new",
362
+ mtime: fileMtime,
363
+ syncedAt
364
+ });
365
+ }
366
+ }
367
+ }
368
+ return changedFiles;
369
+ }
370
+ async function diff(pattern, options = {}) {
371
+ const config = await this.loadConfig();
372
+ const db = this.getDatabase();
373
+ const results = [];
374
+ const useSmart = options.smart !== false;
375
+ if (useSmart && pattern) {
376
+ const candidates = await getChangedFiles.call(this, pattern);
377
+ for (const candidate of candidates) {
378
+ const result = await diffFile.call(this, candidate.fullPath, db);
379
+ if (result && result.changes.length > 0) {
380
+ results.push(result);
381
+ }
382
+ }
383
+ return results;
384
+ }
385
+ for (const view of config.views) {
386
+ const viewDir = join7(this.root, view.path);
387
+ const files = await listViewFiles(viewDir);
388
+ for (const relPath of files) {
389
+ const fullPath = join7(viewDir, relPath);
390
+ const viewRelPath = join7(view.path, relPath);
391
+ if (pattern) {
392
+ const targetPath = join7(this.root, pattern);
393
+ const isExactMatch = fullPath === targetPath;
394
+ const isGlobMatch = minimatch(viewRelPath, pattern);
395
+ const isPrefixMatch = viewRelPath.startsWith(pattern);
396
+ if (!isExactMatch && !isGlobMatch && !isPrefixMatch) continue;
397
+ }
398
+ const result = await diffFile.call(this, fullPath, db);
399
+ if (result && result.changes.length > 0) {
400
+ results.push(result);
401
+ }
402
+ }
403
+ }
404
+ return results;
405
+ }
406
+ async function diffFile(fullPath, db) {
407
+ try {
408
+ const content = await readFile7(fullPath, "utf-8");
409
+ const parsed = parseFile(content, "generic");
410
+ const nodeId = parsed.attrs._id ?? parsed.attrs.id;
411
+ const nodeType = parsed.attrs._type;
412
+ if (!nodeId) return null;
413
+ const dbNode = await db.getNode(nodeId);
414
+ if (!dbNode) {
415
+ return {
416
+ nodeId,
417
+ nodeType: nodeType ?? "unknown",
418
+ filePath: fullPath,
419
+ changes: [{ field: "_new", oldValue: null, newValue: parsed.attrs }]
420
+ };
421
+ }
422
+ const format = getFormat(dbNode.type);
423
+ if (!format) return null;
424
+ const changes = detectChanges(parsed, dbNode, format.editableFields);
425
+ return {
426
+ nodeId,
427
+ nodeType: dbNode.type,
428
+ filePath: fullPath,
429
+ changes
430
+ };
431
+ } catch {
432
+ return null;
433
+ }
434
+ }
435
+ function detectChanges(parsed, dbNode, editableFields) {
436
+ const changes = [];
437
+ const dbAttrs = dbNode.attrs;
438
+ for (const field of editableFields) {
439
+ if (field === "body") {
440
+ const oldBody = (dbAttrs["body"] ?? "").trim();
441
+ const newBody = parsed.body.trim();
442
+ if (newBody !== oldBody) {
443
+ changes.push({ field: "body", oldValue: oldBody, newValue: newBody });
444
+ }
445
+ } else {
446
+ const oldValue = dbAttrs[field];
447
+ const newValue = parsed.attrs[field];
448
+ if (!valuesEqual2(oldValue, newValue)) {
449
+ changes.push({ field, oldValue, newValue });
450
+ }
451
+ }
452
+ }
453
+ return changes;
454
+ }
455
+ function valuesEqual2(a, b) {
456
+ if (a === b) return true;
457
+ if (a == null && b == null) return true;
458
+ if (Array.isArray(a) && Array.isArray(b)) {
459
+ return a.length === b.length && a.every((v, i) => valuesEqual2(v, b[i]));
460
+ }
461
+ return JSON.stringify(a) === JSON.stringify(b);
462
+ }
463
+ var init_diff = __esm({
464
+ "src/hardcopy/diff.ts"() {
465
+ "use strict";
466
+ init_esm_shims();
467
+ init_format();
468
+ init_views();
469
+ }
470
+ });
471
+
472
+ // src/index.ts
473
+ init_esm_shims();
474
+
475
+ // src/types.ts
476
+ init_esm_shims();
477
+ var ConflictStatus = /* @__PURE__ */ ((ConflictStatus2) => {
478
+ ConflictStatus2["CLEAN"] = "clean";
479
+ ConflictStatus2["REMOTE_ONLY"] = "remote";
480
+ ConflictStatus2["DIVERGED"] = "diverged";
481
+ return ConflictStatus2;
482
+ })(ConflictStatus || {});
483
+
484
+ // src/provider.ts
485
+ init_esm_shims();
486
+ var providers = /* @__PURE__ */ new Map();
487
+ function registerProvider(name, factory) {
488
+ providers.set(name, factory);
489
+ }
490
+ function getProvider(name) {
491
+ return providers.get(name);
492
+ }
493
+ function listProviders() {
494
+ return Array.from(providers.keys());
495
+ }
496
+
497
+ // src/index.ts
498
+ init_format();
499
+
500
+ // src/config.ts
501
+ init_esm_shims();
502
+ import { readFile } from "fs/promises";
503
+ import yaml from "yaml";
504
+ async function loadConfig(path2) {
505
+ const content = await readFile(path2, "utf-8");
506
+ return parseConfig(content);
507
+ }
508
+ function parseConfig(content) {
509
+ const parsed = yaml.parse(content);
510
+ return validateConfig(parsed);
511
+ }
512
+ function validateConfig(data) {
513
+ if (!data || typeof data !== "object") {
514
+ throw new Error("Config must be an object");
515
+ }
516
+ const config = data;
517
+ return {
518
+ sources: validateSources(config["sources"]),
519
+ views: validateViews(config["views"])
520
+ };
521
+ }
522
+ function validateSources(data) {
523
+ if (!Array.isArray(data)) {
524
+ return [];
525
+ }
526
+ return data.map((s, i) => {
527
+ if (!s || typeof s !== "object") {
528
+ throw new Error(`Source ${i} must be an object`);
529
+ }
530
+ const source = s;
531
+ if (typeof source["name"] !== "string") {
532
+ throw new Error(`Source ${i} must have a name`);
533
+ }
534
+ if (typeof source["provider"] !== "string") {
535
+ throw new Error(`Source ${i} must have a provider`);
536
+ }
537
+ return source;
538
+ });
539
+ }
540
+ function validateViews(data) {
541
+ if (!Array.isArray(data)) {
542
+ return [];
543
+ }
544
+ return data.map((v, i) => {
545
+ if (!v || typeof v !== "object") {
546
+ throw new Error(`View ${i} must be an object`);
547
+ }
548
+ const view = v;
549
+ if (typeof view["path"] !== "string") {
550
+ throw new Error(`View ${i} must have a path`);
551
+ }
552
+ if (typeof view["query"] !== "string") {
553
+ throw new Error(`View ${i} must have a query`);
554
+ }
555
+ if (!Array.isArray(view["render"])) {
556
+ throw new Error(`View ${i} must have render configs`);
557
+ }
558
+ return view;
559
+ });
560
+ }
561
+
562
+ // src/db.ts
563
+ init_esm_shims();
564
+ import BetterSqlite3 from "better-sqlite3";
565
+ import { existsSync } from "fs";
566
+ import { dirname, join } from "path";
567
+ import { fileURLToPath as fileURLToPath2 } from "url";
568
+ var __dirname2 = dirname(fileURLToPath2(import.meta.url));
569
+ var SCHEMA = `
570
+ CREATE TABLE IF NOT EXISTS hc_nodes (
571
+ id TEXT PRIMARY KEY,
572
+ type TEXT NOT NULL,
573
+ attrs TEXT NOT NULL,
574
+ synced_at INTEGER,
575
+ version_token TEXT,
576
+ cursor TEXT
577
+ );
578
+
579
+ CREATE INDEX IF NOT EXISTS hc_idx_nodes_type ON hc_nodes(type);
580
+ CREATE INDEX IF NOT EXISTS hc_idx_nodes_synced ON hc_nodes(synced_at);
581
+
582
+ CREATE TABLE IF NOT EXISTS hc_edges (
583
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
584
+ type TEXT NOT NULL,
585
+ from_id TEXT NOT NULL,
586
+ to_id TEXT NOT NULL,
587
+ attrs TEXT,
588
+ UNIQUE(type, from_id, to_id)
589
+ );
590
+
591
+ CREATE INDEX IF NOT EXISTS hc_idx_edges_from ON hc_edges(from_id);
592
+ CREATE INDEX IF NOT EXISTS hc_idx_edges_to ON hc_edges(to_id);
593
+ CREATE INDEX IF NOT EXISTS hc_idx_edges_type ON hc_edges(type);
594
+ `;
595
+ var GRAPHQLITE_ENV_PATH = "GRAPHQLITE_EXTENSION_PATH";
596
+ var GRAPHQLITE_TEST_QUERY = "SELECT graphqlite_test() AS result";
597
+ var HardcopyDatabase = class _HardcopyDatabase {
598
+ db;
599
+ graphqliteLoaded = false;
600
+ constructor(db) {
601
+ this.db = db;
602
+ }
603
+ static async open(path2) {
604
+ const db = new BetterSqlite3(path2);
605
+ const hcdb = new _HardcopyDatabase(db);
606
+ await hcdb.initialize();
607
+ return hcdb;
608
+ }
609
+ async initialize() {
610
+ await this.migrateLegacySchema();
611
+ const statements = SCHEMA.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
612
+ for (const sql of statements) {
613
+ this.db.exec(sql);
614
+ }
615
+ }
616
+ async migrateLegacySchema() {
617
+ const legacyNodes = this.getTableColumns("nodes");
618
+ const legacyEdges = this.getTableColumns("edges");
619
+ if (legacyNodes) {
620
+ const isLegacy = legacyNodes.includes("type") || legacyNodes.includes("attrs");
621
+ if (isLegacy) {
622
+ this.renameTableIfNeeded("nodes", "hc_nodes");
623
+ this.dropLegacyIndexes(["idx_nodes_type", "idx_nodes_synced"]);
624
+ }
625
+ }
626
+ if (legacyEdges) {
627
+ const isLegacy = legacyEdges.includes("from_id") || legacyEdges.includes("to_id");
628
+ if (isLegacy) {
629
+ this.renameTableIfNeeded("edges", "hc_edges");
630
+ this.dropLegacyIndexes([
631
+ "idx_edges_from",
632
+ "idx_edges_to",
633
+ "idx_edges_type"
634
+ ]);
635
+ }
636
+ }
637
+ }
638
+ getTableColumns(table) {
639
+ const stmt = this.db.prepare(
640
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
641
+ );
642
+ const result = stmt.all(table);
643
+ if (result.length === 0) return null;
644
+ const columns = this.db.pragma(`table_info(${table})`);
645
+ return columns.map((row) => row.name);
646
+ }
647
+ renameTableIfNeeded(from, to) {
648
+ const stmt = this.db.prepare(
649
+ "SELECT name FROM sqlite_master WHERE type='table' AND name = ?"
650
+ );
651
+ const existing = stmt.all(to);
652
+ if (existing.length > 0) return;
653
+ this.db.exec(`ALTER TABLE ${from} RENAME TO ${to}`);
654
+ }
655
+ dropLegacyIndexes(names) {
656
+ for (const name of names) {
657
+ this.db.exec(`DROP INDEX IF EXISTS ${name}`);
658
+ }
659
+ }
660
+ resolveGraphqliteLoadPath() {
661
+ const envPath = process.env[GRAPHQLITE_ENV_PATH];
662
+ if (envPath) {
663
+ return envPath;
664
+ }
665
+ const extensionCandidates = this.getExtensionCandidates();
666
+ for (const candidate of extensionCandidates) {
667
+ if (existsSync(candidate)) {
668
+ return candidate;
669
+ }
670
+ }
671
+ return null;
672
+ }
673
+ getExtensionCandidates() {
674
+ const platform = process.platform;
675
+ const arch = process.arch;
676
+ const filenames = [];
677
+ if (platform === "darwin" && arch === "arm64") {
678
+ filenames.push("graphqlite-macos-arm64.dylib");
679
+ } else if (platform === "darwin" && arch === "x64") {
680
+ filenames.push("graphqlite-macos-x86_64.dylib");
681
+ } else if (platform === "linux" && arch === "arm64") {
682
+ filenames.push("graphqlite-linux-aarch64.so");
683
+ } else if (platform === "linux" && arch === "x64") {
684
+ filenames.push("graphqlite-linux-x86_64.so");
685
+ } else if (platform === "win32" && arch === "x64") {
686
+ filenames.push("graphqlite-windows-x86_64.dll");
687
+ }
688
+ const searchDirs = [
689
+ join(__dirname2, "..", ".hardcopy", "extensions"),
690
+ join(process.cwd(), ".hardcopy", "extensions")
691
+ ];
692
+ const candidates = [];
693
+ for (const dir of searchDirs) {
694
+ for (const filename of filenames) {
695
+ candidates.push(join(dir, filename));
696
+ }
697
+ }
698
+ return candidates;
699
+ }
700
+ ensureGraphqliteLoaded() {
701
+ if (this.graphqliteLoaded) return;
702
+ try {
703
+ const stmt = this.db.prepare(GRAPHQLITE_TEST_QUERY);
704
+ const result = stmt.all();
705
+ const value2 = String(result[0]?.result ?? "");
706
+ if (value2.toLowerCase().includes("successfully")) {
707
+ this.graphqliteLoaded = true;
708
+ return;
709
+ }
710
+ } catch {
711
+ }
712
+ const loadPath = this.resolveGraphqliteLoadPath();
713
+ if (!loadPath) {
714
+ throw new Error(
715
+ `GraphQLite extension not found. Run \`pnpm setup:graphqlite\` or set ${GRAPHQLITE_ENV_PATH}.`
716
+ );
717
+ }
718
+ this.db.loadExtension(loadPath, "sqlite3_graphqlite_init");
719
+ const verifyStmt = this.db.prepare(GRAPHQLITE_TEST_QUERY);
720
+ const verify = verifyStmt.all();
721
+ const value = String(verify[0]?.result ?? "");
722
+ if (!value.toLowerCase().includes("successfully")) {
723
+ throw new Error("GraphQLite extension loaded but verification failed.");
724
+ }
725
+ this.graphqliteLoaded = true;
726
+ }
727
+ normalizeCypher(query) {
728
+ let normalized = query.replace(/->>'(\w+)'/g, ".$1");
729
+ normalized = normalized.replace(/\.attrs\.(\w+)/g, ".$1");
730
+ normalized = normalized.replace(
731
+ /:([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z0-9_]+)+)/g,
732
+ (_match, label) => `:\`${label}\``
733
+ );
734
+ return normalized;
735
+ }
736
+ escapeCypherType(value) {
737
+ const escaped = value.replace(/`/g, "``");
738
+ return `\`${escaped}\``;
739
+ }
740
+ extractNodeIds(rows) {
741
+ const ids = /* @__PURE__ */ new Set();
742
+ for (const row of rows) {
743
+ for (const [key, value] of Object.entries(row)) {
744
+ if (key === "node_id" && typeof value === "string") {
745
+ ids.add(value);
746
+ continue;
747
+ }
748
+ if (key.endsWith(".node_id") && typeof value === "string") {
749
+ ids.add(value);
750
+ continue;
751
+ }
752
+ if (key === "id" && typeof value === "string") {
753
+ ids.add(value);
754
+ continue;
755
+ }
756
+ if (key.endsWith(".id") && typeof value === "string") {
757
+ ids.add(value);
758
+ continue;
759
+ }
760
+ if (value && typeof value === "object") {
761
+ const obj = value;
762
+ if (typeof obj["node_id"] === "string") {
763
+ ids.add(obj["node_id"]);
764
+ continue;
765
+ }
766
+ const props = obj["properties"];
767
+ if (props && typeof props === "object") {
768
+ const propsObj = props;
769
+ if (typeof propsObj["node_id"] === "string") {
770
+ ids.add(propsObj["node_id"]);
771
+ continue;
772
+ }
773
+ }
774
+ if (typeof obj["id"] === "string") ids.add(obj["id"]);
775
+ }
776
+ }
777
+ }
778
+ return Array.from(ids);
779
+ }
780
+ parseCypherRows(rows) {
781
+ if (!rows.length) return [];
782
+ const row = rows[0];
783
+ const payload = row.result;
784
+ if (payload === null || payload === void 0) return [];
785
+ if (typeof payload !== "string") return [];
786
+ try {
787
+ const parsed = JSON.parse(payload);
788
+ return Array.isArray(parsed) ? parsed : [];
789
+ } catch {
790
+ return [];
791
+ }
792
+ }
793
+ async cypher(query, params) {
794
+ this.ensureGraphqliteLoaded();
795
+ const normalized = this.normalizeCypher(query);
796
+ const stmt = params ? this.db.prepare("SELECT cypher(?, ?) AS result") : this.db.prepare("SELECT cypher(?) AS result");
797
+ const args = params ? [normalized, JSON.stringify(params)] : [normalized];
798
+ const result = stmt.all(...args);
799
+ return this.parseCypherRows(result);
800
+ }
801
+ async queryViewNodes(query, params) {
802
+ const rows = await this.cypher(query, params);
803
+ const ids = this.extractNodeIds(rows);
804
+ if (ids.length === 0) return [];
805
+ return this.getNodesByIds(ids);
806
+ }
807
+ async getNodesByIds(ids) {
808
+ if (ids.length === 0) return [];
809
+ const placeholders = ids.map(() => "?").join(", ");
810
+ const stmt = this.db.prepare(
811
+ `SELECT * FROM hc_nodes WHERE id IN (${placeholders})`
812
+ );
813
+ const result = stmt.all(...ids);
814
+ const byId = new Map(
815
+ result.map((row) => [
816
+ row["id"],
817
+ {
818
+ id: row["id"],
819
+ type: row["type"],
820
+ attrs: JSON.parse(row["attrs"]),
821
+ syncedAt: row["synced_at"],
822
+ versionToken: row["version_token"],
823
+ cursor: row["cursor"]
824
+ }
825
+ ])
826
+ );
827
+ return ids.map((id) => byId.get(id)).filter(Boolean);
828
+ }
829
+ async upsertNode(node) {
830
+ const stmt = this.db.prepare(
831
+ `INSERT INTO hc_nodes (id, type, attrs, synced_at, version_token, cursor)
832
+ VALUES (?, ?, ?, ?, ?, ?)
833
+ ON CONFLICT(id) DO UPDATE SET
834
+ type = excluded.type,
835
+ attrs = excluded.attrs,
836
+ synced_at = excluded.synced_at,
837
+ version_token = excluded.version_token,
838
+ cursor = excluded.cursor`
839
+ );
840
+ stmt.run(
841
+ node.id,
842
+ node.type,
843
+ JSON.stringify(node.attrs),
844
+ node.syncedAt ?? null,
845
+ node.versionToken ?? null,
846
+ node.cursor ?? null
847
+ );
848
+ await this.upsertGraphNode(node);
849
+ }
850
+ async upsertNodes(nodes) {
851
+ if (nodes.length === 0) return;
852
+ const stmt = this.db.prepare(
853
+ `INSERT INTO hc_nodes (id, type, attrs, synced_at, version_token, cursor)
854
+ VALUES (?, ?, ?, ?, ?, ?)
855
+ ON CONFLICT(id) DO UPDATE SET
856
+ type = excluded.type,
857
+ attrs = excluded.attrs,
858
+ synced_at = excluded.synced_at,
859
+ version_token = excluded.version_token,
860
+ cursor = excluded.cursor`
861
+ );
862
+ const insertMany = this.db.transaction((nodes2) => {
863
+ for (const node of nodes2) {
864
+ stmt.run(
865
+ node.id,
866
+ node.type,
867
+ JSON.stringify(node.attrs),
868
+ node.syncedAt ?? null,
869
+ node.versionToken ?? null,
870
+ node.cursor ?? null
871
+ );
872
+ }
873
+ });
874
+ insertMany(nodes);
875
+ for (const node of nodes) {
876
+ await this.upsertGraphNode(node);
877
+ }
878
+ }
879
+ async getNode(id) {
880
+ const stmt = this.db.prepare("SELECT * FROM hc_nodes WHERE id = ?");
881
+ const result = stmt.all(id);
882
+ if (result.length === 0) return null;
883
+ const row = result[0];
884
+ return {
885
+ id: row["id"],
886
+ type: row["type"],
887
+ attrs: JSON.parse(row["attrs"]),
888
+ syncedAt: row["synced_at"],
889
+ versionToken: row["version_token"],
890
+ cursor: row["cursor"]
891
+ };
892
+ }
893
+ async queryNodes(type) {
894
+ const sql = type ? "SELECT * FROM hc_nodes WHERE type = ?" : "SELECT * FROM hc_nodes";
895
+ const stmt = this.db.prepare(sql);
896
+ const result = type ? stmt.all(type) : stmt.all();
897
+ return result.map((row) => ({
898
+ id: row["id"],
899
+ type: row["type"],
900
+ attrs: JSON.parse(row["attrs"]),
901
+ syncedAt: row["synced_at"],
902
+ versionToken: row["version_token"],
903
+ cursor: row["cursor"]
904
+ }));
905
+ }
906
+ async deleteNode(id) {
907
+ const deleteEdgesStmt = this.db.prepare(
908
+ "DELETE FROM hc_edges WHERE from_id = ? OR to_id = ?"
909
+ );
910
+ deleteEdgesStmt.run(id, id);
911
+ const deleteNodeStmt = this.db.prepare("DELETE FROM hc_nodes WHERE id = ?");
912
+ deleteNodeStmt.run(id);
913
+ await this.deleteGraphNode(id);
914
+ }
915
+ async upsertEdge(edge) {
916
+ const stmt = this.db.prepare(
917
+ `INSERT INTO hc_edges (type, from_id, to_id, attrs)
918
+ VALUES (?, ?, ?, ?)
919
+ ON CONFLICT(type, from_id, to_id) DO UPDATE SET
920
+ attrs = excluded.attrs`
921
+ );
922
+ stmt.run(
923
+ edge.type,
924
+ edge.fromId,
925
+ edge.toId,
926
+ edge.attrs ? JSON.stringify(edge.attrs) : null
927
+ );
928
+ await this.upsertGraphEdge(edge);
929
+ }
930
+ async upsertEdges(edges) {
931
+ if (edges.length === 0) return;
932
+ const stmt = this.db.prepare(
933
+ `INSERT INTO hc_edges (type, from_id, to_id, attrs)
934
+ VALUES (?, ?, ?, ?)
935
+ ON CONFLICT(type, from_id, to_id) DO UPDATE SET
936
+ attrs = excluded.attrs`
937
+ );
938
+ const insertMany = this.db.transaction((edges2) => {
939
+ for (const edge of edges2) {
940
+ stmt.run(
941
+ edge.type,
942
+ edge.fromId,
943
+ edge.toId,
944
+ edge.attrs ? JSON.stringify(edge.attrs) : null
945
+ );
946
+ }
947
+ });
948
+ insertMany(edges);
949
+ for (const edge of edges) {
950
+ await this.upsertGraphEdge(edge);
951
+ }
952
+ }
953
+ async getEdges(fromId, toId, type) {
954
+ const conditions = [];
955
+ const args = [];
956
+ if (fromId) {
957
+ conditions.push("from_id = ?");
958
+ args.push(fromId);
959
+ }
960
+ if (toId) {
961
+ conditions.push("to_id = ?");
962
+ args.push(toId);
963
+ }
964
+ if (type) {
965
+ conditions.push("type = ?");
966
+ args.push(type);
967
+ }
968
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
969
+ const stmt = this.db.prepare(`SELECT * FROM hc_edges ${where}`);
970
+ const result = stmt.all(...args);
971
+ return result.map((row) => ({
972
+ id: row["id"],
973
+ type: row["type"],
974
+ fromId: row["from_id"],
975
+ toId: row["to_id"],
976
+ attrs: row["attrs"] ? JSON.parse(row["attrs"]) : void 0
977
+ }));
978
+ }
979
+ async deleteEdge(fromId, toId, type) {
980
+ const stmt = this.db.prepare(
981
+ "DELETE FROM hc_edges WHERE from_id = ? AND to_id = ? AND type = ?"
982
+ );
983
+ stmt.run(fromId, toId, type);
984
+ await this.deleteGraphEdge(fromId, toId, type);
985
+ }
986
+ escapeCypherString(value) {
987
+ return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
988
+ }
989
+ async upsertGraphNode(node) {
990
+ const label = this.escapeCypherType(node.type);
991
+ const escapedNodeId = this.escapeCypherString(node.id);
992
+ const flatAttrs = {
993
+ node_id: node.id,
994
+ node_type: node.type
995
+ };
996
+ if (node.attrs && typeof node.attrs === "object") {
997
+ for (const [key, value] of Object.entries(node.attrs)) {
998
+ if (value === null || value === void 0) {
999
+ flatAttrs[key] = null;
1000
+ } else if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
1001
+ flatAttrs[key] = value;
1002
+ } else if (Array.isArray(value)) {
1003
+ flatAttrs[key] = JSON.stringify(value);
1004
+ } else {
1005
+ flatAttrs[key] = JSON.stringify(value);
1006
+ }
1007
+ }
1008
+ }
1009
+ const setClause = Object.keys(flatAttrs).map((k) => `n.${k} = $${k}`).join(", ");
1010
+ await this.cypher(
1011
+ `MERGE (n:${label} {node_id: '${escapedNodeId}'}) ON CREATE SET ${setClause} ON MATCH SET ${setClause}`,
1012
+ flatAttrs
1013
+ );
1014
+ }
1015
+ async upsertGraphEdge(edge) {
1016
+ const relType = this.escapeCypherType(edge.type);
1017
+ const escapedFromId = this.escapeCypherString(edge.fromId);
1018
+ const escapedToId = this.escapeCypherString(edge.toId);
1019
+ await this.cypher(
1020
+ `MATCH (a {node_id: '${escapedFromId}'}), (b {node_id: '${escapedToId}'}) MERGE (a)-[r:${relType}]->(b)`
1021
+ );
1022
+ }
1023
+ async deleteGraphNode(id) {
1024
+ const escapedId = this.escapeCypherString(id);
1025
+ await this.cypher(`MATCH (n {node_id: '${escapedId}'}) DETACH DELETE n`);
1026
+ }
1027
+ async deleteGraphEdge(fromId, toId, type) {
1028
+ const relType = this.escapeCypherType(type);
1029
+ const escapedFromId = this.escapeCypherString(fromId);
1030
+ const escapedToId = this.escapeCypherString(toId);
1031
+ await this.cypher(
1032
+ `MATCH (a {node_id: '${escapedFromId}'})-[r:${relType}]->(b {node_id: '${escapedToId}'}) DELETE r`
1033
+ );
1034
+ }
1035
+ async close() {
1036
+ this.db.close();
1037
+ }
1038
+ };
1039
+
1040
+ // src/index.ts
1041
+ init_crdt();
1042
+
1043
+ // src/hardcopy.ts
1044
+ init_esm_shims();
1045
+
1046
+ // src/hardcopy/index.ts
1047
+ init_esm_shims();
1048
+ init_format();
1049
+
1050
+ // src/formats/github-issue.ts
1051
+ init_esm_shims();
1052
+ import matter2 from "gray-matter";
1053
+ var githubIssueFormat = {
1054
+ type: "github.Issue",
1055
+ editableFields: ["title", "body", "labels", "assignee", "milestone", "state"],
1056
+ render(node) {
1057
+ const attrs = node.attrs;
1058
+ const frontmatter = {
1059
+ _type: "github.Issue",
1060
+ _id: node.id
1061
+ };
1062
+ const addIfDefined = (key, value) => {
1063
+ if (value !== void 0 && value !== null) {
1064
+ frontmatter[key] = value;
1065
+ }
1066
+ };
1067
+ addIfDefined("number", attrs["number"]);
1068
+ addIfDefined("title", attrs["title"]);
1069
+ addIfDefined("state", attrs["state"]);
1070
+ addIfDefined("url", attrs["url"]);
1071
+ addIfDefined("labels", attrs["labels"]);
1072
+ addIfDefined("assignee", attrs["assignee"]);
1073
+ addIfDefined("milestone", attrs["milestone"]);
1074
+ addIfDefined("created_at", attrs["created_at"]);
1075
+ addIfDefined("updated_at", attrs["updated_at"]);
1076
+ if (attrs["syncedAt"]) {
1077
+ frontmatter["_synced"] = new Date(attrs["syncedAt"]).toISOString();
1078
+ }
1079
+ const body = attrs["body"] ?? "";
1080
+ return matter2.stringify(body, frontmatter);
1081
+ },
1082
+ parse(content) {
1083
+ const { data, content: body } = matter2(content);
1084
+ const attrs = {};
1085
+ if (data["title"]) attrs["title"] = data["title"];
1086
+ if (data["state"]) attrs["state"] = data["state"];
1087
+ if (data["labels"]) attrs["labels"] = data["labels"];
1088
+ if (data["assignee"]) attrs["assignee"] = data["assignee"];
1089
+ if (data["milestone"]) attrs["milestone"] = data["milestone"];
1090
+ return {
1091
+ attrs,
1092
+ body: body.trim()
1093
+ };
1094
+ }
1095
+ };
1096
+
1097
+ // src/providers.ts
1098
+ init_esm_shims();
1099
+
1100
+ // src/providers/github/index.ts
1101
+ init_esm_shims();
1102
+ function createGitHubProvider(config) {
1103
+ const token = config.token ?? process.env["GITHUB_TOKEN"];
1104
+ async function fetchWithAuth(url, options = {}) {
1105
+ const headers = new Headers(options.headers);
1106
+ if (token) {
1107
+ headers.set("Authorization", `Bearer ${token}`);
1108
+ }
1109
+ headers.set("Accept", "application/vnd.github.v3+json");
1110
+ return fetch(url, { ...options, headers });
1111
+ }
1112
+ function issueToNode(owner, name, issue) {
1113
+ const nodeId = `github:${owner}/${name}#${issue.number}`;
1114
+ return {
1115
+ id: nodeId,
1116
+ type: "github.Issue",
1117
+ attrs: {
1118
+ number: issue.number,
1119
+ title: issue.title,
1120
+ body: issue.body,
1121
+ state: issue.state,
1122
+ labels: issue.labels.map((l) => l.name),
1123
+ assignee: issue.assignee?.login,
1124
+ milestone: issue.milestone?.title,
1125
+ created_at: issue.created_at,
1126
+ updated_at: issue.updated_at,
1127
+ url: issue.html_url,
1128
+ repository: `${owner}/${name}`
1129
+ }
1130
+ };
1131
+ }
1132
+ return {
1133
+ name: "github",
1134
+ nodeTypes: [
1135
+ "github.Issue",
1136
+ "github.Label",
1137
+ "github.User",
1138
+ "github.Milestone",
1139
+ "github.Repository"
1140
+ ],
1141
+ edgeTypes: [
1142
+ "github.ASSIGNED_TO",
1143
+ "github.HAS_LABEL",
1144
+ "github.REFERENCES",
1145
+ "github.BELONGS_TO"
1146
+ ],
1147
+ async fetch(request) {
1148
+ const nodes = [];
1149
+ const edges = [];
1150
+ let cursor = request.cursor;
1151
+ let hasMore = false;
1152
+ const repos = config.repos ?? [];
1153
+ if (config.orgs) {
1154
+ for (const org of config.orgs) {
1155
+ const response = await fetchWithAuth(
1156
+ `https://api.github.com/orgs/${org}/repos`
1157
+ );
1158
+ if (response.ok) {
1159
+ const data = await response.json();
1160
+ repos.push(...data.map((r) => r.full_name));
1161
+ }
1162
+ }
1163
+ }
1164
+ for (const repo of repos) {
1165
+ const [owner, name] = repo.split("/");
1166
+ if (!owner || !name) continue;
1167
+ const issuesUrl = `https://api.github.com/repos/${owner}/${name}/issues?state=all&per_page=100${cursor ? `&page=${cursor}` : ""}`;
1168
+ const response = await fetchWithAuth(issuesUrl);
1169
+ if (!response.ok) continue;
1170
+ const etag = response.headers.get("ETag");
1171
+ const data = await response.json();
1172
+ for (const issue of data) {
1173
+ if (issue.pull_request) continue;
1174
+ const node = issueToNode(owner, name, issue);
1175
+ nodes.push(node);
1176
+ if (issue.assignee) {
1177
+ edges.push({
1178
+ type: "github.ASSIGNED_TO",
1179
+ fromId: node.id,
1180
+ toId: `github:user:${issue.assignee.login}`
1181
+ });
1182
+ }
1183
+ for (const label of issue.labels) {
1184
+ edges.push({
1185
+ type: "github.HAS_LABEL",
1186
+ fromId: node.id,
1187
+ toId: `github:label:${owner}/${name}:${label.name}`
1188
+ });
1189
+ }
1190
+ }
1191
+ const linkHeader = response.headers.get("Link");
1192
+ if (linkHeader?.includes('rel="next"')) {
1193
+ hasMore = true;
1194
+ const match = linkHeader.match(/page=(\d+)>; rel="next"/);
1195
+ if (match) cursor = match[1];
1196
+ }
1197
+ }
1198
+ return { nodes, edges, cursor, hasMore, versionToken: null };
1199
+ },
1200
+ async push(node, changes) {
1201
+ if (node.type !== "github.Issue") {
1202
+ return { success: false, error: "Only issues are pushable" };
1203
+ }
1204
+ const match = node.id.match(/^github:([^#]+)#(\d+)$/);
1205
+ if (!match) {
1206
+ return { success: false, error: "Invalid node ID format" };
1207
+ }
1208
+ const [, repo, number] = match;
1209
+ const body = {};
1210
+ for (const change of changes) {
1211
+ if (change.field === "title") body.title = change.newValue;
1212
+ if (change.field === "body") body.body = change.newValue;
1213
+ if (change.field === "state") body.state = change.newValue;
1214
+ if (change.field === "labels") body.labels = change.newValue;
1215
+ if (change.field === "assignee") body.assignees = [change.newValue];
1216
+ if (change.field === "milestone") body.milestone = change.newValue;
1217
+ }
1218
+ const response = await fetchWithAuth(
1219
+ `https://api.github.com/repos/${repo}/issues/${number}`,
1220
+ {
1221
+ method: "PATCH",
1222
+ body: JSON.stringify(body),
1223
+ headers: { "Content-Type": "application/json" }
1224
+ }
1225
+ );
1226
+ if (!response.ok) {
1227
+ const error = await response.text();
1228
+ return { success: false, error };
1229
+ }
1230
+ return { success: true };
1231
+ },
1232
+ async fetchNode(nodeId) {
1233
+ const match = nodeId.match(/^github:([^/]+)\/([^#]+)#(\d+)$/);
1234
+ if (!match) return null;
1235
+ const [, owner, repo, number] = match;
1236
+ if (!owner || !repo || !number) return null;
1237
+ const response = await fetchWithAuth(
1238
+ `https://api.github.com/repos/${owner}/${repo}/issues/${number}`
1239
+ );
1240
+ if (response.status === 404) return null;
1241
+ if (!response.ok) {
1242
+ const error = await response.text();
1243
+ throw new Error(error);
1244
+ }
1245
+ const issue = await response.json();
1246
+ if (issue.pull_request) return null;
1247
+ return issueToNode(owner, repo, issue);
1248
+ },
1249
+ getTools() {
1250
+ return [
1251
+ {
1252
+ name: "github.updateIssue",
1253
+ description: "Update issue title, body, state"
1254
+ },
1255
+ { name: "github.addLabels", description: "Add labels to an issue" },
1256
+ {
1257
+ name: "github.removeLabels",
1258
+ description: "Remove labels from an issue"
1259
+ },
1260
+ { name: "github.assignIssue", description: "Assign users to an issue" },
1261
+ {
1262
+ name: "github.createComment",
1263
+ description: "Create a comment on an issue"
1264
+ }
1265
+ ];
1266
+ }
1267
+ };
1268
+ }
1269
+ registerProvider(
1270
+ "github",
1271
+ (config) => createGitHubProvider(config)
1272
+ );
1273
+
1274
+ // src/providers/a2a/index.ts
1275
+ init_esm_shims();
1276
+ function createA2AProvider(config) {
1277
+ return {
1278
+ name: "a2a",
1279
+ nodeTypes: ["a2a.Task", "a2a.Session", "a2a.Agent"],
1280
+ edgeTypes: ["a2a.TRACKS", "a2a.CREATED_BY", "a2a.PART_OF"],
1281
+ async fetch(request) {
1282
+ const nodes = [];
1283
+ const edges = [];
1284
+ if (!config.endpoint) {
1285
+ return { nodes, edges, hasMore: false };
1286
+ }
1287
+ try {
1288
+ const response = await fetch(`${config.endpoint}/tasks`, {
1289
+ headers: { Accept: "application/json" }
1290
+ });
1291
+ if (!response.ok) {
1292
+ return { nodes, edges, hasMore: false };
1293
+ }
1294
+ const tasks = await response.json();
1295
+ for (const task of tasks) {
1296
+ const nodeId = `a2a:${task.id}`;
1297
+ nodes.push({
1298
+ id: nodeId,
1299
+ type: "a2a.Task",
1300
+ attrs: {
1301
+ name: task.name,
1302
+ status: task.status,
1303
+ description: task.description,
1304
+ created_at: task.created_at,
1305
+ updated_at: task.updated_at,
1306
+ meta: task.meta
1307
+ }
1308
+ });
1309
+ if (task.meta?.github?.issue_number && task.meta?.github?.repository) {
1310
+ edges.push({
1311
+ type: "a2a.TRACKS",
1312
+ fromId: nodeId,
1313
+ toId: `github:${task.meta.github.repository}#${task.meta.github.issue_number}`
1314
+ });
1315
+ }
1316
+ if (task.agent_id) {
1317
+ edges.push({
1318
+ type: "a2a.CREATED_BY",
1319
+ fromId: nodeId,
1320
+ toId: `a2a:agent:${task.agent_id}`
1321
+ });
1322
+ }
1323
+ if (task.session_id) {
1324
+ edges.push({
1325
+ type: "a2a.PART_OF",
1326
+ fromId: nodeId,
1327
+ toId: `a2a:session:${task.session_id}`
1328
+ });
1329
+ }
1330
+ }
1331
+ } catch {
1332
+ }
1333
+ return { nodes, edges, hasMore: false };
1334
+ },
1335
+ async push(node, changes) {
1336
+ if (!config.endpoint) {
1337
+ return { success: false, error: "No A2A endpoint configured" };
1338
+ }
1339
+ const match = node.id.match(/^a2a:(.+)$/);
1340
+ if (!match) {
1341
+ return { success: false, error: "Invalid A2A node ID" };
1342
+ }
1343
+ const taskId = match[1];
1344
+ const body = {};
1345
+ for (const change of changes) {
1346
+ body[change.field] = change.newValue;
1347
+ }
1348
+ try {
1349
+ const response = await fetch(`${config.endpoint}/tasks/${taskId}`, {
1350
+ method: "PATCH",
1351
+ body: JSON.stringify(body),
1352
+ headers: { "Content-Type": "application/json" }
1353
+ });
1354
+ if (!response.ok) {
1355
+ return { success: false, error: await response.text() };
1356
+ }
1357
+ return { success: true };
1358
+ } catch (err) {
1359
+ return { success: false, error: String(err) };
1360
+ }
1361
+ },
1362
+ async fetchNode(_nodeId) {
1363
+ return null;
1364
+ },
1365
+ getTools() {
1366
+ return [
1367
+ { name: "a2a.createTask", description: "Create a new agent task" },
1368
+ {
1369
+ name: "a2a.updateTask",
1370
+ description: "Update task status or metadata"
1371
+ },
1372
+ { name: "a2a.linkIssue", description: "Link task to GitHub issue" }
1373
+ ];
1374
+ }
1375
+ };
1376
+ }
1377
+ registerProvider("a2a", (config) => createA2AProvider(config));
1378
+
1379
+ // src/providers/git/index.ts
1380
+ init_esm_shims();
1381
+ import { exec } from "child_process";
1382
+ import { promisify } from "util";
1383
+ import { readFile as readFile3, access as access2 } from "fs/promises";
1384
+ import { join as join3 } from "path";
1385
+ var execAsync = promisify(exec);
1386
+ async function execGit(cwd, ...args) {
1387
+ const { stdout } = await execAsync(`git ${args.join(" ")}`, { cwd });
1388
+ return stdout.trim();
1389
+ }
1390
+ async function getWorktrees(repoPath) {
1391
+ const output = await execGit(repoPath, "worktree", "list", "--porcelain");
1392
+ const worktrees = [];
1393
+ let current = {};
1394
+ for (const line of output.split("\n")) {
1395
+ if (line.startsWith("worktree ")) {
1396
+ if (current.path) worktrees.push(current);
1397
+ current = { path: line.slice(9) };
1398
+ } else if (line.startsWith("HEAD ")) {
1399
+ current.head = line.slice(5);
1400
+ } else if (line.startsWith("branch ")) {
1401
+ current.branch = line.slice(7).replace("refs/heads/", "");
1402
+ } else if (line === "bare") {
1403
+ current.bare = true;
1404
+ } else if (line === "detached") {
1405
+ current.detached = true;
1406
+ }
1407
+ }
1408
+ if (current.path) worktrees.push(current);
1409
+ return worktrees;
1410
+ }
1411
+ async function getBranches(repoPath) {
1412
+ try {
1413
+ const output = await execGit(
1414
+ repoPath,
1415
+ "branch",
1416
+ "-a",
1417
+ "--format=%(refname:short)"
1418
+ );
1419
+ return output.split("\n").filter(Boolean);
1420
+ } catch {
1421
+ return [];
1422
+ }
1423
+ }
1424
+ async function readWorktreeMeta(path2) {
1425
+ const metaPath = join3(path2, ".a2a", "session.json");
1426
+ try {
1427
+ await access2(metaPath);
1428
+ const content = await readFile3(metaPath, "utf-8");
1429
+ return JSON.parse(content);
1430
+ } catch {
1431
+ return null;
1432
+ }
1433
+ }
1434
+ function createGitProvider(config) {
1435
+ return {
1436
+ name: "git",
1437
+ nodeTypes: ["git.Branch", "git.Worktree", "git.Commit"],
1438
+ edgeTypes: ["git.TRACKS", "git.CONTAINS", "git.WORKTREE_OF"],
1439
+ async fetch(request) {
1440
+ const nodes = [];
1441
+ const edges = [];
1442
+ for (const repoConfig of config.repositories ?? []) {
1443
+ const repoPath = repoConfig.path.replace(
1444
+ /^~/,
1445
+ process.env["HOME"] ?? ""
1446
+ );
1447
+ try {
1448
+ const head = await execGit(repoPath, "rev-parse", "HEAD");
1449
+ if (request.versionToken === head) {
1450
+ return {
1451
+ nodes,
1452
+ edges,
1453
+ hasMore: false,
1454
+ cached: true,
1455
+ versionToken: head
1456
+ };
1457
+ }
1458
+ const worktrees = await getWorktrees(repoPath);
1459
+ const branches = await getBranches(repoPath);
1460
+ for (const wt of worktrees) {
1461
+ const nodeId = `git:worktree:${wt.path}`;
1462
+ const meta = await readWorktreeMeta(wt.path);
1463
+ nodes.push({
1464
+ id: nodeId,
1465
+ type: "git.Worktree",
1466
+ attrs: {
1467
+ path: wt.path,
1468
+ branch: wt.branch,
1469
+ head: wt.head,
1470
+ bare: wt.bare,
1471
+ detached: wt.detached,
1472
+ meta
1473
+ }
1474
+ });
1475
+ if (wt.branch) {
1476
+ edges.push({
1477
+ type: "git.WORKTREE_OF",
1478
+ fromId: `git:branch:${repoPath}:${wt.branch}`,
1479
+ toId: nodeId
1480
+ });
1481
+ }
1482
+ }
1483
+ for (const branch of branches) {
1484
+ const branchNodeId = `git:branch:${repoPath}:${branch}`;
1485
+ const worktree = worktrees.find((wt) => wt.branch === branch);
1486
+ nodes.push({
1487
+ id: branchNodeId,
1488
+ type: "git.Branch",
1489
+ attrs: {
1490
+ name: branch,
1491
+ repository: repoPath,
1492
+ worktreePath: worktree?.path
1493
+ }
1494
+ });
1495
+ if (config.links) {
1496
+ for (const link of config.links) {
1497
+ if (link.edge === "git.TRACKS" && worktree) {
1498
+ const meta = await readWorktreeMeta(worktree.path);
1499
+ if (meta?.a2a && typeof meta.a2a === "object") {
1500
+ const a2aMeta = meta.a2a;
1501
+ if (a2aMeta["task_id"]) {
1502
+ edges.push({
1503
+ type: "git.TRACKS",
1504
+ fromId: branchNodeId,
1505
+ toId: `a2a:${a2aMeta["task_id"]}`
1506
+ });
1507
+ }
1508
+ }
1509
+ }
1510
+ }
1511
+ }
1512
+ }
1513
+ } catch {
1514
+ }
1515
+ }
1516
+ return { nodes, edges, hasMore: false };
1517
+ },
1518
+ async push(node, changes) {
1519
+ return { success: false, error: "Git push not implemented via provider" };
1520
+ },
1521
+ async fetchNode(_nodeId) {
1522
+ return null;
1523
+ },
1524
+ getTools() {
1525
+ return [
1526
+ { name: "git.checkout", description: "Checkout branch" },
1527
+ { name: "git.push", description: "Push changes" },
1528
+ { name: "git.createBranch", description: "Create new branch" },
1529
+ {
1530
+ name: "git.createWorktree",
1531
+ description: "Create worktree for branch"
1532
+ }
1533
+ ];
1534
+ }
1535
+ };
1536
+ }
1537
+ registerProvider("git", (config) => createGitProvider(config));
1538
+
1539
+ // src/hardcopy/core.ts
1540
+ init_esm_shims();
1541
+ import { join as join5 } from "path";
1542
+ import { mkdir as mkdir3 } from "fs/promises";
1543
+ init_crdt();
1544
+
1545
+ // src/conflict-store.ts
1546
+ init_esm_shims();
1547
+ import { mkdir as mkdir2, readFile as readFile4, readdir, rm as rm2, writeFile as writeFile2 } from "fs/promises";
1548
+ import { join as join4 } from "path";
1549
+ import matter3 from "gray-matter";
1550
+
1551
+ // src/conflict.ts
1552
+ init_esm_shims();
1553
+ function detectFieldConflict(field, state) {
1554
+ const localChanged = !valuesEqual(state.local, state.base);
1555
+ const remoteChanged = !valuesEqual(state.remote, state.base);
1556
+ let status2;
1557
+ if (!localChanged && !remoteChanged) {
1558
+ status2 = "clean" /* CLEAN */;
1559
+ } else if (localChanged && !remoteChanged) {
1560
+ status2 = "clean" /* CLEAN */;
1561
+ } else if (!localChanged && remoteChanged) {
1562
+ status2 = "remote" /* REMOTE_ONLY */;
1563
+ } else if (valuesEqual(state.local, state.remote)) {
1564
+ status2 = "clean" /* CLEAN */;
1565
+ } else {
1566
+ status2 = "diverged" /* DIVERGED */;
1567
+ }
1568
+ return {
1569
+ field,
1570
+ status: status2,
1571
+ base: state.base,
1572
+ local: state.local,
1573
+ remote: state.remote,
1574
+ canAutoMerge: isListField(state)
1575
+ };
1576
+ }
1577
+ function detectConflicts(baseNode, localParsed, remoteNode, editableFields) {
1578
+ const conflicts = [];
1579
+ const baseAttrs = baseNode.attrs;
1580
+ const remoteAttrs = remoteNode.attrs;
1581
+ for (const field of editableFields) {
1582
+ const state = field === "body" ? {
1583
+ base: baseAttrs["body"] ?? "",
1584
+ local: localParsed.body ?? "",
1585
+ remote: remoteAttrs["body"] ?? ""
1586
+ } : {
1587
+ base: baseAttrs[field],
1588
+ local: localParsed.attrs[field],
1589
+ remote: remoteAttrs[field]
1590
+ };
1591
+ conflicts.push(detectFieldConflict(field, state));
1592
+ }
1593
+ return conflicts;
1594
+ }
1595
+ function hasUnresolvableConflicts(conflicts) {
1596
+ return conflicts.some(
1597
+ (conflict) => conflict.status === "diverged" /* DIVERGED */ && !conflict.canAutoMerge
1598
+ );
1599
+ }
1600
+ function autoMergeField(conflict) {
1601
+ if (conflict.status !== "diverged" /* DIVERGED */ || !conflict.canAutoMerge) {
1602
+ return null;
1603
+ }
1604
+ const merged = uniqueList([
1605
+ ...Array.isArray(conflict.base) ? conflict.base : [],
1606
+ ...Array.isArray(conflict.local) ? conflict.local : [],
1607
+ ...Array.isArray(conflict.remote) ? conflict.remote : []
1608
+ ]);
1609
+ return merged;
1610
+ }
1611
+ function generateConflictMarkers(conflict) {
1612
+ const local = String(conflict.local ?? "");
1613
+ const base = String(conflict.base ?? "");
1614
+ const remote = String(conflict.remote ?? "");
1615
+ return `<<<<<<< LOCAL
1616
+ ${local}
1617
+ ||||||| BASE
1618
+ ${base}
1619
+ =======
1620
+ ${remote}
1621
+ >>>>>>> REMOTE`;
1622
+ }
1623
+ function parseConflictMarkers(text) {
1624
+ const match = text.match(
1625
+ /<<<<<<< LOCAL\r?\n([\s\S]*?)\r?\n\|\|\|\|\|\|\| BASE\r?\n([\s\S]*?)\r?\n=======\r?\n([\s\S]*?)\r?\n>>>>>>> REMOTE/
1626
+ );
1627
+ if (!match) return null;
1628
+ return {
1629
+ local: match[1] ?? "",
1630
+ base: match[2] ?? "",
1631
+ remote: match[3] ?? ""
1632
+ };
1633
+ }
1634
+ function valuesEqual(a, b) {
1635
+ if (a === b) return true;
1636
+ if (a == null && b == null) return true;
1637
+ if (Array.isArray(a) && Array.isArray(b)) {
1638
+ const normalizedA = normalizeArray(a);
1639
+ const normalizedB = normalizeArray(b);
1640
+ if (normalizedA.length !== normalizedB.length) return false;
1641
+ return normalizedA.every((value, index) => value === normalizedB[index]);
1642
+ }
1643
+ return JSON.stringify(a) === JSON.stringify(b);
1644
+ }
1645
+ function normalizeArray(values) {
1646
+ return values.map((value) => JSON.stringify(value)).sort();
1647
+ }
1648
+ function isListField(state) {
1649
+ return Array.isArray(state.base) || Array.isArray(state.local) || Array.isArray(state.remote);
1650
+ }
1651
+ function uniqueList(values) {
1652
+ const seen = /* @__PURE__ */ new Set();
1653
+ const result = [];
1654
+ for (const value of values) {
1655
+ const key = JSON.stringify(value);
1656
+ if (seen.has(key)) continue;
1657
+ seen.add(key);
1658
+ result.push(value);
1659
+ }
1660
+ return result;
1661
+ }
1662
+
1663
+ // src/conflict-store.ts
1664
+ var ConflictStore = class {
1665
+ conflictsDir;
1666
+ constructor(conflictsDir) {
1667
+ this.conflictsDir = conflictsDir;
1668
+ }
1669
+ async save(info) {
1670
+ await mkdir2(this.conflictsDir, { recursive: true });
1671
+ const filePath = this.getPath(info.nodeId);
1672
+ const body = formatConflictBody(info.fields);
1673
+ const frontmatter = {
1674
+ nodeId: info.nodeId,
1675
+ nodeType: info.nodeType,
1676
+ filePath: info.filePath,
1677
+ detectedAt: new Date(info.detectedAt).toISOString(),
1678
+ fields: info.fields.map((field) => ({
1679
+ field: field.field,
1680
+ status: field.status,
1681
+ canAutoMerge: field.canAutoMerge
1682
+ }))
1683
+ };
1684
+ await writeFile2(filePath, matter3.stringify(body, frontmatter));
1685
+ }
1686
+ async list() {
1687
+ const entries = await readdir(this.conflictsDir).catch(() => []);
1688
+ const conflicts = [];
1689
+ for (const entry of entries) {
1690
+ if (!entry.endsWith(".md")) continue;
1691
+ const fullPath = join4(this.conflictsDir, entry);
1692
+ try {
1693
+ const content = await readFile4(fullPath, "utf-8");
1694
+ const parsed = matter3(content);
1695
+ const data = parsed.data;
1696
+ const fields = parseStoredFields(data["fields"]);
1697
+ const detectedAt = Date.parse(String(data["detectedAt"] ?? ""));
1698
+ conflicts.push({
1699
+ nodeId: String(data["nodeId"] ?? ""),
1700
+ nodeType: String(data["nodeType"] ?? ""),
1701
+ filePath: String(data["filePath"] ?? ""),
1702
+ detectedAt: Number.isNaN(detectedAt) ? 0 : detectedAt,
1703
+ fields
1704
+ });
1705
+ } catch {
1706
+ continue;
1707
+ }
1708
+ }
1709
+ return conflicts.filter((conflict) => conflict.nodeId.length > 0);
1710
+ }
1711
+ async get(nodeId) {
1712
+ const result = await this.read(nodeId);
1713
+ return result?.info ?? null;
1714
+ }
1715
+ async read(nodeId) {
1716
+ const filePath = this.getPath(nodeId);
1717
+ try {
1718
+ const content = await readFile4(filePath, "utf-8");
1719
+ const parsed = matter3(content);
1720
+ const data = parsed.data;
1721
+ const fields = parseStoredFields(data["fields"]);
1722
+ const detectedAt = Date.parse(String(data["detectedAt"] ?? ""));
1723
+ const info = {
1724
+ nodeId: String(data["nodeId"] ?? nodeId),
1725
+ nodeType: String(data["nodeType"] ?? ""),
1726
+ filePath: String(data["filePath"] ?? ""),
1727
+ detectedAt: Number.isNaN(detectedAt) ? 0 : detectedAt,
1728
+ fields
1729
+ };
1730
+ return { info, body: parsed.content };
1731
+ } catch {
1732
+ return null;
1733
+ }
1734
+ }
1735
+ async remove(nodeId) {
1736
+ const filePath = this.getPath(nodeId);
1737
+ try {
1738
+ await rm2(filePath, { force: true });
1739
+ } catch {
1740
+ return;
1741
+ }
1742
+ }
1743
+ getPath(nodeId) {
1744
+ return join4(this.conflictsDir, `${encodeURIComponent(nodeId)}.md`);
1745
+ }
1746
+ getArtifactPath(nodeId) {
1747
+ return this.getPath(nodeId);
1748
+ }
1749
+ };
1750
+ function parseStoredFields(value) {
1751
+ if (!Array.isArray(value)) return [];
1752
+ return value.map((item) => {
1753
+ if (!item || typeof item !== "object") return null;
1754
+ const record = item;
1755
+ const field = String(record["field"] ?? "");
1756
+ if (!field) return null;
1757
+ return {
1758
+ field,
1759
+ status: String(record["status"] ?? "clean"),
1760
+ canAutoMerge: Boolean(record["canAutoMerge"]),
1761
+ base: null,
1762
+ local: null,
1763
+ remote: null
1764
+ };
1765
+ }).filter((item) => item !== null);
1766
+ }
1767
+ function formatConflictBody(fields) {
1768
+ const blocks = fields.filter((field) => field.status === "diverged").map((field) => `## ${field.field}
1769
+ ${generateConflictMarkers(field)}`);
1770
+ return blocks.join("\n\n");
1771
+ }
1772
+
1773
+ // src/env.ts
1774
+ init_esm_shims();
1775
+ import { readFile as readFile5 } from "fs/promises";
1776
+ async function loadEnvFile(path2) {
1777
+ let content;
1778
+ try {
1779
+ content = await readFile5(path2, "utf-8");
1780
+ } catch {
1781
+ return;
1782
+ }
1783
+ for (const rawLine of content.split(/\r?\n/)) {
1784
+ const line = rawLine.trim();
1785
+ if (!line || line.startsWith("#")) continue;
1786
+ const index = line.indexOf("=");
1787
+ if (index <= 0) continue;
1788
+ const key = line.slice(0, index).trim();
1789
+ const rawValue = line.slice(index + 1).trim();
1790
+ if (!key) continue;
1791
+ if (process.env[key] !== void 0) continue;
1792
+ process.env[key] = stripQuotes(rawValue);
1793
+ }
1794
+ }
1795
+ function stripQuotes(value) {
1796
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
1797
+ return value.slice(1, -1);
1798
+ }
1799
+ return value;
1800
+ }
1801
+
1802
+ // src/hardcopy/core.ts
1803
+ var Hardcopy = class {
1804
+ root;
1805
+ dataDir;
1806
+ _db = null;
1807
+ _crdt = null;
1808
+ _config = null;
1809
+ _providers = /* @__PURE__ */ new Map();
1810
+ _conflictStore = null;
1811
+ constructor(options) {
1812
+ this.root = options.root;
1813
+ this.dataDir = join5(options.root, ".hardcopy");
1814
+ }
1815
+ async initialize() {
1816
+ await mkdir3(this.dataDir, { recursive: true });
1817
+ await loadEnvFile(join5(this.dataDir, ".env"));
1818
+ await mkdir3(join5(this.dataDir, "crdt"), { recursive: true });
1819
+ this._db = await HardcopyDatabase.open(join5(this.dataDir, "db.sqlite"));
1820
+ this._crdt = new CRDTStore(join5(this.dataDir, "crdt"));
1821
+ }
1822
+ async loadConfig() {
1823
+ if (this._config) return this._config;
1824
+ const configPath = join5(this.root, "hardcopy.yaml");
1825
+ this._config = await loadConfig(configPath);
1826
+ await this.initializeProviders();
1827
+ return this._config;
1828
+ }
1829
+ async initializeProviders() {
1830
+ if (!this._config) return;
1831
+ for (const source of this._config.sources) {
1832
+ const factory = getProvider(source.provider);
1833
+ if (factory) {
1834
+ this._providers.set(source.name, factory(source));
1835
+ }
1836
+ }
1837
+ }
1838
+ getDatabase() {
1839
+ if (!this._db) throw new Error("Database not initialized");
1840
+ return this._db;
1841
+ }
1842
+ getCRDTStore() {
1843
+ if (!this._crdt) throw new Error("CRDT store not initialized");
1844
+ return this._crdt;
1845
+ }
1846
+ getConflictStore() {
1847
+ if (!this._conflictStore) {
1848
+ this._conflictStore = new ConflictStore(join5(this.dataDir, "conflicts"));
1849
+ }
1850
+ return this._conflictStore;
1851
+ }
1852
+ getProviders() {
1853
+ return this._providers;
1854
+ }
1855
+ async close() {
1856
+ if (this._db) {
1857
+ await this._db.close();
1858
+ this._db = null;
1859
+ }
1860
+ }
1861
+ };
1862
+
1863
+ // src/hardcopy/sync.ts
1864
+ init_esm_shims();
1865
+ async function sync() {
1866
+ const config = await this.loadConfig();
1867
+ const db = this.getDatabase();
1868
+ const providers2 = this.getProviders();
1869
+ const stats = { nodes: 0, edges: 0, errors: [] };
1870
+ for (const source of config.sources) {
1871
+ const provider = providers2.get(source.name);
1872
+ if (!provider) {
1873
+ stats.errors.push(`Provider not found: ${source.provider}`);
1874
+ continue;
1875
+ }
1876
+ try {
1877
+ const result = await provider.fetch({ query: {} });
1878
+ if (!result.cached) {
1879
+ await db.upsertNodes(
1880
+ result.nodes.map((n) => ({
1881
+ ...n,
1882
+ syncedAt: Date.now(),
1883
+ versionToken: result.versionToken ?? void 0
1884
+ }))
1885
+ );
1886
+ await db.upsertEdges(result.edges);
1887
+ stats.nodes += result.nodes.length;
1888
+ stats.edges += result.edges.length;
1889
+ }
1890
+ } catch (err) {
1891
+ stats.errors.push(`Error syncing ${source.name}: ${err}`);
1892
+ }
1893
+ }
1894
+ return stats;
1895
+ }
1896
+
1897
+ // src/hardcopy/index.ts
1898
+ init_views();
1899
+ init_diff();
1900
+
1901
+ // src/hardcopy/push.ts
1902
+ init_esm_shims();
1903
+ init_crdt();
1904
+ import { join as join9 } from "path";
1905
+ import { readFile as readFile8, writeFile as writeFile5 } from "fs/promises";
1906
+ import matter4 from "gray-matter";
1907
+
1908
+ // src/merge.ts
1909
+ init_esm_shims();
1910
+ import { mkdtemp, rm as rm4, writeFile as writeFile4, mkdir as mkdir5 } from "fs/promises";
1911
+ import { join as join8 } from "path";
1912
+ import { tmpdir } from "os";
1913
+ import { spawnSync } from "child_process";
1914
+
1915
+ // src/llm-merge.ts
1916
+ init_esm_shims();
1917
+ var MERGE_SYSTEM_PROMPT = `You are a precise text merge assistant. Your task is to intelligently merge two versions of text that have both been modified from a common base.
1918
+
1919
+ Rules:
1920
+ 1. Preserve ALL meaningful changes from both versions
1921
+ 2. When both versions change the same content differently, combine the intents (e.g., if one adds bold and one restructures, do both)
1922
+ 3. Never lose information - if text was added on either side, include it
1923
+ 4. Maintain consistent formatting and style
1924
+ 5. Output ONLY the merged text, no explanations or markdown code blocks`;
1925
+ async function llmMergeText(base, local, remote, options = {}) {
1926
+ const baseURL = options.baseURL ?? process.env.OPENAI_BASE_URL ?? "http://localhost:6433";
1927
+ const model = options.model ?? process.env.OPENAI_MODEL ?? "gpt-4o";
1928
+ const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
1929
+ const temperature = options.temperature ?? 0;
1930
+ const messages = [
1931
+ { role: "system", content: MERGE_SYSTEM_PROMPT },
1932
+ {
1933
+ role: "user",
1934
+ content: `Merge these two versions that diverged from a common base.
1935
+
1936
+ === BASE (original) ===
1937
+ ${base}
1938
+
1939
+ === LOCAL (my changes) ===
1940
+ ${local}
1941
+
1942
+ === REMOTE (their changes) ===
1943
+ ${remote}
1944
+
1945
+ === MERGED OUTPUT ===`
1946
+ }
1947
+ ];
1948
+ try {
1949
+ const headers = {
1950
+ "Content-Type": "application/json"
1951
+ };
1952
+ if (apiKey) {
1953
+ headers["Authorization"] = `Bearer ${apiKey}`;
1954
+ }
1955
+ const response = await fetch(`${baseURL}/chat/completions`, {
1956
+ method: "POST",
1957
+ headers,
1958
+ body: JSON.stringify({
1959
+ model,
1960
+ messages,
1961
+ temperature,
1962
+ max_tokens: 4096
1963
+ })
1964
+ });
1965
+ if (!response.ok) {
1966
+ return null;
1967
+ }
1968
+ const data = await response.json();
1969
+ return data.choices?.[0]?.message?.content ?? null;
1970
+ } catch {
1971
+ return null;
1972
+ }
1973
+ }
1974
+
1975
+ // src/merge.ts
1976
+ async function mergeText(base, local, remote, options) {
1977
+ if (local === remote) return local;
1978
+ if (local === base) return remote;
1979
+ if (remote === base) return local;
1980
+ const diff3Result = await tryDiff3Merge(base, local, remote, options.tempDir);
1981
+ if (diff3Result !== null) {
1982
+ return diff3Result;
1983
+ }
1984
+ return llmMergeText(base, local, remote, options.llmOptions);
1985
+ }
1986
+ async function tryDiff3Merge(base, local, remote, tempDir) {
1987
+ const root = tempDir ?? join8(tmpdir(), "hardcopy-merge");
1988
+ await mkdir5(root, { recursive: true });
1989
+ const runDir = await mkdtemp(join8(root, "merge-"));
1990
+ const basePath = join8(runDir, "base");
1991
+ const localPath = join8(runDir, "local");
1992
+ const remotePath = join8(runDir, "remote");
1993
+ try {
1994
+ await writeFile4(basePath, base);
1995
+ await writeFile4(localPath, local);
1996
+ await writeFile4(remotePath, remote);
1997
+ const result = spawnSync("diff3", ["-m", localPath, basePath, remotePath], {
1998
+ encoding: "utf-8"
1999
+ });
2000
+ if (result.status === 0 && typeof result.stdout === "string") {
2001
+ return result.stdout;
2002
+ }
2003
+ return null;
2004
+ } catch {
2005
+ return null;
2006
+ } finally {
2007
+ await rm4(runDir, { recursive: true, force: true });
2008
+ }
2009
+ }
2010
+
2011
+ // src/hardcopy/push.ts
2012
+ init_format();
2013
+ init_diff();
2014
+ init_diff();
2015
+ async function push(filePath, options = {}) {
2016
+ await this.loadConfig();
2017
+ const db = this.getDatabase();
2018
+ const crdt = this.getCRDTStore();
2019
+ const stats = {
2020
+ pushed: 0,
2021
+ skipped: 0,
2022
+ conflicts: 0,
2023
+ errors: []
2024
+ };
2025
+ const diffs = await diff.call(this, filePath);
2026
+ for (const diffResult of diffs) {
2027
+ if (diffResult.changes.length === 0) {
2028
+ stats.skipped++;
2029
+ continue;
2030
+ }
2031
+ const provider = findProviderForNode.call(this, diffResult.nodeId);
2032
+ if (!provider) {
2033
+ stats.errors.push(`No provider for ${diffResult.nodeId}`);
2034
+ continue;
2035
+ }
2036
+ const dbNode = await db.getNode(diffResult.nodeId);
2037
+ if (!dbNode) {
2038
+ stats.errors.push(`Node not found: ${diffResult.nodeId}`);
2039
+ continue;
2040
+ }
2041
+ const format = getFormat(dbNode.type);
2042
+ if (!format) {
2043
+ stats.errors.push(`No format for ${dbNode.type}`);
2044
+ continue;
2045
+ }
2046
+ try {
2047
+ const localParsed = await parseLocalFile(diffResult.filePath);
2048
+ if (!localParsed) {
2049
+ stats.errors.push(`Failed to parse ${diffResult.filePath}`);
2050
+ continue;
2051
+ }
2052
+ const remoteNode = await provider.fetchNode(diffResult.nodeId);
2053
+ let changes = diffResult.changes;
2054
+ if (remoteNode && !options.force) {
2055
+ let conflicts = detectConflicts(
2056
+ dbNode,
2057
+ localParsed,
2058
+ remoteNode,
2059
+ format.editableFields
2060
+ );
2061
+ const semanticMerges = await trySemanticMerges.call(
2062
+ this,
2063
+ conflicts,
2064
+ diffResult.filePath
2065
+ );
2066
+ if (semanticMerges.size > 0) {
2067
+ changes = applySemanticMerges(changes, semanticMerges);
2068
+ conflicts = conflicts.map(
2069
+ (conflict) => semanticMerges.has(conflict.field) ? { ...conflict, status: "clean" /* CLEAN */ } : conflict
2070
+ );
2071
+ }
2072
+ if (hasUnresolvableConflicts(conflicts)) {
2073
+ await saveConflict.call(this, diffResult, conflicts, dbNode.type);
2074
+ stats.conflicts++;
2075
+ continue;
2076
+ }
2077
+ changes = applyAutoMerges(changes, conflicts);
2078
+ }
2079
+ const result = await provider.push(dbNode, changes);
2080
+ if (result.success) {
2081
+ const updatedAttrs = { ...dbNode.attrs };
2082
+ for (const change of changes) {
2083
+ updatedAttrs[change.field] = change.newValue;
2084
+ }
2085
+ await db.upsertNode({
2086
+ ...dbNode,
2087
+ attrs: updatedAttrs,
2088
+ syncedAt: Date.now()
2089
+ });
2090
+ const doc = await crdt.loadOrCreate(diffResult.nodeId);
2091
+ const bodyChange = changes.find((c) => c.field === "body");
2092
+ if (bodyChange) {
2093
+ setDocContent(doc, bodyChange.newValue);
2094
+ }
2095
+ await crdt.save(diffResult.nodeId, doc);
2096
+ try {
2097
+ await updateLocalFileAfterPush(diffResult.filePath, changes);
2098
+ } catch (err) {
2099
+ stats.errors.push(
2100
+ `Failed to update local file ${diffResult.filePath}: ${err}`
2101
+ );
2102
+ }
2103
+ stats.pushed++;
2104
+ } else {
2105
+ stats.errors.push(`Push failed for ${diffResult.nodeId}: ${result.error}`);
2106
+ }
2107
+ } catch (err) {
2108
+ stats.errors.push(`Error pushing ${diffResult.nodeId}: ${err}`);
2109
+ }
2110
+ }
2111
+ return stats;
2112
+ }
2113
+ async function status() {
2114
+ const db = this.getDatabase();
2115
+ const nodes = await db.queryNodes();
2116
+ const [edges] = await Promise.all([db.getEdges()]);
2117
+ const byType = /* @__PURE__ */ new Map();
2118
+ for (const node of nodes) {
2119
+ byType.set(node.type, (byType.get(node.type) ?? 0) + 1);
2120
+ }
2121
+ const { getChangedFiles: getChangedFiles2 } = await Promise.resolve().then(() => (init_diff(), diff_exports));
2122
+ const changedFiles = await getChangedFiles2.call(this);
2123
+ const conflicts = await listConflicts.call(this);
2124
+ return {
2125
+ totalNodes: nodes.length,
2126
+ totalEdges: edges.length,
2127
+ nodesByType: Object.fromEntries(byType),
2128
+ changedFiles,
2129
+ conflicts
2130
+ };
2131
+ }
2132
+ async function listConflicts() {
2133
+ return this.getConflictStore().list();
2134
+ }
2135
+ async function getConflict(nodeId) {
2136
+ return this.getConflictStore().get(nodeId);
2137
+ }
2138
+ async function getConflictDetail(nodeId) {
2139
+ const store = this.getConflictStore();
2140
+ const detail = await store.read(nodeId);
2141
+ if (!detail) return null;
2142
+ return {
2143
+ info: detail.info,
2144
+ body: detail.body,
2145
+ artifactPath: store.getArtifactPath(nodeId)
2146
+ };
2147
+ }
2148
+ async function resolveConflict(nodeId, resolution) {
2149
+ const store = this.getConflictStore();
2150
+ const conflict = await store.read(nodeId);
2151
+ if (!conflict) throw new Error(`Conflict not found: ${nodeId}`);
2152
+ const db = this.getDatabase();
2153
+ const dbNode = await db.getNode(nodeId);
2154
+ if (!dbNode) throw new Error(`Node not found: ${nodeId}`);
2155
+ const provider = findProviderForNode.call(this, nodeId);
2156
+ if (!provider) throw new Error(`No provider for ${nodeId}`);
2157
+ const format = getFormat(dbNode.type);
2158
+ if (!format) throw new Error(`No format for ${dbNode.type}`);
2159
+ const blocks = parseConflictBlocks(conflict.body);
2160
+ if (blocks.size === 0) {
2161
+ throw new Error(`No conflict markers found for ${nodeId}`);
2162
+ }
2163
+ const fileContent = await readFile8(conflict.info.filePath, "utf-8");
2164
+ const parsed = matter4(fileContent);
2165
+ const attrs = parsed.data;
2166
+ let body = parsed.content.trim();
2167
+ const updatedAttrs = { ...dbNode.attrs };
2168
+ for (const [field, choice] of Object.entries(resolution)) {
2169
+ const block = blocks.get(field);
2170
+ if (!block) continue;
2171
+ const value = choice === "local" ? block.local : block.remote;
2172
+ if (field === "body") {
2173
+ body = value;
2174
+ updatedAttrs["body"] = value;
2175
+ } else {
2176
+ attrs[field] = value;
2177
+ updatedAttrs[field] = value;
2178
+ }
2179
+ }
2180
+ const nextContent = matter4.stringify(body, attrs);
2181
+ await writeFile5(conflict.info.filePath, nextContent);
2182
+ const parsedForChanges = { attrs, body };
2183
+ const changes = detectChanges(
2184
+ parsedForChanges,
2185
+ dbNode,
2186
+ format.editableFields
2187
+ );
2188
+ if (changes.length > 0) {
2189
+ const result = await provider.push(dbNode, changes);
2190
+ if (!result.success) {
2191
+ throw new Error(`Push failed for ${nodeId}: ${result.error}`);
2192
+ }
2193
+ }
2194
+ await db.upsertNode({
2195
+ ...dbNode,
2196
+ attrs: updatedAttrs,
2197
+ syncedAt: Date.now()
2198
+ });
2199
+ const crdt = this.getCRDTStore();
2200
+ const doc = await crdt.loadOrCreate(nodeId);
2201
+ if (updatedAttrs["body"] !== void 0) {
2202
+ setDocContent(doc, String(updatedAttrs["body"] ?? ""));
2203
+ }
2204
+ await crdt.save(nodeId, doc);
2205
+ await store.remove(nodeId);
2206
+ }
2207
+ function findProviderForNode(nodeId) {
2208
+ const [providerPrefix] = nodeId.split(":");
2209
+ for (const [, provider] of this.getProviders()) {
2210
+ if (provider.name === providerPrefix) return provider;
2211
+ }
2212
+ return void 0;
2213
+ }
2214
+ async function parseLocalFile(fullPath) {
2215
+ try {
2216
+ const content = await readFile8(fullPath, "utf-8");
2217
+ return parseFile(content, "generic");
2218
+ } catch {
2219
+ return null;
2220
+ }
2221
+ }
2222
+ async function updateLocalFileAfterPush(fullPath, changes) {
2223
+ const content = await readFile8(fullPath, "utf-8");
2224
+ const parsed = matter4(content);
2225
+ const attrs = parsed.data;
2226
+ let body = parsed.content;
2227
+ for (const change of changes) {
2228
+ if (change.field === "body") {
2229
+ body = String(change.newValue ?? "");
2230
+ continue;
2231
+ }
2232
+ if (change.newValue === void 0 || change.newValue === null) {
2233
+ delete attrs[change.field];
2234
+ } else {
2235
+ attrs[change.field] = change.newValue;
2236
+ }
2237
+ }
2238
+ const nextContent = matter4.stringify(body, attrs);
2239
+ await writeFile5(fullPath, nextContent);
2240
+ }
2241
+ async function saveConflict(diffResult, conflicts, nodeType) {
2242
+ const store = this.getConflictStore();
2243
+ await store.save({
2244
+ nodeId: diffResult.nodeId,
2245
+ nodeType,
2246
+ filePath: diffResult.filePath,
2247
+ detectedAt: Date.now(),
2248
+ fields: conflicts
2249
+ });
2250
+ }
2251
+ function applyAutoMerges(changes, conflicts) {
2252
+ const mergedByField = /* @__PURE__ */ new Map();
2253
+ for (const conflict of conflicts) {
2254
+ const merged = autoMergeField(conflict);
2255
+ if (merged !== null) {
2256
+ mergedByField.set(conflict.field, merged);
2257
+ }
2258
+ }
2259
+ if (mergedByField.size === 0) return changes;
2260
+ const mergedChanges = changes.map((change) => {
2261
+ if (!mergedByField.has(change.field)) return change;
2262
+ return {
2263
+ ...change,
2264
+ newValue: mergedByField.get(change.field)
2265
+ };
2266
+ });
2267
+ for (const [field, value] of mergedByField) {
2268
+ if (!mergedChanges.find((change) => change.field === field)) {
2269
+ mergedChanges.push({ field, oldValue: void 0, newValue: value });
2270
+ }
2271
+ }
2272
+ return mergedChanges;
2273
+ }
2274
+ async function trySemanticMerges(conflicts, filePath) {
2275
+ const result = /* @__PURE__ */ new Map();
2276
+ const bodyConflict = conflicts.find(
2277
+ (conflict) => conflict.field === "body" && conflict.status === "diverged" /* DIVERGED */ && !conflict.canAutoMerge
2278
+ );
2279
+ if (!bodyConflict) return result;
2280
+ const base = String(bodyConflict.base ?? "");
2281
+ const local = String(bodyConflict.local ?? "");
2282
+ const remote = String(bodyConflict.remote ?? "");
2283
+ const merged = await mergeText(base, local, remote, {
2284
+ tempDir: join9(this.dataDir, "tmp", "merge"),
2285
+ filePath,
2286
+ llmOptions: {
2287
+ baseURL: process.env.OPENAI_BASE_URL,
2288
+ model: process.env.OPENAI_MODEL,
2289
+ apiKey: process.env.OPENAI_API_KEY
2290
+ }
2291
+ });
2292
+ if (merged !== null) {
2293
+ result.set("body", merged);
2294
+ }
2295
+ return result;
2296
+ }
2297
+ function applySemanticMerges(changes, merged) {
2298
+ if (merged.size === 0) return changes;
2299
+ const next = changes.map((change) => {
2300
+ if (!merged.has(change.field)) return change;
2301
+ return { ...change, newValue: merged.get(change.field) };
2302
+ });
2303
+ for (const [field, value] of merged) {
2304
+ if (!next.find((change) => change.field === field)) {
2305
+ next.push({ field, oldValue: void 0, newValue: value });
2306
+ }
2307
+ }
2308
+ return next;
2309
+ }
2310
+ function parseConflictBlocks(content) {
2311
+ const blocks = /* @__PURE__ */ new Map();
2312
+ const regex = /^\s*##\s+(.+?)\r?\n([\s\S]*?)(?=^\s*##\s+|\s*$)/gm;
2313
+ for (const match of content.matchAll(regex)) {
2314
+ const field = match[1]?.trim();
2315
+ const block = match[2] ?? "";
2316
+ if (!field) continue;
2317
+ const parsed = parseConflictMarkers(block);
2318
+ if (parsed) blocks.set(field, parsed);
2319
+ }
2320
+ if (blocks.size === 0) {
2321
+ const parsed = parseConflictMarkers(content);
2322
+ if (parsed) blocks.set("body", parsed);
2323
+ }
2324
+ return blocks;
2325
+ }
2326
+
2327
+ // src/hardcopy/init.ts
2328
+ init_esm_shims();
2329
+ import { join as join10 } from "path";
2330
+ import { mkdir as mkdir6, writeFile as writeFile6, access as access3 } from "fs/promises";
2331
+ async function initHardcopy(root) {
2332
+ const dataDir = join10(root, ".hardcopy");
2333
+ await mkdir6(dataDir, { recursive: true });
2334
+ await mkdir6(join10(dataDir, "crdt"), { recursive: true });
2335
+ await mkdir6(join10(dataDir, "errors"), { recursive: true });
2336
+ const db = await HardcopyDatabase.open(join10(dataDir, "db.sqlite"));
2337
+ await db.close();
2338
+ const configPath = join10(root, "hardcopy.yaml");
2339
+ try {
2340
+ await access3(configPath);
2341
+ } catch {
2342
+ const defaultConfig = `# Hardcopy configuration
2343
+ sources: []
2344
+ views: []
2345
+ `;
2346
+ await writeFile6(configPath, defaultConfig);
2347
+ }
2348
+ }
2349
+
2350
+ // src/hardcopy/types.ts
2351
+ init_esm_shims();
2352
+
2353
+ // src/hardcopy/index.ts
2354
+ registerFormat(githubIssueFormat);
2355
+ Hardcopy.prototype.sync = sync;
2356
+ Hardcopy.prototype.getViews = getViews;
2357
+ Hardcopy.prototype.refreshView = refreshView;
2358
+ Hardcopy.prototype.diff = diff;
2359
+ Hardcopy.prototype.getChangedFiles = getChangedFiles;
2360
+ Hardcopy.prototype.push = push;
2361
+ Hardcopy.prototype.status = status;
2362
+ Hardcopy.prototype.listConflicts = listConflicts;
2363
+ Hardcopy.prototype.getConflict = getConflict;
2364
+ Hardcopy.prototype.getConflictDetail = getConflictDetail;
2365
+ Hardcopy.prototype.resolveConflict = resolveConflict;
2366
+
2367
+ // src/mcp-server.ts
2368
+ init_esm_shims();
2369
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2370
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2371
+ import {
2372
+ CallToolRequestSchema,
2373
+ ErrorCode,
2374
+ ListToolsRequestSchema,
2375
+ McpError
2376
+ } from "@modelcontextprotocol/sdk/types.js";
2377
+ function createMcpServer(root) {
2378
+ const server = new Server(
2379
+ { name: "hardcopy", version: "0.1.0" },
2380
+ { capabilities: { tools: {} } }
2381
+ );
2382
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
2383
+ tools: [
2384
+ {
2385
+ name: "hardcopy_sync",
2386
+ description: "Sync all configured remote sources to local database",
2387
+ inputSchema: {
2388
+ type: "object",
2389
+ properties: {}
2390
+ }
2391
+ },
2392
+ {
2393
+ name: "hardcopy_status",
2394
+ description: "Show sync status including changed files and conflicts",
2395
+ inputSchema: {
2396
+ type: "object",
2397
+ properties: {}
2398
+ }
2399
+ },
2400
+ {
2401
+ name: "hardcopy_refresh",
2402
+ description: "Refresh local files from database for a view pattern",
2403
+ inputSchema: {
2404
+ type: "object",
2405
+ properties: {
2406
+ pattern: {
2407
+ type: "string",
2408
+ description: "View pattern to refresh (supports glob, e.g. docs/issues)"
2409
+ },
2410
+ clean: {
2411
+ type: "boolean",
2412
+ description: "Remove files that no longer match the view",
2413
+ default: false
2414
+ },
2415
+ syncFirst: {
2416
+ type: "boolean",
2417
+ description: "Sync data from remote before refreshing",
2418
+ default: false
2419
+ }
2420
+ },
2421
+ required: ["pattern"]
2422
+ }
2423
+ },
2424
+ {
2425
+ name: "hardcopy_diff",
2426
+ description: "Show local changes vs synced state for files matching pattern",
2427
+ inputSchema: {
2428
+ type: "object",
2429
+ properties: {
2430
+ pattern: {
2431
+ type: "string",
2432
+ description: "File pattern to check (supports glob)"
2433
+ }
2434
+ }
2435
+ }
2436
+ },
2437
+ {
2438
+ name: "hardcopy_push",
2439
+ description: "Push local changes to remote sources",
2440
+ inputSchema: {
2441
+ type: "object",
2442
+ properties: {
2443
+ pattern: {
2444
+ type: "string",
2445
+ description: "File pattern to push (supports glob)"
2446
+ },
2447
+ force: {
2448
+ type: "boolean",
2449
+ description: "Push even if conflicts are detected",
2450
+ default: false
2451
+ }
2452
+ }
2453
+ }
2454
+ },
2455
+ {
2456
+ name: "hardcopy_conflicts",
2457
+ description: "List all unresolved conflicts",
2458
+ inputSchema: {
2459
+ type: "object",
2460
+ properties: {}
2461
+ }
2462
+ },
2463
+ {
2464
+ name: "hardcopy_resolve",
2465
+ description: "Resolve a specific conflict",
2466
+ inputSchema: {
2467
+ type: "object",
2468
+ properties: {
2469
+ nodeId: {
2470
+ type: "string",
2471
+ description: "The node ID of the conflict to resolve"
2472
+ },
2473
+ resolution: {
2474
+ type: "object",
2475
+ description: 'Map of field names to resolution choice ("local" or "remote")',
2476
+ additionalProperties: {
2477
+ type: "string",
2478
+ enum: ["local", "remote"]
2479
+ }
2480
+ }
2481
+ },
2482
+ required: ["nodeId", "resolution"]
2483
+ }
2484
+ }
2485
+ ]
2486
+ }));
2487
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2488
+ const { name, arguments: args = {} } = request.params;
2489
+ const hc = new Hardcopy({ root });
2490
+ try {
2491
+ await hc.initialize();
2492
+ await hc.loadConfig();
2493
+ switch (name) {
2494
+ case "hardcopy_sync":
2495
+ return await handleSync(hc);
2496
+ case "hardcopy_status":
2497
+ return await handleStatus(hc);
2498
+ case "hardcopy_refresh":
2499
+ return await handleRefresh(hc, args);
2500
+ case "hardcopy_diff":
2501
+ return await handleDiff(hc, args);
2502
+ case "hardcopy_push":
2503
+ return await handlePush(hc, args);
2504
+ case "hardcopy_conflicts":
2505
+ return await handleConflicts(hc);
2506
+ case "hardcopy_resolve":
2507
+ return await handleResolve(hc, args);
2508
+ default:
2509
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
2510
+ }
2511
+ } catch (error) {
2512
+ if (error instanceof McpError) throw error;
2513
+ throw new McpError(
2514
+ ErrorCode.InternalError,
2515
+ `Failed to execute ${name}: ${error instanceof Error ? error.message : String(error)}`
2516
+ );
2517
+ } finally {
2518
+ await hc.close();
2519
+ }
2520
+ });
2521
+ return server;
2522
+ }
2523
+ async function handleSync(hc) {
2524
+ const stats = await hc.sync();
2525
+ return {
2526
+ content: [
2527
+ {
2528
+ type: "text",
2529
+ text: JSON.stringify(
2530
+ {
2531
+ synced: { nodes: stats.nodes, edges: stats.edges },
2532
+ errors: stats.errors
2533
+ },
2534
+ null,
2535
+ 2
2536
+ )
2537
+ }
2538
+ ]
2539
+ };
2540
+ }
2541
+ async function handleStatus(hc) {
2542
+ const status2 = await hc.status();
2543
+ return {
2544
+ content: [
2545
+ {
2546
+ type: "text",
2547
+ text: JSON.stringify(
2548
+ {
2549
+ nodes: status2.totalNodes,
2550
+ edges: status2.totalEdges,
2551
+ byType: status2.nodesByType,
2552
+ changedFiles: status2.changedFiles.map((f) => ({
2553
+ path: f.path,
2554
+ status: f.status,
2555
+ nodeId: f.nodeId
2556
+ })),
2557
+ conflicts: status2.conflicts.map((c) => ({
2558
+ nodeId: c.nodeId,
2559
+ fields: c.fields.map((f) => f.field)
2560
+ }))
2561
+ },
2562
+ null,
2563
+ 2
2564
+ )
2565
+ }
2566
+ ]
2567
+ };
2568
+ }
2569
+ async function handleRefresh(hc, args) {
2570
+ const { pattern, clean = false, syncFirst = false } = args;
2571
+ if (syncFirst) {
2572
+ await hc.sync();
2573
+ }
2574
+ const views = await hc.getViews();
2575
+ const matching = views.filter(
2576
+ (v) => v === pattern || v.startsWith(pattern) || pattern.includes("*")
2577
+ );
2578
+ if (matching.length === 0) {
2579
+ return {
2580
+ content: [
2581
+ {
2582
+ type: "text",
2583
+ text: JSON.stringify(
2584
+ { error: `No views match pattern: ${pattern}`, available: views },
2585
+ null,
2586
+ 2
2587
+ )
2588
+ }
2589
+ ]
2590
+ };
2591
+ }
2592
+ const results = [];
2593
+ for (const view of matching) {
2594
+ const result = await hc.refreshView(view, { clean });
2595
+ results.push({
2596
+ view,
2597
+ rendered: result.rendered,
2598
+ orphaned: result.orphaned.length,
2599
+ cleaned: result.cleaned
2600
+ });
2601
+ }
2602
+ return {
2603
+ content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
2604
+ };
2605
+ }
2606
+ async function handleDiff(hc, args) {
2607
+ const diffs = await hc.diff(args.pattern);
2608
+ return {
2609
+ content: [
2610
+ {
2611
+ type: "text",
2612
+ text: JSON.stringify(
2613
+ diffs.map((d) => ({
2614
+ nodeId: d.nodeId,
2615
+ nodeType: d.nodeType,
2616
+ filePath: d.filePath,
2617
+ changes: d.changes.map((c) => ({
2618
+ field: c.field,
2619
+ old: truncate(c.oldValue),
2620
+ new: truncate(c.newValue)
2621
+ }))
2622
+ })),
2623
+ null,
2624
+ 2
2625
+ )
2626
+ }
2627
+ ]
2628
+ };
2629
+ }
2630
+ async function handlePush(hc, args) {
2631
+ const stats = await hc.push(args.pattern, { force: args.force });
2632
+ return {
2633
+ content: [
2634
+ {
2635
+ type: "text",
2636
+ text: JSON.stringify(
2637
+ {
2638
+ pushed: stats.pushed,
2639
+ skipped: stats.skipped,
2640
+ conflicts: stats.conflicts,
2641
+ errors: stats.errors
2642
+ },
2643
+ null,
2644
+ 2
2645
+ )
2646
+ }
2647
+ ]
2648
+ };
2649
+ }
2650
+ async function handleConflicts(hc) {
2651
+ const conflicts = await hc.listConflicts();
2652
+ return {
2653
+ content: [
2654
+ {
2655
+ type: "text",
2656
+ text: JSON.stringify(
2657
+ conflicts.map((c) => ({
2658
+ nodeId: c.nodeId,
2659
+ nodeType: c.nodeType,
2660
+ filePath: c.filePath,
2661
+ fields: c.fields.map((f) => ({
2662
+ field: f.field,
2663
+ status: f.status,
2664
+ canAutoMerge: f.canAutoMerge
2665
+ }))
2666
+ })),
2667
+ null,
2668
+ 2
2669
+ )
2670
+ }
2671
+ ]
2672
+ };
2673
+ }
2674
+ async function handleResolve(hc, args) {
2675
+ await hc.resolveConflict(args.nodeId, args.resolution);
2676
+ return {
2677
+ content: [
2678
+ {
2679
+ type: "text",
2680
+ text: JSON.stringify({ resolved: args.nodeId }, null, 2)
2681
+ }
2682
+ ]
2683
+ };
2684
+ }
2685
+ function truncate(value, maxLen = 100) {
2686
+ const str = typeof value === "string" ? value : JSON.stringify(value);
2687
+ if (str.length <= maxLen) return str;
2688
+ return str.slice(0, maxLen) + "...";
2689
+ }
2690
+ async function serveMcp(root) {
2691
+ const server = createMcpServer(root);
2692
+ const transport = new StdioServerTransport();
2693
+ console.error("Hardcopy MCP Server running on stdio");
2694
+ await server.connect(transport);
2695
+ }
2696
+ if (import.meta.url === `file://${process.argv[1]}`) {
2697
+ serveMcp(process.cwd()).catch((error) => {
2698
+ console.error("Fatal error in MCP server:", error);
2699
+ process.exit(1);
2700
+ });
2701
+ }
2702
+ export {
2703
+ CRDTStore,
2704
+ ConflictStatus,
2705
+ ConflictStore,
2706
+ HardcopyDatabase as Database,
2707
+ Hardcopy,
2708
+ HardcopyDatabase,
2709
+ autoMergeField,
2710
+ createA2AProvider,
2711
+ createGitHubProvider,
2712
+ createGitProvider,
2713
+ createMcpServer,
2714
+ detectConflicts,
2715
+ detectFieldConflict,
2716
+ generateConflictMarkers,
2717
+ getDocAttrs,
2718
+ getDocContent,
2719
+ getFormat,
2720
+ getProvider,
2721
+ hasUnresolvableConflicts,
2722
+ initHardcopy,
2723
+ listFormats,
2724
+ listProviders,
2725
+ llmMergeText,
2726
+ loadConfig,
2727
+ mergeText,
2728
+ parseConfig,
2729
+ parseConflictMarkers,
2730
+ parseFile,
2731
+ registerFormat,
2732
+ registerProvider,
2733
+ renderNode,
2734
+ serveMcp,
2735
+ setDocAttrs,
2736
+ setDocContent
2737
+ };