@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.
- package/autodoc/README.md +324 -0
- package/autodoc/chat-guide.md +268 -365
- package/autodoc/cli-reference.md +399 -473
- package/autodoc/config-reference.md +431 -330
- package/autodoc/embeddings-guide.md +223 -322
- package/autodoc/generate-guide.md +261 -301
- package/autodoc/language-plugin-guide.md +221 -247
- package/autodoc/modules-guide.md +242 -215
- package/autodoc/plugins-guide.md +470 -469
- package/autodoc/quickstart-guide.md +67 -70
- package/autodoc/skills-guide.md +455 -339
- package/autodoc/worker-guide.md +301 -308
- package/package.json +1 -1
- package/src/agents/tools/list.ts +2 -2
- package/src/ai.ts +81 -37
- package/src/chat/CliChatService.ts +1 -1
- package/src/chat/modules/SystemModule.ts +2 -2
- package/src/clients/anthropic.ts +1 -1
- package/src/clients/index.ts +25 -6
- package/src/clients/openai.ts +8 -5
- package/src/clients/types.ts +29 -6
- package/src/clients/withRetry.ts +89 -0
- package/src/commands/agent.ts +30 -0
- package/src/commands/modules.ts +365 -30
- package/src/config.ts +1 -1
- package/src/hashes.ts +8 -9
- package/src/index.ts +4 -2
- package/src/processors/Base64ImageDetector.ts +73 -0
- package/src/services/MediaProcessorService.ts +79 -10
- package/src/services/modules/index.ts +24 -19
- package/tests/processors/Base64ImageDetector.test.ts +160 -0
- package/tests/unit/clients/AIClient.test.ts +446 -0
- package/tests/unit/clients/withRetry.test.ts +319 -0
- package/tests/unit/commands/github-credentials.test.ts +1 -2
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/tools/list.js +2 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/ai.d.ts +3 -3
- package/ts_build/src/ai.js +51 -23
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/CliChatService.js +1 -1
- package/ts_build/src/chat/CliChatService.js.map +1 -1
- package/ts_build/src/chat/modules/SystemModule.js +2 -2
- package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +1 -1
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/index.js +7 -6
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/openai.js +4 -4
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +12 -6
- package/ts_build/src/clients/withRetry.d.ts +2 -0
- package/ts_build/src/clients/withRetry.js +60 -0
- package/ts_build/src/clients/withRetry.js.map +1 -0
- package/ts_build/src/commands/agent.js +25 -0
- package/ts_build/src/commands/agent.js.map +1 -1
- package/ts_build/src/commands/modules.js +297 -17
- package/ts_build/src/commands/modules.js.map +1 -1
- package/ts_build/src/config.js +1 -1
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/hashes.js +5 -7
- package/ts_build/src/hashes.js.map +1 -1
- package/ts_build/src/index.js +1 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processors/Base64ImageDetector.d.ts +3 -0
- package/ts_build/src/processors/Base64ImageDetector.js +42 -0
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/services/MediaProcessorService.d.ts +5 -4
- package/ts_build/src/services/MediaProcessorService.js +53 -8
- package/ts_build/src/services/MediaProcessorService.js.map +1 -1
- package/ts_build/src/services/modules/index.js +17 -13
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/tests/processors/Base64ImageDetector.test.js +111 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
- package/ts_build/tests/unit/clients/AIClient.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/AIClient.test.js +339 -0
- package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -0
- package/ts_build/tests/unit/clients/withRetry.test.d.ts +1 -0
- package/ts_build/tests/unit/clients/withRetry.test.js +225 -0
- package/ts_build/tests/unit/clients/withRetry.test.js.map +1 -0
- package/ts_build/tests/unit/commands/github-credentials.test.js +1 -2
- package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -1
package/src/commands/modules.ts
CHANGED
|
@@ -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
|
-
//
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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 (
|
|
98
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
|
|
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-
|
|
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 (
|
|
65
|
-
|
|
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
|
|
190
|
-
if (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
|