pi-anchor 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,205 @@
1
+ import { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+
3
+ /**
4
+ * Anchor Data Contracts
5
+ * Minimalist cross-session sticky notes stored in ~/.anchor/state.json
6
+ */
7
+ type AnchorStatus = 'active' | 'sleeping' | 'settled';
8
+ type AnchorPriority = 'p0' | 'p1' | 'p2';
9
+ interface AnchorProgress {
10
+ /** Completed steps or units */
11
+ done: number;
12
+ /** Total steps or units */
13
+ total: number;
14
+ /** Optional parsed step labels (e.g. ['抽出接口', '替换Cookie', '补单测']) */
15
+ steps?: string[];
16
+ }
17
+ interface AnchorEvidence {
18
+ settledAt?: number;
19
+ settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
+ commitHash?: string;
21
+ summary?: string;
22
+ touchedFiles?: string[];
23
+ }
24
+ interface Anchor {
25
+ /** Sequential identifier, e.g. 'anc-1' */
26
+ id: string;
27
+ /** Sticky note text */
28
+ title: string;
29
+ /** Optional extra context */
30
+ description?: string;
31
+ /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
32
+ status: AnchorStatus;
33
+ /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
34
+ project: string;
35
+ /** Workspace directory path where the note applies */
36
+ cwd: string;
37
+ /** Creation timestamp (ms) */
38
+ createdAt: number;
39
+ /** Last update timestamp (ms) */
40
+ updatedAt: number;
41
+ /** Last activity timestamp (ms) */
42
+ lastTouchedAt: number;
43
+ /** Associated file paths or globs (explicit, auto-extracted from note text, or inherited from active session) */
44
+ files: string[];
45
+ /** Optional tags */
46
+ tags: string[];
47
+ /** Associated Pi conversation session file (.jsonl) for 1-click session jump/resume */
48
+ sessionFile?: string;
49
+ /** Optional lightweight progress tracker for long-term or multi-step notes */
50
+ progress?: AnchorProgress;
51
+ /** Optional due date ('YYYY-MM-DD') */
52
+ targetDate?: string;
53
+ /** Optional priority kept for backward compatibility */
54
+ priority?: AnchorPriority;
55
+ /** Settlement metadata when archived */
56
+ evidence?: AnchorEvidence;
57
+ }
58
+ interface AnchorStoreState {
59
+ version: number;
60
+ anchors: Anchor[];
61
+ lastSweepAt: number;
62
+ nextId?: number;
63
+ }
64
+ interface TouchMatchResult {
65
+ anchor: Anchor;
66
+ score: number;
67
+ matchedFiles: string[];
68
+ reason: 'exact-file' | 'dir-prefix';
69
+ }
70
+ /** 7 days without activity moves a note to quiet mode (still visible in UI, 0 prompt tokens in unrelated workspaces) */
71
+ declare const QUIET_AFTER_DAYS = 7;
72
+ declare const MS_PER_DAY = 86400000;
73
+
74
+ /**
75
+ * Normalize path separators to forward slashes and strip baseCwd prefix when inside baseCwd
76
+ */
77
+ declare function normalizePath(filePath: string, baseCwd?: string): string;
78
+ /**
79
+ * Safe boundary-aware directory comparison (case-insensitive on Windows)
80
+ */
81
+ declare function isSameOrSubPath(dirA?: string, dirB?: string): boolean;
82
+ /**
83
+ * Convert simple glob pattern (*.ts, src/**\/*.ts) to safe RegExp
84
+ */
85
+ declare function globToRegExp(pattern: string): RegExp;
86
+ /**
87
+ * Clever Touch 1: Automatically spot file or directory paths mentioned inside a sticky note
88
+ * (e.g. "src/auth.ts", "D:/College/优化/notes.md") so the user never has to fill out forms.
89
+ */
90
+ declare function extractPathsFromText(text?: string): string[];
91
+ /**
92
+ * Automatically extract lightweight progress metadata from note text if present:
93
+ * - Explicit ratio: `[2/5]`, `(3/10)`, or standalone `5/30` (where 0 <= done <= total and 2 <= total <= 500)
94
+ * - Numbered inline steps: `1. 抽出接口 2. 替换Cookie 3. 补单测`
95
+ */
96
+ declare function extractProgressFromText(rawTitle: string): {
97
+ cleanTitle: string;
98
+ progress?: AnchorProgress;
99
+ };
100
+ /**
101
+ * Check if a sticky note belongs to or targets the given workspace directory
102
+ */
103
+ declare function isAnchorTargetingWorkspace(anchor: Anchor, targetCwd?: string): boolean;
104
+ /**
105
+ * Match touched files against an anchor's file patterns
106
+ */
107
+ declare function matchAnchorAgainstTouchedFiles(anchor: Anchor, touchedFiles: string[]): TouchMatchResult | null;
108
+ declare function findMatchedAnchors(anchors: Anchor[], touchedFiles: string[]): TouchMatchResult[];
109
+ /**
110
+ * Clever Touch 2: Syntax-safe 1-line comment whisper when the AI reads/edits a matching file.
111
+ * Strictly skips JSON, ENV, lockfiles, and binaries so parsers are never broken.
112
+ */
113
+ declare const COMMENT_FORMATS: Record<string, (msg: string) => string>;
114
+ declare function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): string | null;
115
+ /**
116
+ * Clever Touch 3: Match a `git commit -m "..."` message against a sticky note (supports CJK & English)
117
+ */
118
+ declare function matchesCommitMessage(anchor: Anchor, commitMessages: string[]): {
119
+ matched: boolean;
120
+ message?: string;
121
+ };
122
+ /**
123
+ * Render ultra-compact Turn-1 sticky note context for the AI.
124
+ * Turn 2+ consumes 0 tokens.
125
+ */
126
+ declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number): string;
127
+ declare const renderActiveAnchorsContext: typeof renderColdStartAnchorsContext;
128
+ /**
129
+ * Transition notes untouched for >7 days to 'sleeping' (quiet mode) so they don't burn Turn-1 tokens
130
+ * in unrelated sessions, while NEVER deleting them.
131
+ */
132
+ declare function sweepQuietNotes(store: AnchorStore, now?: number): {
133
+ quieted: string[];
134
+ };
135
+ declare function getDefaultStorageDir(): string;
136
+ declare function acquireSyncLock(lockPath: string, maxWaitMs?: number, staleTimeoutMs?: number): () => void;
137
+ declare function atomicRenameWithRetry(tempPath: string, targetPath: string, maxRetries?: number): void;
138
+ declare class AnchorStore {
139
+ readonly storageDir: string;
140
+ readonly statePath: string;
141
+ readonly lockPath: string;
142
+ readonly archivePath: string;
143
+ constructor(storageDir?: string);
144
+ private ensureDirs;
145
+ withLock<T>(fn: () => T): T;
146
+ loadState(): AnchorStoreState;
147
+ saveState(state: AnchorStoreState): void;
148
+ private generateId;
149
+ /**
150
+ * Pin a new sticky note
151
+ */
152
+ create(input: {
153
+ title: string;
154
+ description?: string;
155
+ priority?: AnchorPriority;
156
+ cwd?: string;
157
+ project?: string;
158
+ files?: string[];
159
+ tags?: string[];
160
+ sessionFile?: string;
161
+ progress?: AnchorProgress;
162
+ }): Anchor;
163
+ private findAnchorIndex;
164
+ get(id: string): Anchor | undefined;
165
+ list(filter?: {
166
+ status?: AnchorStatus;
167
+ cwd?: string;
168
+ all?: boolean;
169
+ }): Anchor[];
170
+ update(id: string, patch: Partial<Omit<Anchor, 'id' | 'createdAt'>>): Anchor;
171
+ touch(id: string, timestamp?: number, sessionFile?: string): Anchor;
172
+ /**
173
+ * Advance (or set) progress on a sticky note:
174
+ * - If delta is provided (default +1), increments `done` up to `total`.
175
+ * - If note had no progress yet, initializes a 4-step bar (`1/4`).
176
+ */
177
+ step(id: string, delta?: number, explicitTotal?: number): {
178
+ anchor: Anchor;
179
+ completed: boolean;
180
+ };
181
+ /**
182
+ * Tear off (settle) a completed sticky note and archive it to archive.jsonl
183
+ */
184
+ settle(id: string, evidence?: AnchorEvidence, now?: number): Anchor;
185
+ /**
186
+ * Put back (undo) the last torn-off sticky note
187
+ */
188
+ undoSettle(): Anchor;
189
+ }
190
+
191
+ declare function parseAnchorCommandInput(raw: string): {
192
+ mode: 'dashboard' | 'undo' | 'settle' | 'step' | 'jump' | 'pin';
193
+ targetId?: string;
194
+ delta?: number;
195
+ title?: string;
196
+ };
197
+ /**
198
+ * Detect 0ms human-first quick note prefix in the input box:
199
+ * e.g. `pin: 明天把Token改Cookie` or `便签: 检查网关重试 [1/4]`
200
+ */
201
+ declare function parseQuickNoteInput(text: string): string | null;
202
+ declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
203
+ declare function export_default(pi: ExtensionAPI): void;
204
+
205
+ export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TouchMatchResult as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, acquireSyncLock as g, atomicRenameWithRetry as h, extractPathsFromText as i, extractProgressFromText as j, findMatchedAnchors as k, getDefaultStorageDir as l, globToRegExp as m, isAnchorTargetingWorkspace as n, isSameOrSubPath as o, makeSafeTaskAnnotation as p, matchAnchorAgainstTouchedFiles as q, matchesCommitMessage as r, normalizePath as s, parseAnchorCommandInput as t, parseQuickNoteInput as u, renderActiveAnchorsContext as v, renderColdStartAnchorsContext as w, resolveAnchorTarget as x, sweepQuietNotes as y, export_default as z };
@@ -0,0 +1,205 @@
1
+ import { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+
3
+ /**
4
+ * Anchor Data Contracts
5
+ * Minimalist cross-session sticky notes stored in ~/.anchor/state.json
6
+ */
7
+ type AnchorStatus = 'active' | 'sleeping' | 'settled';
8
+ type AnchorPriority = 'p0' | 'p1' | 'p2';
9
+ interface AnchorProgress {
10
+ /** Completed steps or units */
11
+ done: number;
12
+ /** Total steps or units */
13
+ total: number;
14
+ /** Optional parsed step labels (e.g. ['抽出接口', '替换Cookie', '补单测']) */
15
+ steps?: string[];
16
+ }
17
+ interface AnchorEvidence {
18
+ settledAt?: number;
19
+ settledBy?: 'commit' | 'manual' | 'one-tap-settlement' | 'manual-command' | 'verification-test';
20
+ commitHash?: string;
21
+ summary?: string;
22
+ touchedFiles?: string[];
23
+ }
24
+ interface Anchor {
25
+ /** Sequential identifier, e.g. 'anc-1' */
26
+ id: string;
27
+ /** Sticky note text */
28
+ title: string;
29
+ /** Optional extra context */
30
+ description?: string;
31
+ /** Lifecycle status: 'active' (recent) or 'sleeping' (>7d untouched, 0 prompt tokens) */
32
+ status: AnchorStatus;
33
+ /** Short workspace label (e.g. 'anchor', 'College/优化', or 'global') */
34
+ project: string;
35
+ /** Workspace directory path where the note applies */
36
+ cwd: string;
37
+ /** Creation timestamp (ms) */
38
+ createdAt: number;
39
+ /** Last update timestamp (ms) */
40
+ updatedAt: number;
41
+ /** Last activity timestamp (ms) */
42
+ lastTouchedAt: number;
43
+ /** Associated file paths or globs (explicit, auto-extracted from note text, or inherited from active session) */
44
+ files: string[];
45
+ /** Optional tags */
46
+ tags: string[];
47
+ /** Associated Pi conversation session file (.jsonl) for 1-click session jump/resume */
48
+ sessionFile?: string;
49
+ /** Optional lightweight progress tracker for long-term or multi-step notes */
50
+ progress?: AnchorProgress;
51
+ /** Optional due date ('YYYY-MM-DD') */
52
+ targetDate?: string;
53
+ /** Optional priority kept for backward compatibility */
54
+ priority?: AnchorPriority;
55
+ /** Settlement metadata when archived */
56
+ evidence?: AnchorEvidence;
57
+ }
58
+ interface AnchorStoreState {
59
+ version: number;
60
+ anchors: Anchor[];
61
+ lastSweepAt: number;
62
+ nextId?: number;
63
+ }
64
+ interface TouchMatchResult {
65
+ anchor: Anchor;
66
+ score: number;
67
+ matchedFiles: string[];
68
+ reason: 'exact-file' | 'dir-prefix';
69
+ }
70
+ /** 7 days without activity moves a note to quiet mode (still visible in UI, 0 prompt tokens in unrelated workspaces) */
71
+ declare const QUIET_AFTER_DAYS = 7;
72
+ declare const MS_PER_DAY = 86400000;
73
+
74
+ /**
75
+ * Normalize path separators to forward slashes and strip baseCwd prefix when inside baseCwd
76
+ */
77
+ declare function normalizePath(filePath: string, baseCwd?: string): string;
78
+ /**
79
+ * Safe boundary-aware directory comparison (case-insensitive on Windows)
80
+ */
81
+ declare function isSameOrSubPath(dirA?: string, dirB?: string): boolean;
82
+ /**
83
+ * Convert simple glob pattern (*.ts, src/**\/*.ts) to safe RegExp
84
+ */
85
+ declare function globToRegExp(pattern: string): RegExp;
86
+ /**
87
+ * Clever Touch 1: Automatically spot file or directory paths mentioned inside a sticky note
88
+ * (e.g. "src/auth.ts", "D:/College/优化/notes.md") so the user never has to fill out forms.
89
+ */
90
+ declare function extractPathsFromText(text?: string): string[];
91
+ /**
92
+ * Automatically extract lightweight progress metadata from note text if present:
93
+ * - Explicit ratio: `[2/5]`, `(3/10)`, or standalone `5/30` (where 0 <= done <= total and 2 <= total <= 500)
94
+ * - Numbered inline steps: `1. 抽出接口 2. 替换Cookie 3. 补单测`
95
+ */
96
+ declare function extractProgressFromText(rawTitle: string): {
97
+ cleanTitle: string;
98
+ progress?: AnchorProgress;
99
+ };
100
+ /**
101
+ * Check if a sticky note belongs to or targets the given workspace directory
102
+ */
103
+ declare function isAnchorTargetingWorkspace(anchor: Anchor, targetCwd?: string): boolean;
104
+ /**
105
+ * Match touched files against an anchor's file patterns
106
+ */
107
+ declare function matchAnchorAgainstTouchedFiles(anchor: Anchor, touchedFiles: string[]): TouchMatchResult | null;
108
+ declare function findMatchedAnchors(anchors: Anchor[], touchedFiles: string[]): TouchMatchResult[];
109
+ /**
110
+ * Clever Touch 2: Syntax-safe 1-line comment whisper when the AI reads/edits a matching file.
111
+ * Strictly skips JSON, ENV, lockfiles, and binaries so parsers are never broken.
112
+ */
113
+ declare const COMMENT_FORMATS: Record<string, (msg: string) => string>;
114
+ declare function makeSafeTaskAnnotation(filePath: string, anchor: Anchor): string | null;
115
+ /**
116
+ * Clever Touch 3: Match a `git commit -m "..."` message against a sticky note (supports CJK & English)
117
+ */
118
+ declare function matchesCommitMessage(anchor: Anchor, commitMessages: string[]): {
119
+ matched: boolean;
120
+ message?: string;
121
+ };
122
+ /**
123
+ * Render ultra-compact Turn-1 sticky note context for the AI.
124
+ * Turn 2+ consumes 0 tokens.
125
+ */
126
+ declare function renderColdStartAnchorsContext(store: AnchorStore, cwdOrNow?: string | number, nowArg?: number): string;
127
+ declare const renderActiveAnchorsContext: typeof renderColdStartAnchorsContext;
128
+ /**
129
+ * Transition notes untouched for >7 days to 'sleeping' (quiet mode) so they don't burn Turn-1 tokens
130
+ * in unrelated sessions, while NEVER deleting them.
131
+ */
132
+ declare function sweepQuietNotes(store: AnchorStore, now?: number): {
133
+ quieted: string[];
134
+ };
135
+ declare function getDefaultStorageDir(): string;
136
+ declare function acquireSyncLock(lockPath: string, maxWaitMs?: number, staleTimeoutMs?: number): () => void;
137
+ declare function atomicRenameWithRetry(tempPath: string, targetPath: string, maxRetries?: number): void;
138
+ declare class AnchorStore {
139
+ readonly storageDir: string;
140
+ readonly statePath: string;
141
+ readonly lockPath: string;
142
+ readonly archivePath: string;
143
+ constructor(storageDir?: string);
144
+ private ensureDirs;
145
+ withLock<T>(fn: () => T): T;
146
+ loadState(): AnchorStoreState;
147
+ saveState(state: AnchorStoreState): void;
148
+ private generateId;
149
+ /**
150
+ * Pin a new sticky note
151
+ */
152
+ create(input: {
153
+ title: string;
154
+ description?: string;
155
+ priority?: AnchorPriority;
156
+ cwd?: string;
157
+ project?: string;
158
+ files?: string[];
159
+ tags?: string[];
160
+ sessionFile?: string;
161
+ progress?: AnchorProgress;
162
+ }): Anchor;
163
+ private findAnchorIndex;
164
+ get(id: string): Anchor | undefined;
165
+ list(filter?: {
166
+ status?: AnchorStatus;
167
+ cwd?: string;
168
+ all?: boolean;
169
+ }): Anchor[];
170
+ update(id: string, patch: Partial<Omit<Anchor, 'id' | 'createdAt'>>): Anchor;
171
+ touch(id: string, timestamp?: number, sessionFile?: string): Anchor;
172
+ /**
173
+ * Advance (or set) progress on a sticky note:
174
+ * - If delta is provided (default +1), increments `done` up to `total`.
175
+ * - If note had no progress yet, initializes a 4-step bar (`1/4`).
176
+ */
177
+ step(id: string, delta?: number, explicitTotal?: number): {
178
+ anchor: Anchor;
179
+ completed: boolean;
180
+ };
181
+ /**
182
+ * Tear off (settle) a completed sticky note and archive it to archive.jsonl
183
+ */
184
+ settle(id: string, evidence?: AnchorEvidence, now?: number): Anchor;
185
+ /**
186
+ * Put back (undo) the last torn-off sticky note
187
+ */
188
+ undoSettle(): Anchor;
189
+ }
190
+
191
+ declare function parseAnchorCommandInput(raw: string): {
192
+ mode: 'dashboard' | 'undo' | 'settle' | 'step' | 'jump' | 'pin';
193
+ targetId?: string;
194
+ delta?: number;
195
+ title?: string;
196
+ };
197
+ /**
198
+ * Detect 0ms human-first quick note prefix in the input box:
199
+ * e.g. `pin: 明天把Token改Cookie` or `便签: 检查网关重试 [1/4]`
200
+ */
201
+ declare function parseQuickNoteInput(text: string): string | null;
202
+ declare function resolveAnchorTarget(store: AnchorStore, idOrIndex: string, cwd?: string): string | undefined;
203
+ declare function export_default(pi: ExtensionAPI): void;
204
+
205
+ export { type Anchor as A, COMMENT_FORMATS as C, MS_PER_DAY as M, QUIET_AFTER_DAYS as Q, type TouchMatchResult as T, type AnchorProgress as a, AnchorStore as b, type AnchorEvidence as c, type AnchorPriority as d, type AnchorStatus as e, type AnchorStoreState as f, acquireSyncLock as g, atomicRenameWithRetry as h, extractPathsFromText as i, extractProgressFromText as j, findMatchedAnchors as k, getDefaultStorageDir as l, globToRegExp as m, isAnchorTargetingWorkspace as n, isSameOrSubPath as o, makeSafeTaskAnnotation as p, matchAnchorAgainstTouchedFiles as q, matchesCommitMessage as r, normalizePath as s, parseAnchorCommandInput as t, parseQuickNoteInput as u, renderActiveAnchorsContext as v, renderColdStartAnchorsContext as w, resolveAnchorTarget as x, sweepQuietNotes as y, export_default as z };