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