@tyvm/knowhow 0.0.108-dev.ed88cf4 ā 0.0.109-dev.05fe5a0
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 +1 -1
- package/scripts/build-for-node.sh +10 -24
- package/src/chat/modules/AgentModule.ts +7 -2
- package/src/chat/modules/SessionsModule.ts +40 -1
- package/src/cloudWorker.ts +110 -122
- package/src/commands/modules.ts +57 -22
- package/src/commands/workers.ts +9 -1
- package/src/fileSync.ts +50 -17
- package/src/services/KnowhowClient.ts +12 -2
- package/src/services/S3.ts +0 -10
- package/src/services/modules/index.ts +27 -3
- package/ts_build/package.json +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/cloudWorker.d.ts +5 -0
- package/ts_build/src/cloudWorker.js +69 -66
- package/ts_build/src/cloudWorker.js.map +1 -1
- package/ts_build/src/commands/modules.js +66 -19
- package/ts_build/src/commands/modules.js.map +1 -1
- package/ts_build/src/commands/workers.js +6 -1
- package/ts_build/src/commands/workers.js.map +1 -1
- package/ts_build/src/fileSync.d.ts +6 -0
- package/ts_build/src/fileSync.js +37 -12
- package/ts_build/src/fileSync.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +1 -1
- package/ts_build/src/services/KnowhowClient.js +8 -2
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/S3.js +0 -7
- package/ts_build/src/services/S3.js.map +1 -1
- package/ts_build/src/services/modules/index.js +22 -3
- package/ts_build/src/services/modules/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -7,11 +7,10 @@
|
|
|
7
7
|
# This script:
|
|
8
8
|
# 1. Compiles TypeScript with Node 20 (required for workspace deps)
|
|
9
9
|
# 2. Creates /tmp/knowhow-node-<major> with the compiled output
|
|
10
|
-
# 3.
|
|
11
|
-
# 4. Symlinks the package globally for ALL installed nvm versions matching the target
|
|
10
|
+
# 3. Symlinks the package globally for ALL installed nvm versions matching the target
|
|
12
11
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
12
|
+
# Note: isolated-vm is now in @tyvm/knowhow-module-script ā install that separately
|
|
13
|
+
# for the correct node version if you need script execution support.
|
|
15
14
|
|
|
16
15
|
set -e
|
|
17
16
|
|
|
@@ -81,23 +80,11 @@ fi
|
|
|
81
80
|
|
|
82
81
|
# Use the last (latest patch) for building
|
|
83
82
|
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
83
|
TARGET_NODE_ACTUAL_VERSION="$("$TARGET_NODE_BIN" --version)"
|
|
87
84
|
|
|
88
85
|
echo "šÆ Found Node $TARGET_VERSION installs: ${TARGET_NODE_BINS[*]}"
|
|
89
86
|
echo "šØ Building with: $TARGET_NODE_BIN ($TARGET_NODE_ACTUAL_VERSION)"
|
|
90
87
|
|
|
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
88
|
# --- Create staging directory ---
|
|
102
89
|
STAGING_DIR="/tmp/knowhow-node-${TARGET_MAJOR}"
|
|
103
90
|
rm -rf "$STAGING_DIR"
|
|
@@ -114,13 +101,11 @@ for item in README.md LICENSE .npmignore; do
|
|
|
114
101
|
[ -e "$PACKAGE_DIR/$item" ] && cp "$PACKAGE_DIR/$item" "$STAGING_DIR/" || true
|
|
115
102
|
done
|
|
116
103
|
|
|
117
|
-
# --- Patch package.json
|
|
118
|
-
echo "š Patching package.json
|
|
104
|
+
# --- Patch package.json to remove workspace protocol deps ---
|
|
105
|
+
echo "š Patching package.json..."
|
|
119
106
|
"$NODE20_BIN" -e "
|
|
120
107
|
const fs = require('fs');
|
|
121
108
|
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
109
|
if (pkg.dependencies) {
|
|
125
110
|
for (const [k, v] of Object.entries(pkg.dependencies)) {
|
|
126
111
|
if (String(v).startsWith('workspace:')) delete pkg.dependencies[k];
|
|
@@ -130,13 +115,14 @@ echo "š Patching package.json for isolated-vm $IVM_VERSION..."
|
|
|
130
115
|
console.log('ā
package.json patched');
|
|
131
116
|
"
|
|
132
117
|
|
|
133
|
-
# --- Install
|
|
118
|
+
# --- Install dependencies in staging dir with target Node ---
|
|
119
|
+
TARGET_NODE_NPM="$(dirname "$TARGET_NODE_BIN")/npm"
|
|
134
120
|
echo ""
|
|
135
121
|
echo "š¦ Installing dependencies in staging dir with Node $TARGET_MAJOR..."
|
|
136
122
|
cd "$STAGING_DIR"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
123
|
+
"$TARGET_NODE_NPM" install --omit=dev
|
|
124
|
+
echo "ā
Dependencies installed"
|
|
125
|
+
cd "$PACKAGE_DIR"
|
|
140
126
|
|
|
141
127
|
# --- Symlink globally for ALL matching Node version installs ---
|
|
142
128
|
PKG_NAME="$("$NODE20_BIN" -e "console.log(require('$STAGING_DIR/package.json').name)")"
|
|
@@ -517,7 +517,12 @@ export class AgentModule extends BaseChatModule {
|
|
|
517
517
|
|
|
518
518
|
// Restore the full message history from the last thread
|
|
519
519
|
const threads = session.threads || [];
|
|
520
|
-
|
|
520
|
+
// Guard against sessions saved with a flat Message[] instead of Message[][]
|
|
521
|
+
// (a bug where threadUpdate emitted a single thread instead of all threads)
|
|
522
|
+
const normalizedThreads: Message[][] = threads.length > 0 && !Array.isArray(threads[0])
|
|
523
|
+
? [threads as unknown as Message[]]
|
|
524
|
+
: threads as Message[][];
|
|
525
|
+
const lastThread = normalizedThreads.length > 0 ? normalizedThreads[normalizedThreads.length - 1] : [];
|
|
521
526
|
const resumeMessages = [...lastThread];
|
|
522
527
|
|
|
523
528
|
// Append the resume prompt to the last user message (or add a new one)
|
|
@@ -701,7 +706,7 @@ export class AgentModule extends BaseChatModule {
|
|
|
701
706
|
|
|
702
707
|
// Set up session update listener
|
|
703
708
|
const threadUpdateHandler = async (threadState: any) => {
|
|
704
|
-
this.updateSession(taskId,
|
|
709
|
+
this.updateSession(taskId, agent.getThreads());
|
|
705
710
|
taskInfo.totalCost = agent.getTotalCostUsd();
|
|
706
711
|
};
|
|
707
712
|
agent.agentEvents.on(agent.eventTypes.threadUpdate, threadUpdateHandler);
|
|
@@ -362,8 +362,47 @@ export class SessionsModule extends BaseChatModule {
|
|
|
362
362
|
// Check filesystem agent (may have metadata with threads)
|
|
363
363
|
const fsAgentPath = path.join(".knowhow", "processes", "agents", id);
|
|
364
364
|
if (fs.existsSync(fsAgentPath)) {
|
|
365
|
+
// Try to load threads from metadata.json and resume
|
|
366
|
+
const metadataPath = path.join(fsAgentPath, "metadata.json");
|
|
367
|
+
if (fs.existsSync(metadataPath)) {
|
|
368
|
+
try {
|
|
369
|
+
const raw = fs.readFileSync(metadataPath, "utf-8");
|
|
370
|
+
const metadata = JSON.parse(raw);
|
|
371
|
+
const threads: any[] = metadata.threads || [];
|
|
372
|
+
const agentName = metadata.agentName || "Developer";
|
|
373
|
+
|
|
374
|
+
// Try to get initialInput from the saved session file (more complete)
|
|
375
|
+
// since metadata.json doesn't always store it
|
|
376
|
+
const savedSession = sessionManager.loadSession(id);
|
|
377
|
+
const initialInput = savedSession?.initialInput || metadata.initialInput || metadata.prompt || "";
|
|
378
|
+
|
|
379
|
+
console.log(`\nš Found task in filesystem: ${id}`);
|
|
380
|
+
console.log(` Agent : ${agentName}`);
|
|
381
|
+
console.log(` Task : ${initialInput}`);
|
|
382
|
+
console.log(` Status : ${metadata.status || "unknown"}`);
|
|
383
|
+
|
|
384
|
+
const additionalContext = await this.chatService?.getInput(
|
|
385
|
+
"Add any additional context for resuming this session (or press Enter to skip): "
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
// Normalize threads: if flat Message[] (old buggy format), wrap in array
|
|
389
|
+
const normalizedThreads = threads.length > 0 && !Array.isArray(threads[0])
|
|
390
|
+
? [threads]
|
|
391
|
+
: threads;
|
|
392
|
+
|
|
393
|
+
await this.agentModule.resumeFromMessages({
|
|
394
|
+
agentName,
|
|
395
|
+
taskId: id,
|
|
396
|
+
threads: normalizedThreads,
|
|
397
|
+
input: additionalContext?.trim() || initialInput || "",
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
} catch (e: any) {
|
|
401
|
+
console.error(`ā ļø Failed to load metadata for task ${id}: ${e.message}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
365
404
|
console.log(
|
|
366
|
-
`ā ļø Task ${id} exists in the filesystem but has no saved session.\n` +
|
|
405
|
+
`ā ļø Task ${id} exists in the filesystem but has no saved session or metadata.\n` +
|
|
367
406
|
` Use /attach ${id} if it is still running.`
|
|
368
407
|
);
|
|
369
408
|
return;
|
package/src/cloudWorker.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { loadJwt } from "./login";
|
|
|
5
5
|
import { getConfig, updateConfig, getLanguageConfig } from "./config";
|
|
6
6
|
import { services } from "./services";
|
|
7
7
|
import { Language, Config, McpConfig } from "./types";
|
|
8
|
-
import {
|
|
8
|
+
import { uploadFile, uploadDirectory } from "./fileSync";
|
|
9
9
|
|
|
10
10
|
export interface CloudWorkerPullOptions {
|
|
11
11
|
id: string;
|
|
@@ -15,6 +15,7 @@ export interface CloudWorkerPullOptions {
|
|
|
15
15
|
export interface CloudWorkerOptions {
|
|
16
16
|
create?: boolean;
|
|
17
17
|
push?: string; // uid of existing cloud worker
|
|
18
|
+
init?: boolean; // initialize config.files entries (mutates config)
|
|
18
19
|
name?: string; // optional name for create
|
|
19
20
|
apiUrl?: string;
|
|
20
21
|
dryRun?: boolean;
|
|
@@ -30,25 +31,6 @@ interface FileToSync {
|
|
|
30
31
|
isDirectory?: boolean; // true if this represents a whole directory
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
/**
|
|
34
|
-
* Recursively list all files in a local directory, returning relative paths
|
|
35
|
-
*/
|
|
36
|
-
function listFilesRecursively(dir: string): string[] {
|
|
37
|
-
const results: string[] = [];
|
|
38
|
-
if (!fs.existsSync(dir)) return results;
|
|
39
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
40
|
-
for (const entry of entries) {
|
|
41
|
-
if (entry.isDirectory()) {
|
|
42
|
-
listFilesRecursively(path.join(dir, entry.name)).forEach((f) =>
|
|
43
|
-
results.push(entry.name + "/" + f)
|
|
44
|
-
);
|
|
45
|
-
} else {
|
|
46
|
-
results.push(entry.name);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return results;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
34
|
/**
|
|
53
35
|
* Build the worker config JSON from the local knowhow config
|
|
54
36
|
*/
|
|
@@ -71,21 +53,19 @@ function buildWorkerConfigJson(config: Config, files: { remotePath: string; loca
|
|
|
71
53
|
}
|
|
72
54
|
|
|
73
55
|
/**
|
|
74
|
-
* Collect all files from the .knowhow directory that should be synced
|
|
75
|
-
*
|
|
76
|
-
*
|
|
56
|
+
* Collect all files from the .knowhow directory that should be synced.
|
|
57
|
+
* Only includes files/directories that currently exist locally.
|
|
58
|
+
* Used by --init to populate config.files.
|
|
77
59
|
*/
|
|
78
60
|
async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
|
|
79
61
|
const filesToSync: FileToSync[] = [];
|
|
80
62
|
|
|
81
|
-
// Helper to add file if it exists
|
|
82
63
|
const addIfExists = (localPath: string, remotePath: string) => {
|
|
83
64
|
if (fs.existsSync(localPath)) {
|
|
84
65
|
filesToSync.push({ localPath, remotePath });
|
|
85
66
|
}
|
|
86
67
|
};
|
|
87
68
|
|
|
88
|
-
// Helper to add a directory entry if it exists (trailing slash = directory mode)
|
|
89
69
|
const addDirIfExists = (localPath: string, remotePath: string) => {
|
|
90
70
|
if (fs.existsSync(localPath)) {
|
|
91
71
|
filesToSync.push({ localPath: localPath + "/", remotePath: remotePath + "/", isDirectory: true });
|
|
@@ -108,7 +88,9 @@ async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
|
|
|
108
88
|
}
|
|
109
89
|
|
|
110
90
|
/**
|
|
111
|
-
* Collect files referenced in language.json sources
|
|
91
|
+
* Collect files referenced in language.json sources.
|
|
92
|
+
* These are always re-collected on both --init and --push so that new
|
|
93
|
+
* language term sources are picked up automatically.
|
|
112
94
|
*/
|
|
113
95
|
async function collectLanguageReferencedFiles(
|
|
114
96
|
language: Language,
|
|
@@ -124,17 +106,14 @@ async function collectLanguageReferencedFiles(
|
|
|
124
106
|
if (source.kind !== "file" || !source.data) continue;
|
|
125
107
|
|
|
126
108
|
for (const filePath of source.data) {
|
|
127
|
-
// Normalize the path (strip leading ./)
|
|
128
109
|
const normalizedPath = filePath.replace(/^\.\//, "");
|
|
129
110
|
|
|
130
111
|
// Skip the main knowhow config ā it should not be synced to the language folder
|
|
131
|
-
// as it would overwrite the worker's own config
|
|
132
112
|
if (normalizedPath === ".knowhow/knowhow.json") continue;
|
|
133
113
|
|
|
134
114
|
if (fs.existsSync(normalizedPath)) {
|
|
135
115
|
const basename = path.basename(normalizedPath);
|
|
136
116
|
const remotePath = `${projectName}/.knowhow/language/${basename}`;
|
|
137
|
-
// localPath is the original path so the worker downloads it to the right place
|
|
138
117
|
filesToSync.push({ localPath: normalizedPath, remotePath, downloadLocalPath: normalizedPath });
|
|
139
118
|
}
|
|
140
119
|
}
|
|
@@ -145,37 +124,83 @@ async function collectLanguageReferencedFiles(
|
|
|
145
124
|
}
|
|
146
125
|
|
|
147
126
|
/**
|
|
148
|
-
*
|
|
127
|
+
* Collect language-referenced files if language.json is present in the
|
|
128
|
+
* given config.files entries. Returns empty array if language.json is not
|
|
129
|
+
* configured for sync.
|
|
130
|
+
*/
|
|
131
|
+
async function collectLanguageFilesIfConfigured(
|
|
132
|
+
configFiles: { remotePath: string; localPath: string }[],
|
|
133
|
+
projectName: string
|
|
134
|
+
): Promise<FileToSync[]> {
|
|
135
|
+
const syncingLanguage = configFiles.some(
|
|
136
|
+
(f) => !f.remotePath.endsWith("/") && f.remotePath.endsWith("language.json")
|
|
137
|
+
);
|
|
138
|
+
if (!syncingLanguage) return [];
|
|
139
|
+
|
|
140
|
+
const language = await getLanguageConfig();
|
|
141
|
+
return collectLanguageReferencedFiles(language, projectName);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Initialize the local config.files entries based on what exists in .knowhow/.
|
|
146
|
+
* This is the --init step ā mutates config. Run once to set up sync entries.
|
|
147
|
+
* language-referenced files are also collected if language.json is present.
|
|
149
148
|
*/
|
|
150
|
-
async function
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
)
|
|
157
|
-
console.log(` ā¬ļø Uploading ${localPath} ā ${remotePath}`);
|
|
158
|
-
|
|
159
|
-
if (dryRun) {
|
|
160
|
-
console.log(` [DRY RUN] Would upload from ${localPath}`);
|
|
161
|
-
return;
|
|
149
|
+
export async function initCloudWorker(options: { apiUrl?: string; dryRun?: boolean } = {}) {
|
|
150
|
+
const { dryRun = false } = options;
|
|
151
|
+
|
|
152
|
+
const config = await getConfig();
|
|
153
|
+
if (!config || Object.keys(config).length === 0) {
|
|
154
|
+
console.error("ā No knowhow config found. Please run 'knowhow init' first.");
|
|
155
|
+
process.exit(1);
|
|
162
156
|
}
|
|
163
157
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
const projectName = path.basename(process.cwd());
|
|
159
|
+
console.log(`š Project name: ${projectName}`);
|
|
160
|
+
|
|
161
|
+
console.log("\nš Collecting files to sync...");
|
|
162
|
+
const mainFiles = await collectFilesToSync(projectName);
|
|
163
|
+
const languageFiles = await collectLanguageFilesIfConfigured(mainFiles, projectName);
|
|
164
|
+
|
|
165
|
+
if (languageFiles.length === 0 && !mainFiles.some((f) => f.remotePath.endsWith("language.json"))) {
|
|
166
|
+
console.log(" ā¹ļø Skipping language-referenced files (language.json not found locally)");
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
// Deduplicate by remotePath
|
|
170
|
+
const allFilesMap = new Map<string, FileToSync>();
|
|
171
|
+
for (const f of [...mainFiles, ...languageFiles]) {
|
|
172
|
+
allFilesMap.set(f.remotePath, f);
|
|
173
|
+
}
|
|
174
|
+
const allFiles = Array.from(allFilesMap.values());
|
|
175
|
+
|
|
176
|
+
console.log(` Found ${allFiles.length} files to register`);
|
|
172
177
|
|
|
173
|
-
const
|
|
174
|
-
|
|
178
|
+
const configFilesEntries = allFiles.map((f) => ({
|
|
179
|
+
remotePath: f.remotePath,
|
|
180
|
+
localPath: f.downloadLocalPath ?? f.localPath,
|
|
181
|
+
direction: "download" as const,
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
console.log("\nš¾ Updating config.files with sync entries...");
|
|
185
|
+
if (!dryRun) {
|
|
186
|
+
const existingFiles = config.files || [];
|
|
187
|
+
const newRemotePaths = new Set(configFilesEntries.map((e) => e.remotePath));
|
|
188
|
+
const preserved = existingFiles.filter((e) => !newRemotePaths.has(e.remotePath));
|
|
189
|
+
config.files = [...preserved, ...configFilesEntries];
|
|
190
|
+
await updateConfig(config);
|
|
191
|
+
console.log(` ā Updated config with ${config.files.length} file entries`);
|
|
192
|
+
} else {
|
|
193
|
+
console.log(` [DRY RUN] Would update config with ${configFilesEntries.length} file entries`);
|
|
194
|
+
for (const f of allFiles) {
|
|
195
|
+
console.log(` ${f.localPath} ā ${f.remotePath}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
175
198
|
}
|
|
176
199
|
|
|
177
200
|
/**
|
|
178
|
-
* Main cloudWorker command handler
|
|
201
|
+
* Main cloudWorker command handler ā push/create only.
|
|
202
|
+
* Reads config.files (set up by --init) and also re-collects any language-referenced
|
|
203
|
+
* files so new language term sources are always included without requiring --init again.
|
|
179
204
|
*/
|
|
180
205
|
export async function cloudWorker(options: CloudWorkerOptions) {
|
|
181
206
|
const {
|
|
@@ -205,10 +230,6 @@ export async function cloudWorker(options: CloudWorkerOptions) {
|
|
|
205
230
|
process.exit(1);
|
|
206
231
|
}
|
|
207
232
|
|
|
208
|
-
// Load language config
|
|
209
|
-
const language = await getLanguageConfig();
|
|
210
|
-
|
|
211
|
-
// Get project name from current directory
|
|
212
233
|
const projectName = path.basename(process.cwd());
|
|
213
234
|
console.log(`š Project name: ${projectName}`);
|
|
214
235
|
|
|
@@ -218,86 +239,63 @@ export async function cloudWorker(options: CloudWorkerOptions) {
|
|
|
218
239
|
// Get S3 service
|
|
219
240
|
const { AwsS3 } = services();
|
|
220
241
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Deduplicate by remotePath
|
|
227
|
-
const allFilesMap = new Map<string, FileToSync>();
|
|
228
|
-
for (const f of [...mainFiles, ...languageFiles]) {
|
|
229
|
-
allFilesMap.set(f.remotePath, f);
|
|
242
|
+
// Start with config.files (set up via --init)
|
|
243
|
+
const configFiles = config.files || [];
|
|
244
|
+
if (configFiles.length === 0) {
|
|
245
|
+
console.warn("ā ļø No files configured. Run 'knowhow cloudworker --init' first to set up file sync entries.");
|
|
230
246
|
}
|
|
231
|
-
const allFiles = Array.from(allFilesMap.values());
|
|
232
|
-
|
|
233
|
-
console.log(` Found ${allFiles.length} files to sync`);
|
|
234
247
|
|
|
235
|
-
if
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
248
|
+
// Re-collect language-referenced files on every push (if language.json is in config.files)
|
|
249
|
+
// so that new language term sources are picked up without needing --init again.
|
|
250
|
+
const languageFiles = await collectLanguageFilesIfConfigured(configFiles, projectName);
|
|
251
|
+
if (languageFiles.length > 0) {
|
|
252
|
+
console.log(` + ${languageFiles.length} language-referenced file(s) to sync`);
|
|
240
253
|
}
|
|
241
254
|
|
|
242
|
-
//
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Preserve any existing files entries not in our set
|
|
253
|
-
const existingFiles = config.files || [];
|
|
254
|
-
const newRemotePaths = new Set(configFilesEntries.map((e) => e.remotePath));
|
|
255
|
-
|
|
256
|
-
// Keep entries that don't overlap with new ones
|
|
257
|
-
const preserved = existingFiles.filter(
|
|
258
|
-
(e) => !newRemotePaths.has(e.remotePath)
|
|
259
|
-
);
|
|
255
|
+
// Merge language files into the upload list (deduplicate by remotePath)
|
|
256
|
+
const allFilesMap = new Map<string, { remotePath: string; localPath: string }>();
|
|
257
|
+
for (const f of configFiles) {
|
|
258
|
+
allFilesMap.set(f.remotePath, f);
|
|
259
|
+
}
|
|
260
|
+
for (const f of languageFiles) {
|
|
261
|
+
const entry = { remotePath: f.remotePath, localPath: f.downloadLocalPath ?? f.localPath };
|
|
262
|
+
allFilesMap.set(f.remotePath, entry);
|
|
263
|
+
}
|
|
264
|
+
const allFiles = Array.from(allFilesMap.values());
|
|
260
265
|
|
|
261
|
-
|
|
266
|
+
// If new language files were found, update config.files so they persist
|
|
267
|
+
if (languageFiles.length > 0 && !dryRun) {
|
|
268
|
+
config.files = allFiles.map((f) => ({ ...f, direction: "download" as const }));
|
|
262
269
|
await updateConfig(config);
|
|
263
|
-
console.log(` ā Updated config with ${config.files.length} file entries`);
|
|
264
|
-
} else {
|
|
265
|
-
console.log(` [DRY RUN] Would update config with ${configFilesEntries.length} file entries`);
|
|
266
270
|
}
|
|
267
271
|
|
|
268
|
-
//
|
|
269
|
-
const workerConfigJson = buildWorkerConfigJson(config,
|
|
272
|
+
// Build the workerConfigJson using the full file list
|
|
273
|
+
const workerConfigJson = buildWorkerConfigJson(config, allFiles.map((f) => ({ ...f, direction: "download" })));
|
|
270
274
|
|
|
271
|
-
//
|
|
272
|
-
console.log(`\nš Uploading ${allFiles.length} files...`);
|
|
275
|
+
// Upload all files
|
|
276
|
+
console.log(`\nš Uploading ${allFiles.length} configured files...`);
|
|
273
277
|
let successCount = 0;
|
|
274
278
|
let failCount = 0;
|
|
275
279
|
|
|
276
|
-
for (const
|
|
280
|
+
for (const mount of allFiles) {
|
|
281
|
+
const { remotePath, localPath } = mount;
|
|
277
282
|
try {
|
|
278
|
-
if (
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const remoteDir = file.remotePath.endsWith("/") ? file.remotePath : file.remotePath + "/";
|
|
282
|
-
const relFiles = listFilesRecursively(localDir);
|
|
283
|
-
console.log(` š Uploading directory ${localDir} ā ${remoteDir} (${relFiles.length} files)`);
|
|
284
|
-
for (const relFile of relFiles) {
|
|
285
|
-
await uploadSingleFile(client, AwsS3, localDir + relFile, remoteDir + relFile, dryRun);
|
|
286
|
-
successCount++;
|
|
287
|
-
}
|
|
283
|
+
if (remotePath.endsWith("/") || localPath.endsWith("/")) {
|
|
284
|
+
const count = await uploadDirectory(client, AwsS3, remotePath, localPath, dryRun);
|
|
285
|
+
successCount += count;
|
|
288
286
|
} else {
|
|
289
|
-
await
|
|
287
|
+
await uploadFile(client, AwsS3, remotePath, localPath, dryRun);
|
|
290
288
|
successCount++;
|
|
291
289
|
}
|
|
292
290
|
} catch (error) {
|
|
293
|
-
console.error(` ā Failed to upload ${
|
|
291
|
+
console.error(` ā Failed to upload ${localPath}: ${error.message}`);
|
|
294
292
|
failCount++;
|
|
295
293
|
}
|
|
296
294
|
}
|
|
297
295
|
|
|
298
296
|
console.log(`\n ā Upload complete: ${successCount} succeeded, ${failCount} failed`);
|
|
299
297
|
|
|
300
|
-
//
|
|
298
|
+
// Create or update cloud worker
|
|
301
299
|
if (create) {
|
|
302
300
|
const workerName = name || `${projectName}-worker`;
|
|
303
301
|
console.log(`\nš©ļø Creating cloud worker "${workerName}"...`);
|
|
@@ -339,16 +337,6 @@ export async function cloudWorker(options: CloudWorkerOptions) {
|
|
|
339
337
|
/**
|
|
340
338
|
* Pull the latest workerConfigJson from the cloud worker API and update the
|
|
341
339
|
* local knowhow.json config to match.
|
|
342
|
-
*
|
|
343
|
-
* This is the "pull" half of the config sync cycle. After running this,
|
|
344
|
-
* you can reload the worker's MCPs (in-process) via the reloadConfig
|
|
345
|
-
* WebSocket message or by calling `knowhow worker` again.
|
|
346
|
-
*
|
|
347
|
-
* Merged fields from workerConfigJson:
|
|
348
|
-
* - mcps ā overwrites config.mcps
|
|
349
|
-
* - modules ā overwrites config.modules (optional, only if present)
|
|
350
|
-
* - plugins ā overwrites config.plugins (optional, only if present)
|
|
351
|
-
* - agents ā overwrites config.agents (optional, only if present)
|
|
352
340
|
*/
|
|
353
341
|
export async function pullCloudWorkerConfig(options: CloudWorkerPullOptions) {
|
|
354
342
|
const { id, apiUrl = KNOWHOW_API_URL } = options;
|
package/src/commands/modules.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
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";
|
|
4
7
|
|
|
5
8
|
// Default built-in modules that `knowhow modules setup` adds to the config.
|
|
@@ -8,6 +11,44 @@ export const BUILTIN_MODULES = [
|
|
|
8
11
|
"@tyvm/knowhow-module-terminal",
|
|
9
12
|
];
|
|
10
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Returns the path to the .knowhow directory (used as npm install prefix).
|
|
16
|
+
* For global: ~/.knowhow
|
|
17
|
+
* For local: <cwd>/.knowhow
|
|
18
|
+
*/
|
|
19
|
+
function getKnowhowDir(isGlobal: boolean): string {
|
|
20
|
+
if (isGlobal) {
|
|
21
|
+
return path.join(os.homedir(), ".knowhow");
|
|
22
|
+
}
|
|
23
|
+
return path.join(process.cwd(), ".knowhow");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Ensures the .knowhow directory has a minimal package.json so
|
|
28
|
+
* `npm install --prefix` works cleanly without polluting the project root.
|
|
29
|
+
*/
|
|
30
|
+
function ensureKnowhowPackageJson(knowhowDir: string): void {
|
|
31
|
+
const pkgPath = path.join(knowhowDir, "package.json");
|
|
32
|
+
if (!fs.existsSync(pkgPath)) {
|
|
33
|
+
fs.mkdirSync(knowhowDir, { recursive: true });
|
|
34
|
+
fs.writeFileSync(
|
|
35
|
+
pkgPath,
|
|
36
|
+
JSON.stringify({ name: "knowhow-modules", private: true, version: "1.0.0" }, null, 2)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Run `npm install --prefix <knowhowDir> <mod>` so that modules land in
|
|
43
|
+
* .knowhow/node_modules rather than the project's node_modules.
|
|
44
|
+
*/
|
|
45
|
+
function npmInstallToKnowhow(mod: string, knowhowDir: string): void {
|
|
46
|
+
execSync(`npm install --prefix "${knowhowDir}" ${mod}`, {
|
|
47
|
+
stdio: "inherit",
|
|
48
|
+
encoding: "utf-8",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
11
52
|
export function addModulesCommand(program: Command): void {
|
|
12
53
|
const modulesCmd = program
|
|
13
54
|
.command("modules")
|
|
@@ -16,7 +57,7 @@ export function addModulesCommand(program: Command): void {
|
|
|
16
57
|
modulesCmd
|
|
17
58
|
.command("setup")
|
|
18
59
|
.description(
|
|
19
|
-
"Add default built-in modules to your config and install them
|
|
60
|
+
"Add default built-in modules to your config and install them into .knowhow/node_modules"
|
|
20
61
|
)
|
|
21
62
|
.option("--global", "Use the global config (~/.knowhow/knowhow.json)")
|
|
22
63
|
.action(async (opts) => {
|
|
@@ -40,15 +81,14 @@ export function addModulesCommand(program: Command): void {
|
|
|
40
81
|
return;
|
|
41
82
|
}
|
|
42
83
|
|
|
84
|
+
const knowhowDir = getKnowhowDir(isGlobal);
|
|
85
|
+
ensureKnowhowPackageJson(knowhowDir);
|
|
86
|
+
|
|
43
87
|
// Install packages that are not local file paths
|
|
44
88
|
for (const mod of toAdd) {
|
|
45
89
|
if (!mod.startsWith(".") && !mod.startsWith("/")) {
|
|
46
90
|
console.log(`š¦ Installing ${mod}...`);
|
|
47
|
-
|
|
48
|
-
execSync(`npm install ${installFlag} ${mod}`, {
|
|
49
|
-
stdio: "inherit",
|
|
50
|
-
encoding: "utf-8",
|
|
51
|
-
});
|
|
91
|
+
npmInstallToKnowhow(mod, knowhowDir);
|
|
52
92
|
}
|
|
53
93
|
cfg.modules!.push(mod);
|
|
54
94
|
console.log(`ā
Added ${mod} to ${configLabel}`);
|
|
@@ -63,7 +103,7 @@ export function addModulesCommand(program: Command): void {
|
|
|
63
103
|
console.log(
|
|
64
104
|
`\nš Setup complete! ${toAdd.length} module(s) added to ${configLabel}`
|
|
65
105
|
);
|
|
66
|
-
} catch (error) {
|
|
106
|
+
} catch (error: any) {
|
|
67
107
|
console.error("Error during modules setup:", error.message ?? error);
|
|
68
108
|
process.exit(1);
|
|
69
109
|
}
|
|
@@ -72,7 +112,7 @@ export function addModulesCommand(program: Command): void {
|
|
|
72
112
|
modulesCmd
|
|
73
113
|
.command("install [module]")
|
|
74
114
|
.description(
|
|
75
|
-
"Install a module
|
|
115
|
+
"Install a module into .knowhow/node_modules and add it to your config. " +
|
|
76
116
|
"If no module name is given, installs all modules already in the config."
|
|
77
117
|
)
|
|
78
118
|
.option("--global", "Use the global config (~/.knowhow/knowhow.json)")
|
|
@@ -86,6 +126,9 @@ export function addModulesCommand(program: Command): void {
|
|
|
86
126
|
|
|
87
127
|
if (!cfg.modules) cfg.modules = [];
|
|
88
128
|
|
|
129
|
+
const knowhowDir = getKnowhowDir(isGlobal);
|
|
130
|
+
ensureKnowhowPackageJson(knowhowDir);
|
|
131
|
+
|
|
89
132
|
if (!moduleName) {
|
|
90
133
|
// No module specified ā install everything already in the config
|
|
91
134
|
const installable = cfg.modules.filter(
|
|
@@ -98,15 +141,11 @@ export function addModulesCommand(program: Command): void {
|
|
|
98
141
|
return;
|
|
99
142
|
}
|
|
100
143
|
console.log(
|
|
101
|
-
`š¦ Installing ${installable.length} module(s) from ${configLabel}...`
|
|
144
|
+
`š¦ Installing ${installable.length} module(s) from ${configLabel} into ${knowhowDir}/node_modules...`
|
|
102
145
|
);
|
|
103
|
-
const installFlag = isGlobal ? "-g" : "";
|
|
104
146
|
for (const mod of installable) {
|
|
105
147
|
console.log(` š¦ Installing ${mod}...`);
|
|
106
|
-
|
|
107
|
-
stdio: "inherit",
|
|
108
|
-
encoding: "utf-8",
|
|
109
|
-
});
|
|
148
|
+
npmInstallToKnowhow(mod, knowhowDir);
|
|
110
149
|
console.log(` ā
Installed ${mod}`);
|
|
111
150
|
}
|
|
112
151
|
console.log(`\nš All modules installed!`);
|
|
@@ -114,12 +153,8 @@ export function addModulesCommand(program: Command): void {
|
|
|
114
153
|
}
|
|
115
154
|
|
|
116
155
|
// Install the specified module
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
execSync(`npm install ${installFlag} ${moduleName}`, {
|
|
120
|
-
stdio: "inherit",
|
|
121
|
-
encoding: "utf-8",
|
|
122
|
-
});
|
|
156
|
+
console.log(`š¦ Installing ${moduleName} into ${knowhowDir}/node_modules...`);
|
|
157
|
+
npmInstallToKnowhow(moduleName, knowhowDir);
|
|
123
158
|
console.log(`ā
Installed ${moduleName}`);
|
|
124
159
|
|
|
125
160
|
// Add to config if not already there
|
|
@@ -134,7 +169,7 @@ export function addModulesCommand(program: Command): void {
|
|
|
134
169
|
} else {
|
|
135
170
|
console.log(`ā¹ ${moduleName} is already in ${configLabel}`);
|
|
136
171
|
}
|
|
137
|
-
} catch (error) {
|
|
172
|
+
} catch (error: any) {
|
|
138
173
|
console.error("Error during module install:", error.message ?? error);
|
|
139
174
|
process.exit(1);
|
|
140
175
|
}
|
|
@@ -174,7 +209,7 @@ export function addModulesCommand(program: Command): void {
|
|
|
174
209
|
localModules.forEach((m, i) => console.log(` ${i + 1}. ${m}`));
|
|
175
210
|
}
|
|
176
211
|
}
|
|
177
|
-
} catch (error) {
|
|
212
|
+
} catch (error: any) {
|
|
178
213
|
console.error("Error listing modules:", error.message ?? error);
|
|
179
214
|
process.exit(1);
|
|
180
215
|
}
|