@stitchdb/cli 0.7.4 → 0.9.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.
- package/dist/cli.js +207 -32
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -84,12 +84,35 @@ function client(cfg) {
|
|
|
84
84
|
console.error('Not logged in. Run: stitch login');
|
|
85
85
|
process.exit(2);
|
|
86
86
|
}
|
|
87
|
+
// Per-project workspace pin: .stitch/project.json's workspace_id wins over
|
|
88
|
+
// the global default. Lets multiple projects share one machine without
|
|
89
|
+
// their memory pools polluting each other.
|
|
90
|
+
const projectWorkspace = readProjectWorkspaceId(process.cwd());
|
|
87
91
|
return new Stitch({
|
|
88
92
|
apiKey: cfg.apiKey,
|
|
89
93
|
baseUrl: cfg.baseUrl,
|
|
90
|
-
workspace: cfg.defaultWorkspace,
|
|
94
|
+
workspace: projectWorkspace || cfg.defaultWorkspace,
|
|
91
95
|
});
|
|
92
96
|
}
|
|
97
|
+
function readProjectWorkspaceId(cwd) {
|
|
98
|
+
let cur = cwd;
|
|
99
|
+
for (let i = 0; i < 8; i++) {
|
|
100
|
+
const projectFile = path.join(cur, '.stitch', 'project.json');
|
|
101
|
+
if (fs.existsSync(projectFile)) {
|
|
102
|
+
try {
|
|
103
|
+
const pj = JSON.parse(fs.readFileSync(projectFile, 'utf8'));
|
|
104
|
+
if (typeof pj.workspace_id === 'string' && pj.workspace_id.startsWith('wsp_'))
|
|
105
|
+
return pj.workspace_id;
|
|
106
|
+
}
|
|
107
|
+
catch { /* fall through */ }
|
|
108
|
+
}
|
|
109
|
+
const next = path.dirname(cur);
|
|
110
|
+
if (next === cur)
|
|
111
|
+
break;
|
|
112
|
+
cur = next;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
93
116
|
// ─── Argv helpers ──────────────────────────────────────────────────────────
|
|
94
117
|
function parseFlag(args, names) {
|
|
95
118
|
for (const name of names) {
|
|
@@ -222,6 +245,9 @@ async function cmdWorkspace(args) {
|
|
|
222
245
|
const w = await stitch.workspaces.update(id, { name });
|
|
223
246
|
console.log(`Renamed ${w.id} → ${w.name}`);
|
|
224
247
|
}
|
|
248
|
+
else if (sub === 'migrate-by-project') {
|
|
249
|
+
return cmdWorkspaceMigrate(rest);
|
|
250
|
+
}
|
|
225
251
|
else if (sub === 'delete') {
|
|
226
252
|
const id = positional(rest)[0];
|
|
227
253
|
if (!id) {
|
|
@@ -375,6 +401,93 @@ async function cmdAgent(args) {
|
|
|
375
401
|
console.error('Usage: stitch agent [register|list|run|dispatch|revoke] …');
|
|
376
402
|
process.exit(2);
|
|
377
403
|
}
|
|
404
|
+
// ── Workspace migration: split a pooled workspace into per-project ones ───
|
|
405
|
+
// Reads every memory in the source workspace, groups by `project:*` tag,
|
|
406
|
+
// and copies each group into a workspace named after the project (creating
|
|
407
|
+
// it if missing), then soft-deletes the source rows. Memories with no
|
|
408
|
+
// `project:*` tag stay put. Idempotent re-runs are safe.
|
|
409
|
+
async function cmdWorkspaceMigrate(args) {
|
|
410
|
+
const cfg = loadConfig();
|
|
411
|
+
if (!cfg.apiKey) {
|
|
412
|
+
console.error('Run `stitch login` first.');
|
|
413
|
+
process.exit(2);
|
|
414
|
+
}
|
|
415
|
+
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
416
|
+
const dryRun = hasFlag(args, ['--dry-run']);
|
|
417
|
+
const stitch = new Stitch({ apiKey: cfg.apiKey, baseUrl });
|
|
418
|
+
const sourceId = parseFlag(args, ['--from']) || (await stitch.workspaces.list())[0]?.id;
|
|
419
|
+
if (!sourceId) {
|
|
420
|
+
console.error('No source workspace found.');
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const source = new Stitch({ apiKey: cfg.apiKey, baseUrl, workspace: sourceId });
|
|
424
|
+
console.log(`Reading memories from ${sourceId}…`);
|
|
425
|
+
const all = [];
|
|
426
|
+
let offset = 0;
|
|
427
|
+
while (true) {
|
|
428
|
+
const batch = await source.list({ limit: 100, offset });
|
|
429
|
+
if (batch.length === 0)
|
|
430
|
+
break;
|
|
431
|
+
all.push(...batch);
|
|
432
|
+
if (batch.length < 100)
|
|
433
|
+
break;
|
|
434
|
+
offset += batch.length;
|
|
435
|
+
}
|
|
436
|
+
console.log(`Found ${all.length} memories.`);
|
|
437
|
+
const byProject = {};
|
|
438
|
+
let untagged = 0;
|
|
439
|
+
for (const m of all) {
|
|
440
|
+
const tag = m.tags.find((t) => t.startsWith('project:'));
|
|
441
|
+
if (!tag) {
|
|
442
|
+
untagged++;
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
const proj = tag.slice('project:'.length).trim();
|
|
446
|
+
if (!proj) {
|
|
447
|
+
untagged++;
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
const slug = slugify(proj);
|
|
451
|
+
(byProject[slug] ??= []).push(m);
|
|
452
|
+
}
|
|
453
|
+
console.log(` ${untagged} untagged → staying in source`);
|
|
454
|
+
for (const [slug, mems] of Object.entries(byProject)) {
|
|
455
|
+
console.log(` ${slug.padEnd(30)} ${mems.length} memories`);
|
|
456
|
+
}
|
|
457
|
+
if (dryRun) {
|
|
458
|
+
console.log('Dry run — nothing written.');
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const allWs = await stitch.workspaces.list();
|
|
462
|
+
let totalMoved = 0;
|
|
463
|
+
for (const [slug, mems] of Object.entries(byProject)) {
|
|
464
|
+
let target = allWs.find((w) => w.name === slug);
|
|
465
|
+
if (!target) {
|
|
466
|
+
target = await stitch.workspaces.create(slug);
|
|
467
|
+
console.log(` + created workspace "${slug}" (${target.id})`);
|
|
468
|
+
allWs.push(target);
|
|
469
|
+
}
|
|
470
|
+
if (target.id === sourceId) {
|
|
471
|
+
console.log(` · ${slug}: target = source — keeping in place`);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const targetClient = new Stitch({ apiKey: cfg.apiKey, baseUrl, workspace: target.id });
|
|
475
|
+
let moved = 0;
|
|
476
|
+
for (const m of mems) {
|
|
477
|
+
try {
|
|
478
|
+
await targetClient.remember(m.content, { kind: m.kind, tags: m.tags, metadata: m.metadata });
|
|
479
|
+
await source.forget(m.id);
|
|
480
|
+
moved++;
|
|
481
|
+
}
|
|
482
|
+
catch (e) {
|
|
483
|
+
console.error(` ! ${m.id}: ${e?.message || e}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
totalMoved += moved;
|
|
487
|
+
console.log(` → ${slug}: moved ${moved}/${mems.length}`);
|
|
488
|
+
}
|
|
489
|
+
console.log(`Migration complete. ${totalMoved} memories moved across ${Object.keys(byProject).length} workspaces.`);
|
|
490
|
+
}
|
|
378
491
|
// ── Threads — append / recall / current ───────────────────────────────────
|
|
379
492
|
function inferThread() {
|
|
380
493
|
// Single source of truth: `.stitch/project.json` if present, else cwd basename.
|
|
@@ -515,6 +628,7 @@ async function cmdHook(args) {
|
|
|
515
628
|
try {
|
|
516
629
|
const stitch = client(cfg);
|
|
517
630
|
const projectTag = (threadName.split('/')[0] || threadName).toLowerCase();
|
|
631
|
+
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
518
632
|
const [thread, memHits, workspaces, fileSummaries, aboutMems] = await Promise.all([
|
|
519
633
|
stitch.thread(threadName).recall({ last: 5 }).catch(() => ({ thread_id: '', recent: [], semantic: [] })),
|
|
520
634
|
stitch.recall(projectTag, { k: 8 }).catch(() => []),
|
|
@@ -522,10 +636,26 @@ async function cmdHook(args) {
|
|
|
522
636
|
stitch.list({ limit: 12 }).then((all) => all.filter((m) => m.tags.some((t) => t.startsWith('file:')))).catch(() => []),
|
|
523
637
|
stitch.list({ tag: 'workspace:about', limit: 1 }).catch(() => []),
|
|
524
638
|
]);
|
|
525
|
-
|
|
639
|
+
// Look up the current workspace using the client's resolved id; fall
|
|
640
|
+
// back to first if resolveWorkspace failed (shouldn't normally).
|
|
641
|
+
const currentWsId = await stitch.resolveWorkspace().catch(() => null);
|
|
642
|
+
const currentWs = (Array.isArray(workspaces) ? workspaces : [])
|
|
643
|
+
.find((w) => w.id === currentWsId) || (Array.isArray(workspaces) ? workspaces[0] : null);
|
|
644
|
+
// Cross-project user-level memories from the special `_global` workspace.
|
|
645
|
+
// Pull top 8 most-recent — these are user preferences / rules that apply
|
|
646
|
+
// EVERYWHERE, regardless of which project this session is in.
|
|
647
|
+
let globalMems = [];
|
|
648
|
+
try {
|
|
649
|
+
const globalWs = (Array.isArray(workspaces) ? workspaces : []).find((w) => w.name === '_global');
|
|
650
|
+
if (globalWs) {
|
|
651
|
+
const globalClient = new Stitch({ apiKey: cfg.apiKey, baseUrl, workspace: globalWs.id });
|
|
652
|
+
globalMems = await globalClient.list({ limit: 8 }).catch(() => []);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
catch { /* ignore */ }
|
|
526
656
|
const lines = [];
|
|
527
657
|
lines.push('<stitch-context>');
|
|
528
|
-
lines.push(`Project: ${threadName} · Workspace: ${currentWs?.name || '(unknown)'} · Stitch MCP tools: recall, remember, thread_recall, thread_append, workspace_setup, file_summary, file_summary_save.`);
|
|
658
|
+
lines.push(`Project: ${threadName} · Workspace: ${currentWs?.name || '(unknown)'} · Stitch MCP tools: recall, remember, recall_global, remember_global, thread_recall, thread_append, workspace_setup, file_summary, file_summary_save.`);
|
|
529
659
|
lines.push('');
|
|
530
660
|
// Nudge the AI to set a meaningful workspace name once.
|
|
531
661
|
if (currentWs?.name === 'default') {
|
|
@@ -533,6 +663,15 @@ async function cmdHook(args) {
|
|
|
533
663
|
lines.push('Call the `workspace_setup` MCP tool with a slug-style name based on this project (e.g. the package.json name or repo dir).');
|
|
534
664
|
lines.push('');
|
|
535
665
|
}
|
|
666
|
+
// Global preferences first — they should bias everything else.
|
|
667
|
+
if (globalMems.length > 0) {
|
|
668
|
+
lines.push('### User-level rules & preferences (apply across all projects)');
|
|
669
|
+
for (const m of globalMems) {
|
|
670
|
+
const txt = String(m.content || '').replace(/\n+/g, ' ').slice(0, 350);
|
|
671
|
+
lines.push(`- **[${m.kind}]** ${txt}`);
|
|
672
|
+
}
|
|
673
|
+
lines.push('');
|
|
674
|
+
}
|
|
536
675
|
if (Array.isArray(aboutMems) && aboutMems.length > 0) {
|
|
537
676
|
lines.push('### About this workspace');
|
|
538
677
|
lines.push(String(aboutMems[0].content || '').slice(0, 400));
|
|
@@ -566,7 +705,7 @@ async function cmdHook(args) {
|
|
|
566
705
|
}
|
|
567
706
|
lines.push('');
|
|
568
707
|
}
|
|
569
|
-
lines.push('Call `recall` for
|
|
708
|
+
lines.push('Call `recall` for project memory, `recall_global` for cross-project user prefs, `thread_recall` for older turns, `file_summary` BEFORE reading any non-trivial file. Save user-level habits/preferences with `remember_global`; project facts with `remember`.');
|
|
570
709
|
lines.push('</stitch-context>');
|
|
571
710
|
process.stdout.write(lines.join('\n'));
|
|
572
711
|
}
|
|
@@ -941,7 +1080,6 @@ async function cmdSummarizeFile(args) {
|
|
|
941
1080
|
return;
|
|
942
1081
|
try {
|
|
943
1082
|
const stitch = client(cfg);
|
|
944
|
-
const projectTag = (inferThread() || 'default').split('/')[0];
|
|
945
1083
|
// Replace any prior summary for this path.
|
|
946
1084
|
const prior = await stitch.list({ tag: `file:${rel}`, limit: 50 }).catch(() => []);
|
|
947
1085
|
for (const p of prior) {
|
|
@@ -950,9 +1088,13 @@ async function cmdSummarizeFile(args) {
|
|
|
950
1088
|
}
|
|
951
1089
|
catch { }
|
|
952
1090
|
}
|
|
1091
|
+
// No project:* tag — the workspace itself is the project identity now.
|
|
1092
|
+
// Adding a project tag based on inferThread() is the bug that caused
|
|
1093
|
+
// cross-workspace contamination when the workspace pin and the thread
|
|
1094
|
+
// pin disagreed.
|
|
953
1095
|
await stitch.remember(summary, {
|
|
954
1096
|
kind: 'snippet',
|
|
955
|
-
tags: [`file:${rel}`, `hash:${hashPrefix}`, 'auto:file-summary'
|
|
1097
|
+
tags: [`file:${rel}`, `hash:${hashPrefix}`, 'auto:file-summary'],
|
|
956
1098
|
});
|
|
957
1099
|
}
|
|
958
1100
|
catch { /* silent — never break a session */ }
|
|
@@ -1096,12 +1238,12 @@ async function cmdDistill(args) {
|
|
|
1096
1238
|
bumpDistillCooldown(thread);
|
|
1097
1239
|
return;
|
|
1098
1240
|
}
|
|
1099
|
-
// Push each to Stitch as a memory with auto:true tag.
|
|
1100
|
-
|
|
1241
|
+
// Push each to Stitch as a memory with auto:true tag. No project:* tag —
|
|
1242
|
+
// the workspace itself is the project identity.
|
|
1101
1243
|
let saved = 0;
|
|
1102
1244
|
for (const m of memories) {
|
|
1103
1245
|
try {
|
|
1104
|
-
const tags = ['auto', 'auto:distill', `thread:${thread}`,
|
|
1246
|
+
const tags = ['auto', 'auto:distill', `thread:${thread}`, ...(m.tags || [])];
|
|
1105
1247
|
await stitch.remember(m.content, { kind: m.kind, tags });
|
|
1106
1248
|
saved++;
|
|
1107
1249
|
}
|
|
@@ -1546,39 +1688,68 @@ async function cmdInstall(args) {
|
|
|
1546
1688
|
console.error('Run `stitch login` first.');
|
|
1547
1689
|
process.exit(2);
|
|
1548
1690
|
}
|
|
1549
|
-
const stitch = client(cfg);
|
|
1550
|
-
const ws = await stitch.resolveWorkspace();
|
|
1551
1691
|
const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
|
|
1552
|
-
|
|
1553
|
-
//
|
|
1554
|
-
//
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
}
|
|
1692
|
+
// Use a "raw" SDK client (no project pin) for the bootstrap so we can list
|
|
1693
|
+
// and create workspaces freely. After this we write .stitch/project.json
|
|
1694
|
+
// and subsequent commands auto-route through the per-project workspace.
|
|
1695
|
+
const rawStitch = new Stitch({ apiKey: cfg.apiKey, baseUrl });
|
|
1696
|
+
// Step 0 — resolve the per-project workspace ─────────────────────────────
|
|
1697
|
+
const projectName = deriveWorkspaceName(process.cwd());
|
|
1698
|
+
process.stdout.write(`• Resolving workspace "${projectName}" for this project… `);
|
|
1699
|
+
let ws;
|
|
1700
|
+
try {
|
|
1701
|
+
const list = await rawStitch.workspaces.list();
|
|
1702
|
+
let target = list.find((w) => w.name === projectName);
|
|
1703
|
+
if (!target) {
|
|
1704
|
+
target = await rawStitch.workspaces.create(projectName);
|
|
1705
|
+
console.log(`created (${target.id})`);
|
|
1567
1706
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
console.log(`• Workspace rename skipped (${e?.message || e}).`);
|
|
1707
|
+
else {
|
|
1708
|
+
console.log(`found (${target.id})`);
|
|
1571
1709
|
}
|
|
1710
|
+
ws = target.id;
|
|
1572
1711
|
}
|
|
1712
|
+
catch (e) {
|
|
1713
|
+
console.log(`failed (${e?.message || e})`);
|
|
1714
|
+
process.exit(1);
|
|
1715
|
+
}
|
|
1716
|
+
// Step 0.5 — write .stitch/project.json so every CLI invocation in this
|
|
1717
|
+
// tree (incl. hooks) routes to the right workspace automatically.
|
|
1718
|
+
try {
|
|
1719
|
+
const dir = path.join(process.cwd(), '.stitch');
|
|
1720
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1721
|
+
const file = path.join(dir, 'project.json');
|
|
1722
|
+
let pj = {};
|
|
1723
|
+
try {
|
|
1724
|
+
pj = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1725
|
+
}
|
|
1726
|
+
catch { }
|
|
1727
|
+
pj.thread = pj.thread || projectName;
|
|
1728
|
+
pj.workspace_id = ws;
|
|
1729
|
+
pj.workspace_name = projectName;
|
|
1730
|
+
pj.linked_at = pj.linked_at || new Date().toISOString();
|
|
1731
|
+
fs.writeFileSync(file, JSON.stringify(pj, null, 2));
|
|
1732
|
+
console.log(`• Pinned project to workspace "${projectName}" (${file})`);
|
|
1733
|
+
}
|
|
1734
|
+
catch (e) {
|
|
1735
|
+
console.log(`• .stitch/project.json write skipped (${e?.message || e})`);
|
|
1736
|
+
}
|
|
1737
|
+
const mcpUrl = `${baseUrl}/mcp/v1/${ws}`;
|
|
1573
1738
|
const noMcp = hasFlag(args, ['--no-mcp']);
|
|
1574
1739
|
const noHooks = hasFlag(args, ['--no-hooks']);
|
|
1575
1740
|
const noClaudeMd = hasFlag(args, ['--no-claude-md']);
|
|
1576
|
-
// 1. claude mcp add
|
|
1741
|
+
// 1. claude mcp add — LOCAL scope (per-user-per-project) so each project's
|
|
1742
|
+
// Claude session loads only its own workspace's tools, not every project's.
|
|
1743
|
+
// If a stale entry exists with a different URL we replace it, since this
|
|
1744
|
+
// project's workspace_id may have changed (e.g. cloned to a new account).
|
|
1577
1745
|
if (!noMcp) {
|
|
1578
1746
|
const claudePath = process.env.STITCH_CLAUDE_BIN || 'claude';
|
|
1579
|
-
process.stdout.write('•
|
|
1747
|
+
process.stdout.write('• Registering Stitch MCP for this project (local scope)… ');
|
|
1748
|
+
// Best-effort remove of any prior local-scope `stitch` entry; ignore errors
|
|
1749
|
+
// (it's fine if there wasn't one).
|
|
1750
|
+
await runSilent(claudePath, ['mcp', 'remove', '--scope', 'local', 'stitch']).catch(() => { });
|
|
1580
1751
|
const { exit_code, stderr } = await runSilent(claudePath, [
|
|
1581
|
-
'mcp', 'add', '--transport', 'http', '--scope', '
|
|
1752
|
+
'mcp', 'add', '--transport', 'http', '--scope', 'local', 'stitch', mcpUrl,
|
|
1582
1753
|
'-H', `Authorization: Bearer ${cfg.apiKey}`,
|
|
1583
1754
|
]);
|
|
1584
1755
|
if (exit_code === 0)
|
|
@@ -1992,6 +2163,10 @@ function help() {
|
|
|
1992
2163
|
for the current repo / cwd.
|
|
1993
2164
|
|
|
1994
2165
|
stitch workspace [list | create <name> | use <id> | rename <id> <name> | delete <id> --yes]
|
|
2166
|
+
stitch workspace migrate-by-project [--from <id>] [--dry-run]
|
|
2167
|
+
Split memories from one pooled
|
|
2168
|
+
workspace into per-project
|
|
2169
|
+
workspaces by their project:* tag.
|
|
1995
2170
|
|
|
1996
2171
|
stitch agent register <name> Create an agent identity (id only).
|
|
1997
2172
|
stitch agent list
|