@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 +2 -1
- package/scripts/build-for-node.sh +162 -0
- package/src/agents/tools/list.ts +1 -0
- package/src/cli.ts +18 -1
- package/src/clients/index.ts +19 -6
- package/src/cloudWorker.ts +314 -0
- package/src/services/KnowhowClient.ts +44 -0
- package/src/services/LazyToolsService.ts +15 -14
- package/test-ai-completion.ts +39 -0
- package/test-mcp-args.ts +71 -0
- package/test-tools-service.ts +45 -0
- package/ts_build/package.json +2 -1
- package/ts_build/src/agents/tools/list.js +1 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/cli.js +18 -1
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/index.js +16 -6
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/cloudWorker.d.ts +8 -0
- package/ts_build/src/cloudWorker.js +239 -0
- package/ts_build/src/cloudWorker.js.map +1 -0
- package/ts_build/src/services/KnowhowClient.d.ts +23 -0
- package/ts_build/src/services/KnowhowClient.js +12 -0
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.js +5 -7
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tyvm/knowhow",
|
|
3
|
-
"version": "0.0.
|
|
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"
|
package/src/agents/tools/list.ts
CHANGED
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")
|
package/src/clients/index.ts
CHANGED
|
@@ -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)
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|