pi-hermes-memory 0.9.0 → 0.9.1
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/package.json +1 -1
- package/src/constants.ts +1 -1
- package/src/index.ts +9 -7
- package/src/store/skill-store.ts +39 -59
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "🧠 Persistent memory + 🔍 session search + 🛡️ secret scanning for Pi. Token-aware policy-only memory by default, SQLite FTS5 search, auto-consolidation, procedural skills. 693 tests. Ported from Hermes agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/constants.ts
CHANGED
|
@@ -312,7 +312,7 @@ WHEN TO CREATE A SKILL:
|
|
|
312
312
|
- When the user teaches you a specific workflow or procedure
|
|
313
313
|
|
|
314
314
|
SCOPE:
|
|
315
|
-
- 'global': transferable procedures that can be reused across repositories. Written to
|
|
315
|
+
- 'global': transferable procedures that can be reused across repositories. Written to ~/.pi/agent/pi-hermes-memory/skills/<slug>/SKILL.md, this extension's own directory, kept separate from skills the user installed themselves. Pi also loads its own ~/.pi/agent/skills/ first, so a name already used there is rejected rather than silently shadowed.
|
|
316
316
|
- 'project': procedures tied to this repo's paths, scripts, architecture, deploy flow, or conventions. Written to ~/.pi/agent/projects-memory/<project>/skills/<slug>/SKILL.md.
|
|
317
317
|
|
|
318
318
|
WHEN TO UPDATE A SKILL:
|
package/src/index.ts
CHANGED
|
@@ -63,10 +63,13 @@ export function resolveProjectSkillDiscovery(
|
|
|
63
63
|
const detected = detectProjectSkills(projectsMemoryDir, cwd);
|
|
64
64
|
skillStore.setProjectContext(detected.name, detected.skillsDir);
|
|
65
65
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
|
|
66
|
+
// Pi auto-discovers its own `~/.pi/agent/skills/`, but this extension keeps
|
|
67
|
+
// its generated skills in a directory of its own so users can audit, wipe, or
|
|
68
|
+
// ignore them without touching skills they installed themselves (#126). Both
|
|
69
|
+
// of ours must therefore be contributed here.
|
|
70
|
+
const skillPaths = [skillStore.getGlobalSkillsDir()];
|
|
71
|
+
if (detected.skillsDir) skillPaths.push(detected.skillsDir);
|
|
72
|
+
return { skillPaths };
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
export function registerProjectSkillDiscoveryHandler(
|
|
@@ -102,13 +105,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
102
105
|
const project = detectProject(config.projectsMemoryDir);
|
|
103
106
|
const projectName = project.name ?? "";
|
|
104
107
|
const skillStore = new SkillStore({
|
|
105
|
-
globalSkillsDir: path.join(
|
|
108
|
+
globalSkillsDir: path.join(globalDir, "skills"),
|
|
109
|
+
piGlobalSkillsDir: path.join(agentRoot, "skills"),
|
|
106
110
|
projectSkillsDir: project.memoryDir ? path.join(project.memoryDir, "skills") : null,
|
|
107
111
|
projectName: project.name,
|
|
108
112
|
legacySkillsDir: path.join(legacyGlobalDir, "skills"),
|
|
109
|
-
legacyExtensionSkillsDir: path.join(globalDir, "skills"),
|
|
110
113
|
migrationSentinelPath: path.join(globalDir, ".skills-migrated-to-extension-storage"),
|
|
111
|
-
extensionSkillsMigrationSentinelPath: path.join(globalDir, ".skills-migrated-to-pi-global"),
|
|
112
114
|
});
|
|
113
115
|
const dbManager = new DatabaseManager(globalDir);
|
|
114
116
|
let databaseMigrationPending = shouldMigrateExtensionRoot
|
package/src/store/skill-store.ts
CHANGED
|
@@ -23,13 +23,14 @@ import type { SkillDocument, SkillIndex, SkillResult, SkillScope } from "../type
|
|
|
23
23
|
import { AGENT_ROOT } from "../paths.js";
|
|
24
24
|
|
|
25
25
|
interface SkillStoreOptions {
|
|
26
|
+
/** Where this extension writes global skills. Never Pi's own root. */
|
|
26
27
|
globalSkillsDir?: string;
|
|
28
|
+
/** Pi's global skills root, consulted read-only to refuse shadowed writes. */
|
|
29
|
+
piGlobalSkillsDir?: string;
|
|
27
30
|
projectSkillsDir?: string | null;
|
|
28
31
|
projectName?: string | null;
|
|
29
32
|
legacySkillsDir?: string;
|
|
30
|
-
legacyExtensionSkillsDir?: string;
|
|
31
33
|
migrationSentinelPath?: string;
|
|
32
|
-
extensionSkillsMigrationSentinelPath?: string;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
interface SkillLocation {
|
|
@@ -154,31 +155,36 @@ export function normalizeSkillPatchContent(
|
|
|
154
155
|
|
|
155
156
|
export class SkillStore {
|
|
156
157
|
private globalSkillsDir: string;
|
|
158
|
+
private piGlobalSkillsDir: string;
|
|
157
159
|
private projectSkillsDir: string | null;
|
|
158
160
|
private projectName: string | null;
|
|
159
161
|
private legacySkillsDir: string;
|
|
160
|
-
private legacyExtensionSkillsDir: string;
|
|
161
162
|
private migrationSentinelPath: string;
|
|
162
|
-
private extensionSkillsMigrationSentinelPath: string;
|
|
163
163
|
|
|
164
164
|
constructor(options: SkillStoreOptions = {}) {
|
|
165
165
|
const agentRoot = AGENT_ROOT;
|
|
166
|
-
this.globalSkillsDir = options.globalSkillsDir ?? path.join(agentRoot, "skills");
|
|
166
|
+
this.globalSkillsDir = options.globalSkillsDir ?? path.join(agentRoot, "pi-hermes-memory", "skills");
|
|
167
|
+
this.piGlobalSkillsDir = options.piGlobalSkillsDir ?? path.join(agentRoot, "skills");
|
|
167
168
|
this.projectSkillsDir = options.projectSkillsDir ?? null;
|
|
168
169
|
this.projectName = options.projectName ?? null;
|
|
169
170
|
this.legacySkillsDir = options.legacySkillsDir ?? path.join(agentRoot, "memory", "skills");
|
|
170
|
-
this.legacyExtensionSkillsDir = options.legacyExtensionSkillsDir
|
|
171
|
-
?? path.join(agentRoot, "pi-hermes-memory", "skills");
|
|
172
171
|
this.migrationSentinelPath = options.migrationSentinelPath
|
|
173
172
|
?? path.join(agentRoot, "pi-hermes-memory", ".skills-migrated-to-extension-storage");
|
|
174
|
-
this.extensionSkillsMigrationSentinelPath = options.extensionSkillsMigrationSentinelPath
|
|
175
|
-
?? path.join(agentRoot, "pi-hermes-memory", ".skills-migrated-to-pi-global");
|
|
176
173
|
}
|
|
177
174
|
|
|
178
175
|
getGlobalSkillsDir(): string {
|
|
179
176
|
return this.globalSkillsDir;
|
|
180
177
|
}
|
|
181
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Pi's own global skills root. Read-only from here: we never create, patch,
|
|
181
|
+
* or delete inside it. It is consulted solely to refuse writes that Pi would
|
|
182
|
+
* silently shadow (see `findShadowingPiGlobalSkill`).
|
|
183
|
+
*/
|
|
184
|
+
getPiGlobalSkillsDir(): string {
|
|
185
|
+
return this.piGlobalSkillsDir;
|
|
186
|
+
}
|
|
187
|
+
|
|
182
188
|
getProjectSkillsDir(): string | null {
|
|
183
189
|
return this.projectSkillsDir;
|
|
184
190
|
}
|
|
@@ -205,7 +211,6 @@ export class SkillStore {
|
|
|
205
211
|
// Always normalize flat markdown files under the global skills root,
|
|
206
212
|
// even when a previous migration sentinel already exists.
|
|
207
213
|
await this.migrateFlatMarkdownInGlobalSkillsDir(result);
|
|
208
|
-
await this.migrateExtensionSkillsToGlobalRoot(result);
|
|
209
214
|
|
|
210
215
|
if (await exists(this.migrationSentinelPath)) return result;
|
|
211
216
|
|
|
@@ -316,60 +321,22 @@ export class SkillStore {
|
|
|
316
321
|
}
|
|
317
322
|
|
|
318
323
|
/**
|
|
319
|
-
*
|
|
324
|
+
* Is `slug` already claimed by a skill in Pi's own global root?
|
|
320
325
|
*
|
|
321
326
|
* Pi keys skills by name, first-loaded wins, and `~/.pi/agent/skills/` is
|
|
322
327
|
* auto-discovered at higher precedence than anything an extension contributes
|
|
323
|
-
* via `resources_discover`.
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* no effect on the skill the agent actually loaded (#125, #126).
|
|
328
|
+
* via `resources_discover`. A global skill we write under a name that also
|
|
329
|
+
* exists there is never the copy Pi loads, so the write succeeds on disk and
|
|
330
|
+
* changes nothing about the agent's behaviour — silent write-loss (#125).
|
|
327
331
|
*
|
|
328
|
-
*
|
|
329
|
-
* shadowed
|
|
332
|
+
* Callers refuse the write and name both paths instead, which makes the
|
|
333
|
+
* shadowed state impossible to create rather than merely reported after the
|
|
334
|
+
* fact. Returns the shadowing path, or null when the name is free.
|
|
330
335
|
*/
|
|
331
|
-
private async
|
|
332
|
-
if (path.resolve(this.
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const entries = await fs.readdir(this.legacyExtensionSkillsDir, { withFileTypes: true });
|
|
337
|
-
let shadowed = 0;
|
|
338
|
-
|
|
339
|
-
for (const entry of entries) {
|
|
340
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
341
|
-
|
|
342
|
-
const sourceDir = path.join(this.legacyExtensionSkillsDir, entry.name);
|
|
343
|
-
if (!await exists(path.join(sourceDir, "SKILL.md"))) continue;
|
|
344
|
-
|
|
345
|
-
const targetDir = path.join(this.globalSkillsDir, entry.name);
|
|
346
|
-
if (await exists(path.join(targetDir, "SKILL.md"))) {
|
|
347
|
-
// Pi already loads the copy in its own root; ours was never reachable.
|
|
348
|
-
// Leave both in place rather than clobbering a skill we did not write.
|
|
349
|
-
shadowed++;
|
|
350
|
-
result.skipped++;
|
|
351
|
-
result.warnings.push(
|
|
352
|
-
`${entry.name}: already exists at ${targetDir}; the copy at ${sourceDir} was never loaded by Pi. `
|
|
353
|
-
+ `Rename or delete one of them, then restart Pi.`,
|
|
354
|
-
);
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
360
|
-
await fs.rename(sourceDir, targetDir);
|
|
361
|
-
result.migrated++;
|
|
362
|
-
} catch (error) {
|
|
363
|
-
shadowed++;
|
|
364
|
-
result.warnings.push(`${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (shadowed === 0) {
|
|
369
|
-
await fs.mkdir(path.dirname(this.extensionSkillsMigrationSentinelPath), { recursive: true });
|
|
370
|
-
await fs.writeFile(this.extensionSkillsMigrationSentinelPath, `${new Date().toISOString()}\n`, "utf-8");
|
|
371
|
-
await fs.rm(this.legacyExtensionSkillsDir, { recursive: true, force: true });
|
|
372
|
-
}
|
|
336
|
+
private async findShadowingPiGlobalSkill(slug: string): Promise<string | null> {
|
|
337
|
+
if (path.resolve(this.piGlobalSkillsDir) === path.resolve(this.globalSkillsDir)) return null;
|
|
338
|
+
const candidate = path.join(this.piGlobalSkillsDir, slug, "SKILL.md");
|
|
339
|
+
return await exists(candidate) ? candidate : null;
|
|
373
340
|
}
|
|
374
341
|
|
|
375
342
|
async loadIndex(scope?: SkillScope): Promise<SkillIndex[]> {
|
|
@@ -452,6 +419,19 @@ export class SkillStore {
|
|
|
452
419
|
suggestedAction: "rename",
|
|
453
420
|
};
|
|
454
421
|
}
|
|
422
|
+
|
|
423
|
+
const shadowedBy = await this.findShadowingPiGlobalSkill(slug);
|
|
424
|
+
if (shadowedBy) {
|
|
425
|
+
return {
|
|
426
|
+
success: false,
|
|
427
|
+
error: `Pi already loads a global skill named '${slug}' from ${shadowedBy}. `
|
|
428
|
+
+ `Pi keys skills by name and loads its own root first, so a skill written to `
|
|
429
|
+
+ `${path.join(this.globalSkillsDir, slug, "SKILL.md")} would never be the copy in effect. `
|
|
430
|
+
+ `Choose a different name, or edit ${shadowedBy} directly.`,
|
|
431
|
+
conflictType: "name-collision",
|
|
432
|
+
suggestedAction: "rename",
|
|
433
|
+
};
|
|
434
|
+
}
|
|
455
435
|
}
|
|
456
436
|
|
|
457
437
|
const filePath = path.join(root, slug, "SKILL.md");
|