pi-anchor 0.2.4 → 0.4.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/dist/index.cjs CHANGED
@@ -30,46 +30,35 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- AnchorProtocol: () => AnchorProtocol,
34
33
  AnchorStore: () => AnchorStore,
35
34
  COMMENT_FORMATS: () => COMMENT_FORMATS,
36
- DEFAULT_DECAY_POLICY: () => DEFAULT_DECAY_POLICY,
37
- DURABLE_DECAY_POLICY: () => DURABLE_DECAY_POLICY,
38
- EPHEMERAL_DECAY_POLICY: () => EPHEMERAL_DECAY_POLICY,
39
- SessionTouchObserver: () => SessionTouchObserver,
35
+ MS_PER_DAY: () => MS_PER_DAY,
36
+ QUIET_AFTER_DAYS: () => QUIET_AFTER_DAYS,
40
37
  acquireSyncLock: () => acquireSyncLock,
41
38
  atomicRenameWithRetry: () => atomicRenameWithRetry,
42
- classifyAnchor: () => classifyAnchor,
43
- detectDurability: () => detectDurability,
44
- detectRecurrence: () => detectRecurrence,
45
- detectTargetDate: () => detectTargetDate,
46
- evaluateAnchorDecay: () => evaluateAnchorDecay,
39
+ extractPathsFromText: () => extractPathsFromText,
47
40
  findMatchedAnchors: () => findMatchedAnchors,
48
- formatCreationTime: () => formatCreationTime,
49
41
  formatOrigin: () => formatOrigin,
50
- formatRelativeTime: () => formatRelativeTime,
51
- formatRemainingTtl: () => formatRemainingTtl,
52
- formatSettlementCard: () => formatSettlementCard,
53
- formatTargetDate: () => formatTargetDate,
54
- generateSettlementProposals: () => generateSettlementProposals,
42
+ formatRelativeAge: () => formatRelativeAge,
55
43
  getDefaultStorageDir: () => getDefaultStorageDir,
56
44
  getDisplayWidth: () => getDisplayWidth,
57
- getEphemeralDecayPolicy: () => getEphemeralDecayPolicy,
58
- getTodayDateString: () => getTodayDateString,
59
45
  globToRegExp: () => globToRegExp,
60
- groupAnchorsByQuadrant: () => groupAnchorsByQuadrant,
61
46
  hasActiveUI: () => hasActiveUI,
47
+ isAnchorTargetingWorkspace: () => isAnchorTargetingWorkspace,
62
48
  isSameOrSubPath: () => isSameOrSubPath,
63
49
  makeSafeTaskAnnotation: () => makeSafeTaskAnnotation,
64
50
  matchAnchorAgainstTouchedFiles: () => matchAnchorAgainstTouchedFiles,
51
+ matchesCommitMessage: () => matchesCommitMessage,
65
52
  normalizePath: () => normalizePath,
66
53
  openAnchorDashboard: () => openAnchorDashboard,
67
54
  padToWidth: () => padToWidth,
55
+ parseAnchorCommandInput: () => parseAnchorCommandInput,
68
56
  renderActiveAnchorsContext: () => renderActiveAnchorsContext,
69
57
  renderColdStartAnchorsContext: () => renderColdStartAnchorsContext,
70
- runPhysicalVerification: () => runPhysicalVerification,
58
+ resolveAnchorTarget: () => resolveAnchorTarget,
59
+ sortAnchorsForDisplay: () => sortAnchorsForDisplay,
71
60
  stripAnsi: () => stripAnsi,
72
- sweepStore: () => sweepStore,
61
+ sweepQuietNotes: () => sweepQuietNotes,
73
62
  truncateToWidth: () => truncateToWidth,
74
63
  updateAnchorStatusBar: () => updateAnchorStatusBar,
75
64
  updateStartupBanner: () => updateStartupBanner
@@ -77,38 +66,13 @@ __export(index_exports, {
77
66
  module.exports = __toCommonJS(index_exports);
78
67
 
79
68
  // src/types.ts
80
- var EPHEMERAL_DECAY_POLICY = {
81
- activeDays: 1,
82
- // 24 hours active
83
- sleepDays: 1,
84
- // 24 hours sleeping
85
- graveyardDays: 2
86
- // 48 hours total before auto-clearing
87
- };
88
- var DURABLE_DECAY_POLICY = {
89
- activeDays: 3,
90
- sleepDays: 30,
91
- graveyardDays: 9999
92
- // Never permanently auto-dropped; preserved indefinitely
93
- };
94
- var DEFAULT_DECAY_POLICY = {
95
- activeDays: 3,
96
- sleepDays: 7,
97
- graveyardDays: 14
98
- };
69
+ var QUIET_AFTER_DAYS = 7;
70
+ var MS_PER_DAY = 864e5;
99
71
 
100
72
  // src/store.ts
101
73
  var import_node_fs = __toESM(require("fs"), 1);
102
- var import_node_path2 = __toESM(require("path"), 1);
103
- var import_node_os = __toESM(require("os"), 1);
104
-
105
- // src/matcher.ts
106
74
  var import_node_path = __toESM(require("path"), 1);
107
- function globToRegExp(pattern) {
108
- const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
109
- const escaped = collapsed.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/(?:[^/]+/)+)").replace(/___GLOB_STAR_STAR___/g, ".*");
110
- return new RegExp(`^${escaped}$`);
111
- }
75
+ var import_node_os = __toESM(require("os"), 1);
112
76
  function normalizePath(filePath, baseCwd) {
113
77
  if (typeof filePath !== "string") return "";
114
78
  let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
@@ -124,14 +88,51 @@ function normalizePath(filePath, baseCwd) {
124
88
  }
125
89
  return norm;
126
90
  }
127
- function isSameOrSubPath(anchorCwd, targetCwd) {
128
- if (!anchorCwd || !targetCwd) return false;
129
- const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
130
- const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
131
- const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
132
- const cmpA = isWinStyle ? normA.toLowerCase() : normA;
133
- const cmpT = isWinStyle ? normT.toLowerCase() : normT;
134
- return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
91
+ function isSameOrSubPath(dirA, dirB) {
92
+ if (!dirA || !dirB) return false;
93
+ const normA = dirA.trim().replace(/\\/g, "/").replace(/\/+$/, "");
94
+ const normB = dirB.trim().replace(/\\/g, "/").replace(/\/+$/, "");
95
+ const isWin = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normB);
96
+ const a = isWin ? normA.toLowerCase() : normA;
97
+ const b = isWin ? normB.toLowerCase() : normB;
98
+ return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
99
+ }
100
+ function globToRegExp(pattern) {
101
+ const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
102
+ const escaped = collapsed.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\/\*\*\//g, "___GLOB_DIR_SLASH___").replace(/\*\*/g, "___GLOB_STAR_STAR___").replace(/\*/g, "[^/]*").replace(/___GLOB_DIR_SLASH___/g, "(?:/|/(?:[^/]+/)+)").replace(/___GLOB_STAR_STAR___/g, ".*");
103
+ return new RegExp(`^${escaped}$`);
104
+ }
105
+ function extractPathsFromText(text) {
106
+ if (!text) return [];
107
+ const results = /* @__PURE__ */ new Set();
108
+ const regex = /(?:[a-zA-Z]:[\\/][^\s"'`,。;!?、,;)>\]]+|(?:\b(?:src|lib|dist|tests|test|docs|scripts|packages|extensions|bin|assets)[\\/][^\s"'`,。;!?、,;)>\]]+)|(?:\b[\w.-]+\.(?:ts|tsx|js|jsx|mjs|cjs|py|rs|go|java|c|cpp|h|cs|md|json|yaml|yml|toml|html|css|sql|sh)\b))/g;
109
+ let match;
110
+ while ((match = regex.exec(text)) !== null) {
111
+ const cleaned = normalizePath(match[0].replace(/[.:;,'")\]}>]+$/, ""));
112
+ if (cleaned && cleaned.length >= 3) {
113
+ results.add(cleaned);
114
+ }
115
+ }
116
+ return Array.from(results);
117
+ }
118
+ function isAnchorTargetingWorkspace(anchor, targetCwd) {
119
+ if (!targetCwd) return true;
120
+ const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
121
+ const targetBase = import_node_path.default.basename(normTarget).toLowerCase();
122
+ if (anchor.project && anchor.project !== "global") {
123
+ const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
124
+ if (isSameOrSubPath(normProj, normTarget)) return true;
125
+ if (import_node_path.default.basename(normProj).toLowerCase() === targetBase) return true;
126
+ }
127
+ if (anchor.files && anchor.files.length > 0) {
128
+ for (const f of anchor.files) {
129
+ if (isSameOrSubPath(f, normTarget)) return true;
130
+ }
131
+ }
132
+ if (anchor.cwd && isSameOrSubPath(anchor.cwd, normTarget)) {
133
+ return true;
134
+ }
135
+ return false;
135
136
  }
136
137
  function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
137
138
  if (!anchor.files || anchor.files.length === 0) return null;
@@ -146,12 +147,9 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
146
147
  matchedExact.push(touched);
147
148
  break;
148
149
  }
149
- if (normPattern.includes("*")) {
150
- const regex = globToRegExp(normPattern);
151
- if (regex.test(touched)) {
152
- matchedExact.push(touched);
153
- break;
154
- }
150
+ if (normPattern.includes("*") && globToRegExp(normPattern).test(touched)) {
151
+ matchedExact.push(touched);
152
+ break;
155
153
  }
156
154
  const dirPrefix = normPattern.endsWith("/") ? normPattern : normPattern + "/";
157
155
  if (touched.startsWith(dirPrefix)) {
@@ -161,193 +159,137 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
161
159
  }
162
160
  }
163
161
  if (matchedExact.length > 0) {
164
- return {
165
- anchor,
166
- score: 1,
167
- matchedFiles: Array.from(new Set(matchedExact)),
168
- reason: "exact-file"
169
- };
162
+ return { anchor, score: 1, matchedFiles: Array.from(new Set(matchedExact)), reason: "exact-file" };
170
163
  }
171
164
  if (matchedPrefix.length > 0) {
172
- return {
173
- anchor,
174
- score: 0.85,
175
- matchedFiles: Array.from(new Set(matchedPrefix)),
176
- reason: "dir-prefix"
177
- };
165
+ return { anchor, score: 0.85, matchedFiles: Array.from(new Set(matchedPrefix)), reason: "dir-prefix" };
178
166
  }
179
167
  return null;
180
168
  }
181
169
  function findMatchedAnchors(anchors, touchedFiles) {
182
- const priorityWeight = { p0: 3, p1: 2, p2: 1 };
183
170
  const results = [];
184
171
  for (const anchor of anchors) {
185
- const match = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
186
- if (match) {
187
- results.push(match);
188
- }
172
+ const m = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
173
+ if (m) results.push(m);
189
174
  }
190
- return results.sort((a, b) => {
191
- if (b.score !== a.score) {
192
- return b.score - a.score;
193
- }
194
- const weightA = priorityWeight[a.anchor.priority] || 0;
195
- const weightB = priorityWeight[b.anchor.priority] || 0;
196
- if (weightB !== weightA) {
197
- return weightB - weightA;
198
- }
199
- return b.anchor.lastTouchedAt - a.anchor.lastTouchedAt;
200
- });
175
+ return results.sort((a, b) => b.score - a.score);
201
176
  }
202
-
203
- // src/store.ts
204
- function detectDurability(title) {
205
- const temporalKeywords = [
206
- "\u4ECA\u5929",
207
- "\u4ECA\u65E5",
208
- "\u665A\u4E0A",
209
- "\u4ECA\u665A",
210
- "\u660E\u5929",
211
- "\u540E\u5929",
212
- "\u5927\u540E\u5929",
213
- "\u7A0D\u540E",
214
- "\u5F85\u4F1A",
215
- "\u4E0B\u5348",
216
- "\u4E0A\u5348",
217
- "\u4E34\u65F6",
218
- "\u4E00\u4F1A\u513F",
219
- "\u7B49\u7B49",
220
- "today",
221
- "tonight",
222
- "tomorrow",
223
- "later",
224
- "temp",
225
- "soon"
226
- ];
227
- const lower = title.toLowerCase();
228
- for (const kw of temporalKeywords) {
229
- if (lower.includes(kw)) {
230
- return "ephemeral";
177
+ var COMMENT_FORMATS = {
178
+ ".ts": (m) => `// ${m}`,
179
+ ".tsx": (m) => `// ${m}`,
180
+ ".js": (m) => `// ${m}`,
181
+ ".jsx": (m) => `// ${m}`,
182
+ ".mjs": (m) => `// ${m}`,
183
+ ".cjs": (m) => `// ${m}`,
184
+ ".go": (m) => `// ${m}`,
185
+ ".rs": (m) => `// ${m}`,
186
+ ".c": (m) => `// ${m}`,
187
+ ".cpp": (m) => `// ${m}`,
188
+ ".h": (m) => `// ${m}`,
189
+ ".cs": (m) => `// ${m}`,
190
+ ".java": (m) => `// ${m}`,
191
+ ".kt": (m) => `// ${m}`,
192
+ ".swift": (m) => `// ${m}`,
193
+ ".dart": (m) => `// ${m}`,
194
+ ".py": (m) => `# ${m}`,
195
+ ".rb": (m) => `# ${m}`,
196
+ ".sh": (m) => `# ${m}`,
197
+ ".bash": (m) => `# ${m}`,
198
+ ".yaml": (m) => `# ${m}`,
199
+ ".yml": (m) => `# ${m}`,
200
+ ".toml": (m) => `# ${m}`,
201
+ ".ps1": (m) => `# ${m}`,
202
+ ".html": (m) => `<!-- ${m} -->`,
203
+ ".xml": (m) => `<!-- ${m} -->`,
204
+ ".svg": (m) => `<!-- ${m} -->`,
205
+ ".vue": (m) => `<!-- ${m} -->`,
206
+ ".svelte": (m) => `<!-- ${m} -->`,
207
+ ".css": (m) => `/* ${m} */`,
208
+ ".scss": (m) => `/* ${m} */`,
209
+ ".sql": (m) => `-- ${m}`,
210
+ ".lua": (m) => `-- ${m}`
211
+ };
212
+ function makeSafeTaskAnnotation(filePath, anchor) {
213
+ const ext = import_node_path.default.extname(filePath).toLowerCase();
214
+ const fmt = COMMENT_FORMATS[ext];
215
+ if (!fmt) return null;
216
+ const num = anchor.id.replace(/^anc-/, "");
217
+ return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
218
+ }
219
+ function matchesCommitMessage(anchor, commitMessages) {
220
+ const idLower = anchor.id.toLowerCase();
221
+ const numId = `#${anchor.id.replace(/^anc-/, "")}`;
222
+ const title = anchor.title.toLowerCase();
223
+ let titleTokens = [];
224
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
225
+ try {
226
+ const seg = new Intl.Segmenter("und", { granularity: "word" });
227
+ titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
228
+ } catch {
229
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
231
230
  }
231
+ } else {
232
+ titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
232
233
  }
233
- return "durable";
234
- }
235
- function getEphemeralDecayPolicy(title) {
236
- const lower = title.toLowerCase();
237
- if (lower.includes("\u5927\u540E\u5929")) {
238
- return { activeDays: 3, sleepDays: 1, graveyardDays: 4 };
239
- }
240
- if (lower.includes("\u540E\u5929")) {
241
- return { activeDays: 2, sleepDays: 1, graveyardDays: 3 };
242
- }
243
- return { ...EPHEMERAL_DECAY_POLICY };
244
- }
245
- function detectTargetDate(titleOrDate, now = Date.now()) {
246
- const lower = titleOrDate.toLowerCase().trim();
247
- const MS_DAY = 24 * 60 * 60 * 1e3;
248
- if (lower.includes("\u4ECA\u5929") || lower.includes("\u4ECA\u65E5") || lower.includes("\u4ECA\u665A") || lower === "today" || lower === "tonight") {
249
- return getTodayDateString(now);
250
- }
251
- if (lower.includes("\u660E\u5929") || lower === "tomorrow") {
252
- return getTodayDateString(now + MS_DAY);
253
- }
254
- if (lower.includes("\u540E\u5929")) {
255
- return getTodayDateString(now + 2 * MS_DAY);
256
- }
257
- if (lower.includes("\u5927\u540E\u5929")) {
258
- return getTodayDateString(now + 3 * MS_DAY);
259
- }
260
- const weekdayMap = {
261
- "\u5468\u65E5": 0,
262
- "\u661F\u671F\u65E5": 0,
263
- "sunday": 0,
264
- "sun": 0,
265
- "\u5468\u4E00": 1,
266
- "\u661F\u671F\u4E00": 1,
267
- "monday": 1,
268
- "mon": 1,
269
- "\u5468\u4E8C": 2,
270
- "\u661F\u671F\u4E8C": 2,
271
- "tuesday": 2,
272
- "tue": 2,
273
- "\u5468\u4E09": 3,
274
- "\u661F\u671F\u4E09": 3,
275
- "wednesday": 3,
276
- "wed": 3,
277
- "\u5468\u56DB": 4,
278
- "\u661F\u671F\u56DB": 4,
279
- "thursday": 4,
280
- "thu": 4,
281
- "\u5468\u4E94": 5,
282
- "\u661F\u671F\u4E94": 5,
283
- "friday": 5,
284
- "fri": 5,
285
- "\u5468\u516D": 6,
286
- "\u661F\u671F\u516D": 6,
287
- "saturday": 6,
288
- "sat": 6
289
- };
290
- const isNextWeek = lower.includes("\u4E0B\u5468") || lower.includes("next");
291
- for (const [name, targetDay] of Object.entries(weekdayMap)) {
292
- if (lower.includes(name)) {
293
- const curDay = new Date(now).getDay();
294
- let diff = targetDay - curDay;
295
- if (diff <= 0 || isNextWeek) {
296
- diff += 7;
234
+ for (const msg of commitMessages) {
235
+ const msgLower = msg.toLowerCase();
236
+ if (msgLower.includes(idLower) || msgLower.includes(numId)) {
237
+ return { matched: true, message: msg };
238
+ }
239
+ if (titleTokens.length > 0) {
240
+ const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
241
+ if (hits >= Math.min(2, titleTokens.length)) {
242
+ return { matched: true, message: msg };
297
243
  }
298
- return getTodayDateString(now + diff * MS_DAY);
299
244
  }
300
245
  }
301
- const fullDateMatch = titleOrDate.match(/\b(20\d\d)-(\d{1,2})-(\d{1,2})\b/);
302
- if (fullDateMatch) {
303
- const y = fullDateMatch[1];
304
- const m = fullDateMatch[2].padStart(2, "0");
305
- const d = fullDateMatch[3].padStart(2, "0");
306
- return `${y}-${m}-${d}`;
307
- }
308
- const mmddMatch = titleOrDate.match(/\b(\d{1,2})-(\d{1,2})\b/);
309
- if (mmddMatch) {
310
- const curYear = new Date(now).getFullYear();
311
- const m = mmddMatch[1].padStart(2, "0");
312
- const d = mmddMatch[2].padStart(2, "0");
313
- return `${curYear}-${m}-${d}`;
314
- }
315
- return void 0;
246
+ return { matched: false };
316
247
  }
317
- function detectRecurrence(title) {
318
- const lower = title.toLowerCase();
319
- if (lower.includes("\u6BCF\u5929") || lower.includes("\u6BCF\u65E5") || lower.includes("daily") || lower.includes("every day")) {
320
- return "daily";
321
- }
322
- return void 0;
323
- }
324
- function getTodayDateString(timestamp = Date.now()) {
325
- const d = new Date(timestamp);
326
- const year = d.getFullYear();
327
- const month = String(d.getMonth() + 1).padStart(2, "0");
328
- const day = String(d.getDate()).padStart(2, "0");
329
- return `${year}-${month}-${day}`;
248
+ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
249
+ let cwd;
250
+ let now = Date.now();
251
+ if (typeof cwdOrNow === "number") now = cwdOrNow;
252
+ else if (typeof cwdOrNow === "string") cwd = cwdOrNow;
253
+ if (typeof nowArg === "number") now = nowArg;
254
+ sweepQuietNotes(store, now);
255
+ const activeNotes = store.list({ status: "active", cwd });
256
+ if (activeNotes.length === 0) return "";
257
+ const lines = activeNotes.map((a) => {
258
+ const num = a.id.replace(/^anc-/, "");
259
+ const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
260
+ const desc = a.description ? ` \u2014 ${a.description}` : "";
261
+ return `\u2022 #${num} [${a.project}] ${a.title}${files}${desc}`;
262
+ });
263
+ return [
264
+ `<sticky-notes count="${activeNotes.length}">`,
265
+ `Pinned notes from previous sessions:`,
266
+ lines.join("\n"),
267
+ `When a note is completed, call anchor(action="settle", id="...") to tear it off.`,
268
+ `</sticky-notes>`
269
+ ].join("\n");
330
270
  }
331
- function getDefaultStorageDir() {
332
- if (process.env.ANCHOR_DIR) {
333
- return import_node_path2.default.resolve(process.env.ANCHOR_DIR);
334
- }
335
- const primaryDir = import_node_path2.default.join(import_node_os.default.homedir(), ".anchor");
336
- const legacyPiDir = import_node_path2.default.join(import_node_os.default.homedir(), ".pi", "agent", "anchors");
337
- if (!import_node_fs.default.existsSync(primaryDir) && import_node_fs.default.existsSync(legacyPiDir)) {
338
- try {
339
- import_node_fs.default.mkdirSync(primaryDir, { recursive: true });
340
- for (const file of ["state.json", "archive.jsonl", "graveyard.jsonl"]) {
341
- const src = import_node_path2.default.join(legacyPiDir, file);
342
- const dest = import_node_path2.default.join(primaryDir, file);
343
- if (import_node_fs.default.existsSync(src) && !import_node_fs.default.existsSync(dest)) {
344
- import_node_fs.default.copyFileSync(src, dest);
345
- }
271
+ var renderActiveAnchorsContext = renderColdStartAnchorsContext;
272
+ function sweepQuietNotes(store, now = Date.now()) {
273
+ return store.withLock(() => {
274
+ const state = store.loadState();
275
+ const quieted = [];
276
+ for (const a of state.anchors) {
277
+ if (a.status === "active" && now - a.lastTouchedAt > QUIET_AFTER_DAYS * MS_PER_DAY) {
278
+ a.status = "sleeping";
279
+ a.updatedAt = now;
280
+ quieted.push(a.id);
346
281
  }
347
- } catch {
348
282
  }
349
- }
350
- return primaryDir;
283
+ if (quieted.length > 0) {
284
+ state.lastSweepAt = now;
285
+ store.saveState(state);
286
+ }
287
+ return { quieted };
288
+ });
289
+ }
290
+ function getDefaultStorageDir() {
291
+ if (process.env.ANCHOR_DIR) return import_node_path.default.resolve(process.env.ANCHOR_DIR);
292
+ return import_node_path.default.join(import_node_os.default.homedir(), ".anchor");
351
293
  }
352
294
  function sleepSync(ms) {
353
295
  const end = Date.now() + ms;
@@ -369,22 +311,18 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
369
311
  attempt++;
370
312
  try {
371
313
  const fd = import_node_fs.default.openSync(lockPath, "wx");
372
- const meta = { pid: process.pid, createdAt: Date.now() };
373
- import_node_fs.default.writeFileSync(fd, JSON.stringify(meta), "utf-8");
314
+ import_node_fs.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), "utf-8");
374
315
  import_node_fs.default.closeSync(fd);
375
316
  return () => {
376
317
  try {
377
- if (import_node_fs.default.existsSync(lockPath)) {
378
- import_node_fs.default.unlinkSync(lockPath);
379
- }
318
+ if (import_node_fs.default.existsSync(lockPath)) import_node_fs.default.unlinkSync(lockPath);
380
319
  } catch {
381
320
  }
382
321
  };
383
322
  } catch (err) {
384
323
  if (err.code === "EEXIST") {
385
324
  try {
386
- const content = import_node_fs.default.readFileSync(lockPath, "utf-8");
387
- const meta = JSON.parse(content);
325
+ const meta = JSON.parse(import_node_fs.default.readFileSync(lockPath, "utf-8"));
388
326
  const isStale = Date.now() - meta.createdAt > staleTimeoutMs;
389
327
  const isDead = meta.pid && !isProcessAlive(meta.pid);
390
328
  if (isStale || isDead) {
@@ -396,8 +334,7 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
396
334
  }
397
335
  } catch {
398
336
  }
399
- const delay = Math.min(40, Math.floor(attempt * 4 + Math.random() * 8));
400
- sleepSync(delay);
337
+ sleepSync(Math.min(40, Math.floor(attempt * 4 + Math.random() * 8)));
401
338
  continue;
402
339
  }
403
340
  break;
@@ -406,50 +343,46 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
406
343
  return () => {
407
344
  };
408
345
  }
409
- function atomicRenameWithRetry(tempPath, targetPath, maxAttempts = 6) {
410
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
346
+ function atomicRenameWithRetry(tempPath, targetPath, maxRetries = 5) {
347
+ let lastErr;
348
+ for (let i = 0; i < maxRetries; i++) {
411
349
  try {
412
350
  import_node_fs.default.renameSync(tempPath, targetPath);
413
351
  return;
414
352
  } catch (err) {
415
- const code = err?.code;
416
- if ((code === "EBUSY" || code === "EPERM" || code === "EACCES") && attempt < maxAttempts) {
417
- const delay = Math.floor(attempt * 20 + Math.random() * 15);
418
- sleepSync(delay);
353
+ lastErr = err;
354
+ if (err.code === "EPERM" || err.code === "EACCES" || err.code === "EBUSY") {
355
+ sleepSync(15 * (i + 1));
419
356
  continue;
420
357
  }
421
- const crashDump = `${tempPath}.failed`;
422
- try {
423
- if (import_node_fs.default.existsSync(tempPath)) import_node_fs.default.renameSync(tempPath, crashDump);
424
- } catch {
425
- }
426
- throw err;
358
+ break;
427
359
  }
428
360
  }
361
+ try {
362
+ import_node_fs.default.copyFileSync(tempPath, targetPath);
363
+ import_node_fs.default.unlinkSync(tempPath);
364
+ } catch {
365
+ throw lastErr;
366
+ }
429
367
  }
430
368
  var AnchorStore = class {
431
369
  storageDir;
432
370
  statePath;
371
+ lockPath;
433
372
  archivePath;
434
- graveyardPath;
435
- constructor(customStorageDir) {
436
- if (customStorageDir) {
437
- this.storageDir = import_node_path2.default.resolve(customStorageDir);
438
- } else {
439
- this.storageDir = getDefaultStorageDir();
440
- }
441
- this.statePath = import_node_path2.default.join(this.storageDir, "state.json");
442
- this.archivePath = import_node_path2.default.join(this.storageDir, "archive.jsonl");
443
- this.graveyardPath = import_node_path2.default.join(this.storageDir, "graveyard.jsonl");
444
- this.ensureDirs();
373
+ constructor(storageDir) {
374
+ this.storageDir = storageDir ? import_node_path.default.resolve(storageDir) : getDefaultStorageDir();
375
+ this.statePath = import_node_path.default.join(this.storageDir, "state.json");
376
+ this.lockPath = import_node_path.default.join(this.storageDir, "state.lock");
377
+ this.archivePath = import_node_path.default.join(this.storageDir, "archive.jsonl");
445
378
  }
446
- get lockPath() {
447
- return import_node_path2.default.join(this.storageDir, "state.lock");
379
+ ensureDirs() {
380
+ if (!import_node_fs.default.existsSync(this.storageDir)) {
381
+ import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
382
+ }
448
383
  }
449
- /**
450
- * Execute mutation within a cross-process exclusive lock
451
- */
452
384
  withLock(fn) {
385
+ this.ensureDirs();
453
386
  const release = acquireSyncLock(this.lockPath);
454
387
  try {
455
388
  return fn();
@@ -457,84 +390,35 @@ var AnchorStore = class {
457
390
  release();
458
391
  }
459
392
  }
460
- ensureDirs() {
461
- if (!import_node_fs.default.existsSync(this.storageDir)) {
462
- try {
463
- import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
464
- } catch {
465
- }
466
- }
467
- }
468
- /**
469
- * Load store state. If corrupt or missing, returns safe default.
470
- */
471
393
  loadState() {
472
394
  this.ensureDirs();
473
395
  if (!import_node_fs.default.existsSync(this.statePath)) {
474
- return {
475
- version: 1,
476
- anchors: [],
477
- lastSweepAt: Date.now()
478
- };
479
- }
480
- let raw = null;
481
- for (let attempt = 1; attempt <= 3; attempt++) {
482
- try {
483
- raw = import_node_fs.default.readFileSync(this.statePath, "utf-8");
484
- break;
485
- } catch (err) {
486
- if ((err?.code === "EBUSY" || err?.code === "EPERM") && attempt < 3) {
487
- sleepSync(15);
488
- continue;
489
- }
490
- }
491
- }
492
- if (raw === null) {
493
- return { version: 1, anchors: [], lastSweepAt: Date.now() };
396
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
494
397
  }
495
398
  try {
496
- const data = JSON.parse(raw);
497
- if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
498
- throw new Error("Invalid state: anchors must be array");
399
+ const parsed = JSON.parse(import_node_fs.default.readFileSync(this.statePath, "utf-8"));
400
+ if (!parsed || !Array.isArray(parsed.anchors)) {
401
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
499
402
  }
500
- data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
501
- ...a,
502
- files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
503
- tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
504
- decay: {
505
- activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
506
- sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
507
- graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
508
- }
509
- }));
510
- return data;
403
+ return parsed;
511
404
  } catch {
512
- const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
513
405
  try {
514
- import_node_fs.default.renameSync(this.statePath, backupPath);
406
+ import_node_fs.default.copyFileSync(this.statePath, import_node_path.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
515
407
  } catch {
516
408
  }
517
- return {
518
- version: 1,
519
- anchors: [],
520
- lastSweepAt: Date.now()
521
- };
409
+ return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
522
410
  }
523
411
  }
524
- /**
525
- * Atomically save store state via temp file + atomic rename with Windows NTFS spin-retry
526
- */
527
412
  saveState(state) {
528
413
  this.ensureDirs();
529
- const rand = Math.random().toString(36).slice(2, 7);
530
- const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
414
+ const tempPath = `${this.statePath}.${process.pid}.${Date.now()}.tmp`;
531
415
  import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
532
416
  atomicRenameWithRetry(tempPath, this.statePath);
533
417
  }
534
418
  generateId(state) {
535
419
  const numbers = state.anchors.map((a) => {
536
- const match = a.id.match(/^anc-(\d+)$/);
537
- return match ? parseInt(match[1], 10) : 0;
420
+ const m = a.id.match(/^anc-(\d+)$/);
421
+ return m ? parseInt(m[1], 10) : 0;
538
422
  }).filter((n) => n > 0);
539
423
  const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
540
424
  const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
@@ -542,37 +426,36 @@ var AnchorStore = class {
542
426
  return `anc-${nextNum}`;
543
427
  }
544
428
  /**
545
- * Create a new Anchor with automatic project and cwd tagging
429
+ * Pin a new sticky note
546
430
  */
547
431
  create(input) {
548
432
  return this.withLock(() => {
549
433
  const state = this.loadState();
550
434
  const now = Date.now();
551
435
  const id = this.generateId(state);
552
- const cwd = input.cwd ? normalizePath(input.cwd) : "";
553
- const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
554
- const recurrence = input.recurrence || detectRecurrence(input.title);
555
- const targetDate = input.targetDate || detectTargetDate(input.title, now);
556
- const durability = recurrence ? "durable" : input.durability || detectDurability(input.title);
557
- const decayPolicy = durability === "ephemeral" ? getEphemeralDecayPolicy(input.title) : DURABLE_DECAY_POLICY;
436
+ const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
437
+ const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${input.title} ${input.description || ""}`);
438
+ let cwd = input.cwd ? normalizePath(input.cwd) : "";
439
+ if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
440
+ cwd = normalizePath(input.project.trim());
441
+ } else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
442
+ const firstDir = import_node_path.default.dirname(inferredFiles[0]);
443
+ if (firstDir && firstDir !== ".") cwd = normalizePath(firstDir);
444
+ }
445
+ const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
558
446
  const anchor = {
559
447
  id,
560
448
  title: input.title.trim(),
561
449
  description: input.description?.trim(),
562
450
  priority: input.priority || "p1",
563
451
  status: "active",
564
- durability,
565
- recurrence,
566
- targetDate,
567
452
  project: projectName,
568
453
  cwd,
569
454
  createdAt: now,
570
455
  updatedAt: now,
571
456
  lastTouchedAt: now,
572
- files: (input.files || []).map((f) => normalizePath(f)).filter(Boolean),
573
- tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
574
- verifyCommand: input.verifyCommand?.trim() || void 0,
575
- decay: input.decay ? { ...input.decay } : { ...decayPolicy }
457
+ files: inferredFiles,
458
+ tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
576
459
  };
577
460
  state.anchors.push(anchor);
578
461
  this.saveState(state);
@@ -582,52 +465,34 @@ var AnchorStore = class {
582
465
  findAnchorIndex(anchors, id) {
583
466
  const clean = id.trim().replace(/^#/, "");
584
467
  return anchors.findIndex(
585
- (a) => a.id === clean || a.id === `anc-${clean}` || (clean.match(/^\d+$/) ? a.id === `anc-${parseInt(clean, 10)}` : false)
468
+ (a) => a.id === clean || a.id === `anc-${clean}` || (/^\d+$/.test(clean) ? a.id === `anc-${parseInt(clean, 10)}` : false)
586
469
  );
587
470
  }
588
- /**
589
- * Get an anchor by ID (supports 'anc-1', '#anc-1', '1', '01')
590
- */
591
471
  get(id) {
592
472
  const state = this.loadState();
593
473
  const idx = this.findAnchorIndex(state.anchors, id);
594
474
  return idx === -1 ? void 0 : state.anchors[idx];
595
475
  }
596
- /**
597
- * List anchors. By default, if cwd is provided, only returns anchors
598
- * belonging to this cwd or marked as global (''). Pass all: true for global view.
599
- */
600
476
  list(filter) {
601
477
  const state = this.loadState();
602
478
  const targetCwd = filter?.cwd ? normalizePath(filter.cwd) : null;
603
- const today = getTodayDateString(filter?.now);
604
479
  return state.anchors.filter((a) => {
605
- if (!filter?.all && filter?.status === "active" && a.recurrence === "daily" && a.lastCompletedDate === today) {
606
- return false;
607
- }
608
480
  if (filter?.status && a.status !== filter.status) return false;
609
- if (filter?.priority && a.priority !== filter.priority) return false;
610
481
  if (!filter?.all && targetCwd) {
611
- if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
482
+ if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
612
483
  return false;
613
484
  }
614
485
  }
615
486
  return true;
616
487
  });
617
488
  }
618
- /**
619
- * Update an existing anchor
620
- */
621
489
  update(id, patch) {
622
490
  return this.withLock(() => {
623
491
  const state = this.loadState();
624
492
  const idx = this.findAnchorIndex(state.anchors, id);
625
- if (idx === -1) {
626
- throw new Error(`Anchor not found: ${id}`);
627
- }
628
- const current = state.anchors[idx];
493
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
629
494
  const updated = {
630
- ...current,
495
+ ...state.anchors[idx],
631
496
  ...patch,
632
497
  updatedAt: Date.now()
633
498
  };
@@ -639,16 +504,11 @@ var AnchorStore = class {
639
504
  return updated;
640
505
  });
641
506
  }
642
- /**
643
- * Touch an anchor to refresh its decay window and reactivate sleeping state
644
- */
645
507
  touch(id, timestamp = Date.now()) {
646
508
  return this.withLock(() => {
647
509
  const state = this.loadState();
648
510
  const idx = this.findAnchorIndex(state.anchors, id);
649
- if (idx === -1) {
650
- throw new Error(`Anchor not found: ${id}`);
651
- }
511
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
652
512
  const current = state.anchors[idx];
653
513
  current.lastTouchedAt = timestamp;
654
514
  current.updatedAt = timestamp;
@@ -661,38 +521,20 @@ var AnchorStore = class {
661
521
  });
662
522
  }
663
523
  /**
664
- * Settle an anchor (recurring daily habits complete for today and wake up tomorrow)
524
+ * Tear off (settle) a completed sticky note and archive it to archive.jsonl
665
525
  */
666
526
  settle(id, evidence = {}, now = Date.now()) {
667
527
  return this.withLock(() => {
668
528
  const state = this.loadState();
669
529
  const idx = this.findAnchorIndex(state.anchors, id);
670
- if (idx === -1) {
671
- throw new Error(`Anchor not found: ${id}`);
672
- }
673
- const anchor = state.anchors[idx];
674
- const today = getTodayDateString(now);
675
- if (anchor.recurrence === "daily") {
676
- anchor.lastCompletedDate = today;
677
- anchor.lastTouchedAt = now;
678
- anchor.updatedAt = now;
679
- anchor.evidence = {
680
- ...evidence,
681
- settledAt: now,
682
- settledBy: evidence.settledBy || "manual-command"
683
- };
684
- this.ensureDirs();
685
- import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify({ ...anchor, settledForDate: today }) + "\n", "utf-8");
686
- this.saveState(state);
687
- return anchor;
688
- }
530
+ if (idx === -1) throw new Error(`Note not found: ${id}`);
689
531
  const [settled] = state.anchors.splice(idx, 1);
690
532
  settled.status = "settled";
691
533
  settled.updatedAt = now;
692
534
  settled.evidence = {
693
535
  ...evidence,
694
536
  settledAt: now,
695
- settledBy: evidence.settledBy || "manual-command"
537
+ settledBy: evidence.settledBy || "manual"
696
538
  };
697
539
  this.ensureDirs();
698
540
  import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
@@ -701,12 +543,12 @@ var AnchorStore = class {
701
543
  });
702
544
  }
703
545
  /**
704
- * Reverse/undo the last settled anchor, popping it from archive.jsonl back into state.json
546
+ * Put back (undo) the last torn-off sticky note
705
547
  */
706
548
  undoSettle() {
707
549
  return this.withLock(() => {
708
550
  if (!import_node_fs.default.existsSync(this.archivePath)) {
709
- throw new Error("No archived anchors to undo");
551
+ throw new Error("No archived notes to restore");
710
552
  }
711
553
  const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
712
554
  const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
@@ -715,23 +557,18 @@ var AnchorStore = class {
715
557
  const candidate = lines.pop();
716
558
  try {
717
559
  const parsed = JSON.parse(candidate);
718
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
560
+ if (parsed && typeof parsed.id === "string" && typeof parsed.title === "string") {
719
561
  anchor = parsed;
720
562
  }
721
563
  } catch {
722
564
  }
723
565
  }
724
- if (!anchor) {
725
- throw new Error("Archive is empty, nothing to undo");
726
- }
566
+ if (!anchor) throw new Error("Archive is empty, nothing to undo");
727
567
  import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
728
568
  anchor.status = "active";
729
569
  anchor.updatedAt = Date.now();
570
+ anchor.lastTouchedAt = Date.now();
730
571
  delete anchor.evidence;
731
- delete anchor.settledForDate;
732
- if (anchor.recurrence === "daily") {
733
- delete anchor.lastCompletedDate;
734
- }
735
572
  const state = this.loadState();
736
573
  const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
737
574
  if (existingIdx !== -1) {
@@ -742,253 +579,18 @@ var AnchorStore = class {
742
579
  state.anchors.push(anchor);
743
580
  }
744
581
  } else {
745
- const match = anchor.id.match(/^anc-(\d+)$/);
746
- if (match) {
747
- const restoredNum = parseInt(match[1], 10);
748
- state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
749
- }
582
+ const m = anchor.id.match(/^anc-(\d+)$/);
583
+ if (m) state.nextId = Math.max(state.nextId || 1, parseInt(m[1], 10) + 1);
750
584
  state.anchors.push(anchor);
751
585
  }
752
586
  this.saveState(state);
753
587
  return anchor;
754
588
  });
755
589
  }
756
- /**
757
- * Evict anchor to graveyard.jsonl upon total decay expiry
758
- */
759
- dropToGraveyard(id, reason) {
760
- return this.withLock(() => {
761
- const state = this.loadState();
762
- const idx = state.anchors.findIndex((a) => a.id === id);
763
- if (idx === -1) {
764
- throw new Error(`Anchor not found: ${id}`);
765
- }
766
- const [anchor] = state.anchors.splice(idx, 1);
767
- anchor.status = "graveyard";
768
- anchor.updatedAt = Date.now();
769
- anchor.evidence = {
770
- summary: reason,
771
- settledAt: Date.now()
772
- };
773
- this.ensureDirs();
774
- import_node_fs.default.appendFileSync(this.graveyardPath, JSON.stringify(anchor) + "\n", "utf-8");
775
- this.saveState(state);
776
- return anchor;
777
- });
778
- }
779
- getArchive(filter) {
780
- if (!import_node_fs.default.existsSync(this.archivePath)) return [];
781
- try {
782
- const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
783
- const all = [];
784
- for (const l of raw.split(/\r?\n/)) {
785
- const trimmed = l.trim();
786
- if (!trimmed) continue;
787
- try {
788
- const parsed = JSON.parse(trimmed);
789
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
790
- } catch {
791
- }
792
- }
793
- if (filter?.all || !filter?.cwd) return all;
794
- const targetCwd = normalizePath(filter.cwd);
795
- return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
796
- } catch {
797
- return [];
798
- }
799
- }
800
- getGraveyard(filter) {
801
- if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
802
- try {
803
- const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
804
- const all = [];
805
- for (const l of raw.split(/\r?\n/)) {
806
- const trimmed = l.trim();
807
- if (!trimmed) continue;
808
- try {
809
- const parsed = JSON.parse(trimmed);
810
- if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
811
- } catch {
812
- }
813
- }
814
- if (filter?.all || !filter?.cwd) return all;
815
- const targetCwd = normalizePath(filter.cwd);
816
- return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
817
- } catch {
818
- return [];
819
- }
820
- }
821
590
  };
822
591
 
823
- // src/decay.ts
824
- var import_node_fs2 = __toESM(require("fs"), 1);
825
- var MS_PER_DAY = 864e5;
826
- function evaluateAnchorDecay(anchor, now = Date.now()) {
827
- if (anchor.status === "settled" || anchor.status === "graveyard") {
828
- return {
829
- currentStatus: anchor.status,
830
- nextStatus: anchor.status,
831
- daysUntouched: 0,
832
- remainingActiveDays: 0,
833
- remainingSleepDays: 0
834
- };
835
- }
836
- const elapsedMs = Math.max(0, now - anchor.lastTouchedAt);
837
- const daysUntouched = elapsedMs / MS_PER_DAY;
838
- const activeDays = anchor.decay.activeDays;
839
- const graveyardDays = anchor.decay.graveyardDays;
840
- let nextStatus = anchor.status;
841
- if (daysUntouched <= activeDays) {
842
- nextStatus = "active";
843
- } else if (daysUntouched <= graveyardDays) {
844
- nextStatus = "sleeping";
845
- } else {
846
- nextStatus = "graveyard";
847
- }
848
- return {
849
- currentStatus: anchor.status,
850
- nextStatus,
851
- daysUntouched: Math.round(daysUntouched * 10) / 10,
852
- remainingActiveDays: Math.max(0, Math.round((activeDays - daysUntouched) * 10) / 10),
853
- remainingSleepDays: Math.max(0, Math.round((graveyardDays - daysUntouched) * 10) / 10)
854
- };
855
- }
856
- function sweepStore(store, now = Date.now()) {
857
- return store.withLock(() => {
858
- const state = store.loadState();
859
- const result = {
860
- transitionedToSleeping: [],
861
- wokenToActive: [],
862
- evictedToGraveyard: []
863
- };
864
- const toEvict = [];
865
- let stateModified = false;
866
- for (const anchor of state.anchors) {
867
- const evalResult = evaluateAnchorDecay(anchor, now);
868
- if (evalResult.nextStatus === "graveyard") {
869
- toEvict.push(anchor);
870
- } else if (evalResult.nextStatus !== anchor.status) {
871
- if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
872
- anchor.status = "sleeping";
873
- anchor.updatedAt = now;
874
- result.transitionedToSleeping.push(anchor.id);
875
- stateModified = true;
876
- } else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
877
- anchor.status = "active";
878
- anchor.updatedAt = now;
879
- result.wokenToActive.push(anchor.id);
880
- stateModified = true;
881
- }
882
- }
883
- }
884
- if (toEvict.length > 0) {
885
- const evictIds = new Set(toEvict.map((e) => e.id));
886
- state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
887
- stateModified = true;
888
- const graveyardLines = toEvict.map((exp) => {
889
- const rec = {
890
- ...exp,
891
- status: "graveyard",
892
- updatedAt: now,
893
- evictedAt: now,
894
- evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
895
- };
896
- return JSON.stringify(rec);
897
- }).join("\n") + "\n";
898
- import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
899
- for (const exp of toEvict) {
900
- result.evictedToGraveyard.push(exp.id);
901
- }
902
- }
903
- state.lastSweepAt = now;
904
- if (stateModified) {
905
- store.saveState(state);
906
- }
907
- return result;
908
- });
909
- }
910
-
911
- // src/settlement.ts
912
- var import_node_child_process = require("child_process");
913
- function runPhysicalVerification(anchor, cwd) {
914
- if (!anchor.verifyCommand?.trim()) {
915
- return { success: false, output: "No verification command specified" };
916
- }
917
- const targetCwd = anchor.cwd || cwd;
918
- try {
919
- const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
920
- cwd: targetCwd,
921
- encoding: "utf-8",
922
- timeout: 8e3,
923
- maxBuffer: 4 * 1024 * 1024,
924
- stdio: ["ignore", "pipe", "pipe"]
925
- });
926
- return { success: true, output: stdout.trim() };
927
- } catch (err) {
928
- return { success: false, output: String(err.stderr || err.stdout || err.message).trim() };
929
- }
930
- }
931
- function generateSettlementProposals(store, touchedFiles, cwd) {
932
- const activeAndSleeping = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
933
- const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
934
- const proposals = [];
935
- for (const match of matches) {
936
- const fileList = match.matchedFiles.slice(0, 3).join(", ");
937
- const moreSuffix = match.matchedFiles.length > 3 ? ` \u7B49 ${match.matchedFiles.length} \u4E2A\u6587\u4EF6` : "";
938
- proposals.push({
939
- anchor: match.anchor,
940
- matchedFiles: match.matchedFiles,
941
- reason: `\u672C\u6B21\u4F1A\u8BDD\u4FEE\u6539\u4E86 ${fileList}${moreSuffix}`,
942
- recommendedAction: match.score >= 0.85 ? "settle" : "defer"
943
- });
944
- }
945
- return proposals;
946
- }
947
- function formatSettlementCard(proposal) {
948
- const a = proposal.anchor;
949
- const tagStr = a.tags.length > 0 ? ` [${a.tags.join(", ")}]` : "";
950
- const fileStr = proposal.matchedFiles.slice(0, 2).join(", ");
951
- return [
952
- `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
953
- `\u2693 \u5173\u95E8\u7ED3\u6848\u63D0\u8BAE | Anchor Settlement`,
954
- ` \u4EFB\u52A1 #${a.id}: ${a.title}${tagStr} (${a.priority.toUpperCase()})`,
955
- ` \u8BC1\u636E\u89E6\u53D1: \u4FEE\u6539\u4E86 ${fileStr}`,
956
- ` [Enter \u786E\u8BA4\u7ED3\u6848\u5E76\u5F52\u6863] / [Tab \u6682\u672A\u5B8C\u6210\uFF0C\u7EE7\u7EED\u6302\u8D77]`,
957
- `\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`
958
- ].join("\n");
959
- }
960
-
961
592
  // src/tui.ts
962
- var import_node_path3 = __toESM(require("path"), 1);
963
- function classifyAnchor(anchor, now = Date.now()) {
964
- if (anchor.recurrence === "daily") {
965
- return "Habits";
966
- }
967
- if (!anchor.targetDate) {
968
- return "Backlog";
969
- }
970
- const todayStr = getTodayDateString(now);
971
- if (anchor.targetDate <= todayStr) {
972
- return "Today";
973
- }
974
- return "Upcoming";
975
- }
976
- function groupAnchorsByQuadrant(anchors, now = Date.now()) {
977
- const groups = {
978
- today: [],
979
- upcoming: [],
980
- habits: [],
981
- backlog: []
982
- };
983
- for (const a of anchors) {
984
- const q = classifyAnchor(a, now);
985
- if (q === "Today") groups.today.push(a);
986
- else if (q === "Upcoming") groups.upcoming.push(a);
987
- else if (q === "Habits") groups.habits.push(a);
988
- else groups.backlog.push(a);
989
- }
990
- return groups;
991
- }
593
+ var import_node_path2 = __toESM(require("path"), 1);
992
594
  var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
993
595
  function stripAnsi(str) {
994
596
  return str.replace(ANSI_REGEX, "");
@@ -1040,14 +642,12 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
1040
642
  const charLen = codePoint > 65535 ? 2 : 1;
1041
643
  const fullChar = singleLine.slice(i, i + charLen);
1042
644
  const charWidth = getDisplayWidth(fullChar);
1043
- if (accumWidth + charWidth > target) {
1044
- break;
1045
- }
645
+ if (accumWidth + charWidth > target) break;
1046
646
  accumulated += fullChar;
1047
647
  accumWidth += charWidth;
1048
648
  i += charLen;
1049
649
  }
1050
- accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
650
+ accumulated = accumulated.replace(/\s*[\(\[\{(【][^)\]})】]*$/, "");
1051
651
  return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
1052
652
  }
1053
653
  function padToWidth(str, targetWidth) {
@@ -1056,82 +656,59 @@ function padToWidth(str, targetWidth) {
1056
656
  if (current >= targetWidth) return truncated;
1057
657
  return truncated + " ".repeat(targetWidth - current);
1058
658
  }
1059
- function formatTargetDate(anchor, now = Date.now()) {
1060
- if (anchor.recurrence === "daily") {
1061
- return "Daily";
1062
- }
1063
- if (!anchor.targetDate) {
1064
- return "Someday";
1065
- }
1066
- const todayStr = getTodayDateString(now);
1067
- if (anchor.targetDate === todayStr) {
1068
- return "Today";
1069
- }
1070
- const MS_DAY = 24 * 60 * 60 * 1e3;
1071
- const tomorrowStr = getTodayDateString(now + MS_DAY);
1072
- if (anchor.targetDate === tomorrowStr) {
1073
- return "Tomorrow";
1074
- }
1075
- const in2dStr = getTodayDateString(now + 2 * MS_DAY);
1076
- if (anchor.targetDate === in2dStr) {
1077
- return "In 2d";
1078
- }
1079
- const in3dStr = getTodayDateString(now + 3 * MS_DAY);
1080
- if (anchor.targetDate === in3dStr) {
1081
- return "In 3d";
1082
- }
1083
- if (anchor.targetDate < todayStr) {
1084
- return "Overdue";
1085
- }
1086
- const parts = anchor.targetDate.split("-");
1087
- if (parts.length === 3) {
1088
- return `${parts[1]}-${parts[2]}`;
1089
- }
1090
- return anchor.targetDate;
659
+ function formatRelativeAge(timestamp, now = Date.now()) {
660
+ const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
661
+ if (diffSec < 60) return "just now";
662
+ const diffMin = Math.floor(diffSec / 60);
663
+ if (diffMin < 60) return `${diffMin}m ago`;
664
+ const diffHours = Math.floor(diffMin / 60);
665
+ if (diffHours < 24) return `${diffHours}h ago`;
666
+ const diffDays = Math.floor(diffHours / 24);
667
+ return `${diffDays}d ago`;
1091
668
  }
1092
- function formatCreationTime(timestamp, now = Date.now()) {
1093
- const d = new Date(timestamp);
1094
- const nowD = new Date(now);
1095
- const pad = (n) => String(n).padStart(2, "0");
1096
- const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
1097
- const todayStr = getTodayDateString(now);
1098
- const createdDayStr = getTodayDateString(timestamp);
1099
- if (createdDayStr === todayStr) {
1100
- return `Today ${timeStr}`;
1101
- }
1102
- const yesterdayStr = getTodayDateString(now - 24 * 60 * 60 * 1e3);
1103
- if (createdDayStr === yesterdayStr) {
1104
- return `Yesterday ${timeStr}`;
669
+ function formatOrigin(anchor) {
670
+ if (typeof anchor === "string") {
671
+ if (!anchor) return "global";
672
+ return import_node_path2.default.basename(anchor.replace(/\\/g, "/")) || "global";
673
+ }
674
+ let proj = "global";
675
+ if (anchor.project && anchor.project !== "global") {
676
+ const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
677
+ if (normProj.includes("/")) {
678
+ const parts = normProj.split("/").filter(Boolean);
679
+ proj = parts.slice(-2).join("/");
680
+ } else {
681
+ proj = normProj;
682
+ }
683
+ } else if (anchor.cwd) {
684
+ proj = import_node_path2.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1105
685
  }
1106
- const mmdd = `${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
1107
- if (d.getFullYear() === nowD.getFullYear()) {
1108
- return `${mmdd} ${timeStr}`;
686
+ if (anchor.files && anchor.files.length > 0) {
687
+ const shortFile = import_node_path2.default.basename(anchor.files[0].replace(/\\/g, "/"));
688
+ if (shortFile && !proj.endsWith(shortFile)) {
689
+ return `${proj} \xB7 ${shortFile}`;
690
+ }
1109
691
  }
1110
- return `${d.getFullYear()}-${mmdd}`;
1111
- }
1112
- function formatRelativeTime(timestamp, now = Date.now()) {
1113
- return formatCreationTime(timestamp, now);
692
+ return proj;
1114
693
  }
1115
- function formatRemainingTtl(anchor, now = Date.now()) {
1116
- return formatTargetDate(anchor, now);
1117
- }
1118
- function formatOrigin(anchor) {
1119
- if (typeof anchor === "object") {
1120
- if (!anchor.cwd) return anchor.project || "global";
1121
- return import_node_path3.default.basename(anchor.cwd) || "global";
1122
- }
1123
- if (!anchor) return "global";
1124
- return import_node_path3.default.basename(anchor) || "global";
694
+ function sortAnchorsForDisplay(anchors, cwd) {
695
+ return [...anchors].sort((a, b) => {
696
+ if (a.status !== b.status) {
697
+ return a.status === "active" ? -1 : 1;
698
+ }
699
+ if (cwd) {
700
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
701
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
702
+ if (aMatch !== bMatch) return aMatch - bMatch;
703
+ }
704
+ return b.lastTouchedAt - a.lastTouchedAt;
705
+ });
1125
706
  }
1126
707
  function updateAnchorStatusBar(ctx, store) {
1127
708
  if (!hasActiveUI(ctx)) return;
1128
709
  try {
1129
710
  const active = store.list({ status: "active", cwd: ctx.cwd });
1130
- if (active.length === 0) {
1131
- ctx.ui.setStatus("anchor", void 0);
1132
- return;
1133
- }
1134
- ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
711
+ ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
1135
712
  } catch {
1136
713
  }
1137
714
  }
@@ -1143,26 +720,30 @@ function updateStartupBanner(ctx, store) {
1143
720
  ctx.ui.setWidget("anchor-startup", void 0);
1144
721
  return;
1145
722
  }
1146
- const groups = groupAnchorsByQuadrant(active);
1147
- const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
723
+ const sorted = sortAnchorsForDisplay(active, ctx.cwd);
1148
724
  const topItems = sorted.slice(0, 3);
1149
725
  const factory = (_tui, theme) => ({
1150
726
  render: (width) => {
1151
- const maxTitleWidth = Math.max(12, width - 36);
727
+ const maxTitleWidth = Math.max(14, width - 34);
1152
728
  const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
729
+ const label = active.length === 1 ? "1 note" : `${active.length} notes`;
1153
730
  const lines = [
1154
- truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
731
+ truncateToWidth(fg("accent", `\u2316 ${label}`), width)
1155
732
  ];
1156
- for (let i = 0; i < topItems.length; i++) {
1157
- const a = topItems[i];
733
+ for (const a of topItems) {
1158
734
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1159
- const q = classifyAnchor(a);
1160
- const tgt = formatTargetDate(a);
1161
- const titlePadded = truncateToWidth(a.title, maxTitleWidth);
1162
- lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
735
+ const origin = formatOrigin(a);
736
+ const age = formatRelativeAge(a.createdAt);
737
+ const titleClipped = truncateToWidth(a.title, maxTitleWidth);
738
+ lines.push(
739
+ truncateToWidth(
740
+ ` ${fg("dim", num)} ${titleClipped} ${fg("muted", `\xB7 ${origin}`)} ${fg("dim", `(${age})`)}`,
741
+ width
742
+ )
743
+ );
1163
744
  }
1164
745
  if (sorted.length > 3) {
1165
- lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
746
+ lines.push(truncateToWidth(fg("dim", ` +${sorted.length - 3} more \xB7 /anchor`), width));
1166
747
  }
1167
748
  return lines;
1168
749
  },
@@ -1176,405 +757,124 @@ function updateStartupBanner(ctx, store) {
1176
757
  }
1177
758
  async function openAnchorDashboard(ctx, store) {
1178
759
  if (!hasActiveUI(ctx)) return;
1179
- const list = store.list({ status: "active", cwd: ctx.cwd });
1180
- if (list.length === 0) {
1181
- ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
760
+ const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
761
+ if (allNotes.length === 0) {
762
+ ctx.ui.notify("\u2316 No sticky notes. Use /pin <note> to stick one.", "info");
1182
763
  return;
1183
764
  }
1184
- const groups = groupAnchorsByQuadrant(list);
1185
- const sortedList = [
1186
- ...groups.today,
1187
- ...groups.upcoming,
1188
- ...groups.habits,
1189
- ...groups.backlog
1190
- ];
765
+ const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
1191
766
  const optionMap = /* @__PURE__ */ new Map();
1192
767
  const displayOptions = [];
1193
768
  const termCols = process.stdout?.columns || 96;
1194
- const isNarrow = termCols < 88;
1195
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
1196
- for (let i = 0; i < sortedList.length; i++) {
1197
- const a = sortedList[i];
769
+ const isNarrow = termCols < 84;
770
+ const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
771
+ for (const a of sorted) {
1198
772
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1199
- const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1200
773
  const origin = formatOrigin(a);
1201
- const target = formatTargetDate(a);
1202
- const created = formatCreationTime(a.createdAt);
1203
- const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
774
+ const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
1204
775
  const colNum = `${num} `;
1205
- const colTitle = padToWidth(titleWithFiles, titleWidth);
1206
- const colOrigin = padToWidth(origin, 10);
1207
- const colTarget = padToWidth(target, 12);
1208
- const colCreated = created;
1209
- const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
776
+ const colTitle = padToWidth(a.title, titleWidth);
777
+ const colOrigin = padToWidth(origin, 22);
778
+ const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
1210
779
  optionMap.set(label, a);
1211
780
  displayOptions.push(label);
1212
781
  }
1213
- const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
782
+ const selected = await ctx.ui.select("\u2316 Notes (select a note):", displayOptions);
1214
783
  if (!selected) return;
1215
784
  const anchor = optionMap.get(selected);
1216
785
  if (!anchor) return;
1217
- const action = await ctx.ui.select(
1218
- `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1219
- [
1220
- "\u2713 Settle (Mark completed and archive)",
1221
- "\u2139 View details & associated files",
1222
- "\u21A9 Cancel"
1223
- ]
1224
- );
786
+ const actions = [
787
+ "\u2713 Done (tear off note)",
788
+ ...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
789
+ "\u2139 Read full note",
790
+ "\u21A9 Back"
791
+ ];
792
+ const action = await ctx.ui.select(`\u2316 #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, actions);
1225
793
  if (!action || action.startsWith("\u21A9")) return;
1226
794
  if (action.startsWith("\u2713")) {
1227
- store.settle(anchor.id, { settledBy: "manual-command" });
1228
- const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1229
- ctx.ui.notify(successMsg, "info");
795
+ store.settle(anchor.id, { settledBy: "manual" });
796
+ ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
797
+ updateAnchorStatusBar(ctx, store);
798
+ } else if (action.startsWith("\u26A1")) {
799
+ store.touch(anchor.id);
800
+ ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
1230
801
  updateAnchorStatusBar(ctx, store);
1231
802
  } else if (action.startsWith("\u2139")) {
1232
803
  const details = [
1233
- `#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
1234
- anchor.description ? `Description: ${anchor.description}` : null,
1235
- anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1236
- anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
1237
- anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
1238
- `Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
804
+ `\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
805
+ anchor.description ? `Details: ${anchor.description}` : null,
806
+ anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
807
+ `Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
1239
808
  ].filter(Boolean).join("\n");
1240
809
  ctx.ui.notify(details, "info");
1241
810
  }
1242
811
  }
1243
812
 
1244
- // src/observer.ts
1245
- var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
1246
- var SessionTouchObserver = class {
1247
- touchedFiles = /* @__PURE__ */ new Set();
1248
- modifiedFiles = /* @__PURE__ */ new Set();
1249
- inspectedFiles = /* @__PURE__ */ new Set();
1250
- committed = false;
1251
- commitMessages = [];
1252
- clear() {
1253
- this.touchedFiles.clear();
1254
- this.modifiedFiles.clear();
1255
- this.inspectedFiles.clear();
1256
- this.committed = false;
1257
- this.commitMessages = [];
1258
- }
1259
- /**
1260
- * Observe and record a tool call invocation, separating inspection from mutation
1261
- */
1262
- recordToolCall(toolName, input) {
1263
- if (!input || typeof input !== "object") return;
1264
- const lowerTool = (toolName || "").toLowerCase();
1265
- const isMutation = MUTATION_TOOLS.has(lowerTool);
1266
- const paths = [];
1267
- const pathField = input.path || input.filePath || input.file;
1268
- if (typeof pathField === "string") {
1269
- paths.push(normalizePath(pathField));
1270
- }
1271
- if (Array.isArray(input.paths)) {
1272
- for (const p of input.paths) {
1273
- if (typeof p === "string") {
1274
- paths.push(normalizePath(p));
1275
- }
1276
- }
1277
- }
1278
- for (const p of paths) {
1279
- this.touchedFiles.add(p);
1280
- if (isMutation) {
1281
- this.modifiedFiles.add(p);
1282
- } else {
1283
- this.inspectedFiles.add(p);
1284
- }
1285
- }
1286
- if (lowerTool === "bash" || lowerTool === "powershell") {
1287
- const cmd = String(input.command || "");
1288
- if (cmd.includes("git commit")) {
1289
- this.committed = true;
1290
- const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
1291
- if (msgMatch && msgMatch[1]) {
1292
- this.commitMessages.push(msgMatch[1]);
1293
- }
1294
- }
1295
- }
1296
- }
1297
- /**
1298
- * Add a file path manually (e.g. from git status diff)
1299
- */
1300
- addTouchedFile(filePath, isModified = true) {
1301
- if (filePath) {
1302
- const norm = normalizePath(filePath);
1303
- this.touchedFiles.add(norm);
1304
- if (isModified) {
1305
- this.modifiedFiles.add(norm);
1306
- } else {
1307
- this.inspectedFiles.add(norm);
1308
- }
1309
- }
1310
- }
1311
- getModifiedFiles() {
1312
- return Array.from(this.modifiedFiles);
1313
- }
1314
- getInspectedFiles() {
1315
- return Array.from(this.inspectedFiles);
1316
- }
1317
- getTouchedFiles() {
1318
- return Array.from(this.touchedFiles);
1319
- }
1320
- hasCommitted() {
1321
- return this.committed;
1322
- }
1323
- getCommitMessages() {
1324
- return [...this.commitMessages];
1325
- }
1326
- /**
1327
- * Check if any commit message explicitly references or resolves an anchor
1328
- */
1329
- matchesCommit(anchor) {
1330
- const idLower = anchor.id.toLowerCase();
1331
- const title = anchor.title.toLowerCase();
1332
- let titleTokens = [];
1333
- if (typeof Intl !== "undefined" && Intl.Segmenter) {
1334
- try {
1335
- const segmenter = new Intl.Segmenter("und", { granularity: "word" });
1336
- titleTokens = Array.from(segmenter.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
1337
- } catch {
1338
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
1339
- }
1340
- } else {
1341
- titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
1342
- }
1343
- for (const msg of this.commitMessages) {
1344
- const msgLower = msg.toLowerCase();
1345
- if (msgLower.includes(idLower)) {
1346
- return { matched: true, message: msg };
1347
- }
1348
- if (titleTokens.length > 0) {
1349
- const matchingCount = titleTokens.filter((tok) => msgLower.includes(tok)).length;
1350
- if (matchingCount >= Math.min(2, titleTokens.length)) {
1351
- return { matched: true, message: msg };
1352
- }
1353
- }
1354
- }
1355
- return { matched: false };
1356
- }
1357
- };
1358
-
1359
- // src/context_injector.ts
1360
- var import_node_path4 = __toESM(require("path"), 1);
1361
- var COMMENT_FORMATS = {
1362
- // Double slash //
1363
- ".ts": (m) => `// ${m}`,
1364
- ".tsx": (m) => `// ${m}`,
1365
- ".js": (m) => `// ${m}`,
1366
- ".jsx": (m) => `// ${m}`,
1367
- ".mjs": (m) => `// ${m}`,
1368
- ".cjs": (m) => `// ${m}`,
1369
- ".java": (m) => `// ${m}`,
1370
- ".kt": (m) => `// ${m}`,
1371
- ".swift": (m) => `// ${m}`,
1372
- ".go": (m) => `// ${m}`,
1373
- ".rs": (m) => `// ${m}`,
1374
- ".c": (m) => `// ${m}`,
1375
- ".cpp": (m) => `// ${m}`,
1376
- ".h": (m) => `// ${m}`,
1377
- ".hpp": (m) => `// ${m}`,
1378
- ".cs": (m) => `// ${m}`,
1379
- ".dart": (m) => `// ${m}`,
1380
- ".scala": (m) => `// ${m}`,
1381
- ".zig": (m) => `// ${m}`,
1382
- ".proto": (m) => `// ${m}`,
1383
- // Hash #
1384
- ".py": (m) => `# ${m}`,
1385
- ".rb": (m) => `# ${m}`,
1386
- ".sh": (m) => `# ${m}`,
1387
- ".bash": (m) => `# ${m}`,
1388
- ".zsh": (m) => `# ${m}`,
1389
- ".yaml": (m) => `# ${m}`,
1390
- ".yml": (m) => `# ${m}`,
1391
- ".toml": (m) => `# ${m}`,
1392
- ".conf": (m) => `# ${m}`,
1393
- ".dockerfile": (m) => `# ${m}`,
1394
- ".ps1": (m) => `# ${m}`,
1395
- ".r": (m) => `# ${m}`,
1396
- // HTML / XML <!-- -->
1397
- ".html": (m) => `<!-- ${m} -->`,
1398
- ".xml": (m) => `<!-- ${m} -->`,
1399
- ".svg": (m) => `<!-- ${m} -->`,
1400
- ".vue": (m) => `<!-- ${m} -->`,
1401
- ".svelte": (m) => `<!-- ${m} -->`,
1402
- // Block comment /* */
1403
- ".css": (m) => `/* ${m} */`,
1404
- ".scss": (m) => `/* ${m} */`,
1405
- ".less": (m) => `/* ${m} */`,
1406
- // SQL / Lua / Haskell --
1407
- ".sql": (m) => `-- ${m}`,
1408
- ".lua": (m) => `-- ${m}`,
1409
- ".hs": (m) => `-- ${m}`,
1410
- // Batch REM
1411
- ".bat": (m) => `REM ${m}`,
1412
- ".cmd": (m) => `REM ${m}`
1413
- };
1414
- function makeSafeTaskAnnotation(filePath, anchor) {
1415
- const ext = import_node_path4.default.extname(filePath).toLowerCase();
1416
- const formatter = COMMENT_FORMATS[ext];
1417
- if (!formatter) {
1418
- return null;
1419
- }
1420
- return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
1421
- }
1422
- function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
1423
- let cwd;
1424
- let now = Date.now();
1425
- if (typeof cwdOrNow === "number") {
1426
- now = cwdOrNow;
1427
- } else if (typeof cwdOrNow === "string") {
1428
- cwd = cwdOrNow;
1429
- }
1430
- if (typeof nowArg === "number") {
1431
- now = nowArg;
1432
- }
1433
- sweepStore(store, now);
1434
- const activeAnchors = store.list({ status: "active", cwd });
1435
- if (activeAnchors.length === 0) {
1436
- return "";
813
+ // src/extension.ts
814
+ var import_typebox = require("@sinclair/typebox");
815
+ function parseAnchorCommandInput(raw) {
816
+ const trimmed = (raw || "").trim();
817
+ if (!trimmed || trimmed === "list" || trimmed === "ls") {
818
+ return { mode: "dashboard" };
819
+ }
820
+ if (trimmed === "undo") {
821
+ return { mode: "undo" };
822
+ }
823
+ const settleMatch = trimmed.match(/^(?:done|rm|close|settle)\s+(.+)$/i);
824
+ if (settleMatch) {
825
+ return {
826
+ mode: "settle",
827
+ targetId: settleMatch[1].trim().replace(/^#/, "")
828
+ };
1437
829
  }
1438
- const lines = activeAnchors.map((a) => {
1439
- const fileHint = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
1440
- const tagHint = a.tags.length > 0 ? ` [${a.tags.join(", ")}]` : "";
1441
- const projHint = a.cwd ? `[${a.project}]` : "[global]";
1442
- let targetHint = "";
1443
- if (a.recurrence === "daily") {
1444
- targetHint = " [\u4ECA\u65E5\u5FAA\u73AF\xB7Daily]";
1445
- } else if (a.targetDate) {
1446
- const todayStr = getTodayDateString(now);
1447
- if (a.targetDate === todayStr) {
1448
- targetHint = " [\u4ECA\u65E5\u805A\u7126\xB7Due Today]";
1449
- } else if (a.targetDate < todayStr) {
1450
- targetHint = " [\u5DF2\u903E\u671F\xB7Overdue]";
1451
- } else {
1452
- targetHint = ` [\u6392\u671F:${a.targetDate}]`;
1453
- }
1454
- } else {
1455
- targetHint = " [\u957F\u671F\u613F\u666F\xB7Someday]";
1456
- }
1457
- const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
1458
- const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
1459
- const failureSnippet = preflightFailures?.get(a.id);
1460
- const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
1461
- return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
1462
- });
1463
- return [
1464
- `<active-anchors count="${activeAnchors.length}">`,
1465
- `Unfinished cross-session commitments from previous sessions:`,
1466
- lines.join("\n"),
1467
- ``,
1468
- `CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
1469
- `- If the user opens the session with a casual greeting or continuation ("hi", "\u5728\u5417", "\u7EE7\u7EED", "\u65E9\u4E0A\u597D"), naturally prioritize items tagged [\u4ECA\u65E5\u805A\u7126\xB7Due Today] in a friendly assistant tone (e.g. "\u55E8\uFF01\u4ECA\u5929\u89C4\u5212\u7684\u3010...\u3011\u73B0\u5728\u5904\u7406\u8FD8\u662F\u770B\u522B\u7684\uFF1F").`,
1470
- `- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
1471
- `- If the user issues a direct, concrete new instruction (e.g. "\u5E2E\u6211\u67E5\u4E0B\u8FD9\u4E2A\u62A5\u9519"), DO NOT interrupt their flow. Focus directly on their instruction.`,
1472
- `- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
1473
- `</active-anchors>`
1474
- ].join("\n");
830
+ return {
831
+ mode: "pin",
832
+ title: trimmed
833
+ };
1475
834
  }
1476
- var renderActiveAnchorsContext = renderColdStartAnchorsContext;
1477
-
1478
- // src/protocol.ts
1479
- var AnchorProtocol = class {
1480
- store;
1481
- observer;
1482
- annotatedThisSession = /* @__PURE__ */ new Set();
1483
- constructor(store) {
1484
- this.store = store;
1485
- this.observer = new SessionTouchObserver();
1486
- }
1487
- /**
1488
- * Run session initialization lifecycle: sweeps stale anchors and resets session state
1489
- */
1490
- handleSessionStart(now = Date.now()) {
1491
- this.observer.clear();
1492
- this.annotatedThisSession.clear();
1493
- const sweep = sweepStore(this.store, now);
1494
- return { sleepingCount: sweep.transitionedToSleeping.length };
1495
- }
1496
- /**
1497
- * Handle turn lifecycle: injects cold-start prompt on turn 1, returns null (0 tokens) on turn 2+
1498
- */
1499
- handleBeforeTurn(turnCount, cwd, now = Date.now()) {
1500
- if (turnCount <= 1) {
1501
- return renderColdStartAnchorsContext(this.store, cwd, now) || null;
835
+ function resolveAnchorTarget(store, idOrIndex, cwd) {
836
+ const clean = idOrIndex.trim().replace(/^#/, "");
837
+ if (/^\d+$/.test(clean)) {
838
+ const seqIndex = parseInt(clean, 10);
839
+ const visible = store.list({ cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
840
+ const sorted = sortAnchorsForDisplay(visible, cwd);
841
+ if (seqIndex >= 1 && seqIndex <= sorted.length) {
842
+ return sorted[seqIndex - 1].id;
1502
843
  }
1503
- return null;
1504
844
  }
1505
- /**
1506
- * Handle tool execution result: detects touched paths, refreshes mutation touch timers,
1507
- * and returns safe language-specific JIT annotation (or null if syntax incompatible/already annotated).
1508
- */
1509
- handleToolResult(input) {
1510
- if (!input.isError) {
1511
- this.observer.recordToolCall(input.toolName, input.filePath ? { path: input.filePath } : {});
1512
- }
1513
- if (!input.filePath) {
1514
- return { annotation: null, matchedAnchors: [] };
1515
- }
1516
- const touchedPath = normalizePath(input.filePath);
1517
- const anchors = this.store.list({ cwd: input.cwd }).filter((a2) => a2.status === "active" || a2.status === "sleeping");
1518
- const matches = findMatchedAnchors(anchors, [touchedPath]);
1519
- if (matches.length === 0) {
1520
- return { annotation: null, matchedAnchors: [] };
1521
- }
1522
- if (input.isMutation && !input.isError) {
1523
- for (const m of matches) {
1524
- this.store.touch(m.anchor.id);
1525
- }
1526
- }
1527
- const a = matches[0].anchor;
1528
- if (!this.annotatedThisSession.has(a.id)) {
1529
- this.annotatedThisSession.add(a.id);
1530
- const annotation = makeSafeTaskAnnotation(touchedPath, a);
1531
- return { annotation, matchedAnchors: matches.map((m) => m.anchor) };
1532
- }
1533
- return { annotation: null, matchedAnchors: matches.map((m) => m.anchor) };
1534
- }
1535
- };
845
+ return store.get(clean)?.id;
846
+ }
1536
847
  // Annotate the CommonJS export names for ESM import in node:
1537
848
  0 && (module.exports = {
1538
- AnchorProtocol,
1539
849
  AnchorStore,
1540
850
  COMMENT_FORMATS,
1541
- DEFAULT_DECAY_POLICY,
1542
- DURABLE_DECAY_POLICY,
1543
- EPHEMERAL_DECAY_POLICY,
1544
- SessionTouchObserver,
851
+ MS_PER_DAY,
852
+ QUIET_AFTER_DAYS,
1545
853
  acquireSyncLock,
1546
854
  atomicRenameWithRetry,
1547
- classifyAnchor,
1548
- detectDurability,
1549
- detectRecurrence,
1550
- detectTargetDate,
1551
- evaluateAnchorDecay,
855
+ extractPathsFromText,
1552
856
  findMatchedAnchors,
1553
- formatCreationTime,
1554
857
  formatOrigin,
1555
- formatRelativeTime,
1556
- formatRemainingTtl,
1557
- formatSettlementCard,
1558
- formatTargetDate,
1559
- generateSettlementProposals,
858
+ formatRelativeAge,
1560
859
  getDefaultStorageDir,
1561
860
  getDisplayWidth,
1562
- getEphemeralDecayPolicy,
1563
- getTodayDateString,
1564
861
  globToRegExp,
1565
- groupAnchorsByQuadrant,
1566
862
  hasActiveUI,
863
+ isAnchorTargetingWorkspace,
1567
864
  isSameOrSubPath,
1568
865
  makeSafeTaskAnnotation,
1569
866
  matchAnchorAgainstTouchedFiles,
867
+ matchesCommitMessage,
1570
868
  normalizePath,
1571
869
  openAnchorDashboard,
1572
870
  padToWidth,
871
+ parseAnchorCommandInput,
1573
872
  renderActiveAnchorsContext,
1574
873
  renderColdStartAnchorsContext,
1575
- runPhysicalVerification,
874
+ resolveAnchorTarget,
875
+ sortAnchorsForDisplay,
1576
876
  stripAnsi,
1577
- sweepStore,
877
+ sweepQuietNotes,
1578
878
  truncateToWidth,
1579
879
  updateAnchorStatusBar,
1580
880
  updateStartupBanner