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/index.cjs
CHANGED
|
@@ -58,6 +58,8 @@ __export(index_exports, {
|
|
|
58
58
|
getTodayDateString: () => getTodayDateString,
|
|
59
59
|
globToRegExp: () => globToRegExp,
|
|
60
60
|
groupAnchorsByQuadrant: () => groupAnchorsByQuadrant,
|
|
61
|
+
hasActiveUI: () => hasActiveUI,
|
|
62
|
+
isSameOrSubPath: () => isSameOrSubPath,
|
|
61
63
|
makeSafeTaskAnnotation: () => makeSafeTaskAnnotation,
|
|
62
64
|
matchAnchorAgainstTouchedFiles: () => matchAnchorAgainstTouchedFiles,
|
|
63
65
|
normalizePath: () => normalizePath,
|
|
@@ -103,20 +105,34 @@ var import_node_os = __toESM(require("os"), 1);
|
|
|
103
105
|
// src/matcher.ts
|
|
104
106
|
var import_node_path = __toESM(require("path"), 1);
|
|
105
107
|
function globToRegExp(pattern) {
|
|
106
|
-
const
|
|
108
|
+
const collapsed = pattern.slice(0, 512).replace(/\/+(\*\*\/+)+/g, "/**/").replace(/\*{3,}/g, "**");
|
|
109
|
+
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, ".*");
|
|
107
110
|
return new RegExp(`^${escaped}$`);
|
|
108
111
|
}
|
|
109
112
|
function normalizePath(filePath, baseCwd) {
|
|
110
113
|
if (typeof filePath !== "string") return "";
|
|
111
114
|
let norm = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
112
|
-
if (baseCwd && (import_node_path.default.isAbsolute(filePath) || norm.startsWith("/"))) {
|
|
113
|
-
const normBase = baseCwd.trim().replace(/\\/g, "/");
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
if (baseCwd && (import_node_path.default.isAbsolute(filePath) || norm.startsWith("/") || /^[a-zA-Z]:\//.test(norm))) {
|
|
116
|
+
const normBase = baseCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
117
|
+
const lowerNorm = norm.toLowerCase();
|
|
118
|
+
const lowerBase = normBase.toLowerCase();
|
|
119
|
+
if (lowerNorm === lowerBase) {
|
|
120
|
+
norm = "";
|
|
121
|
+
} else if (lowerNorm.startsWith(lowerBase + "/")) {
|
|
122
|
+
norm = norm.slice(normBase.length + 1).replace(/^\/+/, "");
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
return norm;
|
|
119
126
|
}
|
|
127
|
+
function isSameOrSubPath(anchorCwd, targetCwd) {
|
|
128
|
+
if (!anchorCwd || !targetCwd) return false;
|
|
129
|
+
const normA = anchorCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
130
|
+
const normT = targetCwd.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
131
|
+
const isWinStyle = process.platform === "win32" || /^[a-zA-Z]:\//.test(normA) || /^[a-zA-Z]:\//.test(normT);
|
|
132
|
+
const cmpA = isWinStyle ? normA.toLowerCase() : normA;
|
|
133
|
+
const cmpT = isWinStyle ? normT.toLowerCase() : normT;
|
|
134
|
+
return cmpA === cmpT || cmpT.startsWith(cmpA + "/") || cmpA.startsWith(cmpT + "/");
|
|
135
|
+
}
|
|
120
136
|
function matchAnchorAgainstTouchedFiles(anchor, touchedFiles) {
|
|
121
137
|
if (!anchor.files || anchor.files.length === 0) return null;
|
|
122
138
|
const normTouched = touchedFiles.map((f) => normalizePath(f, anchor.cwd)).filter(Boolean);
|
|
@@ -478,9 +494,19 @@ var AnchorStore = class {
|
|
|
478
494
|
}
|
|
479
495
|
try {
|
|
480
496
|
const data = JSON.parse(raw);
|
|
481
|
-
if (!Array.isArray(data.anchors)) {
|
|
497
|
+
if (!data || typeof data !== "object" || !Array.isArray(data.anchors)) {
|
|
482
498
|
throw new Error("Invalid state: anchors must be array");
|
|
483
499
|
}
|
|
500
|
+
data.anchors = data.anchors.filter((a) => a && typeof a === "object" && typeof a.id === "string" && typeof a.title === "string").map((a) => ({
|
|
501
|
+
...a,
|
|
502
|
+
files: Array.isArray(a.files) ? a.files.filter((f) => typeof f === "string") : [],
|
|
503
|
+
tags: Array.isArray(a.tags) ? a.tags.filter((t) => typeof t === "string") : [],
|
|
504
|
+
decay: {
|
|
505
|
+
activeDays: Number.isFinite(a.decay?.activeDays) && a.decay.activeDays > 0 ? a.decay.activeDays : DURABLE_DECAY_POLICY.activeDays,
|
|
506
|
+
sleepDays: Number.isFinite(a.decay?.sleepDays) && a.decay.sleepDays > 0 ? a.decay.sleepDays : DURABLE_DECAY_POLICY.sleepDays,
|
|
507
|
+
graveyardDays: Number.isFinite(a.decay?.graveyardDays) && a.decay.graveyardDays > 0 ? a.decay.graveyardDays : DURABLE_DECAY_POLICY.graveyardDays
|
|
508
|
+
}
|
|
509
|
+
}));
|
|
484
510
|
return data;
|
|
485
511
|
} catch {
|
|
486
512
|
const backupPath = import_node_path2.default.join(this.storageDir, `state.corrupt.${Date.now()}.json`);
|
|
@@ -500,16 +526,19 @@ var AnchorStore = class {
|
|
|
500
526
|
*/
|
|
501
527
|
saveState(state) {
|
|
502
528
|
this.ensureDirs();
|
|
503
|
-
const
|
|
529
|
+
const rand = Math.random().toString(36).slice(2, 7);
|
|
530
|
+
const tempPath = import_node_path2.default.join(this.storageDir, `state.tmp.${process.pid}.${Date.now()}.${rand}`);
|
|
504
531
|
import_node_fs.default.writeFileSync(tempPath, JSON.stringify(state, null, 2), "utf-8");
|
|
505
532
|
atomicRenameWithRetry(tempPath, this.statePath);
|
|
506
533
|
}
|
|
507
|
-
generateId(
|
|
508
|
-
const numbers =
|
|
534
|
+
generateId(state) {
|
|
535
|
+
const numbers = state.anchors.map((a) => {
|
|
509
536
|
const match = a.id.match(/^anc-(\d+)$/);
|
|
510
537
|
return match ? parseInt(match[1], 10) : 0;
|
|
511
538
|
}).filter((n) => n > 0);
|
|
512
|
-
const
|
|
539
|
+
const maxExisting = numbers.length > 0 ? Math.max(...numbers) : 0;
|
|
540
|
+
const nextNum = Math.max(maxExisting + 1, state.nextId || 1);
|
|
541
|
+
state.nextId = nextNum + 1;
|
|
513
542
|
return `anc-${nextNum}`;
|
|
514
543
|
}
|
|
515
544
|
/**
|
|
@@ -519,7 +548,7 @@ var AnchorStore = class {
|
|
|
519
548
|
return this.withLock(() => {
|
|
520
549
|
const state = this.loadState();
|
|
521
550
|
const now = Date.now();
|
|
522
|
-
const id = this.generateId(state
|
|
551
|
+
const id = this.generateId(state);
|
|
523
552
|
const cwd = input.cwd ? normalizePath(input.cwd) : "";
|
|
524
553
|
const projectName = input.project ? input.project.trim() : cwd ? import_node_path2.default.basename(cwd) : "global";
|
|
525
554
|
const recurrence = input.recurrence || detectRecurrence(input.title);
|
|
@@ -579,7 +608,7 @@ var AnchorStore = class {
|
|
|
579
608
|
if (filter?.status && a.status !== filter.status) return false;
|
|
580
609
|
if (filter?.priority && a.priority !== filter.priority) return false;
|
|
581
610
|
if (!filter?.all && targetCwd) {
|
|
582
|
-
if (a.files.length > 0 && a.cwd && a.cwd
|
|
611
|
+
if (a.files.length > 0 && a.cwd && !isSameOrSubPath(a.cwd, targetCwd)) {
|
|
583
612
|
return false;
|
|
584
613
|
}
|
|
585
614
|
}
|
|
@@ -680,18 +709,46 @@ var AnchorStore = class {
|
|
|
680
709
|
throw new Error("No archived anchors to undo");
|
|
681
710
|
}
|
|
682
711
|
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
683
|
-
const lines = raw.split(
|
|
684
|
-
|
|
712
|
+
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
713
|
+
let anchor = null;
|
|
714
|
+
while (lines.length > 0 && !anchor) {
|
|
715
|
+
const candidate = lines.pop();
|
|
716
|
+
try {
|
|
717
|
+
const parsed = JSON.parse(candidate);
|
|
718
|
+
if (parsed && typeof parsed === "object" && typeof parsed.id === "string" && typeof parsed.title === "string") {
|
|
719
|
+
anchor = parsed;
|
|
720
|
+
}
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
if (!anchor) {
|
|
685
725
|
throw new Error("Archive is empty, nothing to undo");
|
|
686
726
|
}
|
|
687
|
-
const lastLine = lines.pop();
|
|
688
|
-
const anchor = JSON.parse(lastLine);
|
|
689
727
|
import_node_fs.default.writeFileSync(this.archivePath, lines.length > 0 ? lines.join("\n") + "\n" : "", "utf-8");
|
|
690
728
|
anchor.status = "active";
|
|
691
729
|
anchor.updatedAt = Date.now();
|
|
692
730
|
delete anchor.evidence;
|
|
731
|
+
delete anchor.settledForDate;
|
|
732
|
+
if (anchor.recurrence === "daily") {
|
|
733
|
+
delete anchor.lastCompletedDate;
|
|
734
|
+
}
|
|
693
735
|
const state = this.loadState();
|
|
694
|
-
state.anchors.
|
|
736
|
+
const existingIdx = state.anchors.findIndex((a) => a.id === anchor.id);
|
|
737
|
+
if (existingIdx !== -1) {
|
|
738
|
+
if (state.anchors[existingIdx].title === anchor.title) {
|
|
739
|
+
state.anchors[existingIdx] = anchor;
|
|
740
|
+
} else {
|
|
741
|
+
anchor.id = this.generateId(state);
|
|
742
|
+
state.anchors.push(anchor);
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
const match = anchor.id.match(/^anc-(\d+)$/);
|
|
746
|
+
if (match) {
|
|
747
|
+
const restoredNum = parseInt(match[1], 10);
|
|
748
|
+
state.nextId = Math.max(state.nextId || 1, restoredNum + 1);
|
|
749
|
+
}
|
|
750
|
+
state.anchors.push(anchor);
|
|
751
|
+
}
|
|
695
752
|
this.saveState(state);
|
|
696
753
|
return anchor;
|
|
697
754
|
});
|
|
@@ -723,10 +780,19 @@ var AnchorStore = class {
|
|
|
723
780
|
if (!import_node_fs.default.existsSync(this.archivePath)) return [];
|
|
724
781
|
try {
|
|
725
782
|
const raw = import_node_fs.default.readFileSync(this.archivePath, "utf-8");
|
|
726
|
-
const all =
|
|
783
|
+
const all = [];
|
|
784
|
+
for (const l of raw.split(/\r?\n/)) {
|
|
785
|
+
const trimmed = l.trim();
|
|
786
|
+
if (!trimmed) continue;
|
|
787
|
+
try {
|
|
788
|
+
const parsed = JSON.parse(trimmed);
|
|
789
|
+
if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
|
|
790
|
+
} catch {
|
|
791
|
+
}
|
|
792
|
+
}
|
|
727
793
|
if (filter?.all || !filter?.cwd) return all;
|
|
728
794
|
const targetCwd = normalizePath(filter.cwd);
|
|
729
|
-
return all.filter((a) => !a.cwd || a.cwd
|
|
795
|
+
return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
|
|
730
796
|
} catch {
|
|
731
797
|
return [];
|
|
732
798
|
}
|
|
@@ -735,10 +801,19 @@ var AnchorStore = class {
|
|
|
735
801
|
if (!import_node_fs.default.existsSync(this.graveyardPath)) return [];
|
|
736
802
|
try {
|
|
737
803
|
const raw = import_node_fs.default.readFileSync(this.graveyardPath, "utf-8");
|
|
738
|
-
const all =
|
|
804
|
+
const all = [];
|
|
805
|
+
for (const l of raw.split(/\r?\n/)) {
|
|
806
|
+
const trimmed = l.trim();
|
|
807
|
+
if (!trimmed) continue;
|
|
808
|
+
try {
|
|
809
|
+
const parsed = JSON.parse(trimmed);
|
|
810
|
+
if (parsed && typeof parsed === "object" && typeof parsed.id === "string") all.push(parsed);
|
|
811
|
+
} catch {
|
|
812
|
+
}
|
|
813
|
+
}
|
|
739
814
|
if (filter?.all || !filter?.cwd) return all;
|
|
740
815
|
const targetCwd = normalizePath(filter.cwd);
|
|
741
|
-
return all.filter((a) => !a.cwd || a.cwd
|
|
816
|
+
return all.filter((a) => !a.cwd || isSameOrSubPath(a.cwd, targetCwd));
|
|
742
817
|
} catch {
|
|
743
818
|
return [];
|
|
744
819
|
}
|
|
@@ -779,69 +854,73 @@ function evaluateAnchorDecay(anchor, now = Date.now()) {
|
|
|
779
854
|
};
|
|
780
855
|
}
|
|
781
856
|
function sweepStore(store, now = Date.now()) {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
if (evalResult.nextStatus
|
|
796
|
-
anchor.status
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
anchor.status
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
857
|
+
return store.withLock(() => {
|
|
858
|
+
const state = store.loadState();
|
|
859
|
+
const result = {
|
|
860
|
+
transitionedToSleeping: [],
|
|
861
|
+
wokenToActive: [],
|
|
862
|
+
evictedToGraveyard: []
|
|
863
|
+
};
|
|
864
|
+
const toEvict = [];
|
|
865
|
+
let stateModified = false;
|
|
866
|
+
for (const anchor of state.anchors) {
|
|
867
|
+
const evalResult = evaluateAnchorDecay(anchor, now);
|
|
868
|
+
if (evalResult.nextStatus === "graveyard") {
|
|
869
|
+
toEvict.push(anchor);
|
|
870
|
+
} else if (evalResult.nextStatus !== anchor.status) {
|
|
871
|
+
if (evalResult.nextStatus === "sleeping" && anchor.status === "active") {
|
|
872
|
+
anchor.status = "sleeping";
|
|
873
|
+
anchor.updatedAt = now;
|
|
874
|
+
result.transitionedToSleeping.push(anchor.id);
|
|
875
|
+
stateModified = true;
|
|
876
|
+
} else if (evalResult.nextStatus === "active" && anchor.status === "sleeping") {
|
|
877
|
+
anchor.status = "active";
|
|
878
|
+
anchor.updatedAt = now;
|
|
879
|
+
result.wokenToActive.push(anchor.id);
|
|
880
|
+
stateModified = true;
|
|
881
|
+
}
|
|
805
882
|
}
|
|
806
883
|
}
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
884
|
+
if (toEvict.length > 0) {
|
|
885
|
+
const evictIds = new Set(toEvict.map((e) => e.id));
|
|
886
|
+
state.anchors = state.anchors.filter((a) => !evictIds.has(a.id));
|
|
887
|
+
stateModified = true;
|
|
888
|
+
const graveyardLines = toEvict.map((exp) => {
|
|
889
|
+
const rec = {
|
|
890
|
+
...exp,
|
|
891
|
+
status: "graveyard",
|
|
892
|
+
updatedAt: now,
|
|
893
|
+
evictedAt: now,
|
|
894
|
+
evictionReason: `Exceeded decay threshold (${exp.decay.graveyardDays} days untouched)`
|
|
895
|
+
};
|
|
896
|
+
return JSON.stringify(rec);
|
|
897
|
+
}).join("\n") + "\n";
|
|
898
|
+
import_node_fs2.default.appendFileSync(store.graveyardPath, graveyardLines, "utf-8");
|
|
899
|
+
for (const exp of toEvict) {
|
|
900
|
+
result.evictedToGraveyard.push(exp.id);
|
|
901
|
+
}
|
|
825
902
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
903
|
+
state.lastSweepAt = now;
|
|
904
|
+
if (stateModified) {
|
|
905
|
+
store.saveState(state);
|
|
906
|
+
}
|
|
907
|
+
return result;
|
|
908
|
+
});
|
|
832
909
|
}
|
|
833
910
|
|
|
834
911
|
// src/settlement.ts
|
|
835
912
|
var import_node_child_process = require("child_process");
|
|
836
913
|
function runPhysicalVerification(anchor, cwd) {
|
|
837
|
-
if (!anchor.verifyCommand) {
|
|
914
|
+
if (!anchor.verifyCommand?.trim()) {
|
|
838
915
|
return { success: false, output: "No verification command specified" };
|
|
839
916
|
}
|
|
917
|
+
const targetCwd = anchor.cwd || cwd;
|
|
840
918
|
try {
|
|
841
919
|
const stdout = (0, import_node_child_process.execSync)(anchor.verifyCommand, {
|
|
842
|
-
cwd,
|
|
920
|
+
cwd: targetCwd,
|
|
843
921
|
encoding: "utf-8",
|
|
844
|
-
timeout:
|
|
922
|
+
timeout: 8e3,
|
|
923
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
845
924
|
stdio: ["ignore", "pipe", "pipe"]
|
|
846
925
|
});
|
|
847
926
|
return { success: true, output: stdout.trim() };
|
|
@@ -930,27 +1009,36 @@ function getDisplayWidth(str) {
|
|
|
930
1009
|
}
|
|
931
1010
|
return width;
|
|
932
1011
|
}
|
|
1012
|
+
function hasActiveUI(ctx) {
|
|
1013
|
+
if (!ctx) return false;
|
|
1014
|
+
try {
|
|
1015
|
+
return Boolean(ctx.hasUI && ctx.ui);
|
|
1016
|
+
} catch {
|
|
1017
|
+
return false;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
933
1020
|
function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
934
1021
|
if (maxWidth <= 0) return "";
|
|
935
|
-
const
|
|
936
|
-
|
|
1022
|
+
const singleLine = str.replace(/[\r\n\t]+/g, " ");
|
|
1023
|
+
const totalWidth = getDisplayWidth(singleLine);
|
|
1024
|
+
if (totalWidth <= maxWidth) return singleLine;
|
|
937
1025
|
const ellipsisWidth = getDisplayWidth(ellipsis);
|
|
938
1026
|
const target = Math.max(0, maxWidth - ellipsisWidth);
|
|
939
1027
|
if (target === 0) return ellipsis.slice(0, maxWidth);
|
|
940
1028
|
let accumulated = "";
|
|
941
1029
|
let accumWidth = 0;
|
|
942
1030
|
let i = 0;
|
|
943
|
-
while (i <
|
|
1031
|
+
while (i < singleLine.length) {
|
|
944
1032
|
ANSI_REGEX.lastIndex = i;
|
|
945
|
-
const match = ANSI_REGEX.exec(
|
|
1033
|
+
const match = ANSI_REGEX.exec(singleLine);
|
|
946
1034
|
if (match && match.index === i) {
|
|
947
1035
|
accumulated += match[0];
|
|
948
1036
|
i += match[0].length;
|
|
949
1037
|
continue;
|
|
950
1038
|
}
|
|
951
|
-
const codePoint =
|
|
1039
|
+
const codePoint = singleLine.codePointAt(i) || 0;
|
|
952
1040
|
const charLen = codePoint > 65535 ? 2 : 1;
|
|
953
|
-
const fullChar =
|
|
1041
|
+
const fullChar = singleLine.slice(i, i + charLen);
|
|
954
1042
|
const charWidth = getDisplayWidth(fullChar);
|
|
955
1043
|
if (accumWidth + charWidth > target) {
|
|
956
1044
|
break;
|
|
@@ -960,7 +1048,7 @@ function truncateToWidth(str, maxWidth, ellipsis = "\u2026") {
|
|
|
960
1048
|
i += charLen;
|
|
961
1049
|
}
|
|
962
1050
|
accumulated = accumulated.replace(/\s*[\(\[\{(【]$/, "");
|
|
963
|
-
return accumulated + ellipsis + (
|
|
1051
|
+
return accumulated + ellipsis + (singleLine.includes("\x1B") ? "\x1B[0m" : "");
|
|
964
1052
|
}
|
|
965
1053
|
function padToWidth(str, targetWidth) {
|
|
966
1054
|
const truncated = truncateToWidth(str, targetWidth);
|
|
@@ -1036,52 +1124,58 @@ function formatOrigin(anchor) {
|
|
|
1036
1124
|
return import_node_path3.default.basename(anchor) || "global";
|
|
1037
1125
|
}
|
|
1038
1126
|
function updateAnchorStatusBar(ctx, store) {
|
|
1039
|
-
if (!ctx
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1127
|
+
if (!hasActiveUI(ctx)) return;
|
|
1128
|
+
try {
|
|
1129
|
+
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1130
|
+
if (active.length === 0) {
|
|
1131
|
+
ctx.ui.setStatus("anchor", void 0);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
1135
|
+
} catch {
|
|
1044
1136
|
}
|
|
1045
|
-
ctx.ui.setStatus("anchor", `\u2316 ${active.length}`);
|
|
1046
1137
|
}
|
|
1047
1138
|
function updateStartupBanner(ctx, store) {
|
|
1048
|
-
if (!ctx
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const groups = groupAnchorsByQuadrant(active);
|
|
1055
|
-
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1056
|
-
const topItems = sorted.slice(0, 3);
|
|
1057
|
-
const factory = (_tui, theme) => ({
|
|
1058
|
-
render: (width) => {
|
|
1059
|
-
const maxTitleWidth = Math.max(16, width - 36);
|
|
1060
|
-
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1061
|
-
const lines = [
|
|
1062
|
-
fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`)
|
|
1063
|
-
];
|
|
1064
|
-
for (let i = 0; i < topItems.length; i++) {
|
|
1065
|
-
const a = topItems[i];
|
|
1066
|
-
const num = (i + 1).toString().padStart(2, "0");
|
|
1067
|
-
const q = classifyAnchor(a);
|
|
1068
|
-
const tgt = formatTargetDate(a);
|
|
1069
|
-
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1070
|
-
lines.push(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`);
|
|
1071
|
-
}
|
|
1072
|
-
if (sorted.length > 3) {
|
|
1073
|
-
lines.push(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`));
|
|
1074
|
-
}
|
|
1075
|
-
return lines;
|
|
1076
|
-
},
|
|
1077
|
-
invalidate: () => {
|
|
1139
|
+
if (!hasActiveUI(ctx)) return;
|
|
1140
|
+
try {
|
|
1141
|
+
const active = store.list({ status: "active", cwd: ctx.cwd });
|
|
1142
|
+
if (active.length === 0) {
|
|
1143
|
+
ctx.ui.setWidget("anchor-startup", void 0);
|
|
1144
|
+
return;
|
|
1078
1145
|
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1146
|
+
const groups = groupAnchorsByQuadrant(active);
|
|
1147
|
+
const sorted = [...groups.today, ...groups.upcoming, ...groups.habits, ...groups.backlog];
|
|
1148
|
+
const topItems = sorted.slice(0, 3);
|
|
1149
|
+
const factory = (_tui, theme) => ({
|
|
1150
|
+
render: (width) => {
|
|
1151
|
+
const maxTitleWidth = Math.max(12, width - 36);
|
|
1152
|
+
const fg = theme?.fg ? theme.fg.bind(theme) : (_c, text) => text;
|
|
1153
|
+
const lines = [
|
|
1154
|
+
truncateToWidth(fg("accent", `\u2316 Anchor \xB7 ${active.length} active commitments:`), width)
|
|
1155
|
+
];
|
|
1156
|
+
for (let i = 0; i < topItems.length; i++) {
|
|
1157
|
+
const a = topItems[i];
|
|
1158
|
+
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1159
|
+
const q = classifyAnchor(a);
|
|
1160
|
+
const tgt = formatTargetDate(a);
|
|
1161
|
+
const titlePadded = truncateToWidth(a.title, maxTitleWidth);
|
|
1162
|
+
lines.push(truncateToWidth(` \u2022 ${fg("muted", `[${q}]`)} ${fg("dim", num)} ${titlePadded} ${fg("dim", `(${tgt})`)}`, width));
|
|
1163
|
+
}
|
|
1164
|
+
if (sorted.length > 3) {
|
|
1165
|
+
lines.push(truncateToWidth(fg("dim", ` (+${sorted.length - 3} more \xB7 run /anchor to inspect)`), width));
|
|
1166
|
+
}
|
|
1167
|
+
return lines;
|
|
1168
|
+
},
|
|
1169
|
+
invalidate: () => {
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
const content = ctx.ui?.theme ? factory : factory().render(80);
|
|
1173
|
+
ctx.ui.setWidget("anchor-startup", content, { placement: "aboveEditor" });
|
|
1174
|
+
} catch {
|
|
1175
|
+
}
|
|
1082
1176
|
}
|
|
1083
1177
|
async function openAnchorDashboard(ctx, store) {
|
|
1084
|
-
if (!ctx
|
|
1178
|
+
if (!hasActiveUI(ctx)) return;
|
|
1085
1179
|
const list = store.list({ status: "active", cwd: ctx.cwd });
|
|
1086
1180
|
if (list.length === 0) {
|
|
1087
1181
|
ctx.ui.notify("\u2316 No active anchors. Use /pin <task> to record.", "info");
|
|
@@ -1096,20 +1190,23 @@ async function openAnchorDashboard(ctx, store) {
|
|
|
1096
1190
|
];
|
|
1097
1191
|
const optionMap = /* @__PURE__ */ new Map();
|
|
1098
1192
|
const displayOptions = [];
|
|
1193
|
+
const termCols = process.stdout?.columns || 96;
|
|
1194
|
+
const isNarrow = termCols < 88;
|
|
1195
|
+
const titleWidth = isNarrow ? Math.max(18, termCols - 36) : 34;
|
|
1099
1196
|
for (let i = 0; i < sortedList.length; i++) {
|
|
1100
1197
|
const a = sortedList[i];
|
|
1101
|
-
const num = (
|
|
1198
|
+
const num = a.id.replace(/^anc-/, "").padStart(2, "0");
|
|
1102
1199
|
const category = padToWidth(`[${classifyAnchor(a)}]`, 12);
|
|
1103
1200
|
const origin = formatOrigin(a);
|
|
1104
1201
|
const target = formatTargetDate(a);
|
|
1105
1202
|
const created = formatCreationTime(a.createdAt);
|
|
1106
1203
|
const titleWithFiles = a.files && a.files.length > 0 ? `${a.title} [${a.files.slice(0, 1).join(", ")}]` : a.title;
|
|
1107
1204
|
const colNum = `${num} `;
|
|
1108
|
-
const colTitle = padToWidth(titleWithFiles,
|
|
1205
|
+
const colTitle = padToWidth(titleWithFiles, titleWidth);
|
|
1109
1206
|
const colOrigin = padToWidth(origin, 10);
|
|
1110
1207
|
const colTarget = padToWidth(target, 12);
|
|
1111
1208
|
const colCreated = created;
|
|
1112
|
-
const label = `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
1209
|
+
const label = isNarrow ? `${colNum}${category}${colTitle} ${colTarget}`.trimEnd() : `${colNum}${category}${colTitle} ${colOrigin} ${colTarget} ${colCreated}`.trimEnd();
|
|
1113
1210
|
optionMap.set(label, a);
|
|
1114
1211
|
displayOptions.push(label);
|
|
1115
1212
|
}
|
|
@@ -1466,6 +1563,8 @@ var AnchorProtocol = class {
|
|
|
1466
1563
|
getTodayDateString,
|
|
1467
1564
|
globToRegExp,
|
|
1468
1565
|
groupAnchorsByQuadrant,
|
|
1566
|
+
hasActiveUI,
|
|
1567
|
+
isSameOrSubPath,
|
|
1469
1568
|
makeSafeTaskAnnotation,
|
|
1470
1569
|
matchAnchorAgainstTouchedFiles,
|
|
1471
1570
|
normalizePath,
|
package/dist/index.d.cts
CHANGED
|
@@ -66,6 +66,7 @@ interface AnchorStoreState {
|
|
|
66
66
|
version: number;
|
|
67
67
|
anchors: Anchor[];
|
|
68
68
|
lastSweepAt: number;
|
|
69
|
+
nextId?: number;
|
|
69
70
|
}
|
|
70
71
|
interface TouchMatchResult {
|
|
71
72
|
anchor: Anchor;
|
|
@@ -236,6 +237,10 @@ declare function globToRegExp(pattern: string): RegExp;
|
|
|
236
237
|
* Normalize path separators to POSIX forward slashes and optionally relative to baseCwd
|
|
237
238
|
*/
|
|
238
239
|
declare function normalizePath(filePath: string, baseCwd?: string): string;
|
|
240
|
+
/**
|
|
241
|
+
* Compare two workspace CWDs safely (case-insensitive on Windows, supports subdirectory scoping)
|
|
242
|
+
*/
|
|
243
|
+
declare function isSameOrSubPath(anchorCwd?: string, targetCwd?: string): boolean;
|
|
239
244
|
/**
|
|
240
245
|
* Match touched files against an anchor's registered physical patterns
|
|
241
246
|
* Only anchors with explicit files/globs/prefixes are matched against file touches.
|
|
@@ -298,6 +303,7 @@ declare function stripAnsi(str: string): string;
|
|
|
298
303
|
* - Standard ASCII (width 1)
|
|
299
304
|
*/
|
|
300
305
|
declare function getDisplayWidth(str: string): number;
|
|
306
|
+
declare function hasActiveUI(ctx: ExtensionContext | null | undefined): ctx is ExtensionContext;
|
|
301
307
|
/**
|
|
302
308
|
* Truncate string to target terminal visual width with ellipsis
|
|
303
309
|
*/
|
|
@@ -445,4 +451,4 @@ declare class AnchorProtocol {
|
|
|
445
451
|
};
|
|
446
452
|
}
|
|
447
453
|
|
|
448
|
-
export { type AgentAdapter, type Anchor, type AnchorDecayPolicy, type AnchorDurability, type AnchorEvidence, type AnchorPriority, AnchorProtocol, type AnchorQuadrant, type AnchorStatus, AnchorStore, type AnchorStoreState, COMMENT_FORMATS, DEFAULT_DECAY_POLICY, DURABLE_DECAY_POLICY, type DecayEvaluation, EPHEMERAL_DECAY_POLICY, type GroupedAnchors, SessionTouchObserver, type SettlementProposal, type SweepResult, type TouchMatchResult, acquireSyncLock, atomicRenameWithRetry, classifyAnchor, detectDurability, detectRecurrence, detectTargetDate, evaluateAnchorDecay, findMatchedAnchors, formatCreationTime, formatOrigin, formatRelativeTime, formatRemainingTtl, formatSettlementCard, formatTargetDate, generateSettlementProposals, getDefaultStorageDir, getDisplayWidth, getEphemeralDecayPolicy, getTodayDateString, globToRegExp, groupAnchorsByQuadrant, makeSafeTaskAnnotation, matchAnchorAgainstTouchedFiles, normalizePath, openAnchorDashboard, padToWidth, renderActiveAnchorsContext, renderColdStartAnchorsContext, runPhysicalVerification, stripAnsi, sweepStore, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
|
|
454
|
+
export { type AgentAdapter, type Anchor, type AnchorDecayPolicy, type AnchorDurability, type AnchorEvidence, type AnchorPriority, AnchorProtocol, type AnchorQuadrant, type AnchorStatus, AnchorStore, type AnchorStoreState, COMMENT_FORMATS, DEFAULT_DECAY_POLICY, DURABLE_DECAY_POLICY, type DecayEvaluation, EPHEMERAL_DECAY_POLICY, type GroupedAnchors, SessionTouchObserver, type SettlementProposal, type SweepResult, type TouchMatchResult, acquireSyncLock, atomicRenameWithRetry, classifyAnchor, detectDurability, detectRecurrence, detectTargetDate, evaluateAnchorDecay, findMatchedAnchors, formatCreationTime, formatOrigin, formatRelativeTime, formatRemainingTtl, formatSettlementCard, formatTargetDate, generateSettlementProposals, getDefaultStorageDir, getDisplayWidth, getEphemeralDecayPolicy, getTodayDateString, globToRegExp, groupAnchorsByQuadrant, hasActiveUI, isSameOrSubPath, makeSafeTaskAnnotation, matchAnchorAgainstTouchedFiles, normalizePath, openAnchorDashboard, padToWidth, renderActiveAnchorsContext, renderColdStartAnchorsContext, runPhysicalVerification, stripAnsi, sweepStore, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
|
package/dist/index.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ interface AnchorStoreState {
|
|
|
66
66
|
version: number;
|
|
67
67
|
anchors: Anchor[];
|
|
68
68
|
lastSweepAt: number;
|
|
69
|
+
nextId?: number;
|
|
69
70
|
}
|
|
70
71
|
interface TouchMatchResult {
|
|
71
72
|
anchor: Anchor;
|
|
@@ -236,6 +237,10 @@ declare function globToRegExp(pattern: string): RegExp;
|
|
|
236
237
|
* Normalize path separators to POSIX forward slashes and optionally relative to baseCwd
|
|
237
238
|
*/
|
|
238
239
|
declare function normalizePath(filePath: string, baseCwd?: string): string;
|
|
240
|
+
/**
|
|
241
|
+
* Compare two workspace CWDs safely (case-insensitive on Windows, supports subdirectory scoping)
|
|
242
|
+
*/
|
|
243
|
+
declare function isSameOrSubPath(anchorCwd?: string, targetCwd?: string): boolean;
|
|
239
244
|
/**
|
|
240
245
|
* Match touched files against an anchor's registered physical patterns
|
|
241
246
|
* Only anchors with explicit files/globs/prefixes are matched against file touches.
|
|
@@ -298,6 +303,7 @@ declare function stripAnsi(str: string): string;
|
|
|
298
303
|
* - Standard ASCII (width 1)
|
|
299
304
|
*/
|
|
300
305
|
declare function getDisplayWidth(str: string): number;
|
|
306
|
+
declare function hasActiveUI(ctx: ExtensionContext | null | undefined): ctx is ExtensionContext;
|
|
301
307
|
/**
|
|
302
308
|
* Truncate string to target terminal visual width with ellipsis
|
|
303
309
|
*/
|
|
@@ -445,4 +451,4 @@ declare class AnchorProtocol {
|
|
|
445
451
|
};
|
|
446
452
|
}
|
|
447
453
|
|
|
448
|
-
export { type AgentAdapter, type Anchor, type AnchorDecayPolicy, type AnchorDurability, type AnchorEvidence, type AnchorPriority, AnchorProtocol, type AnchorQuadrant, type AnchorStatus, AnchorStore, type AnchorStoreState, COMMENT_FORMATS, DEFAULT_DECAY_POLICY, DURABLE_DECAY_POLICY, type DecayEvaluation, EPHEMERAL_DECAY_POLICY, type GroupedAnchors, SessionTouchObserver, type SettlementProposal, type SweepResult, type TouchMatchResult, acquireSyncLock, atomicRenameWithRetry, classifyAnchor, detectDurability, detectRecurrence, detectTargetDate, evaluateAnchorDecay, findMatchedAnchors, formatCreationTime, formatOrigin, formatRelativeTime, formatRemainingTtl, formatSettlementCard, formatTargetDate, generateSettlementProposals, getDefaultStorageDir, getDisplayWidth, getEphemeralDecayPolicy, getTodayDateString, globToRegExp, groupAnchorsByQuadrant, makeSafeTaskAnnotation, matchAnchorAgainstTouchedFiles, normalizePath, openAnchorDashboard, padToWidth, renderActiveAnchorsContext, renderColdStartAnchorsContext, runPhysicalVerification, stripAnsi, sweepStore, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
|
|
454
|
+
export { type AgentAdapter, type Anchor, type AnchorDecayPolicy, type AnchorDurability, type AnchorEvidence, type AnchorPriority, AnchorProtocol, type AnchorQuadrant, type AnchorStatus, AnchorStore, type AnchorStoreState, COMMENT_FORMATS, DEFAULT_DECAY_POLICY, DURABLE_DECAY_POLICY, type DecayEvaluation, EPHEMERAL_DECAY_POLICY, type GroupedAnchors, SessionTouchObserver, type SettlementProposal, type SweepResult, type TouchMatchResult, acquireSyncLock, atomicRenameWithRetry, classifyAnchor, detectDurability, detectRecurrence, detectTargetDate, evaluateAnchorDecay, findMatchedAnchors, formatCreationTime, formatOrigin, formatRelativeTime, formatRemainingTtl, formatSettlementCard, formatTargetDate, generateSettlementProposals, getDefaultStorageDir, getDisplayWidth, getEphemeralDecayPolicy, getTodayDateString, globToRegExp, groupAnchorsByQuadrant, hasActiveUI, isSameOrSubPath, makeSafeTaskAnnotation, matchAnchorAgainstTouchedFiles, normalizePath, openAnchorDashboard, padToWidth, renderActiveAnchorsContext, renderColdStartAnchorsContext, runPhysicalVerification, stripAnsi, sweepStore, truncateToWidth, updateAnchorStatusBar, updateStartupBanner };
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
getTodayDateString,
|
|
27
27
|
globToRegExp,
|
|
28
28
|
groupAnchorsByQuadrant,
|
|
29
|
+
hasActiveUI,
|
|
30
|
+
isSameOrSubPath,
|
|
29
31
|
makeSafeTaskAnnotation,
|
|
30
32
|
matchAnchorAgainstTouchedFiles,
|
|
31
33
|
normalizePath,
|
|
@@ -39,7 +41,7 @@ import {
|
|
|
39
41
|
truncateToWidth,
|
|
40
42
|
updateAnchorStatusBar,
|
|
41
43
|
updateStartupBanner
|
|
42
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-G72UHPBN.js";
|
|
43
45
|
|
|
44
46
|
// src/protocol.ts
|
|
45
47
|
var AnchorProtocol = class {
|
|
@@ -128,6 +130,8 @@ export {
|
|
|
128
130
|
getTodayDateString,
|
|
129
131
|
globToRegExp,
|
|
130
132
|
groupAnchorsByQuadrant,
|
|
133
|
+
hasActiveUI,
|
|
134
|
+
isSameOrSubPath,
|
|
131
135
|
makeSafeTaskAnnotation,
|
|
132
136
|
matchAnchorAgainstTouchedFiles,
|
|
133
137
|
normalizePath,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-anchor",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"description": "Cross-session task memory for Pi — pin tasks and reminders across terminal restarts with 0 git repo pollution, 0 idle tokens, and automatic completion on code changes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"module": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"extensions": [
|
|
22
22
|
"dist/extension.js"
|
|
23
23
|
],
|
|
24
|
-
"image": "https://raw.githubusercontent.com/3ZEROS12/anchor/main/assets/hero.svg"
|
|
24
|
+
"image": "https://raw.githubusercontent.com/3ZEROS12/anchor/main/assets/hero-en.svg"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsup src/index.ts src/extension.ts --format esm,cjs --dts --clean",
|