hob-bot 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +111 -2
- package/README.zh-CN.md +112 -0
- package/bin/hob.mjs +7 -0
- package/dist/web/assets/index-D88l35PH.css +1 -0
- package/dist/web/assets/index-DUs8nN3E.js +218 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/fonts/inter-latin.woff2 +0 -0
- package/dist/web/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/web/icons/apple-touch-icon-180.png +0 -0
- package/dist/web/icons/favicon-32.png +0 -0
- package/dist/web/icons/icon-192.png +0 -0
- package/dist/web/icons/icon-512.png +0 -0
- package/dist/web/icons/icon-maskable-512.png +0 -0
- package/dist/web/index.html +32 -0
- package/dist/web/manifest.webmanifest +16 -0
- package/dist/web/sw.js +131 -0
- package/package.json +62 -4
- package/server/src/actions.ts +230 -0
- package/server/src/boot.ts +85 -0
- package/server/src/bus.ts +21 -0
- package/server/src/config.ts +123 -0
- package/server/src/cron-text.ts +49 -0
- package/server/src/delegations.ts +113 -0
- package/server/src/files.ts +142 -0
- package/server/src/http.ts +802 -0
- package/server/src/ids.ts +19 -0
- package/server/src/image.ts +218 -0
- package/server/src/imports.ts +302 -0
- package/server/src/jobs.ts +730 -0
- package/server/src/main.ts +69 -0
- package/server/src/memory.ts +272 -0
- package/server/src/models.ts +358 -0
- package/server/src/overview.ts +116 -0
- package/server/src/pages.ts +54 -0
- package/server/src/password.ts +23 -0
- package/server/src/pi.ts +273 -0
- package/server/src/prompt.ts +227 -0
- package/server/src/providers.ts +532 -0
- package/server/src/push.ts +285 -0
- package/server/src/router-tools.ts +549 -0
- package/server/src/router.ts +841 -0
- package/server/src/routines.ts +129 -0
- package/server/src/runtime.ts +104 -0
- package/server/src/search.ts +91 -0
- package/server/src/secrets.ts +46 -0
- package/server/src/settings.ts +452 -0
- package/server/src/skills.ts +150 -0
- package/server/src/store.ts +324 -0
- package/server/src/text.ts +14 -0
- package/server/src/time.ts +42 -0
- package/server/src/transcript.ts +272 -0
- package/server/src/worker-tools.ts +129 -0
- package/shared/command.ts +42 -0
- package/shared/files.ts +141 -0
- package/shared/media.ts +17 -0
- package/shared/model-note.ts +32 -0
- package/shared/page-shell.ts +108 -0
- package/shared/search.ts +19 -0
- package/shared/skills.ts +37 -0
- package/shared/sprites.ts +422 -0
- package/shared/thinking.ts +118 -0
- package/shared/types.ts +631 -0
- package/vendor/lore-pi-extension/README.md +28 -0
- package/vendor/lore-pi-extension/api.ts +140 -0
- package/vendor/lore-pi-extension/formatters.ts +204 -0
- package/vendor/lore-pi-extension/hooks.ts +195 -0
- package/vendor/lore-pi-extension/index.ts +25 -0
- package/vendor/lore-pi-extension/package.json +23 -0
- package/vendor/lore-pi-extension/skills.ts +447 -0
- package/vendor/lore-pi-extension/tools.ts +344 -0
- package/vendor/lore-pi-extension/uri.ts +67 -0
- package/vendor/lore-pi-extension/vendor/skill-workcopy/index.d.mts +266 -0
- package/vendor/lore-pi-extension/vendor/skill-workcopy/index.mjs +1328 -0
- package/vendor/lore-pi-extension/vendor/skill-workcopy/skills-sync.d.mts +33 -0
- package/vendor/lore-pi-extension/vendor/skill-workcopy/skills-sync.mjs +202 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi skills adapter — thin wrapper over vendor/skill-workcopy.
|
|
3
|
+
* Owns the lore_skill_* management tools and the resources_discover
|
|
4
|
+
* registration that exposes Lore skills through Pi's native skill system.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import { fetchJson, textResult } from './api';
|
|
9
|
+
import { runSkillsSync } from './vendor/skill-workcopy/skills-sync.mjs';
|
|
10
|
+
import {
|
|
11
|
+
normalizeSkillCandidate,
|
|
12
|
+
normalizeSkillDetail,
|
|
13
|
+
normalizeSkillSummary,
|
|
14
|
+
projectWorkCopyRoot,
|
|
15
|
+
skillIdOf,
|
|
16
|
+
skillVersionOf,
|
|
17
|
+
resolveLoreHome as resolveVendorLoreHome,
|
|
18
|
+
type SkillCandidate,
|
|
19
|
+
type SkillDetail,
|
|
20
|
+
type SkillSummary,
|
|
21
|
+
type WorkCopyMarker,
|
|
22
|
+
type WorkCopyStatus,
|
|
23
|
+
type WorkCopyState,
|
|
24
|
+
type SkillFile,
|
|
25
|
+
type MirrorState,
|
|
26
|
+
type MirrorMarker,
|
|
27
|
+
type MirrorStatus,
|
|
28
|
+
} from './vendor/skill-workcopy/index.mjs';
|
|
29
|
+
|
|
30
|
+
// Re-export shared core surface used by tests/index.
|
|
31
|
+
export {
|
|
32
|
+
LORE_SKILL_MARKER,
|
|
33
|
+
LORE_SKILL_SCHEMA,
|
|
34
|
+
LEGACY_MIRROR_SCHEMA,
|
|
35
|
+
SKILL_MD,
|
|
36
|
+
workCopiesRoot,
|
|
37
|
+
projectWorkCopyRoot,
|
|
38
|
+
skillsRoot,
|
|
39
|
+
installedRoot,
|
|
40
|
+
stagingRoot,
|
|
41
|
+
skillInstallPath,
|
|
42
|
+
sanitizeSegment,
|
|
43
|
+
validateSafeRelativePath,
|
|
44
|
+
skillIdOf,
|
|
45
|
+
skillVersionOf,
|
|
46
|
+
skillRevisionOf,
|
|
47
|
+
validateManagedFileList,
|
|
48
|
+
normalizeSkillSummary,
|
|
49
|
+
normalizeSkillDetail,
|
|
50
|
+
normalizeSkillCandidate,
|
|
51
|
+
sha256Buffer,
|
|
52
|
+
sha256Text,
|
|
53
|
+
decodeSkillFileContent,
|
|
54
|
+
computeManifestHash,
|
|
55
|
+
hashLocalSkillFiles,
|
|
56
|
+
readWorkCopyMarker,
|
|
57
|
+
readMirrorMarker,
|
|
58
|
+
inspectLocalWorkCopy,
|
|
59
|
+
inspectLocalMirror,
|
|
60
|
+
validateSkillPayload,
|
|
61
|
+
materializeSkillWorkCopy,
|
|
62
|
+
writeSkillMirrorAtomic,
|
|
63
|
+
} from './vendor/skill-workcopy/index.mjs';
|
|
64
|
+
|
|
65
|
+
export type {
|
|
66
|
+
SkillCandidate,
|
|
67
|
+
SkillDetail,
|
|
68
|
+
SkillSummary,
|
|
69
|
+
SkillFile,
|
|
70
|
+
WorkCopyMarker,
|
|
71
|
+
WorkCopyStatus,
|
|
72
|
+
WorkCopyState,
|
|
73
|
+
MirrorState,
|
|
74
|
+
MirrorMarker,
|
|
75
|
+
MirrorStatus,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function resolveLoreHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
79
|
+
return resolveVendorLoreHome(env);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SkillsClientState {
|
|
83
|
+
projectId?: string;
|
|
84
|
+
catalogRevision?: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- API ----
|
|
88
|
+
|
|
89
|
+
export async function listSkillsApi(pluginCfg: any, includeDisabled = true): Promise<{
|
|
90
|
+
project_id: string;
|
|
91
|
+
catalog_revision: string;
|
|
92
|
+
skills: SkillSummary[];
|
|
93
|
+
}> {
|
|
94
|
+
const qs = new URLSearchParams({ include_disabled: includeDisabled ? 'true' : 'false' });
|
|
95
|
+
const data = await fetchJson(pluginCfg, `/skills?${qs.toString()}`, { method: 'GET' });
|
|
96
|
+
return {
|
|
97
|
+
project_id: String(data?.project_id || ''),
|
|
98
|
+
catalog_revision: String(data?.catalog_revision || ''),
|
|
99
|
+
skills: Array.isArray(data?.skills) ? data.skills.map(normalizeSkillSummary) : [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function searchSkillsApi(pluginCfg: any, query: string, limit?: number): Promise<any> {
|
|
104
|
+
const qs = new URLSearchParams({ query: query || '' });
|
|
105
|
+
if (Number.isFinite(limit)) qs.set('limit', String(limit));
|
|
106
|
+
const data = await fetchJson(pluginCfg, `/skills/recall?${qs.toString()}`, { method: 'GET' });
|
|
107
|
+
if (Array.isArray(data?.candidates)) {
|
|
108
|
+
return { ...data, candidates: data.candidates.map(normalizeSkillCandidate) };
|
|
109
|
+
}
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function createSkillApi(pluginCfg: any, body: Record<string, unknown>): Promise<any> {
|
|
114
|
+
return fetchJson(pluginCfg, '/skills', { method: 'POST', body: JSON.stringify(body) });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function updateSkillApi(pluginCfg: any, skillId: string, body: Record<string, unknown>): Promise<any> {
|
|
118
|
+
return fetchJson(pluginCfg, `/skills/${encodeURIComponent(skillId)}`, {
|
|
119
|
+
method: 'PATCH',
|
|
120
|
+
body: JSON.stringify(body),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function deleteSkillApi(pluginCfg: any, skillId: string): Promise<any> {
|
|
125
|
+
return fetchJson(pluginCfg, `/skills/${encodeURIComponent(skillId)}`, { method: 'DELETE' });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function importSkillApi(pluginCfg: any, body: Record<string, unknown>): Promise<any> {
|
|
129
|
+
return fetchJson(pluginCfg, '/skills/import', { method: 'POST', body: JSON.stringify(body) });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function formatSkillImportText(data: any): string {
|
|
133
|
+
const repo = String(data?.repo || '');
|
|
134
|
+
const ref = String(data?.ref || '');
|
|
135
|
+
const repoLabel = repo ? (ref ? `${repo}@${ref}` : repo) : 'unknown repo';
|
|
136
|
+
const pathLabel = data?.path ? ` (path: ${data.path})` : '';
|
|
137
|
+
if (!Array.isArray(data?.results)) {
|
|
138
|
+
const candidates = Array.isArray(data?.candidates) ? data.candidates : [];
|
|
139
|
+
const lines = candidates.map((c: any) => {
|
|
140
|
+
const parts = [`- ${c?.name ?? '?'}`];
|
|
141
|
+
if (c?.error) parts.push(`error: ${c.error}`);
|
|
142
|
+
else if (c?.exists) parts.push('already exists');
|
|
143
|
+
else parts.push(c?.selected === false ? 'not selected' : 'will import');
|
|
144
|
+
if (c?.path) parts.push(String(c.path));
|
|
145
|
+
if (Number.isFinite(c?.file_count)) parts.push(`${c.file_count} files`);
|
|
146
|
+
if (Number.isFinite(c?.total_size)) parts.push(`${c.total_size} bytes`);
|
|
147
|
+
return parts.join(' — ');
|
|
148
|
+
});
|
|
149
|
+
return [
|
|
150
|
+
`Dry run from ${repoLabel}${pathLabel}: ${candidates.length} candidate skill(s)`,
|
|
151
|
+
...(lines.length ? lines : ['(no skills found)']),
|
|
152
|
+
].join('\n');
|
|
153
|
+
}
|
|
154
|
+
const results = Array.isArray(data?.results) ? data.results : [];
|
|
155
|
+
const counts = new Map<string, number>();
|
|
156
|
+
for (const r of results) {
|
|
157
|
+
const status = String(r?.status || 'unknown');
|
|
158
|
+
counts.set(status, (counts.get(status) || 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
const summary = [...counts.entries()].map(([status, n]) => `${n} ${status}`).join(', ');
|
|
161
|
+
const lines = results.map((r: any) => {
|
|
162
|
+
const id = r?.skill_id ? ` (skill_id: ${r.skill_id})` : '';
|
|
163
|
+
const err = r?.status === 'failed' && r?.error ? ` — ${r.error}` : '';
|
|
164
|
+
return `- ${r?.name ?? '?'}: ${r?.status ?? '?'}${id}${err}`;
|
|
165
|
+
});
|
|
166
|
+
return [`Import from ${repoLabel}${pathLabel}: ${summary || 'no results'}`, ...lines].join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ---- session-scoped skills client used by hooks ----
|
|
170
|
+
|
|
171
|
+
export function createSkillsSession(pluginCfg: any) {
|
|
172
|
+
// Project/catalog identity is recorded from the skills catalog list request
|
|
173
|
+
// (see registerSkillResourceDiscovery) and by the lore_skill_* tools; the
|
|
174
|
+
// session itself never downloads or reconciles skills.
|
|
175
|
+
const state: SkillsClientState = {};
|
|
176
|
+
const loreHome = () => (typeof pluginCfg?.loreHome === 'string' && pluginCfg.loreHome.trim()
|
|
177
|
+
? pluginCfg.loreHome.trim()
|
|
178
|
+
: resolveLoreHome());
|
|
179
|
+
|
|
180
|
+
return { state, loreHome };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export type SkillsSession = ReturnType<typeof createSkillsSession>;
|
|
184
|
+
|
|
185
|
+
// ---- host-native skill directory (resources_discover + background sync) ----
|
|
186
|
+
|
|
187
|
+
/** The catalog list gates skill-path registration, so it keeps a tight deadline. */
|
|
188
|
+
const SKILLS_DISCOVER_TIMEOUT_MS = 3_000;
|
|
189
|
+
|
|
190
|
+
export interface RegisteredSkillResources {
|
|
191
|
+
session: SkillsSession;
|
|
192
|
+
/** Resolves once the latest background sync has recorded its outcome. */
|
|
193
|
+
currentSync: () => Promise<void> | undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Register Pi's resources_discover handler that exposes the Lore work-copy root
|
|
198
|
+
* (~/.lore/skills/<project-id>/) as a native skill directory. The directory is
|
|
199
|
+
* reconciled with the server catalog by a background sync whose result lands in
|
|
200
|
+
* <root>/.lore-sync-status.json; the previous sync's skills are visible in the
|
|
201
|
+
* current session, this sync's in the next one (or after /reload).
|
|
202
|
+
*/
|
|
203
|
+
export function registerSkillResourceDiscovery(pi: any, pluginCfg: any, skillsSession?: SkillsSession): RegisteredSkillResources {
|
|
204
|
+
const session = skillsSession || createSkillsSession(pluginCfg);
|
|
205
|
+
let activeSync: Promise<void> | undefined;
|
|
206
|
+
|
|
207
|
+
pi.on('resources_discover', async () => {
|
|
208
|
+
let catalog: any;
|
|
209
|
+
try {
|
|
210
|
+
catalog = await fetchJson(
|
|
211
|
+
{ ...pluginCfg, timeoutMs: Math.min(pluginCfg.timeoutMs || SKILLS_DISCOVER_TIMEOUT_MS, SKILLS_DISCOVER_TIMEOUT_MS) },
|
|
212
|
+
'/skills',
|
|
213
|
+
{ method: 'GET' },
|
|
214
|
+
);
|
|
215
|
+
} catch (error: any) {
|
|
216
|
+
pi.logger?.debug?.(`lore: skills catalog unavailable (${error.message}); not registering skill paths`);
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
const projectId = String(catalog?.project_id || '').trim();
|
|
220
|
+
if (!projectId) return undefined;
|
|
221
|
+
session.state.projectId = projectId;
|
|
222
|
+
session.state.catalogRevision = catalog?.catalog_revision === undefined || catalog?.catalog_revision === null
|
|
223
|
+
? ''
|
|
224
|
+
: String(catalog.catalog_revision);
|
|
225
|
+
const root = projectWorkCopyRoot(session.loreHome(), projectId);
|
|
226
|
+
try {
|
|
227
|
+
fs.mkdirSync(root, { recursive: true });
|
|
228
|
+
} catch (error: any) {
|
|
229
|
+
pi.logger?.debug?.(`lore: cannot create skills root (${error.message})`);
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
activeSync = runSkillsSync({
|
|
233
|
+
clientType: 'pi',
|
|
234
|
+
root,
|
|
235
|
+
loreHome: session.loreHome(),
|
|
236
|
+
baseUrl: pluginCfg.baseUrl,
|
|
237
|
+
apiToken: pluginCfg.apiToken,
|
|
238
|
+
}).then((status) => {
|
|
239
|
+
if (status.errors.length > 0) {
|
|
240
|
+
pi.logger?.debug?.(`lore: background skills sync finished with ${status.errors.length} error(s)`);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
return { skillPaths: [root] };
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
return { session, currentSync: () => activeSync };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ---- tool registration ----
|
|
250
|
+
|
|
251
|
+
const Type = {
|
|
252
|
+
String: (meta?: Record<string, unknown>) => ({ type: 'string', ...meta }),
|
|
253
|
+
Number: (meta?: Record<string, unknown>) => ({ type: 'number', ...meta }),
|
|
254
|
+
Boolean: (meta?: Record<string, unknown>) => ({ type: 'boolean', ...meta }),
|
|
255
|
+
Array: (items: Record<string, unknown>) => ({ type: 'array', items }),
|
|
256
|
+
Optional: (schema: Record<string, unknown>) => ({ ...schema }),
|
|
257
|
+
Object: (properties: Record<string, unknown>, rest?: Record<string, unknown>) => ({
|
|
258
|
+
type: 'object',
|
|
259
|
+
properties,
|
|
260
|
+
...rest,
|
|
261
|
+
}),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
function skillFileParamSchema() {
|
|
265
|
+
return Type.Object({
|
|
266
|
+
path: Type.String({ description: 'Relative path inside the skill (must include SKILL.md).' }),
|
|
267
|
+
content: Type.Optional(Type.String({ description: 'UTF-8 file content.' })),
|
|
268
|
+
content_base64: Type.Optional(Type.String({ description: 'Base64 file content for binary files.' })),
|
|
269
|
+
media_type: Type.Optional(Type.String({ description: 'Optional media type.' })),
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function registerSkillTools(pi: any, pluginCfg: any, skillsSession?: SkillsSession) {
|
|
274
|
+
const session = skillsSession || createSkillsSession(pluginCfg);
|
|
275
|
+
|
|
276
|
+
pi.registerTool({
|
|
277
|
+
name: 'lore_skill_list',
|
|
278
|
+
label: 'Lore skill list',
|
|
279
|
+
description: 'List Lore skills for the active project, including disabled skills when requested.',
|
|
280
|
+
parameters: Type.Object({
|
|
281
|
+
include_disabled: Type.Optional(Type.Boolean({ description: 'Include disabled skills (default true).' })),
|
|
282
|
+
}),
|
|
283
|
+
async execute(_toolCallId: string, params: any = {}) {
|
|
284
|
+
try {
|
|
285
|
+
const includeDisabled = params?.include_disabled !== false;
|
|
286
|
+
const data = await listSkillsApi(pluginCfg, includeDisabled);
|
|
287
|
+
if (data.project_id) session.state.projectId = data.project_id;
|
|
288
|
+
if (data.catalog_revision) session.state.catalogRevision = data.catalog_revision;
|
|
289
|
+
const lines = (data.skills || []).map((s) => {
|
|
290
|
+
const enabled = s.enabled === false ? 'disabled' : 'enabled';
|
|
291
|
+
return `- ${s.name} (${skillIdOf(s)}) ${enabled} v${skillVersionOf(s) ?? '?'}`;
|
|
292
|
+
});
|
|
293
|
+
const text = lines.length > 0
|
|
294
|
+
? `Project ${data.project_id} rev ${data.catalog_revision}\n${lines.join('\n')}`
|
|
295
|
+
: `Project ${data.project_id || '?'} rev ${data.catalog_revision || '?'}\nNo skills.`;
|
|
296
|
+
return textResult(text, { ok: true, ...data });
|
|
297
|
+
} catch (error: any) {
|
|
298
|
+
return textResult(`Lore skill list failed: ${error.message}`, { ok: false, error: error.message });
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
pi.registerTool({
|
|
304
|
+
name: 'lore_skill_search',
|
|
305
|
+
label: 'Lore skill search',
|
|
306
|
+
description: 'Search or recall Lore skills by query.',
|
|
307
|
+
parameters: Type.Object({
|
|
308
|
+
query: Type.String({ description: 'Search query.' }),
|
|
309
|
+
limit: Type.Optional(Type.Number({ minimum: 1, maximum: 50, description: 'Max candidates.' })),
|
|
310
|
+
}),
|
|
311
|
+
async execute(_toolCallId: string, params: any = {}) {
|
|
312
|
+
try {
|
|
313
|
+
const query = String(params?.query || '');
|
|
314
|
+
const limit = Number.isFinite(params?.limit) ? params.limit : undefined;
|
|
315
|
+
const data = await searchSkillsApi(pluginCfg, query, limit);
|
|
316
|
+
return textResult(JSON.stringify(data, null, 2), { ok: true, result: data });
|
|
317
|
+
} catch (error: any) {
|
|
318
|
+
return textResult(`Lore skill search failed: ${error.message}`, { ok: false, error: error.message });
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
pi.registerTool({
|
|
324
|
+
name: 'lore_skill_create',
|
|
325
|
+
label: 'Lore skill create',
|
|
326
|
+
description: 'Create a Lore skill on the server. Changes sync to the local skills directory when the next session starts (Pi: /reload).',
|
|
327
|
+
parameters: Type.Object({
|
|
328
|
+
name: Type.String({ description: 'Skill name.' }),
|
|
329
|
+
enabled: Type.Optional(Type.Boolean({ description: 'Whether the skill is enabled (default true).' })),
|
|
330
|
+
files: Type.Array(skillFileParamSchema(), { description: 'Skill files; must include SKILL.md.' }),
|
|
331
|
+
}),
|
|
332
|
+
async execute(_toolCallId: string, params: any = {}) {
|
|
333
|
+
const body = {
|
|
334
|
+
name: String(params?.name || '').trim(),
|
|
335
|
+
enabled: params?.enabled !== false,
|
|
336
|
+
files: Array.isArray(params?.files) ? params.files : [],
|
|
337
|
+
};
|
|
338
|
+
try {
|
|
339
|
+
if (!body.name) throw new Error('name is required');
|
|
340
|
+
const data = normalizeSkillDetail(await createSkillApi(pluginCfg, body));
|
|
341
|
+
if (data?.project_id) session.state.projectId = String(data.project_id);
|
|
342
|
+
const createdId = skillIdOf(data) || '?';
|
|
343
|
+
const createdVersion = skillVersionOf(data);
|
|
344
|
+
const createdSuffix = createdVersion !== undefined && createdVersion !== null && createdVersion !== ''
|
|
345
|
+
? `skill_id: ${createdId}, version: ${createdVersion}`
|
|
346
|
+
: `skill_id: ${createdId}`;
|
|
347
|
+
return textResult(`Created skill ${data?.name || body.name} (${createdSuffix})`, { ok: true, result: data });
|
|
348
|
+
} catch (error: any) {
|
|
349
|
+
return textResult(`Lore skill create failed: ${error.message}`, { ok: false, error: error.message, body });
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
pi.registerTool({
|
|
355
|
+
name: 'lore_skill_update',
|
|
356
|
+
label: 'Lore skill update',
|
|
357
|
+
description: 'Update a Lore skill on the server with optimistic concurrency via expected_version. '
|
|
358
|
+
+ 'Changes sync to the local skills directory when the next session starts (Pi: /reload).',
|
|
359
|
+
parameters: Type.Object({
|
|
360
|
+
skill_id: Type.String({ description: 'Skill id.' }),
|
|
361
|
+
expected_version: Type.Number({ description: 'Expected current integer version (optimistic concurrency).' }),
|
|
362
|
+
enabled: Type.Optional(Type.Boolean({ description: 'Enable or disable the skill.' })),
|
|
363
|
+
upsert_files: Type.Optional(Type.Array(skillFileParamSchema(), { description: 'Files to create or replace.' })),
|
|
364
|
+
delete_paths: Type.Optional(Type.Array(Type.String({ description: 'Relative path to delete.' }), { description: 'Paths to delete.' })),
|
|
365
|
+
}),
|
|
366
|
+
async execute(_toolCallId: string, params: any = {}) {
|
|
367
|
+
const skillId = String(params?.skill_id || '').trim();
|
|
368
|
+
const expectedVersionRaw = params?.expected_version;
|
|
369
|
+
const body: Record<string, unknown> = {};
|
|
370
|
+
if (typeof params?.enabled === 'boolean') body.enabled = params.enabled;
|
|
371
|
+
if (Array.isArray(params?.upsert_files)) body.upsert_files = params.upsert_files;
|
|
372
|
+
if (Array.isArray(params?.delete_paths)) body.delete_paths = params.delete_paths;
|
|
373
|
+
try {
|
|
374
|
+
if (!skillId) throw new Error('skill_id is required');
|
|
375
|
+
if (!Number.isInteger(expectedVersionRaw) || expectedVersionRaw < 1) {
|
|
376
|
+
throw new Error('expected_version is required and must be a positive integer');
|
|
377
|
+
}
|
|
378
|
+
body.expected_version = expectedVersionRaw;
|
|
379
|
+
const data = normalizeSkillDetail(await updateSkillApi(pluginCfg, skillId, body));
|
|
380
|
+
const updatedId = skillIdOf(data) || skillId;
|
|
381
|
+
const updatedVersion = skillVersionOf(data);
|
|
382
|
+
const updatedSuffix = updatedVersion !== undefined && updatedVersion !== null && updatedVersion !== ''
|
|
383
|
+
? `skill_id: ${updatedId}, version: ${updatedVersion}`
|
|
384
|
+
: `skill_id: ${updatedId}`;
|
|
385
|
+
return textResult(`Updated skill ${data?.name || skillId} (${updatedSuffix})`, { ok: true, result: data });
|
|
386
|
+
} catch (error: any) {
|
|
387
|
+
return textResult(`Lore skill update failed: ${error.message}`, { ok: false, error: error.message, skill_id: skillId, body });
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
pi.registerTool({
|
|
393
|
+
name: 'lore_skill_delete',
|
|
394
|
+
label: 'Lore skill delete',
|
|
395
|
+
description: 'Archive/delete a Lore skill on the server. Changes sync to the local skills directory when the next session starts (Pi: /reload).',
|
|
396
|
+
parameters: Type.Object({
|
|
397
|
+
skill_id: Type.String({ description: 'Skill id.' }),
|
|
398
|
+
}),
|
|
399
|
+
async execute(_toolCallId: string, params: any = {}) {
|
|
400
|
+
try {
|
|
401
|
+
const skillId = String(params?.skill_id || '').trim();
|
|
402
|
+
if (!skillId) throw new Error('skill_id is required');
|
|
403
|
+
const data = await deleteSkillApi(pluginCfg, skillId);
|
|
404
|
+
if (data?.project_id) session.state.projectId = String(data.project_id);
|
|
405
|
+
if (data?.catalog_revision !== undefined) session.state.catalogRevision = String(data.catalog_revision);
|
|
406
|
+
return textResult(`Deleted skill ${skillId}`, { ok: true, result: data });
|
|
407
|
+
} catch (error: any) {
|
|
408
|
+
return textResult(`Lore skill delete failed: ${error.message}`, { ok: false, error: error.message });
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
pi.registerTool({
|
|
414
|
+
name: 'lore_skill_import',
|
|
415
|
+
label: 'Lore skill import',
|
|
416
|
+
description:
|
|
417
|
+
'Import skills from a public GitHub repository (owner/repo, a GitHub URL, a tree URL to one skill, or a pasted npx skills add command) into Lore. '
|
|
418
|
+
+ 'Run with dry_run first to see what will be imported. Never overwrites existing skills.',
|
|
419
|
+
parameters: Type.Object({
|
|
420
|
+
source: Type.String({ description: 'GitHub source: owner/repo, a GitHub URL, a tree URL, or a pasted npx skills add command.' }),
|
|
421
|
+
skills: Type.Optional(Type.Array(Type.String({ description: 'Skill name to import.' }), { description: 'Skill names to import; omit or pass * to import all found.' })),
|
|
422
|
+
visibility_client_types: Type.Optional(Type.Array(Type.String({ description: 'Client type allowed to use the imported skills.' }), { description: 'Client types that can use the imported skills (same meaning as on create); omit for the server default.' })),
|
|
423
|
+
dry_run: Type.Optional(Type.Boolean({ description: 'Only list candidate skills without importing.' })),
|
|
424
|
+
}),
|
|
425
|
+
async execute(_toolCallId: string, params: any = {}) {
|
|
426
|
+
const source = String(params?.source || '').trim();
|
|
427
|
+
const body: Record<string, unknown> = {};
|
|
428
|
+
if (Array.isArray(params?.skills)) {
|
|
429
|
+
body.skills = params.skills.map((s: unknown) => String(s || '').trim()).filter(Boolean);
|
|
430
|
+
}
|
|
431
|
+
if (Array.isArray(params?.visibility_client_types)) {
|
|
432
|
+
body.visibility_client_types = params.visibility_client_types.map((s: unknown) => String(s || '').trim()).filter(Boolean);
|
|
433
|
+
}
|
|
434
|
+
if (params?.dry_run === true) body.dry_run = true;
|
|
435
|
+
try {
|
|
436
|
+
if (!source) throw new Error('source is required');
|
|
437
|
+
body.source = source;
|
|
438
|
+
const data = await importSkillApi(pluginCfg, body);
|
|
439
|
+
return textResult(`${formatSkillImportText(data)}\n\n${JSON.stringify(data, null, 2)}`, { ok: true, result: data });
|
|
440
|
+
} catch (error: any) {
|
|
441
|
+
return textResult(`Lore skill import failed: ${error.message}`, { ok: false, error: error.message, source });
|
|
442
|
+
}
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
return session;
|
|
447
|
+
}
|