pi-hermes-memory 0.8.1 → 0.9.0
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 +10 -9
- package/src/extension-root-migration.ts +118 -4
- package/src/handlers/auto-consolidate.ts +1 -1
- package/src/index.ts +7 -6
- package/src/project.ts +104 -5
- package/src/store/atomic-lock-coordinator.ts +63 -29
- package/src/store/db.ts +23 -18
- package/src/store/fts-query.ts +31 -0
- package/src/store/markdown-mutation-lock.ts +1 -1
- package/src/store/memory-store.ts +71 -23
- package/src/store/session-search.ts +33 -2
- package/src/store/skill-store.ts +177 -35
- package/src/store/sqlite-memory-store.ts +30 -1
- package/src/store/sqlite-native.ts +241 -0
- package/src/tools/skill-tool.ts +59 -11
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { DatabaseManager } from './db.js';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
buildFallbackFts5Query,
|
|
4
|
+
buildNaturalLanguageFallbackQuery,
|
|
5
|
+
isFts5QueryError,
|
|
6
|
+
normalizeFts5Query,
|
|
7
|
+
normalizeNaturalLanguageFts5Query,
|
|
8
|
+
} from './fts-query.js';
|
|
3
9
|
import { normalizeMemoryLookupText } from './memory-lookup.js';
|
|
4
10
|
import type { MemoryCategory } from '../types.js';
|
|
5
11
|
|
|
@@ -697,6 +703,8 @@ export function searchMemories(
|
|
|
697
703
|
return [];
|
|
698
704
|
}
|
|
699
705
|
|
|
706
|
+
let ftsParseError = false;
|
|
707
|
+
|
|
700
708
|
const runSearch = (matchQuery: string): SqliteMemoryEntry[] => {
|
|
701
709
|
const conditions: string[] = [];
|
|
702
710
|
const params: unknown[] = [];
|
|
@@ -750,6 +758,7 @@ export function searchMemories(
|
|
|
750
758
|
return rows.map(mapRow);
|
|
751
759
|
} catch (err) {
|
|
752
760
|
if (isFts5QueryError(err)) {
|
|
761
|
+
ftsParseError = true;
|
|
753
762
|
return [];
|
|
754
763
|
}
|
|
755
764
|
throw err;
|
|
@@ -761,6 +770,26 @@ export function searchMemories(
|
|
|
761
770
|
return exactResults;
|
|
762
771
|
}
|
|
763
772
|
|
|
773
|
+
// A query with uppercase operator words (e.g. "DO NOT USE FIND /") passes
|
|
774
|
+
// through as raw FTS5 syntax; when that fails to parse, retry it as natural
|
|
775
|
+
// language instead of silently returning nothing. Valid operator queries
|
|
776
|
+
// that legitimately match nothing keep their exact semantics.
|
|
777
|
+
if (ftsParseError) {
|
|
778
|
+
const nlQuery = normalizeNaturalLanguageFts5Query(query);
|
|
779
|
+
if (nlQuery.length === 0 || nlQuery === normalizedQuery) {
|
|
780
|
+
return [];
|
|
781
|
+
}
|
|
782
|
+
const nlResults = runSearch(nlQuery);
|
|
783
|
+
if (nlResults.length > 0) {
|
|
784
|
+
return nlResults;
|
|
785
|
+
}
|
|
786
|
+
const nlFallback = buildNaturalLanguageFallbackQuery(query);
|
|
787
|
+
if (nlFallback && nlFallback !== nlQuery) {
|
|
788
|
+
return runSearch(nlFallback);
|
|
789
|
+
}
|
|
790
|
+
return nlResults;
|
|
791
|
+
}
|
|
792
|
+
|
|
764
793
|
const fallbackQuery = buildFallbackFts5Query(query);
|
|
765
794
|
if (!fallbackQuery || fallbackQuery === normalizedQuery) {
|
|
766
795
|
return exactResults;
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
+
/**
|
|
48
|
+
* True when running under Bun (including the compiled Pi binary).
|
|
49
|
+
*
|
|
50
|
+
* Compiled Pi cannot resolve `better-sqlite3` — neither the bare specifier nor
|
|
51
|
+
* the package's own transitive `require("bindings")` resolve from an on-disk
|
|
52
|
+
* extension file, so every code path that needs SQLite must route through
|
|
53
|
+
* `bun:sqlite` instead of loading the native module.
|
|
54
|
+
*/
|
|
55
|
+
export function isBunRuntime(): boolean {
|
|
56
|
+
return "Bun" in globalThis;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isNativeModuleAbiMismatch(error: unknown): boolean {
|
|
60
|
+
if (!error) return false;
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
if (ABI_MISMATCH_RE.test(message)) return true;
|
|
63
|
+
if (typeof error === "object" && error !== null) {
|
|
64
|
+
const code = "code" in error ? String((error as { code?: unknown }).code ?? "") : "";
|
|
65
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveBetterSqlite3PackageRoot(requireImpl: NodeRequire): string | null {
|
|
71
|
+
try {
|
|
72
|
+
const entry = requireImpl.resolve("better-sqlite3");
|
|
73
|
+
let dir = path.dirname(entry);
|
|
74
|
+
while (true) {
|
|
75
|
+
const pkgPath = path.join(dir, "package.json");
|
|
76
|
+
if (fs.existsSync(pkgPath)) {
|
|
77
|
+
try {
|
|
78
|
+
const raw = fs.readFileSync(pkgPath, "utf-8");
|
|
79
|
+
const pkg = JSON.parse(raw) as { name?: unknown };
|
|
80
|
+
if (pkg && typeof pkg === "object" && pkg.name === "better-sqlite3") return dir;
|
|
81
|
+
} catch {
|
|
82
|
+
// keep walking
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const parent = path.dirname(dir);
|
|
86
|
+
if (parent === dir) break;
|
|
87
|
+
dir = parent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Classic layout fallback: .../node_modules/better-sqlite3/<...>
|
|
91
|
+
const parts = entry.split(path.sep);
|
|
92
|
+
const idx = parts.lastIndexOf("better-sqlite3");
|
|
93
|
+
if (idx >= 0) {
|
|
94
|
+
return parts.slice(0, idx + 1).join(path.sep) || null;
|
|
95
|
+
}
|
|
96
|
+
return path.dirname(entry);
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function defaultRebuild(packageRoot: string): { ok: boolean; detail: string } {
|
|
102
|
+
const npmExecPath = typeof process.env.npm_execpath === "string" && process.env.npm_execpath
|
|
103
|
+
? process.env.npm_execpath
|
|
104
|
+
: null;
|
|
105
|
+
|
|
106
|
+
const attempts: Array<{ command: string; args: string[]; shell?: boolean }> = [];
|
|
107
|
+
if (npmExecPath) {
|
|
108
|
+
attempts.push({ command: process.execPath, args: [npmExecPath, "rebuild", "better-sqlite3"] });
|
|
109
|
+
}
|
|
110
|
+
attempts.push({ command: "npm", args: ["rebuild", "better-sqlite3"] });
|
|
111
|
+
|
|
112
|
+
const details: string[] = [];
|
|
113
|
+
for (const attempt of attempts) {
|
|
114
|
+
const result = spawnSync(attempt.command, attempt.args, {
|
|
115
|
+
cwd: packageRoot,
|
|
116
|
+
encoding: "utf-8",
|
|
117
|
+
env: process.env,
|
|
118
|
+
timeout: 120_000,
|
|
119
|
+
shell: attempt.shell ?? false,
|
|
120
|
+
});
|
|
121
|
+
if (result.error) {
|
|
122
|
+
details.push(`${attempt.command}: ${result.error.message}`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (result.status === 0) {
|
|
126
|
+
return { ok: true, detail: (result.stdout || result.stderr || "rebuild ok").trim() };
|
|
127
|
+
}
|
|
128
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
129
|
+
details.push(`${attempt.command} exited ${result.status}${output ? `: ${output}` : ""}`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { ok: false, detail: details.join(" | ") || "rebuild failed" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function unwrapModule(mod: { default?: BetterSqlite3DatabaseCtor } | BetterSqlite3DatabaseCtor): BetterSqlite3DatabaseCtor {
|
|
136
|
+
return (mod as { default?: BetterSqlite3DatabaseCtor }).default ?? (mod as BetterSqlite3DatabaseCtor);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function clearBetterSqlite3RequireCache(requireImpl: NodeRequire, packageRoot: string | null): void {
|
|
140
|
+
for (const key of Object.keys(requireImpl.cache)) {
|
|
141
|
+
if (!key.includes(`${path.sep}better-sqlite3${path.sep}`) && !key.endsWith(`${path.sep}better-sqlite3`)) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (packageRoot && !key.startsWith(packageRoot)) continue;
|
|
145
|
+
delete requireImpl.cache[key];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function formatBetterSqlite3AbiError(options: {
|
|
150
|
+
originalError: unknown;
|
|
151
|
+
packageRoot: string | null;
|
|
152
|
+
rebuildAttempted: boolean;
|
|
153
|
+
rebuildDetail?: string;
|
|
154
|
+
}): string {
|
|
155
|
+
const original = options.originalError instanceof Error
|
|
156
|
+
? options.originalError.message
|
|
157
|
+
: String(options.originalError);
|
|
158
|
+
const runtime = `Node ${process.version} (NODE_MODULE_VERSION ${process.versions.modules}) via ${process.execPath}`;
|
|
159
|
+
const location = options.packageRoot ?? "(better-sqlite3 package root unknown)";
|
|
160
|
+
const rebuildLine = options.rebuildAttempted
|
|
161
|
+
? (options.rebuildDetail
|
|
162
|
+
? `Automatic rebuild was attempted and failed: ${options.rebuildDetail}`
|
|
163
|
+
: "Automatic rebuild was attempted and failed.")
|
|
164
|
+
: "Automatic rebuild was not attempted.";
|
|
165
|
+
|
|
166
|
+
return [
|
|
167
|
+
"pi-hermes-memory could not load the native better-sqlite3 module for this Node runtime.",
|
|
168
|
+
`Runtime: ${runtime}`,
|
|
169
|
+
`Module: ${location}`,
|
|
170
|
+
`Original error: ${original}`,
|
|
171
|
+
rebuildLine,
|
|
172
|
+
"Fix: rebuild the extension install against the same Node that runs Pi, then restart Pi:",
|
|
173
|
+
options.packageRoot
|
|
174
|
+
? ` cd "${options.packageRoot}" && npm rebuild better-sqlite3`
|
|
175
|
+
: " cd ~/.pi/agent/npm && npm rebuild better-sqlite3",
|
|
176
|
+
"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.",
|
|
177
|
+
].join("\n");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Load better-sqlite3, attempting one rebuild on ABI/dlopen mismatch.
|
|
182
|
+
*/
|
|
183
|
+
export function loadBetterSqlite3(options: SqliteNativeLoadOptions = {}): BetterSqlite3DatabaseCtor {
|
|
184
|
+
const requireImpl = options.requireImpl
|
|
185
|
+
?? createRequire(options.requireFrom ?? import.meta.url);
|
|
186
|
+
|
|
187
|
+
const loadOnce = (): BetterSqlite3DatabaseCtor => {
|
|
188
|
+
const mod = requireImpl("better-sqlite3") as { default?: BetterSqlite3DatabaseCtor } | BetterSqlite3DatabaseCtor;
|
|
189
|
+
return unwrapModule(mod);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
return loadOnce();
|
|
194
|
+
} catch (firstError) {
|
|
195
|
+
const packageRoot = resolveBetterSqlite3PackageRoot(requireImpl);
|
|
196
|
+
const canRebuild = options.allowRebuild ?? isNativeModuleAbiMismatch(firstError);
|
|
197
|
+
if (!canRebuild || !packageRoot) {
|
|
198
|
+
if (isNativeModuleAbiMismatch(firstError)) {
|
|
199
|
+
throw new BetterSqlite3LoadError(
|
|
200
|
+
formatBetterSqlite3AbiError({
|
|
201
|
+
originalError: firstError,
|
|
202
|
+
packageRoot,
|
|
203
|
+
rebuildAttempted: false,
|
|
204
|
+
}),
|
|
205
|
+
{ packageRoot, cause: firstError },
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
throw firstError;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rebuild = options.rebuild ?? defaultRebuild;
|
|
212
|
+
const rebuildResult = rebuild(packageRoot);
|
|
213
|
+
clearBetterSqlite3RequireCache(requireImpl, packageRoot);
|
|
214
|
+
|
|
215
|
+
if (rebuildResult.ok) {
|
|
216
|
+
try {
|
|
217
|
+
return loadOnce();
|
|
218
|
+
} catch (secondError) {
|
|
219
|
+
throw new BetterSqlite3LoadError(
|
|
220
|
+
formatBetterSqlite3AbiError({
|
|
221
|
+
originalError: secondError,
|
|
222
|
+
packageRoot,
|
|
223
|
+
rebuildAttempted: true,
|
|
224
|
+
rebuildDetail: rebuildResult.detail,
|
|
225
|
+
}),
|
|
226
|
+
{ packageRoot, cause: secondError },
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
throw new BetterSqlite3LoadError(
|
|
232
|
+
formatBetterSqlite3AbiError({
|
|
233
|
+
originalError: firstError,
|
|
234
|
+
packageRoot,
|
|
235
|
+
rebuildAttempted: true,
|
|
236
|
+
rebuildDetail: rebuildResult.detail,
|
|
237
|
+
}),
|
|
238
|
+
{ packageRoot, cause: firstError },
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
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": {
|