@tyvm/knowhow 0.0.129 → 0.0.130

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.
Files changed (96) hide show
  1. package/package.json +4 -4
  2. package/src/agents/base/base.ts +40 -5
  3. package/src/agents/tools/index.ts +1 -0
  4. package/src/agents/tools/list.ts +2 -0
  5. package/src/agents/tools/skills.ts +79 -0
  6. package/src/chat/modules/AgentModule.ts +84 -1
  7. package/src/cli.ts +4 -0
  8. package/src/clients/anthropic.ts +59 -1
  9. package/src/clients/openai.ts +28 -5
  10. package/src/clients/pricing/openai.ts +3 -0
  11. package/src/clients/types.ts +27 -4
  12. package/src/clients/xai.ts +2 -2
  13. package/src/commands/agent.ts +19 -0
  14. package/src/commands/behaviors.ts +47 -0
  15. package/src/commands/services.ts +6 -2
  16. package/src/commands/skills.ts +71 -0
  17. package/src/index.ts +2 -0
  18. package/src/plugins/PluginBase.ts +0 -2
  19. package/src/plugins/SkillsPlugin.ts +11 -124
  20. package/src/plugins/language.ts +0 -1
  21. package/src/plugins/plugins.ts +24 -13
  22. package/src/plugins/types.ts +8 -2
  23. package/src/services/BehaviorsService.ts +432 -0
  24. package/src/services/KnowhowClient.ts +61 -0
  25. package/src/services/SkillsService.ts +622 -0
  26. package/src/services/Tools.ts +2 -0
  27. package/src/services/index.ts +8 -2
  28. package/src/services/modules/types.ts +2 -0
  29. package/ts_build/package.json +4 -4
  30. package/ts_build/src/agents/base/base.d.ts +9 -1
  31. package/ts_build/src/agents/base/base.js +29 -4
  32. package/ts_build/src/agents/base/base.js.map +1 -1
  33. package/ts_build/src/agents/tools/index.d.ts +1 -0
  34. package/ts_build/src/agents/tools/index.js +1 -0
  35. package/ts_build/src/agents/tools/index.js.map +1 -1
  36. package/ts_build/src/agents/tools/list.js +2 -0
  37. package/ts_build/src/agents/tools/list.js.map +1 -1
  38. package/ts_build/src/agents/tools/skills.d.ts +33 -0
  39. package/ts_build/src/agents/tools/skills.js +62 -0
  40. package/ts_build/src/agents/tools/skills.js.map +1 -0
  41. package/ts_build/src/chat/modules/AgentModule.d.ts +2 -0
  42. package/ts_build/src/chat/modules/AgentModule.js +69 -0
  43. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  44. package/ts_build/src/chat/modules/CustomCommandsModule.d.ts +1 -1
  45. package/ts_build/src/cli.js +4 -0
  46. package/ts_build/src/cli.js.map +1 -1
  47. package/ts_build/src/clients/anthropic.d.ts +2 -0
  48. package/ts_build/src/clients/anthropic.js +35 -0
  49. package/ts_build/src/clients/anthropic.js.map +1 -1
  50. package/ts_build/src/clients/openai.d.ts +1 -0
  51. package/ts_build/src/clients/openai.js +25 -5
  52. package/ts_build/src/clients/openai.js.map +1 -1
  53. package/ts_build/src/clients/pricing/openai.js +3 -0
  54. package/ts_build/src/clients/pricing/openai.js.map +1 -1
  55. package/ts_build/src/clients/types.d.ts +6 -1
  56. package/ts_build/src/clients/xai.js +1 -1
  57. package/ts_build/src/clients/xai.js.map +1 -1
  58. package/ts_build/src/commands/agent.js +16 -0
  59. package/ts_build/src/commands/agent.js.map +1 -1
  60. package/ts_build/src/commands/behaviors.d.ts +2 -0
  61. package/ts_build/src/commands/behaviors.js +43 -0
  62. package/ts_build/src/commands/behaviors.js.map +1 -0
  63. package/ts_build/src/commands/services.js +4 -2
  64. package/ts_build/src/commands/services.js.map +1 -1
  65. package/ts_build/src/commands/skills.d.ts +2 -0
  66. package/ts_build/src/commands/skills.js +61 -0
  67. package/ts_build/src/commands/skills.js.map +1 -0
  68. package/ts_build/src/index.d.ts +2 -0
  69. package/ts_build/src/index.js +5 -1
  70. package/ts_build/src/index.js.map +1 -1
  71. package/ts_build/src/plugins/PluginBase.d.ts +0 -2
  72. package/ts_build/src/plugins/PluginBase.js.map +1 -1
  73. package/ts_build/src/plugins/SkillsPlugin.d.ts +1 -6
  74. package/ts_build/src/plugins/SkillsPlugin.js +6 -135
  75. package/ts_build/src/plugins/SkillsPlugin.js.map +1 -1
  76. package/ts_build/src/plugins/language.js.map +1 -1
  77. package/ts_build/src/plugins/plugins.d.ts +4 -1
  78. package/ts_build/src/plugins/plugins.js +17 -11
  79. package/ts_build/src/plugins/plugins.js.map +1 -1
  80. package/ts_build/src/plugins/types.d.ts +3 -2
  81. package/ts_build/src/services/BehaviorsService.d.ts +46 -0
  82. package/ts_build/src/services/BehaviorsService.js +358 -0
  83. package/ts_build/src/services/BehaviorsService.js.map +1 -0
  84. package/ts_build/src/services/KnowhowClient.d.ts +8 -0
  85. package/ts_build/src/services/KnowhowClient.js +20 -0
  86. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  87. package/ts_build/src/services/SkillsService.d.ts +53 -0
  88. package/ts_build/src/services/SkillsService.js +478 -0
  89. package/ts_build/src/services/SkillsService.js.map +1 -0
  90. package/ts_build/src/services/Tools.d.ts +2 -0
  91. package/ts_build/src/services/Tools.js.map +1 -1
  92. package/ts_build/src/services/index.d.ts +3 -0
  93. package/ts_build/src/services/index.js +8 -2
  94. package/ts_build/src/services/index.js.map +1 -1
  95. package/ts_build/src/services/modules/index.d.ts +4 -2
  96. package/ts_build/src/services/modules/types.d.ts +2 -0
@@ -0,0 +1,622 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { createHash } from "crypto";
5
+ import * as https from "https";
6
+
7
+ // Default local install dir (relative to cwd)
8
+ const LOCAL_SKILLS_DIR = ".knowhow/skills";
9
+ // Lock file paths
10
+ const LOCK_FILE_PRIMARY = ".knowhow/skills/skills-lock.json";
11
+ const LOCK_FILE_LEGACY = "skills-lock.json";
12
+ // Global install dir (absolute)
13
+ const GLOBAL_SKILLS_DIR = path.join(os.homedir(), ".knowhow", "skills");
14
+ const GLOBAL_LOCK_FILE = path.join(os.homedir(), ".knowhow", "skills", "skills-lock.json");
15
+
16
+ const SKILLS_SH_API = "https://skills.sh";
17
+
18
+ export interface SkillLockEntry {
19
+ source: string;
20
+ sourceType: "github" | "git" | "well-known" | string;
21
+ skillPath?: string;
22
+ computedHash: string;
23
+ commitHash?: string;
24
+ ref?: string;
25
+ sourceUrl?: string;
26
+ }
27
+
28
+ export interface SkillsLock {
29
+ version: number;
30
+ skills: Record<string, SkillLockEntry>;
31
+ }
32
+
33
+ export interface SkillDownloadResponse {
34
+ files: Array<{ path: string; contents: string }>;
35
+ hash: string;
36
+ }
37
+
38
+ export interface SkillsServiceOptions {
39
+ /** If true, use global ~/.knowhow/skills/ as install dir and lock file location */
40
+ global?: boolean;
41
+ /** Custom install directory (absolute or relative to cwd). Overrides global. */
42
+ installDir?: string;
43
+ /** Working directory (defaults to process.cwd()) */
44
+ cwd?: string;
45
+ }
46
+
47
+ function createEmptyLock(): SkillsLock {
48
+ return { version: 1, skills: {} };
49
+ }
50
+
51
+ /**
52
+ * Compute the same hash as `npx skills` `computeSnapshotHash` does:
53
+ * given an array of {path, contents} (as returned by skills.sh API),
54
+ * sort by path, then sha256(path + contents) for each file.
55
+ */
56
+ export function computeSkillApiHash(
57
+ files: Array<{ path: string; contents: string }>
58
+ ): string {
59
+ const sorted = [...files].sort((a, b) => a.path.localeCompare(b.path));
60
+ const hash = createHash("sha256");
61
+ for (const file of sorted) {
62
+ hash.update(file.path);
63
+ hash.update(Buffer.from(file.contents, "utf-8"));
64
+ }
65
+ return hash.digest("hex");
66
+ }
67
+
68
+ /**
69
+ * Compute a content hash for an already-installed skill folder.
70
+ * Sort files by relative path, then hash(relativePath + content) for each.
71
+ */
72
+ export function computeSkillFolderHash(skillDir: string): string {
73
+ const files: Array<{ relativePath: string; content: Buffer }> = [];
74
+ collectFiles(skillDir, skillDir, files);
75
+ files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
76
+ const hash = createHash("sha256");
77
+ for (const file of files) {
78
+ hash.update(file.relativePath);
79
+ hash.update(file.content);
80
+ }
81
+ return hash.digest("hex");
82
+ }
83
+
84
+ function collectFiles(
85
+ baseDir: string,
86
+ currentDir: string,
87
+ results: Array<{ relativePath: string; content: Buffer }>
88
+ ): void {
89
+ if (!fs.existsSync(currentDir)) return;
90
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ const fullPath = path.join(currentDir, entry.name);
93
+ if (entry.isDirectory()) {
94
+ if (entry.name === ".git" || entry.name === "node_modules") continue;
95
+ collectFiles(baseDir, fullPath, results);
96
+ } else if (entry.isFile()) {
97
+ const content = fs.readFileSync(fullPath);
98
+ const relativePath = path
99
+ .relative(baseDir, fullPath)
100
+ .split("\\")
101
+ .join("/");
102
+ results.push({ relativePath, content });
103
+ }
104
+ }
105
+ }
106
+
107
+ function fetchJson<T>(url: string): Promise<T> {
108
+ return new Promise((resolve, reject) => {
109
+ https
110
+ .get(url, { headers: { "User-Agent": "knowhow-cli" } }, (res) => {
111
+ const chunks: Buffer[] = [];
112
+ res.on("data", (chunk: Buffer) => chunks.push(chunk));
113
+ res.on("end", () => {
114
+ try {
115
+ const body = Buffer.concat(chunks).toString("utf-8");
116
+ if (res.statusCode && res.statusCode >= 400) {
117
+ reject(new Error(`HTTP ${res.statusCode}: ${body}`));
118
+ return;
119
+ }
120
+ resolve(JSON.parse(body) as T);
121
+ } catch (e) {
122
+ reject(e);
123
+ }
124
+ });
125
+ })
126
+ .on("error", reject);
127
+ });
128
+ }
129
+
130
+ function fetchRaw(url: string): Promise<string> {
131
+ return new Promise((resolve, reject) => {
132
+ https
133
+ .get(url, { headers: { "User-Agent": "knowhow-cli" } }, (res) => {
134
+ const chunks: Buffer[] = [];
135
+ res.on("data", (chunk: Buffer) => chunks.push(chunk));
136
+ res.on("end", () => {
137
+ const body = Buffer.concat(chunks).toString("utf-8");
138
+ if (res.statusCode && res.statusCode >= 400) {
139
+ reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
140
+ return;
141
+ }
142
+ resolve(body);
143
+ });
144
+ })
145
+ .on("error", reject);
146
+ });
147
+ }
148
+
149
+ /**
150
+ * Fetch the latest commit SHA for a file path in a GitHub repo.
151
+ */
152
+ async function fetchLatestCommitHash(
153
+ owner: string,
154
+ repo: string,
155
+ filePath: string
156
+ ): Promise<string | undefined> {
157
+ try {
158
+ const url = `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/commits?path=${encodeURIComponent(filePath)}&per_page=1`;
159
+ const commits = await fetchJson<Array<{ sha: string }>>(url);
160
+ if (Array.isArray(commits) && commits.length > 0) {
161
+ return commits[0].sha;
162
+ }
163
+ } catch {
164
+ // Non-fatal
165
+ }
166
+ return undefined;
167
+ }
168
+
169
+ /**
170
+ * Parse a skill reference. Supports:
171
+ * - "owner/repo@skill-name"
172
+ * - "owner/repo"
173
+ * - "https://github.com/owner/repo"
174
+ * - "https://github.com/owner/repo@skill-name"
175
+ * - GitHub URL with explicit skillName override
176
+ */
177
+ export function parseSkillRef(
178
+ input: string,
179
+ skillNameOverride?: string
180
+ ): {
181
+ owner: string;
182
+ repo: string;
183
+ skill?: string;
184
+ sourceUrl?: string;
185
+ } {
186
+ // Strip trailing .git
187
+ let normalized = input.replace(/\.git$/, "");
188
+
189
+ // Extract @skill suffix before URL parsing
190
+ let skillFromAt: string | undefined;
191
+ const atIdx = normalized.lastIndexOf("@");
192
+ // Only treat @ as skill separator if it's not part of the protocol (https://)
193
+ if (atIdx > 0 && !normalized.slice(0, atIdx).match(/\/\/$/)) {
194
+ const beforeAt = normalized.slice(0, atIdx);
195
+ const afterAt = normalized.slice(atIdx + 1);
196
+ // Make sure the beforeAt part contains a slash (it's a path, not a protocol)
197
+ if (beforeAt.includes("/")) {
198
+ skillFromAt = afterAt;
199
+ normalized = beforeAt;
200
+ }
201
+ }
202
+
203
+ let owner: string;
204
+ let repo: string;
205
+ let sourceUrl: string | undefined;
206
+
207
+ // GitHub URL format
208
+ const githubMatch = normalized.match(
209
+ /^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/?$/
210
+ );
211
+ if (githubMatch) {
212
+ owner = githubMatch[1];
213
+ repo = githubMatch[2];
214
+ sourceUrl = `https://github.com/${owner}/${repo}`;
215
+ } else {
216
+ // owner/repo format
217
+ const parts = normalized.split("/");
218
+ if (parts.length < 2) {
219
+ throw new Error(
220
+ `Invalid skill reference "${input}". Expected formats:\n` +
221
+ ` owner/repo@skill-name\n` +
222
+ ` https://github.com/owner/repo --skill <name>`
223
+ );
224
+ }
225
+ owner = parts[0];
226
+ repo = parts[1];
227
+ }
228
+
229
+ // Precedence: explicit --skill flag > @skill in string
230
+ const skill = skillNameOverride ?? skillFromAt;
231
+
232
+ return { owner, repo, skill, sourceUrl };
233
+ }
234
+
235
+ export class SkillsService {
236
+ private cwd: string;
237
+ private isGlobal: boolean;
238
+ private customInstallDir?: string;
239
+
240
+ constructor(opts: SkillsServiceOptions = {}) {
241
+ this.cwd = opts.cwd ?? process.cwd();
242
+ this.isGlobal = opts.global ?? false;
243
+ this.customInstallDir = opts.installDir;
244
+ }
245
+
246
+ /** Absolute path to the skills install directory */
247
+ getInstallDir(): string {
248
+ if (this.customInstallDir) {
249
+ return path.isAbsolute(this.customInstallDir)
250
+ ? this.customInstallDir
251
+ : path.join(this.cwd, this.customInstallDir);
252
+ }
253
+ if (this.isGlobal) {
254
+ return GLOBAL_SKILLS_DIR;
255
+ }
256
+ return path.join(this.cwd, LOCAL_SKILLS_DIR);
257
+ }
258
+
259
+ /** Absolute path to the lock file */
260
+ getLockPath(): string {
261
+ if (this.isGlobal) {
262
+ return GLOBAL_LOCK_FILE;
263
+ }
264
+ if (this.customInstallDir) {
265
+ return path.join(this.getInstallDir(), "skills-lock.json");
266
+ }
267
+ // Local: prefer primary, fall back to legacy
268
+ const primary = path.join(this.cwd, LOCK_FILE_PRIMARY);
269
+ const legacy = path.join(this.cwd, LOCK_FILE_LEGACY);
270
+ if (!fs.existsSync(primary) && fs.existsSync(legacy)) {
271
+ return legacy;
272
+ }
273
+ return primary;
274
+ }
275
+
276
+ /** Absolute path to where we always write the lock file */
277
+ getLockWritePath(): string {
278
+ if (this.isGlobal) return GLOBAL_LOCK_FILE;
279
+ if (this.customInstallDir) {
280
+ return path.join(this.getInstallDir(), "skills-lock.json");
281
+ }
282
+ return path.join(this.cwd, LOCK_FILE_PRIMARY);
283
+ }
284
+
285
+ readLock(): SkillsLock {
286
+ const lockPath = this.getLockPath();
287
+ try {
288
+ if (fs.existsSync(lockPath)) {
289
+ const content = fs.readFileSync(lockPath, "utf-8");
290
+ const parsed = JSON.parse(content) as SkillsLock;
291
+ if (typeof parsed.version !== "number" || !parsed.skills) {
292
+ return createEmptyLock();
293
+ }
294
+ return parsed;
295
+ }
296
+ } catch {
297
+ // fall through
298
+ }
299
+ return createEmptyLock();
300
+ }
301
+
302
+ writeLock(lock: SkillsLock): void {
303
+ const lockPath = this.getLockWritePath();
304
+ const dir = path.dirname(lockPath);
305
+ fs.mkdirSync(dir, { recursive: true });
306
+
307
+ // Sort skills keys for deterministic output
308
+ const sortedSkills: Record<string, SkillLockEntry> = {};
309
+ for (const key of Object.keys(lock.skills).sort()) {
310
+ sortedSkills[key] = lock.skills[key];
311
+ }
312
+ const sorted: SkillsLock = { version: lock.version, skills: sortedSkills };
313
+ fs.writeFileSync(lockPath, JSON.stringify(sorted, null, 2) + "\n", "utf-8");
314
+ }
315
+
316
+ /**
317
+ * Fetch skill from skills.sh CDN API.
318
+ */
319
+ async fetchSkillFromApi(
320
+ owner: string,
321
+ repo: string,
322
+ skill: string
323
+ ): Promise<SkillDownloadResponse> {
324
+ const url = `${SKILLS_SH_API}/api/download/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(skill)}`;
325
+ return fetchJson<SkillDownloadResponse>(url);
326
+ }
327
+
328
+ /**
329
+ * Install a skill.
330
+ *
331
+ * @param skillRef - "owner/repo@skill", "owner/repo", or "https://github.com/owner/repo"
332
+ * @param skillNameOverride - explicit skill name (from --skill flag), overrides @skill in ref
333
+ */
334
+ async add(skillRef: string, skillNameOverride?: string): Promise<void> {
335
+ const { owner, repo, skill, sourceUrl } = parseSkillRef(skillRef, skillNameOverride);
336
+ const source = `${owner}/${repo}`;
337
+
338
+ if (!skill) {
339
+ throw new Error(
340
+ `Please specify a skill name:\n` +
341
+ ` knowhow skills add ${source}@<skill-name>\n` +
342
+ ` knowhow skills add https://github.com/${source} --skill <skill-name>`
343
+ );
344
+ }
345
+
346
+ const installDir = this.getInstallDir();
347
+ const scope = this.isGlobal ? "globally" : "locally";
348
+ console.log(`📦 Fetching skill "${skill}" from ${source} (installing ${scope})...`);
349
+
350
+ let download: SkillDownloadResponse;
351
+ try {
352
+ download = await this.fetchSkillFromApi(owner, repo, skill);
353
+ } catch (e) {
354
+ const msg = e instanceof Error ? e.message : String(e);
355
+ throw new Error(
356
+ `Failed to fetch skill "${skill}" from skills.sh API: ${msg}\n` +
357
+ `Make sure the skill exists at: https://skills.sh/${source}/${skill}`
358
+ );
359
+ }
360
+
361
+ if (!download.files || download.files.length === 0) {
362
+ throw new Error(`No files found for skill "${skill}"`);
363
+ }
364
+
365
+ // Write files to install dir
366
+ const skillInstallDir = path.join(installDir, skill);
367
+ fs.mkdirSync(skillInstallDir, { recursive: true });
368
+
369
+ for (const file of download.files) {
370
+ const filePath = path.join(skillInstallDir, file.path);
371
+ const fileDir = path.dirname(filePath);
372
+ fs.mkdirSync(fileDir, { recursive: true });
373
+ fs.writeFileSync(filePath, file.contents, "utf-8");
374
+ }
375
+
376
+ // Compute content hash from downloaded files
377
+ const computedHash = computeSkillApiHash(download.files);
378
+
379
+ if (download.hash && download.hash !== computedHash) {
380
+ console.warn(
381
+ `⚠ Note: API cached hash (${download.hash}) differs from computed hash (${computedHash}). Storing computed hash.`
382
+ );
383
+ }
384
+
385
+ const skillPath = `skills/${skill}/SKILL.md`;
386
+ const commitHash = await fetchLatestCommitHash(owner, repo, skillPath);
387
+ if (commitHash) {
388
+ console.log(`📎 Pinned to commit ${commitHash.slice(0, 8)}`);
389
+ }
390
+
391
+ // Update lock
392
+ const lock = this.readLock();
393
+ lock.skills[skill] = {
394
+ source,
395
+ sourceType: "github",
396
+ skillPath,
397
+ computedHash,
398
+ ...(commitHash ? { commitHash } : {}),
399
+ ...(sourceUrl ? { sourceUrl } : {}),
400
+ };
401
+ this.writeLock(lock);
402
+
403
+ console.log(`✅ Installed skill "${skill}" to ${skillInstallDir}`);
404
+ console.log(`🔒 Updated lock file at ${this.getLockWritePath()}`);
405
+ }
406
+
407
+ /**
408
+ * Remove a skill by name.
409
+ */
410
+ remove(skillName: string): void {
411
+ const lock = this.readLock();
412
+
413
+ if (!lock.skills[skillName]) {
414
+ console.warn(`⚠ Skill "${skillName}" not found in lock file.`);
415
+ } else {
416
+ delete lock.skills[skillName];
417
+ this.writeLock(lock);
418
+ console.log(`🗑 Removed "${skillName}" from lock file.`);
419
+ }
420
+
421
+ const installDir = path.join(this.getInstallDir(), skillName);
422
+ if (fs.existsSync(installDir)) {
423
+ fs.rmSync(installDir, { recursive: true, force: true });
424
+ console.log(`🗑 Deleted ${installDir}`);
425
+ } else {
426
+ console.warn(`⚠ Skill directory not found: ${installDir}`);
427
+ }
428
+ }
429
+
430
+ /**
431
+ * List installed skills from lock file.
432
+ */
433
+ list(): void {
434
+ const lock = this.readLock();
435
+ const skillEntries = Object.entries(lock.skills);
436
+
437
+ if (skillEntries.length === 0) {
438
+ console.log(
439
+ "No skills installed. Run `knowhow skills add <owner/repo@skill>` to install one."
440
+ );
441
+ return;
442
+ }
443
+
444
+ const installDir = this.getInstallDir();
445
+ console.log(`Installed skills (${skillEntries.length}):\n`);
446
+ for (const [name, entry] of skillEntries) {
447
+ const skillDir = path.join(installDir, name);
448
+ const installed = fs.existsSync(skillDir);
449
+ const statusIcon = installed ? "✅" : "❌";
450
+ console.log(` ${statusIcon} ${name}`);
451
+ console.log(` source: ${entry.source}`);
452
+ if (entry.skillPath) console.log(` path: ${entry.skillPath}`);
453
+ console.log(` hash: ${entry.computedHash}`);
454
+ }
455
+ }
456
+
457
+ /**
458
+ * Install all skills from the lock file (restore).
459
+ */
460
+ async install(): Promise<void> {
461
+ const lock = this.readLock();
462
+ const skillEntries = Object.entries(lock.skills);
463
+
464
+ if (skillEntries.length === 0) {
465
+ console.log("No skills in lock file. Nothing to install.");
466
+ return;
467
+ }
468
+
469
+ const installDir = this.getInstallDir();
470
+ console.log(`📦 Installing ${skillEntries.length} skill(s) from lock file...\n`);
471
+ let installed = 0;
472
+ let skipped = 0;
473
+ let failed = 0;
474
+
475
+ for (const [name, entry] of skillEntries) {
476
+ const skillDir = path.join(installDir, name);
477
+
478
+ // Check if already installed with correct hash
479
+ if (fs.existsSync(skillDir)) {
480
+ const currentHash = computeSkillFolderHash(skillDir);
481
+ if (currentHash === entry.computedHash) {
482
+ console.log(` ⏭ ${name} (already up to date)`);
483
+ skipped++;
484
+ continue;
485
+ }
486
+ }
487
+
488
+ const parts = entry.source.split("/");
489
+ if (parts.length < 2 || entry.sourceType !== "github") {
490
+ console.warn(
491
+ ` ⚠ ${name}: unsupported sourceType "${entry.sourceType}", skipping`
492
+ );
493
+ failed++;
494
+ continue;
495
+ }
496
+
497
+ const [owner, repo] = parts;
498
+ let skillSlug = name;
499
+ if (entry.skillPath) {
500
+ const pathParts = entry.skillPath.split("/");
501
+ if (pathParts.length >= 2) {
502
+ skillSlug = pathParts[pathParts.length - 2];
503
+ }
504
+ }
505
+
506
+ try {
507
+ console.log(` 📥 Installing ${name}...`);
508
+ const download = await this.fetchSkillFromApi(owner, repo, skillSlug);
509
+ const files = download.files;
510
+
511
+ fs.mkdirSync(skillDir, { recursive: true });
512
+ for (const file of files) {
513
+ const filePath = path.join(skillDir, file.path);
514
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
515
+ fs.writeFileSync(filePath, file.contents, "utf-8");
516
+ }
517
+
518
+ const actualHash = computeSkillApiHash(files);
519
+ if (actualHash !== entry.computedHash) {
520
+ console.warn(
521
+ ` ⚠ Hash mismatch for ${name}: locked ${entry.computedHash.slice(0, 16)}..., got ${actualHash.slice(0, 16)}...`
522
+ );
523
+ } else {
524
+ console.log(` ✓ Hash verified`);
525
+ }
526
+
527
+ console.log(` ✅ ${name}`);
528
+ installed++;
529
+ } catch (e) {
530
+ const msg = e instanceof Error ? e.message : String(e);
531
+ console.error(` ❌ ${name}: ${msg}`);
532
+ failed++;
533
+ }
534
+ }
535
+
536
+ console.log(
537
+ `\nDone: ${installed} installed, ${skipped} skipped, ${failed} failed.`
538
+ );
539
+ }
540
+
541
+ /**
542
+ * Update installed skills to their latest version.
543
+ */
544
+ async update(skillNames?: string[]): Promise<void> {
545
+ const lock = this.readLock();
546
+ const toUpdate =
547
+ skillNames && skillNames.length > 0
548
+ ? skillNames
549
+ : Object.keys(lock.skills);
550
+
551
+ if (toUpdate.length === 0) {
552
+ console.log("No skills to update.");
553
+ return;
554
+ }
555
+
556
+ const installDir = this.getInstallDir();
557
+ console.log(`🔄 Updating ${toUpdate.length} skill(s)...\n`);
558
+ let updated = 0;
559
+ let unchanged = 0;
560
+ let failed = 0;
561
+
562
+ for (const name of toUpdate) {
563
+ const entry = lock.skills[name];
564
+ if (!entry) {
565
+ console.warn(` ⚠ "${name}" not found in lock file, skipping.`);
566
+ failed++;
567
+ continue;
568
+ }
569
+
570
+ const parts = entry.source.split("/");
571
+ if (parts.length < 2 || entry.sourceType !== "github") {
572
+ console.warn(` ⚠ ${name}: unsupported sourceType, skipping`);
573
+ failed++;
574
+ continue;
575
+ }
576
+
577
+ const [owner, repo] = parts;
578
+ let skillSlug = name;
579
+ if (entry.skillPath) {
580
+ const pathParts = entry.skillPath.split("/");
581
+ if (pathParts.length >= 2) {
582
+ skillSlug = pathParts[pathParts.length - 2];
583
+ }
584
+ }
585
+
586
+ try {
587
+ const download = await this.fetchSkillFromApi(owner, repo, skillSlug);
588
+ const newHash = computeSkillApiHash(download.files);
589
+
590
+ if (newHash === entry.computedHash) {
591
+ console.log(` ⏭ ${name} (already up to date)`);
592
+ unchanged++;
593
+ continue;
594
+ }
595
+
596
+ const skillDir = path.join(installDir, name);
597
+ fs.mkdirSync(skillDir, { recursive: true });
598
+ for (const file of download.files) {
599
+ const filePath = path.join(skillDir, file.path);
600
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
601
+ fs.writeFileSync(filePath, file.contents, "utf-8");
602
+ }
603
+
604
+ lock.skills[name] = { ...entry, computedHash: newHash };
605
+ console.log(` ✅ ${name} (updated)`);
606
+ updated++;
607
+ } catch (e) {
608
+ const msg = e instanceof Error ? e.message : String(e);
609
+ console.error(` ❌ ${name}: ${msg}`);
610
+ failed++;
611
+ }
612
+ }
613
+
614
+ if (updated > 0) {
615
+ this.writeLock(lock);
616
+ }
617
+
618
+ console.log(
619
+ `\nDone: ${updated} updated, ${unchanged} unchanged, ${failed} failed.`
620
+ );
621
+ }
622
+ }
@@ -14,6 +14,7 @@ import {
14
14
  } from "./types";
15
15
  import { PluginService } from "../plugins/plugins";
16
16
  import { McpService } from "./Mcp";
17
+ import { BehaviorsService } from "./BehaviorsService";
17
18
 
18
19
  export interface ToolContext {
19
20
  Agents?: AgentService;
@@ -22,6 +23,7 @@ export interface ToolContext {
22
23
  Tools?: ToolsService;
23
24
  Plugins?: PluginService;
24
25
  Mcp?: McpService;
26
+ Behaviors?: BehaviorsService;
25
27
  metadata?: { [key: string]: any };
26
28
  }
27
29
 
@@ -7,13 +7,14 @@ import { KnowhowSimpleClient } from "./KnowhowClient";
7
7
  import { McpService } from "./Mcp";
8
8
  import { S3Service } from "./S3";
9
9
  import { ToolsService } from "./Tools";
10
- import { PluginService } from "../plugins/plugins";
10
+ import { CliPluginService, PluginService } from "../plugins/plugins";
11
11
  import { DockerService } from "./DockerService";
12
12
  import { AgentSyncKnowhowWeb } from "./AgentSyncKnowhowWeb";
13
13
  import { AgentSyncFs } from "./AgentSyncFs";
14
14
  import { SessionManager } from "./SessionManager";
15
15
  import { TaskRegistry } from "./TaskRegistry";
16
16
  import { MediaProcessorService } from "./MediaProcessorService";
17
+ import { BehaviorsService } from "./BehaviorsService";
17
18
 
18
19
  import { ConversionService } from "./conversion/ConversionService";
19
20
 
@@ -38,6 +39,7 @@ export * from "./SyncerService";
38
39
  export * from "./watchers";
39
40
  export { Clients } from "../clients";
40
41
  export * from "./conversion";
42
+ export { BehaviorsService } from "./BehaviorsService";
41
43
 
42
44
  let Singletons = {} as {
43
45
  Tools: ToolsService;
@@ -53,6 +55,7 @@ let Singletons = {} as {
53
55
  Clients: AIClient;
54
56
  MediaProcessor: MediaProcessorService;
55
57
  Conversion: ConversionService;
58
+ Behaviors: BehaviorsService;
56
59
  };
57
60
 
58
61
  export const services = (): typeof Singletons => {
@@ -61,8 +64,9 @@ export const services = (): typeof Singletons => {
61
64
  const Events = new EventService();
62
65
  const Agents = new AgentService(Tools, Events);
63
66
  const MediaProcessor = new MediaProcessorService(Clients);
67
+ const Behaviors = new BehaviorsService();
64
68
  const Conversion = new ConversionService(Clients, MediaProcessor);
65
- const Plugins = new PluginService({
69
+ const Plugins = new CliPluginService({
66
70
  Agents,
67
71
  Events,
68
72
  Tools,
@@ -83,6 +87,7 @@ export const services = (): typeof Singletons => {
83
87
  Plugins,
84
88
  Tools,
85
89
  knowhowApiClient: new KnowhowSimpleClient(),
90
+ Behaviors,
86
91
  };
87
92
 
88
93
  Singletons.Tools.setContext({
@@ -90,6 +95,7 @@ export const services = (): typeof Singletons => {
90
95
  Events,
91
96
  Plugins,
92
97
  Clients,
98
+ Behaviors,
93
99
  });
94
100
  }
95
101
 
@@ -13,6 +13,7 @@ import { MediaProcessorService } from "../MediaProcessorService";
13
13
  import { TunnelHandler } from "@tyvm/knowhow-tunnel";
14
14
  import { EventService } from "../EventService";
15
15
  import { ConversionService } from "../conversion/ConversionService";
16
+ import { BehaviorsService } from "../BehaviorsService";
16
17
 
17
18
  /*
18
19
  *
@@ -61,6 +62,7 @@ export interface ModuleContext {
61
62
  Conversion?: ConversionService;
62
63
  Tunnel?: TunnelHandler;
63
64
  Program?: Command;
65
+ Behaviors?: BehaviorsService
64
66
  }
65
67
 
66
68
  export interface KnowhowModule {