pi-hermes-memory 0.7.5 → 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 +59 -20
- package/docs/PUBLISHING.md +1 -1
- package/docs/ROADMAP.md +1 -1
- package/package.json +4 -4
- package/src/config.ts +3 -0
- package/src/constants.ts +8 -3
- package/src/handlers/auto-consolidate.ts +5 -3
- 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 +54 -11
- 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/store/sqlite-memory-store.ts +42 -4
- package/src/tools/memory-search-tool.ts +2 -2
- package/src/tools/memory-tool.ts +29 -2
- package/src/tools/session-search-tool.ts +2 -2
- package/src/tools/skill-tool.ts +23 -20
- package/src/types.ts +24 -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
|
+
}
|
|
@@ -67,6 +67,11 @@ export interface SqliteMemoryRemoveResult {
|
|
|
67
67
|
removed: number;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export interface SqliteMemoryRemoveOptions {
|
|
71
|
+
target: 'memory' | 'user' | 'failure';
|
|
72
|
+
project?: string | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
70
75
|
export interface ParsedMarkdownMemoryEntry extends SqliteMemorySyncInput {}
|
|
71
76
|
|
|
72
77
|
function today(): string {
|
|
@@ -481,10 +486,7 @@ export function replaceSyncedMemories(
|
|
|
481
486
|
export function removeSyncedMemories(
|
|
482
487
|
dbManager: DatabaseManager,
|
|
483
488
|
oldText: string,
|
|
484
|
-
options:
|
|
485
|
-
target: 'memory' | 'user' | 'failure';
|
|
486
|
-
project?: string | null;
|
|
487
|
-
},
|
|
489
|
+
options: SqliteMemoryRemoveOptions,
|
|
488
490
|
): SqliteMemoryRemoveResult {
|
|
489
491
|
const db = dbManager.getDb();
|
|
490
492
|
const params: unknown[] = [];
|
|
@@ -512,6 +514,42 @@ export function removeSyncedMemories(
|
|
|
512
514
|
};
|
|
513
515
|
}
|
|
514
516
|
|
|
517
|
+
/**
|
|
518
|
+
* Exact removal for Markdown entries whose full content is known.
|
|
519
|
+
* Used for FIFO eviction cleanup, where substring matching could remove
|
|
520
|
+
* unrelated SQLite mirror rows that merely contain the evicted text.
|
|
521
|
+
*/
|
|
522
|
+
export function removeExactSyncedMemories(
|
|
523
|
+
dbManager: DatabaseManager,
|
|
524
|
+
content: string,
|
|
525
|
+
options: SqliteMemoryRemoveOptions,
|
|
526
|
+
): SqliteMemoryRemoveResult {
|
|
527
|
+
const db = dbManager.getDb();
|
|
528
|
+
const params: unknown[] = [];
|
|
529
|
+
const conditions = buildScopeConditions(params, options.target, options.project ?? undefined);
|
|
530
|
+
conditions.push('content = ?');
|
|
531
|
+
params.push(content.trim());
|
|
532
|
+
|
|
533
|
+
const matchingIds = db.prepare(`
|
|
534
|
+
SELECT id
|
|
535
|
+
FROM memories
|
|
536
|
+
WHERE ${conditions.join(' AND ')}
|
|
537
|
+
`).all(...params) as Array<{ id: number }>;
|
|
538
|
+
|
|
539
|
+
if (matchingIds.length === 0) {
|
|
540
|
+
return { matched: 0, removed: 0 };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const deleteParams = matchingIds.map((row) => row.id);
|
|
544
|
+
const placeholders = deleteParams.map(() => '?').join(', ');
|
|
545
|
+
const result = db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...deleteParams);
|
|
546
|
+
|
|
547
|
+
return {
|
|
548
|
+
matched: matchingIds.length,
|
|
549
|
+
removed: result.changes,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
515
553
|
/**
|
|
516
554
|
* Escape a string for FTS5 query syntax.
|
|
517
555
|
* Wraps the query in double quotes to treat it as a literal phrase.
|
|
@@ -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,13 +4,14 @@
|
|
|
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 {
|
|
13
13
|
formatFailureMemoryContent,
|
|
14
|
+
removeExactSyncedMemories,
|
|
14
15
|
removeSyncedMemories,
|
|
15
16
|
replaceSyncedMemories,
|
|
16
17
|
syncMemoryEntry,
|
|
@@ -159,6 +160,31 @@ async function syncRemoveFromSqlite(
|
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
async function syncEvictionsFromSqlite(
|
|
164
|
+
rawTarget: "memory" | "user" | "project" | "failure",
|
|
165
|
+
evictedEntries: string[] | undefined,
|
|
166
|
+
dbManager: DatabaseManager | null,
|
|
167
|
+
projectName?: string | null,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
if (!dbManager) return;
|
|
170
|
+
if (!evictedEntries || evictedEntries.length === 0) return;
|
|
171
|
+
|
|
172
|
+
const sqliteTarget = sqliteTargetFor(rawTarget);
|
|
173
|
+
const sqliteProject = sqliteProjectFor(rawTarget, projectName);
|
|
174
|
+
|
|
175
|
+
for (const entry of evictedEntries) {
|
|
176
|
+
try {
|
|
177
|
+
removeExactSyncedMemories(dbManager, entry, {
|
|
178
|
+
target: sqliteTarget,
|
|
179
|
+
project: sqliteProject,
|
|
180
|
+
});
|
|
181
|
+
} catch {
|
|
182
|
+
// FIFO already updated the Markdown source of truth. SQLite is only a
|
|
183
|
+
// best-effort search mirror, so eviction cleanup must not fail the write.
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
162
188
|
export function registerMemoryTool(
|
|
163
189
|
pi: ExtensionAPI,
|
|
164
190
|
store: MemoryStore,
|
|
@@ -247,6 +273,7 @@ export function registerMemoryTool(
|
|
|
247
273
|
} else {
|
|
248
274
|
result = await store_.add(target, content);
|
|
249
275
|
if (result.success) {
|
|
276
|
+
await syncEvictionsFromSqlite(rawTarget, result.evicted_entries, dbManager, projectName);
|
|
250
277
|
syncWarning = await syncAddToSqlite(rawTarget, content, undefined, undefined, dbManager, projectName);
|
|
251
278
|
}
|
|
252
279
|
}
|
|
@@ -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
|
|
|
@@ -59,6 +59,8 @@ export interface MemoryConfig {
|
|
|
59
59
|
failureInjectionMaxEntries: number;
|
|
60
60
|
/** Tool calls before triggering background review (in addition to turn count). Default: 15 */
|
|
61
61
|
nudgeToolCalls: number;
|
|
62
|
+
/** Maximum time in milliseconds for auto-consolidation to complete. Default: 60000 */
|
|
63
|
+
consolidationTimeoutMs: number;
|
|
62
64
|
/** Enable session history search via SQLite FTS5. Default: true */
|
|
63
65
|
sessionSearchEnabled?: boolean;
|
|
64
66
|
/** Days to retain session history. Default: 90 */
|
|
@@ -100,12 +102,24 @@ export interface ConsolidationResult {
|
|
|
100
102
|
error?: string;
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
export type SkillScope = "global" | "project";
|
|
106
|
+
|
|
103
107
|
export interface SkillIndex {
|
|
104
|
-
/**
|
|
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) */
|
|
105
113
|
fileName: string;
|
|
106
|
-
/**
|
|
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 */
|
|
107
119
|
name: string;
|
|
108
|
-
/**
|
|
120
|
+
/** Optional human-friendly title preserved for UI output */
|
|
121
|
+
displayName?: string;
|
|
122
|
+
/** Short description shown in skill listings */
|
|
109
123
|
description: string;
|
|
110
124
|
}
|
|
111
125
|
|
|
@@ -125,6 +139,12 @@ export interface SkillResult {
|
|
|
125
139
|
error?: string;
|
|
126
140
|
message?: string;
|
|
127
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";
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
/**
|