@tyvm/knowhow 0.0.109 → 0.0.110

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 (94) hide show
  1. package/autodoc/README.md +324 -0
  2. package/autodoc/chat-guide.md +268 -365
  3. package/autodoc/cli-reference.md +399 -473
  4. package/autodoc/config-reference.md +431 -330
  5. package/autodoc/embeddings-guide.md +223 -322
  6. package/autodoc/generate-guide.md +261 -301
  7. package/autodoc/language-plugin-guide.md +221 -247
  8. package/autodoc/modules-guide.md +242 -215
  9. package/autodoc/plugins-guide.md +470 -469
  10. package/autodoc/quickstart-guide.md +67 -70
  11. package/autodoc/skills-guide.md +455 -339
  12. package/autodoc/worker-guide.md +301 -308
  13. package/package.json +1 -1
  14. package/scripts/build-for-node.sh +10 -24
  15. package/src/agents/tools/list.ts +2 -2
  16. package/src/ai.ts +81 -37
  17. package/src/chat/CliChatService.ts +1 -1
  18. package/src/chat/modules/AgentModule.ts +7 -2
  19. package/src/chat/modules/SessionsModule.ts +40 -1
  20. package/src/chat/modules/SystemModule.ts +2 -2
  21. package/src/clients/anthropic.ts +1 -1
  22. package/src/clients/index.ts +25 -6
  23. package/src/clients/openai.ts +8 -5
  24. package/src/clients/types.ts +29 -6
  25. package/src/clients/withRetry.ts +89 -0
  26. package/src/commands/agent.ts +30 -0
  27. package/src/commands/modules.ts +417 -47
  28. package/src/config.ts +1 -1
  29. package/src/fileSync.ts +20 -12
  30. package/src/hashes.ts +43 -22
  31. package/src/index.ts +4 -2
  32. package/src/processors/Base64ImageDetector.ts +73 -0
  33. package/src/services/MediaProcessorService.ts +79 -10
  34. package/src/services/modules/index.ts +47 -18
  35. package/tests/processors/Base64ImageDetector.test.ts +160 -0
  36. package/tests/unit/clients/AIClient.test.ts +446 -0
  37. package/tests/unit/clients/withRetry.test.ts +319 -0
  38. package/tests/unit/commands/github-credentials.test.ts +1 -2
  39. package/ts_build/package.json +1 -1
  40. package/ts_build/src/agents/tools/list.js +2 -2
  41. package/ts_build/src/agents/tools/list.js.map +1 -1
  42. package/ts_build/src/ai.d.ts +3 -3
  43. package/ts_build/src/ai.js +51 -23
  44. package/ts_build/src/ai.js.map +1 -1
  45. package/ts_build/src/chat/CliChatService.js +1 -1
  46. package/ts_build/src/chat/CliChatService.js.map +1 -1
  47. package/ts_build/src/chat/modules/AgentModule.js +5 -2
  48. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  49. package/ts_build/src/chat/modules/SessionsModule.js +30 -1
  50. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  51. package/ts_build/src/chat/modules/SystemModule.js +2 -2
  52. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  53. package/ts_build/src/clients/anthropic.js +1 -1
  54. package/ts_build/src/clients/anthropic.js.map +1 -1
  55. package/ts_build/src/clients/index.js +7 -6
  56. package/ts_build/src/clients/index.js.map +1 -1
  57. package/ts_build/src/clients/openai.js +4 -4
  58. package/ts_build/src/clients/openai.js.map +1 -1
  59. package/ts_build/src/clients/types.d.ts +12 -6
  60. package/ts_build/src/clients/withRetry.d.ts +2 -0
  61. package/ts_build/src/clients/withRetry.js +60 -0
  62. package/ts_build/src/clients/withRetry.js.map +1 -0
  63. package/ts_build/src/commands/agent.js +25 -0
  64. package/ts_build/src/commands/agent.js.map +1 -1
  65. package/ts_build/src/commands/modules.js +359 -32
  66. package/ts_build/src/commands/modules.js.map +1 -1
  67. package/ts_build/src/config.js +1 -1
  68. package/ts_build/src/config.js.map +1 -1
  69. package/ts_build/src/fileSync.d.ts +2 -2
  70. package/ts_build/src/fileSync.js +13 -11
  71. package/ts_build/src/fileSync.js.map +1 -1
  72. package/ts_build/src/hashes.d.ts +2 -2
  73. package/ts_build/src/hashes.js +40 -16
  74. package/ts_build/src/hashes.js.map +1 -1
  75. package/ts_build/src/index.js +1 -1
  76. package/ts_build/src/index.js.map +1 -1
  77. package/ts_build/src/processors/Base64ImageDetector.d.ts +3 -0
  78. package/ts_build/src/processors/Base64ImageDetector.js +42 -0
  79. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  80. package/ts_build/src/services/MediaProcessorService.d.ts +5 -4
  81. package/ts_build/src/services/MediaProcessorService.js +53 -8
  82. package/ts_build/src/services/MediaProcessorService.js.map +1 -1
  83. package/ts_build/src/services/modules/index.js +35 -12
  84. package/ts_build/src/services/modules/index.js.map +1 -1
  85. package/ts_build/tests/processors/Base64ImageDetector.test.js +111 -0
  86. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  87. package/ts_build/tests/unit/clients/AIClient.test.d.ts +1 -0
  88. package/ts_build/tests/unit/clients/AIClient.test.js +339 -0
  89. package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -0
  90. package/ts_build/tests/unit/clients/withRetry.test.d.ts +1 -0
  91. package/ts_build/tests/unit/clients/withRetry.test.js +225 -0
  92. package/ts_build/tests/unit/clients/withRetry.test.js.map +1 -0
  93. package/ts_build/tests/unit/commands/github-credentials.test.js +1 -2
  94. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -1
@@ -5,6 +5,8 @@ import { AskModule } from "../chat/modules/AskModule";
5
5
  import { SearchModule } from "../chat/modules/SearchModule";
6
6
  import { SessionsModule } from "../chat/modules/SessionsModule";
7
7
  import { SetupModule } from "../chat/modules/SetupModule";
8
+ import { loadRenderer } from "../chat/renderer/loadRenderer";
9
+ import { getConfig } from "../config";
8
10
 
9
11
  async function readStdin(): Promise<string> {
10
12
  return new Promise((resolve) => {
@@ -25,6 +27,24 @@ async function readStdin(): Promise<string> {
25
27
  });
26
28
  }
27
29
 
30
+ /**
31
+ * Set up the renderer on the chat service context.
32
+ * Priority: CLI --renderer flag > config.chat.renderer > "basic" (ConsoleRenderer)
33
+ */
34
+ async function setupRenderer(chatService: any, rendererSpecifier: string): Promise<void> {
35
+ try {
36
+ const renderer = await loadRenderer(rendererSpecifier);
37
+ chatService.setContext({ renderer });
38
+ } catch (err: any) {
39
+ console.warn(`⚠ Could not load renderer "${rendererSpecifier}": ${err.message}`);
40
+ console.warn(" Falling back to basic renderer.");
41
+ try {
42
+ const fallback = await loadRenderer("basic");
43
+ chatService.setContext({ renderer: fallback });
44
+ } catch (_) {}
45
+ }
46
+ }
47
+
28
48
  export function addAgentCommand(program: Command, getChatService: () => any): void {
29
49
  program
30
50
  .command("agent")
@@ -57,11 +77,21 @@ export function addAgentCommand(program: Command, getChatService: () => any): vo
57
77
  "--resume",
58
78
  "Resume a previously started task using the --task-id (local FS or remote)"
59
79
  )
80
+ .option(
81
+ "--renderer <name>",
82
+ "Renderer to use: basic, compact, fancy, or a path/package (default: from config or basic)"
83
+ )
60
84
  .action(async (options) => {
61
85
  try {
62
86
  const { setupServices } = await import("./services");
63
87
  await setupServices();
64
88
  const chatService = getChatService();
89
+
90
+ // Set up renderer: CLI flag > config.chat.renderer > "basic"
91
+ let config: any = {};
92
+ try { config = await getConfig(); } catch (_) {}
93
+ const rendererSpecifier = options.renderer ?? config.chat?.renderer ?? "basic";
94
+ await setupRenderer(chatService, rendererSpecifier);
65
95
  const agentModule = new AgentModule();
66
96
 
67
97
  if (options.resume) {
@@ -1,6 +1,10 @@
1
1
  import { Command } from "commander";
2
2
  import { execSync } from "child_process";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import * as os from "os";
3
6
  import { getConfig, getGlobalConfig, updateConfig, updateGlobalConfig } from "../config";
7
+ import * as readline from "readline";
4
8
 
5
9
  // Default built-in modules that `knowhow modules setup` adds to the config.
6
10
  export const BUILTIN_MODULES = [
@@ -8,6 +12,272 @@ export const BUILTIN_MODULES = [
8
12
  "@tyvm/knowhow-module-terminal",
9
13
  ];
10
14
 
15
+ /**
16
+ * Returns the path to the .knowhow directory (used as npm install prefix).
17
+ * For global: ~/.knowhow
18
+ * For local: <cwd>/.knowhow
19
+ */
20
+ function getKnowhowDir(isGlobal: boolean): string {
21
+ if (isGlobal) {
22
+ return path.join(os.homedir(), ".knowhow");
23
+ }
24
+ return path.join(process.cwd(), ".knowhow");
25
+ }
26
+
27
+ /**
28
+ * Ensures the .knowhow directory has a minimal package.json so
29
+ * `npm install --prefix` works cleanly without polluting the project root.
30
+ */
31
+ function ensureKnowhowPackageJson(knowhowDir: string): void {
32
+ const pkgPath = path.join(knowhowDir, "package.json");
33
+ if (!fs.existsSync(pkgPath)) {
34
+ fs.mkdirSync(knowhowDir, { recursive: true });
35
+ fs.writeFileSync(
36
+ pkgPath,
37
+ JSON.stringify({ name: "knowhow-modules", private: true, version: "1.0.0" }, null, 2)
38
+ );
39
+ }
40
+ }
41
+
42
+ /**
43
+ * Run `npm install --prefix <knowhowDir> <mod>` so that modules land in
44
+ * .knowhow/node_modules rather than the project's node_modules.
45
+ */
46
+ function npmInstallToKnowhow(mod: string, knowhowDir: string): void {
47
+ execSync(`npm install --prefix "${knowhowDir}" ${mod}`, {
48
+ stdio: "inherit",
49
+ encoding: "utf-8",
50
+ });
51
+ }
52
+
53
+ /**
54
+ * Returns true if a module package is already installed in the given knowhow dir.
55
+ */
56
+ function isModuleInstalled(mod: string, knowhowDir: string): boolean {
57
+ try {
58
+ require.resolve(mod, {
59
+ paths: [path.join(knowhowDir, "node_modules"), knowhowDir],
60
+ });
61
+ return true;
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+
67
+ interface NpmRegistryInfo {
68
+ latestVersion: string;
69
+ publishedAt: string;
70
+ }
71
+
72
+ /**
73
+ * Fetch the latest version and publish time from the npm registry for a package.
74
+ * Returns null if the package info can't be fetched.
75
+ */
76
+ async function fetchNpmRegistryInfo(mod: string): Promise<NpmRegistryInfo | null> {
77
+ try {
78
+ // npm view <pkg> version time --json returns either a single value or array
79
+ const output = execSync(`npm view ${mod} version time --json`, {
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ encoding: "utf-8",
82
+ });
83
+ const parsed = JSON.parse(output.trim());
84
+ let latestVersion: string;
85
+ let timestamps: Record<string, string> = {};
86
+ if (Array.isArray(parsed)) {
87
+ latestVersion = parsed[0];
88
+ timestamps = parsed[1] || {};
89
+ } else if (parsed && typeof parsed === "object") {
90
+ latestVersion = parsed["version"] || "";
91
+ timestamps = parsed["time"] || {};
92
+ } else {
93
+ latestVersion = String(parsed);
94
+ }
95
+ const publishedAt = timestamps[latestVersion] || timestamps["modified"] || "";
96
+ return { latestVersion, publishedAt };
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Simple engine range checker — handles the common cases:
104
+ * ">=22" ">=22.0.0" "^20" "20.x" "*" ""
105
+ * Returns true if the given nodeVersion satisfies the range.
106
+ * Falls back to true (permissive) for unsupported range syntax.
107
+ */
108
+ function nodeSatisfiesRange(nodeVersion: string, range: string): boolean {
109
+ if (!range || range === "*" || range === "") return true;
110
+
111
+ // Parse "major.minor.patch" from e.g. "v20.17.0"
112
+ const vMatch = nodeVersion.replace(/^v/, "").match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
113
+ if (!vMatch) return true;
114
+ const vMajor = parseInt(vMatch[1], 10);
115
+ const vMinor = parseInt(vMatch[2] ?? "0", 10);
116
+ const vPatch = parseInt(vMatch[3] ?? "0", 10);
117
+ const vNum = vMajor * 1_000_000 + vMinor * 1_000 + vPatch;
118
+
119
+ function parseVersion(s: string): number {
120
+ const m = s.trim().match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
121
+ if (!m) return 0;
122
+ return parseInt(m[1], 10) * 1_000_000 + parseInt(m[2] ?? "0", 10) * 1_000 + parseInt(m[3] ?? "0", 10);
123
+ }
124
+
125
+ // Handle " || " — any segment satisfying is fine
126
+ if (range.includes("||")) {
127
+ return range.split("||").some((r) => nodeSatisfiesRange(nodeVersion, r.trim()));
128
+ }
129
+
130
+ // Handle space-separated AND conditions e.g. ">=14 <18"
131
+ const parts = range.trim().split(/\s+/);
132
+ for (const part of parts) {
133
+ const gteMatch = part.match(/^>=(.+)/);
134
+ const gtMatch = part.match(/^>(?!=)(.+)/);
135
+ const lteMatch = part.match(/^<=(.+)/);
136
+ const ltMatch = part.match(/^<(?!=)(.+)/);
137
+ const caretMatch = part.match(/^\^(\d+)/);
138
+ const tildeMatch = part.match(/^~(\d+)(?:\.(\d+))?/);
139
+ const exactMatch = part.match(/^(\d+(?:\.\d+)*)/);
140
+
141
+ if (gteMatch) {
142
+ if (vNum < parseVersion(gteMatch[1])) return false;
143
+ } else if (gtMatch) {
144
+ if (vNum <= parseVersion(gtMatch[1])) return false;
145
+ } else if (lteMatch) {
146
+ if (vNum > parseVersion(lteMatch[1])) return false;
147
+ } else if (ltMatch) {
148
+ if (vNum >= parseVersion(ltMatch[1])) return false;
149
+ } else if (caretMatch) {
150
+ const base = parseVersion(caretMatch[1]);
151
+ const baseMajor = parseInt(caretMatch[1], 10);
152
+ if (vNum < base || vMajor !== baseMajor) return false;
153
+ } else if (tildeMatch) {
154
+ const baseMajor = parseInt(tildeMatch[1], 10);
155
+ const baseMinor = parseInt(tildeMatch[2] ?? "0", 10);
156
+ const base = parseVersion(`${baseMajor}.${baseMinor}`);
157
+ const nextMinor = parseVersion(`${baseMajor}.${baseMinor + 1}`);
158
+ if (vNum < base || vNum >= nextMinor) return false;
159
+ } else if (exactMatch && !part.startsWith("v")) {
160
+ // e.g. "20.x" or "20"
161
+ const xMatch = part.match(/^(\d+)(?:\.x)?$/);
162
+ if (xMatch) {
163
+ if (vMajor !== parseInt(xMatch[1], 10)) return false;
164
+ }
165
+ }
166
+ // Unknown operators — fall through (permissive)
167
+ }
168
+ return true;
169
+ }
170
+
171
+ /**
172
+ * Fetch the latest version of a package that is compatible with the current
173
+ * Node.js engine. Falls back to "@latest" if no engine info is available.
174
+ *
175
+ * Uses the npm registry API to get per-version engine requirements.
176
+ */
177
+ async function fetchLatestCompatibleVersion(mod: string): Promise<string> {
178
+ const currentNode = process.version; // e.g. "v20.17.0"
179
+ try {
180
+ // Encode scoped package names for URL (e.g. @tyvm/pkg -> @tyvm%2Fpkg)
181
+ const encodedMod = mod.replace(/^@/, "").replace("/", "%2F");
182
+ const registryUrl = mod.startsWith("@")
183
+ ? `https://registry.npmjs.org/@${encodedMod}`
184
+ : `https://registry.npmjs.org/${mod}`;
185
+
186
+ const response = await fetch(registryUrl);
187
+ if (!response.ok) throw new Error(`Registry returned ${response.status}`);
188
+ const pkgData = await response.json() as any;
189
+
190
+ // pkgData.versions is a map of version -> package metadata
191
+ const versionsMap: Record<string, any> = pkgData.versions ?? {};
192
+ const allVersions = Object.keys(versionsMap);
193
+
194
+ // Build per-version engine map from actual per-version metadata
195
+ const enginesByVersion: Record<string, string> = {};
196
+ for (const [v, meta] of Object.entries(versionsMap)) {
197
+ enginesByVersion[v] = (meta as any)?.engines?.node ?? "";
198
+ }
199
+
200
+ if (allVersions.length === 0) return `${mod}@latest`;
201
+
202
+ // Sort versions descending (simple semver numeric sort)
203
+ const sorted = [...allVersions].sort((a, b) => {
204
+ const pa = a.split(".").map(Number);
205
+ const pb = b.split(".").map(Number);
206
+ for (let i = 0; i < 3; i++) {
207
+ if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pb[i] ?? 0) - (pa[i] ?? 0);
208
+ }
209
+ return 0;
210
+ });
211
+
212
+ for (const v of sorted) {
213
+ const engineRange = enginesByVersion[v] ?? "";
214
+ if (nodeSatisfiesRange(currentNode, engineRange)) {
215
+ return `${mod}@${v}`;
216
+ }
217
+ }
218
+
219
+ // No compatible version found — warn and fall back to latest
220
+ console.warn(`āš ļø No version of ${mod} found compatible with Node ${currentNode}. Installing latest anyway.`);
221
+ return `${mod}@latest`;
222
+ } catch {
223
+ // Can't fetch registry info — fall back to latest
224
+ return `${mod}@latest`;
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Get the currently installed version of a package in .knowhow/node_modules.
230
+ */
231
+ function getInstalledVersion(mod: string, knowhowDir: string): string | null {
232
+ try {
233
+ const pkgJsonPath = path.join(knowhowDir, "node_modules", mod, "package.json");
234
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
235
+ return pkgJson.version ?? null;
236
+ } catch {
237
+ return null;
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Format a date string as a human-readable relative time (e.g. "2 days ago").
243
+ */
244
+ function formatRelativeTime(isoDate: string): string {
245
+ if (!isoDate) return "unknown";
246
+ const then = new Date(isoDate).getTime();
247
+ if (isNaN(then)) return "unknown";
248
+ const diffMs = Date.now() - then;
249
+ const diffMins = Math.floor(diffMs / 60_000);
250
+ if (diffMins < 2) return "just now";
251
+ if (diffMins < 60) return `${diffMins} minutes ago`;
252
+ const diffHours = Math.floor(diffMins / 60);
253
+ if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? "s" : ""} ago`;
254
+ const diffDays = Math.floor(diffHours / 24);
255
+ if (diffDays < 30) return `${diffDays} day${diffDays !== 1 ? "s" : ""} ago`;
256
+ const diffMonths = Math.floor(diffDays / 30);
257
+ if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? "s" : ""} ago`;
258
+ const diffYears = Math.floor(diffMonths / 12);
259
+ return `${diffYears} year${diffYears !== 1 ? "s" : ""} ago`;
260
+ }
261
+
262
+ /**
263
+ * Prompt the user for a yes/no confirmation.
264
+ */
265
+ function promptConfirm(question: string): Promise<boolean> {
266
+ return new Promise((resolve) => {
267
+ const rl = readline.createInterface({
268
+ input: process.stdin,
269
+ output: process.stdout,
270
+ });
271
+ rl.question(`${question} (y/N) `, (answer) => {
272
+ rl.close();
273
+ resolve(
274
+ answer.trim().toLowerCase() === "y" ||
275
+ answer.trim().toLowerCase() === "yes"
276
+ );
277
+ });
278
+ });
279
+ }
280
+
11
281
  export function addModulesCommand(program: Command): void {
12
282
  const modulesCmd = program
13
283
  .command("modules")
@@ -16,7 +286,7 @@ export function addModulesCommand(program: Command): void {
16
286
  modulesCmd
17
287
  .command("setup")
18
288
  .description(
19
- "Add default built-in modules to your config and install them via npm"
289
+ "Add default built-in modules to your config and install them into .knowhow/node_modules"
20
290
  )
21
291
  .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
22
292
  .action(async (opts) => {
@@ -29,41 +299,40 @@ export function addModulesCommand(program: Command): void {
29
299
 
30
300
  if (!cfg.modules) cfg.modules = [];
31
301
 
32
- const toAdd = BUILTIN_MODULES.filter(
33
- (m) => !cfg.modules!.includes(m)
34
- );
35
-
36
- if (toAdd.length === 0) {
37
- console.log(
38
- `āœ… All default modules are already in ${configLabel}. Nothing to do.`
39
- );
40
- return;
41
- }
302
+ const knowhowDir = getKnowhowDir(isGlobal);
303
+ ensureKnowhowPackageJson(knowhowDir);
42
304
 
43
- // Install packages that are not local file paths
44
- for (const mod of toAdd) {
305
+ // Even if modules are already in the config, they may not be installed.
306
+ // Check and install any that are missing from .knowhow/node_modules.
307
+ let anyChanges = false;
308
+ for (const mod of BUILTIN_MODULES) {
45
309
  if (!mod.startsWith(".") && !mod.startsWith("/")) {
46
- console.log(`šŸ“¦ Installing ${mod}...`);
47
- const installFlag = isGlobal ? "-g" : "";
48
- execSync(`npm install ${installFlag} ${mod}`, {
49
- stdio: "inherit",
50
- encoding: "utf-8",
51
- });
310
+ if (!isModuleInstalled(mod, knowhowDir)) {
311
+ const installTarget = await fetchLatestCompatibleVersion(mod);
312
+ console.log(`šŸ“¦ Installing ${installTarget}...`);
313
+ npmInstallToKnowhow(installTarget, knowhowDir);
314
+ console.log(`āœ… Installed ${mod}`);
315
+ anyChanges = true;
316
+ }
317
+ }
318
+ if (!cfg.modules.includes(mod)) {
319
+ cfg.modules.push(mod);
320
+ console.log(`āœ… Added ${mod} to ${configLabel}`);
321
+ anyChanges = true;
52
322
  }
53
- cfg.modules!.push(mod);
54
- console.log(`āœ… Added ${mod} to ${configLabel}`);
55
323
  }
56
324
 
57
- if (isGlobal) {
58
- await updateGlobalConfig(cfg);
325
+ if (!anyChanges) {
326
+ console.log(
327
+ `āœ… All default modules are already in ${configLabel} and installed. Nothing to do.`
328
+ );
59
329
  } else {
60
- await updateConfig(cfg);
330
+ await (isGlobal ? updateGlobalConfig(cfg) : updateConfig(cfg));
331
+ console.log(
332
+ `\nšŸŽ‰ Setup complete! Modules ready in ${knowhowDir}/node_modules.`
333
+ );
61
334
  }
62
-
63
- console.log(
64
- `\nšŸŽ‰ Setup complete! ${toAdd.length} module(s) added to ${configLabel}`
65
- );
66
- } catch (error) {
335
+ } catch (error: any) {
67
336
  console.error("Error during modules setup:", error.message ?? error);
68
337
  process.exit(1);
69
338
  }
@@ -72,10 +341,11 @@ export function addModulesCommand(program: Command): void {
72
341
  modulesCmd
73
342
  .command("install [module]")
74
343
  .description(
75
- "Install a module via npm and add it to your config. " +
344
+ "Install a module into .knowhow/node_modules and add it to your config. " +
76
345
  "If no module name is given, installs all modules already in the config."
77
346
  )
78
347
  .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
348
+ .option("--latest", "Force install the latest version (bypasses package-lock)")
79
349
  .action(async (moduleName: string | undefined, opts) => {
80
350
  try {
81
351
  const isGlobal: boolean = opts.global ?? false;
@@ -86,27 +356,25 @@ export function addModulesCommand(program: Command): void {
86
356
 
87
357
  if (!cfg.modules) cfg.modules = [];
88
358
 
359
+ const knowhowDir = getKnowhowDir(isGlobal);
360
+ ensureKnowhowPackageJson(knowhowDir);
361
+
89
362
  if (!moduleName) {
90
363
  // No module specified — install everything already in the config
91
364
  const installable = cfg.modules.filter(
92
365
  (m) => !m.startsWith(".") && !m.startsWith("/")
93
366
  );
94
367
  if (installable.length === 0) {
95
- console.log(
96
- `ℹ No installable modules found in ${configLabel}.`
97
- );
368
+ console.log(`ℹ No installable modules found in ${configLabel}.`);
98
369
  return;
99
370
  }
100
371
  console.log(
101
- `šŸ“¦ Installing ${installable.length} module(s) from ${configLabel}...`
372
+ `šŸ“¦ Installing ${installable.length} module(s) from ${configLabel} into ${knowhowDir}/node_modules...`
102
373
  );
103
- const installFlag = isGlobal ? "-g" : "";
104
374
  for (const mod of installable) {
105
375
  console.log(` šŸ“¦ Installing ${mod}...`);
106
- execSync(`npm install ${installFlag} ${mod}`, {
107
- stdio: "inherit",
108
- encoding: "utf-8",
109
- });
376
+ const installTarget = opts.latest ? await fetchLatestCompatibleVersion(mod) : mod;
377
+ npmInstallToKnowhow(installTarget, knowhowDir);
110
378
  console.log(` āœ… Installed ${mod}`);
111
379
  }
112
380
  console.log(`\nšŸŽ‰ All modules installed!`);
@@ -114,12 +382,9 @@ export function addModulesCommand(program: Command): void {
114
382
  }
115
383
 
116
384
  // Install the specified module
117
- const installFlag = isGlobal ? "-g" : "";
118
- console.log(`šŸ“¦ Installing ${moduleName}...`);
119
- execSync(`npm install ${installFlag} ${moduleName}`, {
120
- stdio: "inherit",
121
- encoding: "utf-8",
122
- });
385
+ const installTarget = opts.latest ? await fetchLatestCompatibleVersion(moduleName) : moduleName;
386
+ console.log(`šŸ“¦ Installing ${installTarget} into ${knowhowDir}/node_modules...`);
387
+ npmInstallToKnowhow(installTarget, knowhowDir);
123
388
  console.log(`āœ… Installed ${moduleName}`);
124
389
 
125
390
  // Add to config if not already there
@@ -134,7 +399,7 @@ export function addModulesCommand(program: Command): void {
134
399
  } else {
135
400
  console.log(`ℹ ${moduleName} is already in ${configLabel}`);
136
401
  }
137
- } catch (error) {
402
+ } catch (error: any) {
138
403
  console.error("Error during module install:", error.message ?? error);
139
404
  process.exit(1);
140
405
  }
@@ -174,9 +439,114 @@ export function addModulesCommand(program: Command): void {
174
439
  localModules.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
175
440
  }
176
441
  }
177
- } catch (error) {
442
+ } catch (error: any) {
178
443
  console.error("Error listing modules:", error.message ?? error);
179
444
  process.exit(1);
180
445
  }
181
446
  });
447
+
448
+ modulesCmd
449
+ .command("update")
450
+ .description(
451
+ "Check for updates to all modules in your config and update them. " +
452
+ "Shows installed vs latest version with publish date before updating."
453
+ )
454
+ .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
455
+ .option("-y, --yes", "Skip confirmation prompt and update all outdated modules automatically")
456
+ .action(async (opts) => {
457
+ try {
458
+ const isGlobal: boolean = opts.global ?? false;
459
+ const skipConfirm: boolean = opts.yes ?? false;
460
+ const cfg = isGlobal ? await getGlobalConfig() : await getConfig();
461
+ const configLabel = isGlobal
462
+ ? "~/.knowhow/knowhow.json"
463
+ : ".knowhow/knowhow.json";
464
+
465
+ if (!cfg.modules || cfg.modules.length === 0) {
466
+ console.log(`ℹ No modules found in ${configLabel}.`);
467
+ return;
468
+ }
469
+
470
+ const knowhowDir = getKnowhowDir(isGlobal);
471
+ ensureKnowhowPackageJson(knowhowDir);
472
+
473
+ // Only check npm packages (not local paths)
474
+ const installable = cfg.modules.filter(
475
+ (m) => !m.startsWith(".") && !m.startsWith("/")
476
+ );
477
+
478
+ if (installable.length === 0) {
479
+ console.log(`ℹ No npm modules found in ${configLabel}.`);
480
+ return;
481
+ }
482
+
483
+ console.log(`šŸ” Checking for updates to ${installable.length} module(s)...\n`);
484
+
485
+ interface UpdateInfo {
486
+ mod: string;
487
+ installed: string | null;
488
+ latest: string;
489
+ compatibleVersion: string; // e.g. "@tyvm/knowhow-module-script@0.0.4"
490
+ publishedAt: string;
491
+ needsUpdate: boolean;
492
+ }
493
+
494
+ const updates: UpdateInfo[] = [];
495
+
496
+ for (const mod of installable) {
497
+ const registryInfo = await fetchNpmRegistryInfo(mod);
498
+ if (!registryInfo) {
499
+ console.log(` āš ļø ${mod}: could not fetch registry info (skipping)`);
500
+ continue;
501
+ }
502
+ const installed = getInstalledVersion(mod, knowhowDir);
503
+ const { latestVersion, publishedAt } = registryInfo;
504
+ // Find the latest compatible version for the current Node.js engine
505
+ const compatibleInstallTarget = await fetchLatestCompatibleVersion(mod);
506
+ const compatibleVersion = compatibleInstallTarget.replace(/^[^@]+@/, ""); // strip "pkg@" prefix
507
+ const needsUpdate = installed !== compatibleVersion;
508
+ const timeAgo = formatRelativeTime(publishedAt);
509
+
510
+ if (needsUpdate) {
511
+ const installedStr = installed ?? "(not installed)";
512
+ const versionLabel = compatibleVersion !== latestVersion
513
+ ? `${compatibleVersion} (latest compatible with Node ${process.version}; absolute latest: ${latestVersion})`
514
+ : compatibleVersion;
515
+ console.log(` šŸ“¦ ${mod}`);
516
+ console.log(` installed: ${installedStr} → latest: ${versionLabel} (published ${timeAgo})`);
517
+ } else {
518
+ console.log(` āœ… ${mod} v${compatibleVersion} (up to date, published ${timeAgo})`);
519
+ }
520
+ updates.push({ mod, installed, latest: compatibleVersion, compatibleVersion: compatibleInstallTarget, publishedAt, needsUpdate });
521
+ }
522
+
523
+ const toUpdate = updates.filter((u) => u.needsUpdate);
524
+
525
+ if (toUpdate.length === 0) {
526
+ console.log(`\nāœ… All modules are up to date!`);
527
+ return;
528
+ }
529
+
530
+ console.log(`\n${toUpdate.length} module(s) can be updated.`);
531
+
532
+ if (!skipConfirm) {
533
+ const confirmed = await promptConfirm(`Update ${toUpdate.length} module(s) now?`);
534
+ if (!confirmed) {
535
+ console.log("Cancelled.");
536
+ return;
537
+ }
538
+ }
539
+
540
+ console.log("");
541
+ for (const { mod, compatibleVersion } of toUpdate) {
542
+ console.log(` šŸ“¦ Updating ${compatibleVersion}...`);
543
+ npmInstallToKnowhow(compatibleVersion, knowhowDir);
544
+ console.log(` āœ… Updated ${mod}`);
545
+ }
546
+ console.log(`\nšŸŽ‰ Update complete! ${toUpdate.length} module(s) updated.`);
547
+ } catch (error: any) {
548
+ console.error("Error during modules update:", error.message ?? error);
549
+ process.exit(1);
550
+ }
551
+ });
182
552
  }
package/src/config.ts CHANGED
@@ -74,7 +74,7 @@ const defaultConfig = {
74
74
  description:
75
75
  "You can define agents in the config. They will have access to all tools.",
76
76
  instructions: "Reply to the user saying 'Hello, world!'",
77
- model: "gpt-4o-2024-08-06",
77
+ model: "gpt-5.4-nano",
78
78
  provider: "openai",
79
79
  },
80
80
  ],