@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.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/scripts/build-for-node.sh +10 -24
  3. package/src/chat/modules/AgentModule.ts +7 -2
  4. package/src/chat/modules/SessionsModule.ts +40 -1
  5. package/src/cloudWorker.ts +110 -122
  6. package/src/commands/modules.ts +57 -22
  7. package/src/commands/workers.ts +9 -1
  8. package/src/fileSync.ts +50 -17
  9. package/src/services/KnowhowClient.ts +12 -2
  10. package/src/services/S3.ts +0 -10
  11. package/src/services/modules/index.ts +27 -3
  12. package/ts_build/package.json +1 -1
  13. package/ts_build/src/chat/modules/AgentModule.js +5 -2
  14. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  15. package/ts_build/src/chat/modules/SessionsModule.js +30 -1
  16. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  17. package/ts_build/src/cloudWorker.d.ts +5 -0
  18. package/ts_build/src/cloudWorker.js +69 -66
  19. package/ts_build/src/cloudWorker.js.map +1 -1
  20. package/ts_build/src/commands/modules.js +66 -19
  21. package/ts_build/src/commands/modules.js.map +1 -1
  22. package/ts_build/src/commands/workers.js +6 -1
  23. package/ts_build/src/commands/workers.js.map +1 -1
  24. package/ts_build/src/fileSync.d.ts +6 -0
  25. package/ts_build/src/fileSync.js +37 -12
  26. package/ts_build/src/fileSync.js.map +1 -1
  27. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  28. package/ts_build/src/services/KnowhowClient.js +8 -2
  29. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  30. package/ts_build/src/services/S3.js +0 -7
  31. package/ts_build/src/services/S3.js.map +1 -1
  32. package/ts_build/src/services/modules/index.js +22 -3
  33. package/ts_build/src/services/modules/index.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.108-dev.ed88cf4",
3
+ "version": "0.0.109-dev.05fe5a0",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -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. 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
10
+ # 3. Symlinks the package globally for ALL installed nvm versions matching the target
12
11
  #
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.
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 for target isolated-vm version ---
118
- echo "šŸ“ Patching package.json for isolated-vm $IVM_VERSION..."
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 deps in staging dir using target node ---
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
- # 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)"
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
- const lastThread = threads.length > 0 ? threads[threads.length - 1] : [];
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, threadState);
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;
@@ -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 { S3Service } from "./services/S3";
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
- * Uses directory-level entries where possible so the worker config stays compact
76
- * and the folder upload/download feature handles individual files automatically.
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
- * Upload a single file to the cloud worker's file storage
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 uploadSingleFile(
151
- client: KnowhowSimpleClient,
152
- s3Service: S3Service,
153
- localPath: string,
154
- remotePath: string,
155
- dryRun: boolean
156
- ): Promise<void> {
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
- if (!fs.existsSync(localPath)) {
165
- console.warn(` āš ļø Local file not found, skipping: ${localPath}`);
166
- return;
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
- const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
170
- await s3Service.uploadToPresignedUrl(presignedUrl, localPath);
171
- await client.markOrgFileUploadComplete(remotePath);
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 stats = fs.statSync(localPath);
174
- console.log(` āœ“ Uploaded ${stats.size} bytes`);
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
- // Step 1: Collect all files to sync
222
- console.log("\nšŸ“‚ Collecting files to sync...");
223
- const mainFiles = await collectFilesToSync(projectName);
224
- const languageFiles = await collectLanguageReferencedFiles(language, projectName);
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 (dryRun) {
236
- console.log("\nšŸ“‹ Files that would be synced:");
237
- for (const f of allFiles) {
238
- console.log(` ${f.localPath} → ${f.remotePath}`);
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
- // Step 2: Build the config.files array for all synced files
243
- const configFilesEntries = allFiles.map((f) => ({
244
- remotePath: f.remotePath,
245
- localPath: f.downloadLocalPath ?? f.localPath,
246
- direction: "download" as const,
247
- }));
248
-
249
- // Step 3: Update config.files and save
250
- console.log("\nšŸ’¾ Updating config.files with sync entries...");
251
- if (!dryRun) {
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
- config.files = [...preserved, ...configFilesEntries];
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
- // Step 4: Build workerConfigJson
269
- const workerConfigJson = buildWorkerConfigJson(config, configFilesEntries);
272
+ // Build the workerConfigJson using the full file list
273
+ const workerConfigJson = buildWorkerConfigJson(config, allFiles.map((f) => ({ ...f, direction: "download" })));
270
274
 
271
- // Step 5: Upload all files
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 file of allFiles) {
280
+ for (const mount of allFiles) {
281
+ const { remotePath, localPath } = mount;
277
282
  try {
278
- if (file.isDirectory) {
279
- // Upload all files recursively in the local directory
280
- const localDir = file.localPath.endsWith("/") ? file.localPath : file.localPath + "/";
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 uploadSingleFile(client, AwsS3, file.localPath, file.remotePath, dryRun);
287
+ await uploadFile(client, AwsS3, remotePath, localPath, dryRun);
290
288
  successCount++;
291
289
  }
292
290
  } catch (error) {
293
- console.error(` āŒ Failed to upload ${file.localPath}: ${error.message}`);
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
- // Step 6: Create or update cloud worker
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;
@@ -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 via npm"
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
- const installFlag = isGlobal ? "-g" : "";
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 via npm and add it to your config. " +
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
- execSync(`npm install ${installFlag} ${mod}`, {
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
- const installFlag = isGlobal ? "-g" : "";
118
- console.log(`šŸ“¦ Installing ${moduleName}...`);
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
  }