pi-anchor 0.3.0 → 0.5.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 +38 -174
- package/README_zh.md +36 -184
- package/assets/hero-en.svg +48 -47
- package/assets/hero-zh.svg +48 -47
- package/bin/anchor.js +68 -70
- package/dist/chunk-VXEQDTK4.js +1310 -0
- package/dist/extension-Cq1NH1jT.d.cts +205 -0
- package/dist/extension-Cq1NH1jT.d.ts +205 -0
- package/dist/extension.cjs +617 -1354
- package/dist/extension.d.cts +2 -20
- package/dist/extension.d.ts +2 -20
- package/dist/extension.js +6 -536
- package/dist/index.cjs +452 -1152
- package/dist/index.d.cts +22 -250
- package/dist/index.d.ts +22 -250
- package/dist/index.js +21 -103
- package/package.json +2 -2
- package/src/extension.ts +265 -376
- package/src/index.ts +1 -6
- package/src/pi-types.d.ts +5 -1
- package/src/store.ts +385 -427
- package/src/tui.ts +166 -307
- package/src/types.ts +35 -63
- 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
|
@@ -32,39 +32,23 @@ var extension_exports = {};
|
|
|
32
32
|
__export(extension_exports, {
|
|
33
33
|
default: () => extension_default,
|
|
34
34
|
parseAnchorCommandInput: () => parseAnchorCommandInput,
|
|
35
|
+
parseQuickNoteInput: () => parseQuickNoteInput,
|
|
35
36
|
resolveAnchorTarget: () => resolveAnchorTarget
|
|
36
37
|
});
|
|
37
38
|
module.exports = __toCommonJS(extension_exports);
|
|
38
39
|
var import_typebox = require("@sinclair/typebox");
|
|
40
|
+
var import_node_fs3 = __toESM(require("fs"), 1);
|
|
39
41
|
|
|
40
42
|
// src/store.ts
|
|
41
43
|
var import_node_fs = __toESM(require("fs"), 1);
|
|
42
|
-
var
|
|
44
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
43
45
|
var import_node_os = __toESM(require("os"), 1);
|
|
44
46
|
|
|
45
47
|
// 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
|
-
};
|
|
48
|
+
var QUIET_AFTER_DAYS = 7;
|
|
49
|
+
var MS_PER_DAY = 864e5;
|
|
60
50
|
|
|
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
|
-
}
|
|
51
|
+
// src/store.ts
|
|
68
52
|
function normalizePath(filePath, baseCwd) {
|
|
69
53
|
if (typeof filePath !== "string") return "";
|
|
70
54
|
let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -80,14 +64,19 @@ function normalizePath(filePath, baseCwd) {
|
|
|
80
64
|
}
|
|
81
65
|
return norm;
|
|
82
66
|
}
|
|
83
|
-
function isSameOrSubPath(
|
|
84
|
-
if (!
|
|
85
|
-
const normA =
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
return
|
|
67
|
+
function isSameOrSubPath(dirA, dirB) {
|
|
68
|
+
if (!dirA || !dirB) return false;
|
|
69
|
+
const normA = dirA.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
70
|
+
const normB = dirB.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
71
|
+
const isWin = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normB);
|
|
72
|
+
const a = isWin ? normA.toLowerCase() : normA;
|
|
73
|
+
const b = isWin ? normB.toLowerCase() : normB;
|
|
74
|
+
return a === b || b.startsWith(a + "/") || a.startsWith(b + "/");
|
|
75
|
+
}
|
|
76
|
+
function globToRegExp(pattern) {
|
|
77
|
+
const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
|
|
78
|
+
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, ".*");
|
|
79
|
+
return new RegExp(`^${escaped}$`);
|
|
91
80
|
}
|
|
92
81
|
function extractPathsFromText(text) {
|
|
93
82
|
if (!text) return [];
|
|
@@ -102,6 +91,32 @@ function extractPathsFromText(text) {
|
|
|
102
91
|
}
|
|
103
92
|
return Array.from(results);
|
|
104
93
|
}
|
|
94
|
+
function extractProgressFromText(rawTitle) {
|
|
95
|
+
if (!rawTitle) return { cleanTitle: "" };
|
|
96
|
+
const stepParts = Array.from(rawTitle.matchAll(/(?:^|\s)(\d+)\.\s*([^\d]+?)(?=\s+\d+\.\s|$)/g));
|
|
97
|
+
if (stepParts.length >= 2) {
|
|
98
|
+
const steps = stepParts.map((m) => m[2].trim().replace(/[;;,,]+$/, "")).filter(Boolean);
|
|
99
|
+
const firstMatchIdx = rawTitle.indexOf(stepParts[0][0]);
|
|
100
|
+
const prefix = rawTitle.slice(0, firstMatchIdx).replace(/[::\-—\s]+$/, "").trim();
|
|
101
|
+
return {
|
|
102
|
+
cleanTitle: prefix || steps.join(" \u2192 "),
|
|
103
|
+
progress: { done: 0, total: steps.length, steps }
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const ratioMatch = rawTitle.match(/(?:\[(\d{1,3})\/(\d{1,3})\]|\((\d{1,3})\/(\d{1,3})\)|(?:^|\s)(\d{1,3})\/(\d{1,3})(?=\s|$))/);
|
|
107
|
+
if (ratioMatch) {
|
|
108
|
+
const done = parseInt(ratioMatch[1] ?? ratioMatch[3] ?? ratioMatch[5], 10);
|
|
109
|
+
const total = parseInt(ratioMatch[2] ?? ratioMatch[4] ?? ratioMatch[6], 10);
|
|
110
|
+
if (!isNaN(done) && !isNaN(total) && total >= 2 && total <= 500 && done >= 0 && done <= total) {
|
|
111
|
+
const stripped = rawTitle.replace(ratioMatch[0], " ").replace(/\s+/g, " ").trim();
|
|
112
|
+
return {
|
|
113
|
+
cleanTitle: stripped || rawTitle.trim(),
|
|
114
|
+
progress: { done, total }
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { cleanTitle: rawTitle.trim() };
|
|
119
|
+
}
|
|
105
120
|
function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
106
121
|
if (!targetCwd) return true;
|
|
107
122
|
const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -109,8 +124,7 @@ function isAnchorTargetingWorkspace(anchor, targetCwd) {
|
|
|
109
124
|
if (anchor.project && anchor.project !== "global") {
|
|
110
125
|
const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
111
126
|
if (isSameOrSubPath(normProj, normTarget)) return true;
|
|
112
|
-
|
|
113
|
-
if (projBase && projBase === targetBase) return true;
|
|
127
|
+
if (import_node_path.default.basename(normProj).toLowerCase() === targetBase) return true;
|
|
114
128
|
}
|
|
115
129
|
if (anchor.files && anchor.files.length > 0) {
|
|
116
130
|
for (const f of anchor.files) {
|
|
@@ -135,12 +149,9 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
|
|
|
135
149
|
matchedExact.push(touched);
|
|
136
150
|
break;
|
|
137
151
|
}
|
|
138
|
-
if (normPattern.includes("*")) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
matchedExact.push(touched);
|
|
142
|
-
break;
|
|
143
|
-
}
|
|
152
|
+
if (normPattern.includes("*") && globToRegExp(normPattern).test(touched)) {
|
|
153
|
+
matchedExact.push(touched);
|
|
154
|
+
break;
|
|
144
155
|
}
|
|
145
156
|
const dirPrefix = normPattern.endsWith("/") ? normPattern : normPattern + "/";
|
|
146
157
|
if (touched.startsWith(dirPrefix)) {
|
|
@@ -150,193 +161,137 @@ function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
|
|
|
150
161
|
}
|
|
151
162
|
}
|
|
152
163
|
if (matchedExact.length > 0) {
|
|
153
|
-
return {
|
|
154
|
-
anchor,
|
|
155
|
-
score: 1,
|
|
156
|
-
matchedFiles: Array.from(new Set(matchedExact)),
|
|
157
|
-
reason: "exact-file"
|
|
158
|
-
};
|
|
164
|
+
return { anchor, score: 1, matchedFiles: Array.from(new Set(matchedExact)), reason: "exact-file" };
|
|
159
165
|
}
|
|
160
166
|
if (matchedPrefix.length > 0) {
|
|
161
|
-
return {
|
|
162
|
-
anchor,
|
|
163
|
-
score: 0.85,
|
|
164
|
-
matchedFiles: Array.from(new Set(matchedPrefix)),
|
|
165
|
-
reason: "dir-prefix"
|
|
166
|
-
};
|
|
167
|
+
return { anchor, score: 0.85, matchedFiles: Array.from(new Set(matchedPrefix)), reason: "dir-prefix" };
|
|
167
168
|
}
|
|
168
169
|
return null;
|
|
169
170
|
}
|
|
170
171
|
function findMatchedAnchors(anchors, touchedFiles) {
|
|
171
|
-
const priorityWeight = { p0: 3, p1: 2, p2: 1 };
|
|
172
172
|
const results = [];
|
|
173
173
|
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
|
-
}
|
|
174
|
+
const m = matchAnchorAgainstTouchedFiles(anchor, touchedFiles);
|
|
175
|
+
if (m) results.push(m);
|
|
221
176
|
}
|
|
222
|
-
return
|
|
177
|
+
return results.sort((a, b) => b.score - a.score);
|
|
223
178
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
179
|
+
var COMMENT_FORMATS = {
|
|
180
|
+
".ts": (m) => `// ${m}`,
|
|
181
|
+
".tsx": (m) => `// ${m}`,
|
|
182
|
+
".js": (m) => `// ${m}`,
|
|
183
|
+
".jsx": (m) => `// ${m}`,
|
|
184
|
+
".mjs": (m) => `// ${m}`,
|
|
185
|
+
".cjs": (m) => `// ${m}`,
|
|
186
|
+
".go": (m) => `// ${m}`,
|
|
187
|
+
".rs": (m) => `// ${m}`,
|
|
188
|
+
".c": (m) => `// ${m}`,
|
|
189
|
+
".cpp": (m) => `// ${m}`,
|
|
190
|
+
".h": (m) => `// ${m}`,
|
|
191
|
+
".cs": (m) => `// ${m}`,
|
|
192
|
+
".java": (m) => `// ${m}`,
|
|
193
|
+
".kt": (m) => `// ${m}`,
|
|
194
|
+
".swift": (m) => `// ${m}`,
|
|
195
|
+
".dart": (m) => `// ${m}`,
|
|
196
|
+
".py": (m) => `# ${m}`,
|
|
197
|
+
".rb": (m) => `# ${m}`,
|
|
198
|
+
".sh": (m) => `# ${m}`,
|
|
199
|
+
".bash": (m) => `# ${m}`,
|
|
200
|
+
".yaml": (m) => `# ${m}`,
|
|
201
|
+
".yml": (m) => `# ${m}`,
|
|
202
|
+
".toml": (m) => `# ${m}`,
|
|
203
|
+
".ps1": (m) => `# ${m}`,
|
|
204
|
+
".html": (m) => `<!-- ${m} -->`,
|
|
205
|
+
".xml": (m) => `<!-- ${m} -->`,
|
|
206
|
+
".svg": (m) => `<!-- ${m} -->`,
|
|
207
|
+
".vue": (m) => `<!-- ${m} -->`,
|
|
208
|
+
".svelte": (m) => `<!-- ${m} -->`,
|
|
209
|
+
".css": (m) => `/* ${m} */`,
|
|
210
|
+
".scss": (m) => `/* ${m} */`,
|
|
211
|
+
".sql": (m) => `-- ${m}`,
|
|
212
|
+
".lua": (m) => `-- ${m}`
|
|
213
|
+
};
|
|
214
|
+
function makeSafeTaskAnnotation(filePath, anchor) {
|
|
215
|
+
const ext = import_node_path.default.extname(filePath).toLowerCase();
|
|
216
|
+
const fmt = COMMENT_FORMATS[ext];
|
|
217
|
+
if (!fmt) return null;
|
|
218
|
+
const num = anchor.id.replace(/^anc-/, "");
|
|
219
|
+
return "\n\n" + fmt(`\u2316 Note #${num}: ${anchor.title}`);
|
|
233
220
|
}
|
|
234
|
-
function
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
221
|
+
function matchesCommitMessage(anchor, commitMessages) {
|
|
222
|
+
const idLower = anchor.id.toLowerCase();
|
|
223
|
+
const numId = `#${anchor.id.replace(/^anc-/, "")}`;
|
|
224
|
+
const title = anchor.title.toLowerCase();
|
|
225
|
+
let titleTokens = [];
|
|
226
|
+
if (typeof Intl !== "undefined" && Intl.Segmenter) {
|
|
227
|
+
try {
|
|
228
|
+
const seg = new Intl.Segmenter("und", { granularity: "word" });
|
|
229
|
+
titleTokens = Array.from(seg.segment(title)).filter((s) => s.isWordLike).map((s) => s.segment.trim().toLowerCase()).filter((t) => t.length >= 2);
|
|
230
|
+
} catch {
|
|
231
|
+
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
titleTokens = title.split(/[\s,._\-\/]+/).filter((t) => t.length >= 2);
|
|
248
235
|
}
|
|
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;
|
|
236
|
+
for (const msg of commitMessages) {
|
|
237
|
+
const msgLower = msg.toLowerCase();
|
|
238
|
+
if (msgLower.includes(idLower) || msgLower.includes(numId)) {
|
|
239
|
+
return { matched: true, message: msg };
|
|
240
|
+
}
|
|
241
|
+
if (titleTokens.length > 0) {
|
|
242
|
+
const hits = titleTokens.filter((tok) => msgLower.includes(tok)).length;
|
|
243
|
+
if (hits >= Math.min(2, titleTokens.length)) {
|
|
244
|
+
return { matched: true, message: msg };
|
|
286
245
|
}
|
|
287
|
-
return getTodayDateString(now + diff * MS_DAY);
|
|
288
246
|
}
|
|
289
247
|
}
|
|
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;
|
|
305
|
-
}
|
|
306
|
-
function detectRecurrence(title) {
|
|
307
|
-
const lower = title.toLowerCase();
|
|
308
|
-
if (lower.includes("\u6BCF\u5929") || lower.includes("\u6BCF\u65E5") || lower.includes("daily") || lower.includes("every day")) {
|
|
309
|
-
return "daily";
|
|
310
|
-
}
|
|
311
|
-
return void 0;
|
|
248
|
+
return { matched: false };
|
|
312
249
|
}
|
|
313
|
-
function
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
250
|
+
function renderColdStartAnchorsContext(store, cwdOrNow, nowArg) {
|
|
251
|
+
let cwd;
|
|
252
|
+
let now = Date.now();
|
|
253
|
+
if (typeof cwdOrNow === "number") now = cwdOrNow;
|
|
254
|
+
else if (typeof cwdOrNow === "string") cwd = cwdOrNow;
|
|
255
|
+
if (typeof nowArg === "number") now = nowArg;
|
|
256
|
+
sweepQuietNotes(store, now);
|
|
257
|
+
const activeNotes = store.list({ status: "active", cwd });
|
|
258
|
+
if (activeNotes.length === 0) return "";
|
|
259
|
+
const lines = activeNotes.map((a) => {
|
|
260
|
+
const num = a.id.replace(/^anc-/, "");
|
|
261
|
+
const files = a.files.length > 0 ? ` (${a.files.slice(0, 2).join(", ")})` : "";
|
|
262
|
+
const prog = a.progress ? ` [${a.progress.done}/${a.progress.total}${a.progress.steps && a.progress.steps[a.progress.done] ? ` \xB7 next: ${a.progress.steps[a.progress.done]}` : ""}]` : "";
|
|
263
|
+
const desc = a.description ? ` \u2014 ${a.description}` : "";
|
|
264
|
+
return `\u2022 #${num} [${a.project}] ${a.title}${prog}${files}${desc}`;
|
|
265
|
+
});
|
|
266
|
+
return [
|
|
267
|
+
`<sticky-notes count="${activeNotes.length}">`,
|
|
268
|
+
`Pinned notes from previous sessions:`,
|
|
269
|
+
lines.join("\n"),
|
|
270
|
+
`When a note is completed, call anchor(action="settle", id="...") to tear it off.`,
|
|
271
|
+
`</sticky-notes>`
|
|
272
|
+
].join("\n");
|
|
319
273
|
}
|
|
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
|
-
}
|
|
274
|
+
function sweepQuietNotes(store, now = Date.now()) {
|
|
275
|
+
return store.withLock(() => {
|
|
276
|
+
const state = store.loadState();
|
|
277
|
+
const quieted = [];
|
|
278
|
+
for (const a of state.anchors) {
|
|
279
|
+
if (a.status === "active" && now - a.lastTouchedAt > QUIET_AFTER_DAYS * MS_PER_DAY) {
|
|
280
|
+
a.status = "sleeping";
|
|
281
|
+
a.updatedAt = now;
|
|
282
|
+
quieted.push(a.id);
|
|
335
283
|
}
|
|
336
|
-
} catch {
|
|
337
284
|
}
|
|
338
|
-
|
|
339
|
-
|
|
285
|
+
if (quieted.length > 0) {
|
|
286
|
+
state.lastSweepAt = now;
|
|
287
|
+
store.saveState(state);
|
|
288
|
+
}
|
|
289
|
+
return { quieted };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
function getDefaultStorageDir() {
|
|
293
|
+
if (process.env.ANCHOR_DIR) return import_node_path.default.resolve(process.env.ANCHOR_DIR);
|
|
294
|
+
return import_node_path.default.join(import_node_os.default.homedir(), ".anchor");
|
|
340
295
|
}
|
|
341
296
|
function sleepSync(ms) {
|
|
342
297
|
const end = Date.now() + ms;
|
|
@@ -358,22 +313,18 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
|
|
|
358
313
|
attempt++;
|
|
359
314
|
try {
|
|
360
315
|
const fd = import_node_fs.default.openSync(lockPath, "wx");
|
|
361
|
-
|
|
362
|
-
import_node_fs.default.writeFileSync(fd, JSON.stringify(meta), "utf-8");
|
|
316
|
+
import_node_fs.default.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }), "utf-8");
|
|
363
317
|
import_node_fs.default.closeSync(fd);
|
|
364
318
|
return () => {
|
|
365
319
|
try {
|
|
366
|
-
if (import_node_fs.default.existsSync(lockPath))
|
|
367
|
-
import_node_fs.default.unlinkSync(lockPath);
|
|
368
|
-
}
|
|
320
|
+
if (import_node_fs.default.existsSync(lockPath)) import_node_fs.default.unlinkSync(lockPath);
|
|
369
321
|
} catch {
|
|
370
322
|
}
|
|
371
323
|
};
|
|
372
324
|
} catch (err) {
|
|
373
325
|
if (err.code === "EEXIST") {
|
|
374
326
|
try {
|
|
375
|
-
const
|
|
376
|
-
const meta = JSON.parse(content);
|
|
327
|
+
const meta = JSON.parse(import_node_fs.default.readFileSync(lockPath, "utf-8"));
|
|
377
328
|
const isStale = Date.now() - meta.createdAt > staleTimeoutMs;
|
|
378
329
|
const isDead = meta.pid && !isProcessAlive(meta.pid);
|
|
379
330
|
if (isStale || isDead) {
|
|
@@ -385,8 +336,7 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
|
|
|
385
336
|
}
|
|
386
337
|
} catch {
|
|
387
338
|
}
|
|
388
|
-
|
|
389
|
-
sleepSync(delay);
|
|
339
|
+
sleepSync(Math.min(40, Math.floor(attempt * 4 + Math.random() * 8)));
|
|
390
340
|
continue;
|
|
391
341
|
}
|
|
392
342
|
break;
|
|
@@ -395,50 +345,46 @@ function acquireSyncLock(lockPath, maxWaitMs = 1500, staleTimeoutMs = 5e3) {
|
|
|
395
345
|
return () => {
|
|
396
346
|
};
|
|
397
347
|
}
|
|
398
|
-
function atomicRenameWithRetry(tempPath, targetPath,
|
|
399
|
-
|
|
348
|
+
function atomicRenameWithRetry(tempPath, targetPath, maxRetries = 5) {
|
|
349
|
+
let lastErr;
|
|
350
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
400
351
|
try {
|
|
401
352
|
import_node_fs.default.renameSync(tempPath, targetPath);
|
|
402
353
|
return;
|
|
403
354
|
} catch (err) {
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
407
|
-
sleepSync(delay);
|
|
355
|
+
lastErr = err;
|
|
356
|
+
if (err.code === "EPERM" || err.code === "EACCES" || err.code === "EBUSY") {
|
|
357
|
+
sleepSync(15 * (i + 1));
|
|
408
358
|
continue;
|
|
409
359
|
}
|
|
410
|
-
|
|
411
|
-
try {
|
|
412
|
-
if (import_node_fs.default.existsSync(tempPath)) import_node_fs.default.renameSync(tempPath, crashDump);
|
|
413
|
-
} catch {
|
|
414
|
-
}
|
|
415
|
-
throw err;
|
|
360
|
+
break;
|
|
416
361
|
}
|
|
417
362
|
}
|
|
363
|
+
try {
|
|
364
|
+
import_node_fs.default.copyFileSync(tempPath, targetPath);
|
|
365
|
+
import_node_fs.default.unlinkSync(tempPath);
|
|
366
|
+
} catch {
|
|
367
|
+
throw lastErr;
|
|
368
|
+
}
|
|
418
369
|
}
|
|
419
370
|
var AnchorStore = class {
|
|
420
371
|
storageDir;
|
|
421
372
|
statePath;
|
|
373
|
+
lockPath;
|
|
422
374
|
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();
|
|
375
|
+
constructor(storageDir) {
|
|
376
|
+
this.storageDir = storageDir ? import_node_path.default.resolve(storageDir) : getDefaultStorageDir();
|
|
377
|
+
this.statePath = import_node_path.default.join(this.storageDir, "state.json");
|
|
378
|
+
this.lockPath = import_node_path.default.join(this.storageDir, "state.lock");
|
|
379
|
+
this.archivePath = import_node_path.default.join(this.storageDir, "archive.jsonl");
|
|
434
380
|
}
|
|
435
|
-
|
|
436
|
-
|
|
381
|
+
ensureDirs() {
|
|
382
|
+
if (!import_node_fs.default.existsSync(this.storageDir)) {
|
|
383
|
+
import_node_fs.default.mkdirSync(this.storageDir, { recursive: true });
|
|
384
|
+
}
|
|
437
385
|
}
|
|
438
|
-
/**
|
|
439
|
-
* Execute mutation within a cross-process exclusive lock
|
|
440
|
-
*/
|
|
441
386
|
withLock(fn) {
|
|
387
|
+
this.ensureDirs();
|
|
442
388
|
const release = acquireSyncLock(this.lockPath);
|
|
443
389
|
try {
|
|
444
390
|
return fn();
|
|
@@ -446,84 +392,35 @@ var AnchorStore = class {
|
|
|
446
392
|
release();
|
|
447
393
|
}
|
|
448
394
|
}
|
|
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
395
|
loadState() {
|
|
461
396
|
this.ensureDirs();
|
|
462
397
|
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() };
|
|
398
|
+
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
483
399
|
}
|
|
484
400
|
try {
|
|
485
|
-
const
|
|
486
|
-
if (!
|
|
487
|
-
|
|
401
|
+
const parsed = JSON.parse(import_node_fs.default.readFileSync(this.statePath, "utf-8"));
|
|
402
|
+
if (!parsed || !Array.isArray(parsed.anchors)) {
|
|
403
|
+
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
488
404
|
}
|
|
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;
|
|
405
|
+
return parsed;
|
|
500
406
|
} catch {
|
|
501
|
-
const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
|
|
502
407
|
try {
|
|
503
|
-
import_node_fs.default.
|
|
408
|
+
import_node_fs.default.copyFileSync(this.statePath, import_node_path.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`));
|
|
504
409
|
} catch {
|
|
505
410
|
}
|
|
506
|
-
return {
|
|
507
|
-
version: 1,
|
|
508
|
-
anchors: [],
|
|
509
|
-
lastSweepAt: Date.now()
|
|
510
|
-
};
|
|
411
|
+
return { version: 1, anchors: [], lastSweepAt: Date.now(), nextId: 1 };
|
|
511
412
|
}
|
|
512
413
|
}
|
|
513
|
-
/**
|
|
514
|
-
* Atomically save store state via temp file + atomic rename with Windows NTFS spin-retry
|
|
515
|
-
*/
|
|
516
414
|
saveState(state) {
|
|
517
415
|
this.ensureDirs();
|
|
518
|
-
const
|
|
519
|
-
const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
|
|
416
|
+
const tempPath = `${this.statePath}.${process.pid}.${Date.now()}.tmp`;
|
|
520
417
|
import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
|
|
521
418
|
atomicRenameWithRetry(tempPath, this.statePath);
|
|
522
419
|
}
|
|
523
420
|
generateId(state) {
|
|
524
421
|
const numbers = state.anchors.map((a) => {
|
|
525
|
-
const
|
|
526
|
-
return
|
|
422
|
+
const m = a.id.match(/^anc-(\d+)$/);
|
|
423
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
527
424
|
}).filter((n) => n > 0);
|
|
528
425
|
const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
|
|
529
426
|
const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
|
|
@@ -531,38 +428,30 @@ var AnchorStore = class {
|
|
|
531
428
|
return `anc-${nextNum}`;
|
|
532
429
|
}
|
|
533
430
|
/**
|
|
534
|
-
*
|
|
431
|
+
* Pin a new sticky note
|
|
535
432
|
*/
|
|
536
433
|
create(input) {
|
|
537
434
|
return this.withLock(() => {
|
|
538
435
|
const state = this.loadState();
|
|
539
436
|
const now = Date.now();
|
|
540
437
|
const id = this.generateId(state);
|
|
438
|
+
const { cleanTitle, progress: parsedProgress } = extractProgressFromText(input.title);
|
|
541
439
|
const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
|
|
542
|
-
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${
|
|
440
|
+
const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${cleanTitle} ${input.description || ""}`);
|
|
543
441
|
let cwd = input.cwd ? normalizePath(input.cwd) : "";
|
|
544
442
|
if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
|
|
545
443
|
cwd = normalizePath(input.project.trim());
|
|
546
444
|
} 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
|
-
}
|
|
445
|
+
const firstDir = import_node_path.default.dirname(inferredFiles[0]);
|
|
446
|
+
if (firstDir && firstDir !== ".") cwd = normalizePath(firstDir);
|
|
551
447
|
}
|
|
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;
|
|
448
|
+
const projectName = input.project ? input.project.trim() : cwd ? import_node_path.default.basename(cwd) : "global";
|
|
557
449
|
const anchor = {
|
|
558
450
|
id,
|
|
559
|
-
title:
|
|
451
|
+
title: cleanTitle,
|
|
560
452
|
description: input.description?.trim(),
|
|
561
453
|
priority: input.priority || "p1",
|
|
562
454
|
status: "active",
|
|
563
|
-
durability,
|
|
564
|
-
recurrence,
|
|
565
|
-
targetDate,
|
|
566
455
|
project: projectName,
|
|
567
456
|
cwd,
|
|
568
457
|
createdAt: now,
|
|
@@ -570,8 +459,8 @@ var AnchorStore = class {
|
|
|
570
459
|
lastTouchedAt: now,
|
|
571
460
|
files: inferredFiles,
|
|
572
461
|
tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
|
|
573
|
-
|
|
574
|
-
|
|
462
|
+
...input.sessionFile ? { sessionFile: normalizePath(input.sessionFile) } : {},
|
|
463
|
+
...input.progress || parsedProgress ? { progress: input.progress || parsedProgress } : {}
|
|
575
464
|
};
|
|
576
465
|
state.anchors.push(anchor);
|
|
577
466
|
this.saveState(state);
|
|
@@ -581,31 +470,19 @@ var AnchorStore = class {
|
|
|
581
470
|
findAnchorIndex(anchors, id) {
|
|
582
471
|
const clean = id.trim().replace(/^#/, "");
|
|
583
472
|
return anchors.findIndex(
|
|
584
|
-
(a) => a.id === clean || a.id === `anc-${clean}` || (
|
|
473
|
+
(a) => a.id === clean || a.id === `anc-${clean}` || (/^\d+$/.test(clean) ? a.id === `anc-${parseInt(clean, 10)}` : false)
|
|
585
474
|
);
|
|
586
475
|
}
|
|
587
|
-
/**
|
|
588
|
-
* Get an anchor by ID (supports 'anc-1', '#anc-1', '1', '01')
|
|
589
|
-
*/
|
|
590
476
|
get(id) {
|
|
591
477
|
const state = this.loadState();
|
|
592
478
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
593
479
|
return idx === -1 ? void 0 : state.anchors[idx];
|
|
594
480
|
}
|
|
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
481
|
list(filter) {
|
|
600
482
|
const state = this.loadState();
|
|
601
483
|
const targetCwd = filter?.cwd ? normalizePath(filter.cwd) : null;
|
|
602
|
-
const today = getTodayDateString(filter?.now);
|
|
603
484
|
return state.anchors.filter((a) => {
|
|
604
|
-
if (!filter?.all && filter?.status === "active" && a.recurrence === "daily" && a.lastCompletedDate === today) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
485
|
if (filter?.status && a.status !== filter.status) return false;
|
|
608
|
-
if (filter?.priority && a.priority !== filter.priority) return false;
|
|
609
486
|
if (!filter?.all && targetCwd) {
|
|
610
487
|
if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
|
|
611
488
|
return false;
|
|
@@ -614,19 +491,13 @@ var AnchorStore = class {
|
|
|
614
491
|
return true;
|
|
615
492
|
});
|
|
616
493
|
}
|
|
617
|
-
/**
|
|
618
|
-
* Update an existing anchor
|
|
619
|
-
*/
|
|
620
494
|
update(id, patch) {
|
|
621
495
|
return this.withLock(() => {
|
|
622
496
|
const state = this.loadState();
|
|
623
497
|
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];
|
|
498
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
628
499
|
const updated = {
|
|
629
|
-
...
|
|
500
|
+
...state.anchors[idx],
|
|
630
501
|
...patch,
|
|
631
502
|
updatedAt: Date.now()
|
|
632
503
|
};
|
|
@@ -638,60 +509,70 @@ var AnchorStore = class {
|
|
|
638
509
|
return updated;
|
|
639
510
|
});
|
|
640
511
|
}
|
|
641
|
-
|
|
642
|
-
* Touch an anchor to refresh its decay window and reactivate sleeping state
|
|
643
|
-
*/
|
|
644
|
-
touch(id, timestamp = Date.now()) {
|
|
512
|
+
touch(id, timestamp = Date.now(), sessionFile) {
|
|
645
513
|
return this.withLock(() => {
|
|
646
514
|
const state = this.loadState();
|
|
647
515
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
648
|
-
if (idx === -1) {
|
|
649
|
-
throw new Error(`Anchor not found: ${id}`);
|
|
650
|
-
}
|
|
516
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
651
517
|
const current = state.anchors[idx];
|
|
652
518
|
current.lastTouchedAt = timestamp;
|
|
653
519
|
current.updatedAt = timestamp;
|
|
654
520
|
if (current.status === "sleeping") {
|
|
655
521
|
current.status = "active";
|
|
656
522
|
}
|
|
523
|
+
if (sessionFile) {
|
|
524
|
+
current.sessionFile = normalizePath(sessionFile);
|
|
525
|
+
}
|
|
657
526
|
state.anchors[idx] = current;
|
|
658
527
|
this.saveState(state);
|
|
659
528
|
return current;
|
|
660
529
|
});
|
|
661
530
|
}
|
|
662
531
|
/**
|
|
663
|
-
*
|
|
532
|
+
* Advance (or set) progress on a sticky note:
|
|
533
|
+
* - If delta is provided (default +1), increments `done` up to `total`.
|
|
534
|
+
* - If note had no progress yet, initializes a 4-step bar (`1/4`).
|
|
664
535
|
*/
|
|
665
|
-
|
|
536
|
+
step(id, delta = 1, explicitTotal) {
|
|
666
537
|
return this.withLock(() => {
|
|
667
538
|
const state = this.loadState();
|
|
668
539
|
const idx = this.findAnchorIndex(state.anchors, id);
|
|
669
|
-
if (idx === -1) {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
const
|
|
673
|
-
const
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify({ ...anchor, settledForDate: today }) + "\n", "utf-8");
|
|
685
|
-
this.saveState(state);
|
|
686
|
-
return anchor;
|
|
540
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
541
|
+
const current = state.anchors[idx];
|
|
542
|
+
const now = Date.now();
|
|
543
|
+
const total = explicitTotal && explicitTotal >= 2 ? explicitTotal : current.progress?.total || 4;
|
|
544
|
+
const prevDone = current.progress?.done || 0;
|
|
545
|
+
const nextDone = Math.max(0, Math.min(total, prevDone + delta));
|
|
546
|
+
current.progress = {
|
|
547
|
+
...current.progress || {},
|
|
548
|
+
done: nextDone,
|
|
549
|
+
total
|
|
550
|
+
};
|
|
551
|
+
current.lastTouchedAt = now;
|
|
552
|
+
current.updatedAt = now;
|
|
553
|
+
if (current.status === "sleeping") {
|
|
554
|
+
current.status = "active";
|
|
687
555
|
}
|
|
556
|
+
state.anchors[idx] = current;
|
|
557
|
+
this.saveState(state);
|
|
558
|
+
return { anchor: current, completed: nextDone >= total };
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Tear off (settle) a completed sticky note and archive it to archive.jsonl
|
|
563
|
+
*/
|
|
564
|
+
settle(id, evidence = {}, now = Date.now()) {
|
|
565
|
+
return this.withLock(() => {
|
|
566
|
+
const state = this.loadState();
|
|
567
|
+
const idx = this.findAnchorIndex(state.anchors, id);
|
|
568
|
+
if (idx === -1) throw new Error(`Note not found: ${id}`);
|
|
688
569
|
const [settled] = state.anchors.splice(idx, 1);
|
|
689
570
|
settled.status = "settled";
|
|
690
571
|
settled.updatedAt = now;
|
|
691
572
|
settled.evidence = {
|
|
692
573
|
...evidence,
|
|
693
574
|
settledAt: now,
|
|
694
|
-
settledBy: evidence.settledBy || "manual
|
|
575
|
+
settledBy: evidence.settledBy || "manual"
|
|
695
576
|
};
|
|
696
577
|
this.ensureDirs();
|
|
697
578
|
import_node_fs.default.appendFileSync(this.archivePath, JSON.stringify(settled) + "\n", "utf-8");
|
|
@@ -700,12 +581,12 @@ var AnchorStore = class {
|
|
|
700
581
|
});
|
|
701
582
|
}
|
|
702
583
|
/**
|
|
703
|
-
*
|
|
584
|
+
* Put back (undo) the last torn-off sticky note
|
|
704
585
|
*/
|
|
705
586
|
undoSettle() {
|
|
706
587
|
return this.withLock(() => {
|
|
707
588
|
if (!import_node_fs.default.existsSync(this.archivePath)) {
|
|
708
|
-
throw new Error("No archived
|
|
589
|
+
throw new Error("No archived notes to restore");
|
|
709
590
|
}
|
|
710
591
|
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
711
592
|
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
@@ -714,23 +595,18 @@ var AnchorStore = class {
|
|
|
714
595
|
const candidate = lines.pop();
|
|
715
596
|
try {
|
|
716
597
|
const parsed = JSON.parse(candidate);
|
|
717
|
-
if (parsed && typeof parsed
|
|
598
|
+
if (parsed && typeof parsed.id === "string" && typeof parsed.title === "string") {
|
|
718
599
|
anchor = parsed;
|
|
719
600
|
}
|
|
720
601
|
} catch {
|
|
721
602
|
}
|
|
722
603
|
}
|
|
723
|
-
if (!anchor)
|
|
724
|
-
throw new Error("Archive is empty, nothing to undo");
|
|
725
|
-
}
|
|
604
|
+
if (!anchor) throw new Error("Archive is empty, nothing to undo");
|
|
726
605
|
import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
727
606
|
anchor.status = "active";
|
|
728
607
|
anchor.updatedAt = Date.now();
|
|
608
|
+
anchor.lastTouchedAt = Date.now();
|
|
729
609
|
delete anchor.evidence;
|
|
730
|
-
delete anchor.settledForDate;
|
|
731
|
-
if (anchor.recurrence === "daily") {
|
|
732
|
-
delete anchor.lastCompletedDate;
|
|
733
|
-
}
|
|
734
610
|
const state = this.loadState();
|
|
735
611
|
const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
|
|
736
612
|
if (existingIdx !== -1) {
|
|
@@ -741,556 +617,35 @@ var AnchorStore = class {
|
|
|
741
617
|
state.anchors.push(anchor);
|
|
742
618
|
}
|
|
743
619
|
} else {
|
|
744
|
-
const
|
|
745
|
-
if (
|
|
746
|
-
const restoredNum = parseInt(match[1], 10);
|
|
747
|
-
state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
|
|
748
|
-
}
|
|
620
|
+
const m = anchor.id.match(/^anc-(\d+)$/);
|
|
621
|
+
if (m) state.nextId = Math.max(state.nextId || 1, parseInt(m[1], 10) + 1);
|
|
749
622
|
state.anchors.push(anchor);
|
|
750
623
|
}
|
|
751
624
|
this.saveState(state);
|
|
752
625
|
return anchor;
|
|
753
626
|
});
|
|
754
627
|
}
|
|
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
628
|
};
|
|
821
629
|
|
|
822
|
-
// src/
|
|
823
|
-
var
|
|
824
|
-
var
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
}
|
|
630
|
+
// src/tui.ts
|
|
631
|
+
var import_node_fs2 = __toESM(require("fs"), 1);
|
|
632
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
633
|
+
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
634
|
+
function stripAnsi(str) {
|
|
635
|
+
return str.replace(ANSI_REGEX, "");
|
|
636
|
+
}
|
|
637
|
+
function getDisplayWidth(str) {
|
|
638
|
+
const clean = stripAnsi(str);
|
|
639
|
+
let width = 0;
|
|
640
|
+
for (const char of clean) {
|
|
641
|
+
const code = char.codePointAt(0) || 0;
|
|
642
|
+
if (code === 8205 || code === 65039 || code === 65038 || code >= 768 && code <= 879 || code >= 8203 && code <= 8207) {
|
|
643
|
+
continue;
|
|
863
644
|
}
|
|
864
|
-
if (
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
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
|
-
};
|
|
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
|
-
|
|
1226
|
-
// src/tui.ts
|
|
1227
|
-
var import_node_path4 = __toESM(require("path"), 1);
|
|
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
|
-
}
|
|
1278
|
-
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
1279
|
-
function stripAnsi(str) {
|
|
1280
|
-
return str.replace(ANSI_REGEX, "");
|
|
1281
|
-
}
|
|
1282
|
-
function getDisplayWidth(str) {
|
|
1283
|
-
const clean = stripAnsi(str);
|
|
1284
|
-
let width = 0;
|
|
1285
|
-
for (const char of clean) {
|
|
1286
|
-
const code = char.codePointAt(0) || 0;
|
|
1287
|
-
if (code === 8205 || code === 65039 || code === 65038 || code >= 768 && code <= 879 || code >= 8203 && code <= 8207) {
|
|
1288
|
-
continue;
|
|
1289
|
-
}
|
|
1290
|
-
if (code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 131072 && code <= 173791 || code >= 173824 && code <= 177983 || code >= 65281 && code <= 65376 || code >= 12288 && code <= 12351 || code >= 44032 && code <= 55215 || code >= 4352 && code <= 4607 || code >= 12592 && code <= 12687 || code >= 12352 && code <= 12447 || code >= 12448 && code <= 12543 || code >= 127744 && code <= 129535 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767 || code >= 9728 && code <= 10175 || code >= 129648 && code <= 129791) {
|
|
1291
|
-
width += 2;
|
|
1292
|
-
} else {
|
|
1293
|
-
width += 1;
|
|
645
|
+
if (code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 131072 && code <= 173791 || code >= 173824 && code <= 177983 || code >= 65281 && code <= 65376 || code >= 12288 && code <= 12351 || code >= 44032 && code <= 55215 || code >= 4352 && code <= 4607 || code >= 12592 && code <= 12687 || code >= 12352 && code <= 12447 || code >= 12448 && code <= 12543 || code >= 127744 && code <= 129535 || code >= 128512 && code <= 128591 || code >= 128640 && code <= 128767 || code >= 9728 && code <= 10175 || code >= 129648 && code <= 129791) {
|
|
646
|
+
width += 2;
|
|
647
|
+
} else {
|
|
648
|
+
width += 1;
|
|
1294
649
|
}
|
|
1295
650
|
}
|
|
1296
651
|
return width;
|
|
@@ -1326,14 +681,12 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
|
1326
681
|
const charLen = codePoint > 65535 ? 2 : 1;
|
|
1327
682
|
const fullChar = singleLine.slice(i, i + charLen);
|
|
1328
683
|
const charWidth = getDisplayWidth(fullChar);
|
|
1329
|
-
if (accumWidth + charWidth > target)
|
|
1330
|
-
break;
|
|
1331
|
-
}
|
|
684
|
+
if (accumWidth + charWidth > target) break;
|
|
1332
685
|
accumulated += fullChar;
|
|
1333
686
|
accumWidth += charWidth;
|
|
1334
687
|
i += charLen;
|
|
1335
688
|
}
|
|
1336
|
-
accumulated = accumulated.replace(/\s*[\(\[\{(【]
|
|
689
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【][^)\]})】]*$/, "");
|
|
1337
690
|
return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
|
|
1338
691
|
}
|
|
1339
692
|
function padToWidth(str, targetWidth) {
|
|
@@ -1342,84 +695,68 @@ function padToWidth(str, targetWidth) {
|
|
|
1342
695
|
if (current >= targetWidth) return truncated;
|
|
1343
696
|
return truncated + " ".repeat(targetWidth - current);
|
|
1344
697
|
}
|
|
1345
|
-
function
|
|
1346
|
-
if (
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
}
|
|
1352
|
-
const todayStr = getTodayDateString(now);
|
|
1353
|
-
if (anchor.targetDate === todayStr) {
|
|
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;
|
|
698
|
+
function formatProgressBar(progress, cells = 4) {
|
|
699
|
+
if (!progress || !progress.total || progress.total < 1) return "";
|
|
700
|
+
const done = Math.max(0, Math.min(progress.total, progress.done || 0));
|
|
701
|
+
const filled = Math.min(cells, Math.max(done > 0 ? 1 : 0, Math.round(done / progress.total * cells)));
|
|
702
|
+
const finalFilled = done >= progress.total ? cells : done === 0 ? 0 : filled;
|
|
703
|
+
const bar = "\u25A0".repeat(finalFilled) + "\u25A1".repeat(Math.max(0, cells - finalFilled));
|
|
704
|
+
return `[${bar} ${done}/${progress.total}]`;
|
|
1377
705
|
}
|
|
1378
|
-
function
|
|
1379
|
-
const
|
|
1380
|
-
|
|
1381
|
-
const
|
|
1382
|
-
|
|
1383
|
-
const
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
706
|
+
function formatRelativeAge(timestamp, now = Date.now()) {
|
|
707
|
+
const diffSec = Math.max(0, Math.floor((now - timestamp) / 1e3));
|
|
708
|
+
if (diffSec < 60) return "just now";
|
|
709
|
+
const diffMin = Math.floor(diffSec / 60);
|
|
710
|
+
if (diffMin < 60) return `${diffMin}m ago`;
|
|
711
|
+
const diffHours = Math.floor(diffMin / 60);
|
|
712
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
713
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
714
|
+
return `${diffDays}d ago`;
|
|
715
|
+
}
|
|
716
|
+
function formatOrigin(anchor) {
|
|
717
|
+
if (typeof anchor === "string") {
|
|
718
|
+
if (!anchor) return "global";
|
|
719
|
+
return import_node_path2.default.basename(anchor.replace(/\\/g, "/")) || "global";
|
|
1387
720
|
}
|
|
1388
|
-
|
|
1389
|
-
if (
|
|
1390
|
-
|
|
721
|
+
let proj = "global";
|
|
722
|
+
if (anchor.project && anchor.project !== "global") {
|
|
723
|
+
const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
724
|
+
if (normProj.includes("/")) {
|
|
725
|
+
const parts = normProj.split("/").filter(Boolean);
|
|
726
|
+
proj = parts.slice(-2).join("/");
|
|
727
|
+
} else {
|
|
728
|
+
proj = normProj;
|
|
729
|
+
}
|
|
730
|
+
} else if (anchor.cwd) {
|
|
731
|
+
proj = import_node_path2.default.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
|
|
1391
732
|
}
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
733
|
+
if (anchor.files && anchor.files.length > 0) {
|
|
734
|
+
const shortFile = import_node_path2.default.basename(anchor.files[0].replace(/\\/g, "/"));
|
|
735
|
+
if (shortFile && !proj.endsWith(shortFile)) {
|
|
736
|
+
return `${proj} \xB7 ${shortFile}`;
|
|
737
|
+
}
|
|
1395
738
|
}
|
|
1396
|
-
return
|
|
739
|
+
return proj;
|
|
1397
740
|
}
|
|
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;
|
|
741
|
+
function sortAnchorsForDisplay(anchors, cwd) {
|
|
742
|
+
return [...anchors].sort((a, b) => {
|
|
743
|
+
if (a.status !== b.status) {
|
|
744
|
+
return a.status === "active" ? -1 : 1;
|
|
1407
745
|
}
|
|
1408
|
-
if (
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
746
|
+
if (cwd) {
|
|
747
|
+
const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
|
|
748
|
+
const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
|
|
749
|
+
if (aMatch !== bMatch) return aMatch - bMatch;
|
|
750
|
+
}
|
|
751
|
+
return b.lastTouchedAt - a.lastTouchedAt;
|
|
752
|
+
});
|
|
1413
753
|
}
|
|
1414
754
|
function updateAnchorStatusBar(ctx, store) {
|
|
1415
755
|
if (!hasActiveUI(ctx)) return;
|
|
1416
756
|
try {
|
|
1417
757
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
return;
|
|
1421
|
-
}
|
|
1422
|
-
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
758
|
+
ctx.ui.setStatus("anchor", active.length > 0 ? `\u2316 ${active.length}` : void 0);
|
|
759
|
+
updateStartupBanner(ctx, store);
|
|
1423
760
|
} catch {
|
|
1424
761
|
}
|
|
1425
762
|
}
|
|
@@ -1428,113 +765,129 @@ function updateStartupBanner(ctx, store) {
|
|
|
1428
765
|
try {
|
|
1429
766
|
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1430
767
|
if (active.length === 0) {
|
|
1431
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
768
|
+
ctx.ui.setWidget("anchor-startup", void 0, { placement: "belowEditor" });
|
|
1432
769
|
return;
|
|
1433
770
|
}
|
|
1434
|
-
const
|
|
1435
|
-
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1436
|
-
const topItems = sorted.slice(0, 3);
|
|
771
|
+
const sorted = sortAnchorsForDisplay(active, ctx.cwd);
|
|
1437
772
|
const factory = (_tui, theme) => ({
|
|
1438
773
|
render: (width) => {
|
|
1439
|
-
const maxTitleWidth = Math.max(12, width - 36);
|
|
1440
774
|
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
const
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
const
|
|
1449
|
-
const
|
|
1450
|
-
|
|
775
|
+
if (sorted.length === 1) {
|
|
776
|
+
const a = sorted[0];
|
|
777
|
+
const num = a.id.replace(/^anc-/, "");
|
|
778
|
+
const bar = a.progress ? `${fg("accent", formatProgressBar(a.progress))} ` : "";
|
|
779
|
+
const nextStep = a.progress?.steps && a.progress.steps[a.progress.done] ? fg("muted", ` \u25B8 ${a.progress.steps[a.progress.done]}`) : "";
|
|
780
|
+
const origin = formatOrigin(a);
|
|
781
|
+
const sessionIcon = a.sessionFile ? " \u21AA" : "";
|
|
782
|
+
const prefix = `${fg("accent", "\u2316")} ${fg("dim", `#${num}`)} ${bar}`;
|
|
783
|
+
const suffix = ` ${fg("muted", `(${origin}${sessionIcon})`)}`;
|
|
784
|
+
const availTitle = Math.max(12, width - getDisplayWidth(prefix) - getDisplayWidth(nextStep) - getDisplayWidth(suffix) - 2);
|
|
785
|
+
const titleClipped = truncateToWidth(a.title, availTitle);
|
|
786
|
+
return [truncateToWidth(`${prefix}${titleClipped}${nextStep}${suffix}`, width)];
|
|
1451
787
|
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
788
|
+
const countTag = fg("accent", `\u2316 ${sorted.length} notes: `);
|
|
789
|
+
const segments = sorted.slice(0, 3).map((a) => {
|
|
790
|
+
const num = a.id.replace(/^anc-/, "");
|
|
791
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
792
|
+
const shortTitle = truncateToWidth(a.title, Math.max(14, Math.floor((width - 20) / Math.min(3, sorted.length)) - 12));
|
|
793
|
+
return `${fg("dim", `#${num}`)} ${bar}${shortTitle}`;
|
|
794
|
+
});
|
|
795
|
+
const more = sorted.length > 3 ? fg("dim", ` +${sorted.length - 3}`) : "";
|
|
796
|
+
return [truncateToWidth(`${countTag}${segments.join(fg("dim", " \xB7 "))}${more}`, width)];
|
|
1456
797
|
},
|
|
1457
798
|
invalidate: () => {
|
|
1458
799
|
}
|
|
1459
800
|
});
|
|
1460
801
|
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
1461
|
-
ctx.ui.setWidget("anchor-startup", content, { placement: "
|
|
802
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "belowEditor" });
|
|
1462
803
|
} catch {
|
|
1463
804
|
}
|
|
1464
805
|
}
|
|
1465
806
|
async function openAnchorDashboard(ctx, store) {
|
|
1466
807
|
if (!hasActiveUI(ctx)) return;
|
|
1467
|
-
const
|
|
1468
|
-
if (
|
|
1469
|
-
ctx.ui.notify("\u2316 No
|
|
808
|
+
const allNotes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
809
|
+
if (allNotes.length === 0) {
|
|
810
|
+
ctx.ui.notify("\u2316 No sticky notes. Type `pin: <note>` or `/pin <note>` to stick one.", "info");
|
|
1470
811
|
return;
|
|
1471
812
|
}
|
|
1472
|
-
const
|
|
813
|
+
const sorted = sortAnchorsForDisplay(allNotes, ctx.cwd);
|
|
1473
814
|
const optionMap = /* @__PURE__ */ new Map();
|
|
1474
815
|
const displayOptions = [];
|
|
1475
816
|
const termCols = process.stdout?.columns || 96;
|
|
1476
|
-
const isNarrow = termCols <
|
|
1477
|
-
const titleWidth = isNarrow ? Math.max(
|
|
1478
|
-
for (
|
|
1479
|
-
const a = sortedList[i];
|
|
817
|
+
const isNarrow = termCols < 84;
|
|
818
|
+
const titleWidth = isNarrow ? Math.max(20, termCols - 30) : 42;
|
|
819
|
+
for (const a of sorted) {
|
|
1480
820
|
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1481
|
-
const
|
|
1482
|
-
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;
|
|
821
|
+
const bar = a.progress ? `${formatProgressBar(a.progress)} ` : "";
|
|
822
|
+
const origin = formatOrigin(a) + (a.sessionFile ? " \u21AA" : "");
|
|
823
|
+
const age = a.status === "sleeping" ? `${formatRelativeAge(a.createdAt)} \xB7 quiet` : formatRelativeAge(a.createdAt);
|
|
1486
824
|
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();
|
|
825
|
+
const colTitle = padToWidth(`${bar}${a.title}`, titleWidth);
|
|
826
|
+
const colOrigin = padToWidth(origin, 22);
|
|
827
|
+
const label = isNarrow ? `${colNum}${colTitle} ${age}`.trimEnd() : `${colNum}${colTitle} ${colOrigin} ${age}`.trimEnd();
|
|
1492
828
|
optionMap.set(label, a);
|
|
1493
829
|
displayOptions.push(label);
|
|
1494
830
|
}
|
|
1495
|
-
const selected = await ctx.ui.select("\u2316
|
|
831
|
+
const selected = await ctx.ui.select("\u2316 Notes (select a note):", displayOptions);
|
|
1496
832
|
if (!selected) return;
|
|
1497
833
|
const anchor = optionMap.get(selected);
|
|
1498
834
|
if (!anchor) return;
|
|
835
|
+
const currentSessionFile = ctx.sessionManager?.getSessionFile?.();
|
|
836
|
+
const canJumpSession = Boolean(
|
|
837
|
+
anchor.sessionFile && import_node_fs2.default.existsSync(anchor.sessionFile) && typeof ctx.switchSession === "function" && (!currentSessionFile || import_node_path2.default.resolve(anchor.sessionFile) !== import_node_path2.default.resolve(currentSessionFile))
|
|
838
|
+
);
|
|
839
|
+
const stepLabel = anchor.progress ? `\u25B6 Advance step (${anchor.progress.done}/${anchor.progress.total} \u2794 ${Math.min(anchor.progress.total, anchor.progress.done + 1)}/${anchor.progress.total})` : "\u25B6 Start progress bar (1/4)";
|
|
1499
840
|
const actions = [
|
|
1500
|
-
"\u2713
|
|
1501
|
-
|
|
1502
|
-
"\
|
|
1503
|
-
"\
|
|
841
|
+
"\u2713 Done (tear off note)",
|
|
842
|
+
stepLabel,
|
|
843
|
+
...canJumpSession ? ["\u21AA Jump to session (resume linked conversation)"] : [],
|
|
844
|
+
...anchor.status === "sleeping" ? ["\u26A1 Wake up (mark active)"] : [],
|
|
845
|
+
"\u2139 Read full note",
|
|
846
|
+
"\u21A9 Back"
|
|
1504
847
|
];
|
|
1505
|
-
const action = await ctx.ui.select(
|
|
1506
|
-
`\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
|
|
1507
|
-
actions
|
|
1508
|
-
);
|
|
848
|
+
const action = await ctx.ui.select(`\u2316 #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, actions);
|
|
1509
849
|
if (!action || action.startsWith("\u21A9")) return;
|
|
1510
850
|
if (action.startsWith("\u2713")) {
|
|
1511
|
-
store.settle(anchor.id, { settledBy: "manual
|
|
1512
|
-
|
|
1513
|
-
ctx.ui.notify(successMsg, "info");
|
|
851
|
+
store.settle(anchor.id, { settledBy: "manual" });
|
|
852
|
+
ctx.ui.notify(`\u2316 Torn off #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}" (/anchor undo to put back)`, "info");
|
|
1514
853
|
updateAnchorStatusBar(ctx, store);
|
|
854
|
+
} else if (action.startsWith("\u25B6")) {
|
|
855
|
+
const { anchor: updated, completed } = store.step(anchor.id, 1);
|
|
856
|
+
updateAnchorStatusBar(ctx, store);
|
|
857
|
+
if (completed) {
|
|
858
|
+
ctx.ui.notify(
|
|
859
|
+
`\u2316 #${updated.id.replace(/^anc-/, "")} reached ${formatProgressBar(updated.progress)}! Run /done ${updated.id.replace(/^anc-/, "")} to tear off.`,
|
|
860
|
+
"info"
|
|
861
|
+
);
|
|
862
|
+
} else {
|
|
863
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
864
|
+
}
|
|
865
|
+
} else if (action.startsWith("\u21AA") && anchor.sessionFile) {
|
|
866
|
+
try {
|
|
867
|
+
await ctx.switchSession(anchor.sessionFile);
|
|
868
|
+
} catch (err) {
|
|
869
|
+
ctx.ui.notify(`Could not switch session: ${err.message}`, "error");
|
|
870
|
+
}
|
|
1515
871
|
} else if (action.startsWith("\u26A1")) {
|
|
1516
872
|
store.touch(anchor.id);
|
|
1517
|
-
ctx.ui.notify(`\u2316 Woke up #${anchor.id}: "${anchor.title}"`, "info");
|
|
873
|
+
ctx.ui.notify(`\u2316 Woke up #${anchor.id.replace(/^anc-/, "")}: "${anchor.title}"`, "info");
|
|
1518
874
|
updateAnchorStatusBar(ctx, store);
|
|
1519
875
|
} else if (action.startsWith("\u2139")) {
|
|
1520
876
|
const details = [
|
|
1521
|
-
|
|
1522
|
-
anchor.
|
|
1523
|
-
anchor.
|
|
1524
|
-
anchor.files
|
|
1525
|
-
anchor.
|
|
1526
|
-
`
|
|
877
|
+
`\u2316 Note #${anchor.id.replace(/^anc-/, "")}: ${anchor.title}`,
|
|
878
|
+
anchor.progress ? `Progress: ${formatProgressBar(anchor.progress)}${anchor.progress.steps ? ` (${anchor.progress.steps.join(" \u2192 ")})` : ""}` : null,
|
|
879
|
+
anchor.description ? `Details: ${anchor.description}` : null,
|
|
880
|
+
anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
881
|
+
anchor.sessionFile ? `Session: ${import_node_path2.default.basename(anchor.sessionFile)}` : null,
|
|
882
|
+
`Workspace: ${anchor.project || "global"} \xB7 Pinned ${formatRelativeAge(anchor.createdAt)}`
|
|
1527
883
|
].filter(Boolean).join("\n");
|
|
1528
884
|
ctx.ui.notify(details, "info");
|
|
1529
885
|
}
|
|
1530
886
|
}
|
|
1531
887
|
|
|
1532
888
|
// 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()) {
|
|
889
|
+
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
890
|
+
function parseAnchorCommandInput(raw) {
|
|
1538
891
|
const trimmed = (raw || "").trim();
|
|
1539
892
|
if (!trimmed || trimmed === "list" || trimmed === "ls") {
|
|
1540
893
|
return { mode: "dashboard" };
|
|
@@ -1549,21 +902,30 @@ function parseAnchorCommandInput(raw, now = Date.now()) {
|
|
|
1549
902
|
targetId: settleMatch[1].trim().replace(/^#/, "")
|
|
1550
903
|
};
|
|
1551
904
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
905
|
+
const stepMatch = trimmed.match(/^(?:step|\+)\s*(\S+)(?:\s+([+-]?\d+))?$/i);
|
|
906
|
+
if (stepMatch) {
|
|
907
|
+
return {
|
|
908
|
+
mode: "step",
|
|
909
|
+
targetId: stepMatch[1].trim().replace(/^#/, ""),
|
|
910
|
+
delta: stepMatch[2] ? parseInt(stepMatch[2], 10) : 1
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
const jumpMatch = trimmed.match(/^(?:jump|resume|session)\s+(.+)$/i);
|
|
914
|
+
if (jumpMatch) {
|
|
915
|
+
return {
|
|
916
|
+
mode: "jump",
|
|
917
|
+
targetId: jumpMatch[1].trim().replace(/^#/, "")
|
|
918
|
+
};
|
|
1559
919
|
}
|
|
1560
|
-
const cleanTitle = tokens.join(" ").trim();
|
|
1561
920
|
return {
|
|
1562
921
|
mode: "pin",
|
|
1563
|
-
title:
|
|
1564
|
-
targetDate
|
|
922
|
+
title: trimmed
|
|
1565
923
|
};
|
|
1566
924
|
}
|
|
925
|
+
function parseQuickNoteInput(text) {
|
|
926
|
+
const m = (text || "").trim().match(/^(?:pin|note|便签|备忘)\s*[::]\s*(.+)$/i);
|
|
927
|
+
return m && m[1].trim() ? m[1].trim() : null;
|
|
928
|
+
}
|
|
1567
929
|
function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
1568
930
|
const clean = idOrIndex.trim().replace(/^#/, "");
|
|
1569
931
|
if (/^\d+$/.test(clean)) {
|
|
@@ -1574,106 +936,93 @@ function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
|
1574
936
|
return sorted[seqIndex - 1].id;
|
|
1575
937
|
}
|
|
1576
938
|
}
|
|
1577
|
-
|
|
1578
|
-
|
|
939
|
+
return store.get(clean)?.id;
|
|
940
|
+
}
|
|
941
|
+
function getSessionFileSafe(ctx) {
|
|
942
|
+
try {
|
|
943
|
+
const file = ctx?.sessionManager?.getSessionFile?.();
|
|
944
|
+
return typeof file === "string" && file.trim() ? file.trim() : void 0;
|
|
945
|
+
} catch {
|
|
946
|
+
return void 0;
|
|
947
|
+
}
|
|
1579
948
|
}
|
|
1580
949
|
function extension_default(pi) {
|
|
1581
950
|
const store = new AnchorStore();
|
|
1582
|
-
const observer = new SessionTouchObserver();
|
|
1583
951
|
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
1584
|
-
const
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
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 (_) {
|
|
952
|
+
const recentSessionFiles = [];
|
|
953
|
+
function rememberTouchedFile(filePath, cwd) {
|
|
954
|
+
const norm = normalizePath(filePath, cwd);
|
|
955
|
+
if (!norm) return;
|
|
956
|
+
const idx = recentSessionFiles.indexOf(norm);
|
|
957
|
+
if (idx !== -1) recentSessionFiles.splice(idx, 1);
|
|
958
|
+
recentSessionFiles.unshift(norm);
|
|
959
|
+
if (recentSessionFiles.length > 5) recentSessionFiles.pop();
|
|
960
|
+
}
|
|
961
|
+
function createStickyNoteFromUser(title, ctx) {
|
|
962
|
+
const sessionFile = getSessionFileSafe(ctx);
|
|
963
|
+
const anc = store.create({
|
|
964
|
+
title,
|
|
965
|
+
cwd: ctx.cwd,
|
|
966
|
+
sessionFile
|
|
967
|
+
});
|
|
968
|
+
if (anc.files.length === 0 && recentSessionFiles.length > 0) {
|
|
969
|
+
store.update(anc.id, { files: [recentSessionFiles[0]] });
|
|
1629
970
|
}
|
|
1630
971
|
updateAnchorStatusBar(ctx, store);
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
972
|
+
return store.get(anc.id) || anc;
|
|
973
|
+
}
|
|
974
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
975
|
+
annotatedThisSession.clear();
|
|
976
|
+
recentSessionFiles.length = 0;
|
|
977
|
+
sweepQuietNotes(store);
|
|
978
|
+
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));
|
|
979
|
+
for (const q of quietForHere) {
|
|
980
|
+
store.touch(q.id);
|
|
1638
981
|
}
|
|
982
|
+
updateAnchorStatusBar(ctx, store);
|
|
983
|
+
updateStartupBanner(ctx, store);
|
|
1639
984
|
});
|
|
1640
|
-
pi.on("
|
|
985
|
+
pi.on("input", async (event, ctx) => {
|
|
986
|
+
if (event?.source === "extension") return { action: "continue" };
|
|
987
|
+
const quickNote = parseQuickNoteInput(String(event?.text || ""));
|
|
988
|
+
if (!quickNote) return { action: "continue" };
|
|
989
|
+
const anc = createStickyNoteFromUser(quickNote, ctx);
|
|
990
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1641
991
|
if (hasActiveUI(ctx)) {
|
|
1642
|
-
|
|
1643
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1644
|
-
} catch {
|
|
1645
|
-
}
|
|
992
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
1646
993
|
}
|
|
994
|
+
return { action: "handled" };
|
|
1647
995
|
});
|
|
1648
996
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1649
997
|
const entries = ctx.sessionManager?.getEntries() || [];
|
|
1650
998
|
const messageTurns = entries.filter((e) => e.type === "message");
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
return {
|
|
1656
|
-
systemPrompt: `${event.systemPrompt}
|
|
999
|
+
if (messageTurns.length <= 1) {
|
|
1000
|
+
const snippet = renderColdStartAnchorsContext(store, ctx.cwd);
|
|
1001
|
+
if (snippet) {
|
|
1002
|
+
return { systemPrompt: `${event.systemPrompt}
|
|
1657
1003
|
|
|
1658
|
-
${
|
|
1659
|
-
};
|
|
1004
|
+
${snippet}` };
|
|
1660
1005
|
}
|
|
1661
1006
|
}
|
|
1662
1007
|
});
|
|
1663
1008
|
pi.on("tool_result", async (event, ctx) => {
|
|
1009
|
+
if (event.isError) return;
|
|
1664
1010
|
const lowerTool = String(event.toolName || "").toLowerCase();
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1011
|
+
const currentSessionFile = getSessionFileSafe(ctx);
|
|
1012
|
+
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
1013
|
+
const cmd = String(event.input?.command || "");
|
|
1014
|
+
if (cmd.includes("git commit")) {
|
|
1015
|
+
const msgMatch = cmd.match(/-m\s+["']([^"']+)["']/);
|
|
1016
|
+
if (msgMatch && msgMatch[1]) {
|
|
1017
|
+
const commitMsg = msgMatch[1];
|
|
1018
|
+
const candidates = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1019
|
+
for (const a of candidates) {
|
|
1020
|
+
if (matchesCommitMessage(a, [commitMsg]).matched && isAnchorTargetingWorkspace(a, ctx.cwd)) {
|
|
1021
|
+
store.settle(a.id, { settledBy: "commit", summary: commitMsg });
|
|
1022
|
+
updateAnchorStatusBar(ctx, store);
|
|
1023
|
+
if (hasActiveUI(ctx)) {
|
|
1675
1024
|
try {
|
|
1676
|
-
ctx.ui.notify(`\u2316 Auto-
|
|
1025
|
+
ctx.ui.notify(`\u2316 Auto-torn off #${a.id.replace(/^anc-/, "")} ("${a.title}") on git commit`, "info");
|
|
1677
1026
|
} catch {
|
|
1678
1027
|
}
|
|
1679
1028
|
}
|
|
@@ -1684,165 +1033,68 @@ ${contextSnippet}`
|
|
|
1684
1033
|
}
|
|
1685
1034
|
const pathInput = event.input?.path;
|
|
1686
1035
|
if (typeof pathInput !== "string") return;
|
|
1687
|
-
const
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
if (matches.length > 0) {
|
|
1691
|
-
const isMutation = MUTATION_TOOLS2.has((event.toolName || "").toLowerCase());
|
|
1692
|
-
if (isMutation && !event.isError) {
|
|
1693
|
-
for (const m of matches) {
|
|
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 {
|
|
1036
|
+
const isMutation = MUTATION_TOOLS.has(lowerTool);
|
|
1037
|
+
if (isMutation) {
|
|
1038
|
+
rememberTouchedFile(pathInput, ctx.cwd);
|
|
1729
1039
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
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 {
|
|
1040
|
+
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
1041
|
+
const notes = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1042
|
+
const matches = findMatchedAnchors(notes, [touchedPath]);
|
|
1043
|
+
if (matches.length === 0) return;
|
|
1044
|
+
if (isMutation) {
|
|
1045
|
+
for (const m of matches) {
|
|
1046
|
+
store.touch(m.anchor.id, Date.now(), currentSessionFile);
|
|
1744
1047
|
}
|
|
1048
|
+
updateAnchorStatusBar(ctx, store);
|
|
1745
1049
|
}
|
|
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 {
|
|
1050
|
+
const targetNote = matches[0].anchor;
|
|
1051
|
+
if (!annotatedThisSession.has(targetNote.id)) {
|
|
1052
|
+
annotatedThisSession.add(targetNote.id);
|
|
1053
|
+
const whisper = makeSafeTaskAnnotation(touchedPath, targetNote);
|
|
1054
|
+
if (whisper) {
|
|
1055
|
+
const contents = [...event.content || []];
|
|
1056
|
+
for (let i = contents.length - 1; i >= 0; i--) {
|
|
1057
|
+
const item = contents[i];
|
|
1058
|
+
if (item && item.type === "text") {
|
|
1059
|
+
contents[i] = { ...item, text: item.text + whisper };
|
|
1060
|
+
return { content: contents };
|
|
1763
1061
|
}
|
|
1764
1062
|
}
|
|
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
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
continue;
|
|
1782
|
-
}
|
|
1783
|
-
}
|
|
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
1063
|
}
|
|
1800
1064
|
}
|
|
1801
1065
|
});
|
|
1802
1066
|
pi.registerTool({
|
|
1803
1067
|
name: "anchor",
|
|
1804
|
-
label: "Anchor (
|
|
1805
|
-
description: "
|
|
1806
|
-
promptSnippet: "
|
|
1068
|
+
label: "Anchor (Sticky Note)",
|
|
1069
|
+
description: "Pin, list, step, or tear off cross-session sticky notes stored in ~/.anchor/ (zero git repo clutter). Actions: pin (stick a new note), list (view notes), step (advance progress bar), settle (tear off completed note).",
|
|
1070
|
+
promptSnippet: "Pin or tear off cross-session sticky notes that survive terminal restarts",
|
|
1807
1071
|
promptGuidelines: [
|
|
1808
|
-
|
|
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."
|
|
1072
|
+
"Use `anchor` when the user explicitly asks to remember something for later/tomorrow or across sessions.",
|
|
1073
|
+
"BOUNDARY WITH TODO: `todo` is for multi-step work right now in this session; `anchor` is a sticky note for future sessions.",
|
|
1074
|
+
'When a pinned note is completed, call `anchor` with action "settle" to tear it off.'
|
|
1814
1075
|
],
|
|
1815
1076
|
parameters: import_typebox.Type.Object({
|
|
1816
1077
|
action: import_typebox.Type.Union([
|
|
1817
1078
|
import_typebox.Type.Literal("pin"),
|
|
1818
1079
|
import_typebox.Type.Literal("list"),
|
|
1080
|
+
import_typebox.Type.Literal("step"),
|
|
1819
1081
|
import_typebox.Type.Literal("settle"),
|
|
1820
1082
|
import_typebox.Type.Literal("touch"),
|
|
1821
1083
|
import_typebox.Type.Literal("sweep")
|
|
1822
1084
|
]),
|
|
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)" }))
|
|
1085
|
+
title: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Sticky note text (supports [1/4] or 1. A 2. B progress steps)" })),
|
|
1086
|
+
description: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional extra details" })),
|
|
1087
|
+
project: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Optional project name or folder" })),
|
|
1088
|
+
files: import_typebox.Type.Optional(import_typebox.Type.Array(import_typebox.Type.String(), { description: "Optional associated file paths" })),
|
|
1089
|
+
id: import_typebox.Type.Optional(import_typebox.Type.String({ description: "Note ID, e.g. anc-1 or 1 (for step/settle)" }))
|
|
1832
1090
|
}),
|
|
1833
1091
|
renderCall(args, theme) {
|
|
1834
1092
|
const action = args.action || "list";
|
|
1835
1093
|
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
1836
1094
|
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}]`)}`;
|
|
1095
|
+
title += ` "${args.title}"`;
|
|
1096
|
+
} else if ((action === "settle" || action === "touch" || action === "step") && args.id) {
|
|
1097
|
+
title += ` ${theme.fg("dim", `#${String(args.id).replace(/^anc-/, "")}`)}`;
|
|
1846
1098
|
}
|
|
1847
1099
|
return {
|
|
1848
1100
|
render: (width) => [truncateToWidth(title, width)],
|
|
@@ -1851,17 +1103,11 @@ Mark as completed and archive?`
|
|
|
1851
1103
|
};
|
|
1852
1104
|
},
|
|
1853
1105
|
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";
|
|
1106
|
+
const text = result.content?.[0]?.text || (result.isError ? "Failed" : "OK");
|
|
1107
|
+
const color = result.isError ? "error" : "success";
|
|
1108
|
+
const icon = result.isError ? "\u2717" : "\u2714";
|
|
1863
1109
|
return {
|
|
1864
|
-
render: (width) => [truncateToWidth(theme.fg(
|
|
1110
|
+
render: (width) => [truncateToWidth(theme.fg(color, `${icon} ${text}`), width)],
|
|
1865
1111
|
invalidate: () => {
|
|
1866
1112
|
}
|
|
1867
1113
|
};
|
|
@@ -1870,91 +1116,78 @@ Mark as completed and archive?`
|
|
|
1870
1116
|
if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
|
|
1871
1117
|
if (params.action === "pin") {
|
|
1872
1118
|
if (!params.title) {
|
|
1873
|
-
return { content: [{ type: "text", text: "Error: title is required for pin
|
|
1119
|
+
return { content: [{ type: "text", text: "Error: title is required for pin" }], isError: true };
|
|
1874
1120
|
}
|
|
1875
1121
|
const anc = store.create({
|
|
1876
1122
|
title: params.title,
|
|
1877
1123
|
description: params.description,
|
|
1878
|
-
priority: params.priority || "p1",
|
|
1879
1124
|
project: params.project,
|
|
1880
|
-
targetDate: params.targetDate,
|
|
1881
1125
|
cwd: ctx.cwd,
|
|
1882
|
-
files: params.files
|
|
1883
|
-
|
|
1884
|
-
verifyCommand: params.verifyCommand
|
|
1126
|
+
files: params.files?.length ? params.files : recentSessionFiles[0] ? [recentSessionFiles[0]] : [],
|
|
1127
|
+
sessionFile: getSessionFileSafe(ctx)
|
|
1885
1128
|
});
|
|
1886
1129
|
updateAnchorStatusBar(ctx, store);
|
|
1887
1130
|
return {
|
|
1888
|
-
content: [{
|
|
1889
|
-
type: "text",
|
|
1890
|
-
text: `Successfully anchored task #${anc.id}: "${anc.title}" [${anc.project}]. Stored in ~/.anchor/.`
|
|
1891
|
-
}],
|
|
1131
|
+
content: [{ type: "text", text: `Pinned note #${anc.id.replace(/^anc-/, "")}: "${anc.title}"` }],
|
|
1892
1132
|
isError: false
|
|
1893
1133
|
};
|
|
1894
1134
|
}
|
|
1895
1135
|
if (params.action === "list") {
|
|
1896
|
-
const
|
|
1897
|
-
const sleeping = store.list({ status: "sleeping", cwd: ctx.cwd });
|
|
1136
|
+
const notes = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1898
1137
|
return {
|
|
1899
|
-
content: [{
|
|
1900
|
-
type: "text",
|
|
1901
|
-
text: JSON.stringify({ active, sleeping }, null, 2)
|
|
1902
|
-
}],
|
|
1138
|
+
content: [{ type: "text", text: JSON.stringify(notes, null, 2) }],
|
|
1903
1139
|
isError: false
|
|
1904
1140
|
};
|
|
1905
1141
|
}
|
|
1906
|
-
if (params.action === "
|
|
1907
|
-
|
|
1908
|
-
return { content: [{ type: "text", text: "Error: id is required for settle action" }], isError: true };
|
|
1909
|
-
}
|
|
1142
|
+
if (params.action === "step") {
|
|
1143
|
+
const rawId = params.id || "1";
|
|
1910
1144
|
try {
|
|
1911
|
-
const
|
|
1145
|
+
const targetId = resolveAnchorTarget(store, String(rawId), ctx.cwd) || String(rawId);
|
|
1146
|
+
const { anchor: updated } = store.step(targetId, 1);
|
|
1912
1147
|
updateAnchorStatusBar(ctx, store);
|
|
1913
1148
|
return {
|
|
1914
|
-
content: [{
|
|
1915
|
-
type: "text",
|
|
1916
|
-
text: `Anchor #${settled.id} successfully settled and evicted from active context.`
|
|
1917
|
-
}],
|
|
1149
|
+
content: [{ type: "text", text: `Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}` }],
|
|
1918
1150
|
isError: false
|
|
1919
1151
|
};
|
|
1920
1152
|
} catch (err) {
|
|
1921
1153
|
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1922
1154
|
}
|
|
1923
1155
|
}
|
|
1924
|
-
if (params.action === "
|
|
1156
|
+
if (params.action === "settle") {
|
|
1925
1157
|
if (!params.id) {
|
|
1926
|
-
return { content: [{ type: "text", text: "Error: id is required for
|
|
1158
|
+
return { content: [{ type: "text", text: "Error: id is required for settle" }], isError: true };
|
|
1927
1159
|
}
|
|
1928
1160
|
try {
|
|
1929
|
-
const
|
|
1161
|
+
const targetId = resolveAnchorTarget(store, String(params.id), ctx.cwd) || String(params.id);
|
|
1162
|
+
const settled = store.settle(targetId, { settledBy: "manual" });
|
|
1930
1163
|
updateAnchorStatusBar(ctx, store);
|
|
1931
1164
|
return {
|
|
1932
|
-
content: [{
|
|
1933
|
-
type: "text",
|
|
1934
|
-
text: `Anchor #${touched.id} touched and decay timer refreshed.`
|
|
1935
|
-
}],
|
|
1165
|
+
content: [{ type: "text", text: `Torn off note #${settled.id.replace(/^anc-/, "")}: "${settled.title}"` }],
|
|
1936
1166
|
isError: false
|
|
1937
1167
|
};
|
|
1938
1168
|
} catch (err) {
|
|
1939
1169
|
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1940
1170
|
}
|
|
1941
1171
|
}
|
|
1172
|
+
if (params.action === "touch" && params.id) {
|
|
1173
|
+
try {
|
|
1174
|
+
const touched = store.touch(String(params.id), Date.now(), getSessionFileSafe(ctx));
|
|
1175
|
+
updateAnchorStatusBar(ctx, store);
|
|
1176
|
+
return { content: [{ type: "text", text: `Refreshed note #${touched.id}` }], isError: false };
|
|
1177
|
+
} catch (err) {
|
|
1178
|
+
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1942
1181
|
if (params.action === "sweep") {
|
|
1943
|
-
const res =
|
|
1182
|
+
const res = sweepQuietNotes(store);
|
|
1944
1183
|
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
|
-
};
|
|
1184
|
+
return { content: [{ type: "text", text: `Quieted ${res.quieted.length} old note(s).` }], isError: false };
|
|
1952
1185
|
}
|
|
1953
1186
|
return { content: [{ type: "text", text: "Unknown action" }], isError: true };
|
|
1954
1187
|
}
|
|
1955
1188
|
});
|
|
1956
1189
|
pi.registerCommand("anchor", {
|
|
1957
|
-
description: "
|
|
1190
|
+
description: "View, pin, step, jump to session, or tear off sticky notes (/anchor, /anchor step 1, /anchor jump 1, /anchor done 1)",
|
|
1958
1191
|
handler: async (args, ctx) => {
|
|
1959
1192
|
const parsed = parseAnchorCommandInput(args);
|
|
1960
1193
|
if (parsed.mode === "dashboard") {
|
|
@@ -1964,21 +1197,42 @@ Mark as completed and archive?`
|
|
|
1964
1197
|
if (parsed.mode === "undo") {
|
|
1965
1198
|
try {
|
|
1966
1199
|
const restored = store.undoSettle();
|
|
1967
|
-
ctx.ui.notify(`\u2316
|
|
1200
|
+
ctx.ui.notify(`\u2316 Put back #${restored.id.replace(/^anc-/, "")}: "${restored.title}"`, "info");
|
|
1968
1201
|
updateAnchorStatusBar(ctx, store);
|
|
1969
1202
|
} catch (err) {
|
|
1970
|
-
ctx.ui.notify(`
|
|
1203
|
+
ctx.ui.notify(`Undo failed: ${err.message}`, "error");
|
|
1204
|
+
}
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
if (parsed.mode === "step" && parsed.targetId) {
|
|
1208
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1209
|
+
if (!resolvedId) {
|
|
1210
|
+
ctx.ui.notify(`\u2316 Note not found: ${parsed.targetId}`, "error");
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const { anchor: updated } = store.step(resolvedId, parsed.delta || 1);
|
|
1214
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
1215
|
+
updateAnchorStatusBar(ctx, store);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (parsed.mode === "jump" && parsed.targetId) {
|
|
1219
|
+
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1220
|
+
const note = resolvedId ? store.get(resolvedId) : void 0;
|
|
1221
|
+
if (!note || !note.sessionFile || !import_node_fs3.default.existsSync(note.sessionFile)) {
|
|
1222
|
+
ctx.ui.notify(`\u2316 No saved conversation session found for #${parsed.targetId}`, "error");
|
|
1223
|
+
return;
|
|
1971
1224
|
}
|
|
1225
|
+
await ctx.switchSession(note.sessionFile);
|
|
1972
1226
|
return;
|
|
1973
1227
|
}
|
|
1974
1228
|
if (parsed.mode === "settle" && parsed.targetId) {
|
|
1975
1229
|
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
1976
1230
|
if (!resolvedId) {
|
|
1977
|
-
ctx.ui.notify(`\u2316
|
|
1231
|
+
ctx.ui.notify(`\u2316 Note not found: ${parsed.targetId}`, "error");
|
|
1978
1232
|
return;
|
|
1979
1233
|
}
|
|
1980
|
-
const settled = store.settle(resolvedId, { settledBy: "manual
|
|
1981
|
-
ctx.ui.notify(`\u2316
|
|
1234
|
+
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
1235
|
+
ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
|
|
1982
1236
|
updateAnchorStatusBar(ctx, store);
|
|
1983
1237
|
return;
|
|
1984
1238
|
}
|
|
@@ -1986,40 +1240,48 @@ Mark as completed and archive?`
|
|
|
1986
1240
|
if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
|
|
1987
1241
|
const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
|
|
1988
1242
|
if (resolvedId) {
|
|
1989
|
-
const settled = store.settle(resolvedId, { settledBy: "manual
|
|
1990
|
-
ctx.ui.notify(`\u2316
|
|
1243
|
+
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
1244
|
+
ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
|
|
1991
1245
|
updateAnchorStatusBar(ctx, store);
|
|
1992
1246
|
return;
|
|
1993
1247
|
}
|
|
1994
1248
|
}
|
|
1995
|
-
const anc =
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
cwd: ctx.cwd
|
|
1999
|
-
});
|
|
2000
|
-
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
|
|
2001
|
-
updateAnchorStatusBar(ctx, store);
|
|
1249
|
+
const anc = createStickyNoteFromUser(parsed.title || rawTrimmed, ctx);
|
|
1250
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1251
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
2002
1252
|
}
|
|
2003
1253
|
});
|
|
2004
1254
|
pi.registerCommand("pin", {
|
|
2005
|
-
description: "
|
|
1255
|
+
description: "Stick a quick note across sessions (e.g. /pin Migrate auth [1/3])",
|
|
2006
1256
|
handler: async (args, ctx) => {
|
|
2007
|
-
const
|
|
2008
|
-
if (
|
|
1257
|
+
const text = (args || "").trim();
|
|
1258
|
+
if (!text) {
|
|
2009
1259
|
await openAnchorDashboard(ctx, store);
|
|
2010
1260
|
return;
|
|
2011
1261
|
}
|
|
2012
|
-
const anc =
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
1262
|
+
const anc = createStickyNoteFromUser(text, ctx);
|
|
1263
|
+
const bar = anc.progress ? ` ${formatProgressBar(anc.progress)}` : "";
|
|
1264
|
+
ctx.ui.notify(`\u2316 Pinned #${anc.id.replace(/^anc-/, "")}${bar}: "${anc.title}"`, "info");
|
|
1265
|
+
}
|
|
1266
|
+
});
|
|
1267
|
+
pi.registerCommand("step", {
|
|
1268
|
+
description: "Advance progress bar on a sticky note (e.g. /step 1)",
|
|
1269
|
+
handler: async (args, ctx) => {
|
|
1270
|
+
const tokens = (args || "").trim().split(/\s+/).filter(Boolean);
|
|
1271
|
+
const target = tokens[0] || "1";
|
|
1272
|
+
const delta = tokens[1] ? parseInt(tokens[1], 10) : 1;
|
|
1273
|
+
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
1274
|
+
if (!resolvedId) {
|
|
1275
|
+
ctx.ui.notify(`\u2316 Note not found: ${target}`, "error");
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const { anchor: updated } = store.step(resolvedId, isNaN(delta) ? 1 : delta);
|
|
1279
|
+
ctx.ui.notify(`\u2316 Advanced #${updated.id.replace(/^anc-/, "")} to ${formatProgressBar(updated.progress)}`, "info");
|
|
2018
1280
|
updateAnchorStatusBar(ctx, store);
|
|
2019
1281
|
}
|
|
2020
1282
|
});
|
|
2021
1283
|
pi.registerCommand("done", {
|
|
2022
|
-
description: "
|
|
1284
|
+
description: "Tear off a sticky note by number or ID (e.g. /done 1)",
|
|
2023
1285
|
handler: async (args, ctx) => {
|
|
2024
1286
|
const target = (args || "").trim();
|
|
2025
1287
|
if (!target) {
|
|
@@ -2028,16 +1290,16 @@ Mark as completed and archive?`
|
|
|
2028
1290
|
}
|
|
2029
1291
|
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
2030
1292
|
if (!resolvedId) {
|
|
2031
|
-
ctx.ui.notify(`\u2316
|
|
1293
|
+
ctx.ui.notify(`\u2316 Note not found: ${target}`, "error");
|
|
2032
1294
|
return;
|
|
2033
1295
|
}
|
|
2034
|
-
const settled = store.settle(resolvedId, { settledBy: "manual
|
|
2035
|
-
ctx.ui.notify(`\u2316
|
|
1296
|
+
const settled = store.settle(resolvedId, { settledBy: "manual" });
|
|
1297
|
+
ctx.ui.notify(`\u2316 Torn off #${settled.id.replace(/^anc-/, "")}: "${settled.title}" (/anchor undo to put back)`, "info");
|
|
2036
1298
|
updateAnchorStatusBar(ctx, store);
|
|
2037
1299
|
}
|
|
2038
1300
|
});
|
|
2039
1301
|
pi.registerCommand("anchors", {
|
|
2040
|
-
description: "
|
|
1302
|
+
description: "View sticky notes",
|
|
2041
1303
|
handler: async (_args, ctx) => {
|
|
2042
1304
|
await openAnchorDashboard(ctx, store);
|
|
2043
1305
|
}
|
|
@@ -2046,5 +1308,6 @@ Mark as completed and archive?`
|
|
|
2046
1308
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2047
1309
|
0 && (module.exports = {
|
|
2048
1310
|
parseAnchorCommandInput,
|
|
1311
|
+
parseQuickNoteInput,
|
|
2049
1312
|
resolveAnchorTarget
|
|
2050
1313
|
});
|