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