@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.
- package/package.json +1 -1
- package/src/agents/base/base.ts +12 -4
- package/src/agents/patcher/patcher.ts +1 -1
- package/src/agents/tools/list.ts +1 -0
- package/src/chat/modules/AgentModule.ts +24 -22
- package/src/chat/modules/SessionsModule.ts +1 -3
- package/src/chat/modules/SystemModule.ts +23 -8
- package/src/cli.ts +17 -0
- package/src/clients/anthropic.ts +10 -4
- package/src/clients/gemini.ts +23 -5
- package/src/clients/http.ts +5 -3
- package/src/clients/index.ts +261 -139
- package/src/clients/knowhow.ts +15 -4
- package/src/clients/openai.ts +213 -10
- package/src/clients/types.ts +7 -1
- package/src/clients/xai.ts +13 -6
- package/src/cloudWorker.ts +314 -0
- package/src/config.ts +9 -1
- package/src/processors/TokenCompressor.ts +8 -1
- package/src/services/KnowhowClient.ts +56 -12
- package/src/services/LazyToolsService.ts +15 -14
- package/src/services/Tools.ts +10 -1
- package/src/types.ts +11 -2
- package/src/utils/InputQueueManager.ts +131 -20
- package/test-ai-completion.ts +39 -0
- package/test-mcp-args.ts +71 -0
- package/test-tools-service.ts +45 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +8 -2
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/patcher/patcher.js +1 -1
- package/ts_build/src/agents/patcher/patcher.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +1 -0
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +17 -19
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/chat/modules/SessionsModule.js +1 -3
- package/ts_build/src/chat/modules/SessionsModule.js.map +1 -1
- package/ts_build/src/chat/modules/SystemModule.js +16 -8
- package/ts_build/src/chat/modules/SystemModule.js.map +1 -1
- package/ts_build/src/cli.js +17 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.d.ts +9 -7
- package/ts_build/src/clients/anthropic.js +9 -4
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/gemini.d.ts +2 -1
- package/ts_build/src/clients/gemini.js +13 -4
- package/ts_build/src/clients/gemini.js.map +1 -1
- package/ts_build/src/clients/http.d.ts +1 -1
- package/ts_build/src/clients/http.js +2 -2
- package/ts_build/src/clients/http.js.map +1 -1
- package/ts_build/src/clients/index.d.ts +23 -47
- package/ts_build/src/clients/index.js +152 -99
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/knowhow.d.ts +3 -2
- package/ts_build/src/clients/knowhow.js +6 -3
- package/ts_build/src/clients/knowhow.js.map +1 -1
- package/ts_build/src/clients/openai.d.ts +20 -18
- package/ts_build/src/clients/openai.js +166 -8
- package/ts_build/src/clients/openai.js.map +1 -1
- package/ts_build/src/clients/types.d.ts +3 -1
- package/ts_build/src/clients/xai.d.ts +3 -2
- package/ts_build/src/clients/xai.js +10 -4
- package/ts_build/src/clients/xai.js.map +1 -1
- package/ts_build/src/cloudWorker.d.ts +8 -0
- package/ts_build/src/cloudWorker.js +239 -0
- package/ts_build/src/cloudWorker.js.map +1 -0
- package/ts_build/src/config.js +8 -1
- package/ts_build/src/config.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.js +7 -1
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +24 -1
- package/ts_build/src/services/KnowhowClient.js +14 -2
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/LazyToolsService.js +5 -7
- package/ts_build/src/services/LazyToolsService.js.map +1 -1
- package/ts_build/src/services/Tools.js +9 -1
- package/ts_build/src/services/Tools.js.map +1 -1
- package/ts_build/src/types.d.ts +3 -1
- package/ts_build/src/types.js +8 -1
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/utils/InputQueueManager.d.ts +2 -0
- package/ts_build/src/utils/InputQueueManager.js +76 -10
- package/ts_build/src/utils/InputQueueManager.js.map +1 -1
package/src/clients/openai.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
package/src/clients/types.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/src/clients/xai.ts
CHANGED
|
@@ -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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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: [
|
|
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
|
-
|
|
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)) {
|