sticky-note-cli 3.1.32 → 3.1.34

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
 
@@ -4744,27 +4822,42 @@ async function _cmdMigrateDataBranch() {
4744
4822
  print(" [WARN] Could not commit to data branch: " + err.message);
4745
4823
  }
4746
4824
 
4747
- // Step 3: Add .sticky-note/ to .gitignore
4825
+ // Step 3: Add data files to .gitignore — but NOT sticky-note-config.json.
4826
+ // sticky-note-config.json must stay committed so teammates get sticky_url
4827
+ // on git pull (which triggers the API key prompt in `npx sticky-note init`).
4748
4828
  print("");
4749
4829
  const gitignorePath = path.join(cwd, ".gitignore");
4750
4830
  const MARKER = "# sticky-note: data branch migration";
4751
- const IGNORE_LINE = ".sticky-note/";
4831
+ // Ignore only the data files, not the whole directory.
4832
+ const IGNORE_LINES = [
4833
+ ".sticky-note/sticky-note.json",
4834
+ ".sticky-note/audit/",
4835
+ ".sticky-note/presence/",
4836
+ ];
4752
4837
  let gitignoreUpdated = false;
4753
4838
  if (fs.existsSync(gitignorePath)) {
4754
4839
  const existing = fs.readFileSync(gitignorePath, "utf-8");
4755
4840
  const gitignoreLines = existing.split(/\r?\n/).map(l => l.trim());
4756
- if (!gitignoreLines.includes(IGNORE_LINE)) {
4757
- fs.appendFileSync(gitignorePath, "\n" + MARKER + "\n" + IGNORE_LINE + "\n");
4841
+ // Also clean up the old broad ignore if present from a previous migration run
4842
+ const hasBroad = gitignoreLines.includes(".sticky-note/");
4843
+ const missingLines = IGNORE_LINES.filter(l => !gitignoreLines.includes(l));
4844
+ if (hasBroad) {
4845
+ // Replace the broad ignore with specific lines
4846
+ const replaced = existing.replace(/\.sticky-note\/\n?/, IGNORE_LINES.join("\n") + "\n");
4847
+ fs.writeFileSync(gitignorePath, replaced, "utf-8");
4848
+ gitignoreUpdated = true;
4849
+ } else if (missingLines.length > 0) {
4850
+ fs.appendFileSync(gitignorePath, "\n" + MARKER + "\n" + missingLines.join("\n") + "\n");
4758
4851
  gitignoreUpdated = true;
4759
4852
  }
4760
4853
  } else {
4761
- fs.writeFileSync(gitignorePath, MARKER + "\n" + IGNORE_LINE + "\n");
4854
+ fs.writeFileSync(gitignorePath, MARKER + "\n" + IGNORE_LINES.join("\n") + "\n");
4762
4855
  gitignoreUpdated = true;
4763
4856
  }
4764
4857
  if (gitignoreUpdated) {
4765
- print(" [OK] Added .sticky-note/ to .gitignore");
4858
+ print(" [OK] .gitignore: ignoring data files only (sticky-note-config.json stays committed)");
4766
4859
  } else {
4767
- print(" [OK] .gitignore already contains .sticky-note/");
4860
+ print(" [OK] .gitignore already up to date");
4768
4861
  }
4769
4862
 
4770
4863
  // Step 4: Remove sticky-note pre-commit and post-commit hooks
@@ -4980,6 +5073,10 @@ async function main() {
4980
5073
  case "migrate":
4981
5074
  await cmdMigrate();
4982
5075
  break;
5076
+ case "close":
5077
+ case "done":
5078
+ await cmdClose();
5079
+ break;
4983
5080
  case "--version":
4984
5081
  case "-v":
4985
5082
  print(`sticky-note v${VERSION}`);
package/bin/mcp-server.js CHANGED
@@ -706,6 +706,81 @@ function toolGetThreadContextForFiles(params) {
706
706
  };
707
707
  }
708
708
 
709
+ // ──────────────────────────────────────────────
710
+ // close_thread — explicit goal completion signal
711
+ // ──────────────────────────────────────────────
712
+
713
+ async function toolCloseThread(params) {
714
+ const { summary } = params;
715
+ if (!summary || !summary.trim()) {
716
+ return { error: "summary is required" };
717
+ }
718
+
719
+ // Find current session ID from .sticky-session file
720
+ const sessionFile = path.join(stickyDir(), ".sticky-session");
721
+ let sessionId = null;
722
+ if (fs.existsSync(sessionFile)) {
723
+ sessionId = fs.readFileSync(sessionFile, "utf-8").trim();
724
+ }
725
+ if (!sessionId) {
726
+ return { error: "No active session found — .sticky-note/.sticky-session missing or empty" };
727
+ }
728
+
729
+ // Find thread by session_id in local memory
730
+ const memoryPath = path.join(stickyDir(), "sticky-note.json");
731
+ const memory = readJsonSafe(memoryPath, { version: "2", threads: [] });
732
+ const threads = memory.threads || [];
733
+ const thread = threads.find(t => t.session_id === sessionId);
734
+
735
+ if (!thread) {
736
+ return { error: `No thread found for session ${sessionId}` };
737
+ }
738
+
739
+ const now = new Date().toISOString();
740
+ thread.status = "closed";
741
+ thread.goal_achieved = true;
742
+ thread.handoff_summary = summary.trim();
743
+ thread.closed_at = now;
744
+ thread.last_activity_at = now;
745
+ thread.last_note = summary.trim().substring(0, 200);
746
+
747
+ // Save locally
748
+ memory.threads = threads;
749
+ try {
750
+ fs.writeFileSync(memoryPath, JSON.stringify(memory, null, 2) + "\n");
751
+ } catch (err) {
752
+ return { error: "Failed to save: " + err.message };
753
+ }
754
+
755
+ // Push to cloud if configured
756
+ const { url, apiKey } = getCloudConfig();
757
+ if (url) {
758
+ try {
759
+ const headers = {
760
+ "Content-Type": "application/json",
761
+ "X-Sticky-Project": getProjectName(),
762
+ };
763
+ if (apiKey) headers["X-Sticky-API-Key"] = apiKey;
764
+ const controller = new AbortController();
765
+ const t = setTimeout(() => controller.abort(), 8000);
766
+ await fetch(`${url}/threads/${thread.id}`, {
767
+ method: "PUT",
768
+ headers,
769
+ body: JSON.stringify(thread),
770
+ signal: controller.signal,
771
+ }).finally(() => clearTimeout(t));
772
+ } catch (_) { /* non-fatal — local save already succeeded */ }
773
+ }
774
+
775
+ return {
776
+ status: "closed",
777
+ goal_achieved: true,
778
+ thread_id: thread.id,
779
+ summary: summary.trim(),
780
+ message: "Thread marked complete. Thank you — this signal helps improve future sessions.",
781
+ };
782
+ }
783
+
709
784
  // ──────────────────────────────────────────────
710
785
  // Tool registry
711
786
  // ──────────────────────────────────────────────
@@ -852,6 +927,22 @@ const TOOLS = {
852
927
  },
853
928
  handler: toolGetThreadContextForFiles,
854
929
  },
930
+ close_thread: {
931
+ description:
932
+ "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.",
933
+ inputSchema: {
934
+ type: "object",
935
+ properties: {
936
+ summary: {
937
+ type: "string",
938
+ description:
939
+ "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.",
940
+ },
941
+ },
942
+ required: ["summary"],
943
+ },
944
+ handler: toolCloseThread,
945
+ },
855
946
  };
856
947
 
857
948
  // ──────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sticky-note-cli",
3
- "version": "3.1.32",
3
+ "version": "3.1.34",
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
  }