sticky-note-cli 3.1.33 → 3.1.35
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 +82 -0
- package/bin/mcp-server.js +117 -2
- package/package.json +1 -1
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite-shm +0 -0
- package/sticky-server/.wrangler/state/v3/observability/miniflare-wobs-trace-store/a590acd76969f996ec6e4b599c3c09f58c283a76f2d61392b5d3046caf557602.sqlite-wal +0 -0
- package/templates/CLAUDE.md +8 -0
- package/templates/hooks/session-end.js +3 -1
package/bin/cli.js
CHANGED
|
@@ -3462,6 +3462,84 @@ function cmdDeployBackend() {
|
|
|
3462
3462
|
}
|
|
3463
3463
|
|
|
3464
3464
|
// ──────────────────────────────────────────────
|
|
3465
|
+
// CLOSE command — explicit goal completion signal
|
|
3466
|
+
// ──────────────────────────────────────────────
|
|
3467
|
+
|
|
3468
|
+
async function cmdClose() {
|
|
3469
|
+
const summary = process.argv.slice(3).join(" ").trim();
|
|
3470
|
+
if (!summary) {
|
|
3471
|
+
print(" Usage: npx sticky-note close \"what was accomplished\"");
|
|
3472
|
+
print(" Also: npx sticky-note done \"what was accomplished\"");
|
|
3473
|
+
process.exit(1);
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
const stickyDir = getActiveStickyDir();
|
|
3477
|
+
const sessionFile = path.join(stickyDir, ".sticky-session");
|
|
3478
|
+
if (!fs.existsSync(sessionFile)) {
|
|
3479
|
+
print(" [ERR] No active session found (.sticky-session missing).");
|
|
3480
|
+
print(" This command is meant to be called during an active Claude Code session.");
|
|
3481
|
+
process.exit(1);
|
|
3482
|
+
}
|
|
3483
|
+
const sessionId = fs.readFileSync(sessionFile, "utf-8").trim();
|
|
3484
|
+
if (!sessionId) {
|
|
3485
|
+
print(" [ERR] .sticky-session is empty.");
|
|
3486
|
+
process.exit(1);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
const memoryPath = path.join(stickyDir, "sticky-note.json");
|
|
3490
|
+
const memory = readJsonSafe(memoryPath, { version: "2", threads: [] });
|
|
3491
|
+
const threads = memory.threads || [];
|
|
3492
|
+
const thread = threads.find(t => t.session_id === sessionId);
|
|
3493
|
+
|
|
3494
|
+
if (!thread) {
|
|
3495
|
+
print(` [ERR] No thread found for session ${sessionId}.`);
|
|
3496
|
+
process.exit(1);
|
|
3497
|
+
}
|
|
3498
|
+
|
|
3499
|
+
const now = new Date().toISOString();
|
|
3500
|
+
thread.status = "closed";
|
|
3501
|
+
thread.goal_achieved = true;
|
|
3502
|
+
thread.handoff_summary = summary;
|
|
3503
|
+
thread.closed_at = now;
|
|
3504
|
+
thread.last_activity_at = now;
|
|
3505
|
+
thread.last_note = summary.substring(0, 200);
|
|
3506
|
+
|
|
3507
|
+
memory.threads = threads;
|
|
3508
|
+
fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + "\n");
|
|
3509
|
+
print(` [OK] Thread ${thread.id.slice(0, 8)} marked complete.`);
|
|
3510
|
+
print(` [OK] goal_achieved = true`);
|
|
3511
|
+
print(` [OK] Handoff: ${summary.substring(0, 120)}`);
|
|
3512
|
+
|
|
3513
|
+
// Push to cloud
|
|
3514
|
+
const envStickyPath = path.join(process.cwd(), ".env.sticky");
|
|
3515
|
+
if (fs.existsSync(envStickyPath)) {
|
|
3516
|
+
let stickyUrl = "", stickyApiKey = "";
|
|
3517
|
+
for (const line of fs.readFileSync(envStickyPath, "utf-8").split(/\r?\n/)) {
|
|
3518
|
+
const t = line.trim();
|
|
3519
|
+
if (t.startsWith("STICKY_URL=")) stickyUrl = t.slice("STICKY_URL=".length);
|
|
3520
|
+
if (t.startsWith("STICKY_API_KEY=")) stickyApiKey = t.slice("STICKY_API_KEY=".length);
|
|
3521
|
+
}
|
|
3522
|
+
if (stickyUrl) {
|
|
3523
|
+
try {
|
|
3524
|
+
let projectName = "default";
|
|
3525
|
+
try {
|
|
3526
|
+
const remote = execFileSync("git", ["remote", "get-url", "origin"], {
|
|
3527
|
+
encoding: "utf-8", timeout: 3000, stdio: ["pipe", "pipe", "pipe"],
|
|
3528
|
+
}).trim();
|
|
3529
|
+
const match = remote.match(/[:/]([^/]+\/[^/]+?)(?:\.git)?$/);
|
|
3530
|
+
if (match) projectName = match[1];
|
|
3531
|
+
} catch (_) {}
|
|
3532
|
+
const headers = { "Content-Type": "application/json", "X-Sticky-Project": projectName };
|
|
3533
|
+
if (stickyApiKey) headers["X-Sticky-API-Key"] = stickyApiKey;
|
|
3534
|
+
await fetch(`${stickyUrl}/threads/${thread.id}`, {
|
|
3535
|
+
method: "PUT", headers, body: JSON.stringify(thread),
|
|
3536
|
+
});
|
|
3537
|
+
print(` [OK] Pushed to cloud.`);
|
|
3538
|
+
} catch (_) { /* non-fatal */ }
|
|
3539
|
+
}
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3465
3543
|
// MIGRATE --to cloud command (V3 cloud migration)
|
|
3466
3544
|
// ──────────────────────────────────────────────
|
|
3467
3545
|
|
|
@@ -4995,6 +5073,10 @@ async function main() {
|
|
|
4995
5073
|
case "migrate":
|
|
4996
5074
|
await cmdMigrate();
|
|
4997
5075
|
break;
|
|
5076
|
+
case "close":
|
|
5077
|
+
case "done":
|
|
5078
|
+
await cmdClose();
|
|
5079
|
+
break;
|
|
4998
5080
|
case "--version":
|
|
4999
5081
|
case "-v":
|
|
5000
5082
|
print(`sticky-note v${VERSION}`);
|
package/bin/mcp-server.js
CHANGED
|
@@ -359,10 +359,34 @@ function toolSearchThreads(params) {
|
|
|
359
359
|
return q.split(/\s+/).every((word) => searchable.includes(word));
|
|
360
360
|
});
|
|
361
361
|
|
|
362
|
+
// Return summarized thread objects — full objects can exceed token limits
|
|
363
|
+
// when failed_approaches accumulates dozens of entries per thread.
|
|
364
|
+
const summarized = matches.map((t) => ({
|
|
365
|
+
id: t.id,
|
|
366
|
+
status: t.status,
|
|
367
|
+
user: t.user,
|
|
368
|
+
tool: t.tool,
|
|
369
|
+
branch: t.branch,
|
|
370
|
+
work_type: t.work_type,
|
|
371
|
+
goal_achieved: t.goal_achieved || false,
|
|
372
|
+
created_at: t.created_at,
|
|
373
|
+
last_activity_at: t.last_activity_at,
|
|
374
|
+
narrative: (t.narrative || "").substring(0, 300),
|
|
375
|
+
last_note: (t.last_note || "").substring(0, 200),
|
|
376
|
+
handoff_summary: (t.handoff_summary || "").substring(0, 300),
|
|
377
|
+
files_touched: (t.files_touched || []).slice(0, 20),
|
|
378
|
+
failed_approaches: (t.failed_approaches || []).slice(-3).map((f) => ({
|
|
379
|
+
description: typeof f === "string"
|
|
380
|
+
? f.substring(0, 150)
|
|
381
|
+
: (f.description || "").substring(0, 150),
|
|
382
|
+
error: typeof f === "string" ? "" : (f.error || "").substring(0, 100),
|
|
383
|
+
})),
|
|
384
|
+
}));
|
|
385
|
+
|
|
362
386
|
return {
|
|
363
387
|
query,
|
|
364
|
-
count:
|
|
365
|
-
threads:
|
|
388
|
+
count: summarized.length,
|
|
389
|
+
threads: summarized,
|
|
366
390
|
};
|
|
367
391
|
}
|
|
368
392
|
|
|
@@ -706,6 +730,81 @@ function toolGetThreadContextForFiles(params) {
|
|
|
706
730
|
};
|
|
707
731
|
}
|
|
708
732
|
|
|
733
|
+
// ──────────────────────────────────────────────
|
|
734
|
+
// close_thread — explicit goal completion signal
|
|
735
|
+
// ──────────────────────────────────────────────
|
|
736
|
+
|
|
737
|
+
async function toolCloseThread(params) {
|
|
738
|
+
const { summary } = params;
|
|
739
|
+
if (!summary || !summary.trim()) {
|
|
740
|
+
return { error: "summary is required" };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Find current session ID from .sticky-session file
|
|
744
|
+
const sessionFile = path.join(stickyDir(), ".sticky-session");
|
|
745
|
+
let sessionId = null;
|
|
746
|
+
if (fs.existsSync(sessionFile)) {
|
|
747
|
+
sessionId = fs.readFileSync(sessionFile, "utf-8").trim();
|
|
748
|
+
}
|
|
749
|
+
if (!sessionId) {
|
|
750
|
+
return { error: "No active session found — .sticky-note/.sticky-session missing or empty" };
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Find thread by session_id in local memory
|
|
754
|
+
const memoryPath = path.join(stickyDir(), "sticky-note.json");
|
|
755
|
+
const memory = readJsonSafe(memoryPath, { version: "2", threads: [] });
|
|
756
|
+
const threads = memory.threads || [];
|
|
757
|
+
const thread = threads.find(t => t.session_id === sessionId);
|
|
758
|
+
|
|
759
|
+
if (!thread) {
|
|
760
|
+
return { error: `No thread found for session ${sessionId}` };
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const now = new Date().toISOString();
|
|
764
|
+
thread.status = "closed";
|
|
765
|
+
thread.goal_achieved = true;
|
|
766
|
+
thread.handoff_summary = summary.trim();
|
|
767
|
+
thread.closed_at = now;
|
|
768
|
+
thread.last_activity_at = now;
|
|
769
|
+
thread.last_note = summary.trim().substring(0, 200);
|
|
770
|
+
|
|
771
|
+
// Save locally
|
|
772
|
+
memory.threads = threads;
|
|
773
|
+
try {
|
|
774
|
+
fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + "\n");
|
|
775
|
+
} catch (err) {
|
|
776
|
+
return { error: "Failed to save: " + err.message };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// Push to cloud if configured
|
|
780
|
+
const { url, apiKey } = getCloudConfig();
|
|
781
|
+
if (url) {
|
|
782
|
+
try {
|
|
783
|
+
const headers = {
|
|
784
|
+
"Content-Type": "application/json",
|
|
785
|
+
"X-Sticky-Project": getProjectName(),
|
|
786
|
+
};
|
|
787
|
+
if (apiKey) headers["X-Sticky-API-Key"] = apiKey;
|
|
788
|
+
const controller = new AbortController();
|
|
789
|
+
const t = setTimeout(() => controller.abort(), 8000);
|
|
790
|
+
await fetch(`${url}/threads/${thread.id}`, {
|
|
791
|
+
method: "PUT",
|
|
792
|
+
headers,
|
|
793
|
+
body: JSON.stringify(thread),
|
|
794
|
+
signal: controller.signal,
|
|
795
|
+
}).finally(() => clearTimeout(t));
|
|
796
|
+
} catch (_) { /* non-fatal — local save already succeeded */ }
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return {
|
|
800
|
+
status: "closed",
|
|
801
|
+
goal_achieved: true,
|
|
802
|
+
thread_id: thread.id,
|
|
803
|
+
summary: summary.trim(),
|
|
804
|
+
message: "Thread marked complete. Thank you — this signal helps improve future sessions.",
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
|
|
709
808
|
// ──────────────────────────────────────────────
|
|
710
809
|
// Tool registry
|
|
711
810
|
// ──────────────────────────────────────────────
|
|
@@ -852,6 +951,22 @@ const TOOLS = {
|
|
|
852
951
|
},
|
|
853
952
|
handler: toolGetThreadContextForFiles,
|
|
854
953
|
},
|
|
954
|
+
close_thread: {
|
|
955
|
+
description:
|
|
956
|
+
"Mark the current session's thread as explicitly completed by Claude. Sets goal_achieved=true and records a handoff summary. Call this when you have fully completed the user's stated goal — not just when the session ends. This is the signal used to measure whether AI sessions actually succeed.",
|
|
957
|
+
inputSchema: {
|
|
958
|
+
type: "object",
|
|
959
|
+
properties: {
|
|
960
|
+
summary: {
|
|
961
|
+
type: "string",
|
|
962
|
+
description:
|
|
963
|
+
"What was accomplished. Be specific: what was built/fixed/answered, what files were changed, what the outcome was. This becomes the handoff_summary for teammates.",
|
|
964
|
+
},
|
|
965
|
+
},
|
|
966
|
+
required: ["summary"],
|
|
967
|
+
},
|
|
968
|
+
handler: toolCloseThread,
|
|
969
|
+
},
|
|
855
970
|
};
|
|
856
971
|
|
|
857
972
|
// ──────────────────────────────────────────────
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/templates/CLAUDE.md
CHANGED
|
@@ -25,6 +25,7 @@ cat .sticky-note/sticky-note.json
|
|
|
25
25
|
| `narrative` | Summary of what happened |
|
|
26
26
|
| `failed_approaches`| What was tried and didn't work |
|
|
27
27
|
| `handoff_summary` | Handoff notes for teammates |
|
|
28
|
+
| `goal_achieved` | true if Claude explicitly completed the stated goal (via close_thread) |
|
|
28
29
|
| `last_note` | Most recent status note |
|
|
29
30
|
| `work_type` | bug-fix, feature, debugging, refactor, etc.|
|
|
30
31
|
| `prompts` | User prompts from the session (for cross-tool resume) |
|
|
@@ -268,6 +269,13 @@ You have access to a `sticky-note` MCP server. Use it:
|
|
|
268
269
|
`get_full_transcript(id)` to get the verbatim session transcript(s) for
|
|
269
270
|
a thread, when `narrative` or `prompts` aren't enough detail.
|
|
270
271
|
|
|
272
|
+
6. **When you have fully completed the user's stated goal**: Call
|
|
273
|
+
`close_thread(summary)` with a specific description of what was accomplished.
|
|
274
|
+
This is the most important signal sticky-note collects — it distinguishes
|
|
275
|
+
sessions where the goal was actually achieved from sessions that merely ended.
|
|
276
|
+
Be specific: what was built/fixed, which files changed, what the outcome was.
|
|
277
|
+
Do NOT call this if the task is only partially done or if you are unsure.
|
|
278
|
+
|
|
271
279
|
These tools are the primary way sticky-note communicates with you.
|
|
272
280
|
Hook-injected context supplements but does not replace MCP tool calls.
|
|
273
281
|
|
|
@@ -1141,7 +1141,9 @@ async function main() {
|
|
|
1141
1141
|
existing.last_activity_at = now;
|
|
1142
1142
|
// Copilot CLI fires SessionEnd per-turn, not per-session.
|
|
1143
1143
|
// Keep thread open so it isn't prematurely closed between turns.
|
|
1144
|
-
|
|
1144
|
+
// If goal_achieved is already set (via close_thread MCP or CLI), the
|
|
1145
|
+
// thread was explicitly completed — preserve that signal, don't overwrite.
|
|
1146
|
+
if (!isCopilotCli && !existing.goal_achieved) {
|
|
1145
1147
|
existing.status = "closed";
|
|
1146
1148
|
existing.closed_at = now;
|
|
1147
1149
|
}
|