pi-anchor 0.2.1 → 0.2.3
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 +101 -195
- package/README_zh.md +115 -205
- package/dist/{chunk-B652T7QR.js → chunk-G72UHPBN.js} +218 -121
- package/dist/extension.cjs +286 -168
- package/dist/extension.js +73 -48
- package/dist/index.cjs +220 -121
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +3 -3
- package/src/decay.ts +54 -52
- package/src/extension.ts +74 -55
- package/src/matcher.ts +27 -6
- package/src/settlement.ts +5 -3
- package/src/store.ts +75 -18
- package/src/tui.ts +82 -59
- package/src/types.ts +1 -0
package/dist/extension.cjs
CHANGED
|
@@ -59,20 +59,34 @@ var DURABLE_DECAY_POLICY = {
|
|
|
59
59
|
// src/matcher.ts
|
|
60
60
|
var import_node_path = __toESM(require("path"), 1);
|
|
61
61
|
function globToRegExp(pattern) {
|
|
62
|
-
const
|
|
62
|
+
const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
|
|
63
|
+
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, ".*");
|
|
63
64
|
return new RegExp(`^${escaped}$`);
|
|
64
65
|
}
|
|
65
66
|
function normalizePath(filePath, baseCwd) {
|
|
66
67
|
if (typeof filePath !== "string") return "";
|
|
67
68
|
let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
68
|
-
if (baseCwd && (import_node_path.default.isAbsolute(filePath) || norm.startsWith("/"))) {
|
|
69
|
-
const normBase = baseCwd.trim().replace(/\\/g, "/");
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
if (baseCwd && (import_node_path.default.isAbsolute(filePath) || norm.startsWith("/") || /^[a-zA-Z]:\//.test(norm))) {
|
|
70
|
+
const normBase = baseCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
71
|
+
const lowerNorm = norm.toLowerCase();
|
|
72
|
+
const lowerBase = normBase.toLowerCase();
|
|
73
|
+
if (lowerNorm === lowerBase) {
|
|
74
|
+
norm = "";
|
|
75
|
+
} else if (lowerNorm.startsWith(lowerBase + "/")) {
|
|
76
|
+
norm = norm.slice(normBase.length + 1).replace(/^\/+/, "");
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
79
|
return norm;
|
|
75
80
|
}
|
|
81
|
+
function isSameOrSubPath(anchorCwd, targetCwd) {
|
|
82
|
+
if (!anchorCwd || !targetCwd) return false;
|
|
83
|
+
const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
84
|
+
const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
85
|
+
const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
|
|
86
|
+
const cmpA = isWinStyle ? normA.toLowerCase() : normA;
|
|
87
|
+
const cmpT = isWinStyle ? normT.toLowerCase() : normT;
|
|
88
|
+
return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
|
|
89
|
+
}
|
|
76
90
|
function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
|
|
77
91
|
if (!anchor.files || anchor.files.length === 0) return null;
|
|
78
92
|
const normTouched = touchedFiles.map((f) => normalizePath(f, anchor.cwd)).filter(Boolean);
|
|
@@ -434,9 +448,19 @@ var AnchorStore = class {
|
|
|
434
448
|
}
|
|
435
449
|
try {
|
|
436
450
|
const data = JSON.parse(raw);
|
|
437
|
-
if (!Array.isArray(data.anchors)) {
|
|
451
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
|
|
438
452
|
throw new Error("Invalid state: anchors must be array");
|
|
439
453
|
}
|
|
454
|
+
data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
|
|
455
|
+
...a,
|
|
456
|
+
files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
|
|
457
|
+
tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
|
|
458
|
+
decay: {
|
|
459
|
+
activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
|
|
460
|
+
sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
|
|
461
|
+
graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
|
|
462
|
+
}
|
|
463
|
+
}));
|
|
440
464
|
return data;
|
|
441
465
|
} catch {
|
|
442
466
|
const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
|
|
@@ -456,16 +480,19 @@ var AnchorStore = class {
|
|
|
456
480
|
*/
|
|
457
481
|
saveState(state) {
|
|
458
482
|
this.ensureDirs();
|
|
459
|
-
const
|
|
483
|
+
const rand = Math.random().toString(36).slice(2, 7);
|
|
484
|
+
const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
|
|
460
485
|
import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
|
|
461
486
|
atomicRenameWithRetry(tempPath, this.statePath);
|
|
462
487
|
}
|
|
463
|
-
generateId(
|
|
464
|
-
const numbers =
|
|
488
|
+
generateId(state) {
|
|
489
|
+
const numbers = state.anchors.map((a) => {
|
|
465
490
|
const match = a.id.match(/^anc-(\d+)$/);
|
|
466
491
|
return match ? parseInt(match[1], 10) : 0;
|
|
467
492
|
}).filter((n) => n > 0);
|
|
468
|
-
const
|
|
493
|
+
const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
|
|
494
|
+
const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
|
|
495
|
+
state.nextId = nextNum + 1;
|
|
469
496
|
return `anc-${nextNum}`;
|
|
470
497
|
}
|
|
471
498
|
/**
|
|
@@ -475,7 +502,7 @@ var AnchorStore = class {
|
|
|
475
502
|
return this.withLock(() => {
|
|
476
503
|
const state = this.loadState();
|
|
477
504
|
const now = Date.now();
|
|
478
|
-
const id = this.generateId(state
|
|
505
|
+
const id = this.generateId(state);
|
|
479
506
|
const cwd = input.cwd ? normalizePath(input.cwd) : "";
|
|
480
507
|
const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
|
|
481
508
|
const recurrence = input.recurrence || detectRecurrence(input.title);
|
|
@@ -535,7 +562,7 @@ var AnchorStore = class {
|
|
|
535
562
|
if (filter?.status && a.status !== filter.status) return false;
|
|
536
563
|
if (filter?.priority && a.priority !== filter.priority) return false;
|
|
537
564
|
if (!filter?.all && targetCwd) {
|
|
538
|
-
if (a.files.length > 0 && a.cwd && a.cwd
|
|
565
|
+
if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
|
|
539
566
|
return false;
|
|
540
567
|
}
|
|
541
568
|
}
|
|
@@ -636,18 +663,46 @@ var AnchorStore = class {
|
|
|
636
663
|
throw new Error("No archived anchors to undo");
|
|
637
664
|
}
|
|
638
665
|
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
639
|
-
const lines = raw.split(
|
|
640
|
-
|
|
666
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
667
|
+
let anchor = null;
|
|
668
|
+
while (lines.length > 0 && !anchor) {
|
|
669
|
+
const candidate = lines.pop();
|
|
670
|
+
try {
|
|
671
|
+
const parsed = JSON.parse(candidate);
|
|
672
|
+
if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
|
|
673
|
+
anchor = parsed;
|
|
674
|
+
}
|
|
675
|
+
} catch {
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (!anchor) {
|
|
641
679
|
throw new Error("Archive is empty, nothing to undo");
|
|
642
680
|
}
|
|
643
|
-
const lastLine = lines.pop();
|
|
644
|
-
const anchor = JSON.parse(lastLine);
|
|
645
681
|
import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
646
682
|
anchor.status = "active";
|
|
647
683
|
anchor.updatedAt = Date.now();
|
|
648
684
|
delete anchor.evidence;
|
|
685
|
+
delete anchor.settledForDate;
|
|
686
|
+
if (anchor.recurrence === "daily") {
|
|
687
|
+
delete anchor.lastCompletedDate;
|
|
688
|
+
}
|
|
649
689
|
const state = this.loadState();
|
|
650
|
-
state.anchors.
|
|
690
|
+
const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
|
|
691
|
+
if (existingIdx !== -1) {
|
|
692
|
+
if (state.anchors[existingIdx].title === anchor.title) {
|
|
693
|
+
state.anchors[existingIdx] = anchor;
|
|
694
|
+
} else {
|
|
695
|
+
anchor.id = this.generateId(state);
|
|
696
|
+
state.anchors.push(anchor);
|
|
697
|
+
}
|
|
698
|
+
} else {
|
|
699
|
+
const match = anchor.id.match(/^anc-(\d+)$/);
|
|
700
|
+
if (match) {
|
|
701
|
+
const restoredNum = parseInt(match[1], 10);
|
|
702
|
+
state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
|
|
703
|
+
}
|
|
704
|
+
state.anchors.push(anchor);
|
|
705
|
+
}
|
|
651
706
|
this.saveState(state);
|
|
652
707
|
return anchor;
|
|
653
708
|
});
|
|
@@ -679,10 +734,19 @@ var AnchorStore = class {
|
|
|
679
734
|
if (!import_node_fs.default.existsSync(this.archivePath)) return [];
|
|
680
735
|
try {
|
|
681
736
|
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
682
|
-
const all =
|
|
737
|
+
const all = [];
|
|
738
|
+
for (const l of raw.split(/\r?\n/)) {
|
|
739
|
+
const trimmed = l.trim();
|
|
740
|
+
if (!trimmed) continue;
|
|
741
|
+
try {
|
|
742
|
+
const parsed = JSON.parse(trimmed);
|
|
743
|
+
if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
|
|
744
|
+
} catch {
|
|
745
|
+
}
|
|
746
|
+
}
|
|
683
747
|
if (filter?.all || !filter?.cwd) return all;
|
|
684
748
|
const targetCwd = normalizePath(filter.cwd);
|
|
685
|
-
return all.filter((a) => !a.cwd || a.cwd
|
|
749
|
+
return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
|
|
686
750
|
} catch {
|
|
687
751
|
return [];
|
|
688
752
|
}
|
|
@@ -691,10 +755,19 @@ var AnchorStore = class {
|
|
|
691
755
|
if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
|
|
692
756
|
try {
|
|
693
757
|
const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
|
|
694
|
-
const all =
|
|
758
|
+
const all = [];
|
|
759
|
+
for (const l of raw.split(/\r?\n/)) {
|
|
760
|
+
const trimmed = l.trim();
|
|
761
|
+
if (!trimmed) continue;
|
|
762
|
+
try {
|
|
763
|
+
const parsed = JSON.parse(trimmed);
|
|
764
|
+
if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
|
|
765
|
+
} catch {
|
|
766
|
+
}
|
|
767
|
+
}
|
|
695
768
|
if (filter?.all || !filter?.cwd) return all;
|
|
696
769
|
const targetCwd = normalizePath(filter.cwd);
|
|
697
|
-
return all.filter((a) => !a.cwd || a.cwd
|
|
770
|
+
return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
|
|
698
771
|
} catch {
|
|
699
772
|
return [];
|
|
700
773
|
}
|
|
@@ -850,56 +923,58 @@ function evaluateAnchorDecay(anchor, now = Date.now()) {
|
|
|
850
923
|
};
|
|
851
924
|
}
|
|
852
925
|
function sweepStore(store, now = Date.now()) {
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
if (evalResult.nextStatus
|
|
867
|
-
anchor.status
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
anchor.status
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
926
|
+
return store.withLock(() => {
|
|
927
|
+
const state = store.loadState();
|
|
928
|
+
const result = {
|
|
929
|
+
transitionedToSleeping: [],
|
|
930
|
+
wokenToActive: [],
|
|
931
|
+
evictedToGraveyard: []
|
|
932
|
+
};
|
|
933
|
+
const toEvict = [];
|
|
934
|
+
let stateModified = false;
|
|
935
|
+
for (const anchor of state.anchors) {
|
|
936
|
+
const evalResult = evaluateAnchorDecay(anchor, now);
|
|
937
|
+
if (evalResult.nextStatus === "graveyard") {
|
|
938
|
+
toEvict.push(anchor);
|
|
939
|
+
} else if (evalResult.nextStatus !== anchor.status) {
|
|
940
|
+
if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
|
|
941
|
+
anchor.status = "sleeping";
|
|
942
|
+
anchor.updatedAt = now;
|
|
943
|
+
result.transitionedToSleeping.push(anchor.id);
|
|
944
|
+
stateModified = true;
|
|
945
|
+
} else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
|
|
946
|
+
anchor.status = "active";
|
|
947
|
+
anchor.updatedAt = now;
|
|
948
|
+
result.wokenToActive.push(anchor.id);
|
|
949
|
+
stateModified = true;
|
|
950
|
+
}
|
|
876
951
|
}
|
|
877
952
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
953
|
+
if (toEvict.length > 0) {
|
|
954
|
+
const evictIds = new Set(toEvict.map((e) => e.id));
|
|
955
|
+
state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
|
|
956
|
+
stateModified = true;
|
|
957
|
+
const graveyardLines = toEvict.map((exp) => {
|
|
958
|
+
const rec = {
|
|
959
|
+
...exp,
|
|
960
|
+
status: "graveyard",
|
|
961
|
+
updatedAt: now,
|
|
962
|
+
evictedAt: now,
|
|
963
|
+
evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
|
|
964
|
+
};
|
|
965
|
+
return JSON.stringify(rec);
|
|
966
|
+
}).join("\n") + "\n";
|
|
967
|
+
import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
|
|
968
|
+
for (const exp of toEvict) {
|
|
969
|
+
result.evictedToGraveyard.push(exp.id);
|
|
970
|
+
}
|
|
896
971
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
972
|
+
state.lastSweepAt = now;
|
|
973
|
+
if (stateModified) {
|
|
974
|
+
store.saveState(state);
|
|
975
|
+
}
|
|
976
|
+
return result;
|
|
977
|
+
});
|
|
903
978
|
}
|
|
904
979
|
|
|
905
980
|
// src/context_injector.ts
|
|
@@ -1023,14 +1098,16 @@ function renderColdStartAnchorsContext(store, cwdOrNow, nowArg, preflightFailure
|
|
|
1023
1098
|
// src/settlement.ts
|
|
1024
1099
|
var import_node_child_process = require("child_process");
|
|
1025
1100
|
function runPhysicalVerification(anchor, cwd) {
|
|
1026
|
-
if (!anchor.verifyCommand) {
|
|
1101
|
+
if (!anchor.verifyCommand?.trim()) {
|
|
1027
1102
|
return { success: false, output: "No verification command specified" };
|
|
1028
1103
|
}
|
|
1104
|
+
const targetCwd = anchor.cwd || cwd;
|
|
1029
1105
|
try {
|
|
1030
1106
|
const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
|
|
1031
|
-
cwd,
|
|
1107
|
+
cwd: targetCwd,
|
|
1032
1108
|
encoding: "utf-8",
|
|
1033
|
-
timeout:
|
|
1109
|
+
timeout: 8e3,
|
|
1110
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
1034
1111
|
stdio: ["ignore", "pipe", "pipe"]
|
|
1035
1112
|
});
|
|
1036
1113
|
return { success: true, output: stdout.trim() };
|
|
@@ -1106,27 +1183,36 @@ function getDisplayWidth(str) {
|
|
|
1106
1183
|
}
|
|
1107
1184
|
return width;
|
|
1108
1185
|
}
|
|
1186
|
+
function hasActiveUI(ctx) {
|
|
1187
|
+
if (!ctx) return false;
|
|
1188
|
+
try {
|
|
1189
|
+
return Boolean(ctx.hasUI && ctx.ui);
|
|
1190
|
+
} catch {
|
|
1191
|
+
return false;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1109
1194
|
function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
1110
1195
|
if (maxWidth <= 0) return "";
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1196
|
+
const singleLine = str.replace(/[\r\n\t]+/g, " ");
|
|
1197
|
+
const totalWidth = getDisplayWidth(singleLine);
|
|
1198
|
+
if (totalWidth <= maxWidth) return singleLine;
|
|
1113
1199
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
1114
1200
|
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
1115
1201
|
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
1116
1202
|
let accumulated = "";
|
|
1117
1203
|
let accumWidth = 0;
|
|
1118
1204
|
let i = 0;
|
|
1119
|
-
while (i <
|
|
1205
|
+
while (i < singleLine.length) {
|
|
1120
1206
|
ANSI_REGEX.lastIndex = i;
|
|
1121
|
-
const match = ANSI_REGEX.exec(
|
|
1207
|
+
const match = ANSI_REGEX.exec(singleLine);
|
|
1122
1208
|
if (match && match.index === i) {
|
|
1123
1209
|
accumulated += match[0];
|
|
1124
1210
|
i += match[0].length;
|
|
1125
1211
|
continue;
|
|
1126
1212
|
}
|
|
1127
|
-
const codePoint =
|
|
1213
|
+
const codePoint = singleLine.codePointAt(i) || 0;
|
|
1128
1214
|
const charLen = codePoint > 65535 ? 2 : 1;
|
|
1129
|
-
const fullChar =
|
|
1215
|
+
const fullChar = singleLine.slice(i, i + charLen);
|
|
1130
1216
|
const charWidth = getDisplayWidth(fullChar);
|
|
1131
1217
|
if (accumWidth + charWidth > target) {
|
|
1132
1218
|
break;
|
|
@@ -1136,7 +1222,7 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
|
1136
1222
|
i += charLen;
|
|
1137
1223
|
}
|
|
1138
1224
|
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
|
|
1139
|
-
return accumulated + ellipsis + (
|
|
1225
|
+
return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
|
|
1140
1226
|
}
|
|
1141
1227
|
function padToWidth(str, targetWidth) {
|
|
1142
1228
|
const truncated = truncateToWidth(str, targetWidth);
|
|
@@ -1206,52 +1292,58 @@ function formatOrigin(anchor) {
|
|
|
1206
1292
|
return import_node_path4.default.basename(anchor) || "global";
|
|
1207
1293
|
}
|
|
1208
1294
|
function updateAnchorStatusBar(ctx, store) {
|
|
1209
|
-
if (!ctx
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1295
|
+
if (!hasActiveUI(ctx)) return;
|
|
1296
|
+
try {
|
|
1297
|
+
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1298
|
+
if (active.length === 0) {
|
|
1299
|
+
ctx.ui.setStatus("anchor", void 0);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
1303
|
+
} catch {
|
|
1214
1304
|
}
|
|
1215
|
-
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
1216
1305
|
}
|
|
1217
1306
|
function updateStartupBanner(ctx, store) {
|
|
1218
|
-
if (!ctx
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
const groups = groupAnchorsByQuadrant(active);
|
|
1225
|
-
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1226
|
-
const topItems = sorted.slice(0, 3);
|
|
1227
|
-
const factory = (_tui, theme) => ({
|
|
1228
|
-
render: (width) => {
|
|
1229
|
-
const maxTitleWidth = Math.max(16, width - 36);
|
|
1230
|
-
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1231
|
-
const lines = [
|
|
1232
|
-
fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
|
|
1233
|
-
];
|
|
1234
|
-
for (let i = 0; i < topItems.length; i++) {
|
|
1235
|
-
const a = topItems[i];
|
|
1236
|
-
const num = (i + 1).toString().padStart(2, "0");
|
|
1237
|
-
const q = classifyAnchor(a);
|
|
1238
|
-
const tgt = formatTargetDate(a);
|
|
1239
|
-
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1240
|
-
lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
|
|
1241
|
-
}
|
|
1242
|
-
if (sorted.length > 3) {
|
|
1243
|
-
lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
|
|
1244
|
-
}
|
|
1245
|
-
return lines;
|
|
1246
|
-
},
|
|
1247
|
-
invalidate: () => {
|
|
1307
|
+
if (!hasActiveUI(ctx)) return;
|
|
1308
|
+
try {
|
|
1309
|
+
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1310
|
+
if (active.length === 0) {
|
|
1311
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1312
|
+
return;
|
|
1248
1313
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1314
|
+
const groups = groupAnchorsByQuadrant(active);
|
|
1315
|
+
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1316
|
+
const topItems = sorted.slice(0, 3);
|
|
1317
|
+
const factory = (_tui, theme) => ({
|
|
1318
|
+
render: (width) => {
|
|
1319
|
+
const maxTitleWidth = Math.max(12, width - 36);
|
|
1320
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1321
|
+
const lines = [
|
|
1322
|
+
truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
|
|
1323
|
+
];
|
|
1324
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
1325
|
+
const a = topItems[i];
|
|
1326
|
+
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1327
|
+
const q = classifyAnchor(a);
|
|
1328
|
+
const tgt = formatTargetDate(a);
|
|
1329
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1330
|
+
lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
|
|
1331
|
+
}
|
|
1332
|
+
if (sorted.length > 3) {
|
|
1333
|
+
lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
|
|
1334
|
+
}
|
|
1335
|
+
return lines;
|
|
1336
|
+
},
|
|
1337
|
+
invalidate: () => {
|
|
1338
|
+
}
|
|
1339
|
+
});
|
|
1340
|
+
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
1341
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
|
|
1342
|
+
} catch {
|
|
1343
|
+
}
|
|
1252
1344
|
}
|
|
1253
1345
|
async function openAnchorDashboard(ctx, store) {
|
|
1254
|
-
if (!ctx
|
|
1346
|
+
if (!hasActiveUI(ctx)) return;
|
|
1255
1347
|
const list = store.list({ status: "active", cwd: ctx.cwd });
|
|
1256
1348
|
if (list.length === 0) {
|
|
1257
1349
|
ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
|
|
@@ -1266,20 +1358,23 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1266
1358
|
];
|
|
1267
1359
|
const optionMap = /* @__PURE__ */ new Map();
|
|
1268
1360
|
const displayOptions = [];
|
|
1361
|
+
const termCols = process.stdout?.columns || 96;
|
|
1362
|
+
const isNarrow = termCols < 88;
|
|
1363
|
+
const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
|
|
1269
1364
|
for (let i = 0; i < sortedList.length; i++) {
|
|
1270
1365
|
const a = sortedList[i];
|
|
1271
|
-
const num = (
|
|
1366
|
+
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1272
1367
|
const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
|
|
1273
1368
|
const origin = formatOrigin(a);
|
|
1274
1369
|
const target = formatTargetDate(a);
|
|
1275
1370
|
const created = formatCreationTime(a.createdAt);
|
|
1276
1371
|
const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
|
|
1277
1372
|
const colNum = `${num} `;
|
|
1278
|
-
const colTitle = padToWidth(titleWithFiles,
|
|
1373
|
+
const colTitle = padToWidth(titleWithFiles, titleWidth);
|
|
1279
1374
|
const colOrigin = padToWidth(origin, 10);
|
|
1280
1375
|
const colTarget = padToWidth(target, 12);
|
|
1281
1376
|
const colCreated = created;
|
|
1282
|
-
const label = `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
1377
|
+
const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
1283
1378
|
optionMap.set(label, a);
|
|
1284
1379
|
displayOptions.push(label);
|
|
1285
1380
|
}
|
|
@@ -1334,7 +1429,7 @@ function extension_default(pi) {
|
|
|
1334
1429
|
}
|
|
1335
1430
|
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1336
1431
|
for (const a of activeAnchors) {
|
|
1337
|
-
if (a.verifyCommand) {
|
|
1432
|
+
if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
|
|
1338
1433
|
try {
|
|
1339
1434
|
const res = runPhysicalVerification(a, ctx.cwd);
|
|
1340
1435
|
if (res.success) {
|
|
@@ -1355,21 +1450,32 @@ function extension_default(pi) {
|
|
|
1355
1450
|
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1356
1451
|
if (import_node_fs3.default.existsSync(pendingPath)) {
|
|
1357
1452
|
const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
|
|
1358
|
-
|
|
1359
|
-
|
|
1453
|
+
const isExpired = Date.now() - pending.timestamp >= 24 * 60 * 60 * 1e3;
|
|
1454
|
+
if (!isExpired && isSameOrSubPath(pending.cwd, ctx.cwd)) {
|
|
1455
|
+
ctx.ui.notify(`\u2316 [Exit Guard] Detected ${pending.modifiedCount} uncommitted changes from previous session exit. Run /pin to record.`, "info");
|
|
1456
|
+
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1457
|
+
} else if (isExpired) {
|
|
1458
|
+
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1360
1459
|
}
|
|
1361
|
-
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1362
1460
|
}
|
|
1363
1461
|
} catch (_) {
|
|
1364
1462
|
}
|
|
1365
1463
|
updateAnchorStatusBar(ctx, store);
|
|
1366
1464
|
if (event.reason !== "resume") {
|
|
1367
1465
|
updateStartupBanner(ctx, store);
|
|
1466
|
+
} else if (hasActiveUI(ctx)) {
|
|
1467
|
+
try {
|
|
1468
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1368
1471
|
}
|
|
1369
1472
|
});
|
|
1370
1473
|
pi.on("agent_start", async (_event, ctx) => {
|
|
1371
|
-
if (ctx
|
|
1372
|
-
|
|
1474
|
+
if (hasActiveUI(ctx)) {
|
|
1475
|
+
try {
|
|
1476
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1373
1479
|
}
|
|
1374
1480
|
});
|
|
1375
1481
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -1393,7 +1499,7 @@ ${contextSnippet}`
|
|
|
1393
1499
|
}
|
|
1394
1500
|
const pathInput = event.input?.path;
|
|
1395
1501
|
if (typeof pathInput !== "string") return;
|
|
1396
|
-
const touchedPath = normalizePath(pathInput);
|
|
1502
|
+
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
1397
1503
|
const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1398
1504
|
const matches = findMatchedAnchors(anchors, [touchedPath]);
|
|
1399
1505
|
if (matches.length > 0) {
|
|
@@ -1421,20 +1527,37 @@ ${contextSnippet}`
|
|
|
1421
1527
|
}
|
|
1422
1528
|
}
|
|
1423
1529
|
});
|
|
1424
|
-
pi.on("session_shutdown", async (
|
|
1530
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
1531
|
+
let gitStatus = "";
|
|
1425
1532
|
try {
|
|
1426
|
-
|
|
1533
|
+
gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
1427
1534
|
cwd: ctx.cwd,
|
|
1428
1535
|
encoding: "utf-8",
|
|
1429
|
-
timeout:
|
|
1536
|
+
timeout: 2e3,
|
|
1430
1537
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1431
|
-
});
|
|
1432
|
-
const changed = gitStatus.split(
|
|
1538
|
+
}).replace(/[\r\n]+$/, "");
|
|
1539
|
+
const changed = gitStatus.split(/\r?\n/).filter(Boolean).map((l) => l.slice(3).replace(/^"(.*)"$/, "$1").trim()).filter(Boolean);
|
|
1433
1540
|
for (const f of changed) {
|
|
1434
1541
|
observer.addTouchedFile(f);
|
|
1435
1542
|
}
|
|
1436
1543
|
} catch {
|
|
1437
1544
|
}
|
|
1545
|
+
if (event?.reason === "quit" && gitStatus) {
|
|
1546
|
+
try {
|
|
1547
|
+
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1548
|
+
if (activeAnchors.length === 0) {
|
|
1549
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1550
|
+
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1551
|
+
const data = {
|
|
1552
|
+
cwd: ctx.cwd,
|
|
1553
|
+
timestamp: Date.now(),
|
|
1554
|
+
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
1555
|
+
};
|
|
1556
|
+
import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1557
|
+
}
|
|
1558
|
+
} catch {
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1438
1561
|
const modified = observer.getModifiedFiles();
|
|
1439
1562
|
if (modified.length === 0) return;
|
|
1440
1563
|
const proposals = generateSettlementProposals(store, modified, ctx.cwd);
|
|
@@ -1448,7 +1571,12 @@ ${contextSnippet}`
|
|
|
1448
1571
|
summary: `Auto-settled via Git commit: ${commitMatch.message}`,
|
|
1449
1572
|
touchedFiles: prop.matchedFiles
|
|
1450
1573
|
});
|
|
1451
|
-
|
|
1574
|
+
if (hasActiveUI(ctx)) {
|
|
1575
|
+
try {
|
|
1576
|
+
ctx.ui.notify(`\u2316 Auto-settled #${a.id} via Git commit (run /anchor undo to revert)`, "info");
|
|
1577
|
+
} catch {
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1452
1580
|
continue;
|
|
1453
1581
|
}
|
|
1454
1582
|
if (a.verifyCommand) {
|
|
@@ -1459,22 +1587,30 @@ ${contextSnippet}`
|
|
|
1459
1587
|
summary: `Automated test passed: ${a.verifyCommand}`,
|
|
1460
1588
|
touchedFiles: prop.matchedFiles
|
|
1461
1589
|
});
|
|
1462
|
-
|
|
1590
|
+
if (hasActiveUI(ctx)) {
|
|
1591
|
+
try {
|
|
1592
|
+
ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1463
1596
|
continue;
|
|
1464
1597
|
}
|
|
1465
1598
|
}
|
|
1466
|
-
if (!ctx
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1599
|
+
if (!hasActiveUI(ctx)) continue;
|
|
1600
|
+
try {
|
|
1601
|
+
const ok = await ctx.ui.confirm(
|
|
1602
|
+
"\u2316 Settle Anchor Task",
|
|
1603
|
+
`Task #${a.id} [${a.title}] touched files (${prop.matchedFiles.slice(0, 2).join(", ")}).
|
|
1470
1604
|
Mark as completed and archive?`
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1605
|
+
);
|
|
1606
|
+
if (ok) {
|
|
1607
|
+
store.settle(a.id, {
|
|
1608
|
+
settledBy: "one-tap-settlement",
|
|
1609
|
+
touchedFiles: prop.matchedFiles
|
|
1610
|
+
});
|
|
1611
|
+
ctx.ui.notify(`\u2316 Settled #${a.id}: "${a.title}"`, "info");
|
|
1612
|
+
}
|
|
1613
|
+
} catch {
|
|
1478
1614
|
}
|
|
1479
1615
|
}
|
|
1480
1616
|
});
|
|
@@ -1676,28 +1812,10 @@ Mark as completed and archive?`
|
|
|
1676
1812
|
updateAnchorStatusBar(ctx, store);
|
|
1677
1813
|
}
|
|
1678
1814
|
});
|
|
1679
|
-
pi.
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
cwd: ctx.cwd,
|
|
1684
|
-
encoding: "utf-8",
|
|
1685
|
-
timeout: 2e3,
|
|
1686
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1687
|
-
}).trim();
|
|
1688
|
-
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1689
|
-
if (gitStatus && activeAnchors.length === 0) {
|
|
1690
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1691
|
-
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1692
|
-
const data = {
|
|
1693
|
-
cwd: ctx.cwd,
|
|
1694
|
-
timestamp: Date.now(),
|
|
1695
|
-
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
1696
|
-
};
|
|
1697
|
-
import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1698
|
-
}
|
|
1699
|
-
} catch (_) {
|
|
1700
|
-
}
|
|
1815
|
+
pi.registerCommand("anchors", {
|
|
1816
|
+
description: "Inspect and settle active anchors",
|
|
1817
|
+
handler: async (_args, ctx) => {
|
|
1818
|
+
await openAnchorDashboard(ctx, store);
|
|
1701
1819
|
}
|
|
1702
1820
|
});
|
|
1703
1821
|
}
|