@gobi-ai/cli 0.6.9 → 0.6.10

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.
@@ -55,6 +55,8 @@ const EMPTY_STATE = {
55
55
  cursor: null,
56
56
  syncfilesHash: null,
57
57
  patterns: [],
58
+ privatePatterns: [],
59
+ privatefilesHash: null,
58
60
  hashCache: {},
59
61
  };
60
62
  function openDb(gobiDir) {
@@ -84,6 +86,8 @@ export function loadSyncState(gobiDir) {
84
86
  cursor: parsed.cursor ?? null,
85
87
  syncfilesHash: parsed.syncfilesHash ?? null,
86
88
  patterns: parsed.patterns ?? [],
89
+ privatePatterns: parsed.privatePatterns ?? [],
90
+ privatefilesHash: parsed.privatefilesHash ?? null,
87
91
  hashCache: parsed.hashCache ?? {},
88
92
  };
89
93
  saveSyncState(gobiDir, state);
@@ -108,6 +112,8 @@ export function loadSyncState(gobiDir) {
108
112
  cursor: metaMap.cursor ? Number(metaMap.cursor) : EMPTY_STATE.cursor,
109
113
  syncfilesHash: metaMap.syncfiles_hash || EMPTY_STATE.syncfilesHash,
110
114
  patterns: metaMap.patterns ? JSON.parse(metaMap.patterns) : EMPTY_STATE.patterns,
115
+ privatePatterns: metaMap.private_patterns ? JSON.parse(metaMap.private_patterns) : EMPTY_STATE.privatePatterns,
116
+ privatefilesHash: metaMap.privatefiles_hash || EMPTY_STATE.privatefilesHash,
111
117
  hashCache,
112
118
  };
113
119
  }
@@ -123,6 +129,8 @@ export function saveSyncState(gobiDir, state) {
123
129
  upsert.run("cursor", state.cursor !== null ? String(state.cursor) : "");
124
130
  upsert.run("syncfiles_hash", state.syncfilesHash ?? "");
125
131
  upsert.run("patterns", JSON.stringify(state.patterns));
132
+ upsert.run("private_patterns", JSON.stringify(state.privatePatterns));
133
+ upsert.run("privatefiles_hash", state.privatefilesHash ?? "");
126
134
  db.exec("DELETE FROM hash_cache");
127
135
  const insert = db.prepare("INSERT INTO hash_cache (path, hash, mtime, size) VALUES (?, ?, ?, ?)");
128
136
  for (const [path, entry] of Object.entries(state.hashCache)) {
@@ -282,21 +290,6 @@ async function webdriveSync(baseUrl, vaultSlug, body, token) {
282
290
  }
283
291
  return (await res.json());
284
292
  }
285
- async function webdrivePrivatefiles(baseUrl, vaultSlug, patterns, token) {
286
- const url = `${baseUrl}/api/v1/vaults/${vaultSlug}/privatefiles`;
287
- const res = await fetch(url, {
288
- method: "POST",
289
- headers: {
290
- Authorization: `Bearer ${token}`,
291
- "Content-Type": "application/json",
292
- },
293
- body: JSON.stringify({ patterns }),
294
- });
295
- if (!res.ok) {
296
- throw new Error(`Privatefiles request failed: HTTP ${res.status}: ${await res.text()}`);
297
- }
298
- return (await res.json());
299
- }
300
293
  // ─── Conflict Resolution ──────────────────────────────────────────────────────
301
294
  function formatDate(ms) {
302
295
  return new Date(ms).toLocaleString();
@@ -346,10 +339,11 @@ function matchesPaths(filePath, paths) {
346
339
  return false;
347
340
  }
348
341
  // ─── Core Sync ────────────────────────────────────────────────────────────────
349
- async function performSync(baseUrl, vaultSlug, state, syncfilesChanges, localFiles, opts, token) {
342
+ async function performSync(baseUrl, vaultSlug, state, syncfilesChanges, privatefilesChanges, localFiles, opts, token) {
350
343
  const body = {
351
344
  cursor: state.cursor,
352
345
  syncfilesChanges,
346
+ privatefilesChanges,
353
347
  clientFiles: localFiles,
354
348
  uploadOnly: opts.uploadOnly,
355
349
  downloadOnly: opts.downloadOnly,
@@ -371,19 +365,6 @@ export async function runSync(opts) {
371
365
  }
372
366
  const token = opts.authToken ?? (await getValidToken());
373
367
  // Sync privatefiles with server
374
- if (!opts.dryRun) {
375
- try {
376
- const localPrivatePatterns = readPrivatefiles(gobiDir);
377
- const privateresp = await webdrivePrivatefiles(baseUrl, vaultSlug, localPrivatePatterns, token);
378
- if (!opts.uploadOnly && privateresp.patterns.length > 0) {
379
- await writeFile(join(gobiDir, "privatefiles"), privateresp.patterns.join("\n") + "\n");
380
- }
381
- }
382
- catch (err) {
383
- if (!jsonMode)
384
- console.error(`Warning: Failed to sync privatefiles: ${err.message}`);
385
- }
386
- }
387
368
  // Read syncfiles whitelist
388
369
  const { patterns: currPatterns, contentHash: currSyncfilesHash } = readSyncfiles(gobiDir);
389
370
  if (currPatterns.length === 0 && !jsonMode) {
@@ -391,7 +372,45 @@ export async function runSync(opts) {
391
372
  "Add gitignore-style patterns to .gobi/syncfiles to select files for sync.");
392
373
  }
393
374
  const isWhitelisted = buildWhitelistMatcher(currPatterns);
394
- const syncfilesChanges = computeSyncfilesChanges(state.patterns, currPatterns);
375
+ // On bootstrap (no prior state), fetch server's current syncfiles so removals
376
+ // relative to the server are captured correctly in the delta.
377
+ let baseSyncPatterns = state.patterns;
378
+ if (!opts.dryRun && state.syncfilesHash === null) {
379
+ try {
380
+ const serverContent = await webdriveGet(baseUrl, vaultSlug, ".gobi/syncfiles", token);
381
+ baseSyncPatterns = serverContent
382
+ .toString("utf-8")
383
+ .split("\n")
384
+ .map((l) => l.trim())
385
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
386
+ process.stderr.write(`[gobi-sync] bootstrap: fetched server syncfiles, patterns=${JSON.stringify(baseSyncPatterns)}\n`);
387
+ }
388
+ catch {
389
+ baseSyncPatterns = []; // server has no syncfiles yet
390
+ }
391
+ }
392
+ const syncfilesChanges = computeSyncfilesChanges(baseSyncPatterns, currPatterns);
393
+ // Compute privatefiles delta (supports removals via sync endpoint)
394
+ const currPrivatePatterns = readPrivatefiles(gobiDir);
395
+ // On bootstrap (no prior state), fetch server's current patterns so we can compute
396
+ // correct removals. Without this, patterns the user deleted would stay on the server
397
+ // because we'd have no baseline to diff against.
398
+ let basePrivatePatterns = state.privatePatterns;
399
+ if (!opts.dryRun && state.privatefilesHash === null) {
400
+ try {
401
+ const serverContent = await webdriveGet(baseUrl, vaultSlug, ".gobi/privatefiles", token);
402
+ basePrivatePatterns = serverContent
403
+ .toString("utf-8")
404
+ .split("\n")
405
+ .map((l) => l.trim())
406
+ .filter((l) => l.length > 0 && !l.startsWith("#"));
407
+ process.stderr.write(`[gobi-sync] bootstrap: fetched server privatefiles, patterns=${JSON.stringify(basePrivatePatterns)}\n`);
408
+ }
409
+ catch {
410
+ basePrivatePatterns = []; // server has no privatefiles yet
411
+ }
412
+ }
413
+ const privatefilesChanges = computeSyncfilesChanges(basePrivatePatterns, currPrivatePatterns);
395
414
  // Walk local files (only whitelisted, non-ignored)
396
415
  if (!jsonMode)
397
416
  process.stdout.write("Scanning local files...");
@@ -440,7 +459,7 @@ export async function runSync(opts) {
440
459
  process.stdout.write("Syncing with server...");
441
460
  let syncResp;
442
461
  try {
443
- syncResp = await performSync(baseUrl, vaultSlug, state, syncfilesChanges, clientFilesForSync, opts, token);
462
+ syncResp = await performSync(baseUrl, vaultSlug, state, syncfilesChanges, privatefilesChanges, clientFilesForSync, opts, token);
444
463
  }
445
464
  catch (err) {
446
465
  if (err instanceof GobiError && err.status === 409) {
@@ -452,8 +471,10 @@ export async function runSync(opts) {
452
471
  state.cursor = null;
453
472
  state.hashCache = {};
454
473
  state.patterns = []; // reset so retry sends currPatterns as syncfilesChanges.added
474
+ state.privatePatterns = []; // reset so retry sends currPrivatePatterns as privatefilesChanges.added
455
475
  const retryChanges = computeSyncfilesChanges([], currPatterns);
456
- syncResp = await performSync(baseUrl, vaultSlug, state, retryChanges, clientFilesForSync, opts, token);
476
+ const retryPrivateChanges = computeSyncfilesChanges([], currPrivatePatterns);
477
+ syncResp = await performSync(baseUrl, vaultSlug, state, retryChanges, retryPrivateChanges, clientFilesForSync, opts, token);
457
478
  }
458
479
  else {
459
480
  throw err;
@@ -579,14 +600,55 @@ export async function runSync(opts) {
579
600
  console.error(` Error [${entry.action}] ${entry.path}: ${msg}`);
580
601
  }
581
602
  }
603
+ // Download syncfiles from server if the server's hash changed since last sync
604
+ let effectivePatterns = currPatterns;
605
+ process.stderr.write(`[gobi-sync] syncfiles: state=${state.syncfilesHash ?? "null"} server=${syncResp.syncfilesHash ?? "null"}\n`);
606
+ if (!opts.dryRun && syncResp.syncfilesHash && syncResp.syncfilesHash !== state.syncfilesHash) {
607
+ process.stderr.write(`[gobi-sync] syncfiles hash changed — downloading from server\n`);
608
+ try {
609
+ const syncfilesContent = await webdriveGet(baseUrl, vaultSlug, ".gobi/syncfiles", token);
610
+ await writeFile(join(gobiDir, "syncfiles"), syncfilesContent);
611
+ const { patterns: newPatterns } = readSyncfiles(gobiDir);
612
+ effectivePatterns = newPatterns;
613
+ process.stderr.write(`[gobi-sync] syncfiles downloaded OK, patterns=${JSON.stringify(newPatterns)}\n`);
614
+ if (!jsonMode)
615
+ console.log(" Updated local syncfiles from server.");
616
+ }
617
+ catch (err) {
618
+ process.stderr.write(`[gobi-sync] syncfiles download FAILED: ${err.message}\n`);
619
+ if (!jsonMode)
620
+ console.error(`Warning: Failed to download syncfiles from server: ${err.message}`);
621
+ }
622
+ }
623
+ // Download privatefiles from server if the server's hash changed since last sync
624
+ let effectivePrivatePatterns = currPrivatePatterns;
625
+ process.stderr.write(`[gobi-sync] privatefiles: state=${state.privatefilesHash ?? "null"} server=${syncResp.privatefilesHash ?? "null"}\n`);
626
+ if (!opts.dryRun && syncResp.privatefilesHash && syncResp.privatefilesHash !== state.privatefilesHash) {
627
+ process.stderr.write(`[gobi-sync] privatefiles hash changed — downloading from server\n`);
628
+ try {
629
+ const privatefilesContent = await webdriveGet(baseUrl, vaultSlug, ".gobi/privatefiles", token);
630
+ await writeFile(join(gobiDir, "privatefiles"), privatefilesContent);
631
+ effectivePrivatePatterns = readPrivatefiles(gobiDir);
632
+ process.stderr.write(`[gobi-sync] privatefiles downloaded OK, patterns=${JSON.stringify(effectivePrivatePatterns)}\n`);
633
+ if (!jsonMode)
634
+ console.log(" Updated local privatefiles from server.");
635
+ }
636
+ catch (err) {
637
+ process.stderr.write(`[gobi-sync] privatefiles download FAILED: ${err.message}\n`);
638
+ if (!jsonMode)
639
+ console.error(`Warning: Failed to download privatefiles from server: ${err.message}`);
640
+ }
641
+ }
582
642
  // Persist state (always, even on partial failures)
583
643
  const finalCursor = Math.max(syncResp.cursor, maxMutationCursor !== null ? maxMutationCursor : 0);
584
644
  state.cursor = finalCursor;
585
- state.syncfilesHash = currSyncfilesHash;
645
+ state.syncfilesHash = syncResp.syncfilesHash || currSyncfilesHash;
586
646
  // If the server returned an empty syncfilesHash the vault was deleted server-side
587
647
  // (empty patterns path). Reset patterns so the next sync re-registers them as "added",
588
648
  // which lets the 409 retry resurrect the vault.
589
- state.patterns = syncResp.syncfilesHash === "" ? [] : currPatterns;
649
+ state.patterns = syncResp.syncfilesHash === "" ? [] : effectivePatterns;
650
+ state.privatePatterns = effectivePrivatePatterns;
651
+ state.privatefilesHash = syncResp.privatefilesHash || state.privatefilesHash;
590
652
  saveSyncState(gobiDir, state);
591
653
  // Output summary
592
654
  const result = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobi-ai/cli",
3
- "version": "0.6.9",
3
+ "version": "0.6.10",
4
4
  "description": "CLI client for the Gobi collaborative knowledge platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -10,12 +10,12 @@ description: >-
10
10
  allowed-tools: Bash(gobi:*)
11
11
  metadata:
12
12
  author: gobi-ai
13
- version: "0.6.9"
13
+ version: "0.6.10"
14
14
  ---
15
15
 
16
16
  # gobi-cli
17
17
 
18
- A CLI client for the Gobi collaborative knowledge platform (v0.6.9).
18
+ A CLI client for the Gobi collaborative knowledge platform (v0.6.10).
19
19
 
20
20
  ## Prerequisites
21
21