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.
@@ -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 "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { StringEnum } from "@mariozechner/pi-ai";
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';
@@ -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 "@mariozechner/pi-coding-agent";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
8
  import { Type } from "typebox";
9
- import { StringEnum } from "@mariozechner/pi-ai";
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 "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
- import { StringEnum } from "@mariozechner/pi-ai";
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
 
@@ -3,9 +3,9 @@
3
3
  * Complements the `memory` tool (declarative knowledge) with procedural knowledge.
4
4
  */
5
5
 
6
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
7
7
  import { Type } from "typebox";
8
- import { StringEnum } from "@mariozechner/pi-ai";
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
- file_name: Type.Optional(
29
- Type.String({ description: "Skill file name (for view/patch/edit/delete). e.g., 'debug-typescript-errors.md'" })
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, file_name, description, section, content } = params;
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 (!file_name) {
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(file_name);
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 '${file_name}' not found.` }) }],
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 (!file_name) {
91
+ if (!skill_id) {
89
92
  return {
90
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "file_name is required for 'patch' action." }) }],
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(file_name, section, content);
109
+ result = await store.patch(skill_id, section, content);
107
110
  break;
108
111
 
109
112
  case "edit":
110
- if (!file_name) {
113
+ if (!skill_id) {
111
114
  return {
112
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "file_name is required for 'edit' action." }) }],
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(file_name, description || "", content || "");
119
+ result = await store.edit(skill_id, description || "", content || "");
117
120
  break;
118
121
 
119
122
  case "delete":
120
- if (!file_name) {
123
+ if (!skill_id) {
121
124
  return {
122
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "file_name is required for 'delete' action." }) }],
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(file_name);
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 "@mariozechner/pi-ai";
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
- /** File name (slug.md) */
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
- /** Human-readable name */
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
- /** Short description for system prompt index */
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
  /**