pi-anchor 0.3.0 → 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/README.md +33 -170
- package/README_zh.md +32 -176
- package/assets/hero-en.svg +23 -39
- package/assets/hero-zh.svg +23 -39
- package/bin/anchor.js +36 -69
- package/dist/chunk-LKY6WIKH.js +1100 -0
- package/dist/extension-B1-fzCox.d.cts +167 -0
- package/dist/extension-B1-fzCox.d.ts +167 -0
- package/dist/extension.cjs +369 -1315
- package/dist/extension.d.cts +2 -20
- package/dist/extension.d.ts +2 -20
- package/dist/extension.js +4 -536
- package/dist/index.cjs +317 -1143
- package/dist/index.d.cts +17 -250
- package/dist/index.d.ts +17 -250
- package/dist/index.js +15 -103
- package/package.json +2 -2
- package/src/extension.ts +124 -362
- package/src/index.ts +1 -6
- package/src/store.ts +294 -424
- package/src/tui.ts +102 -297
- package/src/types.ts +24 -65
- package/dist/chunk-Q7IILJFA.js +0 -1563
- package/dist/store-t2_TDhJn.d.cts +0 -210
- package/dist/store-t2_TDhJn.d.ts +0 -210
- package/src/context_injector.ts +0 -166
- package/src/decay.ts +0 -125
- package/src/matcher.ts +0 -202
- package/src/observer.ts +0 -141
- package/src/protocol.ts +0 -95
- package/src/settlement.ts +0 -140
package/dist/extension.cjs
CHANGED
|
@@ -39,32 +39,14 @@ var import_typebox = require("@sinclair/typebox");
|
|
|
39
39
|
|
|
40
40
|
// src/store.ts
|
|
41
41
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
42
|
-
var
|
|
42
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
43
43
|
var import_node_os = __toESM(require("os"), 1);
|
|
44
44
|
|
|
45
45
|
// src/types.ts
|
|
46
|
-
var
|
|
47
|
-
|
|
48
|
-
// 7 days active before quiet sleep
|
|
49
|
-
sleepDays: 30,
|
|
50
|
-
graveyardDays: 9999
|
|
51
|
-
// Never silently auto-delete unfinished tasks
|
|
52
|
-
};
|
|
53
|
-
var DURABLE_DECAY_POLICY = {
|
|
54
|
-
activeDays: 7,
|
|
55
|
-
// 7 days active before quiet sleep
|
|
56
|
-
sleepDays: 30,
|
|
57
|
-
graveyardDays: 9999
|
|
58
|
-
// Never permanently auto-dropped; preserved indefinitely
|
|
59
|
-
};
|
|
46
|
+
var QUIET_AFTER_DAYS = 7;
|
|
47
|
+
var MS_PER_DAY = 864e5;
|
|
60
48
|
|
|
61
|
-
// src/
|
|
62
|
-
var import_node_path = __toESM(require("path"), 1);
|
|
63
|
-
function globToRegExp(pattern) {
|
|
64
|
-
const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
|
|
65
|
-
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, ".*");
|
|
66
|
-
return new RegExp(`^${escaped}$`);
|
|
67
|
-
}
|
|
49
|
+
// src/store.ts
|
|
68
50
|
function normalizePath(filePath, baseCwd) {
|
|
69
51
|
if (typeof filePath !== "string") return "";
|
|
70
52
|
let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -80,14 +62,19 @@ function normalizePath(filePath, baseCwd) {
|
|
|
80
62
|
}
|
|
81
63
|
return norm;
|
|
82
64
|
}
|
|
83
|
-
function isSameOrSubPath(
|
|
84
|
-
if (!
|
|
85
|
-
const normA =
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
return
|
|
65
|
+
function isSameOrSubPath(dirA, dirB) {
|
|
66
|
+
if (!dirA || !dirB) return false;
|
|
67
|
+
const normA = dirA.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
68
|
+
const normB = dirB.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
69
|
+
const isWin = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normB);
|
|
70
|
+
const a = isWin ? normA.toLowerCase() : normA;
|
|
71
|
+
const b = isWin ? normB.toLowerCase() : normB;
|
|
72
|
+
return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
|
|
73
|
+
}
|
|
74
|
+
function globToRegExp(pattern) {
|
|
75
|
+
const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
|
|
76
|
+
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, ".*");
|
|
77
|
+
return new RegExp(`^${escaped}$`);
|
|
91
78
|
}
|
|
92
79
|
function extractPathsFromText(text) {
|
|
93
80
|
if (!text) return [];
|
|
@@ -109,8 +96,7 @@ function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
|
109
96
|
if (anchor.project && anchor.project !== "global") {
|
|
110
97
|
const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
111
98
|
if (isSameOrSubPath(normProj, normTarget)) return true;
|
|
112
|
-
|
|
113
|
-
if (projBase && projBase === targetBase) return true;
|
|
99
|
+
if (import_node_path.default.basename(normProj).toLowerCase() === targetBase) return true;
|
|
114
100
|
}
|
|
115
101
|
if (anchor.files && anchor.files.length > 0) {
|
|
116
102
|
for (const f of anchor.files) {
|
|
@@ -135,12 +121,9 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
|
|
|
135
121
|
matchedExact.push(touched);
|
|
136
122
|
break;
|
|
137
123
|
}
|
|
138
|
-
if (normPattern.includes("*")) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
matchedExact.push(touched);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
124
|
+
if (normPattern.includes("*") && globToRegExp(normPattern).test(touched)) {
|
|
125
|
+
matchedExact.push(touched);
|
|
126
|
+
break;
|
|
144
127
|
}
|
|
145
128
|
const dirPrefix = normPattern.endsWith("/") ? normPattern : normPattern + "/";
|
|
146
129
|
if (touched.startsWith(dirPrefix)) {
|
|
@@ -150,193 +133,136 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
|
|
|
150
133
|
}
|
|
151
134
|
}
|
|
152
135
|
if (matchedExact.length > 0) {
|
|
153
|
-
return {
|
|
154
|
-
anchor,
|
|
155
|
-
score: 1,
|
|
156
|
-
matchedFiles: Array.from(new Set(matchedExact)),
|
|
157
|
-
reason: "exact-file"
|
|
158
|
-
};
|
|
136
|
+
return { anchor, score: 1, matchedFiles: Array.from(new Set(matchedExact)), reason: "exact-file" };
|
|
159
137
|
}
|
|
160
138
|
if (matchedPrefix.length > 0) {
|
|
161
|
-
return {
|
|
162
|
-
anchor,
|
|
163
|
-
score: 0.85,
|
|
164
|
-
matchedFiles: Array.from(new Set(matchedPrefix)),
|
|
165
|
-
reason: "dir-prefix"
|
|
166
|
-
};
|
|
139
|
+
return { anchor, score: 0.85, matchedFiles: Array.from(new Set(matchedPrefix)), reason: "dir-prefix" };
|
|
167
140
|
}
|
|
168
141
|
return null;
|
|
169
142
|
}
|
|
170
143
|
function findMatchedAnchors(anchors, touchedFiles) {
|
|
171
|
-
const priorityWeight = { p0: 3, p1: 2, p2: 1 };
|
|
172
144
|
const results = [];
|
|
173
145
|
for (const anchor of anchors) {
|
|
174
|
-
const
|
|
175
|
-
if (
|
|
176
|
-
results.push(match);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return results.sort((a, b) => {
|
|
180
|
-
if (b.score !== a.score) {
|
|
181
|
-
return b.score - a.score;
|
|
182
|
-
}
|
|
183
|
-
const weightA = priorityWeight[a.anchor.priority] || 0;
|
|
184
|
-
const weightB = priorityWeight[b.anchor.priority] || 0;
|
|
185
|
-
if (weightB !== weightA) {
|
|
186
|
-
return weightB - weightA;
|
|
187
|
-
}
|
|
188
|
-
return b.anchor.lastTouchedAt - a.anchor.lastTouchedAt;
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// src/store.ts
|
|
193
|
-
function detectDurability(title) {
|
|
194
|
-
const temporalKeywords = [
|
|
195
|
-
"\u4ECA\u5929",
|
|
196
|
-
"\u4ECA\u65E5",
|
|
197
|
-
"\u665A\u4E0A",
|
|
198
|
-
"\u4ECA\u665A",
|
|
199
|
-
"\u660E\u5929",
|
|
200
|
-
"\u540E\u5929",
|
|
201
|
-
"\u5927\u540E\u5929",
|
|
202
|
-
"\u7A0D\u540E",
|
|
203
|
-
"\u5F85\u4F1A",
|
|
204
|
-
"\u4E0B\u5348",
|
|
205
|
-
"\u4E0A\u5348",
|
|
206
|
-
"\u4E34\u65F6",
|
|
207
|
-
"\u4E00\u4F1A\u513F",
|
|
208
|
-
"\u7B49\u7B49",
|
|
209
|
-
"today",
|
|
210
|
-
"tonight",
|
|
211
|
-
"tomorrow",
|
|
212
|
-
"later",
|
|
213
|
-
"temp",
|
|
214
|
-
"soon"
|
|
215
|
-
];
|
|
216
|
-
const lower = title.toLowerCase();
|
|
217
|
-
for (const kw of temporalKeywords) {
|
|
218
|
-
if (lower.includes(kw)) {
|
|
219
|
-
return "ephemeral";
|
|
220
|
-
}
|
|
146
|
+
const m = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
|
|
147
|
+
if (m) results.push(m);
|
|
221
148
|
}
|
|
222
|
-
return
|
|
149
|
+
return results.sort((a, b) => b.score - a.score);
|
|
223
150
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
151
|
+
var COMMENT_FORMATS = {
|
|
152
|
+
".ts": (m) => `// ${m}`,
|
|
153
|
+
".tsx": (m) => `// ${m}`,
|
|
154
|
+
".js": (m) => `// ${m}`,
|
|
155
|
+
".jsx": (m) => `// ${m}`,
|
|
156
|
+
".mjs": (m) => `// ${m}`,
|
|
157
|
+
".cjs": (m) => `// ${m}`,
|
|
158
|
+
".go": (m) => `// ${m}`,
|
|
159
|
+
".rs": (m) => `// ${m}`,
|
|
160
|
+
".c": (m) => `// ${m}`,
|
|
161
|
+
".cpp": (m) => `// ${m}`,
|
|
162
|
+
".h": (m) => `// ${m}`,
|
|
163
|
+
".cs": (m) => `// ${m}`,
|
|
164
|
+
".java": (m) => `// ${m}`,
|
|
165
|
+
".kt": (m) => `// ${m}`,
|
|
166
|
+
".swift": (m) => `// ${m}`,
|
|
167
|
+
".dart": (m) => `// ${m}`,
|
|
168
|
+
".py": (m) => `# ${m}`,
|
|
169
|
+
".rb": (m) => `# ${m}`,
|
|
170
|
+
".sh": (m) => `# ${m}`,
|
|
171
|
+
".bash": (m) => `# ${m}`,
|
|
172
|
+
".yaml": (m) => `# ${m}`,
|
|
173
|
+
".yml": (m) => `# ${m}`,
|
|
174
|
+
".toml": (m) => `# ${m}`,
|
|
175
|
+
".ps1": (m) => `# ${m}`,
|
|
176
|
+
".html": (m) => `<!-- ${m} -->`,
|
|
177
|
+
".xml": (m) => `<!-- ${m} -->`,
|
|
178
|
+
".svg": (m) => `<!-- ${m} -->`,
|
|
179
|
+
".vue": (m) => `<!-- ${m} -->`,
|
|
180
|
+
".svelte": (m) => `<!-- ${m} -->`,
|
|
181
|
+
".css": (m) => `/* ${m} */`,
|
|
182
|
+
".scss": (m) => `/* ${m} */`,
|
|
183
|
+
".sql": (m) => `-- ${m}`,
|
|
184
|
+
".lua": (m) => `-- ${m}`
|
|
185
|
+
};
|
|
186
|
+
function makeSafeTaskAnnotation(filePath, anchor) {
|
|
187
|
+
const ext = import_node_path.default.extname(filePath).toLowerCase();
|
|
188
|
+
const fmt = COMMENT_FORMATS[ext];
|
|
189
|
+
if (!fmt) return null;
|
|
190
|
+
const num = anchor.id.replace(/^anc-/, "");
|
|
191
|
+
return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
|
|
233
192
|
}
|
|
234
|
-
function
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
193
|
+
function matchesCommitMessage(anchor, commitMessages) {
|
|
194
|
+
const idLower = anchor.id.toLowerCase();
|
|
195
|
+
const numId = `#${anchor.id.replace(/^anc-/, "")}`;
|
|
196
|
+
const title = anchor.title.toLowerCase();
|
|
197
|
+
let titleTokens = [];
|
|
198
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
199
|
+
try {
|
|
200
|
+
const seg = new Intl.Segmenter("und", { granularity: "word" });
|
|
201
|
+
titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
|
|
202
|
+
} catch {
|
|
203
|
+
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
248
207
|
}
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"\u5468\u4E8C": 2,
|
|
259
|
-
"\u661F\u671F\u4E8C": 2,
|
|
260
|
-
"tuesday": 2,
|
|
261
|
-
"tue": 2,
|
|
262
|
-
"\u5468\u4E09": 3,
|
|
263
|
-
"\u661F\u671F\u4E09": 3,
|
|
264
|
-
"wednesday": 3,
|
|
265
|
-
"wed": 3,
|
|
266
|
-
"\u5468\u56DB": 4,
|
|
267
|
-
"\u661F\u671F\u56DB": 4,
|
|
268
|
-
"thursday": 4,
|
|
269
|
-
"thu": 4,
|
|
270
|
-
"\u5468\u4E94": 5,
|
|
271
|
-
"\u661F\u671F\u4E94": 5,
|
|
272
|
-
"friday": 5,
|
|
273
|
-
"fri": 5,
|
|
274
|
-
"\u5468\u516D": 6,
|
|
275
|
-
"\u661F\u671F\u516D": 6,
|
|
276
|
-
"saturday": 6,
|
|
277
|
-
"sat": 6
|
|
278
|
-
};
|
|
279
|
-
const isNextWeek = lower.includes("\u4E0B\u5468") || lower.includes("next");
|
|
280
|
-
for (const [name, targetDay] of Object.entries(weekdayMap)) {
|
|
281
|
-
if (lower.includes(name)) {
|
|
282
|
-
const curDay = new Date(now).getDay();
|
|
283
|
-
let diff = targetDay - curDay;
|
|
284
|
-
if (diff <= 0 || isNextWeek) {
|
|
285
|
-
diff += 7;
|
|
208
|
+
for (const msg of commitMessages) {
|
|
209
|
+
const msgLower = msg.toLowerCase();
|
|
210
|
+
if (msgLower.includes(idLower) || msgLower.includes(numId)) {
|
|
211
|
+
return { matched: true, message: msg };
|
|
212
|
+
}
|
|
213
|
+
if (titleTokens.length > 0) {
|
|
214
|
+
const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
|
|
215
|
+
if (hits >= Math.min(2, titleTokens.length)) {
|
|
216
|
+
return { matched: true, message: msg };
|
|
286
217
|
}
|
|
287
|
-
return getTodayDateString(now + diff * MS_DAY);
|
|
288
218
|
}
|
|
289
219
|
}
|
|
290
|
-
|
|
291
|
-
if (fullDateMatch) {
|
|
292
|
-
const y = fullDateMatch[1];
|
|
293
|
-
const m = fullDateMatch[2].padStart(2, "0");
|
|
294
|
-
const d = fullDateMatch[3].padStart(2, "0");
|
|
295
|
-
return `${y}-${m}-${d}`;
|
|
296
|
-
}
|
|
297
|
-
const mmddMatch = titleOrDate.match(/\b(\d{1,2})-(\d{1,2})\b/);
|
|
298
|
-
if (mmddMatch) {
|
|
299
|
-
const curYear = new Date(now).getFullYear();
|
|
300
|
-
const m = mmddMatch[1].padStart(2, "0");
|
|
301
|
-
const d = mmddMatch[2].padStart(2, "0");
|
|
302
|
-
return `${curYear}-${m}-${d}`;
|
|
303
|
-
}
|
|
304
|
-
return void 0;
|
|
220
|
+
return { matched: false };
|
|
305
221
|
}
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
222
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
223
|
+
let cwd;
|
|
224
|
+
let now = Date.now();
|
|
225
|
+
if (typeof cwdOrNow === "number") now = cwdOrNow;
|
|
226
|
+
else if (typeof cwdOrNow === "string") cwd = cwdOrNow;
|
|
227
|
+
if (typeof nowArg === "number") now = nowArg;
|
|
228
|
+
sweepQuietNotes(store, now);
|
|
229
|
+
const activeNotes = store.list({ status: "active", cwd });
|
|
230
|
+
if (activeNotes.length === 0) return "";
|
|
231
|
+
const lines = activeNotes.map((a) => {
|
|
232
|
+
const num = a.id.replace(/^anc-/, "");
|
|
233
|
+
const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
|
|
234
|
+
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
235
|
+
return `\u2022 #${num} [${a.project}] ${a.title}${files}${desc}`;
|
|
236
|
+
});
|
|
237
|
+
return [
|
|
238
|
+
`<sticky-notes count="${activeNotes.length}">`,
|
|
239
|
+
`Pinned notes from previous sessions:`,
|
|
240
|
+
lines.join("\n"),
|
|
241
|
+
`When a note is completed, call anchor(action="settle", id="...") to tear it off.`,
|
|
242
|
+
`</sticky-notes>`
|
|
243
|
+
].join("\n");
|
|
319
244
|
}
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
for (const file of ["state.json", "archive.jsonl", "graveyard.jsonl"]) {
|
|
330
|
-
const src = import_node_path2.default.join(legacyPiDir, file);
|
|
331
|
-
const dest = import_node_path2.default.join(primaryDir, file);
|
|
332
|
-
if (import_node_fs.default.existsSync(src) && !import_node_fs.default.existsSync(dest)) {
|
|
333
|
-
import_node_fs.default.copyFileSync(src, dest);
|
|
334
|
-
}
|
|
245
|
+
function sweepQuietNotes(store, now = Date.now()) {
|
|
246
|
+
return store.withLock(() => {
|
|
247
|
+
const state = store.loadState();
|
|
248
|
+
const quieted = [];
|
|
249
|
+
for (const a of state.anchors) {
|
|
250
|
+
if (a.status === "active" && now - a.lastTouchedAt > QUIET_AFTER_DAYS * MS_PER_DAY) {
|
|
251
|
+
a.status = "sleeping";
|
|
252
|
+
a.updatedAt = now;
|
|
253
|
+
quieted.push(a.id);
|
|
335
254
|
}
|
|
336
|
-
} catch {
|
|
337
255
|
}
|
|
338
|
-
|
|
339
|
-
|
|
256
|
+
if (quieted.length > 0) {
|
|
257
|
+
state.lastSweepAt = now;
|
|
258
|
+
store.saveState(state);
|
|
259
|
+
}
|
|
260
|
+
return { quieted };
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function getDefaultStorageDir() {
|
|
264
|
+
if (process.env.ANCHOR_DIR) return import_node_path.default.resolve(process.env.ANCHOR_DIR);
|
|
265
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".anchor");
|
|
340
266
|
}
|
|
341
267
|
function sleepSync(ms) {
|
|
342
268
|
const end = Date.now() + ms;
|
|
@@ -358,22 +284,18 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
|
|
|
358
284
|
attempt++;
|
|
359
285
|
try {
|
|
360
286
|
const fd = import_node_fs.default.openSync(lockPath, "wx");
|
|
361
|
-
|
|
362
|
-
import_node_fs.default.writeFileSync(fd, JSON.stringify(meta), "utf-8");
|
|
287
|
+
import_node_fs.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), "utf-8");
|
|
363
288
|
import_node_fs.default.closeSync(fd);
|
|
364
289
|
return () => {
|
|
365
290
|
try {
|
|
366
|
-
if (import_node_fs.default.existsSync(lockPath))
|
|
367
|
-
import_node_fs.default.unlinkSync(lockPath);
|
|
368
|
-
}
|
|
291
|
+
if (import_node_fs.default.existsSync(lockPath)) import_node_fs.default.unlinkSync(lockPath);
|
|
369
292
|
} catch {
|
|
370
293
|
}
|
|
371
294
|
};
|
|
372
295
|
} catch (err) {
|
|
373
296
|
if (err.code === "EEXIST") {
|
|
374
297
|
try {
|
|
375
|
-
const
|
|
376
|
-
const meta = JSON.parse(content);
|
|
298
|
+
const meta = JSON.parse(import_node_fs.default.readFileSync(lockPath, "utf-8"));
|
|
377
299
|
const isStale = Date.now() - meta.createdAt > staleTimeoutMs;
|
|
378
300
|
const isDead = meta.pid && !isProcessAlive(meta.pid);
|
|
379
301
|
if (isStale || isDead) {
|
|
@@ -385,8 +307,7 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
|
|
|
385
307
|
}
|
|
386
308
|
} catch {
|
|
387
309
|
}
|
|
388
|
-
|
|
389
|
-
sleepSync(delay);
|
|
310
|
+
sleepSync(Math.min(40, Math.floor(attempt * 4 + Math.random() * 8)));
|
|
390
311
|
continue;
|
|
391
312
|
}
|
|
392
313
|
break;
|
|
@@ -395,50 +316,46 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
|
|
|
395
316
|
return () => {
|
|
396
317
|
};
|
|
397
318
|
}
|
|
398
|
-
function atomicRenameWithRetry(tempPath, targetPath,
|
|
399
|
-
|
|
319
|
+
function atomicRenameWithRetry(tempPath, targetPath, maxRetries = 5) {
|
|
320
|
+
let lastErr;
|
|
321
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
400
322
|
try {
|
|
401
323
|
import_node_fs.default.renameSync(tempPath, targetPath);
|
|
402
324
|
return;
|
|
403
325
|
} catch (err) {
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
sleepSync(delay);
|
|
326
|
+
lastErr = err;
|
|
327
|
+
if (err.code === "EPERM" || err.code === "EACCES" || err.code === "EBUSY") {
|
|
328
|
+
sleepSync(15 * (i + 1));
|
|
408
329
|
continue;
|
|
409
330
|
}
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
if (import_node_fs.default.existsSync(tempPath)) import_node_fs.default.renameSync(tempPath, crashDump);
|
|
413
|
-
} catch {
|
|
414
|
-
}
|
|
415
|
-
throw err;
|
|
331
|
+
break;
|
|
416
332
|
}
|
|
417
333
|
}
|
|
334
|
+
try {
|
|
335
|
+
import_node_fs.default.copyFileSync(tempPath, targetPath);
|
|
336
|
+
import_node_fs.default.unlinkSync(tempPath);
|
|
337
|
+
} catch {
|
|
338
|
+
throw lastErr;
|
|
339
|
+
}
|
|
418
340
|
}
|
|
419
341
|
var AnchorStore = class {
|
|
420
342
|
storageDir;
|
|
421
343
|
statePath;
|
|
344
|
+
lockPath;
|
|
422
345
|
archivePath;
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
this.storageDir = getDefaultStorageDir();
|
|
429
|
-
}
|
|
430
|
-
this.statePath = import_node_path2.default.join(this.storageDir, "state.json");
|
|
431
|
-
this.archivePath = import_node_path2.default.join(this.storageDir, "archive.jsonl");
|
|
432
|
-
this.graveyardPath = import_node_path2.default.join(this.storageDir, "graveyard.jsonl");
|
|
433
|
-
this.ensureDirs();
|
|
346
|
+
constructor(storageDir) {
|
|
347
|
+
this.storageDir = storageDir ? import_node_path.default.resolve(storageDir) : getDefaultStorageDir();
|
|
348
|
+
this.statePath = import_node_path.default.join(this.storageDir, "state.json");
|
|
349
|
+
this.lockPath = import_node_path.default.join(this.storageDir, "state.lock");
|
|
350
|
+
this.archivePath = import_node_path.default.join(this.storageDir, "archive.jsonl");
|
|
434
351
|
}
|
|
435
|
-
|
|
436
|
-
|
|
352
|
+
ensureDirs() {
|
|
353
|
+
if (!import_node_fs.default.existsSync(this.storageDir)) {
|
|
354
|
+
import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
|
|
355
|
+
}
|
|
437
356
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Execute mutation within a cross-process exclusive lock
|
|
440
|
-
*/
|
|
441
357
|
withLock(fn) {
|
|
358
|
+
this.ensureDirs();
|
|
442
359
|
const release = acquireSyncLock(this.lockPath);
|
|
443
360
|
try {
|
|
444
361
|
return fn();
|
|
@@ -446,84 +363,35 @@ var AnchorStore = class {
|
|
|
446
363
|
release();
|
|
447
364
|
}
|
|
448
365
|
}
|
|
449
|
-
ensureDirs() {
|
|
450
|
-
if (!import_node_fs.default.existsSync(this.storageDir)) {
|
|
451
|
-
try {
|
|
452
|
-
import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
|
|
453
|
-
} catch {
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Load store state. If corrupt or missing, returns safe default.
|
|
459
|
-
*/
|
|
460
366
|
loadState() {
|
|
461
367
|
this.ensureDirs();
|
|
462
368
|
if (!import_node_fs.default.existsSync(this.statePath)) {
|
|
463
|
-
return {
|
|
464
|
-
version: 1,
|
|
465
|
-
anchors: [],
|
|
466
|
-
lastSweepAt: Date.now()
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
let raw = null;
|
|
470
|
-
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
471
|
-
try {
|
|
472
|
-
raw = import_node_fs.default.readFileSync(this.statePath, "utf-8");
|
|
473
|
-
break;
|
|
474
|
-
} catch (err) {
|
|
475
|
-
if ((err?.code === "EBUSY" || err?.code === "EPERM") && attempt < 3) {
|
|
476
|
-
sleepSync(15);
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
if (raw === null) {
|
|
482
|
-
return { version: 1, anchors: [], lastSweepAt: Date.now() };
|
|
369
|
+
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
483
370
|
}
|
|
484
371
|
try {
|
|
485
|
-
const
|
|
486
|
-
if (!
|
|
487
|
-
|
|
372
|
+
const parsed = JSON.parse(import_node_fs.default.readFileSync(this.statePath, "utf-8"));
|
|
373
|
+
if (!parsed || !Array.isArray(parsed.anchors)) {
|
|
374
|
+
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
488
375
|
}
|
|
489
|
-
|
|
490
|
-
...a,
|
|
491
|
-
files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
|
|
492
|
-
tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
|
|
493
|
-
decay: {
|
|
494
|
-
activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
|
|
495
|
-
sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
|
|
496
|
-
graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
|
|
497
|
-
}
|
|
498
|
-
}));
|
|
499
|
-
return data;
|
|
376
|
+
return parsed;
|
|
500
377
|
} catch {
|
|
501
|
-
const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
|
|
502
378
|
try {
|
|
503
|
-
import_node_fs.default.
|
|
379
|
+
import_node_fs.default.copyFileSync(this.statePath, import_node_path.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
|
|
504
380
|
} catch {
|
|
505
381
|
}
|
|
506
|
-
return {
|
|
507
|
-
version: 1,
|
|
508
|
-
anchors: [],
|
|
509
|
-
lastSweepAt: Date.now()
|
|
510
|
-
};
|
|
382
|
+
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
511
383
|
}
|
|
512
384
|
}
|
|
513
|
-
/**
|
|
514
|
-
* Atomically save store state via temp file + atomic rename with Windows NTFS spin-retry
|
|
515
|
-
*/
|
|
516
385
|
saveState(state) {
|
|
517
386
|
this.ensureDirs();
|
|
518
|
-
const
|
|
519
|
-
const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
|
|
387
|
+
const tempPath = `${this.statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
520
388
|
import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
|
|
521
389
|
atomicRenameWithRetry(tempPath, this.statePath);
|
|
522
390
|
}
|
|
523
391
|
generateId(state) {
|
|
524
392
|
const numbers = state.anchors.map((a) => {
|
|
525
|
-
const
|
|
526
|
-
return
|
|
393
|
+
const m = a.id.match(/^anc-(\d+)$/);
|
|
394
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
527
395
|
}).filter((n) => n > 0);
|
|
528
396
|
const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
|
|
529
397
|
const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
|
|
@@ -531,7 +399,7 @@ var AnchorStore = class {
|
|
|
531
399
|
return `anc-${nextNum}`;
|
|
532
400
|
}
|
|
533
401
|
/**
|
|
534
|
-
*
|
|
402
|
+
* Pin a new sticky note
|
|
535
403
|
*/
|
|
536
404
|
create(input) {
|
|
537
405
|
return this.withLock(() => {
|
|
@@ -544,34 +412,23 @@ var AnchorStore = class {
|
|
|
544
412
|
if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
|
|
545
413
|
cwd = normalizePath(input.project.trim());
|
|
546
414
|
} else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
|
|
547
|
-
const firstDir =
|
|
548
|
-
if (firstDir && firstDir !== ".")
|
|
549
|
-
cwd = normalizePath(firstDir);
|
|
550
|
-
}
|
|
415
|
+
const firstDir = import_node_path.default.dirname(inferredFiles[0]);
|
|
416
|
+
if (firstDir && firstDir !== ".") cwd = normalizePath(firstDir);
|
|
551
417
|
}
|
|
552
|
-
const projectName = input.project ? input.project.trim() : cwd ?
|
|
553
|
-
const recurrence = input.recurrence || detectRecurrence(input.title);
|
|
554
|
-
const targetDate = input.targetDate || detectTargetDate(input.title, now);
|
|
555
|
-
const durability = recurrence ? "durable" : input.durability || detectDurability(input.title);
|
|
556
|
-
const decayPolicy = durability === "ephemeral" ? getEphemeralDecayPolicy(input.title) : DURABLE_DECAY_POLICY;
|
|
418
|
+
const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
|
|
557
419
|
const anchor = {
|
|
558
420
|
id,
|
|
559
421
|
title: input.title.trim(),
|
|
560
422
|
description: input.description?.trim(),
|
|
561
423
|
priority: input.priority || "p1",
|
|
562
424
|
status: "active",
|
|
563
|
-
durability,
|
|
564
|
-
recurrence,
|
|
565
|
-
targetDate,
|
|
566
425
|
project: projectName,
|
|
567
426
|
cwd,
|
|
568
427
|
createdAt: now,
|
|
569
428
|
updatedAt: now,
|
|
570
429
|
lastTouchedAt: now,
|
|
571
430
|
files: inferredFiles,
|
|
572
|
-
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
|
|
573
|
-
verifyCommand: input.verifyCommand?.trim() || void 0,
|
|
574
|
-
decay: input.decay ? { ...input.decay } : { ...decayPolicy }
|
|
431
|
+
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean)
|
|
575
432
|
};
|
|
576
433
|
state.anchors.push(anchor);
|
|
577
434
|
this.saveState(state);
|
|
@@ -581,31 +438,19 @@ var AnchorStore = class {
|
|
|
581
438
|
findAnchorIndex(anchors, id) {
|
|
582
439
|
const clean = id.trim().replace(/^#/, "");
|
|
583
440
|
return anchors.findIndex(
|
|
584
|
-
(a) => a.id === clean || a.id === `anc-${clean}` || (
|
|
441
|
+
(a) => a.id === clean || a.id === `anc-${clean}` || (/^\d+$/.test(clean) ? a.id === `anc-${parseInt(clean, 10)}` : false)
|
|
585
442
|
);
|
|
586
443
|
}
|
|
587
|
-
/**
|
|
588
|
-
* Get an anchor by ID (supports 'anc-1', '#anc-1', '1', '01')
|
|
589
|
-
*/
|
|
590
444
|
get(id) {
|
|
591
445
|
const state = this.loadState();
|
|
592
446
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
593
447
|
return idx === -1 ? void 0 : state.anchors[idx];
|
|
594
448
|
}
|
|
595
|
-
/**
|
|
596
|
-
* List anchors. By default, if cwd is provided, only returns anchors
|
|
597
|
-
* belonging to this cwd or marked as global (''). Pass all: true for global view.
|
|
598
|
-
*/
|
|
599
449
|
list(filter) {
|
|
600
450
|
const state = this.loadState();
|
|
601
451
|
const targetCwd = filter?.cwd ? normalizePath(filter.cwd) : null;
|
|
602
|
-
const today = getTodayDateString(filter?.now);
|
|
603
452
|
return state.anchors.filter((a) => {
|
|
604
|
-
if (!filter?.all && filter?.status === "active" && a.recurrence === "daily" && a.lastCompletedDate === today) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
453
|
if (filter?.status && a.status !== filter.status) return false;
|
|
608
|
-
if (filter?.priority && a.priority !== filter.priority) return false;
|
|
609
454
|
if (!filter?.all && targetCwd) {
|
|
610
455
|
if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
|
|
611
456
|
return false;
|
|
@@ -614,19 +459,13 @@ var AnchorStore = class {
|
|
|
614
459
|
return true;
|
|
615
460
|
});
|
|
616
461
|
}
|
|
617
|
-
/**
|
|
618
|
-
* Update an existing anchor
|
|
619
|
-
*/
|
|
620
462
|
update(id, patch) {
|
|
621
463
|
return this.withLock(() => {
|
|
622
464
|
const state = this.loadState();
|
|
623
465
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
624
|
-
if (idx === -1) {
|
|
625
|
-
throw new Error(`Anchor not found: ${id}`);
|
|
626
|
-
}
|
|
627
|
-
const current = state.anchors[idx];
|
|
466
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
628
467
|
const updated = {
|
|
629
|
-
...
|
|
468
|
+
...state.anchors[idx],
|
|
630
469
|
...patch,
|
|
631
470
|
updatedAt: Date.now()
|
|
632
471
|
};
|
|
@@ -638,16 +477,11 @@ var AnchorStore = class {
|
|
|
638
477
|
return updated;
|
|
639
478
|
});
|
|
640
479
|
}
|
|
641
|
-
/**
|
|
642
|
-
* Touch an anchor to refresh its decay window and reactivate sleeping state
|
|
643
|
-
*/
|
|
644
480
|
touch(id, timestamp = Date.now()) {
|
|
645
481
|
return this.withLock(() => {
|
|
646
482
|
const state = this.loadState();
|
|
647
483
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
648
|
-
if (idx === -1) {
|
|
649
|
-
throw new Error(`Anchor not found: ${id}`);
|
|
650
|
-
}
|
|
484
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
651
485
|
const current = state.anchors[idx];
|
|
652
486
|
current.lastTouchedAt = timestamp;
|
|
653
487
|
current.updatedAt = timestamp;
|
|
@@ -660,38 +494,20 @@ var AnchorStore = class {
|
|
|
660
494
|
});
|
|
661
495
|
}
|
|
662
496
|
/**
|
|
663
|
-
*
|
|
497
|
+
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
664
498
|
*/
|
|
665
499
|
settle(id, evidence = {}, now = Date.now()) {
|
|
666
500
|
return this.withLock(() => {
|
|
667
501
|
const state = this.loadState();
|
|
668
502
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
669
|
-
if (idx === -1) {
|
|
670
|
-
throw new Error(`Anchor not found: ${id}`);
|
|
671
|
-
}
|
|
672
|
-
const anchor = state.anchors[idx];
|
|
673
|
-
const today = getTodayDateString(now);
|
|
674
|
-
if (anchor.recurrence === "daily") {
|
|
675
|
-
anchor.lastCompletedDate = today;
|
|
676
|
-
anchor.lastTouchedAt = now;
|
|
677
|
-
anchor.updatedAt = now;
|
|
678
|
-
anchor.evidence = {
|
|
679
|
-
...evidence,
|
|
680
|
-
settledAt: now,
|
|
681
|
-
settledBy: evidence.settledBy || "manual-command"
|
|
682
|
-
};
|
|
683
|
-
this.ensureDirs();
|
|
684
|
-
import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify({ ...anchor, settledForDate: today }) + "\n", "utf-8");
|
|
685
|
-
this.saveState(state);
|
|
686
|
-
return anchor;
|
|
687
|
-
}
|
|
503
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
688
504
|
const [settled] = state.anchors.splice(idx, 1);
|
|
689
505
|
settled.status = "settled";
|
|
690
506
|
settled.updatedAt = now;
|
|
691
507
|
settled.evidence = {
|
|
692
508
|
...evidence,
|
|
693
509
|
settledAt: now,
|
|
694
|
-
settledBy: evidence.settledBy || "manual
|
|
510
|
+
settledBy: evidence.settledBy || "manual"
|
|
695
511
|
};
|
|
696
512
|
this.ensureDirs();
|
|
697
513
|
import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
|
|
@@ -700,12 +516,12 @@ var AnchorStore = class {
|
|
|
700
516
|
});
|
|
701
517
|
}
|
|
702
518
|
/**
|
|
703
|
-
*
|
|
519
|
+
* Put back (undo) the last torn-off sticky note
|
|
704
520
|
*/
|
|
705
521
|
undoSettle() {
|
|
706
522
|
return this.withLock(() => {
|
|
707
523
|
if (!import_node_fs.default.existsSync(this.archivePath)) {
|
|
708
|
-
throw new Error("No archived
|
|
524
|
+
throw new Error("No archived notes to restore");
|
|
709
525
|
}
|
|
710
526
|
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
711
527
|
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
@@ -714,23 +530,18 @@ var AnchorStore = class {
|
|
|
714
530
|
const candidate = lines.pop();
|
|
715
531
|
try {
|
|
716
532
|
const parsed = JSON.parse(candidate);
|
|
717
|
-
if (parsed && typeof parsed
|
|
533
|
+
if (parsed && typeof parsed.id === "string" && typeof parsed.title === "string") {
|
|
718
534
|
anchor = parsed;
|
|
719
535
|
}
|
|
720
536
|
} catch {
|
|
721
537
|
}
|
|
722
538
|
}
|
|
723
|
-
if (!anchor)
|
|
724
|
-
throw new Error("Archive is empty, nothing to undo");
|
|
725
|
-
}
|
|
539
|
+
if (!anchor) throw new Error("Archive is empty, nothing to undo");
|
|
726
540
|
import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
727
541
|
anchor.status = "active";
|
|
728
542
|
anchor.updatedAt = Date.now();
|
|
543
|
+
anchor.lastTouchedAt = Date.now();
|
|
729
544
|
delete anchor.evidence;
|
|
730
|
-
delete anchor.settledForDate;
|
|
731
|
-
if (anchor.recurrence === "daily") {
|
|
732
|
-
delete anchor.lastCompletedDate;
|
|
733
|
-
}
|
|
734
545
|
const state = this.loadState();
|
|
735
546
|
const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
|
|
736
547
|
if (existingIdx !== -1) {
|
|
@@ -741,540 +552,18 @@ var AnchorStore = class {
|
|
|
741
552
|
state.anchors.push(anchor);
|
|
742
553
|
}
|
|
743
554
|
} else {
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
const restoredNum = parseInt(match[1], 10);
|
|
747
|
-
state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
|
|
748
|
-
}
|
|
555
|
+
const m = anchor.id.match(/^anc-(\d+)$/);
|
|
556
|
+
if (m) state.nextId = Math.max(state.nextId || 1, parseInt(m[1], 10) + 1);
|
|
749
557
|
state.anchors.push(anchor);
|
|
750
558
|
}
|
|
751
559
|
this.saveState(state);
|
|
752
560
|
return anchor;
|
|
753
561
|
});
|
|
754
562
|
}
|
|
755
|
-
/**
|
|
756
|
-
* Evict anchor to graveyard.jsonl upon total decay expiry
|
|
757
|
-
*/
|
|
758
|
-
dropToGraveyard(id, reason) {
|
|
759
|
-
return this.withLock(() => {
|
|
760
|
-
const state = this.loadState();
|
|
761
|
-
const idx = state.anchors.findIndex((a) => a.id === id);
|
|
762
|
-
if (idx === -1) {
|
|
763
|
-
throw new Error(`Anchor not found: ${id}`);
|
|
764
|
-
}
|
|
765
|
-
const [anchor] = state.anchors.splice(idx, 1);
|
|
766
|
-
anchor.status = "graveyard";
|
|
767
|
-
anchor.updatedAt = Date.now();
|
|
768
|
-
anchor.evidence = {
|
|
769
|
-
summary: reason,
|
|
770
|
-
settledAt: Date.now()
|
|
771
|
-
};
|
|
772
|
-
this.ensureDirs();
|
|
773
|
-
import_node_fs.default.appendFileSync(this.graveyardPath, JSON.stringify(anchor) + "\n", "utf-8");
|
|
774
|
-
this.saveState(state);
|
|
775
|
-
return anchor;
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
getArchive(filter) {
|
|
779
|
-
if (!import_node_fs.default.existsSync(this.archivePath)) return [];
|
|
780
|
-
try {
|
|
781
|
-
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
782
|
-
const all = [];
|
|
783
|
-
for (const l of raw.split(/\r?\n/)) {
|
|
784
|
-
const trimmed = l.trim();
|
|
785
|
-
if (!trimmed) continue;
|
|
786
|
-
try {
|
|
787
|
-
const parsed = JSON.parse(trimmed);
|
|
788
|
-
if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
|
|
789
|
-
} catch {
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
if (filter?.all || !filter?.cwd) return all;
|
|
793
|
-
const targetCwd = normalizePath(filter.cwd);
|
|
794
|
-
return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
|
|
795
|
-
} catch {
|
|
796
|
-
return [];
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
getGraveyard(filter) {
|
|
800
|
-
if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
|
|
801
|
-
try {
|
|
802
|
-
const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
|
|
803
|
-
const all = [];
|
|
804
|
-
for (const l of raw.split(/\r?\n/)) {
|
|
805
|
-
const trimmed = l.trim();
|
|
806
|
-
if (!trimmed) continue;
|
|
807
|
-
try {
|
|
808
|
-
const parsed = JSON.parse(trimmed);
|
|
809
|
-
if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
|
|
810
|
-
} catch {
|
|
811
|
-
}
|
|
812
|
-
}
|
|
813
|
-
if (filter?.all || !filter?.cwd) return all;
|
|
814
|
-
const targetCwd = normalizePath(filter.cwd);
|
|
815
|
-
return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
|
|
816
|
-
} catch {
|
|
817
|
-
return [];
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
};
|
|
821
|
-
|
|
822
|
-
// src/observer.ts
|
|
823
|
-
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
824
|
-
var SessionTouchObserver = class {
|
|
825
|
-
touchedFiles = /* @__PURE__ */ new Set();
|
|
826
|
-
modifiedFiles = /* @__PURE__ */ new Set();
|
|
827
|
-
inspectedFiles = /* @__PURE__ */ new Set();
|
|
828
|
-
committed = false;
|
|
829
|
-
commitMessages = [];
|
|
830
|
-
clear() {
|
|
831
|
-
this.touchedFiles.clear();
|
|
832
|
-
this.modifiedFiles.clear();
|
|
833
|
-
this.inspectedFiles.clear();
|
|
834
|
-
this.committed = false;
|
|
835
|
-
this.commitMessages = [];
|
|
836
|
-
}
|
|
837
|
-
/**
|
|
838
|
-
* Observe and record a tool call invocation, separating inspection from mutation
|
|
839
|
-
*/
|
|
840
|
-
recordToolCall(toolName, input) {
|
|
841
|
-
if (!input || typeof input !== "object") return;
|
|
842
|
-
const lowerTool = (toolName || "").toLowerCase();
|
|
843
|
-
const isMutation = MUTATION_TOOLS.has(lowerTool);
|
|
844
|
-
const paths = [];
|
|
845
|
-
const pathField = input.path || input.filePath || input.file;
|
|
846
|
-
if (typeof pathField === "string") {
|
|
847
|
-
paths.push(normalizePath(pathField));
|
|
848
|
-
}
|
|
849
|
-
if (Array.isArray(input.paths)) {
|
|
850
|
-
for (const p of input.paths) {
|
|
851
|
-
if (typeof p === "string") {
|
|
852
|
-
paths.push(normalizePath(p));
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
for (const p of paths) {
|
|
857
|
-
this.touchedFiles.add(p);
|
|
858
|
-
if (isMutation) {
|
|
859
|
-
this.modifiedFiles.add(p);
|
|
860
|
-
} else {
|
|
861
|
-
this.inspectedFiles.add(p);
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
865
|
-
const cmd = String(input.command || "");
|
|
866
|
-
if (cmd.includes("git commit")) {
|
|
867
|
-
this.committed = true;
|
|
868
|
-
const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
|
|
869
|
-
if (msgMatch && msgMatch[1]) {
|
|
870
|
-
this.commitMessages.push(msgMatch[1]);
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
/**
|
|
876
|
-
* Add a file path manually (e.g. from git status diff)
|
|
877
|
-
*/
|
|
878
|
-
addTouchedFile(filePath, isModified = true) {
|
|
879
|
-
if (filePath) {
|
|
880
|
-
const norm = normalizePath(filePath);
|
|
881
|
-
this.touchedFiles.add(norm);
|
|
882
|
-
if (isModified) {
|
|
883
|
-
this.modifiedFiles.add(norm);
|
|
884
|
-
} else {
|
|
885
|
-
this.inspectedFiles.add(norm);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
getModifiedFiles() {
|
|
890
|
-
return Array.from(this.modifiedFiles);
|
|
891
|
-
}
|
|
892
|
-
getInspectedFiles() {
|
|
893
|
-
return Array.from(this.inspectedFiles);
|
|
894
|
-
}
|
|
895
|
-
getTouchedFiles() {
|
|
896
|
-
return Array.from(this.touchedFiles);
|
|
897
|
-
}
|
|
898
|
-
hasCommitted() {
|
|
899
|
-
return this.committed;
|
|
900
|
-
}
|
|
901
|
-
getCommitMessages() {
|
|
902
|
-
return [...this.commitMessages];
|
|
903
|
-
}
|
|
904
|
-
/**
|
|
905
|
-
* Check if any commit message explicitly references or resolves an anchor
|
|
906
|
-
*/
|
|
907
|
-
matchesCommit(anchor) {
|
|
908
|
-
const idLower = anchor.id.toLowerCase();
|
|
909
|
-
const title = anchor.title.toLowerCase();
|
|
910
|
-
let titleTokens = [];
|
|
911
|
-
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
912
|
-
try {
|
|
913
|
-
const segmenter = new Intl.Segmenter("und", { granularity: "word" });
|
|
914
|
-
titleTokens = Array.from(segmenter.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
|
|
915
|
-
} catch {
|
|
916
|
-
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
917
|
-
}
|
|
918
|
-
} else {
|
|
919
|
-
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
920
|
-
}
|
|
921
|
-
for (const msg of this.commitMessages) {
|
|
922
|
-
const msgLower = msg.toLowerCase();
|
|
923
|
-
if (msgLower.includes(idLower)) {
|
|
924
|
-
return { matched: true, message: msg };
|
|
925
|
-
}
|
|
926
|
-
if (titleTokens.length > 0) {
|
|
927
|
-
const matchingCount = titleTokens.filter((tok) => msgLower.includes(tok)).length;
|
|
928
|
-
if (matchingCount >= Math.min(2, titleTokens.length)) {
|
|
929
|
-
return { matched: true, message: msg };
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
return { matched: false };
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
// src/decay.ts
|
|
938
|
-
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
939
|
-
var MS_PER_DAY = 864e5;
|
|
940
|
-
function evaluateAnchorDecay(anchor, now = Date.now()) {
|
|
941
|
-
if (anchor.status === "settled" || anchor.status === "graveyard") {
|
|
942
|
-
return {
|
|
943
|
-
currentStatus: anchor.status,
|
|
944
|
-
nextStatus: anchor.status,
|
|
945
|
-
daysUntouched: 0,
|
|
946
|
-
remainingActiveDays: 0,
|
|
947
|
-
remainingSleepDays: 0
|
|
948
|
-
};
|
|
949
|
-
}
|
|
950
|
-
const elapsedMs = Math.max(0, now - anchor.lastTouchedAt);
|
|
951
|
-
const daysUntouched = elapsedMs / MS_PER_DAY;
|
|
952
|
-
const activeDays = anchor.decay.activeDays;
|
|
953
|
-
const graveyardDays = anchor.decay.graveyardDays;
|
|
954
|
-
let nextStatus = anchor.status;
|
|
955
|
-
if (daysUntouched <= activeDays) {
|
|
956
|
-
nextStatus = "active";
|
|
957
|
-
} else if (daysUntouched <= graveyardDays) {
|
|
958
|
-
nextStatus = "sleeping";
|
|
959
|
-
} else {
|
|
960
|
-
nextStatus = "graveyard";
|
|
961
|
-
}
|
|
962
|
-
return {
|
|
963
|
-
currentStatus: anchor.status,
|
|
964
|
-
nextStatus,
|
|
965
|
-
daysUntouched: Math.round(daysUntouched * 10) / 10,
|
|
966
|
-
remainingActiveDays: Math.max(0, Math.round((activeDays - daysUntouched) * 10) / 10),
|
|
967
|
-
remainingSleepDays: Math.max(0, Math.round((graveyardDays - daysUntouched) * 10) / 10)
|
|
968
|
-
};
|
|
969
|
-
}
|
|
970
|
-
function sweepStore(store, now = Date.now()) {
|
|
971
|
-
return store.withLock(() => {
|
|
972
|
-
const state = store.loadState();
|
|
973
|
-
const result = {
|
|
974
|
-
transitionedToSleeping: [],
|
|
975
|
-
wokenToActive: [],
|
|
976
|
-
evictedToGraveyard: []
|
|
977
|
-
};
|
|
978
|
-
const toEvict = [];
|
|
979
|
-
let stateModified = false;
|
|
980
|
-
for (const anchor of state.anchors) {
|
|
981
|
-
const evalResult = evaluateAnchorDecay(anchor, now);
|
|
982
|
-
if (evalResult.nextStatus === "graveyard") {
|
|
983
|
-
toEvict.push(anchor);
|
|
984
|
-
} else if (evalResult.nextStatus !== anchor.status) {
|
|
985
|
-
if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
|
|
986
|
-
anchor.status = "sleeping";
|
|
987
|
-
anchor.updatedAt = now;
|
|
988
|
-
result.transitionedToSleeping.push(anchor.id);
|
|
989
|
-
stateModified = true;
|
|
990
|
-
} else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
|
|
991
|
-
anchor.status = "active";
|
|
992
|
-
anchor.updatedAt = now;
|
|
993
|
-
result.wokenToActive.push(anchor.id);
|
|
994
|
-
stateModified = true;
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
if (toEvict.length > 0) {
|
|
999
|
-
const evictIds = new Set(toEvict.map((e) => e.id));
|
|
1000
|
-
state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
|
|
1001
|
-
stateModified = true;
|
|
1002
|
-
const graveyardLines = toEvict.map((exp) => {
|
|
1003
|
-
const rec = {
|
|
1004
|
-
...exp,
|
|
1005
|
-
status: "graveyard",
|
|
1006
|
-
updatedAt: now,
|
|
1007
|
-
evictedAt: now,
|
|
1008
|
-
evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
|
|
1009
|
-
};
|
|
1010
|
-
return JSON.stringify(rec);
|
|
1011
|
-
}).join("\n") + "\n";
|
|
1012
|
-
import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
|
|
1013
|
-
for (const exp of toEvict) {
|
|
1014
|
-
result.evictedToGraveyard.push(exp.id);
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
state.lastSweepAt = now;
|
|
1018
|
-
if (stateModified) {
|
|
1019
|
-
store.saveState(state);
|
|
1020
|
-
}
|
|
1021
|
-
return result;
|
|
1022
|
-
});
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
// src/context_injector.ts
|
|
1026
|
-
var import_node_path3 = __toESM(require("path"), 1);
|
|
1027
|
-
var COMMENT_FORMATS = {
|
|
1028
|
-
// Double slash //
|
|
1029
|
-
".ts": (m) => `// ${m}`,
|
|
1030
|
-
".tsx": (m) => `// ${m}`,
|
|
1031
|
-
".js": (m) => `// ${m}`,
|
|
1032
|
-
".jsx": (m) => `// ${m}`,
|
|
1033
|
-
".mjs": (m) => `// ${m}`,
|
|
1034
|
-
".cjs": (m) => `// ${m}`,
|
|
1035
|
-
".java": (m) => `// ${m}`,
|
|
1036
|
-
".kt": (m) => `// ${m}`,
|
|
1037
|
-
".swift": (m) => `// ${m}`,
|
|
1038
|
-
".go": (m) => `// ${m}`,
|
|
1039
|
-
".rs": (m) => `// ${m}`,
|
|
1040
|
-
".c": (m) => `// ${m}`,
|
|
1041
|
-
".cpp": (m) => `// ${m}`,
|
|
1042
|
-
".h": (m) => `// ${m}`,
|
|
1043
|
-
".hpp": (m) => `// ${m}`,
|
|
1044
|
-
".cs": (m) => `// ${m}`,
|
|
1045
|
-
".dart": (m) => `// ${m}`,
|
|
1046
|
-
".scala": (m) => `// ${m}`,
|
|
1047
|
-
".zig": (m) => `// ${m}`,
|
|
1048
|
-
".proto": (m) => `// ${m}`,
|
|
1049
|
-
// Hash #
|
|
1050
|
-
".py": (m) => `# ${m}`,
|
|
1051
|
-
".rb": (m) => `# ${m}`,
|
|
1052
|
-
".sh": (m) => `# ${m}`,
|
|
1053
|
-
".bash": (m) => `# ${m}`,
|
|
1054
|
-
".zsh": (m) => `# ${m}`,
|
|
1055
|
-
".yaml": (m) => `# ${m}`,
|
|
1056
|
-
".yml": (m) => `# ${m}`,
|
|
1057
|
-
".toml": (m) => `# ${m}`,
|
|
1058
|
-
".conf": (m) => `# ${m}`,
|
|
1059
|
-
".dockerfile": (m) => `# ${m}`,
|
|
1060
|
-
".ps1": (m) => `# ${m}`,
|
|
1061
|
-
".r": (m) => `# ${m}`,
|
|
1062
|
-
// HTML / XML <!-- -->
|
|
1063
|
-
".html": (m) => `<!-- ${m} -->`,
|
|
1064
|
-
".xml": (m) => `<!-- ${m} -->`,
|
|
1065
|
-
".svg": (m) => `<!-- ${m} -->`,
|
|
1066
|
-
".vue": (m) => `<!-- ${m} -->`,
|
|
1067
|
-
".svelte": (m) => `<!-- ${m} -->`,
|
|
1068
|
-
// Block comment /* */
|
|
1069
|
-
".css": (m) => `/* ${m} */`,
|
|
1070
|
-
".scss": (m) => `/* ${m} */`,
|
|
1071
|
-
".less": (m) => `/* ${m} */`,
|
|
1072
|
-
// SQL / Lua / Haskell --
|
|
1073
|
-
".sql": (m) => `-- ${m}`,
|
|
1074
|
-
".lua": (m) => `-- ${m}`,
|
|
1075
|
-
".hs": (m) => `-- ${m}`,
|
|
1076
|
-
// Batch REM
|
|
1077
|
-
".bat": (m) => `REM ${m}`,
|
|
1078
|
-
".cmd": (m) => `REM ${m}`
|
|
1079
563
|
};
|
|
1080
|
-
function makeSafeTaskAnnotation(filePath, anchor) {
|
|
1081
|
-
const ext = import_node_path3.default.extname(filePath).toLowerCase();
|
|
1082
|
-
const formatter = COMMENT_FORMATS[ext];
|
|
1083
|
-
if (!formatter) {
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
1086
|
-
return "\n\n" + formatter(`\u2316 anchor context: #${anchor.id} ${anchor.title} (${anchor.priority.toUpperCase()})`);
|
|
1087
|
-
}
|
|
1088
|
-
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailures) {
|
|
1089
|
-
let cwd;
|
|
1090
|
-
let now = Date.now();
|
|
1091
|
-
if (typeof cwdOrNow === "number") {
|
|
1092
|
-
now = cwdOrNow;
|
|
1093
|
-
} else if (typeof cwdOrNow === "string") {
|
|
1094
|
-
cwd = cwdOrNow;
|
|
1095
|
-
}
|
|
1096
|
-
if (typeof nowArg === "number") {
|
|
1097
|
-
now = nowArg;
|
|
1098
|
-
}
|
|
1099
|
-
sweepStore(store, now);
|
|
1100
|
-
const activeAnchors = store.list({ status: "active", cwd });
|
|
1101
|
-
if (activeAnchors.length === 0) {
|
|
1102
|
-
return "";
|
|
1103
|
-
}
|
|
1104
|
-
const lines = activeAnchors.map((a) => {
|
|
1105
|
-
const fileHint = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
|
|
1106
|
-
const tagHint = a.tags.length > 0 ? ` [${a.tags.join(", ")}]` : "";
|
|
1107
|
-
const projHint = a.cwd ? `[${a.project}]` : "[global]";
|
|
1108
|
-
let targetHint = "";
|
|
1109
|
-
if (a.recurrence === "daily") {
|
|
1110
|
-
targetHint = " [\u4ECA\u65E5\u5FAA\u73AF\xB7Daily]";
|
|
1111
|
-
} else if (a.targetDate) {
|
|
1112
|
-
const todayStr = getTodayDateString(now);
|
|
1113
|
-
if (a.targetDate === todayStr) {
|
|
1114
|
-
targetHint = " [\u4ECA\u65E5\u805A\u7126\xB7Due Today]";
|
|
1115
|
-
} else if (a.targetDate < todayStr) {
|
|
1116
|
-
targetHint = " [\u5DF2\u903E\u671F\xB7Overdue]";
|
|
1117
|
-
} else {
|
|
1118
|
-
targetHint = ` [\u6392\u671F:${a.targetDate}]`;
|
|
1119
|
-
}
|
|
1120
|
-
} else {
|
|
1121
|
-
targetHint = " [\u957F\u671F\u613F\u666F\xB7Someday]";
|
|
1122
|
-
}
|
|
1123
|
-
const ageDays = Math.floor((now - a.createdAt) / (24 * 60 * 60 * 1e3));
|
|
1124
|
-
const ageHint = ageDays > 0 ? ` (${ageDays}d old)` : "";
|
|
1125
|
-
const failureSnippet = preflightFailures?.get(a.id);
|
|
1126
|
-
const verifyHint = failureSnippet ? ` [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25: ${failureSnippet.replace(/\s+/g, " ").slice(-100)}]` : a.verifyCommand ? ` [verify: ${a.verifyCommand}]` : "";
|
|
1127
|
-
return `\u2022 #${a.id} ${projHint}${targetHint} [${a.priority.toUpperCase()}] ${a.title}${verifyHint}${ageHint}${tagHint}${fileHint}`;
|
|
1128
|
-
});
|
|
1129
|
-
return [
|
|
1130
|
-
`<active-anchors count="${activeAnchors.length}">`,
|
|
1131
|
-
`Unfinished cross-session commitments from previous sessions:`,
|
|
1132
|
-
lines.join("\n"),
|
|
1133
|
-
``,
|
|
1134
|
-
`CONVERSATIONAL PROTOCOL (HUMAN PARTNER TONE):`,
|
|
1135
|
-
`- 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").`,
|
|
1136
|
-
`- If an anchor has [\u26A1 \u7269\u7406\u9A8C\u8BC1\u5931\u8D25], its verifyCommand failed on session start. Target the failing assertion directly.`,
|
|
1137
|
-
`- 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.`,
|
|
1138
|
-
`- When a commitment is satisfied during work, automatically call anchor(action="settle", id=...) to clear it.`,
|
|
1139
|
-
`</active-anchors>`
|
|
1140
|
-
].join("\n");
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
|
-
// src/settlement.ts
|
|
1144
|
-
var import_node_child_process = require("child_process");
|
|
1145
|
-
function runPhysicalVerification(anchor, cwd) {
|
|
1146
|
-
if (!anchor.verifyCommand?.trim()) {
|
|
1147
|
-
return { success: false, output: "No verification command specified" };
|
|
1148
|
-
}
|
|
1149
|
-
const targetCwd = anchor.cwd || cwd;
|
|
1150
|
-
try {
|
|
1151
|
-
const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
|
|
1152
|
-
cwd: targetCwd,
|
|
1153
|
-
encoding: "utf-8",
|
|
1154
|
-
timeout: 8e3,
|
|
1155
|
-
maxBuffer: 4 * 1024 * 1024,
|
|
1156
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
1157
|
-
});
|
|
1158
|
-
return { success: true, output: stdout.trim() };
|
|
1159
|
-
} catch (err) {
|
|
1160
|
-
return { success: false, output: String(err.stderr || err.stdout || err.message).trim() };
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
function tryRealtimeAutoSettle(store, observer, cwd, executedCommand) {
|
|
1164
|
-
const candidates = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1165
|
-
if (candidates.length === 0) return [];
|
|
1166
|
-
const settledList = [];
|
|
1167
|
-
const cmdTrimmed = (executedCommand || "").trim();
|
|
1168
|
-
for (const a of candidates) {
|
|
1169
|
-
if (observer.hasCommitted()) {
|
|
1170
|
-
const commitMatch = observer.matchesCommit(a);
|
|
1171
|
-
if (commitMatch.matched && (!cwd || isAnchorTargetingWorkspace(a, cwd))) {
|
|
1172
|
-
const settled = store.settle(a.id, {
|
|
1173
|
-
settledBy: "verification-test",
|
|
1174
|
-
summary: `Auto-settled via Git commit: ${commitMatch.message}`,
|
|
1175
|
-
touchedFiles: observer.getModifiedFiles()
|
|
1176
|
-
});
|
|
1177
|
-
settledList.push({ anchor: settled, reason: `git commit ("${commitMatch.message}")` });
|
|
1178
|
-
continue;
|
|
1179
|
-
}
|
|
1180
|
-
}
|
|
1181
|
-
if (a.verifyCommand && cmdTrimmed && cmdTrimmed.includes(a.verifyCommand.trim())) {
|
|
1182
|
-
const settled = store.settle(a.id, {
|
|
1183
|
-
settledBy: "verification-test",
|
|
1184
|
-
summary: `Auto-settled via passing command: ${a.verifyCommand}`,
|
|
1185
|
-
touchedFiles: observer.getModifiedFiles()
|
|
1186
|
-
});
|
|
1187
|
-
settledList.push({ anchor: settled, reason: `verifyCommand (${a.verifyCommand})` });
|
|
1188
|
-
}
|
|
1189
|
-
}
|
|
1190
|
-
return settledList;
|
|
1191
|
-
}
|
|
1192
|
-
function generateSettlementProposals(store, touchedFiles, cwd, observer) {
|
|
1193
|
-
const activeAndSleeping = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1194
|
-
const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
|
|
1195
|
-
const seenIds = /* @__PURE__ */ new Set();
|
|
1196
|
-
const proposals = [];
|
|
1197
|
-
for (const match of matches) {
|
|
1198
|
-
seenIds.add(match.anchor.id);
|
|
1199
|
-
const fileList = match.matchedFiles.slice(0, 3).join(", ");
|
|
1200
|
-
const moreSuffix = match.matchedFiles.length > 3 ? ` (+${match.matchedFiles.length - 3} more)` : "";
|
|
1201
|
-
proposals.push({
|
|
1202
|
-
anchor: match.anchor,
|
|
1203
|
-
matchedFiles: match.matchedFiles,
|
|
1204
|
-
reason: `Modified ${fileList}${moreSuffix}`,
|
|
1205
|
-
recommendedAction: match.score >= 0.85 ? "settle" : "defer"
|
|
1206
|
-
});
|
|
1207
|
-
}
|
|
1208
|
-
if (observer && observer.hasCommitted()) {
|
|
1209
|
-
for (const a of activeAndSleeping) {
|
|
1210
|
-
if (seenIds.has(a.id)) continue;
|
|
1211
|
-
const cm = observer.matchesCommit(a);
|
|
1212
|
-
if (cm.matched) {
|
|
1213
|
-
seenIds.add(a.id);
|
|
1214
|
-
proposals.push({
|
|
1215
|
-
anchor: a,
|
|
1216
|
-
matchedFiles: touchedFiles.slice(0, 3),
|
|
1217
|
-
reason: `Matched git commit: "${cm.message}"`,
|
|
1218
|
-
recommendedAction: "settle"
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
return proposals;
|
|
1224
|
-
}
|
|
1225
564
|
|
|
1226
565
|
// src/tui.ts
|
|
1227
|
-
var
|
|
1228
|
-
function classifyAnchor(anchor, now = Date.now()) {
|
|
1229
|
-
if (anchor.recurrence === "daily") {
|
|
1230
|
-
return "Habits";
|
|
1231
|
-
}
|
|
1232
|
-
if (!anchor.targetDate) {
|
|
1233
|
-
return "Backlog";
|
|
1234
|
-
}
|
|
1235
|
-
const todayStr = getTodayDateString(now);
|
|
1236
|
-
if (anchor.targetDate <= todayStr) {
|
|
1237
|
-
return "Today";
|
|
1238
|
-
}
|
|
1239
|
-
return "Upcoming";
|
|
1240
|
-
}
|
|
1241
|
-
function groupAnchorsByQuadrant(anchors, now = Date.now()) {
|
|
1242
|
-
const groups = {
|
|
1243
|
-
today: [],
|
|
1244
|
-
upcoming: [],
|
|
1245
|
-
habits: [],
|
|
1246
|
-
backlog: []
|
|
1247
|
-
};
|
|
1248
|
-
for (const a of anchors) {
|
|
1249
|
-
const q = classifyAnchor(a, now);
|
|
1250
|
-
if (q === "Today") groups.today.push(a);
|
|
1251
|
-
else if (q === "Upcoming") groups.upcoming.push(a);
|
|
1252
|
-
else if (q === "Habits") groups.habits.push(a);
|
|
1253
|
-
else groups.backlog.push(a);
|
|
1254
|
-
}
|
|
1255
|
-
return groups;
|
|
1256
|
-
}
|
|
1257
|
-
var PRIORITY_RANK = { p0: 0, p1: 1, p2: 2 };
|
|
1258
|
-
function sortAnchorsForDisplay(anchors, cwd, now = Date.now()) {
|
|
1259
|
-
const todayStr = getTodayDateString(now);
|
|
1260
|
-
return [...anchors].sort((a, b) => {
|
|
1261
|
-
if (a.status !== b.status) {
|
|
1262
|
-
return a.status === "active" ? -1 : 1;
|
|
1263
|
-
}
|
|
1264
|
-
if (cwd) {
|
|
1265
|
-
const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
|
|
1266
|
-
const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
|
|
1267
|
-
if (aMatch !== bMatch) return aMatch - bMatch;
|
|
1268
|
-
}
|
|
1269
|
-
const aDue = a.targetDate && a.targetDate <= todayStr ? 0 : 1;
|
|
1270
|
-
const bDue = b.targetDate && b.targetDate <= todayStr ? 0 : 1;
|
|
1271
|
-
if (aDue !== bDue) return aDue - bDue;
|
|
1272
|
-
const pA = PRIORITY_RANK[a.priority] ?? 1;
|
|
1273
|
-
const pB = PRIORITY_RANK[b.priority] ?? 1;
|
|
1274
|
-
if (pA !== pB) return pA - pB;
|
|
1275
|
-
return a.createdAt - b.createdAt;
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
566
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
1278
567
|
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
1279
568
|
function stripAnsi(str) {
|
|
1280
569
|
return str.replace(ANSI_REGEX, "");
|
|
@@ -1326,14 +615,12 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
|
1326
615
|
const charLen = codePoint > 65535 ? 2 : 1;
|
|
1327
616
|
const fullChar = singleLine.slice(i, i + charLen);
|
|
1328
617
|
const charWidth = getDisplayWidth(fullChar);
|
|
1329
|
-
if (accumWidth + charWidth > target)
|
|
1330
|
-
break;
|
|
1331
|
-
}
|
|
618
|
+
if (accumWidth + charWidth > target) break;
|
|
1332
619
|
accumulated += fullChar;
|
|
1333
620
|
accumWidth += charWidth;
|
|
1334
621
|
i += charLen;
|
|
1335
622
|
}
|
|
1336
|
-
accumulated = accumulated.replace(/\s*[\(\[\{(【]
|
|
623
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【][^)\]})】]*$/, "");
|
|
1337
624
|
return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
|
|
1338
625
|
}
|
|
1339
626
|
function padToWidth(str, targetWidth) {
|
|
@@ -1342,84 +629,59 @@ function padToWidth(str, targetWidth) {
|
|
|
1342
629
|
if (current >= targetWidth) return truncated;
|
|
1343
630
|
return truncated + " ".repeat(targetWidth - current);
|
|
1344
631
|
}
|
|
1345
|
-
function
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
if (
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
return "Today";
|
|
1355
|
-
}
|
|
1356
|
-
const MS_DAY = 24 * 60 * 60 * 1e3;
|
|
1357
|
-
const tomorrowStr = getTodayDateString(now + MS_DAY);
|
|
1358
|
-
if (anchor.targetDate === tomorrowStr) {
|
|
1359
|
-
return "Tomorrow";
|
|
1360
|
-
}
|
|
1361
|
-
const in2dStr = getTodayDateString(now + 2 * MS_DAY);
|
|
1362
|
-
if (anchor.targetDate === in2dStr) {
|
|
1363
|
-
return "In 2d";
|
|
1364
|
-
}
|
|
1365
|
-
const in3dStr = getTodayDateString(now + 3 * MS_DAY);
|
|
1366
|
-
if (anchor.targetDate === in3dStr) {
|
|
1367
|
-
return "In 3d";
|
|
1368
|
-
}
|
|
1369
|
-
if (anchor.targetDate < todayStr) {
|
|
1370
|
-
return "Overdue";
|
|
1371
|
-
}
|
|
1372
|
-
const parts = anchor.targetDate.split("-");
|
|
1373
|
-
if (parts.length === 3) {
|
|
1374
|
-
return `${parts[1]}-${parts[2]}`;
|
|
1375
|
-
}
|
|
1376
|
-
return anchor.targetDate;
|
|
632
|
+
function formatRelativeAge(timestamp, now = Date.now()) {
|
|
633
|
+
const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
|
|
634
|
+
if (diffSec < 60) return "just now";
|
|
635
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
636
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
637
|
+
const diffHours = Math.floor(diffMin / 60);
|
|
638
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
639
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
640
|
+
return `${diffDays}d ago`;
|
|
1377
641
|
}
|
|
1378
|
-
function
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const timeStr = `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
1383
|
-
const todayStr = getTodayDateString(now);
|
|
1384
|
-
const createdDayStr = getTodayDateString(timestamp);
|
|
1385
|
-
if (createdDayStr === todayStr) {
|
|
1386
|
-
return `Today ${timeStr}`;
|
|
642
|
+
function formatOrigin(anchor) {
|
|
643
|
+
if (typeof anchor === "string") {
|
|
644
|
+
if (!anchor) return "global";
|
|
645
|
+
return import_node_path2.default.basename(anchor.replace(/\\/g, "/")) || "global";
|
|
1387
646
|
}
|
|
1388
|
-
|
|
1389
|
-
if (
|
|
1390
|
-
|
|
647
|
+
let proj = "global";
|
|
648
|
+
if (anchor.project && anchor.project !== "global") {
|
|
649
|
+
const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
650
|
+
if (normProj.includes("/")) {
|
|
651
|
+
const parts = normProj.split("/").filter(Boolean);
|
|
652
|
+
proj = parts.slice(-2).join("/");
|
|
653
|
+
} else {
|
|
654
|
+
proj = normProj;
|
|
655
|
+
}
|
|
656
|
+
} else if (anchor.cwd) {
|
|
657
|
+
proj = import_node_path2.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
|
|
1391
658
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
659
|
+
if (anchor.files && anchor.files.length > 0) {
|
|
660
|
+
const shortFile = import_node_path2.default.basename(anchor.files[0].replace(/\\/g, "/"));
|
|
661
|
+
if (shortFile && !proj.endsWith(shortFile)) {
|
|
662
|
+
return `${proj} \xB7 ${shortFile}`;
|
|
663
|
+
}
|
|
1395
664
|
}
|
|
1396
|
-
return
|
|
665
|
+
return proj;
|
|
1397
666
|
}
|
|
1398
|
-
function
|
|
1399
|
-
|
|
1400
|
-
if (
|
|
1401
|
-
|
|
1402
|
-
if (normProj.includes("/")) {
|
|
1403
|
-
const parts = normProj.split("/").filter(Boolean);
|
|
1404
|
-
return parts.slice(-2).join("/");
|
|
1405
|
-
}
|
|
1406
|
-
return normProj;
|
|
667
|
+
function sortAnchorsForDisplay(anchors, cwd) {
|
|
668
|
+
return [...anchors].sort((a, b) => {
|
|
669
|
+
if (a.status !== b.status) {
|
|
670
|
+
return a.status === "active" ? -1 : 1;
|
|
1407
671
|
}
|
|
1408
|
-
if (
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
672
|
+
if (cwd) {
|
|
673
|
+
const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
|
|
674
|
+
const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
|
|
675
|
+
if (aMatch !== bMatch) return aMatch - bMatch;
|
|
676
|
+
}
|
|
677
|
+
return b.lastTouchedAt - a.lastTouchedAt;
|
|
678
|
+
});
|
|
1413
679
|
}
|
|
1414
680
|
function updateAnchorStatusBar(ctx, store) {
|
|
1415
681
|
if (!hasActiveUI(ctx)) return;
|
|
1416
682
|
try {
|
|
1417
683
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1418
|
-
|
|
1419
|
-
ctx.ui.setStatus("anchor", void 0);
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
684
|
+
ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
|
|
1423
685
|
} catch {
|
|
1424
686
|
}
|
|
1425
687
|
}
|
|
@@ -1431,26 +693,30 @@ function updateStartupBanner(ctx, store) {
|
|
|
1431
693
|
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1432
694
|
return;
|
|
1433
695
|
}
|
|
1434
|
-
const
|
|
1435
|
-
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
696
|
+
const sorted = sortAnchorsForDisplay(active, ctx.cwd);
|
|
1436
697
|
const topItems = sorted.slice(0, 3);
|
|
1437
698
|
const factory = (_tui, theme) => ({
|
|
1438
699
|
render: (width) => {
|
|
1439
|
-
const maxTitleWidth = Math.max(
|
|
700
|
+
const maxTitleWidth = Math.max(14, width - 34);
|
|
1440
701
|
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
702
|
+
const label = active.length === 1 ? "1 note" : `${active.length} notes`;
|
|
1441
703
|
const lines = [
|
|
1442
|
-
truncateToWidth(fg("accent", `\u2316
|
|
704
|
+
truncateToWidth(fg("accent", `\u2316 ${label}`), width)
|
|
1443
705
|
];
|
|
1444
|
-
for (
|
|
1445
|
-
const a = topItems[i];
|
|
706
|
+
for (const a of topItems) {
|
|
1446
707
|
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1447
|
-
const
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1450
|
-
lines.push(
|
|
708
|
+
const origin = formatOrigin(a);
|
|
709
|
+
const age = formatRelativeAge(a.createdAt);
|
|
710
|
+
const titleClipped = truncateToWidth(a.title, maxTitleWidth);
|
|
711
|
+
lines.push(
|
|
712
|
+
truncateToWidth(
|
|
713
|
+
` ${fg("dim", num)} ${titleClipped} ${fg("muted", `\xB7 ${origin}`)} ${fg("dim", `(${age})`)}`,
|
|
714
|
+
width
|
|
715
|
+
)
|
|
716
|
+
);
|
|
1451
717
|
}
|
|
1452
718
|
if (sorted.length > 3) {
|
|
1453
|
-
lines.push(truncateToWidth(fg("dim", `
|
|
719
|
+
lines.push(truncateToWidth(fg("dim", ` +${sorted.length - 3} more \xB7 /anchor`), width));
|
|
1454
720
|
}
|
|
1455
721
|
return lines;
|
|
1456
722
|
},
|
|
@@ -1464,77 +730,62 @@ function updateStartupBanner(ctx, store) {
|
|
|
1464
730
|
}
|
|
1465
731
|
async function openAnchorDashboard(ctx, store) {
|
|
1466
732
|
if (!hasActiveUI(ctx)) return;
|
|
1467
|
-
const
|
|
1468
|
-
if (
|
|
1469
|
-
ctx.ui.notify("\u2316 No
|
|
733
|
+
const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
734
|
+
if (allNotes.length === 0) {
|
|
735
|
+
ctx.ui.notify("\u2316 No sticky notes. Use /pin <note> to stick one.", "info");
|
|
1470
736
|
return;
|
|
1471
737
|
}
|
|
1472
|
-
const
|
|
738
|
+
const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
|
|
1473
739
|
const optionMap = /* @__PURE__ */ new Map();
|
|
1474
740
|
const displayOptions = [];
|
|
1475
741
|
const termCols = process.stdout?.columns || 96;
|
|
1476
|
-
const isNarrow = termCols <
|
|
1477
|
-
const titleWidth = isNarrow ? Math.max(
|
|
1478
|
-
for (
|
|
1479
|
-
const a = sortedList[i];
|
|
742
|
+
const isNarrow = termCols < 84;
|
|
743
|
+
const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
|
|
744
|
+
for (const a of sorted) {
|
|
1480
745
|
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1481
|
-
const badge = a.status === "sleeping" ? padToWidth("[Quiet]", 10) : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
|
|
1482
746
|
const origin = formatOrigin(a);
|
|
1483
|
-
const
|
|
1484
|
-
const created = formatCreationTime(a.createdAt);
|
|
1485
|
-
const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
|
|
747
|
+
const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
|
|
1486
748
|
const colNum = `${num} `;
|
|
1487
|
-
const colTitle = padToWidth(
|
|
1488
|
-
const colOrigin = padToWidth(origin,
|
|
1489
|
-
const
|
|
1490
|
-
const colCreated = created;
|
|
1491
|
-
const label = isNarrow ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
749
|
+
const colTitle = padToWidth(a.title, titleWidth);
|
|
750
|
+
const colOrigin = padToWidth(origin, 22);
|
|
751
|
+
const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
|
|
1492
752
|
optionMap.set(label, a);
|
|
1493
753
|
displayOptions.push(label);
|
|
1494
754
|
}
|
|
1495
|
-
const selected = await ctx.ui.select("\u2316
|
|
755
|
+
const selected = await ctx.ui.select("\u2316 Notes (select a note):", displayOptions);
|
|
1496
756
|
if (!selected) return;
|
|
1497
757
|
const anchor = optionMap.get(selected);
|
|
1498
758
|
if (!anchor) return;
|
|
1499
759
|
const actions = [
|
|
1500
|
-
"\u2713
|
|
1501
|
-
...anchor.status === "sleeping" ? ["\u26A1 Wake up (
|
|
1502
|
-
"\u2139
|
|
1503
|
-
"\u21A9
|
|
760
|
+
"\u2713 Done (tear off note)",
|
|
761
|
+
...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
|
|
762
|
+
"\u2139 Read full note",
|
|
763
|
+
"\u21A9 Back"
|
|
1504
764
|
];
|
|
1505
|
-
const action = await ctx.ui.select(
|
|
1506
|
-
`\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
|
|
1507
|
-
actions
|
|
1508
|
-
);
|
|
765
|
+
const action = await ctx.ui.select(`\u2316 #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, actions);
|
|
1509
766
|
if (!action || action.startsWith("\u21A9")) return;
|
|
1510
767
|
if (action.startsWith("\u2713")) {
|
|
1511
|
-
store.settle(anchor.id, { settledBy: "manual
|
|
1512
|
-
|
|
1513
|
-
ctx.ui.notify(successMsg, "info");
|
|
768
|
+
store.settle(anchor.id, { settledBy: "manual" });
|
|
769
|
+
ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
|
|
1514
770
|
updateAnchorStatusBar(ctx, store);
|
|
1515
771
|
} else if (action.startsWith("\u26A1")) {
|
|
1516
772
|
store.touch(anchor.id);
|
|
1517
|
-
ctx.ui.notify(`\u2316 Woke up #${anchor.id}: "${anchor.title}"`, "info");
|
|
773
|
+
ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
|
|
1518
774
|
updateAnchorStatusBar(ctx, store);
|
|
1519
775
|
} else if (action.startsWith("\u2139")) {
|
|
1520
776
|
const details = [
|
|
1521
|
-
|
|
1522
|
-
anchor.description ? `
|
|
1523
|
-
anchor.
|
|
1524
|
-
|
|
1525
|
-
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
1526
|
-
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
|
|
777
|
+
`\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
|
|
778
|
+
anchor.description ? `Details: ${anchor.description}` : null,
|
|
779
|
+
anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
780
|
+
`Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
|
|
1527
781
|
].filter(Boolean).join("\n");
|
|
1528
782
|
ctx.ui.notify(details, "info");
|
|
1529
783
|
}
|
|
1530
784
|
}
|
|
1531
785
|
|
|
1532
786
|
// src/extension.ts
|
|
1533
|
-
var
|
|
1534
|
-
|
|
1535
|
-
var import_node_path5 = __toESM(require("path"), 1);
|
|
1536
|
-
var MUTATION_TOOLS2 = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
1537
|
-
function parseAnchorCommandInput(raw, now = Date.now()) {
|
|
787
|
+
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
788
|
+
function parseAnchorCommandInput(raw) {
|
|
1538
789
|
const trimmed = (raw || "").trim();
|
|
1539
790
|
if (!trimmed || trimmed === "list" || trimmed === "ls") {
|
|
1540
791
|
return { mode: "dashboard" };
|
|
@@ -1549,19 +800,9 @@ function parseAnchorCommandInput(raw, now = Date.now()) {
|
|
|
1549
800
|
targetId: settleMatch[1].trim().replace(/^#/, "")
|
|
1550
801
|
};
|
|
1551
802
|
}
|
|
1552
|
-
let targetDate;
|
|
1553
|
-
const tokens = trimmed.split(/\s+/);
|
|
1554
|
-
const dueIdx = tokens.findIndex((t) => t === "--due" || t === "-d");
|
|
1555
|
-
if (dueIdx !== -1 && tokens[dueIdx + 1]) {
|
|
1556
|
-
const rawDue = tokens[dueIdx + 1];
|
|
1557
|
-
targetDate = detectTargetDate(rawDue, now) || rawDue;
|
|
1558
|
-
tokens.splice(dueIdx, 2);
|
|
1559
|
-
}
|
|
1560
|
-
const cleanTitle = tokens.join(" ").trim();
|
|
1561
803
|
return {
|
|
1562
804
|
mode: "pin",
|
|
1563
|
-
title:
|
|
1564
|
-
targetDate
|
|
805
|
+
title: trimmed
|
|
1565
806
|
};
|
|
1566
807
|
}
|
|
1567
808
|
function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
@@ -1574,58 +815,17 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
|
1574
815
|
return sorted[seqIndex - 1].id;
|
|
1575
816
|
}
|
|
1576
817
|
}
|
|
1577
|
-
|
|
1578
|
-
return direct?.id;
|
|
818
|
+
return store.get(clean)?.id;
|
|
1579
819
|
}
|
|
1580
820
|
function extension_default(pi) {
|
|
1581
821
|
const store = new AnchorStore();
|
|
1582
|
-
const observer = new SessionTouchObserver();
|
|
1583
822
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
1584
|
-
const preflightFailures = /* @__PURE__ */ new Map();
|
|
1585
823
|
pi.on("session_start", async (event, ctx) => {
|
|
1586
|
-
observer.clear();
|
|
1587
824
|
annotatedThisSession.clear();
|
|
1588
|
-
|
|
1589
|
-
const
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
store.touch(sa.id);
|
|
1593
|
-
}
|
|
1594
|
-
if (sweep.transitionedToSleeping.length > 0 && sleepingForWorkspace.length === 0) {
|
|
1595
|
-
ctx.ui.notify(`\u2316 Anchor: ${sweep.transitionedToSleeping.length} inactive task(s) moved to quiet mode (/anchor to view)`, "info");
|
|
1596
|
-
}
|
|
1597
|
-
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1598
|
-
for (const a of activeAnchors) {
|
|
1599
|
-
if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
|
|
1600
|
-
try {
|
|
1601
|
-
const res = runPhysicalVerification(a, ctx.cwd);
|
|
1602
|
-
if (res.success) {
|
|
1603
|
-
store.settle(a.id, {
|
|
1604
|
-
settledBy: "verification-test",
|
|
1605
|
-
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
1606
|
-
});
|
|
1607
|
-
ctx.ui.notify(`\u2316 Verification passed! Auto-settled #${a.id}: "${a.title}"`, "info");
|
|
1608
|
-
} else {
|
|
1609
|
-
preflightFailures.set(a.id, res.output.slice(-250));
|
|
1610
|
-
}
|
|
1611
|
-
} catch (_) {
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
}
|
|
1615
|
-
try {
|
|
1616
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1617
|
-
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1618
|
-
if (import_node_fs3.default.existsSync(pendingPath)) {
|
|
1619
|
-
const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
|
|
1620
|
-
const isExpired = Date.now() - pending.timestamp >= 24 * 60 * 60 * 1e3;
|
|
1621
|
-
if (!isExpired && isSameOrSubPath(pending.cwd, ctx.cwd)) {
|
|
1622
|
-
ctx.ui.notify(`\u2316 [Exit Guard] Detected ${pending.modifiedCount} uncommitted changes from previous session exit. Run /pin to record.`, "info");
|
|
1623
|
-
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1624
|
-
} else if (isExpired) {
|
|
1625
|
-
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
} catch (_) {
|
|
825
|
+
sweepQuietNotes(store);
|
|
826
|
+
const quietForHere = store.list({ status: "sleeping", all: true }).filter((a) => (a.files.length > 0 || a.project && a.project !== "global" || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
|
|
827
|
+
for (const q of quietForHere) {
|
|
828
|
+
store.touch(q.id);
|
|
1629
829
|
}
|
|
1630
830
|
updateAnchorStatusBar(ctx, store);
|
|
1631
831
|
if (event.reason !== "resume") {
|
|
@@ -1648,32 +848,32 @@ function extension_default(pi) {
|
|
|
1648
848
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1649
849
|
const entries = ctx.sessionManager?.getEntries() || [];
|
|
1650
850
|
const messageTurns = entries.filter((e) => e.type === "message");
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
return {
|
|
1656
|
-
systemPrompt: `${event.systemPrompt}
|
|
851
|
+
if (messageTurns.length <= 1) {
|
|
852
|
+
const snippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
853
|
+
if (snippet) {
|
|
854
|
+
return { systemPrompt: `${event.systemPrompt}
|
|
1657
855
|
|
|
1658
|
-
${
|
|
1659
|
-
};
|
|
856
|
+
${snippet}` };
|
|
1660
857
|
}
|
|
1661
858
|
}
|
|
1662
859
|
});
|
|
1663
860
|
pi.on("tool_result", async (event, ctx) => {
|
|
861
|
+
if (event.isError) return;
|
|
1664
862
|
const lowerTool = String(event.toolName || "").toLowerCase();
|
|
1665
|
-
if (
|
|
1666
|
-
|
|
1667
|
-
if (
|
|
1668
|
-
const
|
|
1669
|
-
if (
|
|
1670
|
-
const
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
if (
|
|
1674
|
-
|
|
863
|
+
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
864
|
+
const cmd = String(event.input?.command || "");
|
|
865
|
+
if (cmd.includes("git commit")) {
|
|
866
|
+
const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
|
|
867
|
+
if (msgMatch && msgMatch[1]) {
|
|
868
|
+
const commitMsg = msgMatch[1];
|
|
869
|
+
const candidates = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
870
|
+
for (const a of candidates) {
|
|
871
|
+
if (matchesCommitMessage(a, [commitMsg]).matched && isAnchorTargetingWorkspace(a, ctx.cwd)) {
|
|
872
|
+
store.settle(a.id, { settledBy: "commit", summary: commitMsg });
|
|
873
|
+
updateAnchorStatusBar(ctx, store);
|
|
874
|
+
if (hasActiveUI(ctx)) {
|
|
1675
875
|
try {
|
|
1676
|
-
ctx.ui.notify(`\u2316 Auto-
|
|
876
|
+
ctx.ui.notify(`\u2316 Auto-torn off #${a.id.replace(/^anc-/, "")} ("${a.title}") on git commit`, "info");
|
|
1677
877
|
} catch {
|
|
1678
878
|
}
|
|
1679
879
|
}
|
|
@@ -1685,132 +885,38 @@ ${contextSnippet}`
|
|
|
1685
885
|
const pathInput = event.input?.path;
|
|
1686
886
|
if (typeof pathInput !== "string") return;
|
|
1687
887
|
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
1688
|
-
const
|
|
1689
|
-
const matches = findMatchedAnchors(
|
|
1690
|
-
if (matches.length
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
store.touch(m.anchor.id);
|
|
1695
|
-
}
|
|
1696
|
-
updateAnchorStatusBar(ctx, store);
|
|
1697
|
-
}
|
|
1698
|
-
const a = matches[0].anchor;
|
|
1699
|
-
if (!annotatedThisSession.has(a.id)) {
|
|
1700
|
-
annotatedThisSession.add(a.id);
|
|
1701
|
-
const alert = makeSafeTaskAnnotation(touchedPath, a);
|
|
1702
|
-
if (alert) {
|
|
1703
|
-
const contents = [...event.content || []];
|
|
1704
|
-
for (let i = contents.length - 1; i >= 0; i--) {
|
|
1705
|
-
const item = contents[i];
|
|
1706
|
-
if (item && item.type === "text") {
|
|
1707
|
-
contents[i] = { ...item, text: item.text + alert };
|
|
1708
|
-
return { content: contents };
|
|
1709
|
-
}
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
});
|
|
1715
|
-
pi.on("session_shutdown", async (event, ctx) => {
|
|
1716
|
-
let gitStatus = "";
|
|
1717
|
-
try {
|
|
1718
|
-
gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
1719
|
-
cwd: ctx.cwd,
|
|
1720
|
-
encoding: "utf-8",
|
|
1721
|
-
timeout: 2e3,
|
|
1722
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1723
|
-
}).replace(/[\r\n]+$/, "");
|
|
1724
|
-
const changed = gitStatus.split(/\r?\n/).filter(Boolean).map((l) => l.slice(3).replace(/^"(.*)"$/, "$1").trim()).filter(Boolean);
|
|
1725
|
-
for (const f of changed) {
|
|
1726
|
-
observer.addTouchedFile(f);
|
|
1727
|
-
}
|
|
1728
|
-
} catch {
|
|
1729
|
-
}
|
|
1730
|
-
if (event?.reason === "quit" && gitStatus) {
|
|
1731
|
-
try {
|
|
1732
|
-
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1733
|
-
if (activeAnchors.length === 0) {
|
|
1734
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1735
|
-
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1736
|
-
const data = {
|
|
1737
|
-
cwd: ctx.cwd,
|
|
1738
|
-
timestamp: Date.now(),
|
|
1739
|
-
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
1740
|
-
};
|
|
1741
|
-
import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1742
|
-
}
|
|
1743
|
-
} catch {
|
|
1744
|
-
}
|
|
888
|
+
const notes = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
889
|
+
const matches = findMatchedAnchors(notes, [touchedPath]);
|
|
890
|
+
if (matches.length === 0) return;
|
|
891
|
+
if (MUTATION_TOOLS.has(lowerTool)) {
|
|
892
|
+
for (const m of matches) store.touch(m.anchor.id);
|
|
893
|
+
updateAnchorStatusBar(ctx, store);
|
|
1745
894
|
}
|
|
1746
|
-
const
|
|
1747
|
-
if (
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
touchedFiles: prop.matchedFiles
|
|
1758
|
-
});
|
|
1759
|
-
if (hasActiveUI(ctx)) {
|
|
1760
|
-
try {
|
|
1761
|
-
ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
|
|
1762
|
-
} catch {
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
continue;
|
|
1766
|
-
}
|
|
1767
|
-
if (a.verifyCommand) {
|
|
1768
|
-
const verifyRes = runPhysicalVerification(a, ctx.cwd);
|
|
1769
|
-
if (verifyRes.success) {
|
|
1770
|
-
store.settle(a.id, {
|
|
1771
|
-
settledBy: "verification-test",
|
|
1772
|
-
summary: `Automated test passed: ${a.verifyCommand}`,
|
|
1773
|
-
touchedFiles: prop.matchedFiles
|
|
1774
|
-
});
|
|
1775
|
-
if (hasActiveUI(ctx)) {
|
|
1776
|
-
try {
|
|
1777
|
-
ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
|
|
1778
|
-
} catch {
|
|
1779
|
-
}
|
|
895
|
+
const targetNote = matches[0].anchor;
|
|
896
|
+
if (!annotatedThisSession.has(targetNote.id)) {
|
|
897
|
+
annotatedThisSession.add(targetNote.id);
|
|
898
|
+
const whisper = makeSafeTaskAnnotation(touchedPath, targetNote);
|
|
899
|
+
if (whisper) {
|
|
900
|
+
const contents = [...event.content || []];
|
|
901
|
+
for (let i = contents.length - 1; i >= 0; i--) {
|
|
902
|
+
const item = contents[i];
|
|
903
|
+
if (item && item.type === "text") {
|
|
904
|
+
contents[i] = { ...item, text: item.text + whisper };
|
|
905
|
+
return { content: contents };
|
|
1780
906
|
}
|
|
1781
|
-
continue;
|
|
1782
907
|
}
|
|
1783
908
|
}
|
|
1784
|
-
if (!hasActiveUI(ctx)) continue;
|
|
1785
|
-
try {
|
|
1786
|
-
const ok = await ctx.ui.confirm(
|
|
1787
|
-
"\u2316 Settle Anchor Task",
|
|
1788
|
-
`Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
|
|
1789
|
-
Mark as completed and archive?`
|
|
1790
|
-
);
|
|
1791
|
-
if (ok) {
|
|
1792
|
-
store.settle(a.id, {
|
|
1793
|
-
settledBy: "one-tap-settlement",
|
|
1794
|
-
touchedFiles: prop.matchedFiles
|
|
1795
|
-
});
|
|
1796
|
-
ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
|
|
1797
|
-
}
|
|
1798
|
-
} catch {
|
|
1799
|
-
}
|
|
1800
909
|
}
|
|
1801
910
|
});
|
|
1802
911
|
pi.registerTool({
|
|
1803
912
|
name: "anchor",
|
|
1804
|
-
label: "Anchor (
|
|
1805
|
-
description: "
|
|
1806
|
-
promptSnippet: "
|
|
913
|
+
label: "Anchor (Sticky Note)",
|
|
914
|
+
description: "Pin, list, or tear off cross-session sticky notes stored in ~/.anchor/ (zero git repo clutter). Actions: pin (stick a new note), list (view notes), settle (tear off completed note).",
|
|
915
|
+
promptSnippet: "Pin or tear off cross-session sticky notes that survive terminal restarts",
|
|
1807
916
|
promptGuidelines: [
|
|
1808
|
-
'Use `anchor` when the user asks to
|
|
1809
|
-
"BOUNDARY WITH TODO: `todo` is
|
|
1810
|
-
|
|
1811
|
-
"Tasks are automatically scoped to the current project context without cluttering the project git repository.",
|
|
1812
|
-
'When code for an anchor is completed and verified, call `anchor` with action "settle" to archive it and free context.',
|
|
1813
|
-
"Active anchors are automatically injected into future sessions in an ultra-compact block."
|
|
917
|
+
'Use `anchor` when the user asks to remember something for later/tomorrow or across sessions (e.g. "\u8BB0\u4E00\u4E0B\u660E\u5929\u91CD\u6784\u9274\u6743", "\u665A\u4E0A\u6E05\u7406\u7F13\u5B58").',
|
|
918
|
+
"BOUNDARY WITH TODO: `todo` is for multi-step work right now in this session; `anchor` is a sticky note for future sessions.",
|
|
919
|
+
'When a pinned note is completed, call `anchor` with action "settle" to tear it off.'
|
|
1814
920
|
],
|
|
1815
921
|
parameters: import_typebox.Type.Object({
|
|
1816
922
|
action: import_typebox.Type.Union([
|
|
@@ -1820,29 +926,19 @@ Mark as completed and archive?`
|
|
|
1820
926
|
import_typebox.Type.Literal("touch"),
|
|
1821
927
|
import_typebox.Type.Literal("sweep")
|
|
1822
928
|
]),
|
|
1823
|
-
title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "
|
|
1824
|
-
description: import_typebox.Type.Optional(import_typebox.Type.String({ description: "
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
files: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Associated file paths or directory prefixes" })),
|
|
1829
|
-
tags: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Domain tags" })),
|
|
1830
|
-
verifyCommand: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional shell command for automated physical verification" })),
|
|
1831
|
-
id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Anchor ID, e.g. anc-1 (for settle or touch)" }))
|
|
929
|
+
title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Sticky note text (for pin)" })),
|
|
930
|
+
description: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional extra details" })),
|
|
931
|
+
project: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional project name or folder" })),
|
|
932
|
+
files: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Optional associated file paths" })),
|
|
933
|
+
id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Note ID, e.g. anc-1 or 1 (for settle)" }))
|
|
1832
934
|
}),
|
|
1833
935
|
renderCall(args, theme) {
|
|
1834
936
|
const action = args.action || "list";
|
|
1835
937
|
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
1836
938
|
if (action === "pin" && args.title) {
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
title += `
|
|
1840
|
-
} else if (action === "settle" && args.id) {
|
|
1841
|
-
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
1842
|
-
} else if (action === "touch" && args.id) {
|
|
1843
|
-
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
1844
|
-
} else if (action === "list") {
|
|
1845
|
-
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
939
|
+
title += ` "${args.title}"`;
|
|
940
|
+
} else if ((action === "settle" || action === "touch") && args.id) {
|
|
941
|
+
title += ` ${theme.fg("dim", `#${String(args.id).replace(/^anc-/, "")}`)}`;
|
|
1846
942
|
}
|
|
1847
943
|
return {
|
|
1848
944
|
render: (width) => [truncateToWidth(title, width)],
|
|
@@ -1851,17 +947,11 @@ Mark as completed and archive?`
|
|
|
1851
947
|
};
|
|
1852
948
|
},
|
|
1853
949
|
renderResult(result, _options, theme) {
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
|
|
1858
|
-
invalidate: () => {
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
const text = result.content?.[0]?.text || "OK";
|
|
950
|
+
const text = result.content?.[0]?.text || (result.isError ? "Failed" : "OK");
|
|
951
|
+
const color = result.isError ? "error" : "success";
|
|
952
|
+
const icon = result.isError ? "\u2717" : "\u2714";
|
|
1863
953
|
return {
|
|
1864
|
-
render: (width) => [truncateToWidth(theme.fg(
|
|
954
|
+
render: (width) => [truncateToWidth(theme.fg(color, `${icon} ${text}`), width)],
|
|
1865
955
|
invalidate: () => {
|
|
1866
956
|
}
|
|
1867
957
|
};
|
|
@@ -1870,91 +960,63 @@ Mark as completed and archive?`
|
|
|
1870
960
|
if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
|
|
1871
961
|
if (params.action === "pin") {
|
|
1872
962
|
if (!params.title) {
|
|
1873
|
-
return { content: [{ type: "text", text: "Error: title is required for pin
|
|
963
|
+
return { content: [{ type: "text", text: "Error: title is required for pin" }], isError: true };
|
|
1874
964
|
}
|
|
1875
965
|
const anc = store.create({
|
|
1876
966
|
title: params.title,
|
|
1877
967
|
description: params.description,
|
|
1878
|
-
priority: params.priority || "p1",
|
|
1879
968
|
project: params.project,
|
|
1880
|
-
targetDate: params.targetDate,
|
|
1881
969
|
cwd: ctx.cwd,
|
|
1882
|
-
files: params.files || []
|
|
1883
|
-
tags: params.tags || [],
|
|
1884
|
-
verifyCommand: params.verifyCommand
|
|
970
|
+
files: params.files || []
|
|
1885
971
|
});
|
|
1886
972
|
updateAnchorStatusBar(ctx, store);
|
|
1887
973
|
return {
|
|
1888
|
-
content: [{
|
|
1889
|
-
type: "text",
|
|
1890
|
-
text: `Successfully anchored task #${anc.id}: "${anc.title}" [${anc.project}]. Stored in ~/.anchor/.`
|
|
1891
|
-
}],
|
|
974
|
+
content: [{ type: "text", text: `Pinned note #${anc.id.replace(/^anc-/, "")}: "${anc.title}"` }],
|
|
1892
975
|
isError: false
|
|
1893
976
|
};
|
|
1894
977
|
}
|
|
1895
978
|
if (params.action === "list") {
|
|
1896
|
-
const
|
|
1897
|
-
const sleeping = store.list({ status: "sleeping", cwd: ctx.cwd });
|
|
979
|
+
const notes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1898
980
|
return {
|
|
1899
|
-
content: [{
|
|
1900
|
-
type: "text",
|
|
1901
|
-
text: JSON.stringify({ active, sleeping }, null, 2)
|
|
1902
|
-
}],
|
|
981
|
+
content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
|
|
1903
982
|
isError: false
|
|
1904
983
|
};
|
|
1905
984
|
}
|
|
1906
985
|
if (params.action === "settle") {
|
|
1907
986
|
if (!params.id) {
|
|
1908
|
-
return { content: [{ type: "text", text: "Error: id is required for settle
|
|
987
|
+
return { content: [{ type: "text", text: "Error: id is required for settle" }], isError: true };
|
|
1909
988
|
}
|
|
1910
989
|
try {
|
|
1911
|
-
const
|
|
990
|
+
const targetId = resolveAnchorTarget(store, String(params.id), ctx.cwd) || String(params.id);
|
|
991
|
+
const settled = store.settle(targetId, { settledBy: "manual" });
|
|
1912
992
|
updateAnchorStatusBar(ctx, store);
|
|
1913
993
|
return {
|
|
1914
|
-
content: [{
|
|
1915
|
-
type: "text",
|
|
1916
|
-
text: `Anchor #${settled.id} successfully settled and evicted from active context.`
|
|
1917
|
-
}],
|
|
994
|
+
content: [{ type: "text", text: `Torn off note #${settled.id.replace(/^anc-/, "")}: "${settled.title}"` }],
|
|
1918
995
|
isError: false
|
|
1919
996
|
};
|
|
1920
997
|
} catch (err) {
|
|
1921
998
|
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1922
999
|
}
|
|
1923
1000
|
}
|
|
1924
|
-
if (params.action === "touch") {
|
|
1925
|
-
if (!params.id) {
|
|
1926
|
-
return { content: [{ type: "text", text: "Error: id is required for touch action" }], isError: true };
|
|
1927
|
-
}
|
|
1001
|
+
if (params.action === "touch" && params.id) {
|
|
1928
1002
|
try {
|
|
1929
|
-
const touched = store.touch(params.id);
|
|
1003
|
+
const touched = store.touch(String(params.id));
|
|
1930
1004
|
updateAnchorStatusBar(ctx, store);
|
|
1931
|
-
return {
|
|
1932
|
-
content: [{
|
|
1933
|
-
type: "text",
|
|
1934
|
-
text: `Anchor #${touched.id} touched and decay timer refreshed.`
|
|
1935
|
-
}],
|
|
1936
|
-
isError: false
|
|
1937
|
-
};
|
|
1005
|
+
return { content: [{ type: "text", text: `Refreshed note #${touched.id}` }], isError: false };
|
|
1938
1006
|
} catch (err) {
|
|
1939
1007
|
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1940
1008
|
}
|
|
1941
1009
|
}
|
|
1942
1010
|
if (params.action === "sweep") {
|
|
1943
|
-
const res =
|
|
1011
|
+
const res = sweepQuietNotes(store);
|
|
1944
1012
|
updateAnchorStatusBar(ctx, store);
|
|
1945
|
-
return {
|
|
1946
|
-
content: [{
|
|
1947
|
-
type: "text",
|
|
1948
|
-
text: `Sweep complete: ${res.transitionedToSleeping.length} sleeping, ${res.evictedToGraveyard.length} evicted to graveyard.`
|
|
1949
|
-
}],
|
|
1950
|
-
isError: false
|
|
1951
|
-
};
|
|
1013
|
+
return { content: [{ type: "text", text: `Quieted ${res.quieted.length} old note(s).` }], isError: false };
|
|
1952
1014
|
}
|
|
1953
1015
|
return { content: [{ type: "text", text: "Unknown action" }], isError: true };
|
|
1954
1016
|
}
|
|
1955
1017
|
});
|
|
1956
1018
|
pi.registerCommand("anchor", {
|
|
1957
|
-
description: "
|
|
1019
|
+
description: "View, pin, or tear off sticky notes (/anchor, /anchor done 1, /anchor undo)",
|
|
1958
1020
|
handler: async (args, ctx) => {
|
|
1959
1021
|
const parsed = parseAnchorCommandInput(args);
|
|
1960
1022
|
if (parsed.mode === "dashboard") {
|
|
@@ -1964,21 +1026,21 @@ Mark as completed and archive?`
|
|
|
1964
1026
|
if (parsed.mode === "undo") {
|
|
1965
1027
|
try {
|
|
1966
1028
|
const restored = store.undoSettle();
|
|
1967
|
-
ctx.ui.notify(`\u2316
|
|
1029
|
+
ctx.ui.notify(`\u2316 Put back #${restored.id.replace(/^anc-/, "")}: "${restored.title}"`, "info");
|
|
1968
1030
|
updateAnchorStatusBar(ctx, store);
|
|
1969
1031
|
} catch (err) {
|
|
1970
|
-
ctx.ui.notify(`
|
|
1032
|
+
ctx.ui.notify(`Undo failed: ${err.message}`, "error");
|
|
1971
1033
|
}
|
|
1972
1034
|
return;
|
|
1973
1035
|
}
|
|
1974
1036
|
if (parsed.mode === "settle" && parsed.targetId) {
|
|
1975
1037
|
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1976
1038
|
if (!resolvedId) {
|
|
1977
|
-
ctx.ui.notify(`\u2316
|
|
1039
|
+
ctx.ui.notify(`\u2316 Note not found: ${parsed.targetId}`, "error");
|
|
1978
1040
|
return;
|
|
1979
1041
|
}
|
|
1980
|
-
const settled = store.settle(resolvedId, { settledBy: "manual
|
|
1981
|
-
ctx.ui.notify(`\u2316
|
|
1042
|
+
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
1043
|
+
ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
|
|
1982
1044
|
updateAnchorStatusBar(ctx, store);
|
|
1983
1045
|
return;
|
|
1984
1046
|
}
|
|
@@ -1986,40 +1048,32 @@ Mark as completed and archive?`
|
|
|
1986
1048
|
if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
|
|
1987
1049
|
const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
|
|
1988
1050
|
if (resolvedId) {
|
|
1989
|
-
const settled = store.settle(resolvedId, { settledBy: "manual
|
|
1990
|
-
ctx.ui.notify(`\u2316
|
|
1051
|
+
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
1052
|
+
ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
|
|
1991
1053
|
updateAnchorStatusBar(ctx, store);
|
|
1992
1054
|
return;
|
|
1993
1055
|
}
|
|
1994
1056
|
}
|
|
1995
|
-
const anc = store.create({
|
|
1996
|
-
|
|
1997
|
-
targetDate: parsed.targetDate,
|
|
1998
|
-
cwd: ctx.cwd
|
|
1999
|
-
});
|
|
2000
|
-
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
|
|
1057
|
+
const anc = store.create({ title: parsed.title || rawTrimmed, cwd: ctx.cwd });
|
|
1058
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}: "${anc.title}"`, "info");
|
|
2001
1059
|
updateAnchorStatusBar(ctx, store);
|
|
2002
1060
|
}
|
|
2003
1061
|
});
|
|
2004
1062
|
pi.registerCommand("pin", {
|
|
2005
|
-
description: "
|
|
1063
|
+
description: "Stick a quick note across sessions (e.g. /pin Check auth token expiry)",
|
|
2006
1064
|
handler: async (args, ctx) => {
|
|
2007
|
-
const
|
|
2008
|
-
if (
|
|
1065
|
+
const text = (args || "").trim();
|
|
1066
|
+
if (!text) {
|
|
2009
1067
|
await openAnchorDashboard(ctx, store);
|
|
2010
1068
|
return;
|
|
2011
1069
|
}
|
|
2012
|
-
const anc = store.create({
|
|
2013
|
-
|
|
2014
|
-
targetDate: parsed.targetDate,
|
|
2015
|
-
cwd: ctx.cwd
|
|
2016
|
-
});
|
|
2017
|
-
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
|
|
1070
|
+
const anc = store.create({ title: text, cwd: ctx.cwd });
|
|
1071
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}: "${anc.title}"`, "info");
|
|
2018
1072
|
updateAnchorStatusBar(ctx, store);
|
|
2019
1073
|
}
|
|
2020
1074
|
});
|
|
2021
1075
|
pi.registerCommand("done", {
|
|
2022
|
-
description: "
|
|
1076
|
+
description: "Tear off a sticky note by number or ID (e.g. /done 1)",
|
|
2023
1077
|
handler: async (args, ctx) => {
|
|
2024
1078
|
const target = (args || "").trim();
|
|
2025
1079
|
if (!target) {
|
|
@@ -2028,16 +1082,16 @@ Mark as completed and archive?`
|
|
|
2028
1082
|
}
|
|
2029
1083
|
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
2030
1084
|
if (!resolvedId) {
|
|
2031
|
-
ctx.ui.notify(`\u2316
|
|
1085
|
+
ctx.ui.notify(`\u2316 Note not found: ${target}`, "error");
|
|
2032
1086
|
return;
|
|
2033
1087
|
}
|
|
2034
|
-
const settled = store.settle(resolvedId, { settledBy: "manual
|
|
2035
|
-
ctx.ui.notify(`\u2316
|
|
1088
|
+
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
1089
|
+
ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
|
|
2036
1090
|
updateAnchorStatusBar(ctx, store);
|
|
2037
1091
|
}
|
|
2038
1092
|
});
|
|
2039
1093
|
pi.registerCommand("anchors", {
|
|
2040
|
-
description: "
|
|
1094
|
+
description: "View sticky notes",
|
|
2041
1095
|
handler: async (_args, ctx) => {
|
|
2042
1096
|
await openAnchorDashboard(ctx, store);
|
|
2043
1097
|
}
|