sticky-note-cli 3.0.0 → 3.1.1
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 +94 -10
- package/bin/cli.js +850 -167
- package/bin/mcp-server.js +218 -36
- package/package.json +5 -5
- package/templates/CLAUDE.md +23 -0
- package/templates/hooks/inject-context.js +26 -12
- package/templates/hooks/on-error.js +22 -9
- package/templates/hooks/on-stop.js +12 -7
- package/templates/hooks/pre-tool-use.js +49 -52
- package/templates/hooks/session-end.js +138 -24
- package/templates/hooks/session-start.js +33 -9
- package/templates/hooks/sticky-codex.ps1 +20 -4
- package/templates/hooks/sticky-codex.sh +14 -5
- package/templates/hooks/sticky-utils.js +306 -20
- package/templates/hooks/track-work.js +27 -13
- package/templates/settings.json +7 -7
- package/templates/sticky-note-config.json +2 -0
- package/templates/sticky-note-install.yml +80 -0
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,137 @@ async function cmdInit() {
|
|
|
660
552
|
print("");
|
|
661
553
|
|
|
662
554
|
// Auto-detect MCP servers and skills
|
|
663
|
-
print("
|
|
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(`
|
|
560
|
+
print(` Found ${detectedMcp.length} MCP server(s):`);
|
|
669
561
|
for (const s of detectedMcp) {
|
|
670
|
-
print(`
|
|
562
|
+
print(` - ${s.name} (${s.source})`);
|
|
671
563
|
}
|
|
672
564
|
}
|
|
673
565
|
if (detectedSkills.length > 0) {
|
|
674
|
-
print(`
|
|
566
|
+
print(` Found ${detectedSkills.length} skill(s):`);
|
|
675
567
|
for (const s of detectedSkills) {
|
|
676
|
-
print(`
|
|
568
|
+
print(` - ${s}`);
|
|
677
569
|
}
|
|
678
570
|
}
|
|
679
571
|
if (detectedMcp.length === 0 && detectedSkills.length === 0) {
|
|
680
|
-
print("
|
|
572
|
+
print(" No existing MCP servers or skills detected");
|
|
681
573
|
}
|
|
682
574
|
print("");
|
|
683
575
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
input: process.stdin,
|
|
687
|
-
output: process.stdout,
|
|
688
|
-
});
|
|
576
|
+
let mcpServers, skills, conventions, staleDaysResolved, injectTokenBudgetResolved;
|
|
577
|
+
let wantsCloud = false;
|
|
689
578
|
|
|
690
|
-
|
|
691
|
-
|
|
579
|
+
if (ciMode) {
|
|
580
|
+
// CI mode: no prompts, use env vars
|
|
581
|
+
const mcpRaw = process.env.STICKY_MCP_SERVERS || "";
|
|
582
|
+
const convRaw = process.env.STICKY_CONVENTIONS || "";
|
|
583
|
+
const staleDaysRaw = process.env.STICKY_STALE_DAYS || "14";
|
|
692
584
|
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
585
|
+
mcpServers = detectedMcp.slice();
|
|
586
|
+
if (mcpRaw) {
|
|
587
|
+
try {
|
|
588
|
+
const parsed = JSON.parse(mcpRaw);
|
|
589
|
+
if (Array.isArray(parsed)) mcpServers = parsed;
|
|
590
|
+
} catch (_) {
|
|
591
|
+
mcpRaw.split(",").map(s => s.trim()).filter(Boolean).forEach(name => {
|
|
592
|
+
mcpServers.push({ name, source: "ci-env" });
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
skills = [...detectedSkills];
|
|
597
|
+
conventions = convRaw ? convRaw.split(",").map(s => s.trim()).filter(Boolean) : [];
|
|
598
|
+
staleDaysResolved = parseIntOr(staleDaysRaw, 14);
|
|
599
|
+
injectTokenBudgetResolved = 1000;
|
|
600
|
+
wantsCloud = v3Mode; // --v3 in CI mode implies cloud
|
|
601
|
+
} else {
|
|
602
|
+
// Interactive prompts
|
|
603
|
+
const rl = readline.createInterface({
|
|
604
|
+
input: process.stdin,
|
|
605
|
+
output: process.stdout,
|
|
606
|
+
});
|
|
706
607
|
|
|
707
|
-
|
|
708
|
-
|
|
608
|
+
print(" Team Configuration");
|
|
609
|
+
print(" (Press Enter to accept detected defaults)\n");
|
|
709
610
|
|
|
710
|
-
|
|
611
|
+
const mcpDefault = detectedMcp.map((s) => s.name).join(", ");
|
|
612
|
+
const mcpServersRaw = await ask(
|
|
613
|
+
rl,
|
|
614
|
+
"MCP servers (comma-separated)",
|
|
615
|
+
mcpDefault || ""
|
|
616
|
+
);
|
|
617
|
+
const conventionsRaw = await ask(
|
|
618
|
+
rl,
|
|
619
|
+
"Team conventions (comma-separated)",
|
|
620
|
+
""
|
|
621
|
+
);
|
|
622
|
+
const staleDays = await ask(rl, "Stale thread age in days", "14");
|
|
623
|
+
const injectTokenBudget = await ask(rl, "Inject context token budget", "1000");
|
|
711
624
|
|
|
712
|
-
|
|
713
|
-
|
|
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
|
-
});
|
|
625
|
+
// Sync MCP servers from teammate's sticky-note config
|
|
626
|
+
await syncMcpFromStickyNote(rl);
|
|
720
627
|
|
|
721
|
-
|
|
628
|
+
const cloudAnswer = await ask(
|
|
629
|
+
rl,
|
|
630
|
+
"Set up Cloudflare cloud backend for real-time team sync? (y/N)",
|
|
631
|
+
v3Mode ? "y" : "N"
|
|
632
|
+
);
|
|
633
|
+
wantsCloud = cloudAnswer.toLowerCase() === "y" || cloudAnswer.toLowerCase() === "yes";
|
|
722
634
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
:
|
|
726
|
-
|
|
727
|
-
|
|
635
|
+
rl.close();
|
|
636
|
+
|
|
637
|
+
// Build MCP server list: prefer detected objects, fall back to names
|
|
638
|
+
const mcpNames = mcpServersRaw
|
|
639
|
+
? mcpServersRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
640
|
+
: [];
|
|
641
|
+
mcpServers = mcpNames.map((name) => {
|
|
642
|
+
const detected = detectedMcp.find((d) => d.name === name);
|
|
643
|
+
return detected || { name, source: "manual" };
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
skills = [...detectedSkills];
|
|
647
|
+
|
|
648
|
+
conventions = conventionsRaw
|
|
649
|
+
? conventionsRaw.split(",").map((s) => s.trim()).filter(Boolean)
|
|
650
|
+
: [];
|
|
651
|
+
staleDaysResolved = parseIntOr(staleDays, 14);
|
|
652
|
+
injectTokenBudgetResolved = parseIntOr(injectTokenBudget, 1000);
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Cloud backend: provision inline if requested, or write existing STICKY_URL from env
|
|
656
|
+
if (wantsCloud) {
|
|
657
|
+
print("\n Cloud Backend Setup...\n");
|
|
658
|
+
const cloudResult = provisionCloudBackend();
|
|
659
|
+
if (cloudResult === null) {
|
|
660
|
+
print(" [ERR] `wrangler` CLI not found.");
|
|
661
|
+
print(" Install it: npm install -g wrangler");
|
|
662
|
+
print(" Then authenticate: wrangler login");
|
|
663
|
+
print(" And run `npx sticky-note deploy-backend` when ready.\n");
|
|
664
|
+
} else if (cloudResult.error) {
|
|
665
|
+
print(" [WARN] Cloud setup failed: " + cloudResult.error);
|
|
666
|
+
print(" Run `npx sticky-note deploy-backend` to retry.\n");
|
|
667
|
+
} else {
|
|
668
|
+
if (cloudResult.secretSet) {
|
|
669
|
+
print("\n Share STICKY_URL and STICKY_API_KEY with teammates securely.");
|
|
670
|
+
print(" (e.g. org secrets, 1Password — do not commit .env.sticky)\n");
|
|
671
|
+
} else {
|
|
672
|
+
print("\n [!] API key written to .env.sticky but NOT yet set on the Worker.");
|
|
673
|
+
print(" Run the manual command shown above, then share with your team.\n");
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} else if (process.env.STICKY_URL) {
|
|
677
|
+
// Write existing STICKY_URL from environment into .env.sticky
|
|
678
|
+
const envStickyPath = path.join(process.cwd(), ".env.sticky");
|
|
679
|
+
const stickyUrl = process.env.STICKY_URL;
|
|
680
|
+
const stickyApiKey = process.env.STICKY_API_KEY || "";
|
|
681
|
+
let content = `STICKY_URL=${stickyUrl}\n`;
|
|
682
|
+
if (stickyApiKey) content += `STICKY_API_KEY=${stickyApiKey}\n`;
|
|
683
|
+
fs.writeFileSync(envStickyPath, content, "utf-8");
|
|
684
|
+
print(" [OK] .env.sticky (cloud backend configured)");
|
|
685
|
+
}
|
|
728
686
|
|
|
729
687
|
print("\n 📁 Creating files...\n");
|
|
730
688
|
|
|
@@ -1418,6 +1376,76 @@ function cmdStatus() {
|
|
|
1418
1376
|
` ${isGitRepo() ? "[OK]" : "[ERR]"} Git repository`
|
|
1419
1377
|
);
|
|
1420
1378
|
|
|
1379
|
+
// V3: Cloud backend health
|
|
1380
|
+
const envStickyPath = path.join(process.cwd(), ".env.sticky");
|
|
1381
|
+
print("\n Cloud Backend (V3):");
|
|
1382
|
+
if (fs.existsSync(envStickyPath)) {
|
|
1383
|
+
try {
|
|
1384
|
+
const envRaw = fs.readFileSync(envStickyPath, "utf-8");
|
|
1385
|
+
let stickyUrl = "";
|
|
1386
|
+
for (const line of envRaw.split(/\r?\n/)) {
|
|
1387
|
+
const trimmed = line.trim();
|
|
1388
|
+
if (trimmed.startsWith("STICKY_URL=")) {
|
|
1389
|
+
stickyUrl = trimmed.slice("STICKY_URL=".length).trim();
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (stickyUrl) {
|
|
1393
|
+
print(` [OK] .env.sticky found (${stickyUrl})`);
|
|
1394
|
+
try {
|
|
1395
|
+
const http = require(stickyUrl.startsWith("https") ? "https" : "http");
|
|
1396
|
+
const healthCheck = new Promise((resolve) => {
|
|
1397
|
+
const req = http.get(`${stickyUrl}/health`, { timeout: 5000 }, (res) => {
|
|
1398
|
+
let body = "";
|
|
1399
|
+
res.on("data", (chunk) => body += chunk);
|
|
1400
|
+
res.on("end", () => {
|
|
1401
|
+
if (res.statusCode === 200) {
|
|
1402
|
+
resolve({ ok: true, body });
|
|
1403
|
+
} else {
|
|
1404
|
+
resolve({ ok: false, status: res.statusCode });
|
|
1405
|
+
}
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
req.on("error", (err) => resolve({ ok: false, error: err.message }));
|
|
1409
|
+
req.on("timeout", () => { req.destroy(); resolve({ ok: false, error: "timeout" }); });
|
|
1410
|
+
});
|
|
1411
|
+
const result = healthCheck;
|
|
1412
|
+
// Synchronous-friendly: use execFileSync with curl/node to check
|
|
1413
|
+
try {
|
|
1414
|
+
const healthResult = execFileSync("node", ["-e", `
|
|
1415
|
+
const url = ${JSON.stringify(stickyUrl + "/health")};
|
|
1416
|
+
const mod = url.startsWith("https") ? require("https") : require("http");
|
|
1417
|
+
const req = mod.get(url, {timeout: 5000}, (res) => {
|
|
1418
|
+
let b = "";
|
|
1419
|
+
res.on("data", c => b += c);
|
|
1420
|
+
res.on("end", () => {
|
|
1421
|
+
if (res.statusCode === 200) process.stdout.write("OK:" + b);
|
|
1422
|
+
else process.stdout.write("ERR:" + res.statusCode);
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
req.on("error", e => process.stdout.write("ERR:" + e.message));
|
|
1426
|
+
req.on("timeout", () => { req.destroy(); process.stdout.write("ERR:timeout"); });
|
|
1427
|
+
`], { encoding: "utf-8", timeout: 8000, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
1428
|
+
if (healthResult.startsWith("OK:")) {
|
|
1429
|
+
print(` [OK] Backend reachable`);
|
|
1430
|
+
} else {
|
|
1431
|
+
print(` [ERR] Backend unreachable (${healthResult.slice(4)})`);
|
|
1432
|
+
}
|
|
1433
|
+
} catch (_) {
|
|
1434
|
+
print(` [ERR] Backend unreachable (health check failed)`);
|
|
1435
|
+
}
|
|
1436
|
+
} catch (_) {
|
|
1437
|
+
print(` [ERR] Backend health check failed`);
|
|
1438
|
+
}
|
|
1439
|
+
} else {
|
|
1440
|
+
print(" [--] .env.sticky found but STICKY_URL not set");
|
|
1441
|
+
}
|
|
1442
|
+
} catch (_) {
|
|
1443
|
+
print(" [ERR] .env.sticky unreadable");
|
|
1444
|
+
}
|
|
1445
|
+
} else {
|
|
1446
|
+
print(" [--] No .env.sticky (local-only mode, V2.5 compatible)");
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1421
1449
|
// V2.5: Attribution engine health
|
|
1422
1450
|
print("\n Attribution Engine (V2.5):");
|
|
1423
1451
|
const attrPath = path.join(process.cwd(), ".claude", "hooks", "sticky-attribution.js");
|
|
@@ -1651,6 +1679,138 @@ function cmdResume() {
|
|
|
1651
1679
|
print(" Run `npx sticky-note resume --clear` to cancel.\n");
|
|
1652
1680
|
}
|
|
1653
1681
|
|
|
1682
|
+
// ──────────────────────────────────────────────
|
|
1683
|
+
// TRANSCRIPT command
|
|
1684
|
+
// ──────────────────────────────────────────────
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* Extract human-readable "[role] text" lines from a raw session transcript
|
|
1688
|
+
* (Claude Code JSONL format, with a fallback for Copilot CLI-style hook
|
|
1689
|
+
* entries). Tool use/results are summarized rather than dumped in full —
|
|
1690
|
+
* pass --raw to cmdTranscript to see the untouched JSONL instead.
|
|
1691
|
+
*/
|
|
1692
|
+
function formatTranscriptText(rawTranscript) {
|
|
1693
|
+
const lines = (rawTranscript || "").split(/\r?\n/).filter((l) => l.trim());
|
|
1694
|
+
const out = [];
|
|
1695
|
+
|
|
1696
|
+
for (const line of lines) {
|
|
1697
|
+
let entry;
|
|
1698
|
+
try { entry = JSON.parse(line); } catch (_) { continue; }
|
|
1699
|
+
|
|
1700
|
+
const message = entry.message;
|
|
1701
|
+
if (message && typeof message === "object" && !Array.isArray(message)) {
|
|
1702
|
+
const role = message.role || entry.role || "unknown";
|
|
1703
|
+
const content = message.content;
|
|
1704
|
+
const blocks = Array.isArray(content) ? content : [content];
|
|
1705
|
+
|
|
1706
|
+
for (const block of blocks) {
|
|
1707
|
+
if (typeof block === "string") {
|
|
1708
|
+
if (block.trim()) out.push(`[${role}] ${block.trim()}`);
|
|
1709
|
+
} else if (block && typeof block === "object") {
|
|
1710
|
+
if (block.type === "text" && block.text) {
|
|
1711
|
+
out.push(`[${role}] ${block.text.trim()}`);
|
|
1712
|
+
} else if (block.type === "tool_use") {
|
|
1713
|
+
out.push(`[${role}] → used tool: ${block.name || "?"}`);
|
|
1714
|
+
} else if (block.type === "tool_result") {
|
|
1715
|
+
const resultText = typeof block.content === "string"
|
|
1716
|
+
? block.content
|
|
1717
|
+
: Array.isArray(block.content)
|
|
1718
|
+
? block.content.filter((c) => c && typeof c === "object" && c.type === "text").map((c) => c.text || "").join(" ")
|
|
1719
|
+
: "";
|
|
1720
|
+
if (resultText.trim()) {
|
|
1721
|
+
out.push(`[tool result]${block.is_error ? " (error)" : ""} ${resultText.trim().slice(0, 300)}`);
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
continue;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
// Copilot CLI-style hook entries
|
|
1730
|
+
if (entry.tool_name) {
|
|
1731
|
+
out.push(`[tool] ${entry.tool_name}`);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return out;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
function _loadStickyUtils() {
|
|
1739
|
+
try {
|
|
1740
|
+
return require(path.join(__dirname, "..", "templates", "hooks", "sticky-utils.js"));
|
|
1741
|
+
} catch (_) {
|
|
1742
|
+
try {
|
|
1743
|
+
return require(path.join(process.cwd(), ".claude", "hooks", "sticky-utils.js"));
|
|
1744
|
+
} catch (_) {
|
|
1745
|
+
return null;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function cmdTranscript() {
|
|
1751
|
+
printBanner();
|
|
1752
|
+
const args = process.argv.slice(3);
|
|
1753
|
+
const raw = args.includes("--raw");
|
|
1754
|
+
const threadIdArg = args.find((a) => !a.startsWith("--"));
|
|
1755
|
+
|
|
1756
|
+
const utils = _loadStickyUtils();
|
|
1757
|
+
if (!utils) {
|
|
1758
|
+
print(" [ERR] Could not load sticky-utils.js. Run `npx sticky-note init` first.");
|
|
1759
|
+
process.exit(1);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
const memory = utils.loadJson(utils.getMemoryPath(), { threads: [] });
|
|
1763
|
+
const threads = (memory.threads || []).filter(Boolean);
|
|
1764
|
+
|
|
1765
|
+
if (!threadIdArg || args.includes("--list")) {
|
|
1766
|
+
const withTranscripts = threads.filter((t) => t.transcript_captured);
|
|
1767
|
+
if (!withTranscripts.length) {
|
|
1768
|
+
print(" No threads have a captured transcript yet.");
|
|
1769
|
+
print(" (Check capture_transcripts in sticky-note-config.json if you expected one.)");
|
|
1770
|
+
return;
|
|
1771
|
+
}
|
|
1772
|
+
print("\n Threads with a captured transcript:\n");
|
|
1773
|
+
for (const t of withTranscripts) {
|
|
1774
|
+
const note = t.last_note ? ` — ${t.last_note.slice(0, 60)}` : "";
|
|
1775
|
+
print(` ${t.id.slice(0, 8)} ${t.user || "?"}${note}`);
|
|
1776
|
+
}
|
|
1777
|
+
print(`\n Usage: npx sticky-note transcript <thread-id> [--raw]\n`);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const match = threads.find((t) => t.id === threadIdArg || t.id.startsWith(threadIdArg));
|
|
1782
|
+
if (!match) {
|
|
1783
|
+
print(` [ERR] No thread found matching "${threadIdArg}".`);
|
|
1784
|
+
print(" Run `npx sticky-note transcript --list` to see threads with a captured transcript.");
|
|
1785
|
+
process.exit(1);
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
const filePath = utils.getTranscriptPath(match.id);
|
|
1789
|
+
if (!fs.existsSync(filePath)) {
|
|
1790
|
+
print(` No transcript captured for thread ${match.id.slice(0, 8)}.`);
|
|
1791
|
+
print(" (capture_transcripts may be disabled, or this thread predates the feature.)");
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/).filter((l) => l.trim());
|
|
1796
|
+
print(`\n ── Transcript for thread ${match.id.slice(0, 8)} (${lines.length} session${lines.length === 1 ? "" : "s"}) ──\n`);
|
|
1797
|
+
|
|
1798
|
+
for (const line of lines) {
|
|
1799
|
+
let entry;
|
|
1800
|
+
try { entry = JSON.parse(line); } catch (_) { continue; }
|
|
1801
|
+
const shortSession = (entry.session_id || "?").slice(0, 8);
|
|
1802
|
+
print(` ── Session ${shortSession} · ${entry.tool || "?"} · ${entry.captured_at || "?"}${entry.redacted ? " · redacted" : ""} ──`);
|
|
1803
|
+
if (raw) {
|
|
1804
|
+
print(entry.transcript || "");
|
|
1805
|
+
} else {
|
|
1806
|
+
const formatted = formatTranscriptText(entry.transcript);
|
|
1807
|
+
if (!formatted.length) print(" (no readable turns found — try --raw)");
|
|
1808
|
+
for (const f of formatted) print(" " + f);
|
|
1809
|
+
}
|
|
1810
|
+
print("");
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1654
1814
|
// ──────────────────────────────────────────────
|
|
1655
1815
|
// AUDIT command
|
|
1656
1816
|
// ──────────────────────────────────────────────
|
|
@@ -2720,6 +2880,511 @@ function cmdGc() {
|
|
|
2720
2880
|
print("");
|
|
2721
2881
|
}
|
|
2722
2882
|
|
|
2883
|
+
// ──────────────────────────────────────────────
|
|
2884
|
+
// DEPLOY-BACKEND command (V3)
|
|
2885
|
+
// ──────────────────────────────────────────────
|
|
2886
|
+
|
|
2887
|
+
// Shared provisioning logic used by both cmdInit and cmdDeployBackend.
|
|
2888
|
+
// Returns null if wrangler is missing.
|
|
2889
|
+
// Returns { workerUrl, apiKey, secretSet } on success.
|
|
2890
|
+
// Returns { error } on deployment failure.
|
|
2891
|
+
function provisionCloudBackend() {
|
|
2892
|
+
// Check wrangler
|
|
2893
|
+
try {
|
|
2894
|
+
execSync("wrangler --version", { stdio: "pipe" });
|
|
2895
|
+
} catch (_) {
|
|
2896
|
+
return null; // caller prints the install message
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
const serverDir = path.join(__dirname, "..", "sticky-server");
|
|
2900
|
+
if (!fs.existsSync(path.join(serverDir, "worker.js"))) {
|
|
2901
|
+
return { error: "sticky-server/worker.js not found. Make sure you have the full sticky-note package." };
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
// Step 1: KV namespace
|
|
2905
|
+
print(" Step 1: Creating KV namespace...");
|
|
2906
|
+
let kvResult;
|
|
2907
|
+
try {
|
|
2908
|
+
kvResult = execSync("wrangler kv:namespace create STICKY_KV", {
|
|
2909
|
+
cwd: serverDir,
|
|
2910
|
+
encoding: "utf-8",
|
|
2911
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2912
|
+
});
|
|
2913
|
+
print(" [OK] KV namespace created");
|
|
2914
|
+
} catch (err) {
|
|
2915
|
+
return { error: "Failed to create KV namespace: " + (err.message || err) + "\n Make sure you're logged in: wrangler login" };
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
const idMatch = kvResult.match(/id\s*=\s*"([^"]+)"/);
|
|
2919
|
+
if (idMatch) {
|
|
2920
|
+
const tomlPath = path.join(serverDir, "wrangler.toml");
|
|
2921
|
+
let toml = fs.readFileSync(tomlPath, "utf-8");
|
|
2922
|
+
toml = toml.replace("PLACEHOLDER_KV_NAMESPACE_ID", idMatch[1]);
|
|
2923
|
+
fs.writeFileSync(tomlPath, toml);
|
|
2924
|
+
print(" [OK] wrangler.toml updated with namespace ID");
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// Step 2: Deploy Worker
|
|
2928
|
+
print(" Step 2: Deploying Worker...");
|
|
2929
|
+
let deployResult;
|
|
2930
|
+
try {
|
|
2931
|
+
deployResult = execSync("wrangler deploy", {
|
|
2932
|
+
cwd: serverDir,
|
|
2933
|
+
encoding: "utf-8",
|
|
2934
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2935
|
+
});
|
|
2936
|
+
} catch (err) {
|
|
2937
|
+
return { error: "Failed to deploy Worker: " + (err.message || err) };
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
const urlMatch = deployResult.match(/(https:\/\/[^\s]+\.workers\.dev)/);
|
|
2941
|
+
const workerUrl = urlMatch ? urlMatch[1] : null;
|
|
2942
|
+
const apiKey = require("crypto").randomBytes(24).toString("base64url");
|
|
2943
|
+
|
|
2944
|
+
if (workerUrl) {
|
|
2945
|
+
print(` [OK] Worker deployed: ${workerUrl}`);
|
|
2946
|
+
} else {
|
|
2947
|
+
print(" [OK] Worker deployed (URL not detected — check wrangler output)");
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
// Step 3: Set API key
|
|
2951
|
+
print(" Step 3: Setting API key on Worker...");
|
|
2952
|
+
let secretSet = false;
|
|
2953
|
+
try {
|
|
2954
|
+
execSync("wrangler secret put STICKY_API_KEY", {
|
|
2955
|
+
cwd: serverDir,
|
|
2956
|
+
input: apiKey,
|
|
2957
|
+
encoding: "utf-8",
|
|
2958
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2959
|
+
});
|
|
2960
|
+
print(" [OK] STICKY_API_KEY set on Worker");
|
|
2961
|
+
secretSet = true;
|
|
2962
|
+
} catch (err) {
|
|
2963
|
+
print(" [WARN] Could not set STICKY_API_KEY automatically: " + (err.message || err));
|
|
2964
|
+
if (workerUrl) print(` Set it manually: echo '${apiKey}' | wrangler secret put STICKY_API_KEY`);
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2967
|
+
// Write .env.sticky
|
|
2968
|
+
if (workerUrl) {
|
|
2969
|
+
const envPath = path.join(process.cwd(), ".env.sticky");
|
|
2970
|
+
fs.writeFileSync(envPath, `STICKY_URL=${workerUrl}\nSTICKY_API_KEY=${apiKey}\n`, "utf-8");
|
|
2971
|
+
print(" [OK] .env.sticky written with STICKY_URL and STICKY_API_KEY");
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
return { workerUrl, apiKey, secretSet };
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
function cmdDeployBackend() {
|
|
2978
|
+
printBanner();
|
|
2979
|
+
print(" Deploy Cloudflare KV Backend\n");
|
|
2980
|
+
|
|
2981
|
+
const result = provisionCloudBackend();
|
|
2982
|
+
|
|
2983
|
+
if (result === null) {
|
|
2984
|
+
print(" [ERR] `wrangler` CLI not found.");
|
|
2985
|
+
print(" Install it: npm install -g wrangler");
|
|
2986
|
+
print(" Then authenticate: wrangler login\n");
|
|
2987
|
+
process.exit(1);
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
if (result.error) {
|
|
2991
|
+
print(" [ERR] " + result.error + "\n");
|
|
2992
|
+
process.exit(1);
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
const { secretSet } = result;
|
|
2996
|
+
if (secretSet) {
|
|
2997
|
+
print("\n Share STICKY_URL and STICKY_API_KEY with your team securely");
|
|
2998
|
+
print(" (e.g. org secrets, 1Password). Do not commit .env.sticky.\n");
|
|
2999
|
+
} else {
|
|
3000
|
+
print("\n [!] API key is in .env.sticky but NOT yet set on the Worker.");
|
|
3001
|
+
print(" Run the manual command above, then share with your team.\n");
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
print("\n Backend ready! Run `npx sticky-note init` in your repos.\n");
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
// ──────────────────────────────────────────────
|
|
3008
|
+
// MIGRATE --to cloud command (V3 cloud migration)
|
|
3009
|
+
// ──────────────────────────────────────────────
|
|
3010
|
+
|
|
3011
|
+
async function cmdMigrateToCloud() {
|
|
3012
|
+
|
|
3013
|
+
printBanner();
|
|
3014
|
+
print(" Migrate V2 -> V3 Cloud\n");
|
|
3015
|
+
|
|
3016
|
+
// Check cloud config
|
|
3017
|
+
const stickyUrl = process.env.STICKY_URL || "";
|
|
3018
|
+
const stickyApiKey = process.env.STICKY_API_KEY || "";
|
|
3019
|
+
|
|
3020
|
+
// Try .env.sticky
|
|
3021
|
+
let envUrl = stickyUrl;
|
|
3022
|
+
let envKey = stickyApiKey;
|
|
3023
|
+
const envStickyPath = path.join(process.cwd(), ".env.sticky");
|
|
3024
|
+
if (!envUrl && fs.existsSync(envStickyPath)) {
|
|
3025
|
+
const raw = fs.readFileSync(envStickyPath, "utf-8");
|
|
3026
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3027
|
+
const trimmed = line.trim();
|
|
3028
|
+
if (trimmed.startsWith("STICKY_URL=")) envUrl = trimmed.slice("STICKY_URL=".length);
|
|
3029
|
+
if (trimmed.startsWith("STICKY_API_KEY=")) envKey = trimmed.slice("STICKY_API_KEY=".length);
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
|
|
3033
|
+
if (!envUrl) {
|
|
3034
|
+
print(" [ERR] STICKY_URL not found.");
|
|
3035
|
+
print(" Set it in .env.sticky or run `npx sticky-note deploy-backend` first.\n");
|
|
3036
|
+
process.exit(1);
|
|
3037
|
+
}
|
|
3038
|
+
|
|
3039
|
+
// Detect project name
|
|
3040
|
+
let projectName = "default";
|
|
3041
|
+
try {
|
|
3042
|
+
const remote = execSync("git remote get-url origin", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
3043
|
+
const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
3044
|
+
if (match) projectName = match[1];
|
|
3045
|
+
} catch (_) {}
|
|
3046
|
+
|
|
3047
|
+
const headers = {
|
|
3048
|
+
"Content-Type": "application/json",
|
|
3049
|
+
"X-Sticky-Project": projectName,
|
|
3050
|
+
};
|
|
3051
|
+
if (envKey) headers["X-Sticky-API-Key"] = envKey;
|
|
3052
|
+
|
|
3053
|
+
async function postToCloud(endpoint, body) {
|
|
3054
|
+
try {
|
|
3055
|
+
const resp = await fetch(`${envUrl}${endpoint}`, {
|
|
3056
|
+
method: "POST",
|
|
3057
|
+
headers,
|
|
3058
|
+
body: JSON.stringify(body),
|
|
3059
|
+
});
|
|
3060
|
+
return resp.ok;
|
|
3061
|
+
} catch (_) {
|
|
3062
|
+
return false;
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
|
|
3066
|
+
async function putToCloud(endpoint, body) {
|
|
3067
|
+
try {
|
|
3068
|
+
const resp = await fetch(`${envUrl}${endpoint}`, {
|
|
3069
|
+
method: "PUT",
|
|
3070
|
+
headers,
|
|
3071
|
+
body: JSON.stringify(body),
|
|
3072
|
+
});
|
|
3073
|
+
return resp.ok;
|
|
3074
|
+
} catch (_) {
|
|
3075
|
+
return false;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
print(` Project: ${projectName}`);
|
|
3080
|
+
print(` Backend: ${envUrl}\n`);
|
|
3081
|
+
|
|
3082
|
+
// Migrate threads
|
|
3083
|
+
const stickyDir = path.join(process.cwd(), ".sticky-note");
|
|
3084
|
+
const memoryPath = path.join(stickyDir, "sticky-note.json");
|
|
3085
|
+
let threadCount = 0;
|
|
3086
|
+
let threadErrors = 0;
|
|
3087
|
+
if (fs.existsSync(memoryPath)) {
|
|
3088
|
+
const memory = readJsonSafe(memoryPath, { threads: [] });
|
|
3089
|
+
const threads = memory.threads || [];
|
|
3090
|
+
print(` Migrating ${threads.length} thread(s)...`);
|
|
3091
|
+
for (const thread of threads) {
|
|
3092
|
+
const ok = await putToCloud(`/threads/${thread.id}`, thread);
|
|
3093
|
+
if (ok) threadCount++;
|
|
3094
|
+
else threadErrors++;
|
|
3095
|
+
}
|
|
3096
|
+
print(` [OK] ${threadCount} threads migrated${threadErrors ? `, ${threadErrors} failed` : ""}`);
|
|
3097
|
+
} else {
|
|
3098
|
+
print(" No sticky-note.json found — skipping threads.");
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
// Migrate audit records
|
|
3102
|
+
const auditDir = path.join(stickyDir, "audit");
|
|
3103
|
+
let auditCount = 0;
|
|
3104
|
+
let auditErrors = 0;
|
|
3105
|
+
if (fs.existsSync(auditDir)) {
|
|
3106
|
+
const files = fs.readdirSync(auditDir).filter(f => f.endsWith(".jsonl"));
|
|
3107
|
+
for (const file of files) {
|
|
3108
|
+
const raw = fs.readFileSync(path.join(auditDir, file), "utf-8");
|
|
3109
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3110
|
+
const trimmed = line.trim();
|
|
3111
|
+
if (!trimmed) continue;
|
|
3112
|
+
try {
|
|
3113
|
+
const record = JSON.parse(trimmed);
|
|
3114
|
+
const ok = await postToCloud("/audit", record);
|
|
3115
|
+
if (ok) auditCount++;
|
|
3116
|
+
else auditErrors++;
|
|
3117
|
+
} catch (_) {
|
|
3118
|
+
auditErrors++;
|
|
3119
|
+
}
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
print(` [OK] ${auditCount} audit records migrated${auditErrors ? `, ${auditErrors} failed` : ""}`);
|
|
3123
|
+
} else {
|
|
3124
|
+
print(" No audit directory found — skipping audit.");
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
// Migrate presence
|
|
3128
|
+
const presenceDir = path.join(stickyDir, "presence");
|
|
3129
|
+
let presenceCount = 0;
|
|
3130
|
+
if (fs.existsSync(presenceDir)) {
|
|
3131
|
+
const files = fs.readdirSync(presenceDir).filter(f => f.endsWith(".json"));
|
|
3132
|
+
for (const file of files) {
|
|
3133
|
+
try {
|
|
3134
|
+
const record = JSON.parse(fs.readFileSync(path.join(presenceDir, file), "utf-8"));
|
|
3135
|
+
const ok = await postToCloud("/presence", record);
|
|
3136
|
+
if (ok) presenceCount++;
|
|
3137
|
+
} catch (_) {}
|
|
3138
|
+
}
|
|
3139
|
+
if (presenceCount) print(` [OK] ${presenceCount} presence records migrated`);
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
// Migrate config
|
|
3143
|
+
const configPath = path.join(stickyDir, "sticky-note-config.json");
|
|
3144
|
+
if (fs.existsSync(configPath)) {
|
|
3145
|
+
const config = readJsonSafe(configPath, {});
|
|
3146
|
+
const ok = await putToCloud("/config", config);
|
|
3147
|
+
if (ok) print(" [OK] Config migrated");
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
print(`\n Migration complete. ${threadCount} threads, ${auditCount} audit records.\n`);
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
// ──────────────────────────────────────────────
|
|
3154
|
+
// MCP-SERVER command — launch the stdio MCP server
|
|
3155
|
+
// (delegates to bin/mcp-server.js; cloud or local determined there)
|
|
3156
|
+
// ──────────────────────────────────────────────
|
|
3157
|
+
|
|
3158
|
+
// V3 cloud-aware MCP server is kept in bin/mcp-server.js.
|
|
3159
|
+
// This stub was the inline V3 implementation; replaced by the cleaner
|
|
3160
|
+
// delegating version from main. Kept as _cmdMcpServerInline for reference.
|
|
3161
|
+
async function _cmdMcpServerInline() {
|
|
3162
|
+
// MCP server runs over stdio using JSON-RPC 2.0
|
|
3163
|
+
const stickyUrl = process.env.STICKY_URL || "";
|
|
3164
|
+
const stickyApiKey = process.env.STICKY_API_KEY || "";
|
|
3165
|
+
|
|
3166
|
+
// Try .env.sticky
|
|
3167
|
+
let envUrl = stickyUrl;
|
|
3168
|
+
let envKey = stickyApiKey;
|
|
3169
|
+
const envStickyPath = path.join(process.cwd(), ".env.sticky");
|
|
3170
|
+
if (!envUrl && fs.existsSync(envStickyPath)) {
|
|
3171
|
+
const raw = fs.readFileSync(envStickyPath, "utf-8");
|
|
3172
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3173
|
+
const trimmed = line.trim();
|
|
3174
|
+
if (trimmed.startsWith("STICKY_URL=")) envUrl = trimmed.slice("STICKY_URL=".length);
|
|
3175
|
+
if (trimmed.startsWith("STICKY_API_KEY=")) envKey = trimmed.slice("STICKY_API_KEY=".length);
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
|
|
3179
|
+
// Detect project
|
|
3180
|
+
let projectName = "default";
|
|
3181
|
+
try {
|
|
3182
|
+
const remote = execSync("git remote get-url origin", { encoding: "utf-8", stdio: "pipe" }).trim();
|
|
3183
|
+
const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
3184
|
+
if (match) projectName = match[1];
|
|
3185
|
+
} catch (_) {}
|
|
3186
|
+
|
|
3187
|
+
const baseHeaders = {
|
|
3188
|
+
"Content-Type": "application/json",
|
|
3189
|
+
"X-Sticky-Project": projectName,
|
|
3190
|
+
};
|
|
3191
|
+
if (envKey) baseHeaders["X-Sticky-API-Key"] = envKey;
|
|
3192
|
+
|
|
3193
|
+
async function cloudGet(endpoint) {
|
|
3194
|
+
if (!envUrl) return null;
|
|
3195
|
+
try {
|
|
3196
|
+
const resp = await fetch(`${envUrl}${endpoint}`, { headers: baseHeaders });
|
|
3197
|
+
if (!resp.ok) return null;
|
|
3198
|
+
return await resp.json();
|
|
3199
|
+
} catch (_) {
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
async function cloudPost(endpoint, body) {
|
|
3205
|
+
if (!envUrl) return null;
|
|
3206
|
+
try {
|
|
3207
|
+
const resp = await fetch(`${envUrl}${endpoint}`, {
|
|
3208
|
+
method: "PUT",
|
|
3209
|
+
headers: baseHeaders,
|
|
3210
|
+
body: JSON.stringify(body),
|
|
3211
|
+
});
|
|
3212
|
+
if (!resp.ok) return null;
|
|
3213
|
+
return await resp.json();
|
|
3214
|
+
} catch (_) {
|
|
3215
|
+
return null;
|
|
3216
|
+
}
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
// Fallback: read from local files
|
|
3220
|
+
const stickyDir = path.join(process.cwd(), ".sticky-note");
|
|
3221
|
+
const memoryPath = path.join(stickyDir, "sticky-note.json");
|
|
3222
|
+
|
|
3223
|
+
function localThreads(filter) {
|
|
3224
|
+
const memory = readJsonSafe(memoryPath, { threads: [] });
|
|
3225
|
+
let threads = memory.threads || [];
|
|
3226
|
+
if (filter) threads = threads.filter(filter);
|
|
3227
|
+
return threads;
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// MCP tool definitions
|
|
3231
|
+
const tools = [
|
|
3232
|
+
{ name: "get_open_threads", description: "Get all open threads", inputSchema: { type: "object", properties: {} } },
|
|
3233
|
+
{ name: "get_stuck_threads", description: "Get all stuck threads", inputSchema: { type: "object", properties: {} } },
|
|
3234
|
+
{ name: "search_threads", description: "Search threads by query", inputSchema: { type: "object", properties: { query: { type: "string" } }, required: ["query"] } },
|
|
3235
|
+
{ name: "get_session_context", description: "Get a specific thread by ID", inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] } },
|
|
3236
|
+
{ name: "write_thread", description: "Create or update a thread", inputSchema: { type: "object", properties: { thread: { type: "object" } }, required: ["thread"] } },
|
|
3237
|
+
{ name: "get_team_config", description: "Get team configuration", inputSchema: { type: "object", properties: {} } },
|
|
3238
|
+
{ name: "get_presence", description: "Get active developer presence", inputSchema: { type: "object", properties: {} } },
|
|
3239
|
+
{ name: "get_audit_trail", description: "Query audit trail", inputSchema: { type: "object", properties: { file: { type: "string" }, user: { type: "string" }, since: { type: "string" }, tool: { type: "string" } } } },
|
|
3240
|
+
];
|
|
3241
|
+
|
|
3242
|
+
// Handle MCP tool calls
|
|
3243
|
+
async function handleToolCall(name, args) {
|
|
3244
|
+
switch (name) {
|
|
3245
|
+
case "get_open_threads": {
|
|
3246
|
+
const result = await cloudGet("/threads?status=open");
|
|
3247
|
+
return result ? result.threads : localThreads(t => t.status === "open");
|
|
3248
|
+
}
|
|
3249
|
+
case "get_stuck_threads": {
|
|
3250
|
+
const result = await cloudGet("/threads?status=stuck");
|
|
3251
|
+
return result ? result.threads : localThreads(t => t.status === "stuck");
|
|
3252
|
+
}
|
|
3253
|
+
case "search_threads": {
|
|
3254
|
+
const q = args.query || "";
|
|
3255
|
+
const result = await cloudGet(`/threads?q=${encodeURIComponent(q)}`);
|
|
3256
|
+
if (result) return result.threads;
|
|
3257
|
+
const qLower = q.toLowerCase();
|
|
3258
|
+
return localThreads(t =>
|
|
3259
|
+
(t.narrative || "").toLowerCase().includes(qLower) ||
|
|
3260
|
+
(t.handoff_summary || "").toLowerCase().includes(qLower) ||
|
|
3261
|
+
(t.files_touched || []).some(f => f.toLowerCase().includes(qLower))
|
|
3262
|
+
);
|
|
3263
|
+
}
|
|
3264
|
+
case "get_session_context": {
|
|
3265
|
+
const id = args.id;
|
|
3266
|
+
const result = await cloudGet(`/threads/${id}`);
|
|
3267
|
+
if (result && !result.error) return result;
|
|
3268
|
+
const memory = readJsonSafe(memoryPath, { threads: [] });
|
|
3269
|
+
return (memory.threads || []).find(t => t.id === id || t.id.startsWith(id)) || null;
|
|
3270
|
+
}
|
|
3271
|
+
case "write_thread": {
|
|
3272
|
+
const thread = args.thread;
|
|
3273
|
+
if (envUrl) await cloudPost(`/threads/${thread.id}`, thread);
|
|
3274
|
+
return { ok: true };
|
|
3275
|
+
}
|
|
3276
|
+
case "get_team_config": {
|
|
3277
|
+
const result = await cloudGet("/config");
|
|
3278
|
+
if (result) return result;
|
|
3279
|
+
const configPath = path.join(stickyDir, "sticky-note-config.json");
|
|
3280
|
+
return readJsonSafe(configPath, {});
|
|
3281
|
+
}
|
|
3282
|
+
case "get_presence": {
|
|
3283
|
+
const result = await cloudGet("/presence");
|
|
3284
|
+
if (result) return result.presence;
|
|
3285
|
+
// Local fallback: read presence files
|
|
3286
|
+
const presDir = path.join(stickyDir, "presence");
|
|
3287
|
+
try {
|
|
3288
|
+
return fs.readdirSync(presDir).filter(f => f.endsWith(".json")).map(f =>
|
|
3289
|
+
JSON.parse(fs.readFileSync(path.join(presDir, f), "utf-8"))
|
|
3290
|
+
);
|
|
3291
|
+
} catch (_) {
|
|
3292
|
+
return [];
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
case "get_audit_trail": {
|
|
3296
|
+
const params = new URLSearchParams();
|
|
3297
|
+
if (args.file) params.set("file", args.file);
|
|
3298
|
+
if (args.user) params.set("user", args.user);
|
|
3299
|
+
if (args.since) params.set("since", args.since);
|
|
3300
|
+
if (args.tool) params.set("tool", args.tool);
|
|
3301
|
+
const qs = params.toString();
|
|
3302
|
+
const result = await cloudGet(`/audit${qs ? "?" + qs : ""}`);
|
|
3303
|
+
if (result) return result.audit;
|
|
3304
|
+
// Local fallback: grep JSONL
|
|
3305
|
+
const auditDir = path.join(stickyDir, "audit");
|
|
3306
|
+
const records = [];
|
|
3307
|
+
try {
|
|
3308
|
+
for (const f of fs.readdirSync(auditDir).filter(f => f.endsWith(".jsonl"))) {
|
|
3309
|
+
const raw = fs.readFileSync(path.join(auditDir, f), "utf-8");
|
|
3310
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
3311
|
+
if (!line.trim()) continue;
|
|
3312
|
+
try {
|
|
3313
|
+
const r = JSON.parse(line);
|
|
3314
|
+
if (args.user && r.user !== args.user) continue;
|
|
3315
|
+
if (args.file && !(r.file || "").includes(args.file)) continue;
|
|
3316
|
+
if (args.tool && r.tool !== args.tool) continue;
|
|
3317
|
+
if (args.since && new Date(r.timestamp) < new Date(args.since)) continue;
|
|
3318
|
+
records.push(r);
|
|
3319
|
+
} catch (_) {}
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
} catch (_) {}
|
|
3323
|
+
return records;
|
|
3324
|
+
}
|
|
3325
|
+
default:
|
|
3326
|
+
return { error: `Unknown tool: ${name}` };
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
|
|
3330
|
+
// MCP stdio protocol
|
|
3331
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
3332
|
+
let buffer = "";
|
|
3333
|
+
|
|
3334
|
+
rl.on("line", async (line) => {
|
|
3335
|
+
try {
|
|
3336
|
+
const msg = JSON.parse(line);
|
|
3337
|
+
|
|
3338
|
+
if (msg.method === "initialize") {
|
|
3339
|
+
const response = {
|
|
3340
|
+
jsonrpc: "2.0",
|
|
3341
|
+
id: msg.id,
|
|
3342
|
+
result: {
|
|
3343
|
+
protocolVersion: "2024-11-05",
|
|
3344
|
+
capabilities: { tools: {} },
|
|
3345
|
+
serverInfo: { name: "sticky-note", version: VERSION },
|
|
3346
|
+
},
|
|
3347
|
+
};
|
|
3348
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
3349
|
+
} else if (msg.method === "notifications/initialized") {
|
|
3350
|
+
// No response needed
|
|
3351
|
+
} else if (msg.method === "tools/list") {
|
|
3352
|
+
const response = {
|
|
3353
|
+
jsonrpc: "2.0",
|
|
3354
|
+
id: msg.id,
|
|
3355
|
+
result: { tools },
|
|
3356
|
+
};
|
|
3357
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
3358
|
+
} else if (msg.method === "tools/call") {
|
|
3359
|
+
const toolName = msg.params.name;
|
|
3360
|
+
const toolArgs = msg.params.arguments || {};
|
|
3361
|
+
const result = await handleToolCall(toolName, toolArgs);
|
|
3362
|
+
const response = {
|
|
3363
|
+
jsonrpc: "2.0",
|
|
3364
|
+
id: msg.id,
|
|
3365
|
+
result: {
|
|
3366
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
3367
|
+
},
|
|
3368
|
+
};
|
|
3369
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
3370
|
+
} else if (msg.id) {
|
|
3371
|
+
// Unknown method with id — return error
|
|
3372
|
+
const response = {
|
|
3373
|
+
jsonrpc: "2.0",
|
|
3374
|
+
id: msg.id,
|
|
3375
|
+
error: { code: -32601, message: `Unknown method: ${msg.method}` },
|
|
3376
|
+
};
|
|
3377
|
+
process.stdout.write(JSON.stringify(response) + "\n");
|
|
3378
|
+
}
|
|
3379
|
+
} catch (err) {
|
|
3380
|
+
// Parse error
|
|
3381
|
+
if (line.trim()) {
|
|
3382
|
+
process.stderr.write(`[sticky-note mcp] Parse error: ${err.message}\n`);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3387
|
+
|
|
2723
3388
|
// ──────────────────────────────────────────────
|
|
2724
3389
|
// sync — commit and optionally push .sticky-note/ files
|
|
2725
3390
|
// ──────────────────────────────────────────────
|
|
@@ -3428,16 +4093,26 @@ function cmdRunHook() {
|
|
|
3428
4093
|
}
|
|
3429
4094
|
|
|
3430
4095
|
// ──────────────────────────────────────────────
|
|
3431
|
-
// MIGRATE command
|
|
4096
|
+
// MIGRATE command (dispatcher)
|
|
4097
|
+
// Default: migrate to git data branch (from main)
|
|
4098
|
+
// --to cloud: migrate local data to Cloudflare KV (from V3)
|
|
3432
4099
|
// ──────────────────────────────────────────────
|
|
3433
4100
|
|
|
4101
|
+
async function cmdMigrate() {
|
|
4102
|
+
const args = process.argv.slice(3);
|
|
4103
|
+
if (args.includes("--to") && args.includes("cloud")) {
|
|
4104
|
+
return cmdMigrateToCloud();
|
|
4105
|
+
}
|
|
4106
|
+
return _cmdMigrateDataBranch();
|
|
4107
|
+
}
|
|
4108
|
+
|
|
3434
4109
|
/**
|
|
3435
4110
|
* One-time migration from .sticky-note/ (feature-branch storage) to
|
|
3436
4111
|
* .git/sticky-note/ (data-branch storage).
|
|
3437
4112
|
*
|
|
3438
4113
|
* Idempotent — safe to run multiple times.
|
|
3439
4114
|
*/
|
|
3440
|
-
async function
|
|
4115
|
+
async function _cmdMigrateDataBranch() {
|
|
3441
4116
|
printBanner();
|
|
3442
4117
|
|
|
3443
4118
|
if (!isGitRepo()) {
|
|
@@ -3703,6 +4378,9 @@ async function main() {
|
|
|
3703
4378
|
case "resume":
|
|
3704
4379
|
cmdResume();
|
|
3705
4380
|
break;
|
|
4381
|
+
case "transcript":
|
|
4382
|
+
cmdTranscript();
|
|
4383
|
+
break;
|
|
3706
4384
|
case "audit":
|
|
3707
4385
|
cmdAudit();
|
|
3708
4386
|
break;
|
|
@@ -3727,6 +4405,9 @@ async function main() {
|
|
|
3727
4405
|
case "checkpoint":
|
|
3728
4406
|
cmdCheckpoint();
|
|
3729
4407
|
break;
|
|
4408
|
+
case "deploy-backend":
|
|
4409
|
+
cmdDeployBackend();
|
|
4410
|
+
break;
|
|
3730
4411
|
case "overlap":
|
|
3731
4412
|
cmdOverlap();
|
|
3732
4413
|
break;
|
|
@@ -3752,10 +4433,7 @@ async function main() {
|
|
|
3752
4433
|
cmdRunHook();
|
|
3753
4434
|
break;
|
|
3754
4435
|
case "migrate":
|
|
3755
|
-
cmdMigrate()
|
|
3756
|
-
console.error(err);
|
|
3757
|
-
process.exit(1);
|
|
3758
|
-
});
|
|
4436
|
+
await cmdMigrate();
|
|
3759
4437
|
break;
|
|
3760
4438
|
case "--version":
|
|
3761
4439
|
case "-v":
|
|
@@ -3767,11 +4445,14 @@ async function main() {
|
|
|
3767
4445
|
printBanner();
|
|
3768
4446
|
print(" Usage: npx sticky-note <command>\n");
|
|
3769
4447
|
print(" Commands:");
|
|
3770
|
-
print(" init Interactive setup — creates
|
|
4448
|
+
print(" init Interactive setup — creates hooks and config");
|
|
4449
|
+
print(" init --v3 V3 setup with Cloudflare KV cloud backend");
|
|
4450
|
+
print(" init --ci Non-interactive setup for CI/GitHub Actions");
|
|
3771
4451
|
print(" update Update hook scripts (preserves data)");
|
|
3772
4452
|
print(" status Diagnostic report: threads, audit, attribution health");
|
|
3773
4453
|
print(" threads List open/stuck threads");
|
|
3774
4454
|
print(" resume Resume a previous thread (--list, --clear, <id>)");
|
|
4455
|
+
print(" transcript Show a thread's captured verbatim transcript (--list, --raw, <id>)");
|
|
3775
4456
|
print(" resume-thread Smart thread resume (--query, --user, --file, --json)");
|
|
3776
4457
|
print(" audit Query audit trail (--file, --user, --since, --session)");
|
|
3777
4458
|
print(" who Show active and recent team members");
|
|
@@ -3780,14 +4461,16 @@ async function main() {
|
|
|
3780
4461
|
print(" reset Wipe all threads and start fresh (--force, --keep-audit)");
|
|
3781
4462
|
print(" get-line-attribution File→thread attribution with line ranges (--file, --lines)");
|
|
3782
4463
|
print(" checkpoint Set work-topic checkpoint for attribution (--topic, --show, --clear)");
|
|
4464
|
+
print(" deploy-backend Provision Cloudflare KV backend (V3)");
|
|
4465
|
+
print(" migrate Migrate to git data branch (default) or cloud (--to cloud)");
|
|
3783
4466
|
print(" overlap Detect file overlaps with open/stuck threads");
|
|
3784
4467
|
print(" claim Claim ownership of files you're working on (--list, --clear)");
|
|
3785
4468
|
print(" sync Commit .sticky-note/ changes to git (--push to also push)");
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
4469
|
+
print(" pull Auto-sync .sticky-note/ then git pull (alias: git spull)");
|
|
4470
|
+
print(" bootstrap Provision MCP servers with secrets from manifest");
|
|
4471
|
+
print(" env Environment management (status, add-server, add-plugin)");
|
|
4472
|
+
print(" mcp-server Launch the sticky-note MCP server (stdio JSON-RPC)");
|
|
4473
|
+
print(" run-hook <name> Run a hook from any subdirectory (finds .claude/hooks/ up the tree)");
|
|
3791
4474
|
print("");
|
|
3792
4475
|
print(" Options:");
|
|
3793
4476
|
print(" --version Show version");
|