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.
- package/README.md +175 -26
- package/dist/commands/sync/conflicts/list.d.ts +16 -0
- package/dist/commands/sync/conflicts/list.js +131 -0
- package/dist/commands/sync/conflicts/resolve.d.ts +17 -0
- package/dist/commands/sync/conflicts/resolve.js +186 -0
- package/dist/commands/sync/conflicts/show.d.ts +16 -0
- package/dist/commands/sync/conflicts/show.js +128 -0
- package/dist/commands/sync/conflicts.d.ts +11 -0
- package/dist/commands/sync/conflicts.js +20 -0
- package/dist/commands/sync/force-pull.d.ts +21 -0
- package/dist/commands/sync/force-pull.js +173 -0
- package/dist/commands/sync/force-push.d.ts +21 -0
- package/dist/commands/sync/force-push.js +269 -0
- package/dist/commands/sync/index.d.ts +10 -0
- package/dist/commands/sync/index.js +187 -0
- package/dist/commands/sync/init.d.ts +13 -0
- package/dist/commands/sync/init.js +261 -0
- package/dist/commands/sync/reset.d.ts +21 -0
- package/dist/commands/sync/reset.js +134 -0
- package/dist/commands/sync/start.d.ts +23 -0
- package/dist/commands/sync/start.js +232 -0
- package/dist/commands/sync/status.d.ts +19 -0
- package/dist/commands/sync/status.js +203 -0
- package/dist/commands/sync/stop.d.ts +14 -0
- package/dist/commands/sync/stop.js +153 -0
- package/dist/index.js +2 -0
- package/dist/lib/api-client.d.ts +13 -0
- package/dist/lib/api-client.js +28 -2
- package/dist/lib/sync/command-utils.d.ts +77 -0
- package/dist/lib/sync/command-utils.js +281 -0
- package/dist/lib/sync/crypto.d.ts +8 -0
- package/dist/lib/sync/crypto.js +11 -0
- package/dist/lib/sync/file-watcher.d.ts +30 -0
- package/dist/lib/sync/file-watcher.js +117 -0
- package/dist/lib/sync/queue.d.ts +44 -0
- package/dist/lib/sync/queue.js +87 -0
- package/dist/lib/sync/remote-listener.d.ts +52 -0
- package/dist/lib/sync/remote-listener.js +228 -0
- package/dist/lib/sync/sync-engine.d.ts +175 -0
- package/dist/lib/sync/sync-engine.js +995 -0
- package/dist/lib/sync/types.d.ts +351 -0
- package/dist/lib/sync/types.js +5 -0
- package/dist/lib/sync/utils.d.ts +51 -0
- package/dist/lib/sync/utils.js +126 -0
- 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,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.
|
|
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.
|
|
47
|
-
"@kontexted/linux-x64": "0.1.
|
|
48
|
-
"@kontexted/windows-x64": "0.1.
|
|
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
|
}
|