@tyvm/knowhow 0.0.71 → 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/tools/startAgentTask.ts +3 -3
- package/src/ai.ts +12 -2
- package/src/chat/modules/AgentModule.ts +11 -7
- package/src/cli.ts +41 -14
- 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/tools/startAgentTask.js +2 -2
- package/ts_build/src/agents/tools/startAgentTask.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/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
|
@@ -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(
|
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/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;
|
|
@@ -221,15 +221,21 @@ export class KnowhowSimpleClient {
|
|
|
221
221
|
const formData = new FormData();
|
|
222
222
|
// options.file can be a Buffer, ReadStream, Blob, or File
|
|
223
223
|
if (Buffer.isBuffer(options.file)) {
|
|
224
|
-
formData.append(
|
|
224
|
+
formData.append(
|
|
225
|
+
"file",
|
|
226
|
+
new Blob([options.file]),
|
|
227
|
+
options.fileName || "audio.mp3"
|
|
228
|
+
);
|
|
225
229
|
} else {
|
|
226
230
|
formData.append("file", options.file);
|
|
227
231
|
}
|
|
228
232
|
if (options.model) formData.append("model", options.model);
|
|
229
233
|
if (options.language) formData.append("language", options.language);
|
|
230
234
|
if (options.prompt) formData.append("prompt", options.prompt);
|
|
231
|
-
if (options.response_format)
|
|
232
|
-
|
|
235
|
+
if (options.response_format)
|
|
236
|
+
formData.append("response_format", options.response_format);
|
|
237
|
+
if (options.temperature != null)
|
|
238
|
+
formData.append("temperature", String(options.temperature));
|
|
233
239
|
|
|
234
240
|
return axios.post<AudioTranscriptionResponse>(
|
|
235
241
|
`${this.baseUrl}/api/proxy/v1/audio/transcriptions`,
|
|
@@ -377,7 +383,11 @@ export class KnowhowSimpleClient {
|
|
|
377
383
|
/**
|
|
378
384
|
* Send a message to a running agent task
|
|
379
385
|
*/
|
|
380
|
-
async sendMessageToAgent(
|
|
386
|
+
async sendMessageToAgent(
|
|
387
|
+
taskId: string,
|
|
388
|
+
message: string,
|
|
389
|
+
role: "user" | "system" = "user"
|
|
390
|
+
) {
|
|
381
391
|
await this.checkJwt();
|
|
382
392
|
return axios.post<SendMessageResponse>(
|
|
383
393
|
`${this.baseUrl}/api/org-agent-tasks/${taskId}/messages`,
|
|
@@ -429,4 +439,166 @@ export class KnowhowSimpleClient {
|
|
|
429
439
|
}
|
|
430
440
|
);
|
|
431
441
|
}
|
|
442
|
+
|
|
443
|
+
// ============================================
|
|
444
|
+
// File Sync Methods
|
|
445
|
+
// Uses existing org-files endpoints: list, text GET/PUT, and create
|
|
446
|
+
// ============================================
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* List all org files for the current user's org
|
|
450
|
+
*/
|
|
451
|
+
async listOrgFiles() {
|
|
452
|
+
await this.checkJwt();
|
|
453
|
+
return axios.get<
|
|
454
|
+
{ id: string; fileName: string; folderPath: string; name: string }[]
|
|
455
|
+
>(`${this.baseUrl}/api/org-files`, { headers: this.headers });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Create a new org file record
|
|
460
|
+
*/
|
|
461
|
+
async createOrgFile(fileName: string, folderPath: string) {
|
|
462
|
+
await this.checkJwt();
|
|
463
|
+
return axios.post<{
|
|
464
|
+
id: string;
|
|
465
|
+
fileName: string;
|
|
466
|
+
folderPath: string;
|
|
467
|
+
name: string;
|
|
468
|
+
}>(
|
|
469
|
+
`${this.baseUrl}/api/org-files`,
|
|
470
|
+
{ fileName, folderPath, name: fileName },
|
|
471
|
+
{ headers: this.headers }
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get text content of an org file by id (returns streaming JSON array of strings)
|
|
477
|
+
*/
|
|
478
|
+
async getOrgFileText(fileId: string) {
|
|
479
|
+
await this.checkJwt();
|
|
480
|
+
return axios.get<string>(`${this.baseUrl}/api/org-files/${fileId}/text`, {
|
|
481
|
+
headers: this.headers,
|
|
482
|
+
params: { reading: "true" },
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Update text content of an org file by id
|
|
488
|
+
*/
|
|
489
|
+
async updateOrgFileText(fileId: string, text: string) {
|
|
490
|
+
await this.checkJwt();
|
|
491
|
+
return axios.put(
|
|
492
|
+
`${this.baseUrl}/api/org-files/${fileId}/text`,
|
|
493
|
+
{ text },
|
|
494
|
+
{ headers: this.headers }
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/**
|
|
499
|
+
* Find an org file by its full remote path (e.g. /test.md or /docs/readme.md)
|
|
500
|
+
* Returns null if not found
|
|
501
|
+
*/
|
|
502
|
+
async findOrgFileByPath(
|
|
503
|
+
remotePath: string
|
|
504
|
+
): Promise<{ id: string; fileName: string; folderPath: string } | null> {
|
|
505
|
+
const lastSlash = remotePath.lastIndexOf("/");
|
|
506
|
+
const rawFolder =
|
|
507
|
+
lastSlash >= 0 ? remotePath.substring(0, lastSlash + 1) : "/";
|
|
508
|
+
// Normalize: "/" and "" both mean root
|
|
509
|
+
const folderPath = rawFolder === "/" ? rawFolder : rawFolder;
|
|
510
|
+
const fileName =
|
|
511
|
+
lastSlash >= 0 ? remotePath.substring(lastSlash + 1) : remotePath;
|
|
512
|
+
|
|
513
|
+
const response = await this.listOrgFiles();
|
|
514
|
+
const files = response.data;
|
|
515
|
+
// DB may store root as "" or "/" - match both
|
|
516
|
+
const isRoot = folderPath === "/" || folderPath === "";
|
|
517
|
+
return (
|
|
518
|
+
files.find(
|
|
519
|
+
(f) =>
|
|
520
|
+
f.fileName === fileName &&
|
|
521
|
+
(f.folderPath === folderPath ||
|
|
522
|
+
(isRoot && (f.folderPath === "/" || f.folderPath === "")))
|
|
523
|
+
) || null
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Find or create an org file by path, returns the file record
|
|
529
|
+
*/
|
|
530
|
+
async findOrCreateOrgFileByPath(
|
|
531
|
+
remotePath: string
|
|
532
|
+
): Promise<{ id: string; fileName: string; folderPath: string }> {
|
|
533
|
+
const existing = await this.findOrgFileByPath(remotePath);
|
|
534
|
+
if (existing) return existing;
|
|
535
|
+
|
|
536
|
+
const lastSlash = remotePath.lastIndexOf("/");
|
|
537
|
+
const folderPath =
|
|
538
|
+
lastSlash >= 0 ? remotePath.substring(0, lastSlash + 1) || "/" : "/";
|
|
539
|
+
const fileName =
|
|
540
|
+
lastSlash >= 0 ? remotePath.substring(lastSlash + 1) : remotePath;
|
|
541
|
+
|
|
542
|
+
const response = await this.createOrgFile(fileName, folderPath);
|
|
543
|
+
return response.data;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Get presigned S3 URL for downloading a file from Knowhow FS.
|
|
548
|
+
* First finds or creates the file by path, then gets its download URL.
|
|
549
|
+
*/
|
|
550
|
+
async getOrgFilePresignedDownloadUrl(filePath: string): Promise<string> {
|
|
551
|
+
await this.checkJwt();
|
|
552
|
+
|
|
553
|
+
// Find the file by path
|
|
554
|
+
const file = await this.findOrgFileByPath(filePath);
|
|
555
|
+
if (!file) {
|
|
556
|
+
throw new Error(`File not found: ${filePath}`);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Get download URL using the file ID
|
|
560
|
+
const response = await axios.post<{ downloadUrl: string }>(
|
|
561
|
+
`${this.baseUrl}/api/org-files/download/${file.id}`,
|
|
562
|
+
{},
|
|
563
|
+
{ headers: this.headers }
|
|
564
|
+
);
|
|
565
|
+
return response.data.downloadUrl;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Notify the backend that a file upload is complete, updating its updatedAt timestamp.
|
|
570
|
+
*/
|
|
571
|
+
async markOrgFileUploadComplete(filePath: string): Promise<void> {
|
|
572
|
+
await this.checkJwt();
|
|
573
|
+
|
|
574
|
+
const file = await this.findOrgFileByPath(filePath);
|
|
575
|
+
if (!file) {
|
|
576
|
+
throw new Error(`File not found: ${filePath}`);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
await axios.post(`${this.baseUrl}/api/org-files/upload/${file.id}/complete`, {}, { headers: this.headers });
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Get presigned S3 URL for uploading a file to Knowhow FS.
|
|
584
|
+
* First finds or creates the file by path, then gets its upload URL.
|
|
585
|
+
*/
|
|
586
|
+
async getOrgFilePresignedUploadUrl(filePath: string): Promise<string> {
|
|
587
|
+
await this.checkJwt();
|
|
588
|
+
|
|
589
|
+
// Find or create the file by path
|
|
590
|
+
const file = await this.findOrCreateOrgFileByPath(filePath);
|
|
591
|
+
|
|
592
|
+
// Extract just the filename from the path
|
|
593
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
594
|
+
const fileName = lastSlash >= 0 ? filePath.substring(lastSlash + 1) : filePath;
|
|
595
|
+
|
|
596
|
+
// Get upload URL using the file ID
|
|
597
|
+
const response = await axios.post<{ uploadUrl: string }>(
|
|
598
|
+
`${this.baseUrl}/api/org-files/upload/${file.id}`,
|
|
599
|
+
{ fileName },
|
|
600
|
+
{ headers: this.headers }
|
|
601
|
+
);
|
|
602
|
+
return response.data.uploadUrl;
|
|
603
|
+
}
|
|
432
604
|
}
|
|
@@ -151,7 +151,7 @@ export class SessionManager {
|
|
|
151
151
|
).toLocaleString()})`
|
|
152
152
|
);
|
|
153
153
|
session.status = "failed";
|
|
154
|
-
|
|
154
|
+
// Preserve original lastUpdated so sessions don't all get the same timestamp
|
|
155
155
|
// Update the session file with failed status
|
|
156
156
|
fs.writeFileSync(filePath, JSON.stringify(session, null, 2));
|
|
157
157
|
}
|