@tyvm/knowhow 0.0.109 ā 0.0.111
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/scripts/build-for-node.sh +10 -24
- package/src/agents/tools/list.ts +2 -2
- package/src/ai.ts +81 -37
- package/src/auth/browserLogin.ts +129 -3
- package/src/chat/CliChatService.ts +1 -1
- package/src/chat/modules/AgentModule.ts +7 -2
- package/src/chat/modules/SessionsModule.ts +40 -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 +417 -47
- package/src/config.ts +1 -1
- package/src/fileSync.ts +20 -12
- package/src/hashes.ts +43 -22
- package/src/index.ts +4 -2
- package/src/login.ts +3 -2
- package/src/processors/Base64ImageDetector.ts +73 -0
- package/src/services/Mcp.ts +14 -1
- package/src/services/MediaProcessorService.ts +79 -10
- package/src/services/modules/index.ts +47 -18
- package/src/utils/http.ts +9 -2
- 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/auth/browserLogin.d.ts +2 -0
- package/ts_build/src/auth/browserLogin.js +91 -3
- package/ts_build/src/auth/browserLogin.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/AgentModule.js +5 -2
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +30 -1
- package/ts_build/src/chat/modules/SessionsModule.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 +359 -32
- 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/fileSync.d.ts +2 -2
- package/ts_build/src/fileSync.js +13 -11
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/hashes.d.ts +2 -2
- package/ts_build/src/hashes.js +40 -16
- 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/login.js +2 -2
- package/ts_build/src/login.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/Mcp.js +9 -1
- package/ts_build/src/services/Mcp.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 +35 -12
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/src/utils/http.d.ts +2 -1
- package/ts_build/src/utils/http.js +11 -2
- package/ts_build/src/utils/http.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
|
@@ -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
|
|
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
|
|
33
|
-
|
|
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
|
-
//
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 (
|
|
58
|
-
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
118
|
-
console.log(`š¦ Installing ${
|
|
119
|
-
|
|
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-
|
|
77
|
+
model: "gpt-5.4-nano",
|
|
78
78
|
provider: "openai",
|
|
79
79
|
},
|
|
80
80
|
],
|
package/src/fileSync.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { loadJwt } from "./login";
|
|
|
6
6
|
import { getConfig } from "./config";
|
|
7
7
|
import { services } from "./services";
|
|
8
8
|
import { S3Service } from "./services/S3";
|
|
9
|
-
import { getHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote, isLocalFileMatchingDownloadHash, saveDownloadHash } from "./hashes";
|
|
9
|
+
import { getHashes, saveHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote, isLocalFileMatchingDownloadHash, saveDownloadHash } from "./hashes";
|
|
10
10
|
|
|
11
11
|
export const DEFAULT_BATCH_SIZE = 5;
|
|
12
12
|
|
|
@@ -165,7 +165,8 @@ export async function downloadFile(
|
|
|
165
165
|
s3Service: S3Service,
|
|
166
166
|
remotePath: string,
|
|
167
167
|
localPath: string,
|
|
168
|
-
dryRun: boolean
|
|
168
|
+
dryRun: boolean,
|
|
169
|
+
hashes?: any
|
|
169
170
|
): Promise<void> {
|
|
170
171
|
console.log(`ā¬ļø Downloading ${remotePath} ā ${localPath}`);
|
|
171
172
|
|
|
@@ -176,8 +177,7 @@ export async function downloadFile(
|
|
|
176
177
|
|
|
177
178
|
try {
|
|
178
179
|
// Fast-path: check stored download hash before hitting the API
|
|
179
|
-
|
|
180
|
-
if (await isLocalFileMatchingDownloadHash(localPath, hashes)) {
|
|
180
|
+
if (hashes && await isLocalFileMatchingDownloadHash(localPath, hashes)) {
|
|
181
181
|
console.log(` ā Skipping ${localPath} (matches stored download hash)`);
|
|
182
182
|
return;
|
|
183
183
|
}
|
|
@@ -189,7 +189,7 @@ export async function downloadFile(
|
|
|
189
189
|
if (isLocalFileMatchingRemote(localPath, checksumSHA256)) {
|
|
190
190
|
console.log(` ā Skipping ${localPath} (matches remote checksum)`);
|
|
191
191
|
// Store the hash so future syncs can skip without hitting the API
|
|
192
|
-
await saveDownloadHash(localPath);
|
|
192
|
+
await saveDownloadHash(localPath, hashes);
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
195
195
|
|
|
@@ -203,7 +203,7 @@ export async function downloadFile(
|
|
|
203
203
|
await s3Service.downloadFromPresignedUrl(downloadUrl, localPath);
|
|
204
204
|
|
|
205
205
|
// Save download hash so we can skip unchanged files next time
|
|
206
|
-
await saveDownloadHash(localPath);
|
|
206
|
+
await saveDownloadHash(localPath, hashes);
|
|
207
207
|
|
|
208
208
|
// Get file size for logging
|
|
209
209
|
const stats = fs.statSync(localPath);
|
|
@@ -221,7 +221,8 @@ export async function uploadFile(
|
|
|
221
221
|
s3Service: S3Service,
|
|
222
222
|
remotePath: string,
|
|
223
223
|
localPath: string,
|
|
224
|
-
dryRun: boolean
|
|
224
|
+
dryRun: boolean,
|
|
225
|
+
hashes?: any
|
|
225
226
|
): Promise<void> {
|
|
226
227
|
console.log(`ā¬ļø Uploading ${localPath} ā ${remotePath}`);
|
|
227
228
|
|
|
@@ -237,8 +238,7 @@ export async function uploadFile(
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// Skip upload if file hasn't changed since last upload
|
|
240
|
-
const
|
|
241
|
-
const changed = await hasFileChangedSinceUpload(localPath, hashes);
|
|
241
|
+
const changed = hashes ? await hasFileChangedSinceUpload(localPath, hashes) : true;
|
|
242
242
|
if (!changed) {
|
|
243
243
|
console.log(` ā Skipping ${localPath} (unchanged since last upload)`);
|
|
244
244
|
return;
|
|
@@ -254,7 +254,7 @@ export async function uploadFile(
|
|
|
254
254
|
await client.markOrgFileUploadComplete(remotePath);
|
|
255
255
|
|
|
256
256
|
// Save upload hash so we can skip unchanged files next time
|
|
257
|
-
await saveUploadHash(localPath);
|
|
257
|
+
await saveUploadHash(localPath, hashes);
|
|
258
258
|
|
|
259
259
|
const stats = fs.statSync(localPath);
|
|
260
260
|
console.log(` ā Uploaded ${stats.size} bytes`);
|
|
@@ -276,6 +276,8 @@ export async function uploadDirectory(
|
|
|
276
276
|
|
|
277
277
|
console.log(`ā¬ļø Uploading directory ${localDir} ā ${remoteDir}`);
|
|
278
278
|
|
|
279
|
+
const hashes = await getHashes();
|
|
280
|
+
|
|
279
281
|
if (!fs.existsSync(localDir)) {
|
|
280
282
|
console.warn(` ā ļø Local directory not found: ${localDir}`);
|
|
281
283
|
return 0;
|
|
@@ -295,7 +297,7 @@ export async function uploadDirectory(
|
|
|
295
297
|
const localFilePath = localDir + relFile;
|
|
296
298
|
const remoteFilePath = remoteDir + relFile;
|
|
297
299
|
try {
|
|
298
|
-
await uploadFile(client, s3Service, remoteFilePath, localFilePath, dryRun);
|
|
300
|
+
await uploadFile(client, s3Service, remoteFilePath, localFilePath, dryRun, hashes);
|
|
299
301
|
return 1;
|
|
300
302
|
} catch (error) {
|
|
301
303
|
console.error(
|
|
@@ -306,6 +308,8 @@ export async function uploadDirectory(
|
|
|
306
308
|
});
|
|
307
309
|
|
|
308
310
|
const counts = await batchRun(tasks);
|
|
311
|
+
await saveHashes(hashes);
|
|
312
|
+
|
|
309
313
|
return counts.reduce((sum, n) => sum + n, 0);
|
|
310
314
|
}
|
|
311
315
|
|
|
@@ -325,6 +329,8 @@ export async function downloadDirectory(
|
|
|
325
329
|
|
|
326
330
|
console.log(`ā¬ļø Downloading directory ${remoteDir} ā ${localDir}`);
|
|
327
331
|
|
|
332
|
+
const hashes = await getHashes();
|
|
333
|
+
|
|
328
334
|
// List all org files and find those in the remote directory
|
|
329
335
|
const response = await client.listOrgFiles();
|
|
330
336
|
const allFiles = response.data;
|
|
@@ -352,10 +358,12 @@ export async function downloadDirectory(
|
|
|
352
358
|
// Strip the base remote dir prefix to get relative path
|
|
353
359
|
const relativePath = fullRemotePath.slice(remoteDir.length);
|
|
354
360
|
const localFilePath = localDir + relativePath;
|
|
355
|
-
await downloadFile(client, s3Service, fullRemotePath, localFilePath, dryRun);
|
|
361
|
+
await downloadFile(client, s3Service, fullRemotePath, localFilePath, dryRun, hashes);
|
|
356
362
|
return 1;
|
|
357
363
|
});
|
|
358
364
|
|
|
359
365
|
const counts = await batchRun(tasks);
|
|
366
|
+
await saveHashes(hashes);
|
|
367
|
+
|
|
360
368
|
return counts.reduce((sum, n) => sum + n, 0);
|
|
361
369
|
}
|