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/README.md +101 -177
- package/README_zh.md +119 -187
- package/dist/{chunk-T62PHIQI.js → chunk-G72UHPBN.js} +261 -128
- package/dist/extension.cjs +332 -178
- package/dist/extension.js +77 -51
- package/dist/index.cjs +263 -128
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/package.json +5 -4
- package/src/decay.ts +54 -52
- package/src/extension.ts +77 -58
- package/src/matcher.ts +27 -6
- package/src/settlement.ts +5 -3
- package/src/store.ts +75 -18
- package/src/tui.ts +133 -68
- 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() };
|
|
@@ -1086,8 +1163,9 @@ function groupAnchorsByQuadrant(anchors, now = Date.now()) {
|
|
|
1086
1163
|
}
|
|
1087
1164
|
return groups;
|
|
1088
1165
|
}
|
|
1166
|
+
var ANSI_REGEX = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
1089
1167
|
function stripAnsi(str) {
|
|
1090
|
-
return str.replace(
|
|
1168
|
+
return str.replace(ANSI_REGEX, "");
|
|
1091
1169
|
}
|
|
1092
1170
|
function getDisplayWidth(str) {
|
|
1093
1171
|
const clean = stripAnsi(str);
|
|
@@ -1105,23 +1183,46 @@ function getDisplayWidth(str) {
|
|
|
1105
1183
|
}
|
|
1106
1184
|
return width;
|
|
1107
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
|
+
}
|
|
1108
1194
|
function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
1109
|
-
|
|
1110
|
-
|
|
1195
|
+
if (maxWidth <= 0) return "";
|
|
1196
|
+
const singleLine = str.replace(/[\r\n\t]+/g, " ");
|
|
1197
|
+
const totalWidth = getDisplayWidth(singleLine);
|
|
1198
|
+
if (totalWidth <= maxWidth) return singleLine;
|
|
1111
1199
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
1112
|
-
const target = maxWidth - ellipsisWidth;
|
|
1113
|
-
if (target
|
|
1200
|
+
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
1201
|
+
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
1114
1202
|
let accumulated = "";
|
|
1115
1203
|
let accumWidth = 0;
|
|
1116
|
-
|
|
1117
|
-
|
|
1204
|
+
let i = 0;
|
|
1205
|
+
while (i < singleLine.length) {
|
|
1206
|
+
ANSI_REGEX.lastIndex = i;
|
|
1207
|
+
const match = ANSI_REGEX.exec(singleLine);
|
|
1208
|
+
if (match && match.index === i) {
|
|
1209
|
+
accumulated += match[0];
|
|
1210
|
+
i += match[0].length;
|
|
1211
|
+
continue;
|
|
1212
|
+
}
|
|
1213
|
+
const codePoint = singleLine.codePointAt(i) || 0;
|
|
1214
|
+
const charLen = codePoint > 65535 ? 2 : 1;
|
|
1215
|
+
const fullChar = singleLine.slice(i, i + charLen);
|
|
1216
|
+
const charWidth = getDisplayWidth(fullChar);
|
|
1118
1217
|
if (accumWidth + charWidth > target) {
|
|
1119
1218
|
break;
|
|
1120
1219
|
}
|
|
1121
|
-
accumulated +=
|
|
1220
|
+
accumulated += fullChar;
|
|
1122
1221
|
accumWidth += charWidth;
|
|
1222
|
+
i += charLen;
|
|
1123
1223
|
}
|
|
1124
|
-
|
|
1224
|
+
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
|
|
1225
|
+
return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
|
|
1125
1226
|
}
|
|
1126
1227
|
function padToWidth(str, targetWidth) {
|
|
1127
1228
|
const truncated = truncateToWidth(str, targetWidth);
|
|
@@ -1191,52 +1292,58 @@ function formatOrigin(anchor) {
|
|
|
1191
1292
|
return import_node_path4.default.basename(anchor) || "global";
|
|
1192
1293
|
}
|
|
1193
1294
|
function updateAnchorStatusBar(ctx, store) {
|
|
1194
|
-
if (!ctx
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
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 {
|
|
1199
1304
|
}
|
|
1200
|
-
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
1201
1305
|
}
|
|
1202
1306
|
function updateStartupBanner(ctx, store) {
|
|
1203
|
-
if (!ctx
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const groups = groupAnchorsByQuadrant(active);
|
|
1210
|
-
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1211
|
-
const topItems = sorted.slice(0, 3);
|
|
1212
|
-
const factory = (_tui, theme) => ({
|
|
1213
|
-
render: (width) => {
|
|
1214
|
-
const maxTitleWidth = Math.max(16, width - 36);
|
|
1215
|
-
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1216
|
-
const lines = [
|
|
1217
|
-
fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
|
|
1218
|
-
];
|
|
1219
|
-
for (let i = 0; i < topItems.length; i++) {
|
|
1220
|
-
const a = topItems[i];
|
|
1221
|
-
const num = (i + 1).toString().padStart(2, "0");
|
|
1222
|
-
const q = classifyAnchor(a);
|
|
1223
|
-
const tgt = formatTargetDate(a);
|
|
1224
|
-
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1225
|
-
lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
|
|
1226
|
-
}
|
|
1227
|
-
if (sorted.length > 3) {
|
|
1228
|
-
lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
|
|
1229
|
-
}
|
|
1230
|
-
return lines;
|
|
1231
|
-
},
|
|
1232
|
-
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;
|
|
1233
1313
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
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
|
+
}
|
|
1237
1344
|
}
|
|
1238
1345
|
async function openAnchorDashboard(ctx, store) {
|
|
1239
|
-
if (!ctx
|
|
1346
|
+
if (!hasActiveUI(ctx)) return;
|
|
1240
1347
|
const list = store.list({ status: "active", cwd: ctx.cwd });
|
|
1241
1348
|
if (list.length === 0) {
|
|
1242
1349
|
ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
|
|
@@ -1251,31 +1358,55 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1251
1358
|
];
|
|
1252
1359
|
const optionMap = /* @__PURE__ */ new Map();
|
|
1253
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;
|
|
1254
1364
|
for (let i = 0; i < sortedList.length; i++) {
|
|
1255
1365
|
const a = sortedList[i];
|
|
1256
|
-
const num = (
|
|
1366
|
+
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1257
1367
|
const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
|
|
1258
1368
|
const origin = formatOrigin(a);
|
|
1259
1369
|
const target = formatTargetDate(a);
|
|
1260
1370
|
const created = formatCreationTime(a.createdAt);
|
|
1261
1371
|
const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
|
|
1262
1372
|
const colNum = `${num} `;
|
|
1263
|
-
const colTitle = padToWidth(titleWithFiles,
|
|
1373
|
+
const colTitle = padToWidth(titleWithFiles, titleWidth);
|
|
1264
1374
|
const colOrigin = padToWidth(origin, 10);
|
|
1265
1375
|
const colTarget = padToWidth(target, 12);
|
|
1266
1376
|
const colCreated = created;
|
|
1267
|
-
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();
|
|
1268
1378
|
optionMap.set(label, a);
|
|
1269
1379
|
displayOptions.push(label);
|
|
1270
1380
|
}
|
|
1271
|
-
const selected = await ctx.ui.select("\u2316 Anchors (
|
|
1381
|
+
const selected = await ctx.ui.select("\u2316 Anchors (select to inspect or settle):", displayOptions);
|
|
1272
1382
|
if (!selected) return;
|
|
1273
1383
|
const anchor = optionMap.get(selected);
|
|
1274
1384
|
if (!anchor) return;
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1385
|
+
const action = await ctx.ui.select(
|
|
1386
|
+
`\u2316 Anchor #${anchor.id}: "${anchor.title}"`,
|
|
1387
|
+
[
|
|
1388
|
+
"\u2713 Settle (Mark completed and archive)",
|
|
1389
|
+
"\u2139 View details & associated files",
|
|
1390
|
+
"\u21A9 Cancel"
|
|
1391
|
+
]
|
|
1392
|
+
);
|
|
1393
|
+
if (!action || action.startsWith("\u21A9")) return;
|
|
1394
|
+
if (action.startsWith("\u2713")) {
|
|
1395
|
+
store.settle(anchor.id, { settledBy: "manual-command" });
|
|
1396
|
+
const successMsg = anchor.recurrence === "daily" ? `\u2316 Completed for today: "${anchor.title}" (resets tomorrow)` : `\u2316 Settled #${anchor.id}: "${anchor.title}" (/anchor undo to revert)`;
|
|
1397
|
+
ctx.ui.notify(successMsg, "info");
|
|
1398
|
+
updateAnchorStatusBar(ctx, store);
|
|
1399
|
+
} else if (action.startsWith("\u2139")) {
|
|
1400
|
+
const details = [
|
|
1401
|
+
`#${anchor.id} [${anchor.priority.toUpperCase()}] ${anchor.title}`,
|
|
1402
|
+
anchor.description ? `Description: ${anchor.description}` : null,
|
|
1403
|
+
anchor.targetDate ? `Target Date: ${anchor.targetDate} (${formatTargetDate(anchor)})` : null,
|
|
1404
|
+
anchor.files && anchor.files.length > 0 ? `Files: ${anchor.files.join(", ")}` : null,
|
|
1405
|
+
anchor.verifyCommand ? `Verify Command: ${anchor.verifyCommand}` : null,
|
|
1406
|
+
`Created: ${formatCreationTime(anchor.createdAt)} (in ${anchor.project || "global"})`
|
|
1407
|
+
].filter(Boolean).join("\n");
|
|
1408
|
+
ctx.ui.notify(details, "info");
|
|
1409
|
+
}
|
|
1279
1410
|
}
|
|
1280
1411
|
|
|
1281
1412
|
// src/extension.ts
|
|
@@ -1298,7 +1429,7 @@ function extension_default(pi) {
|
|
|
1298
1429
|
}
|
|
1299
1430
|
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1300
1431
|
for (const a of activeAnchors) {
|
|
1301
|
-
if (a.verifyCommand) {
|
|
1432
|
+
if (a.verifyCommand && (!a.cwd || isSameOrSubPath(a.cwd, ctx.cwd))) {
|
|
1302
1433
|
try {
|
|
1303
1434
|
const res = runPhysicalVerification(a, ctx.cwd);
|
|
1304
1435
|
if (res.success) {
|
|
@@ -1319,21 +1450,32 @@ function extension_default(pi) {
|
|
|
1319
1450
|
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1320
1451
|
if (import_node_fs3.default.existsSync(pendingPath)) {
|
|
1321
1452
|
const pending = JSON.parse(import_node_fs3.default.readFileSync(pendingPath, "utf-8"));
|
|
1322
|
-
|
|
1323
|
-
|
|
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);
|
|
1324
1459
|
}
|
|
1325
|
-
import_node_fs3.default.unlinkSync(pendingPath);
|
|
1326
1460
|
}
|
|
1327
1461
|
} catch (_) {
|
|
1328
1462
|
}
|
|
1329
1463
|
updateAnchorStatusBar(ctx, store);
|
|
1330
1464
|
if (event.reason !== "resume") {
|
|
1331
1465
|
updateStartupBanner(ctx, store);
|
|
1466
|
+
} else if (hasActiveUI(ctx)) {
|
|
1467
|
+
try {
|
|
1468
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1332
1471
|
}
|
|
1333
1472
|
});
|
|
1334
1473
|
pi.on("agent_start", async (_event, ctx) => {
|
|
1335
|
-
if (ctx
|
|
1336
|
-
|
|
1474
|
+
if (hasActiveUI(ctx)) {
|
|
1475
|
+
try {
|
|
1476
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1477
|
+
} catch {
|
|
1478
|
+
}
|
|
1337
1479
|
}
|
|
1338
1480
|
});
|
|
1339
1481
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
@@ -1357,7 +1499,7 @@ ${contextSnippet}`
|
|
|
1357
1499
|
}
|
|
1358
1500
|
const pathInput = event.input?.path;
|
|
1359
1501
|
if (typeof pathInput !== "string") return;
|
|
1360
|
-
const touchedPath = normalizePath(pathInput);
|
|
1502
|
+
const touchedPath = normalizePath(pathInput, ctx.cwd);
|
|
1361
1503
|
const anchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active" || a.status === "sleeping");
|
|
1362
1504
|
const matches = findMatchedAnchors(anchors, [touchedPath]);
|
|
1363
1505
|
if (matches.length > 0) {
|
|
@@ -1385,20 +1527,37 @@ ${contextSnippet}`
|
|
|
1385
1527
|
}
|
|
1386
1528
|
}
|
|
1387
1529
|
});
|
|
1388
|
-
pi.on("session_shutdown", async (
|
|
1530
|
+
pi.on("session_shutdown", async (event, ctx) => {
|
|
1531
|
+
let gitStatus = "";
|
|
1389
1532
|
try {
|
|
1390
|
-
|
|
1533
|
+
gitStatus = (0, import_node_child_process2.execSync)("git status --porcelain", {
|
|
1391
1534
|
cwd: ctx.cwd,
|
|
1392
1535
|
encoding: "utf-8",
|
|
1393
|
-
timeout:
|
|
1536
|
+
timeout: 2e3,
|
|
1394
1537
|
stdio: ["ignore", "pipe", "ignore"]
|
|
1395
|
-
});
|
|
1396
|
-
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);
|
|
1397
1540
|
for (const f of changed) {
|
|
1398
1541
|
observer.addTouchedFile(f);
|
|
1399
1542
|
}
|
|
1400
1543
|
} catch {
|
|
1401
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
|
+
}
|
|
1402
1561
|
const modified = observer.getModifiedFiles();
|
|
1403
1562
|
if (modified.length === 0) return;
|
|
1404
1563
|
const proposals = generateSettlementProposals(store, modified, ctx.cwd);
|
|
@@ -1412,7 +1571,12 @@ ${contextSnippet}`
|
|
|
1412
1571
|
summary: `Auto-settled via Git commit: ${commitMatch.message}`,
|
|
1413
1572
|
touchedFiles: prop.matchedFiles
|
|
1414
1573
|
});
|
|
1415
|
-
|
|
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
|
+
}
|
|
1416
1580
|
continue;
|
|
1417
1581
|
}
|
|
1418
1582
|
if (a.verifyCommand) {
|
|
@@ -1423,22 +1587,30 @@ ${contextSnippet}`
|
|
|
1423
1587
|
summary: `Automated test passed: ${a.verifyCommand}`,
|
|
1424
1588
|
touchedFiles: prop.matchedFiles
|
|
1425
1589
|
});
|
|
1426
|
-
|
|
1590
|
+
if (hasActiveUI(ctx)) {
|
|
1591
|
+
try {
|
|
1592
|
+
ctx.ui.notify(`\u2316 Test passed (${a.verifyCommand}), auto-settled #${a.id}`, "info");
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1427
1596
|
continue;
|
|
1428
1597
|
}
|
|
1429
1598
|
}
|
|
1430
|
-
if (!ctx
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
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(", ")}).
|
|
1434
1604
|
Mark as completed and archive?`
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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 {
|
|
1442
1614
|
}
|
|
1443
1615
|
}
|
|
1444
1616
|
});
|
|
@@ -1488,7 +1660,7 @@ Mark as completed and archive?`
|
|
|
1488
1660
|
if (args.priority) title += ` ${theme.fg("muted", `[${args.priority}]`)}`;
|
|
1489
1661
|
}
|
|
1490
1662
|
return {
|
|
1491
|
-
render: () => [title],
|
|
1663
|
+
render: (width) => [truncateToWidth(title, width)],
|
|
1492
1664
|
invalidate: () => {
|
|
1493
1665
|
}
|
|
1494
1666
|
};
|
|
@@ -1497,14 +1669,14 @@ Mark as completed and archive?`
|
|
|
1497
1669
|
if (result.isError) {
|
|
1498
1670
|
const errText = result.content?.[0]?.text || "Execution failed";
|
|
1499
1671
|
return {
|
|
1500
|
-
render: () => [theme.fg("error", `\u2717 ${errText}`)],
|
|
1672
|
+
render: (width) => [truncateToWidth(theme.fg("error", `\u2717 ${errText}`), width)],
|
|
1501
1673
|
invalidate: () => {
|
|
1502
1674
|
}
|
|
1503
1675
|
};
|
|
1504
1676
|
}
|
|
1505
1677
|
const text = result.content?.[0]?.text || "OK";
|
|
1506
1678
|
return {
|
|
1507
|
-
render: () => [theme.fg("success", `\u2714 ${text}`)],
|
|
1679
|
+
render: (width) => [truncateToWidth(theme.fg("success", `\u2714 ${text}`), width)],
|
|
1508
1680
|
invalidate: () => {
|
|
1509
1681
|
}
|
|
1510
1682
|
};
|
|
@@ -1640,28 +1812,10 @@ Mark as completed and archive?`
|
|
|
1640
1812
|
updateAnchorStatusBar(ctx, store);
|
|
1641
1813
|
}
|
|
1642
1814
|
});
|
|
1643
|
-
pi.
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
cwd: ctx.cwd,
|
|
1648
|
-
encoding: "utf-8",
|
|
1649
|
-
timeout: 2e3,
|
|
1650
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
1651
|
-
}).trim();
|
|
1652
|
-
const activeAnchors = store.list({ cwd: ctx.cwd }).filter((a) => a.status === "active");
|
|
1653
|
-
if (gitStatus && activeAnchors.length === 0) {
|
|
1654
|
-
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1655
|
-
const pendingPath = import_node_path5.default.join(home, ".anchor", "pending_exit.json");
|
|
1656
|
-
const data = {
|
|
1657
|
-
cwd: ctx.cwd,
|
|
1658
|
-
timestamp: Date.now(),
|
|
1659
|
-
modifiedCount: gitStatus.split("\n").filter(Boolean).length
|
|
1660
|
-
};
|
|
1661
|
-
import_node_fs3.default.writeFileSync(pendingPath, JSON.stringify(data, null, 2), "utf-8");
|
|
1662
|
-
}
|
|
1663
|
-
} catch (_) {
|
|
1664
|
-
}
|
|
1815
|
+
pi.registerCommand("anchors", {
|
|
1816
|
+
description: "Inspect and settle active anchors",
|
|
1817
|
+
handler: async (_args, ctx) => {
|
|
1818
|
+
await openAnchorDashboard(ctx, store);
|
|
1665
1819
|
}
|
|
1666
1820
|
});
|
|
1667
1821
|
}
|