pi-anchor 0.2.0 → 0.2.2

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/dist/extension.js CHANGED
@@ -3,15 +3,18 @@ import {
3
3
  SessionTouchObserver,
4
4
  findMatchedAnchors,
5
5
  generateSettlementProposals,
6
+ hasActiveUI,
7
+ isSameOrSubPath,
6
8
  makeSafeTaskAnnotation,
7
9
  normalizePath,
8
10
  openAnchorDashboard,
9
11
  renderColdStartAnchorsContext,
10
12
  runPhysicalVerification,
11
13
  sweepStore,
14
+ truncateToWidth,
12
15
  updateAnchorStatusBar,
13
16
  updateStartupBanner
14
- } from "./chunk-T62PHIQI.js";
17
+ } from "./chunk-G72UHPBN.js";
15
18
 
16
19
  // src/extension.ts
17
20
  import { Type } from "@sinclair/typebox";
@@ -34,7 +37,7 @@ function extension_default(pi) {
34
37
  }
35
38
  const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
36
39
  for (const a of activeAnchors) {
37
- if (a.verifyCommand) {
40
+ if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
38
41
  try {
39
42
  const res = runPhysicalVerification(a, ctx.cwd);
40
43
  if (res.success) {
@@ -55,21 +58,32 @@ function extension_default(pi) {
55
58
  const pendingPath = path.join(home, ".anchor", "pending_exit.json");
56
59
  if (fs.existsSync(pendingPath)) {
57
60
  const pending = JSON.parse(fs.readFileSync(pendingPath, "utf-8"));
58
- if (pending.cwd === ctx.cwd && Date.now() - pending.timestamp < 24 * 60 * 60 * 1e3) {
59
- ctx.ui.notify(`\u2316 [Anchor \u9000\u51FA\u5B88\u62A4] \u68C0\u6D4B\u5230\u4E0A\u6B21\u5F02\u5E38\u9000\u51FA\u4E14\u6709 ${pending.modifiedCount} \u4E2A\u672A\u63D0\u4EA4\u6539\u52A8\uFF0C\u53EF\u7528 anchor(action="pin") \u56FA\u5316\u4EFB\u52A1\u3002`, "info");
61
+ const isExpired = Date.now() - pending.timestamp >= 24 * 60 * 60 * 1e3;
62
+ if (!isExpired && isSameOrSubPath(pending.cwd, ctx.cwd)) {
63
+ ctx.ui.notify(`\u2316 [Exit Guard] Detected ${pending.modifiedCount} uncommitted changes from previous session exit. Run /pin to record.`, "info");
64
+ fs.unlinkSync(pendingPath);
65
+ } else if (isExpired) {
66
+ fs.unlinkSync(pendingPath);
60
67
  }
61
- fs.unlinkSync(pendingPath);
62
68
  }
63
69
  } catch (_) {
64
70
  }
65
71
  updateAnchorStatusBar(ctx, store);
66
72
  if (event.reason !== "resume") {
67
73
  updateStartupBanner(ctx, store);
74
+ } else if (hasActiveUI(ctx)) {
75
+ try {
76
+ ctx.ui.setWidget("anchor-startup", void 0);
77
+ } catch {
78
+ }
68
79
  }
69
80
  });
70
81
  pi.on("agent_start", async (_event, ctx) => {
71
- if (ctx.hasUI && ctx.ui) {
72
- ctx.ui.setWidget("anchor-startup", void 0);
82
+ if (hasActiveUI(ctx)) {
83
+ try {
84
+ ctx.ui.setWidget("anchor-startup", void 0);
85
+ } catch {
86
+ }
73
87
  }
74
88
  });
75
89
  pi.on("before_agent_start", async (event, ctx) => {
@@ -93,7 +107,7 @@ ${contextSnippet}`
93
107
  }
94
108
  const pathInput = event.input?.path;
95
109
  if (typeof pathInput !== "string") return;
96
- const touchedPath = normalizePath(pathInput);
110
+ const touchedPath = normalizePath(pathInput, ctx.cwd);
97
111
  const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
98
112
  const matches = findMatchedAnchors(anchors, [touchedPath]);
99
113
  if (matches.length > 0) {
@@ -121,20 +135,37 @@ ${contextSnippet}`
121
135
  }
122
136
  }
123
137
  });
124
- pi.on("session_shutdown", async (_event, ctx) => {
138
+ pi.on("session_shutdown", async (event, ctx) => {
139
+ let gitStatus = "";
125
140
  try {
126
- const gitStatus = execSync("git status --porcelain", {
141
+ gitStatus = execSync("git status --porcelain", {
127
142
  cwd: ctx.cwd,
128
143
  encoding: "utf-8",
129
- timeout: 3e3,
144
+ timeout: 2e3,
130
145
  stdio: ["ignore", "pipe", "ignore"]
131
- });
132
- const changed = gitStatus.split("\n").map((l) => l.slice(3).trim()).filter(Boolean);
146
+ }).replace(/[\r\n]+$/, "");
147
+ const changed = gitStatus.split(/\r?\n/).filter(Boolean).map((l) => l.slice(3).replace(/^"(.*)"$/, "$1").trim()).filter(Boolean);
133
148
  for (const f of changed) {
134
149
  observer.addTouchedFile(f);
135
150
  }
136
151
  } catch {
137
152
  }
153
+ if (event?.reason === "quit" && gitStatus) {
154
+ try {
155
+ const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
156
+ if (activeAnchors.length === 0) {
157
+ const home = process.env.HOME || process.env.USERPROFILE || "";
158
+ const pendingPath = path.join(home, ".anchor", "pending_exit.json");
159
+ const data = {
160
+ cwd: ctx.cwd,
161
+ timestamp: Date.now(),
162
+ modifiedCount: gitStatus.split("\n").filter(Boolean).length
163
+ };
164
+ fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
165
+ }
166
+ } catch {
167
+ }
168
+ }
138
169
  const modified = observer.getModifiedFiles();
139
170
  if (modified.length === 0) return;
140
171
  const proposals = generateSettlementProposals(store, modified, ctx.cwd);
@@ -148,7 +179,12 @@ ${contextSnippet}`
148
179
  summary: `Auto-settled via Git commit: ${commitMatch.message}`,
149
180
  touchedFiles: prop.matchedFiles
150
181
  });
151
- ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
182
+ if (hasActiveUI(ctx)) {
183
+ try {
184
+ ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
185
+ } catch {
186
+ }
187
+ }
152
188
  continue;
153
189
  }
154
190
  if (a.verifyCommand) {
@@ -159,22 +195,30 @@ ${contextSnippet}`
159
195
  summary: `Automated test passed: ${a.verifyCommand}`,
160
196
  touchedFiles: prop.matchedFiles
161
197
  });
162
- ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
198
+ if (hasActiveUI(ctx)) {
199
+ try {
200
+ ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
201
+ } catch {
202
+ }
203
+ }
163
204
  continue;
164
205
  }
165
206
  }
166
- if (!ctx.hasUI || !ctx.ui) continue;
167
- const ok = await ctx.ui.confirm(
168
- "\u2316 Settle Anchor Task",
169
- `Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
207
+ if (!hasActiveUI(ctx)) continue;
208
+ try {
209
+ const ok = await ctx.ui.confirm(
210
+ "\u2316 Settle Anchor Task",
211
+ `Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
170
212
  Mark as completed and archive?`
171
- );
172
- if (ok) {
173
- store.settle(a.id, {
174
- settledBy: "one-tap-settlement",
175
- touchedFiles: prop.matchedFiles
176
- });
177
- ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
213
+ );
214
+ if (ok) {
215
+ store.settle(a.id, {
216
+ settledBy: "one-tap-settlement",
217
+ touchedFiles: prop.matchedFiles
218
+ });
219
+ ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
220
+ }
221
+ } catch {
178
222
  }
179
223
  }
180
224
  });
@@ -224,7 +268,7 @@ Mark as completed and archive?`
224
268
  if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
225
269
  }
226
270
  return {
227
- render: () => [title],
271
+ render: (width) => [truncateToWidth(title, width)],
228
272
  invalidate: () => {
229
273
  }
230
274
  };
@@ -233,14 +277,14 @@ Mark as completed and archive?`
233
277
  if (result.isError) {
234
278
  const errText = result.content?.[0]?.text || "Execution failed";
235
279
  return {
236
- render: () => [theme.fg("error", `\u2717 ${errText}`)],
280
+ render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
237
281
  invalidate: () => {
238
282
  }
239
283
  };
240
284
  }
241
285
  const text = result.content?.[0]?.text || "OK";
242
286
  return {
243
- render: () => [theme.fg("success", `\u2714 ${text}`)],
287
+ render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
244
288
  invalidate: () => {
245
289
  }
246
290
  };
@@ -376,28 +420,10 @@ Mark as completed and archive?`
376
420
  updateAnchorStatusBar(ctx, store);
377
421
  }
378
422
  });
379
- pi.on("session_shutdown", async (event, ctx) => {
380
- if (event.reason === "quit") {
381
- try {
382
- const gitStatus = execSync("git status --porcelain", {
383
- cwd: ctx.cwd,
384
- encoding: "utf-8",
385
- timeout: 2e3,
386
- stdio: ["ignore", "pipe", "ignore"]
387
- }).trim();
388
- const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
389
- if (gitStatus && activeAnchors.length === 0) {
390
- const home = process.env.HOME || process.env.USERPROFILE || "";
391
- const pendingPath = path.join(home, ".anchor", "pending_exit.json");
392
- const data = {
393
- cwd: ctx.cwd,
394
- timestamp: Date.now(),
395
- modifiedCount: gitStatus.split("\n").filter(Boolean).length
396
- };
397
- fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
398
- }
399
- } catch (_) {
400
- }
423
+ pi.registerCommand("anchors", {
424
+ description: "Inspect and settle active anchors",
425
+ handler: async (_args, ctx) => {
426
+ await openAnchorDashboard(ctx, store);
401
427
  }
402
428
  });
403
429
  }