@stitchdb/cli 0.7.4 → 0.8.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.
Files changed (2) hide show
  1. package/dist/cli.js +170 -24
  2. 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.
@@ -1546,39 +1659,68 @@ async function cmdInstall(args) {
1546
1659
  console.error('Run `stitch login` first.');
1547
1660
  process.exit(2);
1548
1661
  }
1549
- const stitch = client(cfg);
1550
- const ws = await stitch.resolveWorkspace();
1551
1662
  const baseUrl = cfg.baseUrl || 'https://db.stitchdb.com';
1552
- const mcpUrl = `${baseUrl}/mcp/v1/${ws}`;
1553
- // 0. If the workspace is still called "default", upgrade its name to a
1554
- // project-derived slug so memories surface under a meaningful identity.
1555
- // The AI can later refine it via the workspace_setup MCP tool.
1556
- if (!hasFlag(args, ['--no-rename-workspace'])) {
1557
- try {
1558
- const current = await stitch.workspaces.get(ws);
1559
- if (current.name === 'default') {
1560
- const derived = deriveWorkspaceName(process.cwd());
1561
- if (derived && derived !== 'default') {
1562
- process.stdout.write(`• Naming workspace "${derived}" (was "default")… `);
1563
- await stitch.workspaces.update(ws, { name: derived });
1564
- console.log('ok');
1565
- }
1566
- }
1663
+ // Use a "raw" SDK client (no project pin) for the bootstrap so we can list
1664
+ // and create workspaces freely. After this we write .stitch/project.json
1665
+ // and subsequent commands auto-route through the per-project workspace.
1666
+ const rawStitch = new Stitch({ apiKey: cfg.apiKey, baseUrl });
1667
+ // Step 0 — resolve the per-project workspace ─────────────────────────────
1668
+ const projectName = deriveWorkspaceName(process.cwd());
1669
+ process.stdout.write(`• Resolving workspace "${projectName}" for this project… `);
1670
+ let ws;
1671
+ try {
1672
+ const list = await rawStitch.workspaces.list();
1673
+ let target = list.find((w) => w.name === projectName);
1674
+ if (!target) {
1675
+ target = await rawStitch.workspaces.create(projectName);
1676
+ console.log(`created (${target.id})`);
1567
1677
  }
1568
- catch (e) {
1569
- // Older deployments may not have the PATCH endpoint yet.
1570
- console.log(`• Workspace rename skipped (${e?.message || e}).`);
1678
+ else {
1679
+ console.log(`found (${target.id})`);
1680
+ }
1681
+ ws = target.id;
1682
+ }
1683
+ catch (e) {
1684
+ console.log(`failed (${e?.message || e})`);
1685
+ process.exit(1);
1686
+ }
1687
+ // Step 0.5 — write .stitch/project.json so every CLI invocation in this
1688
+ // tree (incl. hooks) routes to the right workspace automatically.
1689
+ try {
1690
+ const dir = path.join(process.cwd(), '.stitch');
1691
+ fs.mkdirSync(dir, { recursive: true });
1692
+ const file = path.join(dir, 'project.json');
1693
+ let pj = {};
1694
+ try {
1695
+ pj = JSON.parse(fs.readFileSync(file, 'utf8'));
1571
1696
  }
1697
+ catch { }
1698
+ pj.thread = pj.thread || projectName;
1699
+ pj.workspace_id = ws;
1700
+ pj.workspace_name = projectName;
1701
+ pj.linked_at = pj.linked_at || new Date().toISOString();
1702
+ fs.writeFileSync(file, JSON.stringify(pj, null, 2));
1703
+ console.log(`• Pinned project to workspace "${projectName}" (${file})`);
1704
+ }
1705
+ catch (e) {
1706
+ console.log(`• .stitch/project.json write skipped (${e?.message || e})`);
1572
1707
  }
1708
+ const mcpUrl = `${baseUrl}/mcp/v1/${ws}`;
1573
1709
  const noMcp = hasFlag(args, ['--no-mcp']);
1574
1710
  const noHooks = hasFlag(args, ['--no-hooks']);
1575
1711
  const noClaudeMd = hasFlag(args, ['--no-claude-md']);
1576
- // 1. claude mcp add
1712
+ // 1. claude mcp add — LOCAL scope (per-user-per-project) so each project's
1713
+ // Claude session loads only its own workspace's tools, not every project's.
1714
+ // If a stale entry exists with a different URL we replace it, since this
1715
+ // project's workspace_id may have changed (e.g. cloned to a new account).
1577
1716
  if (!noMcp) {
1578
1717
  const claudePath = process.env.STITCH_CLAUDE_BIN || 'claude';
1579
- process.stdout.write('• Adding Stitch as an MCP server in Claude Code… ');
1718
+ process.stdout.write('• Registering Stitch MCP for this project (local scope)… ');
1719
+ // Best-effort remove of any prior local-scope `stitch` entry; ignore errors
1720
+ // (it's fine if there wasn't one).
1721
+ await runSilent(claudePath, ['mcp', 'remove', '--scope', 'local', 'stitch']).catch(() => { });
1580
1722
  const { exit_code, stderr } = await runSilent(claudePath, [
1581
- 'mcp', 'add', '--transport', 'http', '--scope', 'user', 'stitch', mcpUrl,
1723
+ 'mcp', 'add', '--transport', 'http', '--scope', 'local', 'stitch', mcpUrl,
1582
1724
  '-H', `Authorization: Bearer ${cfg.apiKey}`,
1583
1725
  ]);
1584
1726
  if (exit_code === 0)
@@ -1992,6 +2134,10 @@ function help() {
1992
2134
  for the current repo / cwd.
1993
2135
 
1994
2136
  stitch workspace [list | create <name> | use <id> | rename <id> <name> | delete <id> --yes]
2137
+ stitch workspace migrate-by-project [--from <id>] [--dry-run]
2138
+ Split memories from one pooled
2139
+ workspace into per-project
2140
+ workspaces by their project:* tag.
1995
2141
 
1996
2142
  stitch agent register <name> Create an agent identity (id only).
1997
2143
  stitch agent list
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stitchdb/cli",
3
- "version": "0.7.4",
3
+ "version": "0.8.0",
4
4
  "description": "Stitch CLI — manage memory + run agents from your terminal",
5
5
  "type": "module",
6
6
  "bin": {