pi-hermes-memory 0.7.6 → 0.7.7
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 +57 -20
- package/docs/PUBLISHING.md +1 -1
- package/docs/ROADMAP.md +1 -1
- package/package.json +4 -4
- package/src/constants.ts +7 -3
- package/src/handlers/auto-consolidate.ts +1 -1
- package/src/handlers/background-review.ts +1 -1
- package/src/handlers/correction-detector.ts +1 -1
- package/src/handlers/index-sessions.ts +1 -1
- package/src/handlers/insights.ts +1 -1
- package/src/handlers/interview.ts +1 -1
- package/src/handlers/learn-memory.ts +4 -4
- package/src/handlers/preview-context.ts +5 -15
- package/src/handlers/session-flush.ts +1 -1
- package/src/handlers/skill-auto-trigger.ts +24 -4
- package/src/handlers/skills-command.ts +26 -6
- package/src/handlers/switch-project.ts +1 -1
- package/src/handlers/sync-markdown-memories.ts +1 -1
- package/src/index.ts +52 -9
- package/src/project.ts +13 -0
- package/src/prompt-context.ts +0 -4
- package/src/skills/procedural-skill-creator/SKILL.md +146 -0
- package/src/store/skill-store.ts +463 -181
- package/src/store/skill-utils.ts +116 -0
- package/src/tools/memory-search-tool.ts +2 -2
- package/src/tools/memory-tool.ts +2 -2
- package/src/tools/session-search-tool.ts +2 -2
- package/src/tools/skill-tool.ts +23 -20
- package/src/types.ts +22 -4
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import type { SkillDocument, SkillScope } from "../types.js";
|
|
3
|
+
|
|
4
|
+
export interface ParsedSkillFile {
|
|
5
|
+
meta: Record<string, string>;
|
|
6
|
+
body: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseFrontmatter(raw: string): ParsedSkillFile {
|
|
10
|
+
const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
11
|
+
if (!match) return { meta: {}, body: raw.trim() };
|
|
12
|
+
|
|
13
|
+
const meta: Record<string, string> = {};
|
|
14
|
+
for (const line of match[1].split("\n")) {
|
|
15
|
+
const idx = line.indexOf(":");
|
|
16
|
+
if (idx > 0) {
|
|
17
|
+
const key = line.slice(0, idx).trim();
|
|
18
|
+
const value = line.slice(idx + 1).trim();
|
|
19
|
+
meta[key] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { meta, body: match[2].trim() };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function formatFrontmatter(doc: Pick<SkillDocument, "name" | "displayName" | "description" | "version" | "created" | "updated" | "body">): string {
|
|
27
|
+
const lines = [
|
|
28
|
+
"---",
|
|
29
|
+
`name: ${doc.name}`,
|
|
30
|
+
`description: ${doc.description}`,
|
|
31
|
+
`version: ${doc.version}`,
|
|
32
|
+
`created: ${doc.created}`,
|
|
33
|
+
`updated: ${doc.updated}`,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
if (doc.displayName && doc.displayName.trim() && doc.displayName.trim() !== doc.name) {
|
|
37
|
+
lines.push(`display_name: ${doc.displayName.trim()}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
lines.push("---", doc.body);
|
|
41
|
+
return lines.join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function slugify(name: string): string {
|
|
45
|
+
return name
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
48
|
+
.replace(/^-|-$/g, "")
|
|
49
|
+
.replace(/--+/g, "-")
|
|
50
|
+
.slice(0, 64);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function today(): string {
|
|
54
|
+
return new Date().toISOString().split("T")[0];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SKILL_SIMILARITY_STOP_WORDS = new Set([
|
|
58
|
+
"a", "an", "and", "are", "as", "at", "be", "by", "for", "from", "how", "in", "into", "is", "it",
|
|
59
|
+
"of", "on", "or", "that", "the", "this", "to", "use", "using", "with", "workflow", "procedure", "step",
|
|
60
|
+
"steps", "guide", "skill", "skills", "repo", "project",
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
export function tokenizeForSimilarity(input: string): string[] {
|
|
64
|
+
return input
|
|
65
|
+
.toLowerCase()
|
|
66
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
67
|
+
.split(/\s+/)
|
|
68
|
+
.map((token) => token.trim())
|
|
69
|
+
.filter((token) => token.length > 1 && !SKILL_SIMILARITY_STOP_WORDS.has(token));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function jaccardSimilarity(a: string[], b: string[]): number {
|
|
73
|
+
const aSet = new Set(a);
|
|
74
|
+
const bSet = new Set(b);
|
|
75
|
+
if (aSet.size === 0 || bSet.size === 0) return 0;
|
|
76
|
+
|
|
77
|
+
let intersection = 0;
|
|
78
|
+
for (const token of aSet) {
|
|
79
|
+
if (bSet.has(token)) intersection++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const union = new Set([...aSet, ...bSet]).size;
|
|
83
|
+
return union === 0 ? 0 : intersection / union;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildSkillId(scope: SkillScope, slug: string, projectName?: string | null): string {
|
|
87
|
+
return scope === "project" ? `project:${projectName ?? ""}:${slug}` : `global:${slug}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function parseSkillId(skillId: string): { scope: SkillScope; projectName?: string; slug: string } | null {
|
|
91
|
+
if (skillId.startsWith("global:")) {
|
|
92
|
+
return { scope: "global", slug: skillId.slice("global:".length) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (skillId.startsWith("project:")) {
|
|
96
|
+
const rest = skillId.slice("project:".length);
|
|
97
|
+
const idx = rest.indexOf(":");
|
|
98
|
+
if (idx <= 0 || idx === rest.length - 1) return null;
|
|
99
|
+
return {
|
|
100
|
+
scope: "project",
|
|
101
|
+
projectName: rest.slice(0, idx),
|
|
102
|
+
slug: rest.slice(idx + 1),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function exists(filePath: string): Promise<boolean> {
|
|
110
|
+
try {
|
|
111
|
+
await fs.access(filePath);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { StringEnum } from "@
|
|
3
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
4
|
import { DatabaseManager } from '../store/db.js';
|
|
5
5
|
import { searchMemories, getMemoryStats } from '../store/sqlite-memory-store.js';
|
|
6
6
|
import type { MemoryCategory } from '../types.js';
|
package/src/tools/memory-tool.ts
CHANGED
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
* See PLAN.md → "Hermes Source File Reference Map" for source lines.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { ExtensionAPI } from "@
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Type } from "typebox";
|
|
9
|
-
import { StringEnum } from "@
|
|
9
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
10
|
import { MemoryStore } from "../store/memory-store.js";
|
|
11
11
|
import { DatabaseManager } from "../store/db.js";
|
|
12
12
|
import {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import { StringEnum } from "@
|
|
3
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
4
4
|
import { DatabaseManager } from '../store/db.js';
|
|
5
5
|
import { searchSessions, getIndexedMessageCount } from '../store/session-search.js';
|
|
6
6
|
|
package/src/tools/skill-tool.ts
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Complements the `memory` tool (declarative knowledge) with procedural knowledge.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ExtensionAPI } from "@
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { Type } from "typebox";
|
|
8
|
-
import { StringEnum } from "@
|
|
8
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
9
9
|
import { SkillStore } from "../store/skill-store.js";
|
|
10
10
|
import { SKILL_TOOL_DESCRIPTION } from "../constants.js";
|
|
11
11
|
|
|
@@ -17,7 +17,8 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
17
17
|
promptSnippet: "Save or manage reusable procedures and patterns",
|
|
18
18
|
promptGuidelines: [
|
|
19
19
|
"Use the skill tool after completing complex tasks that required trial and error or multiple tool calls.",
|
|
20
|
-
"Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill.",
|
|
20
|
+
"Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill by skill_id.",
|
|
21
|
+
"Choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, conventions, or deploy steps.",
|
|
21
22
|
"Do NOT use skills for temporary task state — only for durable, reusable procedures.",
|
|
22
23
|
],
|
|
23
24
|
parameters: Type.Object({
|
|
@@ -25,12 +26,15 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
25
26
|
name: Type.Optional(
|
|
26
27
|
Type.String({ description: "Skill name (for create). e.g., 'debug-typescript-errors'" })
|
|
27
28
|
),
|
|
28
|
-
|
|
29
|
-
Type.String({ description: "
|
|
29
|
+
skill_id: Type.Optional(
|
|
30
|
+
Type.String({ description: "Stable skill id for view/patch/edit/delete. e.g., 'global:debug-typescript-errors' or 'project:my-repo:release-app'" })
|
|
30
31
|
),
|
|
31
32
|
description: Type.Optional(
|
|
32
33
|
Type.String({ description: "One-line description of when to use this skill (for create/edit)" })
|
|
33
34
|
),
|
|
35
|
+
scope: Type.Optional(
|
|
36
|
+
StringEnum(["global", "project"] as const, { description: "Optional creation scope. Omit to let the extension classify it automatically." })
|
|
37
|
+
),
|
|
34
38
|
section: Type.Optional(
|
|
35
39
|
Type.String({ description: "Section header to patch (for patch action). e.g., 'Procedure', 'Pitfalls'" })
|
|
36
40
|
),
|
|
@@ -39,7 +43,7 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
39
43
|
),
|
|
40
44
|
}),
|
|
41
45
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
42
|
-
const { action, name,
|
|
46
|
+
const { action, name, skill_id, description, scope, section, content } = params;
|
|
43
47
|
|
|
44
48
|
let result;
|
|
45
49
|
switch (action) {
|
|
@@ -62,22 +66,21 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
62
66
|
details: {},
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
|
-
result = await store.create(name, description, content);
|
|
69
|
+
result = await store.create(name, description, content, scope);
|
|
66
70
|
break;
|
|
67
71
|
|
|
68
72
|
case "view":
|
|
69
|
-
if (!
|
|
70
|
-
// List all skills
|
|
73
|
+
if (!skill_id) {
|
|
71
74
|
const index = await store.loadIndex();
|
|
72
75
|
return {
|
|
73
76
|
content: [{ type: "text", text: JSON.stringify({ success: true, skills: index }) }],
|
|
74
77
|
details: { skills: index },
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
|
-
const doc = await store.loadSkill(
|
|
80
|
+
const doc = await store.loadSkill(skill_id);
|
|
78
81
|
if (!doc) {
|
|
79
82
|
return {
|
|
80
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Skill '${
|
|
83
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: `Skill '${skill_id}' not found.` }) }],
|
|
81
84
|
details: {},
|
|
82
85
|
};
|
|
83
86
|
}
|
|
@@ -85,9 +88,9 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
85
88
|
break;
|
|
86
89
|
|
|
87
90
|
case "patch":
|
|
88
|
-
if (!
|
|
91
|
+
if (!skill_id) {
|
|
89
92
|
return {
|
|
90
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "
|
|
93
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "skill_id is required for 'patch' action." }) }],
|
|
91
94
|
details: {},
|
|
92
95
|
};
|
|
93
96
|
}
|
|
@@ -103,27 +106,27 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
103
106
|
details: {},
|
|
104
107
|
};
|
|
105
108
|
}
|
|
106
|
-
result = await store.patch(
|
|
109
|
+
result = await store.patch(skill_id, section, content);
|
|
107
110
|
break;
|
|
108
111
|
|
|
109
112
|
case "edit":
|
|
110
|
-
if (!
|
|
113
|
+
if (!skill_id) {
|
|
111
114
|
return {
|
|
112
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "
|
|
115
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "skill_id is required for 'edit' action." }) }],
|
|
113
116
|
details: {},
|
|
114
117
|
};
|
|
115
118
|
}
|
|
116
|
-
result = await store.edit(
|
|
119
|
+
result = await store.edit(skill_id, description || "", content || "");
|
|
117
120
|
break;
|
|
118
121
|
|
|
119
122
|
case "delete":
|
|
120
|
-
if (!
|
|
123
|
+
if (!skill_id) {
|
|
121
124
|
return {
|
|
122
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error: "
|
|
125
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: "skill_id is required for 'delete' action." }) }],
|
|
123
126
|
details: {},
|
|
124
127
|
};
|
|
125
128
|
}
|
|
126
|
-
result = await store.delete(
|
|
129
|
+
result = await store.delete(skill_id);
|
|
127
130
|
break;
|
|
128
131
|
|
|
129
132
|
default:
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Shared TypeScript types for the Hermes Memory extension.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { TextContent } from "@
|
|
5
|
+
import type { TextContent } from "@earendil-works/pi-ai";
|
|
6
6
|
|
|
7
7
|
export type MemoryOverflowStrategy = "auto-consolidate" | "reject" | "fifo-evict";
|
|
8
8
|
|
|
@@ -102,12 +102,24 @@ export interface ConsolidationResult {
|
|
|
102
102
|
error?: string;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
export type SkillScope = "global" | "project";
|
|
106
|
+
|
|
105
107
|
export interface SkillIndex {
|
|
106
|
-
/**
|
|
108
|
+
/** Stable id for read/update/delete operations */
|
|
109
|
+
skillId: string;
|
|
110
|
+
/** Whether the skill is global or project-scoped */
|
|
111
|
+
scope: SkillScope;
|
|
112
|
+
/** File name on disk (usually SKILL.md) */
|
|
107
113
|
fileName: string;
|
|
108
|
-
/**
|
|
114
|
+
/** Absolute path to the skill file */
|
|
115
|
+
path: string;
|
|
116
|
+
/** Active project name for project-scoped skills */
|
|
117
|
+
projectName?: string;
|
|
118
|
+
/** Pi skill slug stored in frontmatter and folder name */
|
|
109
119
|
name: string;
|
|
110
|
-
/**
|
|
120
|
+
/** Optional human-friendly title preserved for UI output */
|
|
121
|
+
displayName?: string;
|
|
122
|
+
/** Short description shown in skill listings */
|
|
111
123
|
description: string;
|
|
112
124
|
}
|
|
113
125
|
|
|
@@ -127,6 +139,12 @@ export interface SkillResult {
|
|
|
127
139
|
error?: string;
|
|
128
140
|
message?: string;
|
|
129
141
|
fileName?: string;
|
|
142
|
+
skillId?: string;
|
|
143
|
+
scope?: SkillScope;
|
|
144
|
+
path?: string;
|
|
145
|
+
conflictType?: "duplicate" | "similar" | "name-collision";
|
|
146
|
+
similarSkillIds?: string[];
|
|
147
|
+
suggestedAction?: "patch" | "edit" | "rename";
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
/**
|