@tyvm/knowhow 0.0.70 → 0.0.72
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 +3 -2
- package/src/agents/tools/startAgentTask.ts +3 -3
- package/src/agents/tools/stringReplace.ts +42 -12
- package/src/ai.ts +12 -2
- package/src/chat/modules/AgentModule.ts +11 -7
- package/src/cli.ts +41 -14
- package/src/clients/anthropic.ts +41 -22
- package/src/fileSync.ts +165 -0
- package/src/services/AgentSyncFs.ts +25 -6
- package/src/services/AgentSyncKnowhowWeb.ts +25 -6
- package/src/services/KnowhowClient.ts +176 -4
- package/src/services/SessionManager.ts +1 -1
- package/src/types.ts +6 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.d.ts +3 -3
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/tools/startAgentTask.js +2 -2
- package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
- package/ts_build/src/agents/tools/stringReplace.js +29 -12
- package/ts_build/src/agents/tools/stringReplace.js.map +1 -1
- package/ts_build/src/ai.js +8 -2
- package/ts_build/src/ai.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +6 -2
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/cli.js +27 -10
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +43 -21
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/fileSync.d.ts +8 -0
- package/ts_build/src/fileSync.js +116 -0
- package/ts_build/src/fileSync.js.map +1 -0
- package/ts_build/src/services/AgentSyncFs.d.ts +3 -0
- package/ts_build/src/services/AgentSyncFs.js +21 -4
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +3 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +21 -4
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/KnowhowClient.d.ts +27 -0
- package/ts_build/src/services/KnowhowClient.js +67 -1
- package/ts_build/src/services/KnowhowClient.js.map +1 -1
- package/ts_build/src/services/SessionManager.js +0 -1
- package/ts_build/src/services/SessionManager.js.map +1 -1
- package/ts_build/src/types.d.ts +5 -0
- package/ts_build/src/types.js.map +1 -1
- package/ts_build/src/workerSync.d.ts +8 -0
- package/ts_build/src/workerSync.js +120 -0
- package/ts_build/src/workerSync.js.map +1 -0
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { EventEmitter } from "events";
|
|
|
2
2
|
import {
|
|
3
3
|
GenericClient,
|
|
4
4
|
Message,
|
|
5
|
+
MessageContent,
|
|
5
6
|
OutputMessage,
|
|
6
7
|
Tool,
|
|
7
8
|
ToolCall,
|
|
@@ -318,7 +319,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
318
319
|
return this.summaries;
|
|
319
320
|
}
|
|
320
321
|
|
|
321
|
-
abstract getInitialMessages(userInput: string): Promise<Message[]>;
|
|
322
|
+
abstract getInitialMessages(userInput: string | MessageContent[]): Promise<Message[]>;
|
|
322
323
|
|
|
323
324
|
async processToolMessages(toolCall: ToolCall) {
|
|
324
325
|
this.agentEvents.emit(this.eventTypes.toolCall, { toolCall });
|
|
@@ -479,7 +480,7 @@ export abstract class BaseAgent implements IAgent {
|
|
|
479
480
|
} as Message);
|
|
480
481
|
}
|
|
481
482
|
|
|
482
|
-
async call(userInput: string, _messages?: Message[]) {
|
|
483
|
+
async call(userInput: string | MessageContent[], _messages?: Message[]) {
|
|
483
484
|
if (this.status === this.eventTypes.notStarted) {
|
|
484
485
|
this.status = this.eventTypes.inProgress;
|
|
485
486
|
}
|
|
@@ -139,12 +139,12 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
|
|
|
139
139
|
|
|
140
140
|
const syncFsNote = syncFs
|
|
141
141
|
? `\nTask ID: ${taskId}\nAgent dir: ${agentTaskDir}\n` +
|
|
142
|
-
`To send
|
|
142
|
+
`To send agent messages, write to: ${agentTaskDir}/input.txt\n` +
|
|
143
143
|
`To check status, read: ${agentTaskDir}/status.txt\n`
|
|
144
144
|
: "";
|
|
145
145
|
|
|
146
|
-
// Give the agent
|
|
147
|
-
const detachTime =
|
|
146
|
+
// Give the agent 5 seconds to finish before detaching
|
|
147
|
+
const detachTime = 5 * 1000;
|
|
148
148
|
const tid = setTimeout(() => {
|
|
149
149
|
try { child.unref(); } catch {}
|
|
150
150
|
done(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
|
-
import {
|
|
3
|
-
import { lintFile } from ".";
|
|
2
|
+
import { services, ToolsService } from "../../services";
|
|
4
3
|
import { fileExists } from "../../utils";
|
|
5
4
|
|
|
6
5
|
export async function stringReplace(
|
|
@@ -8,6 +7,12 @@ export async function stringReplace(
|
|
|
8
7
|
replaceString: string,
|
|
9
8
|
filePaths: string[]
|
|
10
9
|
): Promise<string> {
|
|
10
|
+
// Get context from bound ToolsService
|
|
11
|
+
const toolService = (
|
|
12
|
+
this instanceof ToolsService ? this : services().Tools
|
|
13
|
+
) as ToolsService;
|
|
14
|
+
const context = toolService.getContext();
|
|
15
|
+
|
|
11
16
|
if (
|
|
12
17
|
!findString ||
|
|
13
18
|
replaceString === undefined ||
|
|
@@ -41,6 +46,20 @@ export async function stringReplace(
|
|
|
41
46
|
continue;
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
// Emit pre-edit blocking event
|
|
50
|
+
const eventResults: any[] = [];
|
|
51
|
+
if (context.Events) {
|
|
52
|
+
eventResults.push(
|
|
53
|
+
...(await context.Events.emitBlocking("file:pre-edit", {
|
|
54
|
+
filePath,
|
|
55
|
+
operation: "stringReplace",
|
|
56
|
+
findString,
|
|
57
|
+
replaceString,
|
|
58
|
+
originalContent,
|
|
59
|
+
}))
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
44
63
|
// Perform the replacement
|
|
45
64
|
const newContent = content.replace(
|
|
46
65
|
new RegExp(findString.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g"),
|
|
@@ -53,18 +72,29 @@ export async function stringReplace(
|
|
|
53
72
|
totalReplacements += matches;
|
|
54
73
|
results.push(`✅ Replaced ${matches} occurrence(s) in: ${filePath}`);
|
|
55
74
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
// Emit post-edit blocking event to get event results
|
|
76
|
+
if (context.Events) {
|
|
77
|
+
eventResults.push(
|
|
78
|
+
...(await context.Events.emitBlocking("file:post-edit", {
|
|
79
|
+
filePath,
|
|
80
|
+
operation: "stringReplace",
|
|
81
|
+
findString,
|
|
82
|
+
replaceString,
|
|
83
|
+
originalContent,
|
|
84
|
+
updatedContent: newContent,
|
|
85
|
+
}))
|
|
86
|
+
);
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
89
|
+
// Format event results if any
|
|
90
|
+
if (eventResults && eventResults.length > 0) {
|
|
91
|
+
const eventResultsText = eventResults
|
|
92
|
+
.filter((r) => r && typeof r === "string" && r.trim())
|
|
93
|
+
.join("\n");
|
|
94
|
+
if (eventResultsText) {
|
|
95
|
+
results.push(eventResultsText);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
68
98
|
} catch (error) {
|
|
69
99
|
results.push(`❌ Error processing ${filePath}: ${error.message}`);
|
|
70
100
|
}
|
package/src/ai.ts
CHANGED
|
@@ -25,9 +25,19 @@ export function readPromptFile(promptFile: string, input: string) {
|
|
|
25
25
|
if (fs.existsSync(promptFile)) {
|
|
26
26
|
const promptTemplate = fs.readFileSync(promptFile, "utf-8");
|
|
27
27
|
if (promptTemplate.includes("{text}")) {
|
|
28
|
-
|
|
28
|
+
// Only replace if input is provided
|
|
29
|
+
if (input) {
|
|
30
|
+
return promptTemplate.replaceAll("{text}", input);
|
|
31
|
+
}
|
|
32
|
+
// If no input provided but template expects it, return template as-is
|
|
33
|
+
// This allows the calling code to handle the missing input
|
|
34
|
+
return promptTemplate;
|
|
29
35
|
} else {
|
|
30
|
-
|
|
36
|
+
// Template doesn't have {text}, so input is optional
|
|
37
|
+
if (input) {
|
|
38
|
+
return `${promptTemplate}\n\n${input}`;
|
|
39
|
+
}
|
|
40
|
+
return promptTemplate;
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
}
|
|
@@ -528,6 +528,10 @@ Please continue from where you left off and complete the original request.
|
|
|
528
528
|
// Save initial session
|
|
529
529
|
this.saveSession(taskId, taskInfo, []);
|
|
530
530
|
|
|
531
|
+
// Reset sync services before setting up new task (removes old listeners)
|
|
532
|
+
this.webSync.reset();
|
|
533
|
+
this.fsSync.reset();
|
|
534
|
+
|
|
531
535
|
// Create Knowhow chat task if messageId provided
|
|
532
536
|
if (
|
|
533
537
|
options.messageId &&
|
|
@@ -566,13 +570,11 @@ Please continue from where you left off and complete the original request.
|
|
|
566
570
|
}
|
|
567
571
|
|
|
568
572
|
// Set up session update listener
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
}
|
|
575
|
-
);
|
|
573
|
+
const threadUpdateHandler = async (threadState: any) => {
|
|
574
|
+
this.updateSession(taskId, threadState);
|
|
575
|
+
taskInfo.totalCost = agent.getTotalCostUsd();
|
|
576
|
+
};
|
|
577
|
+
agent.agentEvents.on(agent.eventTypes.threadUpdate, threadUpdateHandler);
|
|
576
578
|
|
|
577
579
|
console.log(
|
|
578
580
|
Marked.parse(`**Starting ${agent.name} with task ID: ${taskId}...**`)
|
|
@@ -665,6 +667,8 @@ Please continue from where you left off and complete the original request.
|
|
|
665
667
|
console.log("🎯 [AgentModule] Task Completed");
|
|
666
668
|
done = true;
|
|
667
669
|
output = doneMsg || "No response from the AI";
|
|
670
|
+
// Remove threadUpdate listener to prevent cost sharing across tasks
|
|
671
|
+
agent.agentEvents.removeListener(agent.eventTypes.threadUpdate, threadUpdateHandler);
|
|
668
672
|
// Update task info
|
|
669
673
|
taskInfo = this.taskRegistry.get(taskId);
|
|
670
674
|
|
package/src/cli.ts
CHANGED
|
@@ -15,6 +15,7 @@ import * as allTools from "./agents/tools";
|
|
|
15
15
|
import { LazyToolsService, services } from "./services";
|
|
16
16
|
import { login } from "./login";
|
|
17
17
|
import { worker } from "./worker";
|
|
18
|
+
import { fileSync } from "./fileSync";
|
|
18
19
|
import {
|
|
19
20
|
startAllWorkers,
|
|
20
21
|
listWorkerPaths,
|
|
@@ -211,18 +212,23 @@ async function main() {
|
|
|
211
212
|
try {
|
|
212
213
|
await setupServices();
|
|
213
214
|
let input = options.input;
|
|
214
|
-
|
|
215
|
+
|
|
216
|
+
// Only read from stdin if we don't have input and don't have a standalone prompt file
|
|
217
|
+
if (!input && !options.promptFile) {
|
|
215
218
|
input = await readStdin();
|
|
216
|
-
if (!input) {
|
|
217
|
-
console.error(
|
|
218
|
-
"Error: No input provided. Use --input flag or pipe input via stdin."
|
|
219
|
-
);
|
|
220
|
-
process.exit(1);
|
|
221
|
-
}
|
|
222
219
|
}
|
|
223
220
|
|
|
221
|
+
// Read prompt file - it will handle cases where input is empty
|
|
224
222
|
input = readPromptFile(options.promptFile, input);
|
|
225
223
|
|
|
224
|
+
// Only error if we have no prompt file and no input
|
|
225
|
+
if (!input) {
|
|
226
|
+
console.error(
|
|
227
|
+
"Error: No input provided. Use --input flag, pipe input via stdin, or provide --prompt-file."
|
|
228
|
+
);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
|
|
226
232
|
const agentModule = new AgentModule();
|
|
227
233
|
await agentModule.initialize(chatService);
|
|
228
234
|
const { taskCompleted } = await agentModule.setupAgent({
|
|
@@ -250,18 +256,23 @@ async function main() {
|
|
|
250
256
|
try {
|
|
251
257
|
await setupServices();
|
|
252
258
|
let input = options.input;
|
|
253
|
-
|
|
259
|
+
|
|
260
|
+
// Only read from stdin if we don't have input and don't have a standalone prompt file
|
|
261
|
+
if (!input && !options.promptFile) {
|
|
254
262
|
input = await readStdin();
|
|
255
|
-
if (!input) {
|
|
256
|
-
console.error(
|
|
257
|
-
"Error: No question provided. Use --input flag or pipe input via stdin."
|
|
258
|
-
);
|
|
259
|
-
process.exit(1);
|
|
260
|
-
}
|
|
261
263
|
}
|
|
262
264
|
|
|
265
|
+
// Read prompt file - it will handle cases where input is empty
|
|
263
266
|
input = readPromptFile(options.promptFile, input);
|
|
264
267
|
|
|
268
|
+
// Only error if we have no prompt file and no input
|
|
269
|
+
if (!input) {
|
|
270
|
+
console.error(
|
|
271
|
+
"Error: No question provided. Use --input flag, pipe input via stdin, or provide --prompt-file."
|
|
272
|
+
);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
}
|
|
275
|
+
|
|
265
276
|
const askModule = new AskModule();
|
|
266
277
|
await askModule.initialize(chatService);
|
|
267
278
|
await askModule.processAIQuery(input, {
|
|
@@ -358,6 +369,22 @@ async function main() {
|
|
|
358
369
|
await worker(options);
|
|
359
370
|
});
|
|
360
371
|
|
|
372
|
+
program
|
|
373
|
+
.command("files")
|
|
374
|
+
.description("Sync files between local filesystem and Knowhow FS (uses fileMounts config)")
|
|
375
|
+
.option("--upload", "Force upload direction for all mounts")
|
|
376
|
+
.option("--download", "Force download direction for all mounts")
|
|
377
|
+
.option("--config <path>", "Path to knowhow.json", "./knowhow.json")
|
|
378
|
+
.option("--dry-run", "Print what would be synced without doing it")
|
|
379
|
+
.action(async (options) => {
|
|
380
|
+
try {
|
|
381
|
+
await fileSync(options);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
console.error("Error syncing files:", error);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
361
388
|
program
|
|
362
389
|
.command("workers")
|
|
363
390
|
.description("Manage and start all registered workers")
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -243,19 +243,29 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
243
243
|
if (item.type === "image_url") {
|
|
244
244
|
const url = item.image_url.url;
|
|
245
245
|
const isDataUrl = url.startsWith("data:");
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
246
|
+
const isHttpUrl = url.startsWith("http");
|
|
247
|
+
if (isHttpUrl) {
|
|
248
|
+
return {
|
|
249
|
+
type: "image" as const,
|
|
250
|
+
source: {
|
|
251
|
+
type: "url" as const,
|
|
252
|
+
url,
|
|
253
|
+
},
|
|
254
|
+
} as Anthropic.ImageBlockParam;
|
|
255
|
+
} else {
|
|
256
|
+
const base64Data = isDataUrl ? url.split(",")[1] : url;
|
|
257
|
+
const mediaType = isDataUrl
|
|
258
|
+
? url.match(/data:([^;]+);/)?.[1] || "image/jpeg"
|
|
259
|
+
: "image/jpeg";
|
|
260
|
+
return {
|
|
261
|
+
type: "image" as const,
|
|
262
|
+
source: {
|
|
263
|
+
type: "base64" as const,
|
|
264
|
+
media_type: mediaType as any,
|
|
265
|
+
data: base64Data,
|
|
266
|
+
},
|
|
267
|
+
};
|
|
268
|
+
}
|
|
259
269
|
} else if (item.type === "text") {
|
|
260
270
|
return { type: "text" as const, text: item.text };
|
|
261
271
|
}
|
|
@@ -308,15 +318,24 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
308
318
|
}
|
|
309
319
|
if (typeof e === "object" && e.type === "image_url") {
|
|
310
320
|
const isUrl = e.image_url.url.startsWith("http");
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}
|
|
321
|
+
if (isUrl) {
|
|
322
|
+
return {
|
|
323
|
+
type: "image",
|
|
324
|
+
source: {
|
|
325
|
+
type: "url" as const,
|
|
326
|
+
url: e.image_url.url,
|
|
327
|
+
},
|
|
328
|
+
} as Anthropic.ContentBlockParam;
|
|
329
|
+
} else {
|
|
330
|
+
return {
|
|
331
|
+
type: "image",
|
|
332
|
+
source: {
|
|
333
|
+
type: "base64" as const,
|
|
334
|
+
media_type: "image/jpeg",
|
|
335
|
+
data: e.image_url.url,
|
|
336
|
+
},
|
|
337
|
+
} as Anthropic.ContentBlockParam;
|
|
338
|
+
}
|
|
320
339
|
}
|
|
321
340
|
};
|
|
322
341
|
|
package/src/fileSync.ts
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
|
|
4
|
+
import { loadJwt } from "./login";
|
|
5
|
+
import { getConfig } from "./config";
|
|
6
|
+
import { services } from "./services";
|
|
7
|
+
import { S3Service } from "./services/S3";
|
|
8
|
+
|
|
9
|
+
export interface FileSyncOptions {
|
|
10
|
+
upload?: boolean;
|
|
11
|
+
download?: boolean;
|
|
12
|
+
apiUrl?: string;
|
|
13
|
+
configPath?: string;
|
|
14
|
+
dryRun?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sync files between local filesystem and Knowhow FS
|
|
19
|
+
*/
|
|
20
|
+
export async function fileSync(options: FileSyncOptions = {}) {
|
|
21
|
+
const {
|
|
22
|
+
upload = false,
|
|
23
|
+
download = false,
|
|
24
|
+
apiUrl = KNOWHOW_API_URL,
|
|
25
|
+
configPath = "./knowhow.json",
|
|
26
|
+
dryRun = false,
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
// Load configuration
|
|
30
|
+
const config = await getConfig();
|
|
31
|
+
|
|
32
|
+
// Check if files is configured
|
|
33
|
+
if (!config.files || config.files.length === 0) {
|
|
34
|
+
console.log("✓ No files configured, skipping sync");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Load JWT token
|
|
39
|
+
const jwt = await loadJwt();
|
|
40
|
+
if (!jwt) {
|
|
41
|
+
console.error("❌ No JWT token found. Please run 'knowhow login' first.");
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Create API client
|
|
46
|
+
const client = new KnowhowSimpleClient(apiUrl, jwt);
|
|
47
|
+
|
|
48
|
+
// Get S3 service for presigned URL operations
|
|
49
|
+
const { AwsS3 } = services();
|
|
50
|
+
|
|
51
|
+
console.log(`🔄 Starting file sync (${config.files.length} mounts)...`);
|
|
52
|
+
|
|
53
|
+
let successCount = 0;
|
|
54
|
+
let failCount = 0;
|
|
55
|
+
|
|
56
|
+
// Process each file mount
|
|
57
|
+
for (const mount of config.files) {
|
|
58
|
+
const { remotePath, localPath, direction = "download" } = mount;
|
|
59
|
+
|
|
60
|
+
// Determine actual direction based on flags and config
|
|
61
|
+
let actualDirection = direction;
|
|
62
|
+
if (upload) {
|
|
63
|
+
actualDirection = "upload";
|
|
64
|
+
} else if (download) {
|
|
65
|
+
actualDirection = "download";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
if (actualDirection === "download") {
|
|
70
|
+
await downloadFile(client, AwsS3, remotePath, localPath, dryRun);
|
|
71
|
+
successCount++;
|
|
72
|
+
} else if (actualDirection === "upload") {
|
|
73
|
+
await uploadFile(client, AwsS3, remotePath, localPath, dryRun);
|
|
74
|
+
successCount++;
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(`❌ Failed to sync ${remotePath}: ${error.message}`);
|
|
78
|
+
failCount++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log(
|
|
83
|
+
`\n✓ Sync complete: ${successCount} succeeded, ${failCount} failed`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (failCount > 0) {
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Download a file from Knowhow FS to local filesystem
|
|
93
|
+
*/
|
|
94
|
+
async function downloadFile(
|
|
95
|
+
client: KnowhowSimpleClient,
|
|
96
|
+
s3Service: S3Service,
|
|
97
|
+
remotePath: string,
|
|
98
|
+
localPath: string,
|
|
99
|
+
dryRun: boolean
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
console.log(`⬇️ Downloading ${remotePath} → ${localPath}`);
|
|
102
|
+
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
console.log(` [DRY RUN] Would download to ${localPath}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Get presigned download URL
|
|
110
|
+
const presignedUrl = await client.getOrgFilePresignedDownloadUrl(
|
|
111
|
+
remotePath
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Ensure parent directory exists
|
|
115
|
+
const dir = path.dirname(localPath);
|
|
116
|
+
if (!fs.existsSync(dir)) {
|
|
117
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Download file using presigned URL
|
|
121
|
+
await s3Service.downloadFromPresignedUrl(presignedUrl, localPath);
|
|
122
|
+
|
|
123
|
+
// Get file size for logging
|
|
124
|
+
const stats = fs.statSync(localPath);
|
|
125
|
+
console.log(` ✓ Downloaded ${stats.size} bytes`);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Upload a file from local filesystem to Knowhow FS
|
|
133
|
+
*/
|
|
134
|
+
async function uploadFile(
|
|
135
|
+
client: KnowhowSimpleClient,
|
|
136
|
+
s3Service: S3Service,
|
|
137
|
+
remotePath: string,
|
|
138
|
+
localPath: string,
|
|
139
|
+
dryRun: boolean
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
console.log(`⬆️ Uploading ${localPath} → ${remotePath}`);
|
|
142
|
+
|
|
143
|
+
if (dryRun) {
|
|
144
|
+
console.log(` [DRY RUN] Would upload from ${localPath}`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check if local file exists
|
|
149
|
+
if (!fs.existsSync(localPath)) {
|
|
150
|
+
console.warn(` ⚠️ Local file not found: ${localPath}`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Get presigned upload URL
|
|
155
|
+
const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
|
|
156
|
+
|
|
157
|
+
// Upload file using presigned URL
|
|
158
|
+
await s3Service.uploadToPresignedUrl(presignedUrl, localPath);
|
|
159
|
+
|
|
160
|
+
// Notify backend that upload is complete to update the updatedAt timestamp
|
|
161
|
+
await client.markOrgFileUploadComplete(remotePath);
|
|
162
|
+
|
|
163
|
+
const stats = fs.statSync(localPath);
|
|
164
|
+
console.log(` ✓ Uploaded ${stats.size} bytes`);
|
|
165
|
+
}
|
|
@@ -25,6 +25,9 @@ export class AgentSyncFs {
|
|
|
25
25
|
private lastInputContent: string = "";
|
|
26
26
|
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
27
27
|
private finalizationPromise: Promise<void> | null = null;
|
|
28
|
+
private agent: BaseAgent | undefined;
|
|
29
|
+
private threadUpdateHandler: ((...args: any[]) => void) | undefined;
|
|
30
|
+
private doneHandler: ((...args: any[]) => void) | undefined;
|
|
28
31
|
|
|
29
32
|
constructor() {
|
|
30
33
|
// Start cleanup process when created
|
|
@@ -281,8 +284,10 @@ export class AgentSyncFs {
|
|
|
281
284
|
* Set up event handlers for automatic synchronization
|
|
282
285
|
*/
|
|
283
286
|
private setupEventHandlers(agent: BaseAgent): void {
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
this.agent = agent;
|
|
288
|
+
|
|
289
|
+
// Listen to thread updates to sync state (store reference for cleanup)
|
|
290
|
+
this.threadUpdateHandler = async () => {
|
|
286
291
|
if (!this.taskId) return;
|
|
287
292
|
|
|
288
293
|
try {
|
|
@@ -291,10 +296,11 @@ export class AgentSyncFs {
|
|
|
291
296
|
} catch (error) {
|
|
292
297
|
console.error(`❌ Error during threadUpdate sync:`, error);
|
|
293
298
|
}
|
|
294
|
-
}
|
|
299
|
+
};
|
|
300
|
+
agent.agentEvents.on(agent.eventTypes.threadUpdate, this.threadUpdateHandler);
|
|
295
301
|
|
|
296
|
-
// Listen to completion event to finalize task
|
|
297
|
-
|
|
302
|
+
// Listen to completion event to finalize task (store reference for cleanup)
|
|
303
|
+
this.doneHandler = (result: string) => {
|
|
298
304
|
if (!this.taskId) {
|
|
299
305
|
console.warn(`⚠️ [AgentSyncFs] Cannot finalize: taskId=${this.taskId}`);
|
|
300
306
|
return;
|
|
@@ -313,7 +319,8 @@ export class AgentSyncFs {
|
|
|
313
319
|
throw error;
|
|
314
320
|
}
|
|
315
321
|
})();
|
|
316
|
-
}
|
|
322
|
+
};
|
|
323
|
+
agent.agentEvents.on(agent.eventTypes.done, this.doneHandler);
|
|
317
324
|
}
|
|
318
325
|
|
|
319
326
|
/**
|
|
@@ -407,6 +414,18 @@ export class AgentSyncFs {
|
|
|
407
414
|
* Reset synchronization state
|
|
408
415
|
*/
|
|
409
416
|
reset(): void {
|
|
417
|
+
// Remove old event listeners from the agent before resetting
|
|
418
|
+
if (this.agent) {
|
|
419
|
+
if (this.threadUpdateHandler) {
|
|
420
|
+
this.agent.agentEvents.removeListener(this.agent.eventTypes.threadUpdate, this.threadUpdateHandler);
|
|
421
|
+
this.threadUpdateHandler = undefined;
|
|
422
|
+
}
|
|
423
|
+
if (this.doneHandler) {
|
|
424
|
+
this.agent.agentEvents.removeListener(this.agent.eventTypes.done, this.doneHandler);
|
|
425
|
+
this.doneHandler = undefined;
|
|
426
|
+
}
|
|
427
|
+
this.agent = undefined;
|
|
428
|
+
}
|
|
410
429
|
this.cleanup();
|
|
411
430
|
this.taskId = undefined;
|
|
412
431
|
this.taskPath = undefined;
|
|
@@ -33,6 +33,9 @@ export class AgentSyncKnowhowWeb {
|
|
|
33
33
|
private knowhowTaskId: string | undefined;
|
|
34
34
|
private eventHandlersSetup: boolean = false;
|
|
35
35
|
private finalizationPromise: Promise<void> | null = null;
|
|
36
|
+
private agent: BaseAgent | undefined;
|
|
37
|
+
private threadUpdateHandler: ((...args: any[]) => void) | undefined;
|
|
38
|
+
private doneHandler: ((...args: any[]) => void) | undefined;
|
|
36
39
|
|
|
37
40
|
constructor(baseUrl: string = KNOWHOW_API_URL) {
|
|
38
41
|
this.baseUrl = baseUrl;
|
|
@@ -228,8 +231,10 @@ export class AgentSyncKnowhowWeb {
|
|
|
228
231
|
* Set up event handlers for automatic synchronization
|
|
229
232
|
*/
|
|
230
233
|
private setupEventHandlers(agent: BaseAgent): void {
|
|
231
|
-
|
|
232
|
-
|
|
234
|
+
this.agent = agent;
|
|
235
|
+
|
|
236
|
+
// Listen to thread updates to sync state and check for pending messages (store reference for cleanup)
|
|
237
|
+
this.threadUpdateHandler = async () => {
|
|
233
238
|
if (!this.knowhowTaskId || !this.baseUrl) {
|
|
234
239
|
return;
|
|
235
240
|
}
|
|
@@ -244,10 +249,11 @@ export class AgentSyncKnowhowWeb {
|
|
|
244
249
|
console.error(`❌ Error during threadUpdate sync:`, error);
|
|
245
250
|
// Continue execution even if synchronization fails
|
|
246
251
|
}
|
|
247
|
-
}
|
|
252
|
+
};
|
|
253
|
+
agent.agentEvents.on(agent.eventTypes.threadUpdate, this.threadUpdateHandler);
|
|
248
254
|
|
|
249
|
-
// Listen to completion event to finalize task
|
|
250
|
-
|
|
255
|
+
// Listen to completion event to finalize task (store reference for cleanup)
|
|
256
|
+
this.doneHandler = async (result: string) => {
|
|
251
257
|
if (!this.knowhowTaskId || !this.baseUrl) {
|
|
252
258
|
console.warn(`⚠️ [AgentSync] Cannot finalize: knowhowTaskId=${this.knowhowTaskId}, baseUrl=${this.baseUrl}`);
|
|
253
259
|
return;
|
|
@@ -268,7 +274,8 @@ export class AgentSyncKnowhowWeb {
|
|
|
268
274
|
throw error; // Re-throw so CLI can handle it
|
|
269
275
|
}
|
|
270
276
|
})();
|
|
271
|
-
}
|
|
277
|
+
};
|
|
278
|
+
agent.agentEvents.on(agent.eventTypes.done, this.doneHandler);
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
/**
|
|
@@ -284,6 +291,18 @@ export class AgentSyncKnowhowWeb {
|
|
|
284
291
|
* Reset synchronization state (useful for reusing the service)
|
|
285
292
|
*/
|
|
286
293
|
reset(): void {
|
|
294
|
+
// Remove old event listeners from the agent before resetting
|
|
295
|
+
if (this.agent) {
|
|
296
|
+
if (this.threadUpdateHandler) {
|
|
297
|
+
this.agent.agentEvents.removeListener(this.agent.eventTypes.threadUpdate, this.threadUpdateHandler);
|
|
298
|
+
this.threadUpdateHandler = undefined;
|
|
299
|
+
}
|
|
300
|
+
if (this.doneHandler) {
|
|
301
|
+
this.agent.agentEvents.removeListener(this.agent.eventTypes.done, this.doneHandler);
|
|
302
|
+
this.doneHandler = undefined;
|
|
303
|
+
}
|
|
304
|
+
this.agent = undefined;
|
|
305
|
+
}
|
|
287
306
|
this.knowhowTaskId = undefined;
|
|
288
307
|
this.eventHandlersSetup = false;
|
|
289
308
|
this.finalizationPromise = null;
|