pi-anchor 0.2.4 → 0.3.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.
@@ -1,22 +1,22 @@
1
1
  // src/types.ts
2
2
  var EPHEMERAL_DECAY_POLICY = {
3
- activeDays: 1,
4
- // 24 hours active
5
- sleepDays: 1,
6
- // 24 hours sleeping
7
- graveyardDays: 2
8
- // 48 hours total before auto-clearing
3
+ activeDays: 7,
4
+ // 7 days active before quiet sleep
5
+ sleepDays: 30,
6
+ graveyardDays: 9999
7
+ // Never silently auto-delete unfinished tasks
9
8
  };
10
9
  var DURABLE_DECAY_POLICY = {
11
- activeDays: 3,
10
+ activeDays: 7,
11
+ // 7 days active before quiet sleep
12
12
  sleepDays: 30,
13
13
  graveyardDays: 9999
14
14
  // Never permanently auto-dropped; preserved indefinitely
15
15
  };
16
16
  var DEFAULT_DECAY_POLICY = {
17
- activeDays: 3,
18
- sleepDays: 7,
19
- graveyardDays: 14
17
+ activeDays: 7,
18
+ sleepDays: 30,
19
+ graveyardDays: 9999
20
20
  };
21
21
 
22
22
  // src/matcher.ts
@@ -50,6 +50,39 @@ function isSameOrSubPath(anchorCwd, targetCwd) {
50
50
  const cmpT = isWinStyle ? normT.toLowerCase() : normT;
51
51
  return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
52
52
  }
53
+ function extractPathsFromText(text) {
54
+ if (!text) return [];
55
+ const results = /* @__PURE__ */ new Set();
56
+ 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;
57
+ let match;
58
+ while ((match = regex.exec(text)) !== null) {
59
+ const cleaned = normalizePath(match[0].replace(/[.:;,'")\]}>]+$/, ""));
60
+ if (cleaned && cleaned.length >= 3) {
61
+ results.add(cleaned);
62
+ }
63
+ }
64
+ return Array.from(results);
65
+ }
66
+ function isAnchorTargetingWorkspace(anchor, targetCwd) {
67
+ if (!targetCwd) return true;
68
+ const normTarget = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
69
+ const targetBase = path.basename(normTarget).toLowerCase();
70
+ if (anchor.project && anchor.project !== "global") {
71
+ const normProj = anchor.project.trim().replace(/\\/g, "/").replace(/\/+$/, "");
72
+ if (isSameOrSubPath(normProj, normTarget)) return true;
73
+ const projBase = path.basename(normProj).toLowerCase();
74
+ if (projBase && projBase === targetBase) return true;
75
+ }
76
+ if (anchor.files && anchor.files.length > 0) {
77
+ for (const f of anchor.files) {
78
+ if (isSameOrSubPath(f, normTarget)) return true;
79
+ }
80
+ }
81
+ if (anchor.cwd && isSameOrSubPath(anchor.cwd, normTarget)) {
82
+ return true;
83
+ }
84
+ return false;
85
+ }
53
86
  function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
54
87
  if (!anchor.files || anchor.files.length === 0) return null;
55
88
  const normTouched = touchedFiles.map((f) => normalizePath(f, anchor.cwd)).filter(Boolean);
@@ -155,10 +188,10 @@ function detectDurability(title) {
155
188
  function getEphemeralDecayPolicy(title) {
156
189
  const lower = title.toLowerCase();
157
190
  if (lower.includes("\u5927\u540E\u5929")) {
158
- return { activeDays: 3, sleepDays: 1, graveyardDays: 4 };
191
+ return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
159
192
  }
160
193
  if (lower.includes("\u540E\u5929")) {
161
- return { activeDays: 2, sleepDays: 1, graveyardDays: 3 };
194
+ return { activeDays: 7, sleepDays: 30, graveyardDays: 9999 };
162
195
  }
163
196
  return { ...EPHEMERAL_DECAY_POLICY };
164
197
  }
@@ -469,7 +502,17 @@ var AnchorStore = class {
469
502
  const state = this.loadState();
470
503
  const now = Date.now();
471
504
  const id = this.generateId(state);
472
- const cwd = input.cwd ? normalizePath(input.cwd) : "";
505
+ const explicitFiles = (input.files || []).map((f) => normalizePath(f)).filter(Boolean);
506
+ const inferredFiles = explicitFiles.length > 0 ? explicitFiles : extractPathsFromText(`${input.title} ${input.description || ""}`);
507
+ let cwd = input.cwd ? normalizePath(input.cwd) : "";
508
+ if (input.project && (/^[a-zA-Z]:[\\/]/.test(input.project.trim()) || input.project.trim().startsWith("/"))) {
509
+ cwd = normalizePath(input.project.trim());
510
+ } else if (inferredFiles.length > 0 && (/^[a-zA-Z]:\//.test(inferredFiles[0]) || inferredFiles[0].startsWith("/"))) {
511
+ const firstDir = path2.dirname(inferredFiles[0]);
512
+ if (firstDir && firstDir !== ".") {
513
+ cwd = normalizePath(firstDir);
514
+ }
515
+ }
473
516
  const projectName = input.project ? input.project.trim() : cwd ? path2.basename(cwd) : "global";
474
517
  const recurrence = input.recurrence || detectRecurrence(input.title);
475
518
  const targetDate = input.targetDate || detectTargetDate(input.title, now);
@@ -489,7 +532,7 @@ var AnchorStore = class {
489
532
  createdAt: now,
490
533
  updatedAt: now,
491
534
  lastTouchedAt: now,
492
- files: (input.files || []).map((f) => normalizePath(f)).filter(Boolean),
535
+ files: inferredFiles,
493
536
  tags: (input.tags || []).map((t) => t.trim()).filter(Boolean),
494
537
  verifyCommand: input.verifyCommand?.trim() || void 0,
495
538
  decay: input.decay ? { ...input.decay } : { ...decayPolicy }
@@ -528,7 +571,7 @@ var AnchorStore = class {
528
571
  if (filter?.status && a.status !== filter.status) return false;
529
572
  if (filter?.priority && a.priority !== filter.priority) return false;
530
573
  if (!filter?.all && targetCwd) {
531
- if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
574
+ if (a.files.length > 0 && (a.cwd || a.project) && !isAnchorTargetingWorkspace(a, targetCwd)) {
532
575
  return false;
533
576
  }
534
577
  }
@@ -1082,20 +1125,66 @@ function runPhysicalVerification(anchor, cwd) {
1082
1125
  return { success: false, output: String(err.stderr || err.stdout || err.message).trim() };
1083
1126
  }
1084
1127
  }
1085
- function generateSettlementProposals(store, touchedFiles, cwd) {
1128
+ function tryRealtimeAutoSettle(store, observer, cwd, executedCommand) {
1129
+ const candidates = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1130
+ if (candidates.length === 0) return [];
1131
+ const settledList = [];
1132
+ const cmdTrimmed = (executedCommand || "").trim();
1133
+ for (const a of candidates) {
1134
+ if (observer.hasCommitted()) {
1135
+ const commitMatch = observer.matchesCommit(a);
1136
+ if (commitMatch.matched && (!cwd || isAnchorTargetingWorkspace(a, cwd))) {
1137
+ const settled = store.settle(a.id, {
1138
+ settledBy: "verification-test",
1139
+ summary: `Auto-settled via Git commit: ${commitMatch.message}`,
1140
+ touchedFiles: observer.getModifiedFiles()
1141
+ });
1142
+ settledList.push({ anchor: settled, reason: `git commit ("${commitMatch.message}")` });
1143
+ continue;
1144
+ }
1145
+ }
1146
+ if (a.verifyCommand && cmdTrimmed && cmdTrimmed.includes(a.verifyCommand.trim())) {
1147
+ const settled = store.settle(a.id, {
1148
+ settledBy: "verification-test",
1149
+ summary: `Auto-settled via passing command: ${a.verifyCommand}`,
1150
+ touchedFiles: observer.getModifiedFiles()
1151
+ });
1152
+ settledList.push({ anchor: settled, reason: `verifyCommand (${a.verifyCommand})` });
1153
+ }
1154
+ }
1155
+ return settledList;
1156
+ }
1157
+ function generateSettlementProposals(store, touchedFiles, cwd, observer) {
1086
1158
  const activeAndSleeping = store.list({ cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
1087
1159
  const matches = findMatchedAnchors(activeAndSleeping, touchedFiles);
1160
+ const seenIds = /* @__PURE__ */ new Set();
1088
1161
  const proposals = [];
1089
1162
  for (const match of matches) {
1163
+ seenIds.add(match.anchor.id);
1090
1164
  const fileList = match.matchedFiles.slice(0, 3).join(", ");
1091
- const moreSuffix = match.matchedFiles.length > 3 ? ` \u7B49 ${match.matchedFiles.length} \u4E2A\u6587\u4EF6` : "";
1165
+ const moreSuffix = match.matchedFiles.length > 3 ? ` (+${match.matchedFiles.length - 3} more)` : "";
1092
1166
  proposals.push({
1093
1167
  anchor: match.anchor,
1094
1168
  matchedFiles: match.matchedFiles,
1095
- reason: `\u672C\u6B21\u4F1A\u8BDD\u4FEE\u6539\u4E86 ${fileList}${moreSuffix}`,
1169
+ reason: `Modified ${fileList}${moreSuffix}`,
1096
1170
  recommendedAction: match.score >= 0.85 ? "settle" : "defer"
1097
1171
  });
1098
1172
  }
1173
+ if (observer && observer.hasCommitted()) {
1174
+ for (const a of activeAndSleeping) {
1175
+ if (seenIds.has(a.id)) continue;
1176
+ const cm = observer.matchesCommit(a);
1177
+ if (cm.matched) {
1178
+ seenIds.add(a.id);
1179
+ proposals.push({
1180
+ anchor: a,
1181
+ matchedFiles: touchedFiles.slice(0, 3),
1182
+ reason: `Matched git commit: "${cm.message}"`,
1183
+ recommendedAction: "settle"
1184
+ });
1185
+ }
1186
+ }
1187
+ }
1099
1188
  return proposals;
1100
1189
  }
1101
1190
  function formatSettlementCard(proposal) {
@@ -1143,6 +1232,27 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
1143
1232
  }
1144
1233
  return groups;
1145
1234
  }
1235
+ var PRIORITY_RANK = { p0: 0, p1: 1, p2: 2 };
1236
+ function sortAnchorsForDisplay(anchors, cwd, now = Date.now()) {
1237
+ const todayStr = getTodayDateString(now);
1238
+ return [...anchors].sort((a, b) => {
1239
+ if (a.status !== b.status) {
1240
+ return a.status === "active" ? -1 : 1;
1241
+ }
1242
+ if (cwd) {
1243
+ const aMatch = isAnchorTargetingWorkspace(a, cwd) ? 0 : 1;
1244
+ const bMatch = isAnchorTargetingWorkspace(b, cwd) ? 0 : 1;
1245
+ if (aMatch !== bMatch) return aMatch - bMatch;
1246
+ }
1247
+ const aDue = a.targetDate && a.targetDate <= todayStr ? 0 : 1;
1248
+ const bDue = b.targetDate && b.targetDate <= todayStr ? 0 : 1;
1249
+ if (aDue !== bDue) return aDue - bDue;
1250
+ const pA = PRIORITY_RANK[a.priority] ?? 1;
1251
+ const pB = PRIORITY_RANK[b.priority] ?? 1;
1252
+ if (pA !== pB) return pA - pB;
1253
+ return a.createdAt - b.createdAt;
1254
+ });
1255
+ }
1146
1256
  var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
1147
1257
  function stripAnsi(str) {
1148
1258
  return str.replace(ANSI_REGEX, "");
@@ -1271,11 +1381,19 @@ function formatRemainingTtl(anchor, now = Date.now()) {
1271
1381
  }
1272
1382
  function formatOrigin(anchor) {
1273
1383
  if (typeof anchor === "object") {
1274
- if (!anchor.cwd) return anchor.project || "global";
1275
- return path4.basename(anchor.cwd) || "global";
1384
+ if (anchor.project && anchor.project !== "global") {
1385
+ const normProj = anchor.project.replace(/\\/g, "/").replace(/\/+$/, "");
1386
+ if (normProj.includes("/")) {
1387
+ const parts = normProj.split("/").filter(Boolean);
1388
+ return parts.slice(-2).join("/");
1389
+ }
1390
+ return normProj;
1391
+ }
1392
+ if (!anchor.cwd) return "global";
1393
+ return path4.basename(anchor.cwd.replace(/\\/g, "/")) || "global";
1276
1394
  }
1277
1395
  if (!anchor) return "global";
1278
- return path4.basename(anchor) || "global";
1396
+ return path4.basename(anchor.replace(/\\/g, "/")) || "global";
1279
1397
  }
1280
1398
  function updateAnchorStatusBar(ctx, store) {
1281
1399
  if (!hasActiveUI(ctx)) return;
@@ -1330,51 +1448,47 @@ function updateStartupBanner(ctx, store) {
1330
1448
  }
1331
1449
  async function openAnchorDashboard(ctx, store) {
1332
1450
  if (!hasActiveUI(ctx)) return;
1333
- const list = store.list({ status: "active", cwd: ctx.cwd });
1334
- if (list.length === 0) {
1451
+ const allUnfinished = store.list({ cwd: ctx.cwd, all: true }).filter((a) => a.status === "active" || a.status === "sleeping");
1452
+ if (allUnfinished.length === 0) {
1335
1453
  ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
1336
1454
  return;
1337
1455
  }
1338
- const groups = groupAnchorsByQuadrant(list);
1339
- const sortedList = [
1340
- ...groups.today,
1341
- ...groups.upcoming,
1342
- ...groups.habits,
1343
- ...groups.backlog
1344
- ];
1456
+ const sortedList = sortAnchorsForDisplay(allUnfinished, ctx.cwd);
1345
1457
  const optionMap = /* @__PURE__ */ new Map();
1346
1458
  const displayOptions = [];
1347
1459
  const termCols = process.stdout?.columns || 96;
1348
1460
  const isNarrow = termCols < 88;
1349
- const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
1461
+ const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 36;
1350
1462
  for (let i = 0; i < sortedList.length; i++) {
1351
1463
  const a = sortedList[i];
1352
1464
  const num = a.id.replace(/^anc-/, "").padStart(2, "0");
1353
- const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
1465
+ const badge = a.status === "sleeping" ? padToWidth("[Quiet]", 10) : padToWidth(`[${a.priority.toUpperCase()}]`, 10);
1354
1466
  const origin = formatOrigin(a);
1355
- const target = formatTargetDate(a);
1467
+ const target = a.status === "sleeping" ? "Quiet (0 tok)" : formatTargetDate(a);
1356
1468
  const created = formatCreationTime(a.createdAt);
1357
1469
  const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
1358
1470
  const colNum = `${num} `;
1359
1471
  const colTitle = padToWidth(titleWithFiles, titleWidth);
1360
- const colOrigin = padToWidth(origin, 10);
1361
- const colTarget = padToWidth(target, 12);
1472
+ const colOrigin = padToWidth(origin, 12);
1473
+ const colTarget = padToWidth(target, 14);
1362
1474
  const colCreated = created;
1363
- const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1475
+ const label = isNarrow ? `${colNum}${badge}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${badge}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
1364
1476
  optionMap.set(label, a);
1365
1477
  displayOptions.push(label);
1366
1478
  }
1367
- const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
1479
+ const selected = await ctx.ui.select("\u2316 Anchors (select to settle, wake, or inspect):", displayOptions);
1368
1480
  if (!selected) return;
1369
1481
  const anchor = optionMap.get(selected);
1370
1482
  if (!anchor) return;
1483
+ const actions = [
1484
+ "\u2713 Settle (Mark completed and archive)",
1485
+ ...anchor.status === "sleeping" ? ["\u26A1 Wake up (Activate for current session)"] : [],
1486
+ "\u2139 View details & associated files",
1487
+ "\u21A9 Cancel"
1488
+ ];
1371
1489
  const action = await ctx.ui.select(
1372
1490
  `\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
1373
- [
1374
- "\u2713 Settle (Mark completed and archive)",
1375
- "\u2139 View details & associated files",
1376
- "\u21A9 Cancel"
1377
- ]
1491
+ actions
1378
1492
  );
1379
1493
  if (!action || action.startsWith("\u21A9")) return;
1380
1494
  if (action.startsWith("\u2713")) {
@@ -1382,9 +1496,13 @@ async function openAnchorDashboard(ctx, store) {
1382
1496
  const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
1383
1497
  ctx.ui.notify(successMsg, "info");
1384
1498
  updateAnchorStatusBar(ctx, store);
1499
+ } else if (action.startsWith("\u26A1")) {
1500
+ store.touch(anchor.id);
1501
+ ctx.ui.notify(`\u2316 Woke up #${anchor.id}: "${anchor.title}"`, "info");
1502
+ updateAnchorStatusBar(ctx, store);
1385
1503
  } else if (action.startsWith("\u2139")) {
1386
1504
  const details = [
1387
- `#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
1505
+ `#${anchor.id} [${anchor.priority.toUpperCase()} \xB7 ${anchor.status.toUpperCase()}] ${anchor.title}`,
1388
1506
  anchor.description ? `Description: ${anchor.description}` : null,
1389
1507
  anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
1390
1508
  anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
@@ -1402,6 +1520,8 @@ export {
1402
1520
  globToRegExp,
1403
1521
  normalizePath,
1404
1522
  isSameOrSubPath,
1523
+ extractPathsFromText,
1524
+ isAnchorTargetingWorkspace,
1405
1525
  matchAnchorAgainstTouchedFiles,
1406
1526
  findMatchedAnchors,
1407
1527
  detectDurability,
@@ -1421,10 +1541,12 @@ export {
1421
1541
  renderColdStartAnchorsContext,
1422
1542
  renderActiveAnchorsContext,
1423
1543
  runPhysicalVerification,
1544
+ tryRealtimeAutoSettle,
1424
1545
  generateSettlementProposals,
1425
1546
  formatSettlementCard,
1426
1547
  classifyAnchor,
1427
1548
  groupAnchorsByQuadrant,
1549
+ sortAnchorsForDisplay,
1428
1550
  stripAnsi,
1429
1551
  getDisplayWidth,
1430
1552
  hasActiveUI,