pi-hermes-memory 0.8.1 → 0.8.2
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 +17 -0
- package/package.json +1 -1
- package/src/constants.ts +8 -7
- package/src/extension-root-migration.ts +19 -2
- package/src/store/atomic-lock-coordinator.ts +2 -2
- package/src/store/db.ts +6 -6
- package/src/store/memory-store.ts +71 -23
- package/src/store/skill-store.ts +127 -9
- package/src/store/sqlite-native.ts +229 -0
- package/src/tools/skill-tool.ts +59 -11
package/README.md
CHANGED
|
@@ -122,6 +122,23 @@ Or test locally without installing:
|
|
|
122
122
|
pi -e /path/to/pi-hermes-memory/src/index.ts
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### Homebrew / Node ABI mismatches
|
|
126
|
+
|
|
127
|
+
`better-sqlite3` is a native addon. If Pi is installed via Homebrew and the extension was compiled for a different Node ABI, session search may warn:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
was compiled against a different Node.js version using NODE_MODULE_VERSION ...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The extension attempts one automatic `npm rebuild better-sqlite3` against the Node that is running Pi. If that still fails:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
cd ~/.pi/agent/npm/node_modules/better-sqlite3
|
|
137
|
+
npm rebuild better-sqlite3
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Or install Pi with npm so the host runtime and extension install share one Node toolchain.
|
|
141
|
+
|
|
125
142
|
## Two-Tier Memory Architecture
|
|
126
143
|
|
|
127
144
|
The extension stores memory at two levels:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hermes-memory",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.2",
|
|
4
4
|
"description": "🧠 Persistent memory + 🔍 session search + 🛡️ secret scanning for Pi. Token-aware policy-only memory by default, SQLite FTS5 search, auto-consolidation, procedural skills. 693 tests. Ported from Hermes agent.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/constants.ts
CHANGED
|
@@ -70,7 +70,7 @@ If memory conflicts with current evidence, prefer current evidence and mention t
|
|
|
70
70
|
Procedural skills:
|
|
71
71
|
- Use the skill_manage tool during normal work when a task reveals a reusable how-to workflow, or when the user asks you to remember how to do something later.
|
|
72
72
|
- Always pass scope explicitly on create: scope="global" for portable procedures, scope="project" for workflows tied to this repo's paths, scripts, architecture, deploy steps, or conventions.
|
|
73
|
-
- Prefer structured fields for create/update: when_to_use, procedure_steps, pitfalls, verification_steps. Use patch
|
|
73
|
+
- Prefer structured fields for create/update/patch: when_to_use, procedure_steps, pitfalls, verification_steps. Use patch with the matching structured field for one section, update for a full rewrite, and view before changing an existing skill.
|
|
74
74
|
- Do not create skills for one-off task state, generic summaries, or overly file-specific notes that will create noisy future matches.
|
|
75
75
|
|
|
76
76
|
Do not use memory_search for generic questions, one-off examples, or explanations where durable memory would not help.
|
|
@@ -92,7 +92,7 @@ Memory write targets: user for preferences/profile; memory for global notes and
|
|
|
92
92
|
|
|
93
93
|
memory_search filters: target searches user/global/failure memories; project filters project-scoped memories; category filters categorized failure/lesson memories only.
|
|
94
94
|
|
|
95
|
-
Use the skill_manage tool during normal work for reusable procedures. On create, scope is required: global for transferable workflows, project for repo-specific ones. Prefer structured fields for create/update, patch for
|
|
95
|
+
Use the skill_manage tool during normal work for reusable procedures. On create, scope is required: global for transferable workflows, project for repo-specific ones. Prefer structured fields for create/update/patch, patch for one section, and update for full rewrites. Skip one-off or overly narrow skills.
|
|
96
96
|
|
|
97
97
|
Use category only for categorized failure/lesson searches. Do not use memory_search for generic questions, one-off examples, or explanations where durable memory would not help.
|
|
98
98
|
|
|
@@ -315,20 +315,21 @@ SCOPE:
|
|
|
315
315
|
- 'global': transferable procedures that can be reused across repositories
|
|
316
316
|
- 'project': procedures tied to this repo's paths, scripts, architecture, deploy flow, or conventions
|
|
317
317
|
|
|
318
|
-
WHEN TO UPDATE A SKILL
|
|
319
|
-
-
|
|
320
|
-
-
|
|
321
|
-
-
|
|
318
|
+
WHEN TO UPDATE A SKILL:
|
|
319
|
+
- Prefer 'patch' for one section when you can pass structured fields
|
|
320
|
+
- Prefer 'update' for multi-section rewrites or when patch formatting would be unstable
|
|
321
|
+
- Use patch when you discover a better approach, pitfall, or changed step in one section
|
|
322
322
|
|
|
323
323
|
SKILL FORMAT:
|
|
324
324
|
- name: short, descriptive (e.g., "debug-typescript-errors")
|
|
325
325
|
- description: one-line summary of when to use it
|
|
326
326
|
- body: structured with sections — ## When to Use, ## Procedure, ## Pitfalls, ## Verification
|
|
327
|
-
- Prefer structured
|
|
327
|
+
- Prefer structured fields over raw markdown when possible:
|
|
328
328
|
- when_to_use: trigger conditions and boundaries
|
|
329
329
|
- procedure_steps: ordered concrete steps
|
|
330
330
|
- pitfalls: caveats or failure modes
|
|
331
331
|
- verification_steps: checks that prove success
|
|
332
|
+
- For patch, pass section plus the matching structured field (section="Procedure" + procedure_steps, etc.). Do not pass JSON array/object strings as content.
|
|
332
333
|
|
|
333
334
|
ONE-SHOT EXAMPLE:
|
|
334
335
|
{
|
|
@@ -2,10 +2,27 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import Database from "better-sqlite3";
|
|
6
5
|
import { AtomicLockCoordinator, type AtomicLockLease } from "./store/atomic-lock-coordinator.js";
|
|
7
6
|
import { canonicalStoragePathSync } from "./store/canonical-storage-path.js";
|
|
8
|
-
|
|
7
|
+
import { loadBetterSqlite3 } from "./store/sqlite-native.js";
|
|
8
|
+
|
|
9
|
+
type MigrationDatabase = {
|
|
10
|
+
exec: (sql: string) => void;
|
|
11
|
+
prepare: (sql: string) => { get: (...args: unknown[]) => unknown; run: (...args: unknown[]) => unknown };
|
|
12
|
+
close: () => void;
|
|
13
|
+
pragma: (query: string, options?: { simple?: boolean }) => unknown;
|
|
14
|
+
backup: (
|
|
15
|
+
destination: string,
|
|
16
|
+
options?: { progress?: () => void },
|
|
17
|
+
) => Promise<void> | void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type MigrationDatabaseCtor = new (
|
|
21
|
+
dbPath: string,
|
|
22
|
+
options?: { readonly?: boolean; fileMustExist?: boolean; timeout?: number },
|
|
23
|
+
) => MigrationDatabase;
|
|
24
|
+
|
|
25
|
+
const Database = loadBetterSqlite3() as MigrationDatabaseCtor;
|
|
9
26
|
const DATABASE_FILES = ["sessions.db", "sessions.db-wal", "sessions.db-shm"] as const;
|
|
10
27
|
const DATABASE_MIGRATION_PENDING_FILE = ".sessions-db-migration-pending";
|
|
11
28
|
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { loadBetterSqlite3 } from './sqlite-native.js';
|
|
6
7
|
|
|
7
8
|
type StatementLike = {
|
|
8
9
|
run: (...args: unknown[]) => unknown;
|
|
@@ -39,8 +40,7 @@ function loadDatabaseCtor(): DatabaseCtor {
|
|
|
39
40
|
const bunSqlite = require('bun:sqlite') as { Database: DatabaseCtor };
|
|
40
41
|
return bunSqlite.Database;
|
|
41
42
|
}
|
|
42
|
-
|
|
43
|
-
return (mod as { default?: DatabaseCtor }).default ?? (mod as DatabaseCtor);
|
|
43
|
+
return loadBetterSqlite3({ requireImpl: require }) as DatabaseCtor;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
const Database = loadDatabaseCtor();
|
package/src/store/db.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
import { SCHEMA_SQL } from './schema.js';
|
|
5
5
|
import { AtomicLockCoordinator } from './atomic-lock-coordinator.js';
|
|
6
6
|
import { canonicalStoragePathSync } from './canonical-storage-path.js';
|
|
7
|
+
import { loadBetterSqlite3 } from './sqlite-native.js';
|
|
7
8
|
|
|
8
9
|
type StatementLike = {
|
|
9
10
|
run: (...args: any[]) => any;
|
|
@@ -69,6 +70,7 @@ class DatabaseCorruptionError extends Error {
|
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
|
|
73
|
+
export const SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
72
74
|
export const SQLITE_WAL_AUTOCHECKPOINT_PAGES = 1000;
|
|
73
75
|
|
|
74
76
|
const DATABASE_FILE_SUFFIXES: readonly DatabaseFileSuffix[] = ['', '-wal', '-shm'];
|
|
@@ -126,12 +128,7 @@ function loadDatabaseCtor(): DatabaseCtor {
|
|
|
126
128
|
return createBunCompatDatabaseCtor(require);
|
|
127
129
|
}
|
|
128
130
|
|
|
129
|
-
|
|
130
|
-
const mod = require('better-sqlite3') as { default?: DatabaseCtor } | DatabaseCtor;
|
|
131
|
-
return (mod as { default?: DatabaseCtor }).default ?? (mod as DatabaseCtor);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
throw err;
|
|
134
|
-
}
|
|
131
|
+
return loadBetterSqlite3({ requireImpl: require }) as DatabaseCtor;
|
|
135
132
|
}
|
|
136
133
|
|
|
137
134
|
const Database = loadDatabaseCtor();
|
|
@@ -294,6 +291,9 @@ export class DatabaseManager {
|
|
|
294
291
|
}
|
|
295
292
|
|
|
296
293
|
private configureConnection(db: DatabaseLike): void {
|
|
294
|
+
// Wait briefly for concurrent writers across Pi processes instead of failing
|
|
295
|
+
// immediately with SQLITE_BUSY. Connection-local; applies before WAL/schema.
|
|
296
|
+
db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
297
297
|
// Enable WAL mode + FK enforcement for each connection. Keep SQLite's
|
|
298
298
|
// default WAL autocheckpoint size; a very aggressive checkpoint cadence
|
|
299
299
|
// increases the chance that abrupt VM/host shutdown catches a checkpoint.
|
|
@@ -587,6 +587,46 @@ export class MemoryStore {
|
|
|
587
587
|
this.fileFingerprints[filePath] = state.fingerprint;
|
|
588
588
|
}
|
|
589
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Reload target state from disk (source of truth), refresh success metadata,
|
|
592
|
+
* and always notify the mutation observer so SQLite stays aligned even when
|
|
593
|
+
* the mutation itself failed or an external editor raced the write.
|
|
594
|
+
*/
|
|
595
|
+
private async finalizeTargetMutation(
|
|
596
|
+
target: "memory" | "user" | "failure",
|
|
597
|
+
storagePath: string,
|
|
598
|
+
result: MemoryResult,
|
|
599
|
+
): Promise<MemoryResult> {
|
|
600
|
+
const state = await this.readFileState(storagePath);
|
|
601
|
+
this.setEntries(target, [...new Set(state.entries)]);
|
|
602
|
+
this.fileFingerprints[storagePath] = state.fingerprint;
|
|
603
|
+
|
|
604
|
+
let finalized = result;
|
|
605
|
+
if (result.success) {
|
|
606
|
+
finalized = {
|
|
607
|
+
...result,
|
|
608
|
+
...this.successResponse(target, result.message),
|
|
609
|
+
};
|
|
610
|
+
if (result.evicted_entries) finalized.evicted_entries = result.evicted_entries;
|
|
611
|
+
if (result.evicted_count !== undefined) finalized.evicted_count = result.evicted_count;
|
|
612
|
+
if (result.matches) finalized.matches = result.matches;
|
|
613
|
+
if (result.entries) finalized.entries = result.entries;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
if (!this.mutationObserver) return finalized;
|
|
617
|
+
|
|
618
|
+
const warning = await this.mutationObserver(target, [...state.entries]);
|
|
619
|
+
if (!warning || !finalized.success) return finalized;
|
|
620
|
+
|
|
621
|
+
const warnings = [...(finalized.warnings ?? []), warning];
|
|
622
|
+
return {
|
|
623
|
+
...finalized,
|
|
624
|
+
message: finalized.message ? `${finalized.message} Warning: ${warning}` : warning,
|
|
625
|
+
warning,
|
|
626
|
+
warnings,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
590
630
|
private async runTargetMutation(
|
|
591
631
|
target: "memory" | "user" | "failure",
|
|
592
632
|
mutation: () => Promise<MemoryResult>,
|
|
@@ -596,35 +636,32 @@ export class MemoryStore {
|
|
|
596
636
|
for (let attempt = 0; ; attempt++) {
|
|
597
637
|
try {
|
|
598
638
|
const result = await mutation();
|
|
599
|
-
if (result.success
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
this.fileFingerprints[
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
warnings,
|
|
612
|
-
};
|
|
639
|
+
if (result.success) {
|
|
640
|
+
// saveToDisk stamps fileFingerprints on success. If an editor
|
|
641
|
+
// truncates/replaces the file after publish returns, refuse the
|
|
642
|
+
// phantom success and retry against disk truth.
|
|
643
|
+
const expectedFingerprint = this.fileFingerprints[storagePath];
|
|
644
|
+
if (expectedFingerprint !== undefined) {
|
|
645
|
+
const state = await this.readFileState(storagePath);
|
|
646
|
+
if (state.fingerprint !== expectedFingerprint) {
|
|
647
|
+
this.setEntries(target, [...new Set(state.entries)]);
|
|
648
|
+
this.fileFingerprints[storagePath] = state.fingerprint;
|
|
649
|
+
throw new ExternalMemoryWriteConflict();
|
|
650
|
+
}
|
|
613
651
|
}
|
|
614
652
|
}
|
|
615
|
-
return result;
|
|
653
|
+
return await this.finalizeTargetMutation(target, storagePath, result);
|
|
616
654
|
} catch (error) {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
const state = await this.readFileState(filePath);
|
|
655
|
+
delete this.fileFingerprints[storagePath];
|
|
656
|
+
const state = await this.readFileState(storagePath);
|
|
620
657
|
this.setEntries(target, [...new Set(state.entries)]);
|
|
621
|
-
this.fileFingerprints[
|
|
658
|
+
this.fileFingerprints[storagePath] = state.fingerprint;
|
|
622
659
|
if (!(error instanceof ExternalMemoryWriteConflict)) throw error;
|
|
623
660
|
if (attempt >= MAX_EXTERNAL_WRITE_RETRIES) {
|
|
624
|
-
return {
|
|
661
|
+
return await this.finalizeTargetMutation(target, storagePath, {
|
|
625
662
|
success: false,
|
|
626
|
-
error: "Memory file changed repeatedly during this update. No external changes were overwritten.",
|
|
627
|
-
};
|
|
663
|
+
error: "Memory file changed repeatedly during this update. No external changes were overwritten. If you edited the file manually, re-run the memory tool or /memory-sync-markdown after the file is stable.",
|
|
664
|
+
});
|
|
628
665
|
}
|
|
629
666
|
}
|
|
630
667
|
}
|
|
@@ -718,7 +755,18 @@ export class MemoryStore {
|
|
|
718
755
|
}
|
|
719
756
|
|
|
720
757
|
try { await this.unlinkPublishedTempLink(tmpPath); } catch { /* ignore */ }
|
|
721
|
-
|
|
758
|
+
|
|
759
|
+
// Re-read after publish. An external truncate/cp can land between the
|
|
760
|
+
// link/rename and returning success; treat that as a write conflict so
|
|
761
|
+
// the caller retries against disk truth instead of reporting phantom state.
|
|
762
|
+
const publishedFingerprint = this.fingerprint(content);
|
|
763
|
+
this.fileFingerprints[filePath] = publishedFingerprint;
|
|
764
|
+
const publishedState = await this.readFileState(filePath);
|
|
765
|
+
if (publishedState.fingerprint !== publishedFingerprint) {
|
|
766
|
+
this.setEntries(target, [...new Set(publishedState.entries)]);
|
|
767
|
+
this.fileFingerprints[filePath] = publishedState.fingerprint;
|
|
768
|
+
throw new ExternalMemoryWriteConflict();
|
|
769
|
+
}
|
|
722
770
|
} catch (err) {
|
|
723
771
|
try { await fs.unlink(tmpPath); } catch { /* ignore */ }
|
|
724
772
|
throw err;
|
package/src/store/skill-store.ts
CHANGED
|
@@ -46,6 +46,111 @@ export interface LegacySkillMigrationResult {
|
|
|
46
46
|
warnings: string[];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const LIST_SECTIONS = new Set(["procedure", "pitfalls", "verification"]);
|
|
50
|
+
|
|
51
|
+
function normalizeSectionName(section: string): string {
|
|
52
|
+
return section.replace(/^#+\s*/, "").trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isExactSectionHeader(line: string, section: string): boolean {
|
|
56
|
+
const heading = line.trim().match(/^##\s+(.+?)\s*$/);
|
|
57
|
+
if (!heading) return false;
|
|
58
|
+
return heading[1].trim().toLowerCase() === section.trim().toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function looksLikeJsonArray(content: string): boolean {
|
|
62
|
+
const trimmed = content.trim();
|
|
63
|
+
return trimmed.startsWith("[") && trimmed.endsWith("]");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function looksLikeJsonObject(content: string): boolean {
|
|
67
|
+
const trimmed = content.trim();
|
|
68
|
+
return trimmed.startsWith("{") && trimmed.endsWith("}");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatPatchList(section: string, items: string[]): string {
|
|
72
|
+
const cleaned = items.map((item) => item.trim()).filter(Boolean);
|
|
73
|
+
if (cleaned.length === 0) return "";
|
|
74
|
+
const key = section.trim().toLowerCase();
|
|
75
|
+
if (key === "pitfalls") {
|
|
76
|
+
return cleaned.map((item) => `- ${item.replace(/^[-*]\s+/, "")}`).join("\n");
|
|
77
|
+
}
|
|
78
|
+
// Procedure / Verification default to ordered steps.
|
|
79
|
+
return cleaned
|
|
80
|
+
.map((item, index) => `${index + 1}. ${item.replace(/^\d+\.\s+/, "").replace(/^[-*]\s+/, "")}`)
|
|
81
|
+
.join("\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Normalize/validate free-form patch content so LLM format drift cannot wipe
|
|
86
|
+
* or splice skill sections. Coerces JSON string arrays into Markdown lists for
|
|
87
|
+
* list-shaped sections and rejects empty / object / header-injecting payloads.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeSkillPatchContent(
|
|
90
|
+
section: string,
|
|
91
|
+
rawContent: string,
|
|
92
|
+
): { content: string } | { error: string } {
|
|
93
|
+
const sectionName = normalizeSectionName(section);
|
|
94
|
+
if (!sectionName) {
|
|
95
|
+
return { error: "section is required for patch." };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let content = typeof rawContent === "string" ? rawContent.trim() : "";
|
|
99
|
+
if (!content) {
|
|
100
|
+
return {
|
|
101
|
+
error: "New content is required for patch. Prefer structured fields (procedure_steps, pitfalls, verification_steps, when_to_use) over free-form content.",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (looksLikeJsonObject(content)) {
|
|
106
|
+
return {
|
|
107
|
+
error: "Patch content looks like a JSON object. Provide Markdown section body or a string array via structured fields.",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (looksLikeJsonArray(content)) {
|
|
112
|
+
try {
|
|
113
|
+
const parsed: unknown = JSON.parse(content);
|
|
114
|
+
if (!Array.isArray(parsed)) {
|
|
115
|
+
return { error: "Patch content looks like JSON but is not a string array." };
|
|
116
|
+
}
|
|
117
|
+
const items = parsed
|
|
118
|
+
.filter((item): item is string => typeof item === "string")
|
|
119
|
+
.map((item) => item.trim())
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
if (items.length === 0) {
|
|
122
|
+
return { error: "Patch content JSON array must contain non-empty strings." };
|
|
123
|
+
}
|
|
124
|
+
const key = sectionName.toLowerCase();
|
|
125
|
+
if (key === "when to use") {
|
|
126
|
+
content = items.join("\n\n");
|
|
127
|
+
} else if (LIST_SECTIONS.has(key)) {
|
|
128
|
+
content = formatPatchList(sectionName, items);
|
|
129
|
+
} else {
|
|
130
|
+
content = items.map((item) => `- ${item}`).join("\n");
|
|
131
|
+
}
|
|
132
|
+
} catch {
|
|
133
|
+
return {
|
|
134
|
+
error: "Patch content looks like a JSON array but could not be parsed. Use Markdown or structured string[] fields.",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Reject payloads that would inject extra ## sections mid-body.
|
|
140
|
+
if (/^#{1,6}\s+\S/m.test(content)) {
|
|
141
|
+
return {
|
|
142
|
+
error: "Patch content must not include Markdown section headers (## ...). Patch only the body of the target section.",
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!content.trim()) {
|
|
147
|
+
return { error: "New content is required for patch." };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return { content: content.trim() };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
49
154
|
export class SkillStore {
|
|
50
155
|
private globalSkillsDir: string;
|
|
51
156
|
private projectSkillsDir: string | null;
|
|
@@ -352,34 +457,47 @@ export class SkillStore {
|
|
|
352
457
|
}
|
|
353
458
|
|
|
354
459
|
async patch(skillId: string, section: string, newContent: string): Promise<SkillResult> {
|
|
355
|
-
|
|
356
|
-
if (!
|
|
460
|
+
const sectionName = normalizeSectionName(section);
|
|
461
|
+
if (!sectionName) return { success: false, error: "section is required for patch." };
|
|
462
|
+
|
|
463
|
+
const normalized = normalizeSkillPatchContent(sectionName, newContent);
|
|
464
|
+
if ("error" in normalized) return { success: false, error: normalized.error };
|
|
465
|
+
const content = normalized.content;
|
|
357
466
|
|
|
358
|
-
const scanError = scanContent(
|
|
467
|
+
const scanError = scanContent(content);
|
|
359
468
|
if (scanError) return { success: false, error: scanError };
|
|
360
469
|
|
|
361
470
|
const doc = await this.loadSkill(skillId);
|
|
362
471
|
if (!doc) return { success: false, error: `Skill '${skillId}' not found.` };
|
|
363
472
|
|
|
364
|
-
const sectionHeader = `## ${
|
|
473
|
+
const sectionHeader = `## ${sectionName}`;
|
|
365
474
|
const lines = doc.body.split("\n");
|
|
366
475
|
let found = false;
|
|
367
476
|
const result: string[] = [];
|
|
368
477
|
|
|
369
478
|
for (let i = 0; i < lines.length; i++) {
|
|
370
|
-
if (lines[i]
|
|
479
|
+
if (isExactSectionHeader(lines[i], sectionName)) {
|
|
371
480
|
result.push(sectionHeader);
|
|
372
|
-
|
|
481
|
+
// Preserve multi-line body as discrete lines so later headers stay exact.
|
|
482
|
+
for (const bodyLine of content.split("\n")) {
|
|
483
|
+
result.push(bodyLine);
|
|
484
|
+
}
|
|
373
485
|
found = true;
|
|
374
486
|
i++;
|
|
375
|
-
while (i < lines.length && !lines[i].startsWith("## ")) i++;
|
|
487
|
+
while (i < lines.length && !lines[i].trim().startsWith("## ")) i++;
|
|
376
488
|
if (i < lines.length) result.push(lines[i]);
|
|
377
489
|
} else {
|
|
378
490
|
result.push(lines[i]);
|
|
379
491
|
}
|
|
380
492
|
}
|
|
381
493
|
|
|
382
|
-
if (!found)
|
|
494
|
+
if (!found) {
|
|
495
|
+
if (result.length > 0 && result[result.length - 1] !== "") result.push("");
|
|
496
|
+
result.push(sectionHeader);
|
|
497
|
+
for (const bodyLine of content.split("\n")) {
|
|
498
|
+
result.push(bodyLine);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
383
501
|
|
|
384
502
|
await this.atomicWrite(doc.path, formatFrontmatter({
|
|
385
503
|
name: doc.name,
|
|
@@ -393,7 +511,7 @@ export class SkillStore {
|
|
|
393
511
|
|
|
394
512
|
return {
|
|
395
513
|
success: true,
|
|
396
|
-
message: `Skill '${doc.displayName || doc.name}' section '${
|
|
514
|
+
message: `Skill '${doc.displayName || doc.name}' section '${sectionName}' updated.`,
|
|
397
515
|
fileName: doc.fileName,
|
|
398
516
|
skillId: doc.skillId,
|
|
399
517
|
scope: doc.scope,
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared better-sqlite3 loader with ABI mismatch recovery.
|
|
3
|
+
*
|
|
4
|
+
* Pi installs extension deps under ~/.pi/agent/npm. When the host Node that
|
|
5
|
+
* runs Pi (e.g. Homebrew) differs from the Node that compiled better-sqlite3,
|
|
6
|
+
* require() throws NODE_MODULE_VERSION errors. Detect that, attempt one
|
|
7
|
+
* npm rebuild against the current runtime, and surface a clear recovery path.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
|
|
15
|
+
export type BetterSqlite3DatabaseCtor = new (
|
|
16
|
+
dbPath: string,
|
|
17
|
+
options?: { readonly?: boolean; fileMustExist?: boolean; timeout?: number },
|
|
18
|
+
) => unknown;
|
|
19
|
+
|
|
20
|
+
export interface SqliteNativeLoadOptions {
|
|
21
|
+
/** Override createRequire base URL (tests). */
|
|
22
|
+
requireFrom?: string | URL;
|
|
23
|
+
/** Inject require() for tests. */
|
|
24
|
+
requireImpl?: NodeRequire;
|
|
25
|
+
/** Inject rebuild runner for tests. */
|
|
26
|
+
rebuild?: (packageRoot: string) => { ok: boolean; detail: string };
|
|
27
|
+
/** Force treating the first load failure as rebuild-eligible (tests). */
|
|
28
|
+
allowRebuild?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class BetterSqlite3LoadError extends Error {
|
|
32
|
+
readonly code = "BETTER_SQLITE3_LOAD_FAILED";
|
|
33
|
+
readonly packageRoot: string | null;
|
|
34
|
+
readonly causeError: unknown;
|
|
35
|
+
|
|
36
|
+
constructor(message: string, options: { packageRoot?: string | null; cause?: unknown } = {}) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.name = "BetterSqlite3LoadError";
|
|
39
|
+
this.packageRoot = options.packageRoot ?? null;
|
|
40
|
+
this.causeError = options.cause;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ABI_MISMATCH_RE =
|
|
45
|
+
/NODE_MODULE_VERSION|was compiled against a different Node\.js version|ERR_DLOPEN_FAILED/i;
|
|
46
|
+
|
|
47
|
+
export function isNativeModuleAbiMismatch(error: unknown): boolean {
|
|
48
|
+
if (!error) return false;
|
|
49
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
50
|
+
if (ABI_MISMATCH_RE.test(message)) return true;
|
|
51
|
+
if (typeof error === "object" && error !== null) {
|
|
52
|
+
const code = "code" in error ? String((error as { code?: unknown }).code ?? "") : "";
|
|
53
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function resolveBetterSqlite3PackageRoot(requireImpl: NodeRequire): string | null {
|
|
59
|
+
try {
|
|
60
|
+
const entry = requireImpl.resolve("better-sqlite3");
|
|
61
|
+
let dir = path.dirname(entry);
|
|
62
|
+
while (true) {
|
|
63
|
+
const pkgPath = path.join(dir, "package.json");
|
|
64
|
+
if (fs.existsSync(pkgPath)) {
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
67
|
+
const pkg = JSON.parse(raw) as { name?: unknown };
|
|
68
|
+
if (pkg && typeof pkg === "object" && pkg.name === "better-sqlite3") return dir;
|
|
69
|
+
} catch {
|
|
70
|
+
// keep walking
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const parent = path.dirname(dir);
|
|
74
|
+
if (parent === dir) break;
|
|
75
|
+
dir = parent;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Classic layout fallback: .../node_modules/better-sqlite3/<...>
|
|
79
|
+
const parts = entry.split(path.sep);
|
|
80
|
+
const idx = parts.lastIndexOf("better-sqlite3");
|
|
81
|
+
if (idx >= 0) {
|
|
82
|
+
return parts.slice(0, idx + 1).join(path.sep) || null;
|
|
83
|
+
}
|
|
84
|
+
return path.dirname(entry);
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function defaultRebuild(packageRoot: string): { ok: boolean; detail: string } {
|
|
90
|
+
const npmExecPath = typeof process.env.npm_execpath === "string" && process.env.npm_execpath
|
|
91
|
+
? process.env.npm_execpath
|
|
92
|
+
: null;
|
|
93
|
+
|
|
94
|
+
const attempts: Array<{ command: string; args: string[]; shell?: boolean }> = [];
|
|
95
|
+
if (npmExecPath) {
|
|
96
|
+
attempts.push({ command: process.execPath, args: [npmExecPath, "rebuild", "better-sqlite3"] });
|
|
97
|
+
}
|
|
98
|
+
attempts.push({ command: "npm", args: ["rebuild", "better-sqlite3"] });
|
|
99
|
+
|
|
100
|
+
const details: string[] = [];
|
|
101
|
+
for (const attempt of attempts) {
|
|
102
|
+
const result = spawnSync(attempt.command, attempt.args, {
|
|
103
|
+
cwd: packageRoot,
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
env: process.env,
|
|
106
|
+
timeout: 120_000,
|
|
107
|
+
shell: attempt.shell ?? false,
|
|
108
|
+
});
|
|
109
|
+
if (result.error) {
|
|
110
|
+
details.push(`${attempt.command}: ${result.error.message}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (result.status === 0) {
|
|
114
|
+
return { ok: true, detail: (result.stdout || result.stderr || "rebuild ok").trim() };
|
|
115
|
+
}
|
|
116
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
117
|
+
details.push(`${attempt.command} exited ${result.status}${output ? `: ${output}` : ""}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { ok: false, detail: details.join(" | ") || "rebuild failed" };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function unwrapModule(mod: { default?: BetterSqlite3DatabaseCtor } | BetterSqlite3DatabaseCtor): BetterSqlite3DatabaseCtor {
|
|
124
|
+
return (mod as { default?: BetterSqlite3DatabaseCtor }).default ?? (mod as BetterSqlite3DatabaseCtor);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function clearBetterSqlite3RequireCache(requireImpl: NodeRequire, packageRoot: string | null): void {
|
|
128
|
+
for (const key of Object.keys(requireImpl.cache)) {
|
|
129
|
+
if (!key.includes(`${path.sep}better-sqlite3${path.sep}`) && !key.endsWith(`${path.sep}better-sqlite3`)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (packageRoot && !key.startsWith(packageRoot)) continue;
|
|
133
|
+
delete requireImpl.cache[key];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatBetterSqlite3AbiError(options: {
|
|
138
|
+
originalError: unknown;
|
|
139
|
+
packageRoot: string | null;
|
|
140
|
+
rebuildAttempted: boolean;
|
|
141
|
+
rebuildDetail?: string;
|
|
142
|
+
}): string {
|
|
143
|
+
const original = options.originalError instanceof Error
|
|
144
|
+
? options.originalError.message
|
|
145
|
+
: String(options.originalError);
|
|
146
|
+
const runtime = `Node ${process.version} (NODE_MODULE_VERSION ${process.versions.modules}) via ${process.execPath}`;
|
|
147
|
+
const location = options.packageRoot ?? "(better-sqlite3 package root unknown)";
|
|
148
|
+
const rebuildLine = options.rebuildAttempted
|
|
149
|
+
? (options.rebuildDetail
|
|
150
|
+
? `Automatic rebuild was attempted and failed: ${options.rebuildDetail}`
|
|
151
|
+
: "Automatic rebuild was attempted and failed.")
|
|
152
|
+
: "Automatic rebuild was not attempted.";
|
|
153
|
+
|
|
154
|
+
return [
|
|
155
|
+
"pi-hermes-memory could not load the native better-sqlite3 module for this Node runtime.",
|
|
156
|
+
`Runtime: ${runtime}`,
|
|
157
|
+
`Module: ${location}`,
|
|
158
|
+
`Original error: ${original}`,
|
|
159
|
+
rebuildLine,
|
|
160
|
+
"Fix: rebuild the extension install against the same Node that runs Pi, then restart Pi:",
|
|
161
|
+
options.packageRoot
|
|
162
|
+
? ` cd "${options.packageRoot}" && npm rebuild better-sqlite3`
|
|
163
|
+
: " cd ~/.pi/agent/npm && npm rebuild better-sqlite3",
|
|
164
|
+
"If you installed Pi with Homebrew, either rebuild as above after brew Node upgrades, or install Pi with npm so the extension and host share one Node toolchain.",
|
|
165
|
+
].join("\n");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Load better-sqlite3, attempting one rebuild on ABI/dlopen mismatch.
|
|
170
|
+
*/
|
|
171
|
+
export function loadBetterSqlite3(options: SqliteNativeLoadOptions = {}): BetterSqlite3DatabaseCtor {
|
|
172
|
+
const requireImpl = options.requireImpl
|
|
173
|
+
?? createRequire(options.requireFrom ?? import.meta.url);
|
|
174
|
+
|
|
175
|
+
const loadOnce = (): BetterSqlite3DatabaseCtor => {
|
|
176
|
+
const mod = requireImpl("better-sqlite3") as { default?: BetterSqlite3DatabaseCtor } | BetterSqlite3DatabaseCtor;
|
|
177
|
+
return unwrapModule(mod);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
return loadOnce();
|
|
182
|
+
} catch (firstError) {
|
|
183
|
+
const packageRoot = resolveBetterSqlite3PackageRoot(requireImpl);
|
|
184
|
+
const canRebuild = options.allowRebuild ?? isNativeModuleAbiMismatch(firstError);
|
|
185
|
+
if (!canRebuild || !packageRoot) {
|
|
186
|
+
if (isNativeModuleAbiMismatch(firstError)) {
|
|
187
|
+
throw new BetterSqlite3LoadError(
|
|
188
|
+
formatBetterSqlite3AbiError({
|
|
189
|
+
originalError: firstError,
|
|
190
|
+
packageRoot,
|
|
191
|
+
rebuildAttempted: false,
|
|
192
|
+
}),
|
|
193
|
+
{ packageRoot, cause: firstError },
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
throw firstError;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rebuild = options.rebuild ?? defaultRebuild;
|
|
200
|
+
const rebuildResult = rebuild(packageRoot);
|
|
201
|
+
clearBetterSqlite3RequireCache(requireImpl, packageRoot);
|
|
202
|
+
|
|
203
|
+
if (rebuildResult.ok) {
|
|
204
|
+
try {
|
|
205
|
+
return loadOnce();
|
|
206
|
+
} catch (secondError) {
|
|
207
|
+
throw new BetterSqlite3LoadError(
|
|
208
|
+
formatBetterSqlite3AbiError({
|
|
209
|
+
originalError: secondError,
|
|
210
|
+
packageRoot,
|
|
211
|
+
rebuildAttempted: true,
|
|
212
|
+
rebuildDetail: rebuildResult.detail,
|
|
213
|
+
}),
|
|
214
|
+
{ packageRoot, cause: secondError },
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
throw new BetterSqlite3LoadError(
|
|
220
|
+
formatBetterSqlite3AbiError({
|
|
221
|
+
originalError: firstError,
|
|
222
|
+
packageRoot,
|
|
223
|
+
rebuildAttempted: true,
|
|
224
|
+
rebuildDetail: rebuildResult.detail,
|
|
225
|
+
}),
|
|
226
|
+
{ packageRoot, cause: firstError },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
package/src/tools/skill-tool.ts
CHANGED
|
@@ -66,22 +66,22 @@ const SKILL_TOOL_PARAMETERS = Type.Object({
|
|
|
66
66
|
description: "Required for create. Use 'global' for portable procedures and 'project' for repo-specific workflows.",
|
|
67
67
|
})),
|
|
68
68
|
section: Type.Optional(Type.String({
|
|
69
|
-
description: "Required for patch. Section header to patch. e.g., 'Procedure', 'Pitfalls'.",
|
|
69
|
+
description: "Required for patch. Section header to patch. e.g., 'Procedure', 'Pitfalls', 'Verification', 'When to Use'.",
|
|
70
70
|
})),
|
|
71
71
|
content: Type.Optional(Type.String({
|
|
72
|
-
description: "Raw markdown body for create/update/edit, or
|
|
72
|
+
description: "Raw markdown body for create/update/edit, or Markdown section body for patch. Prefer structured fields over free-form content when possible. For patch, JSON arrays are auto-coerced for list sections; JSON objects are rejected.",
|
|
73
73
|
})),
|
|
74
74
|
when_to_use: Type.Optional(Type.String({
|
|
75
|
-
description: "Structured create/update/edit field
|
|
75
|
+
description: "Structured create/update/edit field, or structured patch body when section is 'When to Use'.",
|
|
76
76
|
})),
|
|
77
77
|
procedure_steps: Type.Optional(Type.Array(Type.String(), {
|
|
78
|
-
description: "Structured create/update/edit field. Ordered concrete steps
|
|
78
|
+
description: "Structured create/update/edit field, or structured patch body when section is 'Procedure'. Ordered concrete steps.",
|
|
79
79
|
})),
|
|
80
80
|
pitfalls: Type.Optional(Type.Array(Type.String(), {
|
|
81
|
-
description: "Structured create/update/edit field
|
|
81
|
+
description: "Structured create/update/edit field, or structured patch body when section is 'Pitfalls'.",
|
|
82
82
|
})),
|
|
83
83
|
verification_steps: Type.Optional(Type.Array(Type.String(), {
|
|
84
|
-
description: "Structured create/update/edit field
|
|
84
|
+
description: "Structured create/update/edit field, or structured patch body when section is 'Verification'.",
|
|
85
85
|
})),
|
|
86
86
|
}, { additionalProperties: false });
|
|
87
87
|
|
|
@@ -97,7 +97,9 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
97
97
|
"Use the skill_manage tool after completing complex tasks that required trial and error or multiple tool calls.",
|
|
98
98
|
"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.",
|
|
99
99
|
"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.",
|
|
100
|
-
"Prefer structured fields for create/update: when_to_use, procedure_steps, pitfalls, and verification_steps. The tool
|
|
100
|
+
"Prefer structured fields for create/update/patch: when_to_use, procedure_steps, pitfalls, and verification_steps. The tool renders valid SKILL.md sections for you.",
|
|
101
|
+
"For patch, pass section plus the matching structured field (e.g. section='Procedure' with procedure_steps). Avoid free-form content that is a JSON array/object string.",
|
|
102
|
+
"Prefer 'update' for multi-section rewrites when patch content would be large or format-unstable.",
|
|
101
103
|
"Use 'view' before patching or updating when you need to inspect an existing skill.",
|
|
102
104
|
"Do NOT use skills for temporary task state — only for durable, reusable procedures.",
|
|
103
105
|
],
|
|
@@ -206,7 +208,7 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
206
208
|
result = { success: true, ...doc };
|
|
207
209
|
break;
|
|
208
210
|
|
|
209
|
-
case "patch":
|
|
211
|
+
case "patch": {
|
|
210
212
|
if (!skill_id) {
|
|
211
213
|
return {
|
|
212
214
|
content: [{ type: "text", text: JSON.stringify({ success: false, error: "skill_id is required for 'patch' action." }) }],
|
|
@@ -219,14 +221,60 @@ export function registerSkillTool(pi: ExtensionAPI, store: SkillStore): void {
|
|
|
219
221
|
details: {},
|
|
220
222
|
};
|
|
221
223
|
}
|
|
222
|
-
|
|
224
|
+
|
|
225
|
+
const sectionKey = section.replace(/^#+\s*/, "").trim().toLowerCase();
|
|
226
|
+
let patchContent = content?.trim() ?? "";
|
|
227
|
+
|
|
228
|
+
// Prefer structured fields matching the target section so the LLM
|
|
229
|
+
// does not have to invent Markdown list formatting.
|
|
230
|
+
if (sectionKey === "procedure" && procedureSteps.length > 0) {
|
|
231
|
+
patchContent = formatOrderedList(procedureSteps);
|
|
232
|
+
} else if (sectionKey === "pitfalls" && pitfallItems.length > 0) {
|
|
233
|
+
patchContent = formatBulletList(pitfallItems, "No notable pitfalls recorded yet.");
|
|
234
|
+
} else if (sectionKey === "verification" && verificationSteps.length > 0) {
|
|
235
|
+
patchContent = formatOrderedList(verificationSteps);
|
|
236
|
+
} else if ((sectionKey === "when to use" || sectionKey === "when_to_use") && whenToUse) {
|
|
237
|
+
patchContent = whenToUse;
|
|
238
|
+
} else if (!patchContent && hasStructuredBody) {
|
|
239
|
+
// Allow a single structured field even if section name is non-standard.
|
|
240
|
+
if (procedureSteps.length > 0 && pitfallItems.length === 0 && verificationSteps.length === 0 && !whenToUse) {
|
|
241
|
+
patchContent = formatOrderedList(procedureSteps);
|
|
242
|
+
} else if (pitfallItems.length > 0 && procedureSteps.length === 0 && verificationSteps.length === 0 && !whenToUse) {
|
|
243
|
+
patchContent = formatBulletList(pitfallItems, "No notable pitfalls recorded yet.");
|
|
244
|
+
} else if (verificationSteps.length > 0 && procedureSteps.length === 0 && pitfallItems.length === 0 && !whenToUse) {
|
|
245
|
+
patchContent = formatOrderedList(verificationSteps);
|
|
246
|
+
} else if (whenToUse && procedureSteps.length === 0 && pitfallItems.length === 0 && verificationSteps.length === 0) {
|
|
247
|
+
patchContent = whenToUse;
|
|
248
|
+
} else {
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: JSON.stringify({
|
|
253
|
+
success: false,
|
|
254
|
+
error: "For patch, provide content or exactly one structured field matching the target section (procedure_steps, pitfalls, verification_steps, or when_to_use). Use update for multi-section rewrites.",
|
|
255
|
+
}),
|
|
256
|
+
}],
|
|
257
|
+
details: {},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!patchContent) {
|
|
223
263
|
return {
|
|
224
|
-
content: [{
|
|
264
|
+
content: [{
|
|
265
|
+
type: "text",
|
|
266
|
+
text: JSON.stringify({
|
|
267
|
+
success: false,
|
|
268
|
+
error: "content or a matching structured field is required for 'patch' action. Prefer procedure_steps/pitfalls/verification_steps/when_to_use.",
|
|
269
|
+
}),
|
|
270
|
+
}],
|
|
225
271
|
details: {},
|
|
226
272
|
};
|
|
227
273
|
}
|
|
228
|
-
|
|
274
|
+
|
|
275
|
+
result = await store.patch(skill_id, section, patchContent);
|
|
229
276
|
break;
|
|
277
|
+
}
|
|
230
278
|
|
|
231
279
|
case "update":
|
|
232
280
|
case "edit": {
|