@gobi-ai/cli 0.6.9 → 0.6.11
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 +1 -1
- package/dist/commands/sync.js +96 -34
- package/package.json +1 -1
- package/skills/gobi/SKILL.md +2 -2
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@gobi-ai/cli)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
|
-
Command-line interface for the [Gobi](https://
|
|
7
|
+
Command-line interface for the [Gobi](https://gobispace.com) collaborative knowledge platform.
|
|
8
8
|
|
|
9
9
|
## Installation
|
|
10
10
|
|
package/dist/commands/sync.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 === "" ? [] :
|
|
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
package/skills/gobi/SKILL.md
CHANGED
|
@@ -10,12 +10,12 @@ description: >-
|
|
|
10
10
|
allowed-tools: Bash(gobi:*)
|
|
11
11
|
metadata:
|
|
12
12
|
author: gobi-ai
|
|
13
|
-
version: "0.6.
|
|
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.
|
|
18
|
+
A CLI client for the Gobi collaborative knowledge platform (v0.6.10).
|
|
19
19
|
|
|
20
20
|
## Prerequisites
|
|
21
21
|
|