@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
@@ -0,0 +1,406 @@
1
+ import { Database } from 'better-sqlite3';
2
+ export { Database as BetterSqlite3Database } from 'better-sqlite3';
3
+ import { LoroDoc } from 'loro-crdt';
4
+ export { createMcpServer, serveMcp } from './mcp-server.js';
5
+ import '@modelcontextprotocol/sdk/server/index.js';
6
+
7
+ interface Node {
8
+ id: string;
9
+ type: string;
10
+ attrs: Record<string, unknown>;
11
+ syncedAt?: number;
12
+ versionToken?: string;
13
+ cursor?: string;
14
+ }
15
+ interface Edge {
16
+ id?: number;
17
+ type: string;
18
+ fromId: string;
19
+ toId: string;
20
+ attrs?: Record<string, unknown>;
21
+ }
22
+ interface FetchRequest {
23
+ query: NodeQuery;
24
+ cursor?: string;
25
+ pageSize?: number;
26
+ versionToken?: string;
27
+ }
28
+ interface FetchResult {
29
+ nodes: Node[];
30
+ edges: Edge[];
31
+ cursor?: string;
32
+ hasMore: boolean;
33
+ versionToken?: string | null;
34
+ cached?: boolean;
35
+ }
36
+ interface PushResult {
37
+ success: boolean;
38
+ error?: string;
39
+ versionToken?: string;
40
+ }
41
+ interface NodeQuery {
42
+ id?: string;
43
+ type?: string;
44
+ attrs?: Record<string, unknown>;
45
+ }
46
+ interface Change {
47
+ field: string;
48
+ oldValue: unknown;
49
+ newValue: unknown;
50
+ }
51
+ interface SyncDecision {
52
+ strategy: "auto" | "llm" | "manual";
53
+ reason: string;
54
+ }
55
+ interface SyncError {
56
+ resourceId: string;
57
+ strategy: "auto" | "llm";
58
+ error: string;
59
+ llmExplanation?: string;
60
+ suggestedActions?: string[];
61
+ }
62
+ interface IndexState {
63
+ cursor?: string;
64
+ total?: number;
65
+ loaded: number;
66
+ pageSize: number;
67
+ lastFetch: string;
68
+ ttl: number;
69
+ }
70
+ declare enum ConflictStatus {
71
+ CLEAN = "clean",
72
+ REMOTE_ONLY = "remote",
73
+ DIVERGED = "diverged"
74
+ }
75
+ interface ThreeWayState {
76
+ base: unknown;
77
+ local: unknown;
78
+ remote: unknown;
79
+ }
80
+ interface FieldConflict {
81
+ field: string;
82
+ status: ConflictStatus;
83
+ base: unknown;
84
+ local: unknown;
85
+ remote: unknown;
86
+ canAutoMerge: boolean;
87
+ }
88
+ interface ConflictInfo {
89
+ nodeId: string;
90
+ nodeType: string;
91
+ filePath: string;
92
+ detectedAt: number;
93
+ fields: FieldConflict[];
94
+ }
95
+
96
+ interface Tool {
97
+ name: string;
98
+ description: string;
99
+ parameters?: Record<string, unknown>;
100
+ }
101
+ interface Provider {
102
+ name: string;
103
+ nodeTypes: string[];
104
+ edgeTypes: string[];
105
+ fetch(request: FetchRequest): Promise<FetchResult>;
106
+ push(node: Node, changes: Change[]): Promise<PushResult>;
107
+ fetchNode(nodeId: string): Promise<Node | null>;
108
+ getTools(): Tool[];
109
+ }
110
+ type ProviderFactory = (config: Record<string, unknown>) => Provider;
111
+ declare function registerProvider(name: string, factory: ProviderFactory): void;
112
+ declare function getProvider(name: string): ProviderFactory | undefined;
113
+ declare function listProviders(): string[];
114
+
115
+ interface ParsedFile {
116
+ attrs: Record<string, unknown>;
117
+ body: string;
118
+ }
119
+ interface FormatHandler {
120
+ type: string;
121
+ editableFields: string[];
122
+ render(node: Node): string;
123
+ parse(content: string): ParsedFile;
124
+ }
125
+ declare function registerFormat(handler: FormatHandler): void;
126
+ declare function getFormat(type: string): FormatHandler | undefined;
127
+ declare function listFormats(): string[];
128
+ declare function renderNode(node: Node, template?: string): string;
129
+ declare function parseFile(content: string, type: string): ParsedFile;
130
+
131
+ interface LinkConfig {
132
+ edge: string;
133
+ to: string;
134
+ match: string;
135
+ }
136
+ interface SourceConfig {
137
+ name: string;
138
+ provider: string;
139
+ orgs?: string[];
140
+ repositories?: {
141
+ path: string;
142
+ }[];
143
+ links?: LinkConfig[];
144
+ [key: string]: unknown;
145
+ }
146
+ interface RenderConfig {
147
+ path: string;
148
+ type?: string;
149
+ template?: string;
150
+ args?: Record<string, unknown>;
151
+ }
152
+ interface ViewConfig {
153
+ path: string;
154
+ description?: string;
155
+ query: string;
156
+ partition?: {
157
+ by: string;
158
+ fallback?: string;
159
+ };
160
+ render: RenderConfig[];
161
+ }
162
+ interface Config {
163
+ sources: SourceConfig[];
164
+ views: ViewConfig[];
165
+ }
166
+ declare function loadConfig(path: string): Promise<Config>;
167
+ declare function parseConfig(content: string): Config;
168
+
169
+ declare class HardcopyDatabase {
170
+ private db;
171
+ private graphqliteLoaded;
172
+ constructor(db: Database);
173
+ static open(path: string): Promise<HardcopyDatabase>;
174
+ private initialize;
175
+ private migrateLegacySchema;
176
+ private getTableColumns;
177
+ private renameTableIfNeeded;
178
+ private dropLegacyIndexes;
179
+ private resolveGraphqliteLoadPath;
180
+ private getExtensionCandidates;
181
+ private ensureGraphqliteLoaded;
182
+ private normalizeCypher;
183
+ private escapeCypherType;
184
+ private extractNodeIds;
185
+ private parseCypherRows;
186
+ cypher(query: string, params?: Record<string, unknown>): Promise<Record<string, unknown>[]>;
187
+ queryViewNodes(query: string, params?: Record<string, unknown>): Promise<Node[]>;
188
+ getNodesByIds(ids: string[]): Promise<Node[]>;
189
+ upsertNode(node: Node): Promise<void>;
190
+ upsertNodes(nodes: Node[]): Promise<void>;
191
+ getNode(id: string): Promise<Node | null>;
192
+ queryNodes(type?: string): Promise<Node[]>;
193
+ deleteNode(id: string): Promise<void>;
194
+ upsertEdge(edge: Edge): Promise<void>;
195
+ upsertEdges(edges: Edge[]): Promise<void>;
196
+ getEdges(fromId?: string, toId?: string, type?: string): Promise<Edge[]>;
197
+ deleteEdge(fromId: string, toId: string, type: string): Promise<void>;
198
+ private escapeCypherString;
199
+ private upsertGraphNode;
200
+ private upsertGraphEdge;
201
+ private deleteGraphNode;
202
+ private deleteGraphEdge;
203
+ close(): Promise<void>;
204
+ }
205
+
206
+ declare class CRDTStore {
207
+ private basePath;
208
+ constructor(basePath: string);
209
+ private getPath;
210
+ exists(nodeId: string): Promise<boolean>;
211
+ load(nodeId: string): Promise<LoroDoc | null>;
212
+ save(nodeId: string, doc: LoroDoc): Promise<void>;
213
+ create(nodeId: string): Promise<LoroDoc>;
214
+ loadOrCreate(nodeId: string): Promise<LoroDoc>;
215
+ delete(nodeId: string): Promise<void>;
216
+ merge(nodeId: string, remote: LoroDoc): Promise<LoroDoc>;
217
+ }
218
+ declare function setDocContent(doc: LoroDoc, content: string): void;
219
+ declare function getDocContent(doc: LoroDoc): string;
220
+ declare function setDocAttrs(doc: LoroDoc, attrs: Record<string, unknown>): void;
221
+ declare function getDocAttrs(doc: LoroDoc): Record<string, unknown>;
222
+
223
+ interface HardcopyOptions {
224
+ root: string;
225
+ }
226
+ interface SyncStats {
227
+ nodes: number;
228
+ edges: number;
229
+ errors: string[];
230
+ }
231
+ interface StatusInfo {
232
+ totalNodes: number;
233
+ totalEdges: number;
234
+ nodesByType: Record<string, number>;
235
+ changedFiles: ChangedFile[];
236
+ conflicts: ConflictInfo[];
237
+ }
238
+ interface ChangedFile {
239
+ path: string;
240
+ fullPath: string;
241
+ nodeId: string;
242
+ nodeType: string;
243
+ status: "new" | "modified" | "deleted";
244
+ mtime: number;
245
+ syncedAt: number;
246
+ }
247
+ interface RefreshResult {
248
+ rendered: number;
249
+ orphaned: string[];
250
+ cleaned: boolean;
251
+ }
252
+ interface DiffResult {
253
+ nodeId: string;
254
+ nodeType: string;
255
+ filePath: string;
256
+ changes: Change[];
257
+ }
258
+ interface PushStats {
259
+ pushed: number;
260
+ skipped: number;
261
+ conflicts: number;
262
+ errors: string[];
263
+ }
264
+
265
+ interface GitHubConfig {
266
+ orgs?: string[];
267
+ repos?: string[];
268
+ token?: string;
269
+ }
270
+ declare function createGitHubProvider(config: GitHubConfig): Provider;
271
+
272
+ interface A2AConfig {
273
+ endpoint?: string;
274
+ links?: {
275
+ edge: string;
276
+ to: string;
277
+ match: string;
278
+ }[];
279
+ }
280
+ declare function createA2AProvider(config: A2AConfig): Provider;
281
+
282
+ interface GitConfig {
283
+ repositories?: {
284
+ path: string;
285
+ }[];
286
+ links?: {
287
+ edge: string;
288
+ to: string;
289
+ match: string;
290
+ }[];
291
+ }
292
+ declare function createGitProvider(config: GitConfig): Provider;
293
+
294
+ declare class ConflictStore {
295
+ private conflictsDir;
296
+ constructor(conflictsDir: string);
297
+ save(info: ConflictInfo): Promise<void>;
298
+ list(): Promise<ConflictInfo[]>;
299
+ get(nodeId: string): Promise<ConflictInfo | null>;
300
+ read(nodeId: string): Promise<{
301
+ info: ConflictInfo;
302
+ body: string;
303
+ } | null>;
304
+ remove(nodeId: string): Promise<void>;
305
+ private getPath;
306
+ getArtifactPath(nodeId: string): string;
307
+ }
308
+
309
+ declare class Hardcopy {
310
+ readonly root: string;
311
+ readonly dataDir: string;
312
+ private _db;
313
+ private _crdt;
314
+ private _config;
315
+ private _providers;
316
+ private _conflictStore;
317
+ constructor(options: HardcopyOptions);
318
+ initialize(): Promise<void>;
319
+ loadConfig(): Promise<Config>;
320
+ private initializeProviders;
321
+ getDatabase(): HardcopyDatabase;
322
+ getCRDTStore(): CRDTStore;
323
+ getConflictStore(): ConflictStore;
324
+ getProviders(): Map<string, Provider>;
325
+ close(): Promise<void>;
326
+ }
327
+
328
+ declare function initHardcopy(root: string): Promise<void>;
329
+
330
+ declare module "./core" {
331
+ interface Hardcopy {
332
+ sync(): Promise<SyncStats>;
333
+ getViews(): Promise<string[]>;
334
+ refreshView(viewPath: string, options?: {
335
+ clean?: boolean;
336
+ }): Promise<RefreshResult>;
337
+ diff(pattern?: string, options?: {
338
+ smart?: boolean;
339
+ }): Promise<DiffResult[]>;
340
+ getChangedFiles(pattern?: string): Promise<ChangedFile[]>;
341
+ push(filePath?: string, options?: {
342
+ force?: boolean;
343
+ }): Promise<PushStats>;
344
+ status(): Promise<StatusInfo>;
345
+ listConflicts(): Promise<ConflictInfo[]>;
346
+ getConflict(nodeId: string): Promise<ConflictInfo | null>;
347
+ getConflictDetail(nodeId: string): Promise<{
348
+ info: ConflictInfo;
349
+ body: string;
350
+ artifactPath: string;
351
+ } | null>;
352
+ resolveConflict(nodeId: string, resolution: Record<string, "local" | "remote">): Promise<void>;
353
+ }
354
+ }
355
+
356
+ declare function detectFieldConflict(field: string, state: ThreeWayState): FieldConflict;
357
+ declare function detectConflicts(baseNode: Node, localParsed: ParsedFile, remoteNode: Node, editableFields: string[]): FieldConflict[];
358
+ declare function hasUnresolvableConflicts(conflicts: FieldConflict[]): boolean;
359
+ declare function autoMergeField(conflict: FieldConflict): unknown | null;
360
+ declare function generateConflictMarkers(conflict: FieldConflict): string;
361
+ declare function parseConflictMarkers(text: string): {
362
+ local: string;
363
+ base: string;
364
+ remote: string;
365
+ } | null;
366
+
367
+ /**
368
+ * LLM-based merge for conflicts that can't be auto-resolved.
369
+ *
370
+ * Uses an OpenAI-compatible endpoint (e.g., copilot-proxy) to intelligently
371
+ * merge conflicting changes by understanding semantic intent.
372
+ */
373
+ interface LLMMergeOptions {
374
+ /** OpenAI-compatible API base URL (default: OPENAI_BASE_URL or http://localhost:6433) */
375
+ baseURL?: string;
376
+ /** Model to use (default: OPENAI_MODEL or gpt-4o) */
377
+ model?: string;
378
+ /** API key for authentication (default: OPENAI_API_KEY) */
379
+ apiKey?: string;
380
+ /** Temperature for generation (default: 0) */
381
+ temperature?: number;
382
+ }
383
+ /**
384
+ * Attempts to merge conflicting text using an LLM.
385
+ *
386
+ * @param base - The common ancestor text
387
+ * @param local - The local (your) version
388
+ * @param remote - The remote (their) version
389
+ * @param options - LLM configuration options
390
+ * @returns The merged text, or null if the LLM call fails
391
+ */
392
+ declare function llmMergeText(base: string, local: string, remote: string, options?: LLMMergeOptions): Promise<string | null>;
393
+
394
+ interface SemanticMergeOptions {
395
+ tempDir?: string;
396
+ filePath: string;
397
+ /** LLM merge options (URL, model, etc.) */
398
+ llmOptions?: LLMMergeOptions;
399
+ }
400
+ /**
401
+ * Attempts a 3-way merge using diff3, with LLM fallback for conflicts.
402
+ * Returns the merged text if successful, or null if merge fails entirely.
403
+ */
404
+ declare function mergeText(base: string, local: string, remote: string, options: SemanticMergeOptions): Promise<string | null>;
405
+
406
+ export { CRDTStore, type Change, type ChangedFile, type Config, type ConflictInfo, ConflictStatus, ConflictStore, HardcopyDatabase as Database, type DiffResult, type Edge, type FetchRequest, type FetchResult, type FieldConflict, type FormatHandler, Hardcopy, HardcopyDatabase, type HardcopyOptions, type IndexState, type LLMMergeOptions, type LinkConfig, type Node, type NodeQuery, type ParsedFile, type Provider, type ProviderFactory, type PushResult, type PushStats, type RefreshResult, type RenderConfig, type SemanticMergeOptions, type SourceConfig, type StatusInfo, type SyncDecision, type SyncError, type SyncStats, type ThreeWayState, type Tool, type ViewConfig, autoMergeField, createA2AProvider, createGitHubProvider, createGitProvider, detectConflicts, detectFieldConflict, generateConflictMarkers, getDocAttrs, getDocContent, getFormat, getProvider, hasUnresolvableConflicts, initHardcopy, listFormats, listProviders, llmMergeText, loadConfig, mergeText, parseConfig, parseConflictMarkers, parseFile, registerFormat, registerProvider, renderNode, setDocAttrs, setDocContent };