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