sticky-note-cli 3.0.0 → 3.1.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/bin/cli.js CHANGED
@@ -204,118 +204,6 @@ function makeExecutable(filePath) {
204
204
  }
205
205
  }
206
206
 
207
- const STICKY_PRE_COMMIT_MARKER = "# [sticky-note:auto-stage]";
208
- const STICKY_PRE_COMMIT_END = "# [/sticky-note:auto-stage]";
209
-
210
- /**
211
- * Install a pre-commit git hook that auto-stages .sticky-note/ tracked files.
212
- * If an existing pre-commit hook is present, our block is prepended (or updated)
213
- * so it chains cleanly with linters, husky, etc.
214
- */
215
- function installPreCommitHook() {
216
- try {
217
- const gitDir = path.join(process.cwd(), ".git");
218
- if (!fs.existsSync(gitDir)) return false;
219
- const hooksDir = path.join(gitDir, "hooks");
220
- mkdirSafe(hooksDir);
221
- const dest = path.join(hooksDir, "pre-commit");
222
-
223
- const stickyBlock = [
224
- STICKY_PRE_COMMIT_MARKER,
225
- '# Auto-stage .sticky-note/ files so they ride along with every commit.',
226
- '_sn_root="$(git rev-parse --show-toplevel 2>/dev/null)"',
227
- 'if [ -d "$_sn_root/.sticky-note" ]; then',
228
- ' [ -f "$_sn_root/.sticky-note/sticky-note.json" ] && git add "$_sn_root/.sticky-note/sticky-note.json" 2>/dev/null || true',
229
- ' [ -f "$_sn_root/.sticky-note/merge-driver.js" ] && git add "$_sn_root/.sticky-note/merge-driver.js" 2>/dev/null || true',
230
- ' [ -d "$_sn_root/.sticky-note/audit" ] && git add "$_sn_root/.sticky-note/audit/" 2>/dev/null || true',
231
- ' [ -d "$_sn_root/.sticky-note/presence" ] && git add "$_sn_root/.sticky-note/presence/" 2>/dev/null || true',
232
- ' [ -d "$_sn_root/.sticky-note/environment" ] && git add "$_sn_root/.sticky-note/environment/" 2>/dev/null || true',
233
- 'fi',
234
- STICKY_PRE_COMMIT_END,
235
- ].join("\n");
236
-
237
- if (fs.existsSync(dest)) {
238
- let existing = fs.readFileSync(dest, "utf-8");
239
- const startIdx = existing.indexOf(STICKY_PRE_COMMIT_MARKER);
240
- const endIdx = existing.indexOf(STICKY_PRE_COMMIT_END);
241
-
242
- if (startIdx !== -1 && endIdx !== -1) {
243
- // Update our existing block in-place
244
- existing = existing.slice(0, startIdx) + stickyBlock + existing.slice(endIdx + STICKY_PRE_COMMIT_END.length);
245
- } else {
246
- // Prepend our block after the shebang (or at the top)
247
- const shebangMatch = existing.match(/^#!.*\n/);
248
- if (shebangMatch) {
249
- existing = shebangMatch[0] + stickyBlock + "\n" + existing.slice(shebangMatch[0].length);
250
- } else {
251
- existing = "#!/bin/sh\n" + stickyBlock + "\n" + existing;
252
- }
253
- }
254
- fs.writeFileSync(dest, existing, "utf-8");
255
- } else {
256
- // No existing hook — create a fresh one
257
- fs.writeFileSync(dest, "#!/bin/sh\n" + stickyBlock + "\n", "utf-8");
258
- }
259
- makeExecutable(dest);
260
- return true;
261
- } catch (err) {
262
- debugLog("installPreCommitHook failed: " + (err.message || err));
263
- return false;
264
- }
265
- }
266
-
267
- const STICKY_POST_COMMIT_MARKER = "# [sticky-note:auto-sync]";
268
- const STICKY_POST_COMMIT_END = "# [/sticky-note:auto-sync]";
269
-
270
- /**
271
- * Install a post-commit git hook that auto-commits leftover .sticky-note/ changes.
272
- * Uses the same prepend/update strategy as installPreCommitHook.
273
- */
274
- function installPostCommitHook() {
275
- try {
276
- const gitDir = path.join(process.cwd(), ".git");
277
- if (!fs.existsSync(gitDir)) return false;
278
- const hooksDir = path.join(gitDir, "hooks");
279
- mkdirSafe(hooksDir);
280
- const dest = path.join(hooksDir, "post-commit");
281
- const src = path.join(TEMPLATES_DIR, "hooks", "post-commit");
282
- const template = fs.existsSync(src) ? fs.readFileSync(src, "utf-8") : "";
283
-
284
- // Extract just the body (skip shebang + first comment line from template)
285
- const bodyLines = template.split("\n").filter(l => !l.startsWith("#!/"));
286
- const stickyBlock = [
287
- STICKY_POST_COMMIT_MARKER,
288
- ...bodyLines,
289
- STICKY_POST_COMMIT_END,
290
- ].join("\n");
291
-
292
- if (fs.existsSync(dest)) {
293
- let existing = fs.readFileSync(dest, "utf-8");
294
- const startIdx = existing.indexOf(STICKY_POST_COMMIT_MARKER);
295
- const endIdx = existing.indexOf(STICKY_POST_COMMIT_END);
296
-
297
- if (startIdx !== -1 && endIdx !== -1) {
298
- existing = existing.slice(0, startIdx) + stickyBlock + existing.slice(endIdx + STICKY_POST_COMMIT_END.length);
299
- } else {
300
- const shebangMatch = existing.match(/^#!.*\n/);
301
- if (shebangMatch) {
302
- existing = shebangMatch[0] + stickyBlock + "\n" + existing.slice(shebangMatch[0].length);
303
- } else {
304
- existing = "#!/bin/sh\n" + stickyBlock + "\n" + existing;
305
- }
306
- }
307
- fs.writeFileSync(dest, existing, "utf-8");
308
- } else {
309
- fs.writeFileSync(dest, "#!/bin/sh\n" + stickyBlock + "\n", "utf-8");
310
- }
311
- makeExecutable(dest);
312
- return true;
313
- } catch (err) {
314
- debugLog("installPostCommitHook failed: " + (err.message || err));
315
- return false;
316
- }
317
- }
318
-
319
207
  function installGitHook(hookName) {
320
208
  try {
321
209
  const gitDir = path.join(process.cwd(), ".git");
@@ -647,6 +535,10 @@ async function syncMcpFromStickyNote(rl) {
647
535
  // ──────────────────────────────────────────────
648
536
 
649
537
  async function cmdInit() {
538
+ const args = process.argv.slice(3);
539
+ const ciMode = args.includes("--ci") || args.includes("--no-prompts");
540
+ const v3Mode = args.includes("--v3");
541
+
650
542
  printBanner();
651
543
 
652
544
  // Preflight checks
@@ -660,71 +552,112 @@ async function cmdInit() {
660
552
  print("");
661
553
 
662
554
  // Auto-detect MCP servers and skills
663
- print(" 🔍 Scanning for existing configuration...");
555
+ print(" Scanning for existing configuration...");
664
556
  const detectedMcp = detectMcpServers();
665
557
  const detectedSkills = detectSkills();
666
558
 
667
559
  if (detectedMcp.length > 0) {
668
- print(` 📡 Found ${detectedMcp.length} MCP server(s):`);
560
+ print(` Found ${detectedMcp.length} MCP server(s):`);
669
561
  for (const s of detectedMcp) {
670
- print(` • ${s.name} (${s.source})`);
562
+ print(` - ${s.name} (${s.source})`);
671
563
  }
672
564
  }
673
565
  if (detectedSkills.length > 0) {
674
- print(` 🧩 Found ${detectedSkills.length} skill(s):`);
566
+ print(` Found ${detectedSkills.length} skill(s):`);
675
567
  for (const s of detectedSkills) {
676
- print(` • ${s}`);
568
+ print(` - ${s}`);
677
569
  }
678
570
  }
679
571
  if (detectedMcp.length === 0 && detectedSkills.length === 0) {
680
- print(" ⏭️ No existing MCP servers or skills detected");
572
+ print(" No existing MCP servers or skills detected");
681
573
  }
682
574
  print("");
683
575
 
684
- // Interactive prompts
685
- const rl = readline.createInterface({
686
- input: process.stdin,
687
- output: process.stdout,
688
- });
576
+ let mcpServers, skills, conventions, staleDaysResolved, injectTokenBudgetResolved;
689
577
 
690
- print(" 📋 Team Configuration");
691
- print(" (Press Enter to accept detected defaults)\n");
578
+ if (ciMode) {
579
+ // CI mode: no prompts, use env vars
580
+ const mcpRaw = process.env.STICKY_MCP_SERVERS || "";
581
+ const convRaw = process.env.STICKY_CONVENTIONS || "";
582
+ const staleDaysRaw = process.env.STICKY_STALE_DAYS || "14";
692
583
 
693
- const mcpDefault = detectedMcp.map((s) => s.name).join(", ");
694
- const mcpServersRaw = await ask(
695
- rl,
696
- "MCP servers (comma-separated)",
697
- mcpDefault || ""
698
- );
699
- const conventionsRaw = await ask(
700
- rl,
701
- "Team conventions (comma-separated)",
702
- ""
703
- );
704
- const staleDays = await ask(rl, "Stale thread age in days", "14");
705
- const injectTokenBudget = await ask(rl, "Inject context token budget", "1000");
584
+ mcpServers = detectedMcp.slice();
585
+ if (mcpRaw) {
586
+ try {
587
+ const parsed = JSON.parse(mcpRaw);
588
+ if (Array.isArray(parsed)) mcpServers = parsed;
589
+ } catch (_) {
590
+ mcpRaw.split(",").map(s => s.trim()).filter(Boolean).forEach(name => {
591
+ mcpServers.push({ name, source: "ci-env" });
592
+ });
593
+ }
594
+ }
595
+ skills = [...detectedSkills];
596
+ conventions = convRaw ? convRaw.split(",").map(s => s.trim()).filter(Boolean) : [];
597
+ staleDaysResolved = parseIntOr(staleDaysRaw, 14);
598
+ injectTokenBudgetResolved = 1000;
599
+ } else {
600
+ // Interactive prompts
601
+ const rl = readline.createInterface({
602
+ input: process.stdin,
603
+ output: process.stdout,
604
+ });
706
605
 
707
- // Sync MCP servers from teammate's sticky-note config
708
- await syncMcpFromStickyNote(rl);
606
+ print(" Team Configuration");
607
+ print(" (Press Enter to accept detected defaults)\n");
709
608
 
710
- rl.close();
609
+ const mcpDefault = detectedMcp.map((s) => s.name).join(", ");
610
+ const mcpServersRaw = await ask(
611
+ rl,
612
+ "MCP servers (comma-separated)",
613
+ mcpDefault || ""
614
+ );
615
+ const conventionsRaw = await ask(
616
+ rl,
617
+ "Team conventions (comma-separated)",
618
+ ""
619
+ );
620
+ const staleDays = await ask(rl, "Stale thread age in days", "14");
621
+ const injectTokenBudget = await ask(rl, "Inject context token budget", "1000");
711
622
 
712
- // Build MCP server list: prefer detected objects, fall back to names
713
- const mcpNames = mcpServersRaw
714
- ? mcpServersRaw.split(",").map((s) => s.trim()).filter(Boolean)
715
- : [];
716
- const mcpServers = mcpNames.map((name) => {
717
- const detected = detectedMcp.find((d) => d.name === name);
718
- return detected || { name, source: "manual" };
719
- });
623
+ // Sync MCP servers from teammate's sticky-note config
624
+ await syncMcpFromStickyNote(rl);
720
625
 
721
- const skills = [...detectedSkills];
626
+ rl.close();
722
627
 
723
- const conventions = conventionsRaw
724
- ? conventionsRaw.split(",").map((s) => s.trim()).filter(Boolean)
725
- : [];
726
- const staleDaysResolved = parseIntOr(staleDays, 14);
727
- const injectTokenBudgetResolved = parseIntOr(injectTokenBudget, 1000);
628
+ // Build MCP server list: prefer detected objects, fall back to names
629
+ const mcpNames = mcpServersRaw
630
+ ? mcpServersRaw.split(",").map((s) => s.trim()).filter(Boolean)
631
+ : [];
632
+ mcpServers = mcpNames.map((name) => {
633
+ const detected = detectedMcp.find((d) => d.name === name);
634
+ return detected || { name, source: "manual" };
635
+ });
636
+
637
+ skills = [...detectedSkills];
638
+
639
+ conventions = conventionsRaw
640
+ ? conventionsRaw.split(",").map((s) => s.trim()).filter(Boolean)
641
+ : [];
642
+ staleDaysResolved = parseIntOr(staleDays, 14);
643
+ injectTokenBudgetResolved = parseIntOr(injectTokenBudget, 1000);
644
+ }
645
+
646
+ // V3: write .env.sticky if STICKY_URL is available
647
+ if (v3Mode || process.env.STICKY_URL) {
648
+ const envStickyPath = path.join(process.cwd(), ".env.sticky");
649
+ const stickyUrl = process.env.STICKY_URL || "";
650
+ const stickyApiKey = process.env.STICKY_API_KEY || "";
651
+ if (stickyUrl) {
652
+ let content = `STICKY_URL=${stickyUrl}\n`;
653
+ if (stickyApiKey) content += `STICKY_API_KEY=${stickyApiKey}\n`;
654
+ fs.writeFileSync(envStickyPath, content, "utf-8");
655
+ print(" [OK] .env.sticky (cloud backend configured)");
656
+ } else if (v3Mode && !ciMode) {
657
+ print(" [WARN] --v3 specified but STICKY_URL not set.");
658
+ print(" Run `npx sticky-note deploy-backend` to provision a cloud backend.");
659
+ }
660
+ }
728
661
 
729
662
  print("\n 📁 Creating files...\n");
730
663
 
@@ -1418,6 +1351,76 @@ function cmdStatus() {
1418
1351
  ` ${isGitRepo() ? "[OK]" : "[ERR]"} Git repository`
1419
1352
  );
1420
1353
 
1354
+ // V3: Cloud backend health
1355
+ const envStickyPath = path.join(process.cwd(), ".env.sticky");
1356
+ print("\n Cloud Backend (V3):");
1357
+ if (fs.existsSync(envStickyPath)) {
1358
+ try {
1359
+ const envRaw = fs.readFileSync(envStickyPath, "utf-8");
1360
+ let stickyUrl = "";
1361
+ for (const line of envRaw.split(/\r?\n/)) {
1362
+ const trimmed = line.trim();
1363
+ if (trimmed.startsWith("STICKY_URL=")) {
1364
+ stickyUrl = trimmed.slice("STICKY_URL=".length).trim();
1365
+ }
1366
+ }
1367
+ if (stickyUrl) {
1368
+ print(` [OK] .env.sticky found (${stickyUrl})`);
1369
+ try {
1370
+ const http = require(stickyUrl.startsWith("https") ? "https" : "http");
1371
+ const healthCheck = new Promise((resolve) => {
1372
+ const req = http.get(`${stickyUrl}/health`, { timeout: 5000 }, (res) => {
1373
+ let body = "";
1374
+ res.on("data", (chunk) => body += chunk);
1375
+ res.on("end", () => {
1376
+ if (res.statusCode === 200) {
1377
+ resolve({ ok: true, body });
1378
+ } else {
1379
+ resolve({ ok: false, status: res.statusCode });
1380
+ }
1381
+ });
1382
+ });
1383
+ req.on("error", (err) => resolve({ ok: false, error: err.message }));
1384
+ req.on("timeout", () => { req.destroy(); resolve({ ok: false, error: "timeout" }); });
1385
+ });
1386
+ const result = healthCheck;
1387
+ // Synchronous-friendly: use execFileSync with curl/node to check
1388
+ try {
1389
+ const healthResult = execFileSync("node", ["-e", `
1390
+ const url = ${JSON.stringify(stickyUrl + "/health")};
1391
+ const mod = url.startsWith("https") ? require("https") : require("http");
1392
+ const req = mod.get(url, {timeout: 5000}, (res) => {
1393
+ let b = "";
1394
+ res.on("data", c => b += c);
1395
+ res.on("end", () => {
1396
+ if (res.statusCode === 200) process.stdout.write("OK:" + b);
1397
+ else process.stdout.write("ERR:" + res.statusCode);
1398
+ });
1399
+ });
1400
+ req.on("error", e => process.stdout.write("ERR:" + e.message));
1401
+ req.on("timeout", () => { req.destroy(); process.stdout.write("ERR:timeout"); });
1402
+ `], { encoding: "utf-8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] }).trim();
1403
+ if (healthResult.startsWith("OK:")) {
1404
+ print(` [OK] Backend reachable`);
1405
+ } else {
1406
+ print(` [ERR] Backend unreachable (${healthResult.slice(4)})`);
1407
+ }
1408
+ } catch (_) {
1409
+ print(` [ERR] Backend unreachable (health check failed)`);
1410
+ }
1411
+ } catch (_) {
1412
+ print(` [ERR] Backend health check failed`);
1413
+ }
1414
+ } else {
1415
+ print(" [--] .env.sticky found but STICKY_URL not set");
1416
+ }
1417
+ } catch (_) {
1418
+ print(" [ERR] .env.sticky unreadable");
1419
+ }
1420
+ } else {
1421
+ print(" [--] No .env.sticky (local-only mode, V2.5 compatible)");
1422
+ }
1423
+
1421
1424
  // V2.5: Attribution engine health
1422
1425
  print("\n Attribution Engine (V2.5):");
1423
1426
  const attrPath = path.join(process.cwd(), ".claude", "hooks", "sticky-attribution.js");
@@ -1651,6 +1654,138 @@ function cmdResume() {
1651
1654
  print(" Run `npx sticky-note resume --clear` to cancel.\n");
1652
1655
  }
1653
1656
 
1657
+ // ──────────────────────────────────────────────
1658
+ // TRANSCRIPT command
1659
+ // ──────────────────────────────────────────────
1660
+
1661
+ /**
1662
+ * Extract human-readable "[role] text" lines from a raw session transcript
1663
+ * (Claude Code JSONL format, with a fallback for Copilot CLI-style hook
1664
+ * entries). Tool use/results are summarized rather than dumped in full —
1665
+ * pass --raw to cmdTranscript to see the untouched JSONL instead.
1666
+ */
1667
+ function formatTranscriptText(rawTranscript) {
1668
+ const lines = (rawTranscript || "").split(/\r?\n/).filter((l) => l.trim());
1669
+ const out = [];
1670
+
1671
+ for (const line of lines) {
1672
+ let entry;
1673
+ try { entry = JSON.parse(line); } catch (_) { continue; }
1674
+
1675
+ const message = entry.message;
1676
+ if (message && typeof message === "object" && !Array.isArray(message)) {
1677
+ const role = message.role || entry.role || "unknown";
1678
+ const content = message.content;
1679
+ const blocks = Array.isArray(content) ? content : [content];
1680
+
1681
+ for (const block of blocks) {
1682
+ if (typeof block === "string") {
1683
+ if (block.trim()) out.push(`[${role}] ${block.trim()}`);
1684
+ } else if (block && typeof block === "object") {
1685
+ if (block.type === "text" && block.text) {
1686
+ out.push(`[${role}] ${block.text.trim()}`);
1687
+ } else if (block.type === "tool_use") {
1688
+ out.push(`[${role}] → used tool: ${block.name || "?"}`);
1689
+ } else if (block.type === "tool_result") {
1690
+ const resultText = typeof block.content === "string"
1691
+ ? block.content
1692
+ : Array.isArray(block.content)
1693
+ ? block.content.filter((c) => c && typeof c === "object" && c.type === "text").map((c) => c.text || "").join(" ")
1694
+ : "";
1695
+ if (resultText.trim()) {
1696
+ out.push(`[tool result]${block.is_error ? " (error)" : ""} ${resultText.trim().slice(0, 300)}`);
1697
+ }
1698
+ }
1699
+ }
1700
+ }
1701
+ continue;
1702
+ }
1703
+
1704
+ // Copilot CLI-style hook entries
1705
+ if (entry.tool_name) {
1706
+ out.push(`[tool] ${entry.tool_name}`);
1707
+ }
1708
+ }
1709
+
1710
+ return out;
1711
+ }
1712
+
1713
+ function _loadStickyUtils() {
1714
+ try {
1715
+ return require(path.join(__dirname, "..", "templates", "hooks", "sticky-utils.js"));
1716
+ } catch (_) {
1717
+ try {
1718
+ return require(path.join(process.cwd(), ".claude", "hooks", "sticky-utils.js"));
1719
+ } catch (_) {
1720
+ return null;
1721
+ }
1722
+ }
1723
+ }
1724
+
1725
+ function cmdTranscript() {
1726
+ printBanner();
1727
+ const args = process.argv.slice(3);
1728
+ const raw = args.includes("--raw");
1729
+ const threadIdArg = args.find((a) => !a.startsWith("--"));
1730
+
1731
+ const utils = _loadStickyUtils();
1732
+ if (!utils) {
1733
+ print(" [ERR] Could not load sticky-utils.js. Run `npx sticky-note init` first.");
1734
+ process.exit(1);
1735
+ }
1736
+
1737
+ const memory = utils.loadJson(utils.getMemoryPath(), { threads: [] });
1738
+ const threads = (memory.threads || []).filter(Boolean);
1739
+
1740
+ if (!threadIdArg || args.includes("--list")) {
1741
+ const withTranscripts = threads.filter((t) => t.transcript_captured);
1742
+ if (!withTranscripts.length) {
1743
+ print(" No threads have a captured transcript yet.");
1744
+ print(" (Check capture_transcripts in sticky-note-config.json if you expected one.)");
1745
+ return;
1746
+ }
1747
+ print("\n Threads with a captured transcript:\n");
1748
+ for (const t of withTranscripts) {
1749
+ const note = t.last_note ? ` — ${t.last_note.slice(0, 60)}` : "";
1750
+ print(` ${t.id.slice(0, 8)} ${t.user || "?"}${note}`);
1751
+ }
1752
+ print(`\n Usage: npx sticky-note transcript <thread-id> [--raw]\n`);
1753
+ return;
1754
+ }
1755
+
1756
+ const match = threads.find((t) => t.id === threadIdArg || t.id.startsWith(threadIdArg));
1757
+ if (!match) {
1758
+ print(` [ERR] No thread found matching "${threadIdArg}".`);
1759
+ print(" Run `npx sticky-note transcript --list` to see threads with a captured transcript.");
1760
+ process.exit(1);
1761
+ }
1762
+
1763
+ const filePath = utils.getTranscriptPath(match.id);
1764
+ if (!fs.existsSync(filePath)) {
1765
+ print(` No transcript captured for thread ${match.id.slice(0, 8)}.`);
1766
+ print(" (capture_transcripts may be disabled, or this thread predates the feature.)");
1767
+ return;
1768
+ }
1769
+
1770
+ const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter((l) => l.trim());
1771
+ print(`\n ── Transcript for thread ${match.id.slice(0, 8)} (${lines.length} session${lines.length === 1 ? "" : "s"}) ──\n`);
1772
+
1773
+ for (const line of lines) {
1774
+ let entry;
1775
+ try { entry = JSON.parse(line); } catch (_) { continue; }
1776
+ const shortSession = (entry.session_id || "?").slice(0, 8);
1777
+ print(` ── Session ${shortSession} · ${entry.tool || "?"} · ${entry.captured_at || "?"}${entry.redacted ? " · redacted" : ""} ──`);
1778
+ if (raw) {
1779
+ print(entry.transcript || "");
1780
+ } else {
1781
+ const formatted = formatTranscriptText(entry.transcript);
1782
+ if (!formatted.length) print(" (no readable turns found — try --raw)");
1783
+ for (const f of formatted) print(" " + f);
1784
+ }
1785
+ print("");
1786
+ }
1787
+ }
1788
+
1654
1789
  // ──────────────────────────────────────────────
1655
1790
  // AUDIT command
1656
1791
  // ──────────────────────────────────────────────
@@ -2720,6 +2855,493 @@ function cmdGc() {
2720
2855
  print("");
2721
2856
  }
2722
2857
 
2858
+ // ──────────────────────────────────────────────
2859
+ // DEPLOY-BACKEND command (V3)
2860
+ // ──────────────────────────────────────────────
2861
+
2862
+ function cmdDeployBackend() {
2863
+ printBanner();
2864
+ print(" Deploy Cloudflare KV Backend\n");
2865
+
2866
+ // Check wrangler
2867
+ try {
2868
+ execSync("wrangler --version", { stdio: "pipe" });
2869
+ } catch (_) {
2870
+ print(" [ERR] `wrangler` CLI not found.");
2871
+ print(" Install it: npm install -g wrangler");
2872
+ print(" Then authenticate: wrangler login\n");
2873
+ process.exit(1);
2874
+ }
2875
+
2876
+ const serverDir = path.join(__dirname, "..", "sticky-server");
2877
+ if (!fs.existsSync(path.join(serverDir, "worker.js"))) {
2878
+ print(" [ERR] sticky-server/worker.js not found.");
2879
+ print(" Make sure you have the full sticky-note package.\n");
2880
+ process.exit(1);
2881
+ }
2882
+
2883
+ print(" Step 1: Creating KV namespace...");
2884
+ try {
2885
+ const result = execSync("wrangler kv:namespace create STICKY_KV", {
2886
+ cwd: serverDir,
2887
+ encoding: "utf-8",
2888
+ stdio: ["pipe", "pipe", "pipe"],
2889
+ });
2890
+ print(" [OK] KV namespace created");
2891
+
2892
+ // Extract namespace ID from wrangler output
2893
+ const idMatch = result.match(/id\s*=\s*"([^"]+)"/);
2894
+ if (idMatch) {
2895
+ const namespaceId = idMatch[1];
2896
+ // Update wrangler.toml with real namespace ID
2897
+ const tomlPath = path.join(serverDir, "wrangler.toml");
2898
+ let toml = fs.readFileSync(tomlPath, "utf-8");
2899
+ toml = toml.replace("PLACEHOLDER_KV_NAMESPACE_ID", namespaceId);
2900
+ fs.writeFileSync(tomlPath, toml);
2901
+ print(` [OK] wrangler.toml updated with namespace ID`);
2902
+ }
2903
+ } catch (err) {
2904
+ print(" [ERR] Failed to create KV namespace: " + (err.message || err));
2905
+ print(" Make sure you're logged in: wrangler login\n");
2906
+ process.exit(1);
2907
+ }
2908
+
2909
+ print(" Step 2: Deploying Worker...");
2910
+ try {
2911
+ const result = execSync("wrangler deploy", {
2912
+ cwd: serverDir,
2913
+ encoding: "utf-8",
2914
+ stdio: ["pipe", "pipe", "pipe"],
2915
+ });
2916
+ // Extract worker URL
2917
+ const urlMatch = result.match(/(https:\/\/[^\s]+\.workers\.dev)/);
2918
+ const workerUrl = urlMatch ? urlMatch[1] : null;
2919
+
2920
+ const apiKey = require("crypto").randomBytes(24).toString("base64url");
2921
+
2922
+ if (workerUrl) {
2923
+ print(` [OK] Worker deployed: ${workerUrl}`);
2924
+
2925
+ print(" Step 3: Setting API key on Worker...");
2926
+ let secretSet = false;
2927
+ try {
2928
+ execSync("wrangler secret put STICKY_API_KEY", {
2929
+ cwd: serverDir,
2930
+ input: apiKey,
2931
+ encoding: "utf-8",
2932
+ stdio: ["pipe", "pipe", "pipe"],
2933
+ });
2934
+ print(" [OK] STICKY_API_KEY secret set on Worker");
2935
+ secretSet = true;
2936
+ } catch (err) {
2937
+ print(" [WARN] Could not set STICKY_API_KEY automatically: " + (err.message || err));
2938
+ print(` Set it manually: echo '${apiKey}' | wrangler secret put STICKY_API_KEY`);
2939
+ }
2940
+
2941
+ // Always write .env.sticky so the key is available even if secret-put failed
2942
+ const envPath = path.join(process.cwd(), ".env.sticky");
2943
+ fs.writeFileSync(envPath, `STICKY_URL=${workerUrl}\nSTICKY_API_KEY=${apiKey}\n`, "utf-8");
2944
+ print(" [OK] .env.sticky written with STICKY_URL and STICKY_API_KEY");
2945
+
2946
+ if (secretSet) {
2947
+ print("\n Share STICKY_URL and STICKY_API_KEY with your team securely");
2948
+ print(" (e.g. org secrets, 1Password). Do not commit .env.sticky.\n");
2949
+ } else {
2950
+ print("\n [!] API key is in .env.sticky but NOT yet set on the Worker.");
2951
+ print(" Run the manual command above, then share with your team.\n");
2952
+ }
2953
+ } else {
2954
+ print(" [OK] Worker deployed (URL not detected — check wrangler output)");
2955
+ }
2956
+ } catch (err) {
2957
+ print(" [ERR] Failed to deploy Worker: " + (err.message || err));
2958
+ process.exit(1);
2959
+ }
2960
+
2961
+ print("\n Backend ready! Run `npx sticky-note init --v3` in your repos.\n");
2962
+ }
2963
+
2964
+ // ──────────────────────────────────────────────
2965
+ // MIGRATE --to cloud command (V3 cloud migration)
2966
+ // ──────────────────────────────────────────────
2967
+
2968
+ async function cmdMigrateToCloud() {
2969
+
2970
+ printBanner();
2971
+ print(" Migrate V2 -> V3 Cloud\n");
2972
+
2973
+ // Check cloud config
2974
+ const stickyUrl = process.env.STICKY_URL || "";
2975
+ const stickyApiKey = process.env.STICKY_API_KEY || "";
2976
+
2977
+ // Try .env.sticky
2978
+ let envUrl = stickyUrl;
2979
+ let envKey = stickyApiKey;
2980
+ const envStickyPath = path.join(process.cwd(), ".env.sticky");
2981
+ if (!envUrl && fs.existsSync(envStickyPath)) {
2982
+ const raw = fs.readFileSync(envStickyPath, "utf-8");
2983
+ for (const line of raw.split(/\r?\n/)) {
2984
+ const trimmed = line.trim();
2985
+ if (trimmed.startsWith("STICKY_URL=")) envUrl = trimmed.slice("STICKY_URL=".length);
2986
+ if (trimmed.startsWith("STICKY_API_KEY=")) envKey = trimmed.slice("STICKY_API_KEY=".length);
2987
+ }
2988
+ }
2989
+
2990
+ if (!envUrl) {
2991
+ print(" [ERR] STICKY_URL not found.");
2992
+ print(" Set it in .env.sticky or run `npx sticky-note deploy-backend` first.\n");
2993
+ process.exit(1);
2994
+ }
2995
+
2996
+ // Detect project name
2997
+ let projectName = "default";
2998
+ try {
2999
+ const remote = execSync("git remote get-url origin", { encoding: "utf-8", stdio: "pipe" }).trim();
3000
+ const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
3001
+ if (match) projectName = match[1];
3002
+ } catch (_) {}
3003
+
3004
+ const headers = {
3005
+ "Content-Type": "application/json",
3006
+ "X-Sticky-Project": projectName,
3007
+ };
3008
+ if (envKey) headers["X-Sticky-API-Key"] = envKey;
3009
+
3010
+ async function postToCloud(endpoint, body) {
3011
+ try {
3012
+ const resp = await fetch(`${envUrl}${endpoint}`, {
3013
+ method: "POST",
3014
+ headers,
3015
+ body: JSON.stringify(body),
3016
+ });
3017
+ return resp.ok;
3018
+ } catch (_) {
3019
+ return false;
3020
+ }
3021
+ }
3022
+
3023
+ async function putToCloud(endpoint, body) {
3024
+ try {
3025
+ const resp = await fetch(`${envUrl}${endpoint}`, {
3026
+ method: "PUT",
3027
+ headers,
3028
+ body: JSON.stringify(body),
3029
+ });
3030
+ return resp.ok;
3031
+ } catch (_) {
3032
+ return false;
3033
+ }
3034
+ }
3035
+
3036
+ print(` Project: ${projectName}`);
3037
+ print(` Backend: ${envUrl}\n`);
3038
+
3039
+ // Migrate threads
3040
+ const stickyDir = path.join(process.cwd(), ".sticky-note");
3041
+ const memoryPath = path.join(stickyDir, "sticky-note.json");
3042
+ let threadCount = 0;
3043
+ let threadErrors = 0;
3044
+ if (fs.existsSync(memoryPath)) {
3045
+ const memory = readJsonSafe(memoryPath, { threads: [] });
3046
+ const threads = memory.threads || [];
3047
+ print(` Migrating ${threads.length} thread(s)...`);
3048
+ for (const thread of threads) {
3049
+ const ok = await putToCloud(`/threads/${thread.id}`, thread);
3050
+ if (ok) threadCount++;
3051
+ else threadErrors++;
3052
+ }
3053
+ print(` [OK] ${threadCount} threads migrated${threadErrors ? `, ${threadErrors} failed` : ""}`);
3054
+ } else {
3055
+ print(" No sticky-note.json found — skipping threads.");
3056
+ }
3057
+
3058
+ // Migrate audit records
3059
+ const auditDir = path.join(stickyDir, "audit");
3060
+ let auditCount = 0;
3061
+ let auditErrors = 0;
3062
+ if (fs.existsSync(auditDir)) {
3063
+ const files = fs.readdirSync(auditDir).filter(f => f.endsWith(".jsonl"));
3064
+ for (const file of files) {
3065
+ const raw = fs.readFileSync(path.join(auditDir, file), "utf-8");
3066
+ for (const line of raw.split(/\r?\n/)) {
3067
+ const trimmed = line.trim();
3068
+ if (!trimmed) continue;
3069
+ try {
3070
+ const record = JSON.parse(trimmed);
3071
+ const ok = await postToCloud("/audit", record);
3072
+ if (ok) auditCount++;
3073
+ else auditErrors++;
3074
+ } catch (_) {
3075
+ auditErrors++;
3076
+ }
3077
+ }
3078
+ }
3079
+ print(` [OK] ${auditCount} audit records migrated${auditErrors ? `, ${auditErrors} failed` : ""}`);
3080
+ } else {
3081
+ print(" No audit directory found — skipping audit.");
3082
+ }
3083
+
3084
+ // Migrate presence
3085
+ const presenceDir = path.join(stickyDir, "presence");
3086
+ let presenceCount = 0;
3087
+ if (fs.existsSync(presenceDir)) {
3088
+ const files = fs.readdirSync(presenceDir).filter(f => f.endsWith(".json"));
3089
+ for (const file of files) {
3090
+ try {
3091
+ const record = JSON.parse(fs.readFileSync(path.join(presenceDir, file), "utf-8"));
3092
+ const ok = await postToCloud("/presence", record);
3093
+ if (ok) presenceCount++;
3094
+ } catch (_) {}
3095
+ }
3096
+ if (presenceCount) print(` [OK] ${presenceCount} presence records migrated`);
3097
+ }
3098
+
3099
+ // Migrate config
3100
+ const configPath = path.join(stickyDir, "sticky-note-config.json");
3101
+ if (fs.existsSync(configPath)) {
3102
+ const config = readJsonSafe(configPath, {});
3103
+ const ok = await putToCloud("/config", config);
3104
+ if (ok) print(" [OK] Config migrated");
3105
+ }
3106
+
3107
+ print(`\n Migration complete. ${threadCount} threads, ${auditCount} audit records.\n`);
3108
+ }
3109
+
3110
+ // ──────────────────────────────────────────────
3111
+ // MCP-SERVER command — launch the stdio MCP server
3112
+ // (delegates to bin/mcp-server.js; cloud or local determined there)
3113
+ // ──────────────────────────────────────────────
3114
+
3115
+ // V3 cloud-aware MCP server is kept in bin/mcp-server.js.
3116
+ // This stub was the inline V3 implementation; replaced by the cleaner
3117
+ // delegating version from main. Kept as _cmdMcpServerInline for reference.
3118
+ async function _cmdMcpServerInline() {
3119
+ // MCP server runs over stdio using JSON-RPC 2.0
3120
+ const stickyUrl = process.env.STICKY_URL || "";
3121
+ const stickyApiKey = process.env.STICKY_API_KEY || "";
3122
+
3123
+ // Try .env.sticky
3124
+ let envUrl = stickyUrl;
3125
+ let envKey = stickyApiKey;
3126
+ const envStickyPath = path.join(process.cwd(), ".env.sticky");
3127
+ if (!envUrl && fs.existsSync(envStickyPath)) {
3128
+ const raw = fs.readFileSync(envStickyPath, "utf-8");
3129
+ for (const line of raw.split(/\r?\n/)) {
3130
+ const trimmed = line.trim();
3131
+ if (trimmed.startsWith("STICKY_URL=")) envUrl = trimmed.slice("STICKY_URL=".length);
3132
+ if (trimmed.startsWith("STICKY_API_KEY=")) envKey = trimmed.slice("STICKY_API_KEY=".length);
3133
+ }
3134
+ }
3135
+
3136
+ // Detect project
3137
+ let projectName = "default";
3138
+ try {
3139
+ const remote = execSync("git remote get-url origin", { encoding: "utf-8", stdio: "pipe" }).trim();
3140
+ const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
3141
+ if (match) projectName = match[1];
3142
+ } catch (_) {}
3143
+
3144
+ const baseHeaders = {
3145
+ "Content-Type": "application/json",
3146
+ "X-Sticky-Project": projectName,
3147
+ };
3148
+ if (envKey) baseHeaders["X-Sticky-API-Key"] = envKey;
3149
+
3150
+ async function cloudGet(endpoint) {
3151
+ if (!envUrl) return null;
3152
+ try {
3153
+ const resp = await fetch(`${envUrl}${endpoint}`, { headers: baseHeaders });
3154
+ if (!resp.ok) return null;
3155
+ return await resp.json();
3156
+ } catch (_) {
3157
+ return null;
3158
+ }
3159
+ }
3160
+
3161
+ async function cloudPost(endpoint, body) {
3162
+ if (!envUrl) return null;
3163
+ try {
3164
+ const resp = await fetch(`${envUrl}${endpoint}`, {
3165
+ method: "PUT",
3166
+ headers: baseHeaders,
3167
+ body: JSON.stringify(body),
3168
+ });
3169
+ if (!resp.ok) return null;
3170
+ return await resp.json();
3171
+ } catch (_) {
3172
+ return null;
3173
+ }
3174
+ }
3175
+
3176
+ // Fallback: read from local files
3177
+ const stickyDir = path.join(process.cwd(), ".sticky-note");
3178
+ const memoryPath = path.join(stickyDir, "sticky-note.json");
3179
+
3180
+ function localThreads(filter) {
3181
+ const memory = readJsonSafe(memoryPath, { threads: [] });
3182
+ let threads = memory.threads || [];
3183
+ if (filter) threads = threads.filter(filter);
3184
+ return threads;
3185
+ }
3186
+
3187
+ // MCP tool definitions
3188
+ const tools = [
3189
+ { name: "get_open_threads", description: "Get all open threads", inputSchema: { type: "object", properties: {} } },
3190
+ { name: "get_stuck_threads", description: "Get all stuck threads", inputSchema: { type: "object", properties: {} } },
3191
+ { name: "search_threads", description: "Search threads by query", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
3192
+ { name: "get_session_context", description: "Get a specific thread by ID", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
3193
+ { name: "write_thread", description: "Create or update a thread", inputSchema: { type: "object", properties: { thread: { type: "object" } }, required: ["thread"] } },
3194
+ { name: "get_team_config", description: "Get team configuration", inputSchema: { type: "object", properties: {} } },
3195
+ { name: "get_presence", description: "Get active developer presence", inputSchema: { type: "object", properties: {} } },
3196
+ { name: "get_audit_trail", description: "Query audit trail", inputSchema: { type: "object", properties: { file: { type: "string" }, user: { type: "string" }, since: { type: "string" }, tool: { type: "string" } } } },
3197
+ ];
3198
+
3199
+ // Handle MCP tool calls
3200
+ async function handleToolCall(name, args) {
3201
+ switch (name) {
3202
+ case "get_open_threads": {
3203
+ const result = await cloudGet("/threads?status=open");
3204
+ return result ? result.threads : localThreads(t => t.status === "open");
3205
+ }
3206
+ case "get_stuck_threads": {
3207
+ const result = await cloudGet("/threads?status=stuck");
3208
+ return result ? result.threads : localThreads(t => t.status === "stuck");
3209
+ }
3210
+ case "search_threads": {
3211
+ const q = args.query || "";
3212
+ const result = await cloudGet(`/threads?q=${encodeURIComponent(q)}`);
3213
+ if (result) return result.threads;
3214
+ const qLower = q.toLowerCase();
3215
+ return localThreads(t =>
3216
+ (t.narrative || "").toLowerCase().includes(qLower) ||
3217
+ (t.handoff_summary || "").toLowerCase().includes(qLower) ||
3218
+ (t.files_touched || []).some(f => f.toLowerCase().includes(qLower))
3219
+ );
3220
+ }
3221
+ case "get_session_context": {
3222
+ const id = args.id;
3223
+ const result = await cloudGet(`/threads/${id}`);
3224
+ if (result && !result.error) return result;
3225
+ const memory = readJsonSafe(memoryPath, { threads: [] });
3226
+ return (memory.threads || []).find(t => t.id === id || t.id.startsWith(id)) || null;
3227
+ }
3228
+ case "write_thread": {
3229
+ const thread = args.thread;
3230
+ if (envUrl) await cloudPost(`/threads/${thread.id}`, thread);
3231
+ return { ok: true };
3232
+ }
3233
+ case "get_team_config": {
3234
+ const result = await cloudGet("/config");
3235
+ if (result) return result;
3236
+ const configPath = path.join(stickyDir, "sticky-note-config.json");
3237
+ return readJsonSafe(configPath, {});
3238
+ }
3239
+ case "get_presence": {
3240
+ const result = await cloudGet("/presence");
3241
+ if (result) return result.presence;
3242
+ // Local fallback: read presence files
3243
+ const presDir = path.join(stickyDir, "presence");
3244
+ try {
3245
+ return fs.readdirSync(presDir).filter(f => f.endsWith(".json")).map(f =>
3246
+ JSON.parse(fs.readFileSync(path.join(presDir, f), "utf-8"))
3247
+ );
3248
+ } catch (_) {
3249
+ return [];
3250
+ }
3251
+ }
3252
+ case "get_audit_trail": {
3253
+ const params = new URLSearchParams();
3254
+ if (args.file) params.set("file", args.file);
3255
+ if (args.user) params.set("user", args.user);
3256
+ if (args.since) params.set("since", args.since);
3257
+ if (args.tool) params.set("tool", args.tool);
3258
+ const qs = params.toString();
3259
+ const result = await cloudGet(`/audit${qs ? "?" + qs : ""}`);
3260
+ if (result) return result.audit;
3261
+ // Local fallback: grep JSONL
3262
+ const auditDir = path.join(stickyDir, "audit");
3263
+ const records = [];
3264
+ try {
3265
+ for (const f of fs.readdirSync(auditDir).filter(f => f.endsWith(".jsonl"))) {
3266
+ const raw = fs.readFileSync(path.join(auditDir, f), "utf-8");
3267
+ for (const line of raw.split(/\r?\n/)) {
3268
+ if (!line.trim()) continue;
3269
+ try {
3270
+ const r = JSON.parse(line);
3271
+ if (args.user && r.user !== args.user) continue;
3272
+ if (args.file && !(r.file || "").includes(args.file)) continue;
3273
+ if (args.tool && r.tool !== args.tool) continue;
3274
+ if (args.since && new Date(r.timestamp) < new Date(args.since)) continue;
3275
+ records.push(r);
3276
+ } catch (_) {}
3277
+ }
3278
+ }
3279
+ } catch (_) {}
3280
+ return records;
3281
+ }
3282
+ default:
3283
+ return { error: `Unknown tool: ${name}` };
3284
+ }
3285
+ }
3286
+
3287
+ // MCP stdio protocol
3288
+ const rl = readline.createInterface({ input: process.stdin });
3289
+ let buffer = "";
3290
+
3291
+ rl.on("line", async (line) => {
3292
+ try {
3293
+ const msg = JSON.parse(line);
3294
+
3295
+ if (msg.method === "initialize") {
3296
+ const response = {
3297
+ jsonrpc: "2.0",
3298
+ id: msg.id,
3299
+ result: {
3300
+ protocolVersion: "2024-11-05",
3301
+ capabilities: { tools: {} },
3302
+ serverInfo: { name: "sticky-note", version: VERSION },
3303
+ },
3304
+ };
3305
+ process.stdout.write(JSON.stringify(response) + "\n");
3306
+ } else if (msg.method === "notifications/initialized") {
3307
+ // No response needed
3308
+ } else if (msg.method === "tools/list") {
3309
+ const response = {
3310
+ jsonrpc: "2.0",
3311
+ id: msg.id,
3312
+ result: { tools },
3313
+ };
3314
+ process.stdout.write(JSON.stringify(response) + "\n");
3315
+ } else if (msg.method === "tools/call") {
3316
+ const toolName = msg.params.name;
3317
+ const toolArgs = msg.params.arguments || {};
3318
+ const result = await handleToolCall(toolName, toolArgs);
3319
+ const response = {
3320
+ jsonrpc: "2.0",
3321
+ id: msg.id,
3322
+ result: {
3323
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
3324
+ },
3325
+ };
3326
+ process.stdout.write(JSON.stringify(response) + "\n");
3327
+ } else if (msg.id) {
3328
+ // Unknown method with id — return error
3329
+ const response = {
3330
+ jsonrpc: "2.0",
3331
+ id: msg.id,
3332
+ error: { code: -32601, message: `Unknown method: ${msg.method}` },
3333
+ };
3334
+ process.stdout.write(JSON.stringify(response) + "\n");
3335
+ }
3336
+ } catch (err) {
3337
+ // Parse error
3338
+ if (line.trim()) {
3339
+ process.stderr.write(`[sticky-note mcp] Parse error: ${err.message}\n`);
3340
+ }
3341
+ }
3342
+ });
3343
+ }
3344
+
2723
3345
  // ──────────────────────────────────────────────
2724
3346
  // sync — commit and optionally push .sticky-note/ files
2725
3347
  // ──────────────────────────────────────────────
@@ -3428,16 +4050,26 @@ function cmdRunHook() {
3428
4050
  }
3429
4051
 
3430
4052
  // ──────────────────────────────────────────────
3431
- // MIGRATE command
4053
+ // MIGRATE command (dispatcher)
4054
+ // Default: migrate to git data branch (from main)
4055
+ // --to cloud: migrate local data to Cloudflare KV (from V3)
3432
4056
  // ──────────────────────────────────────────────
3433
4057
 
4058
+ async function cmdMigrate() {
4059
+ const args = process.argv.slice(3);
4060
+ if (args.includes("--to") && args.includes("cloud")) {
4061
+ return cmdMigrateToCloud();
4062
+ }
4063
+ return _cmdMigrateDataBranch();
4064
+ }
4065
+
3434
4066
  /**
3435
4067
  * One-time migration from .sticky-note/ (feature-branch storage) to
3436
4068
  * .git/sticky-note/ (data-branch storage).
3437
4069
  *
3438
4070
  * Idempotent — safe to run multiple times.
3439
4071
  */
3440
- async function cmdMigrate() {
4072
+ async function _cmdMigrateDataBranch() {
3441
4073
  printBanner();
3442
4074
 
3443
4075
  if (!isGitRepo()) {
@@ -3703,6 +4335,9 @@ async function main() {
3703
4335
  case "resume":
3704
4336
  cmdResume();
3705
4337
  break;
4338
+ case "transcript":
4339
+ cmdTranscript();
4340
+ break;
3706
4341
  case "audit":
3707
4342
  cmdAudit();
3708
4343
  break;
@@ -3727,6 +4362,9 @@ async function main() {
3727
4362
  case "checkpoint":
3728
4363
  cmdCheckpoint();
3729
4364
  break;
4365
+ case "deploy-backend":
4366
+ cmdDeployBackend();
4367
+ break;
3730
4368
  case "overlap":
3731
4369
  cmdOverlap();
3732
4370
  break;
@@ -3752,10 +4390,7 @@ async function main() {
3752
4390
  cmdRunHook();
3753
4391
  break;
3754
4392
  case "migrate":
3755
- cmdMigrate().catch((err) => {
3756
- console.error(err);
3757
- process.exit(1);
3758
- });
4393
+ await cmdMigrate();
3759
4394
  break;
3760
4395
  case "--version":
3761
4396
  case "-v":
@@ -3767,11 +4402,14 @@ async function main() {
3767
4402
  printBanner();
3768
4403
  print(" Usage: npx sticky-note <command>\n");
3769
4404
  print(" Commands:");
3770
- print(" init Interactive setup — creates V2.5 hooks and config");
4405
+ print(" init Interactive setup — creates hooks and config");
4406
+ print(" init --v3 V3 setup with Cloudflare KV cloud backend");
4407
+ print(" init --ci Non-interactive setup for CI/GitHub Actions");
3771
4408
  print(" update Update hook scripts (preserves data)");
3772
4409
  print(" status Diagnostic report: threads, audit, attribution health");
3773
4410
  print(" threads List open/stuck threads");
3774
4411
  print(" resume Resume a previous thread (--list, --clear, <id>)");
4412
+ print(" transcript Show a thread's captured verbatim transcript (--list, --raw, <id>)");
3775
4413
  print(" resume-thread Smart thread resume (--query, --user, --file, --json)");
3776
4414
  print(" audit Query audit trail (--file, --user, --since, --session)");
3777
4415
  print(" who Show active and recent team members");
@@ -3780,14 +4418,16 @@ async function main() {
3780
4418
  print(" reset Wipe all threads and start fresh (--force, --keep-audit)");
3781
4419
  print(" get-line-attribution File→thread attribution with line ranges (--file, --lines)");
3782
4420
  print(" checkpoint Set work-topic checkpoint for attribution (--topic, --show, --clear)");
4421
+ print(" deploy-backend Provision Cloudflare KV backend (V3)");
4422
+ print(" migrate Migrate to git data branch (default) or cloud (--to cloud)");
3783
4423
  print(" overlap Detect file overlaps with open/stuck threads");
3784
4424
  print(" claim Claim ownership of files you're working on (--list, --clear)");
3785
4425
  print(" sync Commit .sticky-note/ changes to git (--push to also push)");
3786
- print(" pull Auto-sync .sticky-note/ then git pull (alias: git spull)");
3787
- print(" bootstrap Provision MCP servers with secrets from manifest");
3788
- print(" env Environment management (status, add-server, add-plugin)");
3789
- print(" mcp-server Launch the sticky-note MCP server (stdio JSON-RPC)");
3790
- print(" run-hook <name> Run a hook from any subdirectory (finds .claude/hooks/ up the tree)");
4426
+ print(" pull Auto-sync .sticky-note/ then git pull (alias: git spull)");
4427
+ print(" bootstrap Provision MCP servers with secrets from manifest");
4428
+ print(" env Environment management (status, add-server, add-plugin)");
4429
+ print(" mcp-server Launch the sticky-note MCP server (stdio JSON-RPC)");
4430
+ print(" run-hook <name> Run a hook from any subdirectory (finds .claude/hooks/ up the tree)");
3791
4431
  print("");
3792
4432
  print(" Options:");
3793
4433
  print(" --version Show version");