@tyvm/knowhow 0.0.98 → 0.0.100

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.98",
3
+ "version": "0.0.100",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "test:debug": "node --inspect-brk ../../node_modules/jest/bin/jest.js --detectOpenHandles --forceExit --testTimeout 300000",
12
12
  "compile": "npm run clean && tsc",
13
13
  "clean": "rm -rf ts_build",
14
+ "node:build": "bash scripts/build-for-node.sh",
14
15
  "start": "npm run compile && node --no-node-snapshot ts_build/src/server/index.js",
15
16
  "dataset:diffs:generate": "ts-node src/dataset/diffs/generate.ts",
16
17
  "dataset:diffs:jsonl": "ts-node src/dataset/diffs/jsonl.ts",
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: bash scripts/build-for-node.sh [node-version]
3
+ # Example: bash scripts/build-for-node.sh 24 # any Node 24.x
4
+ # Example: bash scripts/build-for-node.sh 20.19.0 # exact version
5
+ # Example: npm run node:build 24
6
+ #
7
+ # This script:
8
+ # 1. Compiles TypeScript with Node 20 (required for workspace deps)
9
+ # 2. Creates /tmp/knowhow-node-<major> with the compiled output
10
+ # 3. Installs the correct isolated-vm version for the target node in that dir
11
+ # 4. Symlinks the package globally for ALL installed nvm versions matching the target
12
+ #
13
+ # This approach avoids polluting the workspace node_modules with a different
14
+ # isolated-vm ABI, so Node 20 and Node 24 builds can coexist.
15
+
16
+ set -e
17
+
18
+ TARGET_VERSION="${1:-}"
19
+
20
+ if [ -z "$TARGET_VERSION" ]; then
21
+ echo "Usage: $0 <node-version>"
22
+ echo "Example: $0 24"
23
+ echo "Example: $0 20.19.0"
24
+ exit 1
25
+ fi
26
+
27
+ # Extract the major version number for staging dir naming and glob matching
28
+ TARGET_MAJOR="$(echo "$TARGET_VERSION" | cut -d. -f1)"
29
+
30
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
31
+ PACKAGE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
32
+
33
+ echo "šŸ“¦ Package dir: $PACKAGE_DIR"
34
+
35
+ # --- Find Node 20 for compiling TypeScript ---
36
+ NODE20_BIN=""
37
+ for dir in "$HOME/.nvm/versions/node"/v20.*/bin; do
38
+ if [ -f "$dir/node" ]; then
39
+ NODE20_BIN="$dir/node"
40
+ break
41
+ fi
42
+ done
43
+
44
+ if [ -z "$NODE20_BIN" ]; then
45
+ echo "āš ļø Node 20 not found via nvm, falling back to current node for TS compile"
46
+ NODE20_BIN="$(which node)"
47
+ fi
48
+
49
+ NODE20_NPM="$(dirname "$NODE20_BIN")/npm"
50
+ echo "šŸ”Ø Compiling TypeScript with: $NODE20_BIN ($(${NODE20_BIN} --version))"
51
+
52
+ # --- Compile TypeScript ---
53
+ cd "$PACKAGE_DIR"
54
+ "$NODE20_NPM" run compile
55
+ echo "āœ… TypeScript compiled"
56
+
57
+ # --- Find target Node binaries ---
58
+ # If exact version given (e.g. 20.19.0), match exactly. Otherwise match all patch versions.
59
+ TARGET_NODE_BINS=()
60
+
61
+ if echo "$TARGET_VERSION" | grep -qE '^\d+\.\d+\.\d+$'; then
62
+ # Exact version like 20.19.0
63
+ exact_dir="$HOME/.nvm/versions/node/v${TARGET_VERSION}/bin"
64
+ if [ -f "$exact_dir/node" ]; then
65
+ TARGET_NODE_BINS+=("$exact_dir/node")
66
+ fi
67
+ else
68
+ # Major only — collect all patch versions
69
+ for dir in "$HOME/.nvm/versions/node"/v${TARGET_MAJOR}.*/bin; do
70
+ if [ -f "$dir/node" ]; then
71
+ TARGET_NODE_BINS+=("$dir/node")
72
+ fi
73
+ done
74
+ fi
75
+
76
+ if [ ${#TARGET_NODE_BINS[@]} -eq 0 ]; then
77
+ echo "āŒ Node $TARGET_VERSION not found in ~/.nvm/versions/node/"
78
+ echo " Run: nvm install $TARGET_VERSION"
79
+ exit 1
80
+ fi
81
+
82
+ # Use the last (latest patch) for building
83
+ TARGET_NODE_BIN="${TARGET_NODE_BINS[${#TARGET_NODE_BINS[@]}-1]}"
84
+ TARGET_NODE_NPM="$(dirname "$TARGET_NODE_BIN")/npm"
85
+ TARGET_NODE_DIR="$(dirname "$TARGET_NODE_BIN")"
86
+ TARGET_NODE_ACTUAL_VERSION="$("$TARGET_NODE_BIN" --version)"
87
+
88
+ echo "šŸŽÆ Found Node $TARGET_VERSION installs: ${TARGET_NODE_BINS[*]}"
89
+ echo "šŸ”Ø Building with: $TARGET_NODE_BIN ($TARGET_NODE_ACTUAL_VERSION)"
90
+
91
+ # --- Pick the right isolated-vm version for the target node ---
92
+ # isolated-vm@5.x supports Node <22, isolated-vm@6.x requires Node >=22
93
+ if [ "$TARGET_MAJOR" -ge 22 ]; then
94
+ IVM_VERSION="^6.0.0"
95
+ echo "šŸ“Œ Using isolated-vm@6.x (Node >= 22)"
96
+ else
97
+ IVM_VERSION="^5.0.4"
98
+ echo "šŸ“Œ Using isolated-vm@5.x (Node < 22)"
99
+ fi
100
+
101
+ # --- Create staging directory ---
102
+ STAGING_DIR="/tmp/knowhow-node-${TARGET_MAJOR}"
103
+ rm -rf "$STAGING_DIR"
104
+ mkdir -p "$STAGING_DIR"
105
+ echo ""
106
+ echo "šŸ“ Staging dir: $STAGING_DIR"
107
+
108
+ # --- Copy compiled output and package files into staging dir ---
109
+ echo "šŸ“‹ Copying compiled output to staging dir..."
110
+ cp -r "$PACKAGE_DIR/ts_build" "$STAGING_DIR/ts_build"
111
+ cp -r "$PACKAGE_DIR/bin" "$STAGING_DIR/bin" 2>/dev/null || true
112
+ cp "$PACKAGE_DIR/package.json" "$STAGING_DIR/package.json"
113
+ for item in README.md LICENSE .npmignore; do
114
+ [ -e "$PACKAGE_DIR/$item" ] && cp "$PACKAGE_DIR/$item" "$STAGING_DIR/" || true
115
+ done
116
+
117
+ # --- Patch package.json for target isolated-vm version ---
118
+ echo "šŸ“ Patching package.json for isolated-vm $IVM_VERSION..."
119
+ "$NODE20_BIN" -e "
120
+ const fs = require('fs');
121
+ const pkg = JSON.parse(fs.readFileSync('$STAGING_DIR/package.json', 'utf8'));
122
+ pkg.dependencies['isolated-vm'] = '$IVM_VERSION';
123
+ // Remove workspace protocol deps that won't resolve outside the monorepo
124
+ if (pkg.dependencies) {
125
+ for (const [k, v] of Object.entries(pkg.dependencies)) {
126
+ if (String(v).startsWith('workspace:')) delete pkg.dependencies[k];
127
+ }
128
+ }
129
+ fs.writeFileSync('$STAGING_DIR/package.json', JSON.stringify(pkg, null, 2));
130
+ console.log('āœ… package.json patched');
131
+ "
132
+
133
+ # --- Install deps in staging dir using target node ---
134
+ echo ""
135
+ echo "šŸ“¦ Installing dependencies in staging dir with Node $TARGET_MAJOR..."
136
+ cd "$STAGING_DIR"
137
+ # Prepend target node bin to PATH so npm/node-gyp uses the correct node version
138
+ PATH="$TARGET_NODE_DIR:$PATH" "$TARGET_NODE_NPM" install --no-save 2>&1
139
+ echo "āœ… Dependencies installed (isolated-vm compiled for Node $TARGET_MAJOR)"
140
+
141
+ # --- Symlink globally for ALL matching Node version installs ---
142
+ PKG_NAME="$("$NODE20_BIN" -e "console.log(require('$STAGING_DIR/package.json').name)")"
143
+ PKG_BIN_NAME="$("$NODE20_BIN" -e "const b=require('$STAGING_DIR/package.json').bin; console.log(Object.keys(b)[0])")"
144
+ PKG_BIN_FILE="$("$NODE20_BIN" -e "const b=require('$STAGING_DIR/package.json').bin; console.log(Object.values(b)[0])")"
145
+
146
+ echo ""
147
+ echo "šŸ”— Linking $PKG_NAME globally for all Node $TARGET_MAJOR installs..."
148
+ for node_bin in "${TARGET_NODE_BINS[@]}"; do
149
+ node_prefix="$("$node_bin" -e "const p=require('path');console.log(p.join(p.dirname(process.execPath),'..'))" 2>/dev/null)"
150
+ global_modules="$node_prefix/lib/node_modules"
151
+ global_bin="$node_prefix/bin"
152
+ mkdir -p "$global_modules/@tyvm"
153
+ rm -rf "$global_modules/$PKG_NAME"
154
+ ln -sf "$STAGING_DIR" "$global_modules/$PKG_NAME"
155
+ rm -f "$global_bin/$PKG_BIN_NAME"
156
+ ln -sf "$global_modules/$PKG_NAME/$PKG_BIN_FILE" "$global_bin/$PKG_BIN_NAME"
157
+ echo "āœ… $("$node_bin" --version): $global_modules/$PKG_NAME → $STAGING_DIR"
158
+ done
159
+
160
+ echo ""
161
+ echo "šŸŽ‰ Done! Switch to Node $TARGET_MAJOR and run: knowhow"
162
+ echo " nvm use $TARGET_MAJOR && knowhow"
@@ -459,6 +459,7 @@ export const includedTools = [
459
459
  },
460
460
  },
461
461
  required: ["provider"],
462
+ positional: true,
462
463
  },
463
464
  returns: {
464
465
  type: "object",
package/src/cli.ts CHANGED
@@ -138,7 +138,7 @@ async function main() {
138
138
  console.log(`Current version: ${version}`);
139
139
 
140
140
  console.log("šŸ“¦ Installing latest version from npm...");
141
- execSync("npm install -g knowhow@latest", {
141
+ execSync("npm install -g @tyvm/knowhow@latest", {
142
142
  stdio: "inherit",
143
143
  encoding: "utf-8",
144
144
  });
@@ -489,6 +489,23 @@ async function main() {
489
489
  }
490
490
  });
491
491
 
492
+ program
493
+ .command("cloudworker")
494
+ .description("Create or sync a cloud worker with your local knowhow config")
495
+ .option("--create", "Create a new cloud worker with synced config and files")
496
+ .option("--push <uid>", "Push/sync local config and files to an existing cloud worker")
497
+ .option("--name <name>", "Name for the cloud worker (used with --create)")
498
+ .option("--dry-run", "Print what would be synced without doing it")
499
+ .action(async (options) => {
500
+ try {
501
+ const { cloudWorker } = await import("./cloudWorker");
502
+ await cloudWorker(options);
503
+ } catch (error) {
504
+ console.error("Error running cloudworker:", error);
505
+ process.exit(1);
506
+ }
507
+ });
508
+
492
509
  program
493
510
  .command("script")
494
511
  .description("Run a local tool script file using the executeScript sandbox")
@@ -217,7 +217,12 @@ export class AIClient {
217
217
  for (const entry of providers) {
218
218
  const client = this.resolveClient(entry);
219
219
 
220
- if (!client) continue;
220
+ if (!client) {
221
+ if (entry.provider === "knowhow") {
222
+ console.warn(`āš ļø Knowhow provider is not logged in. Run 'knowhow login' to enable Knowhow models.`);
223
+ }
224
+ continue;
225
+ }
221
226
 
222
227
  const reg = this.providerRegistry[entry.provider];
223
228
 
@@ -493,11 +498,19 @@ export class AIClient {
493
498
  return foundByModel;
494
499
  }
495
500
 
496
- console.log("unable to find", {
497
- provider,
498
- model,
499
- all: this.listAllModels(),
500
- });
501
+ const allModels = this.listAllModels();
502
+ const hasKnowhowModels =
503
+ allModels["knowhow"] && allModels["knowhow"].length > 0;
504
+ const knowhowIsConfigured = Object.keys(allModels).includes("knowhow");
505
+
506
+ console.warn(`āš ļø Unable to find model '${model}' for provider '${provider}'.`);
507
+ console.warn(` Available providers: ${Object.keys(allModels).join(", ") || "(none)"}`);
508
+
509
+ if (!hasKnowhowModels && !knowhowIsConfigured) {
510
+ console.warn(` Tip: Run 'knowhow login' to enable Knowhow models.`);
511
+ } else if (!hasKnowhowModels) {
512
+ console.warn(` Tip: The Knowhow provider returned no models. Try running 'knowhow login' to re-authenticate.`);
513
+ }
501
514
 
502
515
  return { provider, model };
503
516
  }
@@ -0,0 +1,314 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { glob } from "glob";
4
+ import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
5
+ import { loadJwt } from "./login";
6
+ import { getConfig, updateConfig, getLanguageConfig } from "./config";
7
+ import { services } from "./services";
8
+ import { Language, Config } from "./types";
9
+ import { S3Service } from "./services/S3";
10
+
11
+ export interface CloudWorkerOptions {
12
+ create?: boolean;
13
+ push?: string; // uid of existing cloud worker
14
+ name?: string; // optional name for create
15
+ apiUrl?: string;
16
+ dryRun?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Represents a file to be synced to the remote cloud worker
21
+ */
22
+ interface FileToSync {
23
+ localPath: string;
24
+ remotePath: string;
25
+ downloadLocalPath?: string; // override localPath used when worker downloads the file
26
+ }
27
+
28
+ /**
29
+ * Build the worker config JSON from the local knowhow config
30
+ */
31
+ function buildWorkerConfigJson(config: Config, files: { remotePath: string; localPath: string; direction?: string }[]) {
32
+ return {
33
+ promptsDir: config.promptsDir,
34
+ modules: config.modules,
35
+ plugins: config.plugins,
36
+ lintCommands: config.lintCommands,
37
+ embedSources: config.embedSources,
38
+ sources: config.sources,
39
+ agents: config.agents,
40
+ files,
41
+ worker: {
42
+ tunnel: {
43
+ allowedPorts: config.worker?.tunnel?.allowedPorts ?? [],
44
+ },
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Collect all files from the .knowhow directory that should be synced
51
+ */
52
+ async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
53
+ const filesToSync: FileToSync[] = [];
54
+
55
+ // Helper to add file if it exists
56
+ const addIfExists = (localPath: string, remotePath: string) => {
57
+ if (fs.existsSync(localPath)) {
58
+ filesToSync.push({ localPath, remotePath });
59
+ }
60
+ };
61
+
62
+ // .knowhow/language.json
63
+ addIfExists(".knowhow/language.json", `${projectName}/.knowhow/language.json`);
64
+
65
+ // .knowhow/hashes.json
66
+ addIfExists(".knowhow/hashes.json", `${projectName}/.knowhow/hashes.json`);
67
+
68
+ // .knowhow/prompts/**/*
69
+ const promptFiles = await glob(".knowhow/prompts/**/*", { nodir: true });
70
+ for (const filePath of promptFiles) {
71
+ const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
72
+ const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
73
+ filesToSync.push({ localPath: filePath, remotePath });
74
+ }
75
+
76
+ // .knowhow/scripts/**/* (if exists)
77
+ if (fs.existsSync(".knowhow/scripts")) {
78
+ const scriptFiles = await glob(".knowhow/scripts/**/*", { nodir: true });
79
+ for (const filePath of scriptFiles) {
80
+ const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
81
+ const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
82
+ filesToSync.push({ localPath: filePath, remotePath });
83
+ }
84
+ }
85
+
86
+ // .knowhow/skills/**/* (if exists)
87
+ if (fs.existsSync(".knowhow/skills")) {
88
+ const skillFiles = await glob(".knowhow/skills/**/*", { nodir: true });
89
+ for (const filePath of skillFiles) {
90
+ const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
91
+ const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
92
+ filesToSync.push({ localPath: filePath, remotePath });
93
+ }
94
+ }
95
+
96
+ return filesToSync;
97
+ }
98
+
99
+ /**
100
+ * Collect files referenced in language.json sources
101
+ */
102
+ async function collectLanguageReferencedFiles(
103
+ language: Language,
104
+ projectName: string
105
+ ): Promise<FileToSync[]> {
106
+ const filesToSync: FileToSync[] = [];
107
+
108
+ for (const term of Object.keys(language)) {
109
+ const entry = language[term];
110
+ if (!entry.sources) continue;
111
+
112
+ for (const source of entry.sources) {
113
+ if (source.kind !== "file" || !source.data) continue;
114
+
115
+ for (const filePath of source.data) {
116
+ // Normalize the path (strip leading ./)
117
+ const normalizedPath = filePath.replace(/^\.\//, "");
118
+
119
+ // Skip the main knowhow config — it should not be synced to the language folder
120
+ // as it would overwrite the worker's own config
121
+ if (normalizedPath === ".knowhow/knowhow.json") continue;
122
+
123
+ if (fs.existsSync(normalizedPath)) {
124
+ const basename = path.basename(normalizedPath);
125
+ const remotePath = `${projectName}/.knowhow/language/${basename}`;
126
+ // localPath is the original path so the worker downloads it to the right place
127
+ filesToSync.push({ localPath: normalizedPath, remotePath, downloadLocalPath: normalizedPath });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ return filesToSync;
134
+ }
135
+
136
+ /**
137
+ * Upload a single file to the cloud worker's file storage
138
+ */
139
+ async function uploadSingleFile(
140
+ client: KnowhowSimpleClient,
141
+ s3Service: S3Service,
142
+ localPath: string,
143
+ remotePath: string,
144
+ dryRun: boolean
145
+ ): Promise<void> {
146
+ console.log(` ā¬†ļø Uploading ${localPath} → ${remotePath}`);
147
+
148
+ if (dryRun) {
149
+ console.log(` [DRY RUN] Would upload from ${localPath}`);
150
+ return;
151
+ }
152
+
153
+ if (!fs.existsSync(localPath)) {
154
+ console.warn(` āš ļø Local file not found, skipping: ${localPath}`);
155
+ return;
156
+ }
157
+
158
+ const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
159
+ await s3Service.uploadToPresignedUrl(presignedUrl, localPath);
160
+ await client.markOrgFileUploadComplete(remotePath);
161
+
162
+ const stats = fs.statSync(localPath);
163
+ console.log(` āœ“ Uploaded ${stats.size} bytes`);
164
+ }
165
+
166
+ /**
167
+ * Main cloudWorker command handler
168
+ */
169
+ export async function cloudWorker(options: CloudWorkerOptions) {
170
+ const {
171
+ create = false,
172
+ push,
173
+ name,
174
+ apiUrl = KNOWHOW_API_URL,
175
+ dryRun = false,
176
+ } = options;
177
+
178
+ if (!create && !push) {
179
+ console.error("āŒ Please specify --create or --push <uid>");
180
+ process.exit(1);
181
+ }
182
+
183
+ // Load JWT token
184
+ const jwt = await loadJwt();
185
+ if (!jwt) {
186
+ console.error("āŒ No JWT token found. Please run 'knowhow login' first.");
187
+ process.exit(1);
188
+ }
189
+
190
+ // Load local config
191
+ const config = await getConfig();
192
+ if (!config || Object.keys(config).length === 0) {
193
+ console.error("āŒ No knowhow config found. Please run 'knowhow init' first.");
194
+ process.exit(1);
195
+ }
196
+
197
+ // Load language config
198
+ const language = await getLanguageConfig();
199
+
200
+ // Get project name from current directory
201
+ const projectName = path.basename(process.cwd());
202
+ console.log(`šŸ“ Project name: ${projectName}`);
203
+
204
+ // Create API client
205
+ const client = new KnowhowSimpleClient(apiUrl, jwt);
206
+
207
+ // Get S3 service
208
+ const { AwsS3 } = services();
209
+
210
+ // Step 1: Collect all files to sync
211
+ console.log("\nšŸ“‚ Collecting files to sync...");
212
+ const mainFiles = await collectFilesToSync(projectName);
213
+ const languageFiles = await collectLanguageReferencedFiles(language, projectName);
214
+
215
+ // Deduplicate by remotePath
216
+ const allFilesMap = new Map<string, FileToSync>();
217
+ for (const f of [...mainFiles, ...languageFiles]) {
218
+ allFilesMap.set(f.remotePath, f);
219
+ }
220
+ const allFiles = Array.from(allFilesMap.values());
221
+
222
+ console.log(` Found ${allFiles.length} files to sync`);
223
+
224
+ if (dryRun) {
225
+ console.log("\nšŸ“‹ Files that would be synced:");
226
+ for (const f of allFiles) {
227
+ console.log(` ${f.localPath} → ${f.remotePath}`);
228
+ }
229
+ }
230
+
231
+ // Step 2: Build the config.files array for all synced files
232
+ const configFilesEntries = allFiles.map((f) => ({
233
+ remotePath: f.remotePath,
234
+ localPath: f.downloadLocalPath ?? f.localPath,
235
+ direction: "download" as const,
236
+ }));
237
+
238
+ // Step 3: Update config.files and save
239
+ console.log("\nšŸ’¾ Updating config.files with sync entries...");
240
+ if (!dryRun) {
241
+ // Preserve any existing files entries not in our set
242
+ const existingFiles = config.files || [];
243
+ const newRemotePaths = new Set(configFilesEntries.map((e) => e.remotePath));
244
+
245
+ // Keep entries that don't overlap with new ones
246
+ const preserved = existingFiles.filter(
247
+ (e) => !newRemotePaths.has(e.remotePath)
248
+ );
249
+
250
+ config.files = [...preserved, ...configFilesEntries];
251
+ await updateConfig(config);
252
+ console.log(` āœ“ Updated config with ${config.files.length} file entries`);
253
+ } else {
254
+ console.log(` [DRY RUN] Would update config with ${configFilesEntries.length} file entries`);
255
+ }
256
+
257
+ // Step 4: Build workerConfigJson
258
+ const workerConfigJson = buildWorkerConfigJson(config, configFilesEntries);
259
+
260
+ // Step 5: Upload all files
261
+ console.log(`\nšŸš€ Uploading ${allFiles.length} files...`);
262
+ let successCount = 0;
263
+ let failCount = 0;
264
+
265
+ for (const file of allFiles) {
266
+ try {
267
+ await uploadSingleFile(client, AwsS3, file.localPath, file.remotePath, dryRun);
268
+ successCount++;
269
+ } catch (error) {
270
+ console.error(` āŒ Failed to upload ${file.localPath}: ${error.message}`);
271
+ failCount++;
272
+ }
273
+ }
274
+
275
+ console.log(`\n āœ“ Upload complete: ${successCount} succeeded, ${failCount} failed`);
276
+
277
+ // Step 6: Create or update cloud worker
278
+ if (create) {
279
+ const workerName = name || `${projectName}-worker`;
280
+ console.log(`\nšŸŒ©ļø Creating cloud worker "${workerName}"...`);
281
+
282
+ if (dryRun) {
283
+ console.log(` [DRY RUN] Would create cloud worker with name: ${workerName}`);
284
+ console.log(` [DRY RUN] workerConfigJson:`, JSON.stringify(workerConfigJson, null, 2));
285
+ } else {
286
+ const result = await client.createCloudWorker({
287
+ name: workerName,
288
+ workerConfigJson,
289
+ });
290
+ const createdWorker = result.data;
291
+ console.log(` āœ“ Cloud worker created!`);
292
+ console.log(` ID: ${createdWorker.id}`);
293
+ console.log(` Name: ${createdWorker.name}`);
294
+ console.log(`\nšŸ’” To push updates later, run:`);
295
+ console.log(` knowhow cloudworker --push ${createdWorker.id}`);
296
+ }
297
+ } else if (push) {
298
+ console.log(`\nšŸŒ©ļø Updating cloud worker "${push}"...`);
299
+
300
+ if (dryRun) {
301
+ console.log(` [DRY RUN] Would update cloud worker ${push}`);
302
+ console.log(` [DRY RUN] workerConfigJson:`, JSON.stringify(workerConfigJson, null, 2));
303
+ } else {
304
+ await client.updateCloudWorker(push, { workerConfigJson });
305
+ console.log(` āœ“ Cloud worker updated!`);
306
+ }
307
+ }
308
+
309
+ if (failCount > 0) {
310
+ console.warn(`\nāš ļø ${failCount} file(s) failed to upload.`);
311
+ } else {
312
+ console.log(`\nāœ… Cloud worker sync complete!`);
313
+ }
314
+ }
@@ -648,4 +648,48 @@ export class KnowhowSimpleClient {
648
648
  );
649
649
  return response.data;
650
650
  }
651
+
652
+ // ============================================
653
+ // Cloud Worker Methods
654
+ // ============================================
655
+
656
+ /**
657
+ * List all cloud workers for the current user's org
658
+ */
659
+ async listCloudWorkers() {
660
+ await this.checkJwt();
661
+ return http.get<
662
+ { id: string; name: string; status: string; workerConfigJson?: Record<string, unknown> }[]
663
+ >(`${this.baseUrl}/api/cloud-workers`, { headers: this.headers });
664
+ }
665
+
666
+ /**
667
+ * Create a new cloud worker
668
+ */
669
+ async createCloudWorker(data: {
670
+ name: string;
671
+ workerConfigJson?: Record<string, unknown>;
672
+ }) {
673
+ await this.checkJwt();
674
+ return http.post<{ id: string; name: string; status: string; workerConfigJson?: Record<string, unknown> }>(
675
+ `${this.baseUrl}/api/cloud-workers`,
676
+ data,
677
+ { headers: this.headers }
678
+ );
679
+ }
680
+
681
+ /**
682
+ * Update an existing cloud worker
683
+ */
684
+ async updateCloudWorker(
685
+ id: string,
686
+ data: { workerConfigJson?: Record<string, unknown> }
687
+ ) {
688
+ await this.checkJwt();
689
+ return http.put<{ id: string; name: string; status: string; workerConfigJson?: Record<string, unknown> }>(
690
+ `${this.baseUrl}/api/cloud-workers/${id}`,
691
+ data,
692
+ { headers: this.headers }
693
+ );
694
+ }
651
695
  }
@@ -125,23 +125,24 @@ export class LazyToolsService extends ToolsService {
125
125
  async callTool(toolCall: ToolCall, enabledTools?: string[]) {
126
126
  const functionName = toolCall.function.name;
127
127
 
128
- // If no explicit enabledTools list was provided and the tool isn't currently
129
- // visible, check if it exists in allTools and auto-enable it
130
- if (!enabledTools) {
131
- const isCurrentlyEnabled = this.tools.some(
132
- (t) => t.function.name === functionName
133
- );
134
- const existsInAll = this.allTools.some(
135
- (t) => t.function.name === functionName
136
- );
128
+ // If the tool isn't currently visible but exists in allTools, auto-enable it.
129
+ // This handles the case where the agent explicitly calls a tool without first
130
+ // enabling it via listAvailableTools/enableTools.
131
+ const isCurrentlyEnabled = this.tools.some(
132
+ (t) => t.function.name === functionName
133
+ );
134
+ const existsInAll = this.allTools.some(
135
+ (t) => t.function.name === functionName
136
+ );
137
137
 
138
- if (!isCurrentlyEnabled && existsInAll) {
139
- // Auto-enable by adding the tool name as an exact pattern
140
- this.enableTools([functionName]);
141
- }
138
+ if (!isCurrentlyEnabled && existsInAll) {
139
+ // Auto-enable by adding the tool name as an exact pattern
140
+ this.enableTools([functionName]);
142
141
  }
143
142
 
144
- return super.callTool(toolCall, enabledTools ?? this.getToolNames());
143
+ // Always use the current enabled tool names after any auto-enable above,
144
+ // so the base class check sees the freshly-enabled tool in the allowed list.
145
+ return super.callTool(toolCall, this.getToolNames());
145
146
  }
146
147
 
147
148
  // Internal: Update visible tools based on patterns