pi-hermes-memory 0.7.8 → 0.7.10
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 +21 -17
- package/package.json +1 -1
- package/src/config.ts +14 -1
- package/src/extension-root-migration.ts +101 -0
- package/src/handlers/index-sessions.ts +1 -1
- package/src/handlers/skills-command.ts +544 -75
- package/src/index.ts +37 -11
- package/src/project-memory-migration.ts +1 -1
- package/src/store/memory-store.ts +1 -1
- package/src/store/session-anchor-search.ts +472 -0
- package/src/store/skill-store.ts +142 -43
- package/src/store/skill-utils.ts +23 -6
- package/src/tools/session-search-tool.ts +106 -1
- package/src/types.ts +10 -5
package/src/store/skill-store.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SkillStore — procedural memory stored as Pi-native skills.
|
|
3
3
|
*
|
|
4
|
-
* Global skills live in ~/.pi/agent/skills/<slug>/SKILL.md.
|
|
4
|
+
* Global skills live in ~/.pi/agent/pi-hermes-memory/skills/<slug>/SKILL.md.
|
|
5
5
|
* Project skills live in ~/.pi/agent/<projectsMemoryDir>/<project>/skills/<slug>/SKILL.md.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -27,6 +27,7 @@ interface SkillStoreOptions {
|
|
|
27
27
|
projectSkillsDir?: string | null;
|
|
28
28
|
projectName?: string | null;
|
|
29
29
|
legacySkillsDir?: string;
|
|
30
|
+
legacyPiGlobalSkillsDir?: string;
|
|
30
31
|
migrationSentinelPath?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
@@ -50,6 +51,7 @@ export class SkillStore {
|
|
|
50
51
|
private projectSkillsDir: string | null;
|
|
51
52
|
private projectName: string | null;
|
|
52
53
|
private legacySkillsDir: string;
|
|
54
|
+
private legacyPiGlobalSkillsDir: string;
|
|
53
55
|
private migrationSentinelPath: string;
|
|
54
56
|
|
|
55
57
|
constructor(options: SkillStoreOptions = {}) {
|
|
@@ -58,8 +60,9 @@ export class SkillStore {
|
|
|
58
60
|
this.projectSkillsDir = options.projectSkillsDir ?? null;
|
|
59
61
|
this.projectName = options.projectName ?? null;
|
|
60
62
|
this.legacySkillsDir = options.legacySkillsDir ?? path.join(agentRoot, "memory", "skills");
|
|
63
|
+
this.legacyPiGlobalSkillsDir = options.legacyPiGlobalSkillsDir ?? path.join(agentRoot, "skills");
|
|
61
64
|
this.migrationSentinelPath = options.migrationSentinelPath
|
|
62
|
-
?? path.join(agentRoot, "memory", ".skills-migrated-to-
|
|
65
|
+
?? path.join(agentRoot, "pi-hermes-memory", ".skills-migrated-to-extension-storage");
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
getGlobalSkillsDir(): string {
|
|
@@ -89,52 +92,17 @@ export class SkillStore {
|
|
|
89
92
|
async migrateLegacySkills(): Promise<LegacySkillMigrationResult> {
|
|
90
93
|
const result: LegacySkillMigrationResult = { migrated: 0, skipped: 0, warnings: [] };
|
|
91
94
|
|
|
95
|
+
// Always normalize flat markdown files under the global skills root,
|
|
96
|
+
// even when a previous migration sentinel already exists.
|
|
97
|
+
await this.migrateFlatMarkdownInGlobalSkillsDir(result);
|
|
98
|
+
|
|
92
99
|
if (await exists(this.migrationSentinelPath)) return result;
|
|
93
100
|
|
|
94
101
|
await fs.mkdir(path.dirname(this.migrationSentinelPath), { recursive: true });
|
|
95
102
|
|
|
96
103
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const files = (await fs.readdir(this.legacySkillsDir))
|
|
100
|
-
.filter((file) => file.endsWith(".md"))
|
|
101
|
-
.sort();
|
|
102
|
-
|
|
103
|
-
for (const file of files) {
|
|
104
|
-
const legacyPath = path.join(this.legacySkillsDir, file);
|
|
105
|
-
try {
|
|
106
|
-
const raw = await fs.readFile(legacyPath, "utf-8");
|
|
107
|
-
const parsed = parseFrontmatter(raw);
|
|
108
|
-
const fallbackSlug = slugify(path.basename(file, ".md"));
|
|
109
|
-
const slug = slugify(parsed.meta.name || fallbackSlug);
|
|
110
|
-
if (!slug) {
|
|
111
|
-
result.skipped++;
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const targetPath = path.join(this.globalSkillsDir, slug, "SKILL.md");
|
|
116
|
-
if (await exists(targetPath)) {
|
|
117
|
-
result.skipped++;
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const skillDoc = {
|
|
122
|
-
name: slug,
|
|
123
|
-
displayName: parsed.meta.display_name?.trim() || parsed.meta.name?.trim() || undefined,
|
|
124
|
-
description: parsed.meta.description?.trim() || `Migrated legacy skill: ${slug}`,
|
|
125
|
-
version: Number.parseInt(parsed.meta.version || "1", 10) || 1,
|
|
126
|
-
created: parsed.meta.created || today(),
|
|
127
|
-
updated: parsed.meta.updated || today(),
|
|
128
|
-
body: parsed.body || `# ${slug}\n`,
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
132
|
-
await this.atomicWrite(targetPath, formatFrontmatter(skillDoc));
|
|
133
|
-
result.migrated++;
|
|
134
|
-
} catch (error) {
|
|
135
|
-
result.warnings.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
104
|
+
await this.migrateLegacyMarkdownSkills(result);
|
|
105
|
+
await this.migrateLegacyPiGlobalSkillDirs(result);
|
|
138
106
|
} finally {
|
|
139
107
|
if (result.warnings.length === 0) {
|
|
140
108
|
await fs.writeFile(this.migrationSentinelPath, `${new Date().toISOString()}\n`, "utf-8");
|
|
@@ -144,6 +112,137 @@ export class SkillStore {
|
|
|
144
112
|
return result;
|
|
145
113
|
}
|
|
146
114
|
|
|
115
|
+
private async migrateLegacyMarkdownSkills(result: LegacySkillMigrationResult): Promise<void> {
|
|
116
|
+
if (!await exists(this.legacySkillsDir)) return;
|
|
117
|
+
|
|
118
|
+
const files = (await fs.readdir(this.legacySkillsDir))
|
|
119
|
+
.filter((file) => file.endsWith(".md"))
|
|
120
|
+
.sort();
|
|
121
|
+
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const legacyPath = path.join(this.legacySkillsDir, file);
|
|
124
|
+
try {
|
|
125
|
+
const raw = await fs.readFile(legacyPath, "utf-8");
|
|
126
|
+
const parsed = parseFrontmatter(raw);
|
|
127
|
+
const fallbackSlug = slugify(path.basename(file, ".md"));
|
|
128
|
+
const slug = slugify(parsed.meta.name || fallbackSlug);
|
|
129
|
+
if (!slug) {
|
|
130
|
+
result.skipped++;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const targetPath = path.join(this.globalSkillsDir, slug, "SKILL.md");
|
|
135
|
+
if (await exists(targetPath)) {
|
|
136
|
+
result.skipped++;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const skillDoc = {
|
|
141
|
+
name: slug,
|
|
142
|
+
displayName: parsed.meta.display_name?.trim() || parsed.meta.name?.trim() || undefined,
|
|
143
|
+
description: parsed.meta.description?.trim() || `Migrated legacy skill: ${slug}`,
|
|
144
|
+
version: Number.parseInt(parsed.meta.version || "1", 10) || 1,
|
|
145
|
+
created: parsed.meta.created || today(),
|
|
146
|
+
updated: parsed.meta.updated || today(),
|
|
147
|
+
body: parsed.body || `# ${slug}\n`,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
151
|
+
await this.atomicWrite(targetPath, formatFrontmatter(skillDoc));
|
|
152
|
+
result.migrated++;
|
|
153
|
+
} catch (error) {
|
|
154
|
+
result.warnings.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async migrateFlatMarkdownInGlobalSkillsDir(result: LegacySkillMigrationResult): Promise<void> {
|
|
160
|
+
if (!await exists(this.globalSkillsDir)) return;
|
|
161
|
+
|
|
162
|
+
const files = (await fs.readdir(this.globalSkillsDir))
|
|
163
|
+
.filter((file) => file.endsWith(".md") && file !== "SKILL.md")
|
|
164
|
+
.sort();
|
|
165
|
+
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
const legacyPath = path.join(this.globalSkillsDir, file);
|
|
168
|
+
try {
|
|
169
|
+
const raw = await fs.readFile(legacyPath, "utf-8");
|
|
170
|
+
const parsed = parseFrontmatter(raw);
|
|
171
|
+
const fallbackSlug = slugify(path.basename(file, ".md"));
|
|
172
|
+
const slug = slugify(parsed.meta.name || fallbackSlug);
|
|
173
|
+
if (!slug) {
|
|
174
|
+
result.skipped++;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const targetPath = path.join(this.globalSkillsDir, slug, "SKILL.md");
|
|
179
|
+
if (await exists(targetPath)) {
|
|
180
|
+
await fs.rm(legacyPath, { force: true });
|
|
181
|
+
result.skipped++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const skillDoc = {
|
|
186
|
+
name: slug,
|
|
187
|
+
displayName: parsed.meta.display_name?.trim() || parsed.meta.name?.trim() || undefined,
|
|
188
|
+
description: parsed.meta.description?.trim() || `Migrated legacy skill: ${slug}`,
|
|
189
|
+
version: Number.parseInt(parsed.meta.version || "1", 10) || 1,
|
|
190
|
+
created: parsed.meta.created || today(),
|
|
191
|
+
updated: parsed.meta.updated || today(),
|
|
192
|
+
body: parsed.body || `# ${slug}\n`,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
196
|
+
await this.atomicWrite(targetPath, formatFrontmatter(skillDoc));
|
|
197
|
+
await fs.rm(legacyPath, { force: true });
|
|
198
|
+
result.migrated++;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
result.warnings.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async migrateLegacyPiGlobalSkillDirs(result: LegacySkillMigrationResult): Promise<void> {
|
|
206
|
+
if (path.resolve(this.legacyPiGlobalSkillsDir) === path.resolve(this.globalSkillsDir)) return;
|
|
207
|
+
if (!await exists(this.legacyPiGlobalSkillsDir)) return;
|
|
208
|
+
|
|
209
|
+
const entries = await fs.readdir(this.legacyPiGlobalSkillsDir, { withFileTypes: true });
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
212
|
+
|
|
213
|
+
const sourceDir = path.join(this.legacyPiGlobalSkillsDir, entry.name);
|
|
214
|
+
const sourceSkill = path.join(sourceDir, "SKILL.md");
|
|
215
|
+
if (!await exists(sourceSkill)) continue;
|
|
216
|
+
|
|
217
|
+
const targetDir = path.join(this.globalSkillsDir, entry.name);
|
|
218
|
+
const targetSkill = path.join(targetDir, "SKILL.md");
|
|
219
|
+
if (await exists(targetSkill)) {
|
|
220
|
+
result.skipped++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const raw = await fs.readFile(sourceSkill, "utf-8");
|
|
226
|
+
const parsed = parseFrontmatter(raw);
|
|
227
|
+
const hasExtensionManagedMeta = Boolean(parsed.meta.display_name)
|
|
228
|
+
&& Boolean(parsed.meta.created)
|
|
229
|
+
&& Boolean(parsed.meta.updated)
|
|
230
|
+
&& /^\d+$/.test(parsed.meta.version ?? "");
|
|
231
|
+
|
|
232
|
+
if (!hasExtensionManagedMeta) {
|
|
233
|
+
result.skipped++;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
238
|
+
await fs.rename(sourceDir, targetDir);
|
|
239
|
+
result.migrated++;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
result.warnings.push(`${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
147
246
|
async loadIndex(scope?: SkillScope): Promise<SkillIndex[]> {
|
|
148
247
|
const locations = await this.collectLocations(scope);
|
|
149
248
|
const skills: SkillIndex[] = [];
|
package/src/store/skill-utils.ts
CHANGED
|
@@ -6,6 +6,19 @@ export interface ParsedSkillFile {
|
|
|
6
6
|
body: string;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function parseScalar(value: string): string {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (trimmed.length >= 2 && trimmed.startsWith('"') && trimmed.endsWith('"')) {
|
|
12
|
+
try {
|
|
13
|
+
const parsed = JSON.parse(trimmed);
|
|
14
|
+
if (typeof parsed === "string") return parsed;
|
|
15
|
+
} catch {
|
|
16
|
+
// fall through to raw trimmed
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return trimmed;
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
export function parseFrontmatter(raw: string): ParsedSkillFile {
|
|
10
23
|
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
11
24
|
if (!match) return { meta: {}, body: raw.trim() };
|
|
@@ -15,7 +28,7 @@ export function parseFrontmatter(raw: string): ParsedSkillFile {
|
|
|
15
28
|
const idx = line.indexOf(":");
|
|
16
29
|
if (idx > 0) {
|
|
17
30
|
const key = line.slice(0, idx).trim();
|
|
18
|
-
const value = line.slice(idx + 1)
|
|
31
|
+
const value = parseScalar(line.slice(idx + 1));
|
|
19
32
|
meta[key] = value;
|
|
20
33
|
}
|
|
21
34
|
}
|
|
@@ -23,18 +36,22 @@ export function parseFrontmatter(raw: string): ParsedSkillFile {
|
|
|
23
36
|
return { meta, body: match[2].trim() };
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
function yamlDoubleQuoted(value: string): string {
|
|
40
|
+
return JSON.stringify(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
26
43
|
export function formatFrontmatter(doc: Pick<SkillDocument, "name" | "displayName" | "description" | "version" | "created" | "updated" | "body">): string {
|
|
27
44
|
const lines = [
|
|
28
45
|
"---",
|
|
29
|
-
`name: ${doc.name}`,
|
|
30
|
-
`description: ${doc.description}`,
|
|
46
|
+
`name: ${yamlDoubleQuoted(doc.name)}`,
|
|
47
|
+
`description: ${yamlDoubleQuoted(doc.description)}`,
|
|
31
48
|
`version: ${doc.version}`,
|
|
32
|
-
`created: ${doc.created}`,
|
|
33
|
-
`updated: ${doc.updated}`,
|
|
49
|
+
`created: ${yamlDoubleQuoted(doc.created)}`,
|
|
50
|
+
`updated: ${yamlDoubleQuoted(doc.updated)}`,
|
|
34
51
|
];
|
|
35
52
|
|
|
36
53
|
if (doc.displayName && doc.displayName.trim() && doc.displayName.trim() !== doc.name) {
|
|
37
|
-
lines.push(`display_name: ${doc.displayName.trim()}`);
|
|
54
|
+
lines.push(`display_name: ${yamlDoubleQuoted(doc.displayName.trim())}`);
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
lines.push("---", doc.body);
|
|
@@ -1,17 +1,122 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import * as os from 'node:os';
|
|
1
3
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
4
|
import { Type } from "typebox";
|
|
3
5
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
6
|
import { DatabaseManager } from '../store/db.js';
|
|
5
7
|
import { searchSessions, getIndexedMessageCount } from '../store/session-search.js';
|
|
8
|
+
import { searchSessionAnchors } from '../store/session-anchor-search.js';
|
|
9
|
+
import type { SessionAnchorRange, SessionAnchorSearchResult } from '../store/session-anchor-search.js';
|
|
10
|
+
import type { SessionSearchConfig } from '../types.js';
|
|
6
11
|
|
|
7
12
|
interface SearchResult {
|
|
8
13
|
success: boolean;
|
|
9
14
|
count?: number;
|
|
10
15
|
message?: string;
|
|
11
16
|
output?: string;
|
|
17
|
+
ranges?: SessionAnchorRange[];
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
|
|
20
|
+
interface SessionSearchToolOptions {
|
|
21
|
+
sessionsDir?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_SESSIONS_DIR = path.join(os.homedir(), '.pi', 'agent', 'sessions');
|
|
25
|
+
|
|
26
|
+
export function registerSessionSearchTool(
|
|
27
|
+
pi: ExtensionAPI,
|
|
28
|
+
dbManager: DatabaseManager,
|
|
29
|
+
sessionSearchConfig: SessionSearchConfig = { variant: 'legacy' },
|
|
30
|
+
options: SessionSearchToolOptions = {},
|
|
31
|
+
): void {
|
|
32
|
+
if (sessionSearchConfig.variant === 'anchors') {
|
|
33
|
+
registerAnchorSessionSearchTool(pi, options.sessionsDir ?? DEFAULT_SESSIONS_DIR);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
registerLegacySessionSearchTool(pi, dbManager);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function registerAnchorSessionSearchTool(pi: ExtensionAPI, sessionsDir: string): void {
|
|
41
|
+
pi.registerTool({
|
|
42
|
+
name: 'session_search',
|
|
43
|
+
label: 'Session Search',
|
|
44
|
+
description: `Search Pi session JSONL files in the opt-in anchor mode using a Markdown request.
|
|
45
|
+
|
|
46
|
+
This mode accepts only a markdown request. Supported scalar fields are from, to, cwd, and limit. Supported list sections are all, any, and exclude: all terms must match, any requires at least one listed term, and exclude removes matching ranges. It returns compact JSONL line-range anchors, not summaries or previews. Output is plain text: count, optional message, then anchors as path:startLine-endLine with a short reason.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
from: 2026-05-14
|
|
50
|
+
to: 2026-05-15
|
|
51
|
+
cwd: /path/to/project
|
|
52
|
+
limit: 20
|
|
53
|
+
|
|
54
|
+
all:
|
|
55
|
+
- alpha
|
|
56
|
+
|
|
57
|
+
any:
|
|
58
|
+
- beta
|
|
59
|
+
- gamma
|
|
60
|
+
|
|
61
|
+
exclude:
|
|
62
|
+
- delta`,
|
|
63
|
+
promptSnippet: 'Search past session JSONL files for compact source anchors',
|
|
64
|
+
promptGuidelines: [
|
|
65
|
+
'Use session_search with markdown only when the session search anchor mode is configured.',
|
|
66
|
+
'Request source anchors, not summaries or previews.',
|
|
67
|
+
'Use all for required terms, any for alternatives, and exclude for terms that must not appear in a returned range.',
|
|
68
|
+
],
|
|
69
|
+
parameters: Type.Object({
|
|
70
|
+
markdown: Type.String({ description: 'Markdown request with optional from/to/cwd/limit fields and all/any/exclude lists.' }),
|
|
71
|
+
}),
|
|
72
|
+
execute: async (_id: string, args: { markdown: string }) => {
|
|
73
|
+
const markdown = args.markdown;
|
|
74
|
+
|
|
75
|
+
if (!markdown || markdown.trim().length === 0) {
|
|
76
|
+
const result: SearchResult = { success: false, message: 'markdown is required' };
|
|
77
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const searchResult = searchSessionAnchors(markdown, { sessionsDir });
|
|
81
|
+
if (!searchResult.success) {
|
|
82
|
+
const result: SearchResult = { success: false, message: searchResult.message ?? 'Anchor session search failed.' };
|
|
83
|
+
return { content: [{ type: 'text' as const, text: result.message! }], details: result };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const output = formatAnchorSearchOutput(searchResult);
|
|
87
|
+
const result: SearchResult = {
|
|
88
|
+
success: true,
|
|
89
|
+
count: searchResult.ranges.length,
|
|
90
|
+
message: searchResult.message,
|
|
91
|
+
output,
|
|
92
|
+
ranges: searchResult.ranges,
|
|
93
|
+
};
|
|
94
|
+
return { content: [{ type: 'text' as const, text: output }], details: result };
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatAnchorSearchOutput(searchResult: SessionAnchorSearchResult): string {
|
|
100
|
+
const lines = [`count: ${searchResult.ranges.length}`];
|
|
101
|
+
if (searchResult.message) lines.push(`message: ${searchResult.message}`);
|
|
102
|
+
if (searchResult.ranges.length > 0) {
|
|
103
|
+
lines.push("anchors:");
|
|
104
|
+
for (const range of searchResult.ranges) {
|
|
105
|
+
const anchor = `${range.path}:${range.startLine}-${range.endLine}`;
|
|
106
|
+
const reason = compactReason(range.reason);
|
|
107
|
+
lines.push(reason ? `- ${anchor} — ${reason}` : `- ${anchor}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function compactReason(reason: string | undefined): string {
|
|
114
|
+
if (!reason) return "";
|
|
115
|
+
const oneLine = reason.replace(/\s+/g, " ").trim();
|
|
116
|
+
return oneLine.length <= 180 ? oneLine : `${oneLine.slice(0, 177)}...`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function registerLegacySessionSearchTool(pi: ExtensionAPI, dbManager: DatabaseManager): void {
|
|
15
120
|
pi.registerTool({
|
|
16
121
|
name: 'session_search',
|
|
17
122
|
label: 'Session Search',
|
package/src/types.ts
CHANGED
|
@@ -6,6 +6,13 @@ import type { TextContent } from "@earendil-works/pi-ai";
|
|
|
6
6
|
|
|
7
7
|
export type MemoryOverflowStrategy = "auto-consolidate" | "reject" | "fifo-evict";
|
|
8
8
|
|
|
9
|
+
export type SessionSearchVariant = "legacy" | "anchors";
|
|
10
|
+
|
|
11
|
+
export interface SessionSearchConfig {
|
|
12
|
+
/** Session search implementation variant. Default: legacy */
|
|
13
|
+
variant: SessionSearchVariant;
|
|
14
|
+
}
|
|
15
|
+
|
|
9
16
|
export interface MemoryConfig {
|
|
10
17
|
/** Prompt memory mode. Default: policy-only */
|
|
11
18
|
memoryMode: "policy-only" | "legacy-inject";
|
|
@@ -33,10 +40,12 @@ export interface MemoryConfig {
|
|
|
33
40
|
flushMinTurns: number;
|
|
34
41
|
/** Recent conversation messages included in session flush. 0 = all. Default: 0 */
|
|
35
42
|
flushRecentMessages?: number;
|
|
36
|
-
/** Override
|
|
43
|
+
/** Override extension storage directory. Default: ~/.pi/agent/pi-hermes-memory */
|
|
37
44
|
memoryDir?: string;
|
|
38
45
|
/** Directory for project-scoped memory (relative to ~/.pi/agent). Default: "projects-memory" */
|
|
39
46
|
projectsMemoryDir?: string;
|
|
47
|
+
/** Session search configuration. Default: { variant: "legacy" } */
|
|
48
|
+
sessionSearch?: SessionSearchConfig;
|
|
40
49
|
/** Strategy when memory is full. Default: auto-consolidate */
|
|
41
50
|
memoryOverflowStrategy?: MemoryOverflowStrategy;
|
|
42
51
|
/** Legacy alias for memoryOverflowStrategy. Default: true */
|
|
@@ -61,10 +70,6 @@ export interface MemoryConfig {
|
|
|
61
70
|
nudgeToolCalls: number;
|
|
62
71
|
/** Maximum time in milliseconds for auto-consolidation to complete. Default: 60000 */
|
|
63
72
|
consolidationTimeoutMs: number;
|
|
64
|
-
/** Enable session history search via SQLite FTS5. Default: true */
|
|
65
|
-
sessionSearchEnabled?: boolean;
|
|
66
|
-
/** Days to retain session history. Default: 90 */
|
|
67
|
-
sessionRetentionDays?: number;
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
export type MemoryCategory =
|