pi-hermes-memory 0.7.10 → 0.7.12
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 +36 -7
- package/docs/images/source-architecture.svg +1 -1
- package/docs/mermaid/source-architecture.mmd +0 -4
- package/package.json +1 -1
- package/src/config.ts +9 -2
- package/src/constants.ts +42 -6
- package/src/handlers/auto-consolidate.ts +46 -15
- package/src/handlers/learn-memory.ts +1 -1
- package/src/handlers/skills-command.ts +116 -15
- package/src/handlers/sync-markdown-memories.ts +10 -4
- package/src/index.ts +18 -16
- package/src/paths.ts +57 -0
- package/src/project-memory-migration.ts +1 -2
- package/src/project.ts +2 -1
- package/src/store/db.ts +3 -0
- package/src/store/fts-query.ts +38 -0
- package/src/store/session-search.ts +14 -16
- package/src/store/skill-store.ts +7 -3
- package/src/store/sqlite-memory-store.ts +31 -28
- package/src/tools/skill-tool.ts +166 -31
- package/src/types.ts +5 -5
- package/src/handlers/skill-auto-trigger.ts +0 -128
- package/src/skills/procedural-skill-creator/SKILL.md +0 -146
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Escape a string for FTS5 query syntax.
|
|
5
|
-
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
6
|
-
*/
|
|
7
|
-
function escapeFts5Query(query: string): string {
|
|
8
|
-
// If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
|
|
9
|
-
if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
|
|
10
|
-
return query;
|
|
11
|
-
}
|
|
12
|
-
// Otherwise, wrap in double quotes to treat as literal phrase
|
|
13
|
-
return `"${query.replace(/"/g, '""')}"`;
|
|
14
|
-
}
|
|
2
|
+
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
15
3
|
|
|
16
4
|
/**
|
|
17
5
|
* Search result from session history.
|
|
@@ -52,6 +40,10 @@ export function searchSessions(
|
|
|
52
40
|
query: string,
|
|
53
41
|
options: SessionSearchOptions = {}
|
|
54
42
|
): SessionSearchResult[] {
|
|
43
|
+
if (query.trim().length === 0) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
55
47
|
const db = dbManager.getDb();
|
|
56
48
|
const { limit = 10, project, role, since } = options;
|
|
57
49
|
|
|
@@ -60,8 +52,12 @@ export function searchSessions(
|
|
|
60
52
|
const params: unknown[] = [];
|
|
61
53
|
|
|
62
54
|
// FTS5 match condition — use subquery for reliable rowid matching
|
|
55
|
+
const normalizedQuery = normalizeFts5Query(query);
|
|
56
|
+
if (normalizedQuery.length === 0) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
63
59
|
conditions.push('m.rowid IN (SELECT rowid FROM message_fts WHERE message_fts MATCH ?)');
|
|
64
|
-
params.push(
|
|
60
|
+
params.push(normalizedQuery);
|
|
65
61
|
|
|
66
62
|
// Project filter
|
|
67
63
|
if (project) {
|
|
@@ -119,8 +115,10 @@ export function searchSessions(
|
|
|
119
115
|
snippet: row.snippet,
|
|
120
116
|
}));
|
|
121
117
|
} catch (err) {
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
if (isFts5QueryError(err)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
124
122
|
}
|
|
125
123
|
}
|
|
126
124
|
|
package/src/store/skill-store.ts
CHANGED
|
@@ -253,6 +253,8 @@ export class SkillStore {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
return skills.sort((a, b) => {
|
|
256
|
+
if (a.updated !== b.updated) return b.updated.localeCompare(a.updated);
|
|
257
|
+
if (a.created !== b.created) return b.created.localeCompare(a.created);
|
|
256
258
|
if (a.scope !== b.scope) return a.scope.localeCompare(b.scope);
|
|
257
259
|
return (a.displayName || a.name).localeCompare(b.displayName || b.name);
|
|
258
260
|
});
|
|
@@ -290,7 +292,7 @@ export class SkillStore {
|
|
|
290
292
|
if (existing) {
|
|
291
293
|
return {
|
|
292
294
|
success: false,
|
|
293
|
-
error: `Skill '${slug}' already exists (${skillId}). Use 'patch' or '
|
|
295
|
+
error: `Skill '${slug}' already exists (${skillId}). Use 'patch' or 'update' to update it.`,
|
|
294
296
|
conflictType: "duplicate",
|
|
295
297
|
similarSkillIds: [skillId],
|
|
296
298
|
suggestedAction: "patch",
|
|
@@ -303,7 +305,7 @@ export class SkillStore {
|
|
|
303
305
|
const targetId = similarSkillIds[0];
|
|
304
306
|
return {
|
|
305
307
|
success: false,
|
|
306
|
-
error: `A similar global skill already exists (${targetId}). Enhance the existing skill with new learnings/failures using 'patch' or '
|
|
308
|
+
error: `A similar global skill already exists (${targetId}). Enhance the existing skill with new learnings/failures using 'patch' or 'update' instead of creating a duplicate.`,
|
|
307
309
|
conflictType: "similar",
|
|
308
310
|
similarSkillIds,
|
|
309
311
|
suggestedAction: "patch",
|
|
@@ -315,7 +317,7 @@ export class SkillStore {
|
|
|
315
317
|
const targetId = collidingNameSkillIds[0];
|
|
316
318
|
return {
|
|
317
319
|
success: false,
|
|
318
|
-
error: `A near-name global skill already exists (${targetId}) but with different intent. Use a clearer differentiated name for the new skill, or patch/
|
|
320
|
+
error: `A near-name global skill already exists (${targetId}) but with different intent. Use a clearer differentiated name for the new skill, or patch/update the existing skill if the intent is actually the same.`,
|
|
319
321
|
conflictType: "name-collision",
|
|
320
322
|
similarSkillIds: collidingNameSkillIds,
|
|
321
323
|
suggestedAction: "rename",
|
|
@@ -790,6 +792,8 @@ export class SkillStore {
|
|
|
790
792
|
name: doc.name,
|
|
791
793
|
displayName: doc.displayName,
|
|
792
794
|
description: doc.description,
|
|
795
|
+
created: doc.created,
|
|
796
|
+
updated: doc.updated,
|
|
793
797
|
};
|
|
794
798
|
}
|
|
795
799
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
+
import { isFts5QueryError, normalizeFts5Query } from './fts-query.js';
|
|
2
3
|
import type { MemoryCategory } from '../types.js';
|
|
3
4
|
|
|
4
5
|
const MEMORY_SELECT_COLUMNS = `
|
|
@@ -550,19 +551,6 @@ export function removeExactSyncedMemories(
|
|
|
550
551
|
};
|
|
551
552
|
}
|
|
552
553
|
|
|
553
|
-
/**
|
|
554
|
-
* Escape a string for FTS5 query syntax.
|
|
555
|
-
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
556
|
-
*/
|
|
557
|
-
function escapeFts5Query(query: string): string {
|
|
558
|
-
// If the query already contains FTS5 operators (OR, AND, NOT, NEAR), leave it as-is
|
|
559
|
-
if (/\b(OR|AND|NOT|NEAR)\b/.test(query)) {
|
|
560
|
-
return query;
|
|
561
|
-
}
|
|
562
|
-
// Otherwise, wrap in double quotes to treat as literal phrase
|
|
563
|
-
return `"${query.replace(/"/g, '""')}"`;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
554
|
/**
|
|
567
555
|
* Search memories using FTS5.
|
|
568
556
|
*/
|
|
@@ -571,6 +559,10 @@ export function searchMemories(
|
|
|
571
559
|
query: string,
|
|
572
560
|
options: { project?: string; target?: string; category?: MemoryCategory; limit?: number } = {}
|
|
573
561
|
): SqliteMemoryEntry[] {
|
|
562
|
+
if (query.trim().length === 0) {
|
|
563
|
+
return [];
|
|
564
|
+
}
|
|
565
|
+
|
|
574
566
|
const db = dbManager.getDb();
|
|
575
567
|
const { project, target, category, limit = 10 } = options;
|
|
576
568
|
|
|
@@ -578,8 +570,12 @@ export function searchMemories(
|
|
|
578
570
|
const params: unknown[] = [];
|
|
579
571
|
|
|
580
572
|
// FTS5 match via subquery with escaped query
|
|
573
|
+
const normalizedQuery = normalizeFts5Query(query);
|
|
574
|
+
if (normalizedQuery.length === 0) {
|
|
575
|
+
return [];
|
|
576
|
+
}
|
|
581
577
|
conditions.push('m.id IN (SELECT rowid FROM memory_fts WHERE memory_fts MATCH ?)');
|
|
582
|
-
params.push(
|
|
578
|
+
params.push(normalizedQuery);
|
|
583
579
|
|
|
584
580
|
if (project !== undefined) {
|
|
585
581
|
if (project === null) {
|
|
@@ -611,20 +607,27 @@ export function searchMemories(
|
|
|
611
607
|
`;
|
|
612
608
|
params.push(limit);
|
|
613
609
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
610
|
+
try {
|
|
611
|
+
const rows = db.prepare(sql).all(...params) as Array<{
|
|
612
|
+
id: number;
|
|
613
|
+
project: string | null;
|
|
614
|
+
target: string;
|
|
615
|
+
category: string | null;
|
|
616
|
+
content: string;
|
|
617
|
+
failure_reason: string | null;
|
|
618
|
+
tool_state: string | null;
|
|
619
|
+
corrected_to: string | null;
|
|
620
|
+
created: string;
|
|
621
|
+
last_referenced: string;
|
|
622
|
+
}>;
|
|
623
|
+
|
|
624
|
+
return rows.map(mapRow);
|
|
625
|
+
} catch (err) {
|
|
626
|
+
if (isFts5QueryError(err)) {
|
|
627
|
+
return [];
|
|
628
|
+
}
|
|
629
|
+
throw err;
|
|
630
|
+
}
|
|
628
631
|
}
|
|
629
632
|
|
|
630
633
|
/**
|
package/src/tools/skill-tool.ts
CHANGED
|
@@ -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
|
-
"
|
|
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:
|
|
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
|
|
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
|
-
|
|
173
|
+
const createBodyResult = buildBodyOrError();
|
|
174
|
+
if (!createBodyResult.body) {
|
|
64
175
|
return {
|
|
65
|
-
content: [{ type: "text", text: JSON.stringify({ success: false, error:
|
|
176
|
+
content: [{ type: "text", text: JSON.stringify({ success: false, error: createBodyResult.error }) }],
|
|
66
177
|
details: {},
|
|
67
178
|
};
|
|
68
179
|
}
|
|
69
|
-
|
|
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 "
|
|
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:
|
|
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,
|
|
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,
|
|
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" | "
|
|
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
|
-
}
|