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