@tyvm/knowhow 0.0.97 → 0.0.99

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 (84) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +12 -4
  3. package/src/agents/patcher/patcher.ts +1 -1
  4. package/src/agents/tools/list.ts +1 -0
  5. package/src/chat/modules/AgentModule.ts +24 -22
  6. package/src/chat/modules/SessionsModule.ts +1 -3
  7. package/src/chat/modules/SystemModule.ts +23 -8
  8. package/src/cli.ts +17 -0
  9. package/src/clients/anthropic.ts +10 -4
  10. package/src/clients/gemini.ts +23 -5
  11. package/src/clients/http.ts +5 -3
  12. package/src/clients/index.ts +261 -139
  13. package/src/clients/knowhow.ts +15 -4
  14. package/src/clients/openai.ts +213 -10
  15. package/src/clients/types.ts +7 -1
  16. package/src/clients/xai.ts +13 -6
  17. package/src/cloudWorker.ts +314 -0
  18. package/src/config.ts +9 -1
  19. package/src/processors/TokenCompressor.ts +8 -1
  20. package/src/services/KnowhowClient.ts +56 -12
  21. package/src/services/LazyToolsService.ts +15 -14
  22. package/src/services/Tools.ts +10 -1
  23. package/src/types.ts +11 -2
  24. package/src/utils/InputQueueManager.ts +131 -20
  25. package/test-ai-completion.ts +39 -0
  26. package/test-mcp-args.ts +71 -0
  27. package/test-tools-service.ts +45 -0
  28. package/ts_build/package.json +1 -1
  29. package/ts_build/src/agents/base/base.js +8 -2
  30. package/ts_build/src/agents/base/base.js.map +1 -1
  31. package/ts_build/src/agents/patcher/patcher.js +1 -1
  32. package/ts_build/src/agents/patcher/patcher.js.map +1 -1
  33. package/ts_build/src/agents/tools/list.js +1 -0
  34. package/ts_build/src/agents/tools/list.js.map +1 -1
  35. package/ts_build/src/chat/modules/AgentModule.js +17 -19
  36. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  37. package/ts_build/src/chat/modules/SessionsModule.js +1 -3
  38. package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
  39. package/ts_build/src/chat/modules/SystemModule.js +16 -8
  40. package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
  41. package/ts_build/src/cli.js +17 -0
  42. package/ts_build/src/cli.js.map +1 -1
  43. package/ts_build/src/clients/anthropic.d.ts +9 -7
  44. package/ts_build/src/clients/anthropic.js +9 -4
  45. package/ts_build/src/clients/anthropic.js.map +1 -1
  46. package/ts_build/src/clients/gemini.d.ts +2 -1
  47. package/ts_build/src/clients/gemini.js +13 -4
  48. package/ts_build/src/clients/gemini.js.map +1 -1
  49. package/ts_build/src/clients/http.d.ts +1 -1
  50. package/ts_build/src/clients/http.js +2 -2
  51. package/ts_build/src/clients/http.js.map +1 -1
  52. package/ts_build/src/clients/index.d.ts +23 -47
  53. package/ts_build/src/clients/index.js +152 -99
  54. package/ts_build/src/clients/index.js.map +1 -1
  55. package/ts_build/src/clients/knowhow.d.ts +3 -2
  56. package/ts_build/src/clients/knowhow.js +6 -3
  57. package/ts_build/src/clients/knowhow.js.map +1 -1
  58. package/ts_build/src/clients/openai.d.ts +20 -18
  59. package/ts_build/src/clients/openai.js +166 -8
  60. package/ts_build/src/clients/openai.js.map +1 -1
  61. package/ts_build/src/clients/types.d.ts +3 -1
  62. package/ts_build/src/clients/xai.d.ts +3 -2
  63. package/ts_build/src/clients/xai.js +10 -4
  64. package/ts_build/src/clients/xai.js.map +1 -1
  65. package/ts_build/src/cloudWorker.d.ts +8 -0
  66. package/ts_build/src/cloudWorker.js +239 -0
  67. package/ts_build/src/cloudWorker.js.map +1 -0
  68. package/ts_build/src/config.js +8 -1
  69. package/ts_build/src/config.js.map +1 -1
  70. package/ts_build/src/processors/TokenCompressor.js +7 -1
  71. package/ts_build/src/processors/TokenCompressor.js.map +1 -1
  72. package/ts_build/src/services/KnowhowClient.d.ts +24 -1
  73. package/ts_build/src/services/KnowhowClient.js +14 -2
  74. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  75. package/ts_build/src/services/LazyToolsService.js +5 -7
  76. package/ts_build/src/services/LazyToolsService.js.map +1 -1
  77. package/ts_build/src/services/Tools.js +9 -1
  78. package/ts_build/src/services/Tools.js.map +1 -1
  79. package/ts_build/src/types.d.ts +3 -1
  80. package/ts_build/src/types.js +8 -1
  81. package/ts_build/src/types.js.map +1 -1
  82. package/ts_build/src/utils/InputQueueManager.d.ts +2 -0
  83. package/ts_build/src/utils/InputQueueManager.js +76 -10
  84. package/ts_build/src/utils/InputQueueManager.js.map +1 -1
@@ -28,8 +28,20 @@ import {
28
28
  ChatCompletionToolMessageParam,
29
29
  ChatCompletionMessageToolCall,
30
30
  } from "openai/resources/chat";
31
+ import { ResponseFunctionToolCall } from "openai/resources/responses/responses";
31
32
 
32
- import { EmbeddingModels, Models, OpenAiReasoningModels } from "../types";
33
+ import {
34
+ EmbeddingModels,
35
+ Models,
36
+ OpenAiReasoningModels,
37
+ OpenAiResponsesOnlyModels,
38
+ OpenAiImageModels,
39
+ OpenAiVideoModels,
40
+ OpenAiTTSModels,
41
+ OpenAiTranscriptionModels,
42
+ OpenAiEmbeddingModels,
43
+ } from "../types";
44
+ import { ModelModality } from "./types";
33
45
 
34
46
  const config = getConfigSync();
35
47
 
@@ -87,6 +99,11 @@ export class GenericOpenAiClient implements GenericClient {
87
99
  async createChatCompletion(
88
100
  options: CompletionOptions
89
101
  ): Promise<CompletionResponse> {
102
+ // Route to Responses API for models that don't support Chat Completions
103
+ if (OpenAiResponsesOnlyModels.includes(options.model)) {
104
+ return this.createChatResponse(options);
105
+ }
106
+
90
107
  const openaiMessages = options.messages.map((msg) => {
91
108
  if (msg.role === "tool") {
92
109
  return {
@@ -133,6 +150,184 @@ export class GenericOpenAiClient implements GenericClient {
133
150
  usd_cost: usdCost,
134
151
  };
135
152
  }
153
+ /**
154
+ * Creates a completion using the OpenAI Responses API.
155
+ * Used for models that only support the Responses API (e.g. gpt-5.3-codex, gpt-5.4).
156
+ * Translates Chat Completions message format to Responses API format and maps the
157
+ * response back to CompletionResponse.
158
+ */
159
+ /**
160
+ * Attempts to repair truncated JSON arguments from the Responses API.
161
+ * Codex sometimes returns function_call arguments with truncated JSON strings.
162
+ * This tries to close open strings/objects to produce valid JSON.
163
+ */
164
+ private repairTruncatedJson(args: string): string {
165
+ try {
166
+ JSON.parse(args);
167
+ return args; // Already valid
168
+ } catch {
169
+ // Try to repair by closing open structures
170
+ let repaired = args.trimEnd();
171
+ // Count open/close braces and brackets
172
+ let depth = 0;
173
+ let inString = false;
174
+ let escaped = false;
175
+ for (const ch of repaired) {
176
+ if (escaped) { escaped = false; continue; }
177
+ if (ch === '\\' && inString) { escaped = true; continue; }
178
+ if (ch === '"') { inString = !inString; continue; }
179
+ if (!inString) {
180
+ if (ch === '{' || ch === '[') depth++;
181
+ else if (ch === '}' || ch === ']') depth--;
182
+ }
183
+ }
184
+ // If we're inside a string, close it
185
+ if (inString) repaired += '"';
186
+ // Close any open objects/arrays
187
+ for (let i = 0; i < depth; i++) repaired += '}';
188
+ try {
189
+ JSON.parse(repaired);
190
+ return repaired;
191
+ } catch {
192
+ return args; // Return original if repair failed
193
+ }
194
+ }
195
+ }
196
+
197
+ async createChatResponse(
198
+ options: CompletionOptions
199
+ ): Promise<CompletionResponse> {
200
+ // Extract system message to use as instructions
201
+ const systemMessages = options.messages.filter(
202
+ (m) => m.role === "system"
203
+ );
204
+ const nonSystemMessages = options.messages.filter(
205
+ (m) => m.role !== "system"
206
+ );
207
+ const instructions = systemMessages
208
+ .map((m) => (typeof m.content === "string" ? m.content : ""))
209
+ .join("\n")
210
+ .trim() || undefined;
211
+
212
+ // Convert chat messages to Responses API input items
213
+ // The Responses API accepts: user/assistant/system messages and function_call_output items
214
+ const input: any[] = nonSystemMessages.map((msg) => {
215
+ if (msg.role === "tool") {
216
+ // tool result → function_call_output
217
+ return {
218
+ type: "function_call_output",
219
+ call_id: msg.tool_call_id,
220
+ output: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
221
+ };
222
+ }
223
+ if (msg.role === "assistant" && msg.tool_calls?.length) {
224
+ // assistant message with tool calls → function_call items
225
+ return msg.tool_calls.map((tc) => ({
226
+ type: "function_call",
227
+ // id must start with 'fc_'; call_id is the original call_ ID used for function_call_output matching
228
+ id: tc.id.startsWith("fc") ? tc.id : `fc_${tc.id}`,
229
+ call_id: tc.id,
230
+ name: tc.function.name,
231
+ arguments: tc.function.arguments,
232
+ }));
233
+ }
234
+ // Regular user/assistant message
235
+ return {
236
+ role: msg.role,
237
+ content: typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content),
238
+ };
239
+ }).flat();
240
+
241
+ // Convert Chat Completions tool definitions to Responses API FunctionTool format
242
+ const tools = options.tools?.map((tool) => ({
243
+ type: "function" as const,
244
+ name: tool.function.name,
245
+ description: tool.function.description,
246
+ parameters: tool.function.parameters as Record<string, unknown>,
247
+ strict: false,
248
+ }));
249
+
250
+ const response = await this.client.responses.create({
251
+ model: options.model as any,
252
+ input,
253
+ ...(instructions && { instructions }),
254
+ // Don't limit max_output_tokens for Responses API - codex truncates tool call arguments when limited
255
+ ...(OpenAiReasoningModels.includes(options.model) && {
256
+ max_output_tokens: Math.max(options.max_tokens || 0, 16000),
257
+ reasoning: { effort: this.reasoningEffort(options.messages) },
258
+ }),
259
+ ...(tools?.length && {
260
+ tools,
261
+ tool_choice: "auto",
262
+ }),
263
+ store: false,
264
+ } as any);
265
+
266
+ // Map Responses API usage to Chat Completions usage format
267
+ const usage = response.usage
268
+ ? {
269
+ prompt_tokens: response.usage.input_tokens,
270
+ completion_tokens: response.usage.output_tokens,
271
+ total_tokens:
272
+ response.usage.input_tokens + response.usage.output_tokens,
273
+ }
274
+ : undefined;
275
+
276
+ const usdCost = usage
277
+ ? this.calculateCost(options.model, usage)
278
+ : undefined;
279
+
280
+ // Collect text content and tool calls from the output items
281
+ let textContent: string | null = null;
282
+ const toolCalls: ChatCompletionMessageToolCall[] = [];
283
+
284
+ for (const item of response.output) {
285
+ if (item.type === "message") {
286
+ // ResponseOutputMessage
287
+ const msgItem = item as any;
288
+ for (const part of msgItem.content ?? []) {
289
+ if (part.type === "output_text") {
290
+ textContent = (textContent ?? "") + part.text;
291
+ }
292
+ }
293
+ } else if (item.type === "function_call") {
294
+ // ResponseFunctionToolCall
295
+ const fc = item as ResponseFunctionToolCall;
296
+ const repairedArgs = this.repairTruncatedJson(fc.arguments);
297
+ // Validate at the boundary - log if still invalid after repair
298
+ try {
299
+ JSON.parse(repairedArgs);
300
+ } catch (e) {
301
+ console.warn(`[Responses API] Invalid JSON arguments for ${fc.name} after repair: ${e.message}`);
302
+ }
303
+ toolCalls.push({
304
+ // Store call_id so function_call_output.call_id matches it in subsequent turns
305
+ id: fc.call_id,
306
+ type: "function",
307
+ function: {
308
+ name: fc.name,
309
+ arguments: repairedArgs,
310
+ },
311
+ });
312
+ }
313
+ }
314
+
315
+ return {
316
+ choices: [
317
+ {
318
+ message: {
319
+ role: "assistant",
320
+ content: textContent,
321
+ ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
322
+ },
323
+ },
324
+ ],
325
+ model: options.model,
326
+ usage,
327
+ usd_cost: usdCost,
328
+ };
329
+ }
330
+
136
331
 
137
332
  pricesPerMillion() {
138
333
  return OpenAiTextPricing;
@@ -167,15 +362,23 @@ export class GenericOpenAiClient implements GenericClient {
167
362
  return total;
168
363
  }
169
364
 
170
- async getModels() {
171
- const models = await this.client.models.list();
172
- return models.data.map((m) => {
173
- return {
174
- id: m.id,
175
- object: m.object,
176
- owned_by: m.owned_by,
365
+ async getModels(modality?: ModelModality): Promise<{ id: string }[]> {
366
+ if (modality) {
367
+ const map: Partial<Record<ModelModality, string[]>> = {
368
+ completion: Object.values(Models.openai),
369
+ embedding: OpenAiEmbeddingModels,
370
+ image: OpenAiImageModels,
371
+ audio: [...OpenAiTTSModels, ...OpenAiTranscriptionModels],
372
+ transcription: OpenAiTranscriptionModels,
373
+ video: OpenAiVideoModels,
177
374
  };
178
- });
375
+ return (map[modality] ?? []).map((id) => ({ id }));
376
+ }
377
+ // No modality — live API call (backward compat)
378
+ const models = await this.client.models.list();
379
+ return models.data.map((m) => ({
380
+ id: m.id,
381
+ }));
179
382
  }
180
383
 
181
384
  async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
@@ -213,7 +416,7 @@ export class GenericOpenAiClient implements GenericClient {
213
416
 
214
417
  // Calculate cost: $0.006 per minute for Whisper
215
418
  const duration = typeof response === "object" && "duration" in response && typeof response.duration === "number"
216
- ? response.duration
419
+ ? response.duration
217
420
  : undefined;
218
421
  const usdCost = duration ? (duration / 60) * 0.006 : undefined;
219
422
 
@@ -1,3 +1,5 @@
1
+ export type ModelModality = "completion" | "embedding" | "image" | "audio" | "video" | "transcription";
2
+
1
3
  export type MessageContent =
2
4
  | { type: "text"; text: string }
3
5
  | { type: "image_url"; image_url: { url: string } }
@@ -257,7 +259,11 @@ export interface GenericClient {
257
259
  uploadFile?(options: FileUploadOptions): Promise<FileUploadResponse>;
258
260
  /** Download a file from the provider's file storage */
259
261
  downloadFile?(options: FileDownloadOptions): Promise<FileDownloadResponse>;
260
- getModels(): Promise<{ id: string }[]>;
262
+ /**
263
+ * When modality is provided, return only models for that modality (static list).
264
+ * When omitted, return ALL models (backward compat — may do a live API call).
265
+ */
266
+ getModels(modality?: ModelModality): Promise<{ id: string; modality?: ModelModality[] }[]>;
261
267
  /**
262
268
  * Returns the context window limit and compression threshold for a given model,
263
269
  * or undefined if the model is not known to this client.
@@ -27,7 +27,8 @@ import {
27
27
  ChatCompletionToolMessageParam,
28
28
  } from "openai/resources/chat";
29
29
 
30
- import { Models } from "../types";
30
+ import { Models, XaiImageModels, XaiVideoModels } from "../types";
31
+ import { ModelModality } from "./types";
31
32
 
32
33
  export class GenericXAIClient implements GenericClient {
33
34
  private client: OpenAI;
@@ -124,11 +125,17 @@ export class GenericXAIClient implements GenericClient {
124
125
  return total;
125
126
  }
126
127
 
127
- async getModels() {
128
- // XAI doesn't provide a model listing endpoint, so we'll return the static list
129
- return Object.keys(Models.xai).map((key) => ({
130
- id: Models.xai[key],
131
- }));
128
+ async getModels(modality?: ModelModality): Promise<{ id: string }[]> {
129
+ if (modality) {
130
+ const map: Partial<Record<ModelModality, string[]>> = {
131
+ completion: Object.values(Models.xai),
132
+ image: XaiImageModels,
133
+ video: XaiVideoModels,
134
+ };
135
+ return (map[modality] ?? []).map((id) => ({ id }));
136
+ }
137
+ // No modality — return full static list (XAI has no /models endpoint)
138
+ return Object.values(Models.xai).map((id) => ({ id }));
132
139
  }
133
140
 
134
141
  async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
@@ -0,0 +1,314 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import { glob } from "glob";
4
+ import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
5
+ import { loadJwt } from "./login";
6
+ import { getConfig, updateConfig, getLanguageConfig } from "./config";
7
+ import { services } from "./services";
8
+ import { Language, Config } from "./types";
9
+ import { S3Service } from "./services/S3";
10
+
11
+ export interface CloudWorkerOptions {
12
+ create?: boolean;
13
+ push?: string; // uid of existing cloud worker
14
+ name?: string; // optional name for create
15
+ apiUrl?: string;
16
+ dryRun?: boolean;
17
+ }
18
+
19
+ /**
20
+ * Represents a file to be synced to the remote cloud worker
21
+ */
22
+ interface FileToSync {
23
+ localPath: string;
24
+ remotePath: string;
25
+ downloadLocalPath?: string; // override localPath used when worker downloads the file
26
+ }
27
+
28
+ /**
29
+ * Build the worker config JSON from the local knowhow config
30
+ */
31
+ function buildWorkerConfigJson(config: Config, files: { remotePath: string; localPath: string; direction?: string }[]) {
32
+ return {
33
+ promptsDir: config.promptsDir,
34
+ modules: config.modules,
35
+ plugins: config.plugins,
36
+ lintCommands: config.lintCommands,
37
+ embedSources: config.embedSources,
38
+ sources: config.sources,
39
+ agents: config.agents,
40
+ files,
41
+ worker: {
42
+ tunnel: {
43
+ allowedPorts: config.worker?.tunnel?.allowedPorts ?? [],
44
+ },
45
+ },
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Collect all files from the .knowhow directory that should be synced
51
+ */
52
+ async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
53
+ const filesToSync: FileToSync[] = [];
54
+
55
+ // Helper to add file if it exists
56
+ const addIfExists = (localPath: string, remotePath: string) => {
57
+ if (fs.existsSync(localPath)) {
58
+ filesToSync.push({ localPath, remotePath });
59
+ }
60
+ };
61
+
62
+ // .knowhow/language.json
63
+ addIfExists(".knowhow/language.json", `${projectName}/.knowhow/language.json`);
64
+
65
+ // .knowhow/hashes.json
66
+ addIfExists(".knowhow/hashes.json", `${projectName}/.knowhow/hashes.json`);
67
+
68
+ // .knowhow/prompts/**/*
69
+ const promptFiles = await glob(".knowhow/prompts/**/*", { nodir: true });
70
+ for (const filePath of promptFiles) {
71
+ const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
72
+ const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
73
+ filesToSync.push({ localPath: filePath, remotePath });
74
+ }
75
+
76
+ // .knowhow/scripts/**/* (if exists)
77
+ if (fs.existsSync(".knowhow/scripts")) {
78
+ const scriptFiles = await glob(".knowhow/scripts/**/*", { nodir: true });
79
+ for (const filePath of scriptFiles) {
80
+ const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
81
+ const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
82
+ filesToSync.push({ localPath: filePath, remotePath });
83
+ }
84
+ }
85
+
86
+ // .knowhow/skills/**/* (if exists)
87
+ if (fs.existsSync(".knowhow/skills")) {
88
+ const skillFiles = await glob(".knowhow/skills/**/*", { nodir: true });
89
+ for (const filePath of skillFiles) {
90
+ const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
91
+ const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
92
+ filesToSync.push({ localPath: filePath, remotePath });
93
+ }
94
+ }
95
+
96
+ return filesToSync;
97
+ }
98
+
99
+ /**
100
+ * Collect files referenced in language.json sources
101
+ */
102
+ async function collectLanguageReferencedFiles(
103
+ language: Language,
104
+ projectName: string
105
+ ): Promise<FileToSync[]> {
106
+ const filesToSync: FileToSync[] = [];
107
+
108
+ for (const term of Object.keys(language)) {
109
+ const entry = language[term];
110
+ if (!entry.sources) continue;
111
+
112
+ for (const source of entry.sources) {
113
+ if (source.kind !== "file" || !source.data) continue;
114
+
115
+ for (const filePath of source.data) {
116
+ // Normalize the path (strip leading ./)
117
+ const normalizedPath = filePath.replace(/^\.\//, "");
118
+
119
+ // Skip the main knowhow config — it should not be synced to the language folder
120
+ // as it would overwrite the worker's own config
121
+ if (normalizedPath === ".knowhow/knowhow.json") continue;
122
+
123
+ if (fs.existsSync(normalizedPath)) {
124
+ const basename = path.basename(normalizedPath);
125
+ const remotePath = `${projectName}/.knowhow/language/${basename}`;
126
+ // localPath is the original path so the worker downloads it to the right place
127
+ filesToSync.push({ localPath: normalizedPath, remotePath, downloadLocalPath: normalizedPath });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ return filesToSync;
134
+ }
135
+
136
+ /**
137
+ * Upload a single file to the cloud worker's file storage
138
+ */
139
+ async function uploadSingleFile(
140
+ client: KnowhowSimpleClient,
141
+ s3Service: S3Service,
142
+ localPath: string,
143
+ remotePath: string,
144
+ dryRun: boolean
145
+ ): Promise<void> {
146
+ console.log(` ā¬†ļø Uploading ${localPath} → ${remotePath}`);
147
+
148
+ if (dryRun) {
149
+ console.log(` [DRY RUN] Would upload from ${localPath}`);
150
+ return;
151
+ }
152
+
153
+ if (!fs.existsSync(localPath)) {
154
+ console.warn(` āš ļø Local file not found, skipping: ${localPath}`);
155
+ return;
156
+ }
157
+
158
+ const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
159
+ await s3Service.uploadToPresignedUrl(presignedUrl, localPath);
160
+ await client.markOrgFileUploadComplete(remotePath);
161
+
162
+ const stats = fs.statSync(localPath);
163
+ console.log(` āœ“ Uploaded ${stats.size} bytes`);
164
+ }
165
+
166
+ /**
167
+ * Main cloudWorker command handler
168
+ */
169
+ export async function cloudWorker(options: CloudWorkerOptions) {
170
+ const {
171
+ create = false,
172
+ push,
173
+ name,
174
+ apiUrl = KNOWHOW_API_URL,
175
+ dryRun = false,
176
+ } = options;
177
+
178
+ if (!create && !push) {
179
+ console.error("āŒ Please specify --create or --push <uid>");
180
+ process.exit(1);
181
+ }
182
+
183
+ // Load JWT token
184
+ const jwt = await loadJwt();
185
+ if (!jwt) {
186
+ console.error("āŒ No JWT token found. Please run 'knowhow login' first.");
187
+ process.exit(1);
188
+ }
189
+
190
+ // Load local config
191
+ const config = await getConfig();
192
+ if (!config || Object.keys(config).length === 0) {
193
+ console.error("āŒ No knowhow config found. Please run 'knowhow init' first.");
194
+ process.exit(1);
195
+ }
196
+
197
+ // Load language config
198
+ const language = await getLanguageConfig();
199
+
200
+ // Get project name from current directory
201
+ const projectName = path.basename(process.cwd());
202
+ console.log(`šŸ“ Project name: ${projectName}`);
203
+
204
+ // Create API client
205
+ const client = new KnowhowSimpleClient(apiUrl, jwt);
206
+
207
+ // Get S3 service
208
+ const { AwsS3 } = services();
209
+
210
+ // Step 1: Collect all files to sync
211
+ console.log("\nšŸ“‚ Collecting files to sync...");
212
+ const mainFiles = await collectFilesToSync(projectName);
213
+ const languageFiles = await collectLanguageReferencedFiles(language, projectName);
214
+
215
+ // Deduplicate by remotePath
216
+ const allFilesMap = new Map<string, FileToSync>();
217
+ for (const f of [...mainFiles, ...languageFiles]) {
218
+ allFilesMap.set(f.remotePath, f);
219
+ }
220
+ const allFiles = Array.from(allFilesMap.values());
221
+
222
+ console.log(` Found ${allFiles.length} files to sync`);
223
+
224
+ if (dryRun) {
225
+ console.log("\nšŸ“‹ Files that would be synced:");
226
+ for (const f of allFiles) {
227
+ console.log(` ${f.localPath} → ${f.remotePath}`);
228
+ }
229
+ }
230
+
231
+ // Step 2: Build the config.files array for all synced files
232
+ const configFilesEntries = allFiles.map((f) => ({
233
+ remotePath: f.remotePath,
234
+ localPath: f.downloadLocalPath ?? f.localPath,
235
+ direction: "download" as const,
236
+ }));
237
+
238
+ // Step 3: Update config.files and save
239
+ console.log("\nšŸ’¾ Updating config.files with sync entries...");
240
+ if (!dryRun) {
241
+ // Preserve any existing files entries not in our set
242
+ const existingFiles = config.files || [];
243
+ const newRemotePaths = new Set(configFilesEntries.map((e) => e.remotePath));
244
+
245
+ // Keep entries that don't overlap with new ones
246
+ const preserved = existingFiles.filter(
247
+ (e) => !newRemotePaths.has(e.remotePath)
248
+ );
249
+
250
+ config.files = [...preserved, ...configFilesEntries];
251
+ await updateConfig(config);
252
+ console.log(` āœ“ Updated config with ${config.files.length} file entries`);
253
+ } else {
254
+ console.log(` [DRY RUN] Would update config with ${configFilesEntries.length} file entries`);
255
+ }
256
+
257
+ // Step 4: Build workerConfigJson
258
+ const workerConfigJson = buildWorkerConfigJson(config, configFilesEntries);
259
+
260
+ // Step 5: Upload all files
261
+ console.log(`\nšŸš€ Uploading ${allFiles.length} files...`);
262
+ let successCount = 0;
263
+ let failCount = 0;
264
+
265
+ for (const file of allFiles) {
266
+ try {
267
+ await uploadSingleFile(client, AwsS3, file.localPath, file.remotePath, dryRun);
268
+ successCount++;
269
+ } catch (error) {
270
+ console.error(` āŒ Failed to upload ${file.localPath}: ${error.message}`);
271
+ failCount++;
272
+ }
273
+ }
274
+
275
+ console.log(`\n āœ“ Upload complete: ${successCount} succeeded, ${failCount} failed`);
276
+
277
+ // Step 6: Create or update cloud worker
278
+ if (create) {
279
+ const workerName = name || `${projectName}-worker`;
280
+ console.log(`\nšŸŒ©ļø Creating cloud worker "${workerName}"...`);
281
+
282
+ if (dryRun) {
283
+ console.log(` [DRY RUN] Would create cloud worker with name: ${workerName}`);
284
+ console.log(` [DRY RUN] workerConfigJson:`, JSON.stringify(workerConfigJson, null, 2));
285
+ } else {
286
+ const result = await client.createCloudWorker({
287
+ name: workerName,
288
+ workerConfigJson,
289
+ });
290
+ const createdWorker = result.data;
291
+ console.log(` āœ“ Cloud worker created!`);
292
+ console.log(` ID: ${createdWorker.id}`);
293
+ console.log(` Name: ${createdWorker.name}`);
294
+ console.log(`\nšŸ’” To push updates later, run:`);
295
+ console.log(` knowhow cloudworker --push ${createdWorker.id}`);
296
+ }
297
+ } else if (push) {
298
+ console.log(`\nšŸŒ©ļø Updating cloud worker "${push}"...`);
299
+
300
+ if (dryRun) {
301
+ console.log(` [DRY RUN] Would update cloud worker ${push}`);
302
+ console.log(` [DRY RUN] workerConfigJson:`, JSON.stringify(workerConfigJson, null, 2));
303
+ } else {
304
+ await client.updateCloudWorker(push, { workerConfigJson });
305
+ console.log(` āœ“ Cloud worker updated!`);
306
+ }
307
+ }
308
+
309
+ if (failCount > 0) {
310
+ console.warn(`\nāš ļø ${failCount} file(s) failed to upload.`);
311
+ } else {
312
+ console.log(`\nāœ… Cloud worker sync complete!`);
313
+ }
314
+ }
package/src/config.ts CHANGED
@@ -4,6 +4,7 @@ import * as os from "os";
4
4
  import gitignoreToGlob from "gitignore-to-glob";
5
5
  import { Prompts } from "./prompts";
6
6
  import { promisify } from "util";
7
+ import { KNOWHOW_API_URL } from "./services/KnowhowClient";
7
8
  import {
8
9
  Config,
9
10
  Language,
@@ -85,7 +86,14 @@ const defaultConfig = {
85
86
  },
86
87
  ],
87
88
 
88
- modelProviders: [{ url: "http://localhost:1234", provider: "lms" }],
89
+ modelProviders: [
90
+ { provider: "openai", envKey: "OPENAI_API_KEY" },
91
+ { provider: "anthropic", envKey: "ANTHROPIC_API_KEY" },
92
+ { provider: "google", envKey: "GEMINI_API_KEY" },
93
+ { provider: "xai", envKey: "XAI_API_KEY" },
94
+ { provider: "knowhow" },
95
+ { provider: "lms", url: "http://localhost:1234" },
96
+ ],
89
97
 
90
98
  ycmd: {
91
99
  enabled: false,
@@ -241,7 +241,14 @@ export class TokenCompressor implements JsonCompressorStorage {
241
241
  public async compressMessage(message: Message) {
242
242
  // Compress content if it's a string
243
243
  if (typeof message.content === "string") {
244
- message.content = this.compressContent(message.content);
244
+ const compressed = this.compressContent(message.content);
245
+ // If this is a tool message with a tool_call_id, append it to the compressed content
246
+ // so the agent knows which toolCallId to use for grepToolResponse/jqToolResponse/tailToolResponse
247
+ if (message.role === "tool" && message.tool_call_id && compressed !== message.content) {
248
+ message.content = compressed + `\n[toolCallId: ${message.tool_call_id}]`;
249
+ } else {
250
+ message.content = compressed;
251
+ }
245
252
  }
246
253
  // Handle array content (multimodal)
247
254
  else if (Array.isArray(message.content)) {