pi-anchor 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -170
- package/README_zh.md +32 -176
- package/assets/hero-en.svg +23 -39
- package/assets/hero-zh.svg +23 -39
- package/bin/anchor.js +36 -69
- package/dist/chunk-LKY6WIKH.js +1100 -0
- package/dist/extension-B1-fzCox.d.cts +167 -0
- package/dist/extension-B1-fzCox.d.ts +167 -0
- package/dist/extension.cjs +369 -1315
- package/dist/extension.d.cts +2 -20
- package/dist/extension.d.ts +2 -20
- package/dist/extension.js +4 -536
- package/dist/index.cjs +317 -1143
- package/dist/index.d.cts +17 -250
- package/dist/index.d.ts +17 -250
- package/dist/index.js +15 -103
- package/package.json +2 -2
- package/src/extension.ts +124 -362
- package/src/index.ts +1 -6
- package/src/store.ts +294 -424
- package/src/tui.ts +102 -297
- package/src/types.ts +24 -65
- package/dist/chunk-Q7IILJFA.js +0 -1563
- package/dist/store-t2_TDhJn.d.cts +0 -210
- package/dist/store-t2_TDhJn.d.ts +0 -210
- package/src/context_injector.ts +0 -166
- package/src/decay.ts +0 -125
- package/src/matcher.ts +0 -202
- package/src/observer.ts +0 -141
- package/src/protocol.ts +0 -95
- package/src/settlement.ts +0 -140
package/dist/extension.d.cts
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Parse inline `--due <date>` / `-d <date>` flags and `done`/`rm`/`close`/`settle` subcommands
|
|
6
|
-
* for `/anchor` and `/pin` slash commands.
|
|
7
|
-
*/
|
|
8
|
-
declare function parseAnchorCommandInput(raw: string, now?: number): {
|
|
9
|
-
mode: 'dashboard' | 'undo' | 'settle' | 'pin';
|
|
10
|
-
targetId?: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
targetDate?: string;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Resolve either an explicit anchor ID ('anc-2', '2') or a 1-based visual dashboard index ('01', '1')
|
|
16
|
-
*/
|
|
17
|
-
declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
|
|
18
|
-
declare function export_default(pi: ExtensionAPI): void;
|
|
19
|
-
|
|
20
|
-
export { export_default as default, parseAnchorCommandInput, resolveAnchorTarget };
|
|
1
|
+
import '@earendil-works/pi-coding-agent';
|
|
2
|
+
export { w as default, r as parseAnchorCommandInput, u as resolveAnchorTarget } from './extension-B1-fzCox.cjs';
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,20 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Parse inline `--due <date>` / `-d <date>` flags and `done`/`rm`/`close`/`settle` subcommands
|
|
6
|
-
* for `/anchor` and `/pin` slash commands.
|
|
7
|
-
*/
|
|
8
|
-
declare function parseAnchorCommandInput(raw: string, now?: number): {
|
|
9
|
-
mode: 'dashboard' | 'undo' | 'settle' | 'pin';
|
|
10
|
-
targetId?: string;
|
|
11
|
-
title?: string;
|
|
12
|
-
targetDate?: string;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Resolve either an explicit anchor ID ('anc-2', '2') or a 1-based visual dashboard index ('01', '1')
|
|
16
|
-
*/
|
|
17
|
-
declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
|
|
18
|
-
declare function export_default(pi: ExtensionAPI): void;
|
|
19
|
-
|
|
20
|
-
export { export_default as default, parseAnchorCommandInput, resolveAnchorTarget };
|
|
1
|
+
import '@earendil-works/pi-coding-agent';
|
|
2
|
+
export { w as default, r as parseAnchorCommandInput, u as resolveAnchorTarget } from './extension-B1-fzCox.js';
|
package/dist/extension.js
CHANGED
|
@@ -1,540 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
generateSettlementProposals,
|
|
7
|
-
hasActiveUI,
|
|
8
|
-
isAnchorTargetingWorkspace,
|
|
9
|
-
isSameOrSubPath,
|
|
10
|
-
makeSafeTaskAnnotation,
|
|
11
|
-
normalizePath,
|
|
12
|
-
openAnchorDashboard,
|
|
13
|
-
renderColdStartAnchorsContext,
|
|
14
|
-
runPhysicalVerification,
|
|
15
|
-
sortAnchorsForDisplay,
|
|
16
|
-
sweepStore,
|
|
17
|
-
truncateToWidth,
|
|
18
|
-
tryRealtimeAutoSettle,
|
|
19
|
-
updateAnchorStatusBar,
|
|
20
|
-
updateStartupBanner
|
|
21
|
-
} from "./chunk-Q7IILJFA.js";
|
|
22
|
-
|
|
23
|
-
// src/extension.ts
|
|
24
|
-
import { Type } from "@sinclair/typebox";
|
|
25
|
-
import { execSync } from "child_process";
|
|
26
|
-
import fs from "fs";
|
|
27
|
-
import path from "path";
|
|
28
|
-
var MUTATION_TOOLS = /* @__PURE__ */ new Set(["edit", "write", "patch", "apply_diff", "create_file", "modify"]);
|
|
29
|
-
function parseAnchorCommandInput(raw, now = Date.now()) {
|
|
30
|
-
const trimmed = (raw || "").trim();
|
|
31
|
-
if (!trimmed || trimmed === "list" || trimmed === "ls") {
|
|
32
|
-
return { mode: "dashboard" };
|
|
33
|
-
}
|
|
34
|
-
if (trimmed === "undo") {
|
|
35
|
-
return { mode: "undo" };
|
|
36
|
-
}
|
|
37
|
-
const settleMatch = trimmed.match(/^(?:done|rm|close|settle)\s+(.+)$/i);
|
|
38
|
-
if (settleMatch) {
|
|
39
|
-
return {
|
|
40
|
-
mode: "settle",
|
|
41
|
-
targetId: settleMatch[1].trim().replace(/^#/, "")
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
let targetDate;
|
|
45
|
-
const tokens = trimmed.split(/\s+/);
|
|
46
|
-
const dueIdx = tokens.findIndex((t) => t === "--due" || t === "-d");
|
|
47
|
-
if (dueIdx !== -1 && tokens[dueIdx + 1]) {
|
|
48
|
-
const rawDue = tokens[dueIdx + 1];
|
|
49
|
-
targetDate = detectTargetDate(rawDue, now) || rawDue;
|
|
50
|
-
tokens.splice(dueIdx, 2);
|
|
51
|
-
}
|
|
52
|
-
const cleanTitle = tokens.join(" ").trim();
|
|
53
|
-
return {
|
|
54
|
-
mode: "pin",
|
|
55
|
-
title: cleanTitle || trimmed,
|
|
56
|
-
targetDate
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
function resolveAnchorTarget(store, idOrIndex, cwd) {
|
|
60
|
-
const clean = idOrIndex.trim().replace(/^#/, "");
|
|
61
|
-
if (/^\d+$/.test(clean)) {
|
|
62
|
-
const seqIndex = parseInt(clean, 10);
|
|
63
|
-
const visible = store.list({ cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
64
|
-
const sorted = sortAnchorsForDisplay(visible, cwd);
|
|
65
|
-
if (seqIndex >= 1 && seqIndex <= sorted.length) {
|
|
66
|
-
return sorted[seqIndex - 1].id;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const direct = store.get(clean);
|
|
70
|
-
return direct?.id;
|
|
71
|
-
}
|
|
72
|
-
function extension_default(pi) {
|
|
73
|
-
const store = new AnchorStore();
|
|
74
|
-
const observer = new SessionTouchObserver();
|
|
75
|
-
const annotatedThisSession = /* @__PURE__ */ new Set();
|
|
76
|
-
const preflightFailures = /* @__PURE__ */ new Map();
|
|
77
|
-
pi.on("session_start", async (event, ctx) => {
|
|
78
|
-
observer.clear();
|
|
79
|
-
annotatedThisSession.clear();
|
|
80
|
-
preflightFailures.clear();
|
|
81
|
-
const sweep = sweepStore(store);
|
|
82
|
-
const sleepingForWorkspace = store.list({ status: "sleeping", all: true }).filter((a) => (a.files.length > 0 || a.project && a.project !== "global" || a.cwd) && isAnchorTargetingWorkspace(a, ctx.cwd));
|
|
83
|
-
for (const sa of sleepingForWorkspace) {
|
|
84
|
-
store.touch(sa.id);
|
|
85
|
-
}
|
|
86
|
-
if (sweep.transitionedToSleeping.length > 0 && sleepingForWorkspace.length === 0) {
|
|
87
|
-
ctx.ui.notify(`\u2316 Anchor: ${sweep.transitionedToSleeping.length} inactive task(s) moved to quiet mode (/anchor to view)`, "info");
|
|
88
|
-
}
|
|
89
|
-
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
90
|
-
for (const a of activeAnchors) {
|
|
91
|
-
if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
|
|
92
|
-
try {
|
|
93
|
-
const res = runPhysicalVerification(a, ctx.cwd);
|
|
94
|
-
if (res.success) {
|
|
95
|
-
store.settle(a.id, {
|
|
96
|
-
settledBy: "verification-test",
|
|
97
|
-
summary: `Physical pre-flight test passed: ${a.verifyCommand}`
|
|
98
|
-
});
|
|
99
|
-
ctx.ui.notify(`\u2316 Verification passed! Auto-settled #${a.id}: "${a.title}"`, "info");
|
|
100
|
-
} else {
|
|
101
|
-
preflightFailures.set(a.id, res.output.slice(-250));
|
|
102
|
-
}
|
|
103
|
-
} catch (_) {
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
109
|
-
const pendingPath = path.join(home, ".anchor", "pending_exit.json");
|
|
110
|
-
if (fs.existsSync(pendingPath)) {
|
|
111
|
-
const pending = JSON.parse(fs.readFileSync(pendingPath, "utf-8"));
|
|
112
|
-
const isExpired = Date.now() - pending.timestamp >= 24 * 60 * 60 * 1e3;
|
|
113
|
-
if (!isExpired && isSameOrSubPath(pending.cwd, ctx.cwd)) {
|
|
114
|
-
ctx.ui.notify(`\u2316 [Exit Guard] Detected ${pending.modifiedCount} uncommitted changes from previous session exit. Run /pin to record.`, "info");
|
|
115
|
-
fs.unlinkSync(pendingPath);
|
|
116
|
-
} else if (isExpired) {
|
|
117
|
-
fs.unlinkSync(pendingPath);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
} catch (_) {
|
|
121
|
-
}
|
|
122
|
-
updateAnchorStatusBar(ctx, store);
|
|
123
|
-
if (event.reason !== "resume") {
|
|
124
|
-
updateStartupBanner(ctx, store);
|
|
125
|
-
} else if (hasActiveUI(ctx)) {
|
|
126
|
-
try {
|
|
127
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
128
|
-
} catch {
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
pi.on("agent_start", async (_event, ctx) => {
|
|
133
|
-
if (hasActiveUI(ctx)) {
|
|
134
|
-
try {
|
|
135
|
-
ctx.ui.setWidget("anchor-startup", void 0);
|
|
136
|
-
} catch {
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
pi.on("before_agent_start", async (event, ctx) => {
|
|
141
|
-
const entries = ctx.sessionManager?.getEntries() || [];
|
|
142
|
-
const messageTurns = entries.filter((e) => e.type === "message");
|
|
143
|
-
const isColdStart = messageTurns.length <= 1;
|
|
144
|
-
if (isColdStart) {
|
|
145
|
-
const contextSnippet = renderColdStartAnchorsContext(store, ctx.cwd, void 0, preflightFailures);
|
|
146
|
-
if (contextSnippet) {
|
|
147
|
-
return {
|
|
148
|
-
systemPrompt: `${event.systemPrompt}
|
|
149
|
-
|
|
150
|
-
${contextSnippet}`
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
});
|
|
155
|
-
pi.on("tool_result", async (event, ctx) => {
|
|
156
|
-
const lowerTool = String(event.toolName || "").toLowerCase();
|
|
157
|
-
if (!event.isError) {
|
|
158
|
-
observer.recordToolCall(event.toolName, event.input || {});
|
|
159
|
-
if (lowerTool === "bash" || lowerTool === "powershell") {
|
|
160
|
-
const executedCmd = String(event.input?.command || "");
|
|
161
|
-
if (executedCmd.includes("git commit") || executedCmd.length > 0) {
|
|
162
|
-
const settledNow = tryRealtimeAutoSettle(store, observer, ctx.cwd, executedCmd);
|
|
163
|
-
if (settledNow.length > 0) {
|
|
164
|
-
updateAnchorStatusBar(ctx, store);
|
|
165
|
-
if (hasActiveUI(ctx)) {
|
|
166
|
-
for (const item of settledNow) {
|
|
167
|
-
try {
|
|
168
|
-
ctx.ui.notify(`\u2316 Auto-settled #${item.anchor.id} ("${item.anchor.title}") via ${item.reason}`, "info");
|
|
169
|
-
} catch {
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
const pathInput = event.input?.path;
|
|
178
|
-
if (typeof pathInput !== "string") return;
|
|
179
|
-
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
180
|
-
const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
181
|
-
const matches = findMatchedAnchors(anchors, [touchedPath]);
|
|
182
|
-
if (matches.length > 0) {
|
|
183
|
-
const isMutation = MUTATION_TOOLS.has((event.toolName || "").toLowerCase());
|
|
184
|
-
if (isMutation && !event.isError) {
|
|
185
|
-
for (const m of matches) {
|
|
186
|
-
store.touch(m.anchor.id);
|
|
187
|
-
}
|
|
188
|
-
updateAnchorStatusBar(ctx, store);
|
|
189
|
-
}
|
|
190
|
-
const a = matches[0].anchor;
|
|
191
|
-
if (!annotatedThisSession.has(a.id)) {
|
|
192
|
-
annotatedThisSession.add(a.id);
|
|
193
|
-
const alert = makeSafeTaskAnnotation(touchedPath, a);
|
|
194
|
-
if (alert) {
|
|
195
|
-
const contents = [...event.content || []];
|
|
196
|
-
for (let i = contents.length - 1; i >= 0; i--) {
|
|
197
|
-
const item = contents[i];
|
|
198
|
-
if (item && item.type === "text") {
|
|
199
|
-
contents[i] = { ...item, text: item.text + alert };
|
|
200
|
-
return { content: contents };
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
pi.on("session_shutdown", async (event, ctx) => {
|
|
208
|
-
let gitStatus = "";
|
|
209
|
-
try {
|
|
210
|
-
gitStatus = execSync("git status --porcelain", {
|
|
211
|
-
cwd: ctx.cwd,
|
|
212
|
-
encoding: "utf-8",
|
|
213
|
-
timeout: 2e3,
|
|
214
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
215
|
-
}).replace(/[\r\n]+$/, "");
|
|
216
|
-
const changed = gitStatus.split(/\r?\n/).filter(Boolean).map((l) => l.slice(3).replace(/^"(.*)"$/, "$1").trim()).filter(Boolean);
|
|
217
|
-
for (const f of changed) {
|
|
218
|
-
observer.addTouchedFile(f);
|
|
219
|
-
}
|
|
220
|
-
} catch {
|
|
221
|
-
}
|
|
222
|
-
if (event?.reason === "quit" && gitStatus) {
|
|
223
|
-
try {
|
|
224
|
-
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
225
|
-
if (activeAnchors.length === 0) {
|
|
226
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
227
|
-
const pendingPath = path.join(home, ".anchor", "pending_exit.json");
|
|
228
|
-
const data = {
|
|
229
|
-
cwd: ctx.cwd,
|
|
230
|
-
timestamp: Date.now(),
|
|
231
|
-
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
232
|
-
};
|
|
233
|
-
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
234
|
-
}
|
|
235
|
-
} catch {
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
const modified = observer.getModifiedFiles();
|
|
239
|
-
if (modified.length === 0 && !observer.hasCommitted()) return;
|
|
240
|
-
const proposals = generateSettlementProposals(store, modified, ctx.cwd, observer);
|
|
241
|
-
if (proposals.length === 0) return;
|
|
242
|
-
for (const prop of proposals) {
|
|
243
|
-
const a = prop.anchor;
|
|
244
|
-
const commitMatch = observer.matchesCommit(a);
|
|
245
|
-
if (commitMatch.matched) {
|
|
246
|
-
store.settle(a.id, {
|
|
247
|
-
settledBy: "verification-test",
|
|
248
|
-
summary: `Auto-settled via Git commit: ${commitMatch.message}`,
|
|
249
|
-
touchedFiles: prop.matchedFiles
|
|
250
|
-
});
|
|
251
|
-
if (hasActiveUI(ctx)) {
|
|
252
|
-
try {
|
|
253
|
-
ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
|
|
254
|
-
} catch {
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (a.verifyCommand) {
|
|
260
|
-
const verifyRes = runPhysicalVerification(a, ctx.cwd);
|
|
261
|
-
if (verifyRes.success) {
|
|
262
|
-
store.settle(a.id, {
|
|
263
|
-
settledBy: "verification-test",
|
|
264
|
-
summary: `Automated test passed: ${a.verifyCommand}`,
|
|
265
|
-
touchedFiles: prop.matchedFiles
|
|
266
|
-
});
|
|
267
|
-
if (hasActiveUI(ctx)) {
|
|
268
|
-
try {
|
|
269
|
-
ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
|
|
270
|
-
} catch {
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
if (!hasActiveUI(ctx)) continue;
|
|
277
|
-
try {
|
|
278
|
-
const ok = await ctx.ui.confirm(
|
|
279
|
-
"\u2316 Settle Anchor Task",
|
|
280
|
-
`Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
|
|
281
|
-
Mark as completed and archive?`
|
|
282
|
-
);
|
|
283
|
-
if (ok) {
|
|
284
|
-
store.settle(a.id, {
|
|
285
|
-
settledBy: "one-tap-settlement",
|
|
286
|
-
touchedFiles: prop.matchedFiles
|
|
287
|
-
});
|
|
288
|
-
ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
|
|
289
|
-
}
|
|
290
|
-
} catch {
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
pi.registerTool({
|
|
295
|
-
name: "anchor",
|
|
296
|
-
label: "Anchor (Cross-session Task Protocol)",
|
|
297
|
-
description: "Manage cross-session persistent tasks that survive terminal restarts and auto-evict upon code changes or settlement. Use when the user asks to retain, pin, remember, or track a multi-session goal across sessions, or when an ongoing commitment must not be forgotten. Actions: pin (create new cross-session anchor), list (view active and sleeping anchors), settle (close and archive a completed anchor), touch (refresh activity), sweep (run decay cleanup). Stored in ~/.anchor/ with zero project repository pollution.",
|
|
298
|
-
promptSnippet: "Anchor cross-session task contracts that survive terminal restarts and auto-evict",
|
|
299
|
-
promptGuidelines: [
|
|
300
|
-
'Use `anchor` when the user asks to retain a goal across sessions or record a reminder for later/tonight/tomorrow (e.g. "\u665A\u4E0A\u6E05\u7406\u5783\u573E", "\u660E\u5929\u4F18\u5316X", "\u4FDD\u7559\u4EFB\u52A1\u76F4\u5230\u5B8C\u6210").',
|
|
301
|
-
"BOUNDARY WITH TODO: `todo` is strictly for intra-session active work breakdown (step 1, step 2, step 3 right now). For future reminders or cross-session goals, ONLY use `anchor`. NEVER duplicate a cross-session reminder into both `todo` and `anchor`.",
|
|
302
|
-
"DO NOT over-engineer or assume automated scheduled tasks unless the user explicitly requests Windows Task Scheduler or cron.",
|
|
303
|
-
"Tasks are automatically scoped to the current project context without cluttering the project git repository.",
|
|
304
|
-
'When code for an anchor is completed and verified, call `anchor` with action "settle" to archive it and free context.',
|
|
305
|
-
"Active anchors are automatically injected into future sessions in an ultra-compact block."
|
|
306
|
-
],
|
|
307
|
-
parameters: Type.Object({
|
|
308
|
-
action: Type.Union([
|
|
309
|
-
Type.Literal("pin"),
|
|
310
|
-
Type.Literal("list"),
|
|
311
|
-
Type.Literal("settle"),
|
|
312
|
-
Type.Literal("touch"),
|
|
313
|
-
Type.Literal("sweep")
|
|
314
|
-
]),
|
|
315
|
-
title: Type.Optional(Type.String({ description: "Short imperative task title (for pin)" })),
|
|
316
|
-
description: Type.Optional(Type.String({ description: "Detailed context, acceptance criteria, or technical notes" })),
|
|
317
|
-
priority: Type.Optional(Type.Union([Type.Literal("p0"), Type.Literal("p1"), Type.Literal("p2")])),
|
|
318
|
-
project: Type.Optional(Type.String({ description: "Target project name or workspace (defaults to current directory if omitted)" })),
|
|
319
|
-
targetDate: Type.Optional(Type.String({ description: "Expected completion date (e.g. YYYY-MM-DD, today, tomorrow)" })),
|
|
320
|
-
files: Type.Optional(Type.Array(Type.String(), { description: "Associated file paths or directory prefixes" })),
|
|
321
|
-
tags: Type.Optional(Type.Array(Type.String(), { description: "Domain tags" })),
|
|
322
|
-
verifyCommand: Type.Optional(Type.String({ description: "Optional shell command for automated physical verification" })),
|
|
323
|
-
id: Type.Optional(Type.String({ description: "Anchor ID, e.g. anc-1 (for settle or touch)" }))
|
|
324
|
-
}),
|
|
325
|
-
renderCall(args, theme) {
|
|
326
|
-
const action = args.action || "list";
|
|
327
|
-
let title = theme.fg("toolTitle", theme.bold("\u2316 anchor ")) + theme.fg("accent", action);
|
|
328
|
-
if (action === "pin" && args.title) {
|
|
329
|
-
const due = args.targetDate ? ` ${theme.fg("dim", `[${args.targetDate}]`)}` : "";
|
|
330
|
-
const files = args.files && args.files.length > 0 ? ` ${theme.fg("muted", `(${args.files.slice(0, 2).join(", ")})`)}` : "";
|
|
331
|
-
title += ` "${args.title}"${due}${files}`;
|
|
332
|
-
} else if (action === "settle" && args.id) {
|
|
333
|
-
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
334
|
-
} else if (action === "touch" && args.id) {
|
|
335
|
-
title += ` ${theme.fg("dim", `#${args.id}`)}`;
|
|
336
|
-
} else if (action === "list") {
|
|
337
|
-
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
338
|
-
}
|
|
339
|
-
return {
|
|
340
|
-
render: (width) => [truncateToWidth(title, width)],
|
|
341
|
-
invalidate: () => {
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
},
|
|
345
|
-
renderResult(result, _options, theme) {
|
|
346
|
-
if (result.isError) {
|
|
347
|
-
const errText = result.content?.[0]?.text || "Execution failed";
|
|
348
|
-
return {
|
|
349
|
-
render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
|
|
350
|
-
invalidate: () => {
|
|
351
|
-
}
|
|
352
|
-
};
|
|
353
|
-
}
|
|
354
|
-
const text = result.content?.[0]?.text || "OK";
|
|
355
|
-
return {
|
|
356
|
-
render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
|
|
357
|
-
invalidate: () => {
|
|
358
|
-
}
|
|
359
|
-
};
|
|
360
|
-
},
|
|
361
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
362
|
-
if (!ctx) return { content: [{ type: "text", text: "Error: context required" }], isError: true };
|
|
363
|
-
if (params.action === "pin") {
|
|
364
|
-
if (!params.title) {
|
|
365
|
-
return { content: [{ type: "text", text: "Error: title is required for pin action" }], isError: true };
|
|
366
|
-
}
|
|
367
|
-
const anc = store.create({
|
|
368
|
-
title: params.title,
|
|
369
|
-
description: params.description,
|
|
370
|
-
priority: params.priority || "p1",
|
|
371
|
-
project: params.project,
|
|
372
|
-
targetDate: params.targetDate,
|
|
373
|
-
cwd: ctx.cwd,
|
|
374
|
-
files: params.files || [],
|
|
375
|
-
tags: params.tags || [],
|
|
376
|
-
verifyCommand: params.verifyCommand
|
|
377
|
-
});
|
|
378
|
-
updateAnchorStatusBar(ctx, store);
|
|
379
|
-
return {
|
|
380
|
-
content: [{
|
|
381
|
-
type: "text",
|
|
382
|
-
text: `Successfully anchored task #${anc.id}: "${anc.title}" [${anc.project}]. Stored in ~/.anchor/.`
|
|
383
|
-
}],
|
|
384
|
-
isError: false
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
if (params.action === "list") {
|
|
388
|
-
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
389
|
-
const sleeping = store.list({ status: "sleeping", cwd: ctx.cwd });
|
|
390
|
-
return {
|
|
391
|
-
content: [{
|
|
392
|
-
type: "text",
|
|
393
|
-
text: JSON.stringify({ active, sleeping }, null, 2)
|
|
394
|
-
}],
|
|
395
|
-
isError: false
|
|
396
|
-
};
|
|
397
|
-
}
|
|
398
|
-
if (params.action === "settle") {
|
|
399
|
-
if (!params.id) {
|
|
400
|
-
return { content: [{ type: "text", text: "Error: id is required for settle action" }], isError: true };
|
|
401
|
-
}
|
|
402
|
-
try {
|
|
403
|
-
const settled = store.settle(params.id, { settledBy: "verification-test" });
|
|
404
|
-
updateAnchorStatusBar(ctx, store);
|
|
405
|
-
return {
|
|
406
|
-
content: [{
|
|
407
|
-
type: "text",
|
|
408
|
-
text: `Anchor #${settled.id} successfully settled and evicted from active context.`
|
|
409
|
-
}],
|
|
410
|
-
isError: false
|
|
411
|
-
};
|
|
412
|
-
} catch (err) {
|
|
413
|
-
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (params.action === "touch") {
|
|
417
|
-
if (!params.id) {
|
|
418
|
-
return { content: [{ type: "text", text: "Error: id is required for touch action" }], isError: true };
|
|
419
|
-
}
|
|
420
|
-
try {
|
|
421
|
-
const touched = store.touch(params.id);
|
|
422
|
-
updateAnchorStatusBar(ctx, store);
|
|
423
|
-
return {
|
|
424
|
-
content: [{
|
|
425
|
-
type: "text",
|
|
426
|
-
text: `Anchor #${touched.id} touched and decay timer refreshed.`
|
|
427
|
-
}],
|
|
428
|
-
isError: false
|
|
429
|
-
};
|
|
430
|
-
} catch (err) {
|
|
431
|
-
return { content: [{ type: "text", text: err.message }], isError: true };
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
if (params.action === "sweep") {
|
|
435
|
-
const res = sweepStore(store);
|
|
436
|
-
updateAnchorStatusBar(ctx, store);
|
|
437
|
-
return {
|
|
438
|
-
content: [{
|
|
439
|
-
type: "text",
|
|
440
|
-
text: `Sweep complete: ${res.transitionedToSleeping.length} sleeping, ${res.evictedToGraveyard.length} evicted to graveyard.`
|
|
441
|
-
}],
|
|
442
|
-
isError: false
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
return { content: [{ type: "text", text: "Unknown action" }], isError: true };
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
pi.registerCommand("anchor", {
|
|
449
|
-
description: "Inspect, pin, or settle active anchors (e.g. /anchor, /anchor done 1, /anchor undo)",
|
|
450
|
-
handler: async (args, ctx) => {
|
|
451
|
-
const parsed = parseAnchorCommandInput(args);
|
|
452
|
-
if (parsed.mode === "dashboard") {
|
|
453
|
-
await openAnchorDashboard(ctx, store);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
456
|
-
if (parsed.mode === "undo") {
|
|
457
|
-
try {
|
|
458
|
-
const restored = store.undoSettle();
|
|
459
|
-
ctx.ui.notify(`\u2316 Restored #${restored.id}: "${restored.title}"`, "info");
|
|
460
|
-
updateAnchorStatusBar(ctx, store);
|
|
461
|
-
} catch (err) {
|
|
462
|
-
ctx.ui.notify(`Revert failed: ${err.message}`, "error");
|
|
463
|
-
}
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (parsed.mode === "settle" && parsed.targetId) {
|
|
467
|
-
const resolvedId = resolveAnchorTarget(store, parsed.targetId, ctx.cwd);
|
|
468
|
-
if (!resolvedId) {
|
|
469
|
-
ctx.ui.notify(`\u2316 Anchor not found: ${parsed.targetId}`, "error");
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
const settled = store.settle(resolvedId, { settledBy: "manual-command" });
|
|
473
|
-
ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
|
|
474
|
-
updateAnchorStatusBar(ctx, store);
|
|
475
|
-
return;
|
|
476
|
-
}
|
|
477
|
-
const rawTrimmed = (args || "").trim();
|
|
478
|
-
if (/^(?:#?anc-\d+|#\d+)$/i.test(rawTrimmed)) {
|
|
479
|
-
const resolvedId = resolveAnchorTarget(store, rawTrimmed, ctx.cwd);
|
|
480
|
-
if (resolvedId) {
|
|
481
|
-
const settled = store.settle(resolvedId, { settledBy: "manual-command" });
|
|
482
|
-
ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
|
|
483
|
-
updateAnchorStatusBar(ctx, store);
|
|
484
|
-
return;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
const anc = store.create({
|
|
488
|
-
title: parsed.title || rawTrimmed,
|
|
489
|
-
targetDate: parsed.targetDate,
|
|
490
|
-
cwd: ctx.cwd
|
|
491
|
-
});
|
|
492
|
-
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
|
|
493
|
-
updateAnchorStatusBar(ctx, store);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
pi.registerCommand("pin", {
|
|
497
|
-
description: "Pin a task across sessions (e.g. /pin Refactor auth --due tomorrow)",
|
|
498
|
-
handler: async (args, ctx) => {
|
|
499
|
-
const parsed = parseAnchorCommandInput(args);
|
|
500
|
-
if (parsed.mode === "dashboard" || !parsed.title) {
|
|
501
|
-
await openAnchorDashboard(ctx, store);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
const anc = store.create({
|
|
505
|
-
title: parsed.title,
|
|
506
|
-
targetDate: parsed.targetDate,
|
|
507
|
-
cwd: ctx.cwd
|
|
508
|
-
});
|
|
509
|
-
ctx.ui.notify(`\u2316 Pinned #${anc.id}: "${anc.title}"${anc.targetDate ? ` (due ${anc.targetDate})` : ""}`, "info");
|
|
510
|
-
updateAnchorStatusBar(ctx, store);
|
|
511
|
-
}
|
|
512
|
-
});
|
|
513
|
-
pi.registerCommand("done", {
|
|
514
|
-
description: "Complete an active anchor by row number or ID (e.g. /done 1)",
|
|
515
|
-
handler: async (args, ctx) => {
|
|
516
|
-
const target = (args || "").trim();
|
|
517
|
-
if (!target) {
|
|
518
|
-
await openAnchorDashboard(ctx, store);
|
|
519
|
-
return;
|
|
520
|
-
}
|
|
521
|
-
const resolvedId = resolveAnchorTarget(store, target, ctx.cwd);
|
|
522
|
-
if (!resolvedId) {
|
|
523
|
-
ctx.ui.notify(`\u2316 Anchor not found: ${target}`, "error");
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
const settled = store.settle(resolvedId, { settledBy: "manual-command" });
|
|
527
|
-
ctx.ui.notify(`\u2316 Settled #${settled.id}: "${settled.title}" (/anchor undo to revert)`, "info");
|
|
528
|
-
updateAnchorStatusBar(ctx, store);
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
pi.registerCommand("anchors", {
|
|
532
|
-
description: "Inspect and settle active anchors",
|
|
533
|
-
handler: async (_args, ctx) => {
|
|
534
|
-
await openAnchorDashboard(ctx, store);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
}
|
|
2
|
+
extension_default,
|
|
3
|
+
parseAnchorCommandInput,
|
|
4
|
+
resolveAnchorTarget
|
|
5
|
+
} from "./chunk-LKY6WIKH.js";
|
|
538
6
|
export {
|
|
539
7
|
extension_default as default,
|
|
540
8
|
parseAnchorCommandInput,
|