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 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: matches.length,
365
- threads: matches,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "3.1.33",
3
+ "version": "3.1.35",
4
4
  "description": "Human-to-human handoff for AI coding assistants. Git-backed shared memory for Claude Code and Copilot CLI.",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -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
- if (!isCopilotCli) {
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
  }