kontexted 0.1.12 → 0.1.13

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 (45) hide show
  1. package/README.md +175 -26
  2. package/dist/commands/sync/conflicts/list.d.ts +16 -0
  3. package/dist/commands/sync/conflicts/list.js +131 -0
  4. package/dist/commands/sync/conflicts/resolve.d.ts +17 -0
  5. package/dist/commands/sync/conflicts/resolve.js +186 -0
  6. package/dist/commands/sync/conflicts/show.d.ts +16 -0
  7. package/dist/commands/sync/conflicts/show.js +128 -0
  8. package/dist/commands/sync/conflicts.d.ts +11 -0
  9. package/dist/commands/sync/conflicts.js +20 -0
  10. package/dist/commands/sync/force-pull.d.ts +21 -0
  11. package/dist/commands/sync/force-pull.js +173 -0
  12. package/dist/commands/sync/force-push.d.ts +21 -0
  13. package/dist/commands/sync/force-push.js +269 -0
  14. package/dist/commands/sync/index.d.ts +10 -0
  15. package/dist/commands/sync/index.js +187 -0
  16. package/dist/commands/sync/init.d.ts +13 -0
  17. package/dist/commands/sync/init.js +261 -0
  18. package/dist/commands/sync/reset.d.ts +21 -0
  19. package/dist/commands/sync/reset.js +134 -0
  20. package/dist/commands/sync/start.d.ts +23 -0
  21. package/dist/commands/sync/start.js +232 -0
  22. package/dist/commands/sync/status.d.ts +19 -0
  23. package/dist/commands/sync/status.js +203 -0
  24. package/dist/commands/sync/stop.d.ts +14 -0
  25. package/dist/commands/sync/stop.js +153 -0
  26. package/dist/index.js +2 -0
  27. package/dist/lib/api-client.d.ts +13 -0
  28. package/dist/lib/api-client.js +28 -2
  29. package/dist/lib/sync/command-utils.d.ts +77 -0
  30. package/dist/lib/sync/command-utils.js +281 -0
  31. package/dist/lib/sync/crypto.d.ts +8 -0
  32. package/dist/lib/sync/crypto.js +11 -0
  33. package/dist/lib/sync/file-watcher.d.ts +30 -0
  34. package/dist/lib/sync/file-watcher.js +117 -0
  35. package/dist/lib/sync/queue.d.ts +44 -0
  36. package/dist/lib/sync/queue.js +87 -0
  37. package/dist/lib/sync/remote-listener.d.ts +52 -0
  38. package/dist/lib/sync/remote-listener.js +228 -0
  39. package/dist/lib/sync/sync-engine.d.ts +175 -0
  40. package/dist/lib/sync/sync-engine.js +995 -0
  41. package/dist/lib/sync/types.d.ts +351 -0
  42. package/dist/lib/sync/types.js +5 -0
  43. package/dist/lib/sync/utils.d.ts +51 -0
  44. package/dist/lib/sync/utils.js +126 -0
  45. package/package.json +7 -4
@@ -0,0 +1,351 @@
1
+ /**
2
+ * Type definitions for the sync feature
3
+ * @packageDocumentation
4
+ */
5
+ /**
6
+ * Sync configuration stored in .sync/config.json
7
+ */
8
+ export interface SyncConfig {
9
+ /** Workspace slug being synced */
10
+ workspaceSlug: string;
11
+ /** Profile alias used for authentication */
12
+ alias: string;
13
+ /** Server URL */
14
+ serverUrl: string;
15
+ /** Sync mode */
16
+ syncMode: "auto" | "manual";
17
+ /** Conflict resolution strategy */
18
+ conflictStrategy: "newer-wins" | "local-wins" | "remote-wins";
19
+ /** Timestamp when sync was initialized */
20
+ initializedAt: string;
21
+ /** Daemon process ID (null if not running) */
22
+ daemonPid: number | null;
23
+ /** Sync directory path (relative to project root) */
24
+ syncDir: string;
25
+ }
26
+ /**
27
+ * File sync state stored in .sync/state.json
28
+ */
29
+ export interface SyncState {
30
+ /** Per-file sync state */
31
+ files: Record<string, FileSyncState>;
32
+ /** Per-folder sync state */
33
+ folders: Record<string, FolderSyncState>;
34
+ /** Last full sync timestamp */
35
+ lastFullSync: string | null;
36
+ /** Sync version (for future migrations) */
37
+ version: number;
38
+ }
39
+ /**
40
+ * State for a single file
41
+ */
42
+ export interface FileSyncState {
43
+ /** Content hash (SHA-256) of local file */
44
+ localHash: string | null;
45
+ /** Content hash (SHA-256) of remote note */
46
+ remoteHash: string | null;
47
+ /** Local file modification time */
48
+ localMtime: string | null;
49
+ /** Remote note updatedAt timestamp */
50
+ remoteMtime: string | null;
51
+ /** Last successful sync timestamp */
52
+ lastSync: string | null;
53
+ /** Remote note publicId */
54
+ publicId: string;
55
+ /** Remote note internal id */
56
+ noteId: number;
57
+ /** Folder path (relative to .kontexted/) */
58
+ folderPath: string | null;
59
+ }
60
+ /**
61
+ * State for a single folder
62
+ */
63
+ export interface FolderSyncState {
64
+ /** Local folder modification time */
65
+ localMtime: string | null;
66
+ /** Remote folder updatedAt timestamp */
67
+ remoteMtime: string | null;
68
+ /** Last successful sync timestamp */
69
+ lastSync: string | null;
70
+ /** Remote folder publicId */
71
+ publicId: string;
72
+ /** Remote folder internal id */
73
+ folderId: number;
74
+ /** Folder path (relative to sync directory) */
75
+ folderPath: string;
76
+ }
77
+ /**
78
+ * Pending change in the queue (SQLite)
79
+ */
80
+ export interface PendingChange {
81
+ id: number;
82
+ /** Relative file path */
83
+ filePath: string;
84
+ /** Change type */
85
+ type: "create" | "update" | "delete";
86
+ /** Content (null for delete) */
87
+ content: string | null;
88
+ /** When the change was detected */
89
+ detectedAt: string;
90
+ /** Number of retry attempts */
91
+ retryCount: number;
92
+ /** Last error message (if any) */
93
+ lastError: string | null;
94
+ }
95
+ /**
96
+ * Conflict log entry
97
+ */
98
+ export interface ConflictLogEntry {
99
+ timestamp: string;
100
+ filePath: string;
101
+ winner: "local" | "remote";
102
+ loserPath: string;
103
+ localMtime: string;
104
+ remoteMtime: string;
105
+ }
106
+ /**
107
+ * Remote note representation for sync
108
+ */
109
+ export interface RemoteNote {
110
+ id: number;
111
+ publicId: string;
112
+ name: string;
113
+ title: string;
114
+ content: string;
115
+ folderId: number | null;
116
+ folderPath: string | null;
117
+ updatedAt: string;
118
+ }
119
+ /**
120
+ * Sync status response
121
+ */
122
+ export interface SyncStatus {
123
+ status: "running" | "stopped" | "paused" | "error";
124
+ workspaceSlug: string;
125
+ filesSynced: number;
126
+ pendingChanges: number;
127
+ conflicts: number;
128
+ lastSync: string | null;
129
+ uptime: number | null;
130
+ error: string | null;
131
+ }
132
+ /**
133
+ * File change event from watcher
134
+ */
135
+ export interface FileChangeEvent {
136
+ /** Type of change detected */
137
+ type: "create" | "update" | "delete";
138
+ /** Absolute file path */
139
+ filePath: string;
140
+ /** Relative path from sync directory */
141
+ relativePath: string;
142
+ }
143
+ /**
144
+ * Folder change event from watcher
145
+ */
146
+ export interface FolderChangeEvent {
147
+ /** Type of change detected */
148
+ type: "create" | "delete";
149
+ /** Relative folder path */
150
+ folderPath: string;
151
+ /** Absolute folder path */
152
+ absolutePath: string;
153
+ }
154
+ /**
155
+ * Remote change event from SSE
156
+ */
157
+ export interface RemoteChangeEvent {
158
+ /** Type of remote change */
159
+ type: "create" | "update" | "delete";
160
+ /** The remote note (for create/update) */
161
+ note?: RemoteNote;
162
+ /** The public ID of the note (for delete) */
163
+ publicId?: string;
164
+ }
165
+ /**
166
+ * Remote folder change event from SSE
167
+ */
168
+ export interface RemoteFolderChangeEvent {
169
+ /** Type of remote folder change */
170
+ type: "folder.create" | "folder.delete" | "folder.update";
171
+ /** The remote folder (for create/update) */
172
+ folder?: RemoteFolder;
173
+ /** The public ID of the folder (for delete) */
174
+ publicId?: string;
175
+ }
176
+ /**
177
+ * Deleted note information from pull response
178
+ */
179
+ export interface DeletedNoteInfo {
180
+ /** Public ID of the deleted note */
181
+ publicId: string;
182
+ /** When the note was deleted */
183
+ deletedAt: string;
184
+ }
185
+ /**
186
+ * Remote folder representation
187
+ */
188
+ export interface RemoteFolder {
189
+ /** Internal folder ID */
190
+ id: number;
191
+ /** Public folder ID */
192
+ publicId: string;
193
+ /** Folder name (slug) */
194
+ name: string;
195
+ /** Display name */
196
+ displayName: string;
197
+ /** Parent folder ID */
198
+ parentId: number | null;
199
+ /** Folder path (relative to workspace root) */
200
+ folderPath: string;
201
+ /** Last update timestamp */
202
+ updatedAt: string;
203
+ /** Whether the folder is empty */
204
+ isEmpty?: boolean;
205
+ }
206
+ /**
207
+ * Response from /api/sync/pull
208
+ */
209
+ export interface SyncPullResponse {
210
+ /** Notes that have been created or updated since the requested timestamp */
211
+ notes: RemoteNote[];
212
+ /** Notes that have been deleted since the requested timestamp */
213
+ deleted: DeletedNoteInfo[];
214
+ /** Folders that have been created or updated */
215
+ folders: RemoteFolder[];
216
+ /** Server timestamp for the next sync request */
217
+ syncTimestamp: string;
218
+ }
219
+ /**
220
+ * Individual change in push request
221
+ */
222
+ export type SyncPushChange = SyncPushChangeCreate | SyncPushChangeUpdate | SyncPushChangeDelete | SyncPushChangeFolderCreate | SyncPushChangeFolderDelete;
223
+ /**
224
+ * Create change payload
225
+ */
226
+ export interface SyncPushChangeCreate {
227
+ /** Change type */
228
+ type: "create";
229
+ /** Temporary ID for correlating request with response */
230
+ tempId: string;
231
+ /** Note name (slug) */
232
+ name: string;
233
+ /** Note title */
234
+ title: string;
235
+ /** Note content (Markdown) */
236
+ content: string;
237
+ /** Target folder path */
238
+ folderPath: string | null;
239
+ }
240
+ /**
241
+ * Update change payload
242
+ */
243
+ export interface SyncPushChangeUpdate {
244
+ /** Change type */
245
+ type: "update";
246
+ /** Public ID of the note to update */
247
+ publicId: string;
248
+ /** Note name (slug) */
249
+ name: string;
250
+ /** Note title */
251
+ title: string;
252
+ /** Note content (Markdown) */
253
+ content: string;
254
+ /** Target folder path */
255
+ folderPath: string | null;
256
+ /** Expected last modification time for conflict detection */
257
+ expectedMtime: string;
258
+ }
259
+ /**
260
+ * Delete change payload
261
+ */
262
+ export interface SyncPushChangeDelete {
263
+ /** Change type */
264
+ type: "delete";
265
+ /** Public ID of the note to delete */
266
+ publicId: string;
267
+ }
268
+ /**
269
+ * Folder create change payload
270
+ */
271
+ export interface SyncPushChangeFolderCreate {
272
+ /** Change type */
273
+ type: "folder.create";
274
+ /** Folder name (slug) */
275
+ name: string;
276
+ /** Parent folder path (null for root) */
277
+ parentPath: string | null;
278
+ }
279
+ /**
280
+ * Folder delete change payload
281
+ */
282
+ export interface SyncPushChangeFolderDelete {
283
+ /** Change type */
284
+ type: "folder.delete";
285
+ /** Public ID of the folder to delete */
286
+ publicId: string;
287
+ }
288
+ /**
289
+ * Request to /api/sync/push
290
+ */
291
+ export interface SyncPushRequest {
292
+ /** Workspace slug */
293
+ workspaceSlug: string;
294
+ /** List of changes to push */
295
+ changes: SyncPushChange[];
296
+ }
297
+ /**
298
+ * Accepted change result
299
+ */
300
+ export interface SyncPushAccepted {
301
+ /** Public ID of the note */
302
+ publicId: string;
303
+ /** Status of the operation */
304
+ status: "updated";
305
+ }
306
+ /**
307
+ * Created note result
308
+ */
309
+ export interface SyncPushCreated {
310
+ /** Temporary ID from request (for correlation) */
311
+ tempId: string;
312
+ /** Public ID of the newly created note */
313
+ publicId: string;
314
+ /** Status of the operation */
315
+ status: "created";
316
+ }
317
+ /**
318
+ * Conflict information
319
+ */
320
+ export interface SyncPushConflict {
321
+ /** Public ID of the conflicting note */
322
+ publicId: string;
323
+ /** Reason for conflict */
324
+ reason: "remote_modified";
325
+ /** Remote note's last modification time */
326
+ remoteMtime: string;
327
+ /** Expected modification time sent in request */
328
+ expectedMtime: string;
329
+ }
330
+ /**
331
+ * Push error information
332
+ */
333
+ export interface SyncPushError {
334
+ /** Public ID of the note that failed */
335
+ publicId: string;
336
+ /** Error message */
337
+ error: string;
338
+ }
339
+ /**
340
+ * Response from /api/sync/push
341
+ */
342
+ export interface SyncPushResponse {
343
+ /** Successfully updated notes */
344
+ accepted: SyncPushAccepted[];
345
+ /** Successfully created notes */
346
+ created: SyncPushCreated[];
347
+ /** Conflicts that need resolution */
348
+ conflicts: SyncPushConflict[];
349
+ /** Errors that occurred during processing */
350
+ errors: SyncPushError[];
351
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Type definitions for the sync feature
3
+ * @packageDocumentation
4
+ */
5
+ export {};
@@ -0,0 +1,51 @@
1
+ import type { RemoteNote } from "./types.js";
2
+ /**
3
+ * Update .gitignore to exclude the sync directory.
4
+ * Also creates a .ignore file with negation pattern for grep compatibility.
5
+ *
6
+ * @param syncDir - The sync directory name to exclude (e.g., ".kontexted-sync")
7
+ */
8
+ export declare function updateGitignore(syncDir: string): Promise<void>;
9
+ /**
10
+ * Parse markdown content to extract title and body.
11
+ * Checks for frontmatter first, then falls back to H1 heading.
12
+ *
13
+ * @param content - The markdown content to parse
14
+ * @returns Object containing extracted title and remaining body content
15
+ */
16
+ export declare function parseMarkdown(content: string): {
17
+ title: string;
18
+ body: string;
19
+ };
20
+ /**
21
+ * Format a remote note as markdown.
22
+ * Adds title as H1 if not already present in content.
23
+ *
24
+ * @param note - The remote note to format
25
+ * @returns Markdown-formatted string with title as H1
26
+ */
27
+ export declare function formatMarkdown(note: RemoteNote): string;
28
+ /**
29
+ * Ensure a directory exists, creating it recursively if necessary.
30
+ *
31
+ * @param filePath - The file path whose directory should exist
32
+ */
33
+ export declare function ensureDirectoryExists(filePath: string): Promise<void>;
34
+ /**
35
+ * Compute the relative file path for a note.
36
+ * Uses the note's name with .md extension and folder path if present.
37
+ *
38
+ * @param note - The remote note to compute path for
39
+ * @returns Relative file path (e.g., "folder/subfolder/note.md")
40
+ */
41
+ export declare function computeFilePath(note: RemoteNote): string;
42
+ /**
43
+ * Execute a function with exponential backoff retry.
44
+ *
45
+ * @param fn - The async function to execute
46
+ * @param maxRetries - Maximum number of retry attempts (default: 3)
47
+ * @param baseDelay - Base delay in milliseconds (default: 1000)
48
+ * @returns Promise that resolves with the function's result
49
+ * @throws Last error if all retries are exhausted
50
+ */
51
+ export declare function withRetry<T>(fn: () => Promise<T>, maxRetries?: number, baseDelay?: number): Promise<T>;
@@ -0,0 +1,126 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ /**
4
+ * Update .gitignore to exclude the sync directory.
5
+ * Also creates a .ignore file with negation pattern for grep compatibility.
6
+ *
7
+ * @param syncDir - The sync directory name to exclude (e.g., ".kontexted-sync")
8
+ */
9
+ export async function updateGitignore(syncDir) {
10
+ const gitignorePath = join(process.cwd(), ".gitignore");
11
+ let content = "";
12
+ try {
13
+ content = await readFile(gitignorePath, "utf-8");
14
+ }
15
+ catch {
16
+ // File doesn't exist
17
+ }
18
+ if (!content.includes(`${syncDir}/`)) {
19
+ content += `\n# Kontexted sync\n${syncDir}/\n`;
20
+ await writeFile(gitignorePath, content, "utf-8");
21
+ }
22
+ // Create .ignore for grep compatibility
23
+ const ignorePath = join(process.cwd(), ".ignore");
24
+ let ignoreContent = "";
25
+ try {
26
+ ignoreContent = await readFile(ignorePath, "utf-8");
27
+ }
28
+ catch {
29
+ // File doesn't exist
30
+ }
31
+ if (!ignoreContent.includes(`!${syncDir}/`)) {
32
+ ignoreContent += `\n!${syncDir}/\n`;
33
+ await writeFile(ignorePath, ignoreContent, "utf-8");
34
+ }
35
+ }
36
+ /**
37
+ * Parse markdown content to extract title and body.
38
+ * Checks for frontmatter first, then falls back to H1 heading.
39
+ *
40
+ * @param content - The markdown content to parse
41
+ * @returns Object containing extracted title and remaining body content
42
+ */
43
+ export function parseMarkdown(content) {
44
+ // Check for frontmatter
45
+ if (content.startsWith("---\n")) {
46
+ const endIndex = content.indexOf("\n---\n", 4);
47
+ if (endIndex !== -1) {
48
+ const frontmatter = content.slice(4, endIndex);
49
+ const body = content.slice(endIndex + 5);
50
+ const titleMatch = frontmatter.match(/title:\s*(.+)/);
51
+ return {
52
+ title: titleMatch?.[1]?.trim() ?? "",
53
+ body
54
+ };
55
+ }
56
+ }
57
+ // Check for H1 title
58
+ const h1Match = content.match(/^#\s+(.+)$/m);
59
+ return {
60
+ title: h1Match?.[1]?.trim() ?? "",
61
+ body: content
62
+ };
63
+ }
64
+ /**
65
+ * Format a remote note as markdown.
66
+ * Adds title as H1 if not already present in content.
67
+ *
68
+ * @param note - The remote note to format
69
+ * @returns Markdown-formatted string with title as H1
70
+ */
71
+ export function formatMarkdown(note) {
72
+ // Handle case where content is undefined (from some SSE events)
73
+ const content = note.content ?? "";
74
+ // If content starts with H1 matching title, keep as-is
75
+ if (content.startsWith(`# ${note.title}`)) {
76
+ return content;
77
+ }
78
+ // Otherwise, add title as H1
79
+ return `# ${note.title}\n\n${content}`;
80
+ }
81
+ /**
82
+ * Ensure a directory exists, creating it recursively if necessary.
83
+ *
84
+ * @param filePath - The file path whose directory should exist
85
+ */
86
+ export async function ensureDirectoryExists(filePath) {
87
+ const dir = dirname(filePath);
88
+ await mkdir(dir, { recursive: true });
89
+ }
90
+ /**
91
+ * Compute the relative file path for a note.
92
+ * Uses the note's name with .md extension and folder path if present.
93
+ *
94
+ * @param note - The remote note to compute path for
95
+ * @returns Relative file path (e.g., "folder/subfolder/note.md")
96
+ */
97
+ export function computeFilePath(note) {
98
+ const fileName = `${note.name}.md`;
99
+ if (note.folderPath) {
100
+ return join(note.folderPath, fileName);
101
+ }
102
+ return fileName;
103
+ }
104
+ /**
105
+ * Execute a function with exponential backoff retry.
106
+ *
107
+ * @param fn - The async function to execute
108
+ * @param maxRetries - Maximum number of retry attempts (default: 3)
109
+ * @param baseDelay - Base delay in milliseconds (default: 1000)
110
+ * @returns Promise that resolves with the function's result
111
+ * @throws Last error if all retries are exhausted
112
+ */
113
+ export async function withRetry(fn, maxRetries = 3, baseDelay = 1000) {
114
+ let lastError = null;
115
+ for (let i = 0; i < maxRetries; i++) {
116
+ try {
117
+ return await fn();
118
+ }
119
+ catch (error) {
120
+ lastError = error instanceof Error ? error : new Error(String(error));
121
+ const delay = baseDelay * Math.pow(2, i);
122
+ await new Promise((resolve) => setTimeout(resolve, delay));
123
+ }
124
+ }
125
+ throw lastError;
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kontexted",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "CLI tool for Kontexted - MCP proxy, workspaces management, and local server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -30,21 +30,24 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@modelcontextprotocol/sdk": "^1.25.3",
33
+ "chokidar": "^5.0.0",
33
34
  "commander": "^12.1.0",
35
+ "eventsource": "^4.1.0",
34
36
  "yargs": "^17.7.2",
35
37
  "zod": "^3.23.8"
36
38
  },
37
39
  "devDependencies": {
38
40
  "@types/node": "^20.0.0",
39
41
  "@types/yargs": "^17.0.35",
42
+ "bun-types": "^1.3.9",
40
43
  "tsc-alias": "^1.8.10",
41
44
  "tsup": "^8.3.5",
42
45
  "tsx": "^4.19.2",
43
46
  "typescript": "^5.6.0"
44
47
  },
45
48
  "optionalDependencies": {
46
- "@kontexted/darwin-arm64": "0.1.12",
47
- "@kontexted/linux-x64": "0.1.12",
48
- "@kontexted/windows-x64": "0.1.12"
49
+ "@kontexted/darwin-arm64": "0.1.13",
50
+ "@kontexted/linux-x64": "0.1.13",
51
+ "@kontexted/windows-x64": "0.1.13"
49
52
  }
50
53
  }