@tyvm/knowhow 0.0.130 → 0.0.132

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 (97) hide show
  1. package/package.json +1 -1
  2. package/src/agents/configurable/ConfigAgent.ts +0 -1
  3. package/src/agents/tools/startAgentTask.ts +17 -13
  4. package/src/ai.ts +55 -24
  5. package/src/chat/CliChatService.ts +7 -1
  6. package/src/chat/modules/AgentModule.ts +72 -14
  7. package/src/chat/renderer/PlainRenderer.ts +218 -0
  8. package/src/chat/renderer/index.ts +1 -0
  9. package/src/chat/renderer/loadRenderer.ts +2 -0
  10. package/src/chat.ts +2 -2
  11. package/src/cli.ts +23 -0
  12. package/src/clients/anthropic.ts +9 -1
  13. package/src/clients/fireworks.ts +10 -1
  14. package/src/clients/index.ts +6 -3
  15. package/src/clients/meta.ts +9 -0
  16. package/src/clients/pricing/anthropic.ts +11 -0
  17. package/src/clients/pricing/fireworks.ts +12 -0
  18. package/src/commands/agent.ts +53 -20
  19. package/src/commands/agents.ts +556 -0
  20. package/src/commands/hash.ts +170 -0
  21. package/src/commands/misc.ts +157 -3
  22. package/src/commands/skills.ts +36 -0
  23. package/src/config.ts +54 -2
  24. package/src/generate.ts +507 -0
  25. package/src/index.ts +32 -216
  26. package/src/services/BehaviorsService.ts +32 -0
  27. package/src/services/SkillsService.ts +198 -0
  28. package/src/types.ts +36 -4
  29. package/ts_build/package.json +1 -1
  30. package/ts_build/src/agents/configurable/ConfigAgent.js.map +1 -1
  31. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -2
  32. package/ts_build/src/agents/tools/startAgentTask.js +13 -12
  33. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  34. package/ts_build/src/ai.d.ts +10 -4
  35. package/ts_build/src/ai.js +27 -25
  36. package/ts_build/src/ai.js.map +1 -1
  37. package/ts_build/src/chat/CliChatService.d.ts +1 -1
  38. package/ts_build/src/chat/CliChatService.js +4 -1
  39. package/ts_build/src/chat/CliChatService.js.map +1 -1
  40. package/ts_build/src/chat/modules/AgentModule.js +61 -4
  41. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  42. package/ts_build/src/chat/renderer/PlainRenderer.d.ts +28 -0
  43. package/ts_build/src/chat/renderer/PlainRenderer.js +169 -0
  44. package/ts_build/src/chat/renderer/PlainRenderer.js.map +1 -0
  45. package/ts_build/src/chat/renderer/index.d.ts +1 -0
  46. package/ts_build/src/chat/renderer/index.js +3 -1
  47. package/ts_build/src/chat/renderer/index.js.map +1 -1
  48. package/ts_build/src/chat/renderer/loadRenderer.js +2 -0
  49. package/ts_build/src/chat/renderer/loadRenderer.js.map +1 -1
  50. package/ts_build/src/chat.d.ts +1 -1
  51. package/ts_build/src/chat.js +2 -2
  52. package/ts_build/src/chat.js.map +1 -1
  53. package/ts_build/src/cli.js +16 -0
  54. package/ts_build/src/cli.js.map +1 -1
  55. package/ts_build/src/clients/anthropic.js +4 -1
  56. package/ts_build/src/clients/anthropic.js.map +1 -1
  57. package/ts_build/src/clients/fireworks.js +7 -0
  58. package/ts_build/src/clients/fireworks.js.map +1 -1
  59. package/ts_build/src/clients/index.js +5 -1
  60. package/ts_build/src/clients/index.js.map +1 -1
  61. package/ts_build/src/clients/meta.d.ts +2 -0
  62. package/ts_build/src/clients/meta.js +5 -0
  63. package/ts_build/src/clients/meta.js.map +1 -1
  64. package/ts_build/src/clients/pricing/anthropic.d.ts +3 -0
  65. package/ts_build/src/clients/pricing/anthropic.js +6 -0
  66. package/ts_build/src/clients/pricing/anthropic.js.map +1 -1
  67. package/ts_build/src/clients/pricing/fireworks.d.ts +1 -0
  68. package/ts_build/src/clients/pricing/fireworks.js +5 -1
  69. package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
  70. package/ts_build/src/commands/agent.js +44 -17
  71. package/ts_build/src/commands/agent.js.map +1 -1
  72. package/ts_build/src/commands/agents.d.ts +2 -0
  73. package/ts_build/src/commands/agents.js +460 -0
  74. package/ts_build/src/commands/agents.js.map +1 -0
  75. package/ts_build/src/commands/hash.d.ts +2 -0
  76. package/ts_build/src/commands/hash.js +150 -0
  77. package/ts_build/src/commands/hash.js.map +1 -0
  78. package/ts_build/src/commands/misc.js +108 -2
  79. package/ts_build/src/commands/misc.js.map +1 -1
  80. package/ts_build/src/commands/skills.js +24 -0
  81. package/ts_build/src/commands/skills.js.map +1 -1
  82. package/ts_build/src/config.js +37 -1
  83. package/ts_build/src/config.js.map +1 -1
  84. package/ts_build/src/generate.d.ts +16 -0
  85. package/ts_build/src/generate.js +304 -0
  86. package/ts_build/src/generate.js.map +1 -0
  87. package/ts_build/src/index.d.ts +1 -3
  88. package/ts_build/src/index.js +9 -118
  89. package/ts_build/src/index.js.map +1 -1
  90. package/ts_build/src/services/BehaviorsService.d.ts +2 -0
  91. package/ts_build/src/services/BehaviorsService.js +24 -0
  92. package/ts_build/src/services/BehaviorsService.js.map +1 -1
  93. package/ts_build/src/services/SkillsService.d.ts +7 -0
  94. package/ts_build/src/services/SkillsService.js +163 -0
  95. package/ts_build/src/services/SkillsService.js.map +1 -1
  96. package/ts_build/src/types.d.ts +14 -4
  97. package/ts_build/src/types.js.map +1 -1
package/src/index.ts CHANGED
@@ -1,14 +1,8 @@
1
1
  import { summarizeFiles, summarizeFile } from "./ai";
2
- import {
3
- saveAllFileHashes,
4
- saveHashes,
5
- getHashes,
6
- checkNoFilesChanged,
7
- } from "./hashes";
2
+ import type { AgentOptions } from "./ai";
8
3
  import * as fs from "fs";
9
4
  import * as path from "path";
10
5
  import * as crypto from "crypto";
11
- import { promisify } from "util";
12
6
  import { globSync } from "glob";
13
7
 
14
8
  import { Prompts } from "./prompts";
@@ -37,12 +31,25 @@ import {
37
31
  getConfiguredEmbeddingMap,
38
32
  } from "./embeddings";
39
33
 
40
- import { abort } from "process";
41
34
  import { convertToText } from "./conversion";
42
35
  import { knowhowMcpClient } from "./services/Mcp";
43
36
  import { services } from "./services/";
44
37
  import { Models } from "./types";
45
38
 
39
+ // ---------------------------------------------------------------------------
40
+ // Re-export generate pipeline (moved to src/generate.ts)
41
+ // ---------------------------------------------------------------------------
42
+ export {
43
+ generate,
44
+ GenerateOptions,
45
+ buildWaves,
46
+ normalizeInputPattern,
47
+ withOutputTarget,
48
+ writeAgentOrSummaryOutput,
49
+ handleMultiOutputGeneration,
50
+ handleSingleOutputGeneration,
51
+ } from "./generate";
52
+
46
53
  export * as clients from "./clients";
47
54
  export * as agents from "./agents";
48
55
  export * as services from "./services";
@@ -65,8 +72,11 @@ export { SkillsPlugin } from "./plugins/SkillsPlugin";
65
72
  // Export embedding types
66
73
  export { MinimalEmbedding, Embeddable } from "./types";
67
74
 
75
+ // ---------------------------------------------------------------------------
76
+ // Embed
77
+ // ---------------------------------------------------------------------------
78
+
68
79
  export async function embed() {
69
- // load config
70
80
  const config = await getConfig();
71
81
  const ignorePattern = await getIgnorePattern();
72
82
 
@@ -74,7 +84,6 @@ export async function embed() {
74
84
  config.embeddingModel || EmbeddingModels.openai.EmbeddingAda2;
75
85
 
76
86
  if (!config.embedSources) {
77
- // No embeddings configured
78
87
  return;
79
88
  }
80
89
 
@@ -83,6 +92,10 @@ export async function embed() {
83
92
  }
84
93
  }
85
94
 
95
+ // ---------------------------------------------------------------------------
96
+ // Purge
97
+ // ---------------------------------------------------------------------------
98
+
86
99
  export async function purge(globPath: string) {
87
100
  const files = globSync(globPath);
88
101
  const embeddings = await getConfiguredEmbeddingMap();
@@ -109,6 +122,10 @@ export async function purge(globPath: string) {
109
122
  }
110
123
  }
111
124
 
125
+ // ---------------------------------------------------------------------------
126
+ // Upload
127
+ // ---------------------------------------------------------------------------
128
+
112
129
  export async function upload() {
113
130
  const config = await getConfig();
114
131
  const { AwsS3, Embeddings, knowhowApiClient } = services();
@@ -126,10 +143,6 @@ export async function upload() {
126
143
  }
127
144
  const items = JSON.parse(await readFile(source.output, "utf8"));
128
145
  const { name: embeddingName } = path.parse(source.output);
129
- const data = {
130
- embeddingName,
131
- items,
132
- };
133
146
 
134
147
  if (Embeddings.hasResolver(source.remoteType) && source.remoteType !== "knowhow") {
135
148
  console.log(
@@ -144,7 +157,6 @@ export async function upload() {
144
157
  if (!source.remoteId) {
145
158
  throw new Error("remoteId is required for knowhow uploads");
146
159
  }
147
- // Warn if the local embeddingModel differs from the one stored on the backend
148
160
  try {
149
161
  const remoteEmbedding = await knowhowApiClient.getOrgEmbedding(source.remoteId);
150
162
  const localModel = config.embeddingModel || EmbeddingModels.openai.EmbeddingAda2;
@@ -159,12 +171,11 @@ export async function upload() {
159
171
  );
160
172
  }
161
173
  } catch (e) {
162
- // Non-fatal — don't block upload if metadata fetch fails
174
+ // Non-fatal
163
175
  }
164
176
  const url = await knowhowApiClient.getPresignedUploadUrl(source);
165
177
  console.log("Uploading to", url);
166
178
  await AwsS3.uploadToPresignedUrl(url, source.output);
167
- // Sync config metadata back to the backend DB
168
179
  await knowhowApiClient.updateEmbeddingMetadata(source.remoteId, {
169
180
  inputGlob: source.input,
170
181
  outputPath: source.output,
@@ -183,201 +194,9 @@ export async function upload() {
183
194
  }
184
195
  }
185
196
 
186
- /**
187
- * Normalizes an input pattern to a valid glob pattern.
188
- * Supports:
189
- * - Standard glob patterns (e.g. "src/**\/*.ts")
190
- * - Brace expansion (e.g. "{src/a.ts,src/b.ts}")
191
- * - Comma-separated file paths (e.g. "src/a.ts,src/b.ts") — auto-converted to brace expansion
192
- * - Mixed comma-separated list with globs (e.g. "src/a.ts,src/commands/**\/*.ts")
193
- */
194
- function normalizeInputPattern(input: string): string {
195
- // If it already has braces, use as-is (already brace-expanded)
196
- if (input.includes("{")) {
197
- return input;
198
- }
199
- // If it contains commas, treat as comma-separated list and wrap in braces
200
- // This also handles the mixed case: "src/a.ts,src/commands/**/*.ts"
201
- if (input.includes(",")) {
202
- const parts = input.split(",").map((p) => p.trim());
203
- return `{${parts.join(",")}}`;
204
- }
205
- return input;
206
- }
207
-
208
- export async function generate(): Promise<void> {
209
- const config = await getConfig();
210
- for (const source of config.sources) {
211
- console.log("Generating", source.input, "to", source.output);
212
- if (source.kind === "file" || !source.kind) {
213
- const files = globSync(normalizeInputPattern(source.input));
214
- const prompt = await loadPrompt(source.prompt);
215
-
216
- if (source.output.endsWith("/")) {
217
- await handleMultiOutputGeneration(
218
- source.model,
219
- source.input,
220
- files,
221
- prompt,
222
- source.output,
223
- source.outputExt,
224
- source.outputName,
225
- source.kind,
226
- source.agent
227
- );
228
- } else {
229
- await handleSingleOutputGeneration(
230
- source.model,
231
- files,
232
- prompt,
233
- source.output,
234
- source.kind,
235
- source.agent
236
- );
237
- }
238
- } else {
239
- await handleAllKindsGeneration(source);
240
- }
241
- }
242
- }
243
-
244
- async function handleAllKindsGeneration(source: GenerationSource) {
245
- const { Plugins } = services();
246
- const { kind, input } = source;
247
- if (Plugins.isPlugin(kind)) {
248
- const data = await Plugins.call(kind, input);
249
- if (source.output.endsWith("/")) {
250
- throw new Error(`Plugin ${kind} can only output to a single file`);
251
- }
252
- await writeFile(source.output, data);
253
- }
254
- return handleFileKindGeneration(source);
255
- }
256
-
257
- async function handleFileKindGeneration(source: GenerationSource) {
258
- const prompt = await loadPrompt(source.prompt);
259
- const files = globSync(normalizeInputPattern(source.input));
260
- console.log("Analyzing files: ", files);
261
-
262
- if (source.output.endsWith("/")) {
263
- await handleMultiOutputGeneration(
264
- source.model,
265
- source.input,
266
- files,
267
- prompt,
268
- source.output,
269
- source.outputExt,
270
- source.outputName,
271
- source.kind,
272
- source.agent
273
- );
274
- } else {
275
- await handleSingleOutputGeneration(
276
- source.model,
277
- files,
278
- prompt,
279
- source.output,
280
- source.kind,
281
- source.agent
282
- );
283
- }
284
- }
285
- export async function handleMultiOutputGeneration(
286
- model: string,
287
- inputPattern: string,
288
- files: string[],
289
- prompt: string,
290
- output: string,
291
- outputExt = "mdx",
292
- outputName?: string,
293
- kind?: string,
294
- agent?: string
295
- ) {
296
- // get the hash of the prompt
297
- const promptHash = crypto.createHash("md5").update(prompt).digest("hex");
298
-
299
- // get the files matching the input pattern
300
- const hashes = await getHashes();
301
-
302
- const inputPath = inputPattern.includes("**")
303
- ? inputPattern.split("**")[0]
304
- : "";
305
-
306
- for (const file of files) {
307
- // get the hash of the file
308
- const fileContent = await convertToText(file);
309
- const fileHash = crypto.createHash("md5").update(fileContent).digest("hex");
310
-
311
- if (!hashes[file]) {
312
- hashes[file] = { promptHash: "", fileHash: "" };
313
- }
314
-
315
- // write the summary to the output file
316
- const { name, ext, dir } = path.parse(file);
317
- const nestedFolder = inputPath ? (dir + "/").replace(inputPath, "") : "";
318
- const outputFolder = path.join(output, nestedFolder);
319
-
320
- if (!fs.existsSync(outputFolder)) {
321
- fs.mkdirSync(outputFolder, { recursive: true });
322
- }
323
-
324
- const outputFileName = outputName || name;
325
- const outputFile = path.join(
326
- outputFolder,
327
- outputFileName + "." + outputExt
328
- );
329
- console.log({ dir, inputPath, nestedFolder, outputFile });
330
-
331
- const toCheck = [file, outputFile];
332
- const noChanges = await checkNoFilesChanged(toCheck, promptHash, hashes);
333
- if (noChanges) {
334
- console.log("Skipping file", file, "because it hasn't changed");
335
- continue;
336
- }
337
-
338
- // summarize the file
339
- console.log("Summarizing", file);
340
- const summary = prompt
341
- ? await summarizeFile(file, prompt, model, agent)
342
- : fileContent;
343
-
344
- console.log("Writing summary to", outputFile);
345
- await writeFile(outputFile, summary);
346
-
347
- await saveAllFileHashes(toCheck, promptHash);
348
- }
349
- }
350
-
351
- export async function handleSingleOutputGeneration(
352
- model: string,
353
- files: string[],
354
- prompt: string,
355
- outputFile: string,
356
- kind?: string,
357
- agent?: string
358
- ) {
359
- const hashes = await getHashes();
360
- const promptHash = crypto.createHash("md5").update(prompt).digest("hex");
361
-
362
- const filesToCheck = [outputFile, ...files];
363
- const noChanges = await checkNoFilesChanged(filesToCheck, promptHash, hashes);
364
- if (noChanges) {
365
- console.log(`Skipping ${files.length} files because they haven't changed`);
366
- return;
367
- }
368
-
369
- console.log("Summarizing", files.length, "files");
370
- const summary = prompt
371
- ? await summarizeFiles(files, prompt, model, agent)
372
- : (await Promise.all(files.map(convertToText))).join("\n\n");
373
-
374
- const fileHash = crypto.createHash("md5").update(summary).digest("hex");
375
-
376
- console.log("Writing summary to", outputFile);
377
- await writeFile(outputFile, summary);
378
-
379
- await saveAllFileHashes(filesToCheck, promptHash);
380
- }
197
+ // ---------------------------------------------------------------------------
198
+ // Download
199
+ // ---------------------------------------------------------------------------
381
200
 
382
201
  export async function download() {
383
202
  const config = await getConfig();
@@ -421,10 +240,7 @@ export async function download() {
421
240
  "to",
422
241
  destinationPath
423
242
  );
424
- const preSignedUrl = await knowhowApiClient.getPresignedDownloadUrl(
425
- source
426
- );
427
- // Ensure output directory exists
243
+ const preSignedUrl = await knowhowApiClient.getPresignedDownloadUrl(source);
428
244
  const outputDir = path.dirname(destinationPath);
429
245
  if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
430
246
  await AwsS3.downloadFromPresignedUrl(preSignedUrl, destinationPath);
@@ -190,6 +190,38 @@ export class BehaviorsService {
190
190
  return null;
191
191
  }
192
192
 
193
+ /**
194
+ * Find a behavior from local disk by its ID field.
195
+ */
196
+ findBehaviorById(id: string): Behavior | null {
197
+ const behaviors = this.loadLocal();
198
+ return behaviors.find((b) => b.id === id) ?? null;
199
+ }
200
+
201
+ /**
202
+ * Load a behavior directly from a file path (.json or .md).
203
+ */
204
+ loadBehaviorFromFile(filePath: string): Behavior | null {
205
+ const resolved = path.isAbsolute(filePath)
206
+ ? filePath
207
+ : path.resolve(process.cwd(), filePath);
208
+
209
+ if (!fs.existsSync(resolved)) {
210
+ return null;
211
+ }
212
+
213
+ try {
214
+ const raw = fs.readFileSync(resolved, "utf-8");
215
+ if (resolved.endsWith(".md")) {
216
+ return this.parseMdBehavior(raw);
217
+ } else {
218
+ return JSON.parse(raw) as Behavior;
219
+ }
220
+ } catch {
221
+ return null;
222
+ }
223
+ }
224
+
193
225
  async list(opts: { skillsOnly?: boolean; includeInternal?: boolean } = {}) {
194
226
  const behaviors = opts.skillsOnly
195
227
  ? await this.client.getOrgSkills(opts.includeInternal ?? true)
@@ -3,6 +3,7 @@ import * as path from "path";
3
3
  import * as os from "os";
4
4
  import { createHash } from "crypto";
5
5
  import * as https from "https";
6
+ import { KnowhowSimpleClient } from "./KnowhowClient";
6
7
 
7
8
  // Default local install dir (relative to cwd)
8
9
  const LOCAL_SKILLS_DIR = ".knowhow/skills";
@@ -236,6 +237,7 @@ export class SkillsService {
236
237
  private cwd: string;
237
238
  private isGlobal: boolean;
238
239
  private customInstallDir?: string;
240
+ public client = new KnowhowSimpleClient();
239
241
 
240
242
  constructor(opts: SkillsServiceOptions = {}) {
241
243
  this.cwd = opts.cwd ?? process.cwd();
@@ -619,4 +621,200 @@ export class SkillsService {
619
621
  `\nDone: ${updated} updated, ${unchanged} unchanged, ${failed} failed.`
620
622
  );
621
623
  }
624
+
625
+ /**
626
+ * Upload locally installed skills (from .knowhow/skills/<name>/SKILL.md) to the backend as behaviors.
627
+ * Each skill is upserted: created if it has no id in frontmatter, updated if it does.
628
+ */
629
+ async upload(): Promise<void> {
630
+ const installDir = this.getInstallDir();
631
+
632
+ if (!fs.existsSync(installDir)) {
633
+ console.error(
634
+ `No skills directory found at ${installDir}. Run 'knowhow skills add <ref>' first.`
635
+ );
636
+ return;
637
+ }
638
+
639
+ const entries = fs.readdirSync(installDir, { withFileTypes: true });
640
+ const skillDirs = entries.filter((e) => e.isDirectory());
641
+
642
+ if (skillDirs.length === 0) {
643
+ console.log("No skill directories found. Nothing to upload.");
644
+ return;
645
+ }
646
+
647
+ let uploaded = 0;
648
+ let failed = 0;
649
+
650
+ for (const dir of skillDirs) {
651
+ const skillFile = path.join(installDir, dir.name, "SKILL.md");
652
+ if (!fs.existsSync(skillFile)) {
653
+ console.warn(` ⚠ ${dir.name}: no SKILL.md found, skipping.`);
654
+ continue;
655
+ }
656
+
657
+ try {
658
+ const raw = fs.readFileSync(skillFile, "utf-8");
659
+ const behavior = this.parseSkillMd(raw, dir.name);
660
+
661
+ // Always mark as a skill
662
+ const payload: Record<string, unknown> = {
663
+ ...behavior,
664
+ isSkill: true,
665
+ };
666
+
667
+ if (behavior.id) {
668
+ await this.client.updateOrgBehavior(behavior.id as string, payload);
669
+ console.log(` 📝 Updated: ${behavior.name} (${behavior.id as string})`);
670
+ } else {
671
+ const result = await this.client.createOrgBehavior(payload);
672
+ const created = result.data as Record<string, unknown>;
673
+ if (created.id) {
674
+ // Write the assigned id back into the SKILL.md frontmatter so future uploads update instead of create
675
+ const withId = this.injectIdIntoSkillMd(raw, created.id as string);
676
+ fs.writeFileSync(skillFile, withId, "utf-8");
677
+ console.log(` ✨ Created: ${behavior.name} (id: ${created.id})`);
678
+ } else {
679
+ console.log(` ✨ Created: ${behavior.name}`);
680
+ }
681
+ }
682
+ uploaded++;
683
+ } catch (e) {
684
+ const msg = e instanceof Error ? e.message : String(e);
685
+ console.error(` ❌ ${dir.name}: ${msg}`);
686
+ failed++;
687
+ }
688
+ }
689
+
690
+ console.log(`\nUploaded ${uploaded} skill(s). Failed: ${failed}.`);
691
+ }
692
+
693
+ /**
694
+ * Download skill behaviors from the backend and write them as SKILL.md files
695
+ * into the skills install directory (.knowhow/skills/<name>/SKILL.md).
696
+ */
697
+ async download(): Promise<void> {
698
+ const installDir = this.getInstallDir();
699
+ const result = await this.client.getOrgSkills(false); // false = exclude platform/internal skills
700
+ const skills = result.data as Record<string, unknown>[];
701
+
702
+ if (skills.length === 0) {
703
+ console.log("No skills found on backend.");
704
+ return;
705
+ }
706
+
707
+ let created = 0;
708
+ let updated = 0;
709
+ let unchanged = 0;
710
+
711
+ for (const skill of skills) {
712
+ const name = ((skill.name as string) || "unnamed")
713
+ .toLowerCase()
714
+ .replace(/[^a-z0-9-]+/g, "-");
715
+ const skillDir = path.join(installDir, name);
716
+ fs.mkdirSync(skillDir, { recursive: true });
717
+
718
+ const skillFile = path.join(skillDir, "SKILL.md");
719
+ const content = this.serializeSkillMd(skill);
720
+
721
+ const hash = (s: string) => createHash("sha256").update(s).digest("hex");
722
+ if (!fs.existsSync(skillFile)) {
723
+ fs.writeFileSync(skillFile, content, "utf-8");
724
+ console.log(` ✨ Created: ${skillFile}`);
725
+ created++;
726
+ } else {
727
+ const existing = fs.readFileSync(skillFile, "utf-8");
728
+ if (hash(existing) !== hash(content)) {
729
+ fs.writeFileSync(skillFile, content, "utf-8");
730
+ console.log(` 📝 Updated: ${skillFile}`);
731
+ updated++;
732
+ } else {
733
+ unchanged++;
734
+ }
735
+ }
736
+ }
737
+
738
+ console.log(
739
+ `\nSync complete: ${created} created, ${updated} updated, ${unchanged} unchanged → ${installDir}/`
740
+ );
741
+ }
742
+
743
+ /**
744
+ * Parse a SKILL.md file into a behavior object.
745
+ * Falls back to using the directory name if name is missing from frontmatter.
746
+ */
747
+ private parseSkillMd(content: string, fallbackName: string): Record<string, unknown> {
748
+ const match = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
749
+ if (!match) {
750
+ return { name: fallbackName, description: "", instructions: content.trim(), isSkill: true };
751
+ }
752
+
753
+ const meta: Record<string, string> = {};
754
+ for (const line of match[1].split("\n")) {
755
+ const colonIdx = line.indexOf(":");
756
+ if (colonIdx > 0) {
757
+ const key = line.slice(0, colonIdx).trim();
758
+ const val = line.slice(colonIdx + 1).trim();
759
+ meta[key] = val;
760
+ }
761
+ }
762
+
763
+ return {
764
+ id: meta.id || undefined,
765
+ name: meta.name || fallbackName,
766
+ description: meta.description || "",
767
+ instructions: match[2].trim(),
768
+ textTrigger: meta.textTrigger || undefined,
769
+ semanticTriggerText: meta.semanticTriggerText || undefined,
770
+ model: meta.model || undefined,
771
+ mcpServers: meta.mcpServers || undefined,
772
+ tools: meta.tools || undefined,
773
+ embeddings: meta.embeddings || undefined,
774
+ isSkill: true,
775
+ isPublic: meta.isPublic === "true",
776
+ };
777
+ }
778
+
779
+ /**
780
+ * Serialize a backend behavior record as a SKILL.md string.
781
+ */
782
+ private serializeSkillMd(skill: Record<string, unknown>): string {
783
+ const lines: string[] = ["---"];
784
+ lines.push(`name: ${skill.name || ""}`);
785
+ lines.push(`description: ${skill.description || ""}`);
786
+ if (skill.model) lines.push(`model: ${skill.model}`);
787
+ if (skill.textTrigger) lines.push(`textTrigger: ${skill.textTrigger}`);
788
+ if (skill.semanticTriggerText) lines.push(`semanticTriggerText: ${skill.semanticTriggerText}`);
789
+ if (skill.mcpServers) lines.push(`mcpServers: ${skill.mcpServers}`);
790
+ if (skill.tools) lines.push(`tools: ${skill.tools}`);
791
+ if (skill.embeddings) lines.push(`embeddings: ${skill.embeddings}`);
792
+ if (skill.isPublic !== undefined) lines.push(`isPublic: ${skill.isPublic}`);
793
+ if (skill.id) lines.push(`id: ${skill.id}`);
794
+ lines.push("---");
795
+ lines.push("");
796
+ lines.push(((skill.instructions as string) || "").trim());
797
+ lines.push("");
798
+ return lines.join("\n");
799
+ }
800
+
801
+ /**
802
+ * Inject or replace the `id:` field in a SKILL.md frontmatter.
803
+ */
804
+ private injectIdIntoSkillMd(content: string, id: string): string {
805
+ const match = content.match(/^(---\n[\s\S]*?\n---\n?)([\s\S]*)$/);
806
+ if (!match) return content;
807
+
808
+ let frontmatter = match[1];
809
+ const body = match[2];
810
+
811
+ if (/^id:/m.test(frontmatter)) {
812
+ frontmatter = frontmatter.replace(/^id:.*$/m, `id: ${id}`);
813
+ } else {
814
+ // Insert before closing ---
815
+ frontmatter = frontmatter.replace(/\n---\n?$/, `\nid: ${id}\n---\n`);
816
+ }
817
+
818
+ return frontmatter + body;
819
+ }
622
820
  }
package/src/types.ts CHANGED
@@ -1,23 +1,55 @@
1
1
  import { AssistantTool } from "openai/resources/beta";
2
2
  import { WebSocket } from "ws";
3
3
 
4
+ /**
5
+ * Flexible hash store. Each key is either:
6
+ * - a file path (legacy): { fileHash, promptHash, [promptHash]: fileHash }
7
+ * - a named build slot (new): { [inputKey: string]: hashValue }
8
+ *
9
+ * Both formats use the same shape: an object of string→string pairs.
10
+ * `fileHash` and `promptHash` are just conventional keys in the legacy format.
11
+ */
4
12
  export type Hashes = {
5
- [file: string]: {
6
- [promptHash: string]: string;
7
- promptHash: string;
8
- fileHash: string;
13
+ [key: string]: {
14
+ [inputKey: string]: string;
9
15
  };
10
16
  };
11
17
 
18
+ /**
19
+ * A reference to an external JSON file containing an array of GenerationSource
20
+ * or EmbedSource entries. Used in knowhow.json `sources` / `embedSources` arrays
21
+ * to split a large config across multiple files:
22
+ * { "from": "./generate/autodoc.json" }
23
+ */
24
+ export type SourceRef = { from: string };
25
+
12
26
  export type GenerationSource = {
13
27
  model?: string;
14
28
  agent?: string;
29
+ name?: string;
15
30
  input: string;
16
31
  output: string;
17
32
  prompt: string;
18
33
  kind?: string;
19
34
  outputExt?: string;
20
35
  outputName?: string;
36
+ /** Pass to runAgentTask: enable .knowhow/processes/agents/<taskId> directory */
37
+ syncFs?: boolean;
38
+ /**
39
+ * Deterministic task ID for this source (used with syncFs).
40
+ * Defaults to "generate:<sanitized-output>" when syncFs is enabled.
41
+ */
42
+ taskId?: string;
43
+ /** Time limit in minutes for the agent (optional) */
44
+ maxTimeLimit?: number;
45
+ /** Cost limit in dollars for the agent (optional) */
46
+ maxSpendLimit?: number;
47
+ /**
48
+ * Names of other GenerationSource entries (by `name` field) that must
49
+ * complete before this source runs. Used for explicit dependency ordering
50
+ * when automatic I/O overlap detection is insufficient.
51
+ */
52
+ dependsOn?: string[];
21
53
  };
22
54
 
23
55
  export type EmbedSource = {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.130",
3
+ "version": "0.0.132",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"ConfigAgent.js","sourceRoot":"","sources":["../../../../src/agents/configurable/ConfigAgent.ts"],"names":[],"mappings":";;;AAAA,uCAAuD;AAMvD,MAAa,WAAY,SAAQ,gBAAS;IAIpB;IAHpB,IAAI,CAAS;IACb,WAAW,CAAS;IAEpB,YAAoB,MAAiB,EAAE,OAAqB;QAC1D,KAAK,CAAC,OAAO,CAAC,CAAC;QADG,WAAM,GAAN,MAAM,CAAW;QAEnC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,mBAAmB,CAAC;YACvB,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;SACnD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,SAAiB;QACxC,OAAO;YACL,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE;YACrD,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE;SACxB,CAAC;IACjB,CAAC;CACF;AAlBD,kCAkBC"}
1
+ {"version":3,"file":"ConfigAgent.js","sourceRoot":"","sources":["../../../../src/agents/configurable/ConfigAgent.ts"],"names":[],"mappings":";;;AAAA,uCAAuD;AAKvD,MAAa,WAAY,SAAQ,gBAAS;IAIpB;IAHpB,IAAI,CAAS;IACb,WAAW,CAAS;IAEpB,YAAoB,MAAiB,EAAE,OAAqB;QAC1D,KAAK,CAAC,OAAO,CAAC,CAAC;QADG,WAAM,GAAN,MAAM,CAAW;QAEnC,IAAI,CAAC,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,mBAAmB,CAAC;YACvB,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;SACnD,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,kBAAkB,CAAC,SAAiB;QACxC,OAAO;YACL,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,YAAY,EAAE;YACrD,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE;SACxB,CAAC;IACjB,CAAC;CACF;AAlBD,kCAkBC"}
@@ -1,5 +1,5 @@
1
1
  import { Tool } from "../../clients/types";
2
- interface StartAgentTaskParams {
2
+ export interface StartAgentTaskParams {
3
3
  messageId?: string;
4
4
  syncFs?: boolean;
5
5
  taskId?: string;
@@ -10,7 +10,7 @@ interface StartAgentTaskParams {
10
10
  agentName?: string;
11
11
  maxTimeLimit?: number;
12
12
  maxSpendLimit?: number;
13
+ waitForCompletion?: boolean;
13
14
  }
14
15
  export declare function startAgentTask(params: StartAgentTaskParams): Promise<string>;
15
16
  export declare const startAgentTaskDefinition: Tool;
16
- export {};