@tyvm/knowhow 0.0.108-dev.126b29e → 0.0.108-dev.501f36f

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 (177) hide show
  1. package/package.json +2 -3
  2. package/src/agents/base/base.ts +9 -0
  3. package/src/agents/tools/index.ts +0 -1
  4. package/src/agents/tools/list.ts +0 -2
  5. package/src/chat/CliChatService.ts +10 -1
  6. package/src/chat/renderer/CompactRenderer.ts +20 -0
  7. package/src/chat/renderer/ConsoleRenderer.ts +19 -0
  8. package/src/chat/renderer/FancyRenderer.ts +19 -0
  9. package/src/chat/renderer/types.ts +11 -0
  10. package/src/cli.ts +91 -664
  11. package/src/clients/index.ts +6 -5
  12. package/src/clients/types.ts +12 -4
  13. package/src/cloudWorker.ts +110 -122
  14. package/src/commands/agent.ts +246 -0
  15. package/src/commands/misc.ts +174 -0
  16. package/src/commands/modules.ts +182 -0
  17. package/src/commands/services.ts +77 -0
  18. package/src/commands/workers.ts +168 -0
  19. package/src/config.ts +37 -0
  20. package/src/fileSync.ts +50 -17
  21. package/src/index.ts +1 -0
  22. package/src/logger.ts +197 -0
  23. package/src/plugins/plugins.ts +0 -21
  24. package/src/processors/JsonCompressor.ts +6 -6
  25. package/src/services/EventService.ts +61 -1
  26. package/src/services/KnowhowClient.ts +12 -2
  27. package/src/services/S3.ts +0 -10
  28. package/src/services/modules/index.ts +70 -50
  29. package/src/services/modules/types.ts +4 -0
  30. package/src/tunnel.ts +216 -0
  31. package/src/types.ts +0 -1
  32. package/src/worker.ts +65 -336
  33. package/src/workers/auth/WsMiddleware.ts +99 -0
  34. package/src/workers/auth/authMiddleware.ts +104 -0
  35. package/src/workers/auth/types.ts +14 -2
  36. package/tests/unit/commands/github-credentials.test.ts +211 -0
  37. package/tests/unit/modules/moduleLoading.test.ts +39 -37
  38. package/tests/unit/plugins/pluginLoading.test.ts +0 -85
  39. package/ts_build/package.json +2 -3
  40. package/ts_build/src/agents/base/base.js +10 -0
  41. package/ts_build/src/agents/base/base.js.map +1 -1
  42. package/ts_build/src/agents/tools/index.d.ts +0 -1
  43. package/ts_build/src/agents/tools/index.js +0 -1
  44. package/ts_build/src/agents/tools/index.js.map +1 -1
  45. package/ts_build/src/agents/tools/list.js +0 -2
  46. package/ts_build/src/agents/tools/list.js.map +1 -1
  47. package/ts_build/src/chat/CliChatService.js +13 -1
  48. package/ts_build/src/chat/CliChatService.js.map +1 -1
  49. package/ts_build/src/chat/renderer/CompactRenderer.d.ts +4 -0
  50. package/ts_build/src/chat/renderer/CompactRenderer.js +16 -0
  51. package/ts_build/src/chat/renderer/CompactRenderer.js.map +1 -1
  52. package/ts_build/src/chat/renderer/ConsoleRenderer.d.ts +4 -0
  53. package/ts_build/src/chat/renderer/ConsoleRenderer.js +16 -0
  54. package/ts_build/src/chat/renderer/ConsoleRenderer.js.map +1 -1
  55. package/ts_build/src/chat/renderer/FancyRenderer.d.ts +4 -0
  56. package/ts_build/src/chat/renderer/FancyRenderer.js +16 -0
  57. package/ts_build/src/chat/renderer/FancyRenderer.js.map +1 -1
  58. package/ts_build/src/chat/renderer/types.d.ts +2 -0
  59. package/ts_build/src/cli.js +47 -525
  60. package/ts_build/src/cli.js.map +1 -1
  61. package/ts_build/src/clients/index.js +2 -4
  62. package/ts_build/src/clients/index.js.map +1 -1
  63. package/ts_build/src/clients/types.d.ts +2 -2
  64. package/ts_build/src/cloudWorker.d.ts +5 -0
  65. package/ts_build/src/cloudWorker.js +69 -66
  66. package/ts_build/src/cloudWorker.js.map +1 -1
  67. package/ts_build/src/commands/agent.d.ts +6 -0
  68. package/ts_build/src/commands/agent.js +229 -0
  69. package/ts_build/src/commands/agent.js.map +1 -0
  70. package/ts_build/src/commands/misc.d.ts +10 -0
  71. package/ts_build/src/commands/misc.js +197 -0
  72. package/ts_build/src/commands/misc.js.map +1 -0
  73. package/ts_build/src/commands/modules.d.ts +3 -0
  74. package/ts_build/src/commands/modules.js +160 -0
  75. package/ts_build/src/commands/modules.js.map +1 -0
  76. package/ts_build/src/commands/services.d.ts +5 -0
  77. package/ts_build/src/commands/services.js +87 -0
  78. package/ts_build/src/commands/services.js.map +1 -0
  79. package/ts_build/src/commands/workers.d.ts +6 -0
  80. package/ts_build/src/commands/workers.js +168 -0
  81. package/ts_build/src/commands/workers.js.map +1 -0
  82. package/ts_build/src/config.d.ts +1 -0
  83. package/ts_build/src/config.js +32 -0
  84. package/ts_build/src/config.js.map +1 -1
  85. package/ts_build/src/fileSync.d.ts +6 -0
  86. package/ts_build/src/fileSync.js +37 -12
  87. package/ts_build/src/fileSync.js.map +1 -1
  88. package/ts_build/src/index.d.ts +1 -0
  89. package/ts_build/src/index.js +3 -1
  90. package/ts_build/src/index.js.map +1 -1
  91. package/ts_build/src/logger.d.ts +21 -0
  92. package/ts_build/src/logger.js +106 -0
  93. package/ts_build/src/logger.js.map +1 -0
  94. package/ts_build/src/plugins/plugins.d.ts +0 -2
  95. package/ts_build/src/plugins/plugins.js +0 -11
  96. package/ts_build/src/plugins/plugins.js.map +1 -1
  97. package/ts_build/src/processors/JsonCompressor.js +4 -4
  98. package/ts_build/src/processors/JsonCompressor.js.map +1 -1
  99. package/ts_build/src/services/EventService.d.ts +6 -1
  100. package/ts_build/src/services/EventService.js +29 -0
  101. package/ts_build/src/services/EventService.js.map +1 -1
  102. package/ts_build/src/services/KnowhowClient.d.ts +1 -1
  103. package/ts_build/src/services/KnowhowClient.js +8 -2
  104. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  105. package/ts_build/src/services/S3.js +0 -7
  106. package/ts_build/src/services/S3.js.map +1 -1
  107. package/ts_build/src/services/modules/index.d.ts +33 -0
  108. package/ts_build/src/services/modules/index.js +46 -45
  109. package/ts_build/src/services/modules/index.js.map +1 -1
  110. package/ts_build/src/services/modules/types.d.ts +4 -0
  111. package/ts_build/src/tunnel.d.ts +27 -0
  112. package/ts_build/src/tunnel.js +112 -0
  113. package/ts_build/src/tunnel.js.map +1 -0
  114. package/ts_build/src/types.d.ts +0 -1
  115. package/ts_build/src/types.js.map +1 -1
  116. package/ts_build/src/worker.d.ts +1 -4
  117. package/ts_build/src/worker.js +38 -244
  118. package/ts_build/src/worker.js.map +1 -1
  119. package/ts_build/src/workers/auth/WsMiddleware.d.ts +8 -0
  120. package/ts_build/src/workers/auth/WsMiddleware.js +65 -0
  121. package/ts_build/src/workers/auth/WsMiddleware.js.map +1 -0
  122. package/ts_build/src/workers/auth/authMiddleware.d.ts +3 -0
  123. package/ts_build/src/workers/auth/authMiddleware.js +60 -0
  124. package/ts_build/src/workers/auth/authMiddleware.js.map +1 -0
  125. package/ts_build/src/workers/auth/types.d.ts +8 -1
  126. package/ts_build/tests/unit/commands/github-credentials.test.d.ts +1 -0
  127. package/ts_build/tests/unit/commands/github-credentials.test.js +146 -0
  128. package/ts_build/tests/unit/commands/github-credentials.test.js.map +1 -0
  129. package/ts_build/tests/unit/modules/moduleLoading.test.js +20 -26
  130. package/ts_build/tests/unit/modules/moduleLoading.test.js.map +1 -1
  131. package/ts_build/tests/unit/plugins/pluginLoading.test.js +0 -65
  132. package/ts_build/tests/unit/plugins/pluginLoading.test.js.map +1 -1
  133. package/src/agents/tools/executeScript/README.md +0 -94
  134. package/src/agents/tools/executeScript/definition.ts +0 -79
  135. package/src/agents/tools/executeScript/examples/dependency-injection-validation.ts +0 -272
  136. package/src/agents/tools/executeScript/examples/quick-test.ts +0 -74
  137. package/src/agents/tools/executeScript/examples/serialization-test.ts +0 -321
  138. package/src/agents/tools/executeScript/examples/test-runner.ts +0 -197
  139. package/src/agents/tools/executeScript/index.ts +0 -98
  140. package/src/services/script-execution/SandboxContext.ts +0 -282
  141. package/src/services/script-execution/ScriptExecutor.ts +0 -441
  142. package/src/services/script-execution/ScriptPolicy.ts +0 -194
  143. package/src/services/script-execution/ScriptTracer.ts +0 -249
  144. package/src/services/script-execution/types.ts +0 -134
  145. package/ts_build/src/agents/tools/executeScript/definition.d.ts +0 -2
  146. package/ts_build/src/agents/tools/executeScript/definition.js +0 -76
  147. package/ts_build/src/agents/tools/executeScript/definition.js.map +0 -1
  148. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.d.ts +0 -18
  149. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js +0 -192
  150. package/ts_build/src/agents/tools/executeScript/examples/dependency-injection-validation.js.map +0 -1
  151. package/ts_build/src/agents/tools/executeScript/examples/quick-test.d.ts +0 -3
  152. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js +0 -64
  153. package/ts_build/src/agents/tools/executeScript/examples/quick-test.js.map +0 -1
  154. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.d.ts +0 -15
  155. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js +0 -266
  156. package/ts_build/src/agents/tools/executeScript/examples/serialization-test.js.map +0 -1
  157. package/ts_build/src/agents/tools/executeScript/examples/test-runner.d.ts +0 -4
  158. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js +0 -208
  159. package/ts_build/src/agents/tools/executeScript/examples/test-runner.js.map +0 -1
  160. package/ts_build/src/agents/tools/executeScript/index.d.ts +0 -28
  161. package/ts_build/src/agents/tools/executeScript/index.js +0 -72
  162. package/ts_build/src/agents/tools/executeScript/index.js.map +0 -1
  163. package/ts_build/src/services/script-execution/SandboxContext.d.ts +0 -34
  164. package/ts_build/src/services/script-execution/SandboxContext.js +0 -189
  165. package/ts_build/src/services/script-execution/SandboxContext.js.map +0 -1
  166. package/ts_build/src/services/script-execution/ScriptExecutor.d.ts +0 -19
  167. package/ts_build/src/services/script-execution/ScriptExecutor.js +0 -269
  168. package/ts_build/src/services/script-execution/ScriptExecutor.js.map +0 -1
  169. package/ts_build/src/services/script-execution/ScriptPolicy.d.ts +0 -28
  170. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -115
  171. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +0 -1
  172. package/ts_build/src/services/script-execution/ScriptTracer.d.ts +0 -19
  173. package/ts_build/src/services/script-execution/ScriptTracer.js +0 -186
  174. package/ts_build/src/services/script-execution/ScriptTracer.js.map +0 -1
  175. package/ts_build/src/services/script-execution/types.d.ts +0 -108
  176. package/ts_build/src/services/script-execution/types.js +0 -3
  177. package/ts_build/src/services/script-execution/types.js.map +0 -1
@@ -577,11 +577,12 @@ export class AIClient {
577
577
  * @param modelQuery - the model name to search for (can be partial/normalized)
578
578
  * @param provider - optional provider to restrict search to
579
579
  */
580
- findModelFuzzy(modelQuery: string, provider?: string): { provider: string; model: string } | undefined {
580
+ findModelFuzzy(
581
+ modelQuery: string,
582
+ provider?: string
583
+ ): { provider: string; model: string } | undefined {
581
584
  const queryNorm = AIClient.normalizeModelId(modelQuery);
582
- const providers = provider
583
- ? [provider]
584
- : Object.keys(this.clientModels);
585
+ const providers = provider ? [provider] : Object.keys(this.clientModels);
585
586
 
586
587
  for (const p of providers) {
587
588
  const models = (this.clientModels[p] as string[]) ?? [];
@@ -835,7 +836,7 @@ export class AIClient {
835
836
  const splitModel = m.id.split("/");
836
837
 
837
838
  if (splitModel.length < 2) {
838
- console.error(`Cannot parse model format: ${m.id}`);
839
+ console.warn(`Cannot parse model format: ${m.id}`);
839
840
  }
840
841
 
841
842
  const provider = splitModel.length > 1 ? splitModel[0] : "";
@@ -1,4 +1,10 @@
1
- export type ModelModality = "completion" | "embedding" | "image" | "audio" | "video" | "transcription";
1
+ export type ModelModality =
2
+ | "completion"
3
+ | "embedding"
4
+ | "image"
5
+ | "audio"
6
+ | "video"
7
+ | "transcription";
2
8
 
3
9
  export type MessageContent =
4
10
  | { type: "text"; text: string }
@@ -8,7 +14,7 @@ export type MessageContent =
8
14
 
9
15
  export interface Message {
10
16
  role: "system" | "user" | "assistant" | "tool";
11
- content?: string | MessageContent[];
17
+ content?: string | MessageContent[] | null;
12
18
 
13
19
  name?: string;
14
20
  tool_call_id?: string;
@@ -16,7 +22,7 @@ export interface Message {
16
22
  }
17
23
 
18
24
  export interface OutputMessage extends Message {
19
- content: string;
25
+ content?: string | null;
20
26
  }
21
27
 
22
28
  export interface ToolProp {
@@ -301,7 +307,9 @@ export interface GenericClient {
301
307
  * When modality is provided, return only models for that modality (static list).
302
308
  * When omitted, return ALL models (backward compat — may do a live API call).
303
309
  */
304
- getModels(modality?: ModelModality): Promise<{ id: string; modality?: ModelModality[] }[]>;
310
+ getModels(
311
+ modality?: ModelModality
312
+ ): Promise<{ id: string; modality?: ModelModality[] }[]>;
305
313
  /**
306
314
  * Returns the context window limit and compression threshold for a given model,
307
315
  * or undefined if the model is not known to this client.
@@ -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;
@@ -0,0 +1,246 @@
1
+ import { Command } from "commander";
2
+ import { readPromptFile } from "../ai";
3
+ import { AgentModule } from "../chat/modules/AgentModule";
4
+ import { AskModule } from "../chat/modules/AskModule";
5
+ import { SearchModule } from "../chat/modules/SearchModule";
6
+ import { SessionsModule } from "../chat/modules/SessionsModule";
7
+ import { SetupModule } from "../chat/modules/SetupModule";
8
+
9
+ async function readStdin(): Promise<string> {
10
+ return new Promise((resolve) => {
11
+ let data = "";
12
+ process.stdin.setEncoding("utf8");
13
+
14
+ if (process.stdin.isTTY) {
15
+ resolve("");
16
+ return;
17
+ }
18
+
19
+ process.stdin.on("readable", () => {
20
+ const chunk = process.stdin.read();
21
+ if (chunk !== null) data += chunk;
22
+ });
23
+
24
+ process.stdin.on("end", () => resolve(data.trim()));
25
+ });
26
+ }
27
+
28
+ export function addAgentCommand(program: Command, getChatService: () => any): void {
29
+ program
30
+ .command("agent")
31
+ .description("Spin up agents directly from CLI")
32
+ .option(
33
+ "--provider <provider>",
34
+ "AI provider (openai, anthropic, google, xai)"
35
+ )
36
+ .option("--model <model>", "Specific model for the provider")
37
+ .option("--agent-name <name>", "Which agent to use", "Patcher")
38
+ .option(
39
+ "--max-time-limit <minutes>",
40
+ "Time limit for agent execution (minutes)",
41
+ "30"
42
+ )
43
+ .option(
44
+ "--max-spend-limit <dollars>",
45
+ "Cost limit for agent execution (dollars)",
46
+ "10"
47
+ )
48
+ .option("--message-id <messageId>", "Knowhow message ID for task tracking")
49
+ .option("--sync-fs", "Enable filesystem-based synchronization")
50
+ .option(
51
+ "--task-id <taskId>",
52
+ "Pre-generated task ID (used with --sync-fs for predictable agent directory path)"
53
+ )
54
+ .option("--prompt-file <path>", "Custom prompt template file with {text}")
55
+ .option("--input <text>", "Task input (fallback to stdin if not provided)")
56
+ .option(
57
+ "--resume",
58
+ "Resume a previously started task using the --task-id (local FS or remote)"
59
+ )
60
+ .action(async (options) => {
61
+ try {
62
+ const { setupServices } = await import("./services");
63
+ await setupServices();
64
+ const chatService = getChatService();
65
+ const agentModule = new AgentModule();
66
+
67
+ if (options.resume) {
68
+ const threads = await agentModule.loadThreadsForTask(
69
+ options.taskId,
70
+ options.messageId
71
+ );
72
+ const resumeInput =
73
+ options.input || "Please continue from where you left off.";
74
+
75
+ await agentModule.initialize(chatService);
76
+ const { taskCompleted: resumed } =
77
+ await agentModule.resumeFromMessages({
78
+ agentName: options.agentName || "Patcher",
79
+ input: resumeInput,
80
+ threads,
81
+ messageId: options.messageId,
82
+ taskId: options.taskId,
83
+ });
84
+ await resumed;
85
+ return;
86
+ }
87
+
88
+ let input = options.input;
89
+
90
+ if (!input && !options.promptFile) {
91
+ input = await readStdin();
92
+ }
93
+
94
+ input = readPromptFile(options.promptFile, input);
95
+
96
+ if (!input) {
97
+ console.error(
98
+ "Error: No input provided. Use --input flag, pipe input via stdin, or provide --prompt-file."
99
+ );
100
+ process.exit(1);
101
+ }
102
+
103
+ await agentModule.initialize(chatService);
104
+ const { taskCompleted } = await agentModule.setupAgent({
105
+ ...options,
106
+ input,
107
+ maxTimeLimit: parseInt(options.maxTimeLimit, 10),
108
+ maxSpendLimit: parseFloat(options.maxSpendLimit),
109
+ run: true,
110
+ });
111
+ await taskCompleted;
112
+ } catch (error) {
113
+ console.error("Error running agent:", error);
114
+ process.exit(1);
115
+ }
116
+ });
117
+ }
118
+
119
+ export function addAskCommand(program: Command, getChatService: () => any, getConfig: () => any): void {
120
+ program
121
+ .command("ask")
122
+ .description("Direct AI questioning without agent overhead")
123
+ .option("--provider <provider>", "AI provider to use")
124
+ .option("--model <model>", "Specific model")
125
+ .option("--input <text>", "Question (fallback to stdin if not provided)")
126
+ .option("--prompt-file <path>", "Custom prompt template file")
127
+ .action(async (options) => {
128
+ try {
129
+ const { setupServices } = await import("./services");
130
+ await setupServices();
131
+ const chatService = getChatService();
132
+ const config = getConfig();
133
+ let input = options.input;
134
+
135
+ if (!input && !options.promptFile) {
136
+ input = await readStdin();
137
+ }
138
+
139
+ input = readPromptFile(options.promptFile, input);
140
+
141
+ if (!input) {
142
+ console.error(
143
+ "Error: No question provided. Use --input flag, pipe input via stdin, or provide --prompt-file."
144
+ );
145
+ process.exit(1);
146
+ }
147
+
148
+ const askModule = new AskModule();
149
+ await askModule.initialize(chatService);
150
+ await askModule.processAIQuery(input, {
151
+ plugins: config.plugins.enabled,
152
+ currentModel: options.model,
153
+ currentProvider: options.provider,
154
+ chatHistory: [],
155
+ });
156
+ } catch (error) {
157
+ console.error("Error asking AI:", error);
158
+ process.exit(1);
159
+ }
160
+ });
161
+ }
162
+
163
+ export function addSetupCommand(program: Command, getChatService: () => any): void {
164
+ program
165
+ .command("setup")
166
+ .description("Ask the agent to configure knowhow")
167
+ .action(async () => {
168
+ try {
169
+ const { setupServices } = await import("./services");
170
+ await setupServices();
171
+ const chatService = getChatService();
172
+ const agentModule = new AgentModule();
173
+ await agentModule.initialize(chatService);
174
+ const setupModule = new SetupModule(agentModule);
175
+ await setupModule.initialize(chatService);
176
+ await setupModule.handleSetupCommand([]);
177
+ } catch (error) {
178
+ console.error("Error running agent:", error);
179
+ process.exit(1);
180
+ }
181
+ });
182
+ }
183
+
184
+ export function addSearchCommand(program: Command): void {
185
+ program
186
+ .command("search")
187
+ .description("Search embeddings directly from CLI")
188
+ .option(
189
+ "--input <text>",
190
+ "Search query (fallback to stdin if not provided)"
191
+ )
192
+ .option(
193
+ "-e, --embedding <path>",
194
+ "Specific embedding path (default: all)",
195
+ "all"
196
+ )
197
+ .action(async (options) => {
198
+ try {
199
+ const { setupServices } = await import("./services");
200
+ await setupServices();
201
+ let input = options.input;
202
+ if (!input) {
203
+ input = await readStdin();
204
+ if (!input) {
205
+ console.error(
206
+ "Error: No search query provided. Use --input flag or pipe input via stdin."
207
+ );
208
+ process.exit(1);
209
+ }
210
+ }
211
+
212
+ await new SearchModule().searchEmbeddingsCLI(input, options.embedding);
213
+ } catch (error) {
214
+ console.error("Error searching embeddings:", error);
215
+ process.exit(1);
216
+ }
217
+ });
218
+ }
219
+
220
+ export function addSessionsCommand(program: Command, getChatService: () => any): void {
221
+ program
222
+ .command("sessions")
223
+ .description("Manage agent sessions from CLI")
224
+ .option(
225
+ "--all",
226
+ "Show all historical sessions (default: current process only)"
227
+ )
228
+ .option("--csv", "Output sessions as CSV")
229
+ .action(async (options) => {
230
+ try {
231
+ const chatService = getChatService();
232
+ const agentModule = new AgentModule();
233
+ await agentModule.initialize(chatService);
234
+ const sessionsModule = new SessionsModule(agentModule);
235
+ await sessionsModule.initialize(chatService);
236
+ await sessionsModule.logSessionTable(
237
+ options.all || false,
238
+ options.csv || false,
239
+ true
240
+ );
241
+ } catch (error) {
242
+ console.error("Error listing sessions:", error);
243
+ process.exit(1);
244
+ }
245
+ });
246
+ }