@tyvm/knowhow 0.0.109-dev.e88af1e → 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 (82) 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/src/agents/tools/list.ts +2 -2
  15. package/src/ai.ts +81 -37
  16. package/src/chat/CliChatService.ts +1 -1
  17. package/src/chat/modules/SystemModule.ts +2 -2
  18. package/src/clients/anthropic.ts +1 -1
  19. package/src/clients/index.ts +25 -6
  20. package/src/clients/openai.ts +8 -5
  21. package/src/clients/types.ts +29 -6
  22. package/src/clients/withRetry.ts +89 -0
  23. package/src/commands/agent.ts +30 -0
  24. package/src/commands/modules.ts +365 -30
  25. package/src/config.ts +1 -1
  26. package/src/hashes.ts +8 -9
  27. package/src/index.ts +4 -2
  28. package/src/processors/Base64ImageDetector.ts +73 -0
  29. package/src/services/MediaProcessorService.ts +79 -10
  30. package/src/services/modules/index.ts +24 -19
  31. package/tests/processors/Base64ImageDetector.test.ts +160 -0
  32. package/tests/unit/clients/AIClient.test.ts +446 -0
  33. package/tests/unit/clients/withRetry.test.ts +319 -0
  34. package/tests/unit/commands/github-credentials.test.ts +1 -2
  35. package/ts_build/package.json +1 -1
  36. package/ts_build/src/agents/tools/list.js +2 -2
  37. package/ts_build/src/agents/tools/list.js.map +1 -1
  38. package/ts_build/src/ai.d.ts +3 -3
  39. package/ts_build/src/ai.js +51 -23
  40. package/ts_build/src/ai.js.map +1 -1
  41. package/ts_build/src/chat/CliChatService.js +1 -1
  42. package/ts_build/src/chat/CliChatService.js.map +1 -1
  43. package/ts_build/src/chat/modules/SystemModule.js +2 -2
  44. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  45. package/ts_build/src/clients/anthropic.js +1 -1
  46. package/ts_build/src/clients/anthropic.js.map +1 -1
  47. package/ts_build/src/clients/index.js +7 -6
  48. package/ts_build/src/clients/index.js.map +1 -1
  49. package/ts_build/src/clients/openai.js +4 -4
  50. package/ts_build/src/clients/openai.js.map +1 -1
  51. package/ts_build/src/clients/types.d.ts +12 -6
  52. package/ts_build/src/clients/withRetry.d.ts +2 -0
  53. package/ts_build/src/clients/withRetry.js +60 -0
  54. package/ts_build/src/clients/withRetry.js.map +1 -0
  55. package/ts_build/src/commands/agent.js +25 -0
  56. package/ts_build/src/commands/agent.js.map +1 -1
  57. package/ts_build/src/commands/modules.js +297 -17
  58. package/ts_build/src/commands/modules.js.map +1 -1
  59. package/ts_build/src/config.js +1 -1
  60. package/ts_build/src/config.js.map +1 -1
  61. package/ts_build/src/hashes.js +5 -7
  62. package/ts_build/src/hashes.js.map +1 -1
  63. package/ts_build/src/index.js +1 -1
  64. package/ts_build/src/index.js.map +1 -1
  65. package/ts_build/src/processors/Base64ImageDetector.d.ts +3 -0
  66. package/ts_build/src/processors/Base64ImageDetector.js +42 -0
  67. package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
  68. package/ts_build/src/services/MediaProcessorService.d.ts +5 -4
  69. package/ts_build/src/services/MediaProcessorService.js +53 -8
  70. package/ts_build/src/services/MediaProcessorService.js.map +1 -1
  71. package/ts_build/src/services/modules/index.js +17 -13
  72. package/ts_build/src/services/modules/index.js.map +1 -1
  73. package/ts_build/tests/processors/Base64ImageDetector.test.js +111 -0
  74. package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
  75. package/ts_build/tests/unit/clients/AIClient.test.d.ts +1 -0
  76. package/ts_build/tests/unit/clients/AIClient.test.js +339 -0
  77. package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -0
  78. package/ts_build/tests/unit/clients/withRetry.test.d.ts +1 -0
  79. package/ts_build/tests/unit/clients/withRetry.test.js +225 -0
  80. package/ts_build/tests/unit/clients/withRetry.test.js.map +1 -0
  81. package/ts_build/tests/unit/commands/github-credentials.test.js +1 -2
  82. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -1
@@ -4,6 +4,7 @@ import * as fs from "fs";
4
4
  import * as path from "path";
5
5
  import * as os from "os";
6
6
  import { getConfig, getGlobalConfig, updateConfig, updateGlobalConfig } from "../config";
7
+ import * as readline from "readline";
7
8
 
8
9
  // Default built-in modules that `knowhow modules setup` adds to the config.
9
10
  export const BUILTIN_MODULES = [
@@ -49,6 +50,234 @@ function npmInstallToKnowhow(mod: string, knowhowDir: string): void {
49
50
  });
50
51
  }
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
+
52
281
  export function addModulesCommand(program: Command): void {
53
282
  const modulesCmd = program
54
283
  .command("modules")
@@ -70,39 +299,39 @@ export function addModulesCommand(program: Command): void {
70
299
 
71
300
  if (!cfg.modules) cfg.modules = [];
72
301
 
73
- const toAdd = BUILTIN_MODULES.filter(
74
- (m) => !cfg.modules!.includes(m)
75
- );
76
-
77
- if (toAdd.length === 0) {
78
- console.log(
79
- `✅ All default modules are already in ${configLabel}. Nothing to do.`
80
- );
81
- return;
82
- }
83
-
84
302
  const knowhowDir = getKnowhowDir(isGlobal);
85
303
  ensureKnowhowPackageJson(knowhowDir);
86
304
 
87
- // Install packages that are not local file paths
88
- 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) {
89
309
  if (!mod.startsWith(".") && !mod.startsWith("/")) {
90
- console.log(`📦 Installing ${mod}...`);
91
- npmInstallToKnowhow(mod, knowhowDir);
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;
92
322
  }
93
- cfg.modules!.push(mod);
94
- console.log(`✅ Added ${mod} to ${configLabel}`);
95
323
  }
96
324
 
97
- if (isGlobal) {
98
- await updateGlobalConfig(cfg);
325
+ if (!anyChanges) {
326
+ console.log(
327
+ `✅ All default modules are already in ${configLabel} and installed. Nothing to do.`
328
+ );
99
329
  } else {
100
- await updateConfig(cfg);
330
+ await (isGlobal ? updateGlobalConfig(cfg) : updateConfig(cfg));
331
+ console.log(
332
+ `\n🎉 Setup complete! Modules ready in ${knowhowDir}/node_modules.`
333
+ );
101
334
  }
102
-
103
- console.log(
104
- `\n🎉 Setup complete! ${toAdd.length} module(s) added to ${configLabel}`
105
- );
106
335
  } catch (error: any) {
107
336
  console.error("Error during modules setup:", error.message ?? error);
108
337
  process.exit(1);
@@ -116,6 +345,7 @@ export function addModulesCommand(program: Command): void {
116
345
  "If no module name is given, installs all modules already in the config."
117
346
  )
118
347
  .option("--global", "Use the global config (~/.knowhow/knowhow.json)")
348
+ .option("--latest", "Force install the latest version (bypasses package-lock)")
119
349
  .action(async (moduleName: string | undefined, opts) => {
120
350
  try {
121
351
  const isGlobal: boolean = opts.global ?? false;
@@ -135,9 +365,7 @@ export function addModulesCommand(program: Command): void {
135
365
  (m) => !m.startsWith(".") && !m.startsWith("/")
136
366
  );
137
367
  if (installable.length === 0) {
138
- console.log(
139
- `ℹ No installable modules found in ${configLabel}.`
140
- );
368
+ console.log(`ℹ No installable modules found in ${configLabel}.`);
141
369
  return;
142
370
  }
143
371
  console.log(
@@ -145,7 +373,8 @@ export function addModulesCommand(program: Command): void {
145
373
  );
146
374
  for (const mod of installable) {
147
375
  console.log(` 📦 Installing ${mod}...`);
148
- npmInstallToKnowhow(mod, knowhowDir);
376
+ const installTarget = opts.latest ? await fetchLatestCompatibleVersion(mod) : mod;
377
+ npmInstallToKnowhow(installTarget, knowhowDir);
149
378
  console.log(` ✅ Installed ${mod}`);
150
379
  }
151
380
  console.log(`\n🎉 All modules installed!`);
@@ -153,8 +382,9 @@ export function addModulesCommand(program: Command): void {
153
382
  }
154
383
 
155
384
  // Install the specified module
156
- console.log(`📦 Installing ${moduleName} into ${knowhowDir}/node_modules...`);
157
- npmInstallToKnowhow(moduleName, knowhowDir);
385
+ const installTarget = opts.latest ? await fetchLatestCompatibleVersion(moduleName) : moduleName;
386
+ console.log(`📦 Installing ${installTarget} into ${knowhowDir}/node_modules...`);
387
+ npmInstallToKnowhow(installTarget, knowhowDir);
158
388
  console.log(`✅ Installed ${moduleName}`);
159
389
 
160
390
  // Add to config if not already there
@@ -214,4 +444,109 @@ export function addModulesCommand(program: Command): void {
214
444
  process.exit(1);
215
445
  }
216
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
+ });
217
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
  ],
package/src/hashes.ts CHANGED
@@ -54,18 +54,17 @@ export async function checkNoFilesChanged(
54
54
  return false;
55
55
  }
56
56
 
57
- if (
57
+ // Check if this file has changed (either format)
58
+ const matchesLegacy =
58
59
  hashes[file].promptHash === promptHash &&
59
- hashes[file].fileHash === fileHash
60
- ) {
61
- return true;
62
- }
60
+ hashes[file].fileHash === fileHash;
61
+ const matchesCurrent = hashes[file][promptHash] === fileHash;
63
62
 
64
- if (hashes[file][promptHash] === fileHash) {
65
- return true;
63
+ if (!matchesLegacy && !matchesCurrent) {
64
+ // This file has changed — re-generation needed
65
+ return false;
66
66
  }
67
-
68
- return false;
67
+ // This file is unchanged — continue checking the rest
69
68
  }
70
69
 
71
70
  return true;
package/src/index.ts CHANGED
@@ -184,13 +184,15 @@ export async function upload() {
184
184
  * - Standard glob patterns (e.g. "src/**\/*.ts")
185
185
  * - Brace expansion (e.g. "{src/a.ts,src/b.ts}")
186
186
  * - Comma-separated file paths (e.g. "src/a.ts,src/b.ts") — auto-converted to brace expansion
187
+ * - Mixed comma-separated list with globs (e.g. "src/a.ts,src/commands/**\/*.ts")
187
188
  */
188
189
  function normalizeInputPattern(input: string): string {
189
- // If it already has braces or glob chars other than comma, use as-is
190
- if (input.includes("{") || input.includes("*") || input.includes("?")) {
190
+ // If it already has braces, use as-is (already brace-expanded)
191
+ if (input.includes("{")) {
191
192
  return input;
192
193
  }
193
194
  // If it contains commas, treat as comma-separated list and wrap in braces
195
+ // This also handles the mixed case: "src/a.ts,src/commands/**/*.ts"
194
196
  if (input.includes(",")) {
195
197
  const parts = input.split(",").map((p) => p.trim());
196
198
  return `{${parts.join(",")}}`;
@@ -17,6 +17,15 @@ interface TextContent {
17
17
  text: string;
18
18
  }
19
19
 
20
+ /**
21
+ * Regex that matches common image file paths (absolute, relative, or just filenames)
22
+ * ending in a known image extension.
23
+ */
24
+ const IMAGE_PATH_REGEX =
25
+ /(?:^|[\s"'(,\[{])([^\s"'(,\[{]*?\.(?:png|jpe?g|gif|webp|bmp|svg))(?=$|[\s"'),\]}])/gi;
26
+
27
+ const IMAGE_EXTENSIONS = ["png", "jpeg", "jpg", "gif", "webp", "bmp", "svg"];
28
+
20
29
  export class Base64ImageProcessor {
21
30
  private imageDetail: "auto" | "low" | "high" = "auto";
22
31
  private supportedFormats = ["png", "jpeg", "jpg", "gif", "webp"];
@@ -118,6 +127,61 @@ export class Base64ImageProcessor {
118
127
  }
119
128
  }
120
129
 
130
+ /**
131
+ * Finds all image file paths mentioned in a text string.
132
+ * Returns deduplicated list of paths like "/tmp/screenshot.png" or "screenshot.jpg".
133
+ */
134
+ private findImageFilePaths(text: string): string[] {
135
+ const found: string[] = [];
136
+ const seen = new Set<string>();
137
+ IMAGE_PATH_REGEX.lastIndex = 0;
138
+ let match: RegExpExecArray | null;
139
+ while ((match = IMAGE_PATH_REGEX.exec(text)) !== null) {
140
+ const filePath = match[1];
141
+ if (filePath && !seen.has(filePath)) {
142
+ seen.add(filePath);
143
+ found.push(filePath);
144
+ }
145
+ }
146
+ return found;
147
+ }
148
+
149
+ /**
150
+ * Appends a hint to a text string if image file paths are detected.
151
+ * The hint tells the model it can use loadImageAsBase64 to view the image.
152
+ */
153
+ private addImagePathHint(text: string): string {
154
+ const paths = this.findImageFilePaths(text);
155
+ if (paths.length === 0) return text;
156
+
157
+ const hints = paths.map(
158
+ (p) =>
159
+ `loadImageAsBase64("${p}") to view the image at ${p}`
160
+ );
161
+
162
+ const hintBlock =
163
+ paths.length === 1
164
+ ? `\n\n[TIP: An image file path was detected: ${paths[0]}. Use the \`loadImageAsBase64\` tool with this path to load and view the image: ${hints[0]}]`
165
+ : `\n\n[TIP: Image file paths were detected. Use the \`loadImageAsBase64\` tool to load and view them:\n${hints.map((h) => ` - ${h}`).join("\n")}]`;
166
+
167
+ return text + hintBlock;
168
+ }
169
+
170
+ /**
171
+ * Applies image path hints to a message's text content items.
172
+ */
173
+ private applyImagePathHintsToMessage(message: Message): void {
174
+ if (typeof message.content === "string") {
175
+ message.content = this.addImagePathHint(message.content);
176
+ } else if (Array.isArray(message.content)) {
177
+ for (const item of message.content) {
178
+ if (item && (item as TextContent).type === "text" && typeof (item as TextContent).text === "string") {
179
+ (item as TextContent).text = this.addImagePathHint((item as TextContent).text);
180
+ }
181
+ }
182
+ }
183
+ }
184
+
121
185
  private processToolCallArguments(message: Message): void {
122
186
  if (message.tool_calls) {
123
187
  for (const toolCall of message.tool_calls) {
@@ -209,6 +273,15 @@ export class Base64ImageProcessor {
209
273
  // and converted to proper image content before the agent sees them
210
274
  if (message.role === "tool") {
211
275
  this.processToolMessageContent(message);
276
+ // After processing tool content (which may not convert to image if it's plain text
277
+ // describing a screenshot path), add hints for any image file paths found in the text.
278
+ this.applyImagePathHintsToMessage(message);
279
+ }
280
+
281
+ // Also apply hints to assistant messages — e.g. when an assistant message
282
+ // contains the result of a screenshot tool that returned a file path.
283
+ if (message.role === "assistant") {
284
+ this.applyImagePathHintsToMessage(message);
212
285
  }
213
286
 
214
287
  // Process tool calls in any message