pi-hermes-memory 0.7.9 → 0.7.11

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.
@@ -9,6 +9,82 @@ 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
 
12
+ function normalizeTextList(value: unknown): string[] {
13
+ if (!Array.isArray(value)) return [];
14
+ return value
15
+ .filter((item): item is string => typeof item === "string")
16
+ .map((item) => item.trim())
17
+ .filter(Boolean);
18
+ }
19
+
20
+ function formatOrderedList(items: string[]): string {
21
+ return items.map((item, index) => `${index + 1}. ${item}`).join("\n");
22
+ }
23
+
24
+ function formatBulletList(items: string[], fallback: string): string {
25
+ if (items.length === 0) return `- ${fallback}`;
26
+ return items.map((item) => `- ${item}`).join("\n");
27
+ }
28
+
29
+ function buildStructuredSkillBody(
30
+ whenToUse: string,
31
+ procedureSteps: string[],
32
+ pitfalls: string[],
33
+ verificationSteps: string[],
34
+ ): string {
35
+ return [
36
+ "## When to Use",
37
+ whenToUse,
38
+ "",
39
+ "## Procedure",
40
+ formatOrderedList(procedureSteps),
41
+ "",
42
+ "## Pitfalls",
43
+ formatBulletList(pitfalls, "No notable pitfalls recorded yet."),
44
+ "",
45
+ "## Verification",
46
+ formatOrderedList(verificationSteps),
47
+ ].join("\n");
48
+ }
49
+
50
+ const SKILL_ID_PARAM = Type.String({
51
+ description: "Stable skill id for view/patch/update/delete. e.g., 'global:debug-typescript-errors' or 'project:my-repo:release-app'. Legacy alias 'edit' also accepts this field.",
52
+ });
53
+
54
+ const SKILL_TOOL_PARAMETERS = Type.Object({
55
+ action: StringEnum(["create", "view", "patch", "update", "edit", "delete"] as const, {
56
+ description: "The skill action to perform.",
57
+ }),
58
+ name: Type.Optional(Type.String({
59
+ description: "Skill name for create. e.g., 'debug-typescript-errors'.",
60
+ })),
61
+ skill_id: Type.Optional(SKILL_ID_PARAM),
62
+ description: Type.Optional(Type.String({
63
+ description: "One-line description of when to use this skill. Required for create; optional for update/edit.",
64
+ })),
65
+ scope: Type.Optional(StringEnum(["global", "project"] as const, {
66
+ description: "Required for create. Use 'global' for portable procedures and 'project' for repo-specific workflows.",
67
+ })),
68
+ section: Type.Optional(Type.String({
69
+ description: "Required for patch. Section header to patch. e.g., 'Procedure', 'Pitfalls'.",
70
+ })),
71
+ content: Type.Optional(Type.String({
72
+ description: "Raw markdown body for create/update/edit, or new section content for patch. For create/update/edit you can provide this or the structured fields below.",
73
+ })),
74
+ when_to_use: Type.Optional(Type.String({
75
+ description: "Structured create/update/edit field. Explain when this skill should be used and where its boundaries are.",
76
+ })),
77
+ procedure_steps: Type.Optional(Type.Array(Type.String(), {
78
+ description: "Structured create/update/edit field. Ordered concrete steps for the workflow.",
79
+ })),
80
+ pitfalls: Type.Optional(Type.Array(Type.String(), {
81
+ description: "Structured create/update/edit field. Optional common mistakes, caveats, or failure modes to avoid.",
82
+ })),
83
+ verification_steps: Type.Optional(Type.Array(Type.String(), {
84
+ description: "Structured create/update/edit field. Concrete checks that confirm the workflow succeeded.",
85
+ })),
86
+ }, { additionalProperties: false });
87
+
12
88
  export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
13
89
  pi.registerTool({
14
90
  name: "skill",
@@ -17,33 +93,67 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
17
93
  promptSnippet: "Save or manage reusable procedures and patterns",
18
94
  promptGuidelines: [
19
95
  "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 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.",
96
+ "Use 'create' to save a new reusable procedure, 'patch' to update a section of an existing skill by skill_id, and 'update' for a full rewrite.",
97
+ "Scope is required on create: choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, conventions, or deploy steps.",
98
+ "Prefer structured fields for create/update: when_to_use, procedure_steps, pitfalls, and verification_steps. The tool will render valid SKILL.md sections for you.",
99
+ "Use 'view' before patching or updating when you need to inspect an existing skill.",
22
100
  "Do NOT use skills for temporary task state — only for durable, reusable procedures.",
23
101
  ],
24
- parameters: Type.Object({
25
- action: StringEnum(["create", "view", "patch", "edit", "delete"] as const),
26
- name: Type.Optional(
27
- Type.String({ description: "Skill name (for create). e.g., 'debug-typescript-errors'" })
28
- ),
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'" })
31
- ),
32
- description: Type.Optional(
33
- Type.String({ description: "One-line description of when to use this skill (for create/edit)" })
34
- ),
35
- scope: Type.Optional(
36
- StringEnum(["global", "project"] as const, { description: "Optional creation scope. Omit to let the extension classify it automatically." })
37
- ),
38
- section: Type.Optional(
39
- Type.String({ description: "Section header to patch (for patch action). e.g., 'Procedure', 'Pitfalls'" })
40
- ),
41
- content: Type.Optional(
42
- Type.String({ description: "Body content for create, new section content for patch, or new body for edit" })
43
- ),
44
- }),
102
+ parameters: SKILL_TOOL_PARAMETERS,
45
103
  async execute(toolCallId, params, signal, onUpdate, ctx) {
46
- const { action, name, skill_id, description, scope, section, content } = params;
104
+ const skillParams = params as {
105
+ action: "create" | "view" | "patch" | "update" | "edit" | "delete";
106
+ name?: string;
107
+ skill_id?: string;
108
+ description?: string;
109
+ scope?: "global" | "project";
110
+ section?: string;
111
+ content?: string;
112
+ when_to_use?: string;
113
+ procedure_steps?: unknown;
114
+ pitfalls?: unknown;
115
+ verification_steps?: unknown;
116
+ };
117
+ const {
118
+ action,
119
+ name,
120
+ skill_id,
121
+ description,
122
+ scope,
123
+ section,
124
+ content,
125
+ when_to_use,
126
+ procedure_steps,
127
+ pitfalls,
128
+ verification_steps,
129
+ } = skillParams;
130
+
131
+ const whenToUse = typeof when_to_use === "string" ? when_to_use.trim() : "";
132
+ const procedureSteps = normalizeTextList(procedure_steps);
133
+ const pitfallItems = normalizeTextList(pitfalls);
134
+ const verificationSteps = normalizeTextList(verification_steps);
135
+ const hasStructuredBody = Boolean(whenToUse) || procedureSteps.length > 0 || pitfallItems.length > 0 || verificationSteps.length > 0;
136
+
137
+ const buildBodyOrError = () => {
138
+ if (content?.trim()) return { body: content.trim() };
139
+ if (!hasStructuredBody) {
140
+ return {
141
+ error: "Either content or structured fields are required. Prefer when_to_use, procedure_steps, pitfalls, and verification_steps for create/update.",
142
+ };
143
+ }
144
+ if (!whenToUse) {
145
+ return { error: "when_to_use is required when content is omitted." };
146
+ }
147
+ if (procedureSteps.length === 0) {
148
+ return { error: "procedure_steps is required when content is omitted." };
149
+ }
150
+ if (verificationSteps.length === 0) {
151
+ return { error: "verification_steps is required when content is omitted." };
152
+ }
153
+ return {
154
+ body: buildStructuredSkillBody(whenToUse, procedureSteps, pitfallItems, verificationSteps),
155
+ };
156
+ };
47
157
 
48
158
  let result;
49
159
  switch (action) {
@@ -60,13 +170,20 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
60
170
  details: {},
61
171
  };
62
172
  }
63
- if (!content) {
173
+ const createBodyResult = buildBodyOrError();
174
+ if (!createBodyResult.body) {
64
175
  return {
65
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "content (skill body) is required for 'create' action." }) }],
176
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: createBodyResult.error }) }],
66
177
  details: {},
67
178
  };
68
179
  }
69
- result = await store.create(name, description, content, scope);
180
+ if (!scope) {
181
+ return {
182
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: "scope is required for 'create' action. Use 'global' or 'project'." }) }],
183
+ details: {},
184
+ };
185
+ }
186
+ result = await store.create(name, description, createBodyResult.body, scope);
70
187
  break;
71
188
 
72
189
  case "view":
@@ -109,15 +226,33 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
109
226
  result = await store.patch(skill_id, section, content);
110
227
  break;
111
228
 
112
- case "edit":
229
+ case "update":
230
+ case "edit": {
231
+ const updateActionLabel = action === "edit" ? "edit" : "update";
113
232
  if (!skill_id) {
114
233
  return {
115
- content: [{ type: "text", text: JSON.stringify({ success: false, error: "skill_id is required for 'edit' action." }) }],
234
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `skill_id is required for '${updateActionLabel}' action.` }) }],
235
+ details: {},
236
+ };
237
+ }
238
+ const updateBodyResult = buildBodyOrError();
239
+ const nextDescription = description?.trim() || "";
240
+ const nextBody = updateBodyResult.body ?? content?.trim() ?? "";
241
+ if (!nextDescription && !nextBody) {
242
+ return {
243
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: `Provide description, content, or structured fields for '${updateActionLabel}'.` }) }],
244
+ details: {},
245
+ };
246
+ }
247
+ if (hasStructuredBody && !updateBodyResult.body) {
248
+ return {
249
+ content: [{ type: "text", text: JSON.stringify({ success: false, error: updateBodyResult.error }) }],
116
250
  details: {},
117
251
  };
118
252
  }
119
- result = await store.edit(skill_id, description || "", content || "");
253
+ result = await store.edit(skill_id, nextDescription, nextBody);
120
254
  break;
255
+ }
121
256
 
122
257
  case "delete":
123
258
  if (!skill_id) {
@@ -132,7 +267,7 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
132
267
  default:
133
268
  result = {
134
269
  success: false,
135
- error: `Unknown action '${action}'. Use: create, view, patch, edit, delete`,
270
+ error: `Unknown action '${action}'. Use: create, view, patch, update, delete`,
136
271
  };
137
272
  }
138
273
 
package/src/types.ts CHANGED
@@ -126,6 +126,10 @@ export interface SkillIndex {
126
126
  displayName?: string;
127
127
  /** Short description shown in skill listings */
128
128
  description: string;
129
+ /** ISO date created */
130
+ created: string;
131
+ /** ISO date last updated */
132
+ updated: string;
129
133
  }
130
134
 
131
135
  export interface SkillDocument extends SkillIndex {
@@ -133,10 +137,6 @@ export interface SkillDocument extends SkillIndex {
133
137
  body: string;
134
138
  /** Version number */
135
139
  version: number;
136
- /** ISO date created */
137
- created: string;
138
- /** ISO date last updated */
139
- updated: string;
140
140
  }
141
141
 
142
142
  export interface SkillResult {
@@ -149,7 +149,7 @@ export interface SkillResult {
149
149
  path?: string;
150
150
  conflictType?: "duplicate" | "similar" | "name-collision" | "scope-conflict";
151
151
  similarSkillIds?: string[];
152
- suggestedAction?: "patch" | "edit" | "rename";
152
+ suggestedAction?: "patch" | "update" | "rename";
153
153
  }
154
154
 
155
155
  /**
@@ -1,128 +0,0 @@
1
- /**
2
- * Skill auto-trigger — after complex tasks (8+ tool calls, 2+ distinct tool types),
3
- * trigger automatic skill extraction via pi.exec().
4
- *
5
- * This implements Hermes' "self-evaluation checkpoint" pattern.
6
- */
7
-
8
- import * as path from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
- import { MemoryStore } from "../store/memory-store.js";
12
- import { SkillStore } from "../store/skill-store.js";
13
- import { COMBINED_REVIEW_PROMPT, DEFAULT_SKILL_TRIGGER_TOOL_CALLS, ENTRY_DELIMITER } from "../constants.js";
14
- import type { MemoryConfig } from "../types.js";
15
- import { getMessageText } from "../types.js";
16
-
17
- const proceduralSkillCreatorPath = path.join(
18
- path.dirname(fileURLToPath(import.meta.url)),
19
- "..",
20
- "skills",
21
- "procedural-skill-creator",
22
- );
23
-
24
- export function setupSkillAutoTrigger(
25
- pi: ExtensionAPI,
26
- store: MemoryStore,
27
- skillStore: SkillStore,
28
- config: MemoryConfig,
29
- ): void {
30
- let triggeredThisSession = false;
31
-
32
- // Accumulate tool calls across turns (reset on trigger)
33
- let toolCallCount = 0;
34
- const toolTypes = new Set<string>();
35
-
36
- pi.on("turn_end", async (event, ctx) => {
37
- if (triggeredThisSession) return;
38
-
39
- // Count tool calls from this turn's message only (not cumulative branch scan —
40
- // otherwise the counter accumulates historical tool calls and fires prematurely).
41
- try {
42
- const msg = event.message;
43
- if (msg?.role === "assistant") {
44
- const content = msg?.content;
45
- if (Array.isArray(content)) {
46
- for (const block of content) {
47
- if (block && typeof block === "object" && block.type === "toolCall") {
48
- toolCallCount++;
49
- if ((block as { name?: string }).name) toolTypes.add((block as { name: string }).name);
50
- }
51
- }
52
- }
53
- }
54
- } catch {
55
- return;
56
- }
57
-
58
- // Require 8+ tool calls AND 2+ distinct tool types
59
- if (toolCallCount < DEFAULT_SKILL_TRIGGER_TOOL_CALLS) return;
60
- if (toolTypes.size < 2) return;
61
-
62
- triggeredThisSession = true;
63
-
64
- try {
65
- // Build conversation context
66
- const branch = ctx.sessionManager.getBranch();
67
- const parts: string[] = [];
68
-
69
- for (const entry of branch) {
70
- if (entry.type !== "message") continue;
71
- const msg = entry.message;
72
- const text = getMessageText(msg);
73
- if (!text) continue;
74
- const prefix = msg.role === "user" ? "[USER]" : "[ASSISTANT]";
75
- parts.push(`${prefix}: ${text}`);
76
- }
77
-
78
- // Only include recent context
79
- const recentParts = parts.slice(-10);
80
-
81
- const currentMemory = store.getMemoryEntries().join(ENTRY_DELIMITER);
82
- const skillIndex = await skillStore.loadIndex();
83
- const skillSummary = skillIndex
84
- .map((skill) => `${skill.skillId} [${skill.scope}]: ${skill.displayName || skill.name} - ${skill.description}`)
85
- .join("\n");
86
-
87
- const activeProjectName = skillStore.getProjectName();
88
- const scopeLine = activeProjectName
89
- ? `Active project context: '${activeProjectName}'. If workflow details depend on this project, use scope='project'; otherwise use scope='global'.`
90
- : "No active project context detected. Use scope='global' unless the workflow is clearly project-specific.";
91
-
92
- const prompt = [
93
- "This was a complex task that required multiple tool calls. Extract any reusable procedures as skills.",
94
- "A bundled skill named procedural-skill-creator is loaded for you. Read and follow it before deciding whether to create or patch a skill.",
95
- "Always pass scope explicitly when creating a skill: scope='global' or scope='project'.",
96
- "Choose scope='global' for transferable procedures and scope='project' when the workflow depends on this repo's paths, scripts, architecture, deploy steps, or conventions.",
97
- scopeLine,
98
- "",
99
- "--- Existing Skills ---",
100
- skillSummary || "(none)",
101
- "",
102
- "--- Current Memory ---",
103
- currentMemory || "(empty)",
104
- "",
105
- "--- Recent Conversation ---",
106
- recentParts.join("\n\n"),
107
- "",
108
- "If a skill should be created, use the skill tool with action 'create'.",
109
- "If a related skill already exists, use 'patch' with its skill_id to update it.",
110
- "If nothing reusable happened, say 'Nothing to extract.' and stop.",
111
- ].join("\n");
112
-
113
- const result = await pi.exec("pi", ["-p", "--no-session", "--skill", proceduralSkillCreatorPath, prompt], {
114
- signal: ctx.signal,
115
- timeout: 60000,
116
- });
117
-
118
- if (result.code === 0 && result.stdout) {
119
- const output = result.stdout.trim();
120
- if (output && !output.toLowerCase().includes("nothing to extract")) {
121
- ctx.ui.notify("🧠 Complex task detected — skill extracted", "info");
122
- }
123
- }
124
- } catch {
125
- // Best-effort — don't block
126
- }
127
- });
128
- }
@@ -1,146 +0,0 @@
1
- ---
2
- name: procedural-skill-creator
3
- description: Create, patch, or improve a procedural skill using the extension's skill tool. Use when recent work should be captured as a reusable global or project-scoped procedure.
4
- ---
5
-
6
- # Procedural Skill Creator
7
-
8
- Turn recent work into a durable procedural skill managed by the extension `skill` tool.
9
-
10
- ## Goal
11
-
12
- Capture repeatable **how-to workflows** so future agents can execute them reliably.
13
-
14
- - `create` when no skill covers the workflow.
15
- - `patch` when an existing skill should absorb new learning.
16
- - skip extraction when the work is one-off.
17
-
18
- ## Operating Principle
19
-
20
- Extract from what already happened first (conversation, commands, edits, failures, fixes).
21
- Ask follow-up questions only if critical details are missing.
22
-
23
- ## Extraction Gate
24
-
25
- Save a skill only if all checks pass:
26
-
27
- 1. Multi-step workflow (not a single trivial action)
28
- 2. Likely to recur
29
- 3. Includes non-obvious pitfalls/decision points
30
- 4. Can be verified with concrete pass/fail checks
31
-
32
- If any check fails, respond exactly:
33
-
34
- `Nothing to extract.`
35
-
36
- ## Scope Decision
37
-
38
- - `global`: portable across repos/projects
39
- - `project`: depends on this repo's paths, scripts, architecture, or conventions
40
-
41
- Heuristic: if repo-specific paths or commands are required, use `project`.
42
-
43
- ## Global Skill De-duplication Protocol
44
-
45
- Before creating a **global** skill, prevent overlap with existing global skills.
46
-
47
- 1. List skills with `skill(action="view")`.
48
- 2. Filter to `scope=global`.
49
- 3. Compare candidate against existing skills in three passes:
50
- - **Name pass**: exact slug match or near-name match (same core verb+noun)
51
- - **Description pass**: same trigger intent / same expected outcome
52
- - **Procedure pass**: substantially same step sequence
53
- 4. Decide action:
54
- - **Exact same name/intent** → do **not** create; use `patch`/`edit`
55
- - **Different name, same intent** → merge into existing skill (patch existing), avoid duplicate
56
- - **Adjacent but distinct intent** → create new skill with sharper boundary in `When to Use`
57
-
58
- If uncertain between two similar skills, run a tie-breaker:
59
- - ask: "Would both skills trigger for the same user prompt and produce the same outcome?"
60
- - if yes, merge; if no, keep separate and clarify boundaries.
61
-
62
- Default bias: **merge over duplicate**.
63
-
64
- ## Action Decision
65
-
66
- - `create`: no existing skill covers the core job
67
- - `patch`: existing skill exists; improve only changed section(s)
68
-
69
- Prefer patching over creating overlapping skills.
70
-
71
- ## Workflow
72
-
73
- 1. **Capture intent**
74
- - What job should this skill enable?
75
- - When should it trigger?
76
- - What outcome should it produce?
77
- 2. **Collect evidence from recent work**
78
- - successful sequence
79
- - dead ends and corrections
80
- - verification signals
81
- 3. **Run de-dup check** (required for global scope)
82
- 4. **Decide** `create` / `patch` / `Nothing to extract.`
83
- 5. **Draft or revise** using required sections:
84
- - `## When to Use`
85
- - `## Procedure`
86
- - `## Pitfalls`
87
- - `## Verification`
88
- 6. **Run a lightweight eval pass** (before saving):
89
- - one normal case
90
- - one edge case
91
- - one near-miss (should *not* use this skill)
92
- Refine if ambiguous.
93
- 7. **Persist with `skill` tool**
94
- - create: `name`, `description`, `scope` (always explicit), full body
95
- - patch: `skill_id`, `section`, section content
96
-
97
- ## Authoring Standards
98
-
99
- ### Name
100
-
101
- - short kebab-case (`debug-ci-timeouts`, `backfill-flag-rollout`)
102
- - name the reusable job, not the incident
103
-
104
- ### Description (Trigger Quality)
105
-
106
- The description is the main trigger signal.
107
- Include:
108
- - what it does
109
- - when to use it
110
- - nearby phrasing users might use
111
-
112
- Be explicit enough to avoid under-triggering, without becoming spammy.
113
-
114
- ### Section Quality
115
-
116
- - **When to Use**: trigger conditions + boundaries
117
- - **Procedure**: ordered, actionable steps
118
- - **Pitfalls**: frequent failure modes + prevention
119
- - **Verification**: concrete checks (tests, logs, files, outputs)
120
-
121
- ## Generalization Guardrails
122
-
123
- - Don’t overfit to one transcript.
124
- - Explain *why* key steps matter when non-obvious.
125
- - Prefer principles + steps over rigid cargo-cult rules.
126
- - Keep it lean; remove steps that do not change outcomes.
127
-
128
- ## Patch Guidance
129
-
130
- When patching:
131
-
132
- - use existing `skill_id`
133
- - patch only changed section(s)
134
- - prioritize `Procedure`, `Pitfalls`, `Verification`
135
- - avoid rewriting unrelated content
136
-
137
- ## Rules
138
-
139
- - Use `skill` tool only (no direct file writes).
140
- - Prefer one strong skill over many near-duplicates.
141
- - Do not store temporary task state, ticket notes, or one-off results.
142
-
143
- ## Completion Standard
144
-
145
- A saved skill must allow a future agent to execute the workflow with minimal guesswork and clear verification.
146
- If not, refine before saving.