@tyvm/knowhow 0.0.80 → 0.0.82

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/agents/base/base.ts +27 -2
  3. package/src/agents/tools/startAgentTask.ts +24 -3
  4. package/src/chat/modules/AgentModule.ts +118 -1
  5. package/src/cli.ts +43 -6
  6. package/src/clients/http.ts +108 -67
  7. package/src/index.ts +11 -0
  8. package/src/login.ts +0 -4
  9. package/src/services/KnowhowClient.ts +28 -1
  10. package/src/services/McpWebsocketTransport.ts +7 -1
  11. package/ts_build/package.json +1 -1
  12. package/ts_build/src/agents/base/base.d.ts +1 -1
  13. package/ts_build/src/agents/base/base.js +19 -2
  14. package/ts_build/src/agents/base/base.js.map +1 -1
  15. package/ts_build/src/agents/tools/startAgentTask.d.ts +2 -0
  16. package/ts_build/src/agents/tools/startAgentTask.js +15 -2
  17. package/ts_build/src/agents/tools/startAgentTask.js.map +1 -1
  18. package/ts_build/src/chat/modules/AgentModule.d.ts +11 -0
  19. package/ts_build/src/chat/modules/AgentModule.js +103 -0
  20. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  21. package/ts_build/src/cli.js +20 -2
  22. package/ts_build/src/cli.js.map +1 -1
  23. package/ts_build/src/clients/http.d.ts +1 -0
  24. package/ts_build/src/clients/http.js +87 -51
  25. package/ts_build/src/clients/http.js.map +1 -1
  26. package/ts_build/src/index.js +10 -0
  27. package/ts_build/src/index.js.map +1 -1
  28. package/ts_build/src/login.js +0 -1
  29. package/ts_build/src/login.js.map +1 -1
  30. package/ts_build/src/services/KnowhowClient.d.ts +8 -1
  31. package/ts_build/src/services/KnowhowClient.js +10 -0
  32. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  33. package/ts_build/src/services/McpWebsocketTransport.js +3 -1
  34. package/ts_build/src/services/McpWebsocketTransport.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -480,7 +480,7 @@ export abstract class BaseAgent implements IAgent {
480
480
  } as Message);
481
481
  }
482
482
 
483
- async call(userInput: string | MessageContent[], _messages?: Message[]) {
483
+ async call(userInput: string | MessageContent[], _messages?: Message[], retryCount = 0) {
484
484
  if (this.status === this.eventTypes.notStarted) {
485
485
  this.status = this.eventTypes.inProgress;
486
486
  }
@@ -704,7 +704,32 @@ export abstract class BaseAgent implements IAgent {
704
704
  } catch (e) {
705
705
  if (e.toString().includes("429")) {
706
706
  this.setNotHealthy();
707
- return this.call(userInput, _messages);
707
+ return this.call(userInput, _messages, retryCount);
708
+ const errorStr = e.toString();
709
+ const isNonRetriable =
710
+ errorStr.includes("401") ||
711
+ errorStr.includes("403") ||
712
+ errorStr.includes("404");
713
+
714
+ const isRetriable =
715
+ !isNonRetriable &&
716
+ (errorStr.match(/5\d\d/) ||
717
+ errorStr.includes("Failed to get models") ||
718
+ errorStr.includes("timeout") ||
719
+ errorStr.includes("ECONNRESET") ||
720
+ errorStr.includes("ETIMEDOUT") ||
721
+ errorStr.includes("Invalid response format from MCP"));
722
+
723
+ if (isRetriable && retryCount < 3) {
724
+ const delay = 1000 * Math.pow(2, retryCount);
725
+ console.warn(
726
+ `Agent request failed (attempt ${retryCount + 1}/3), retrying in ${delay}ms...`,
727
+ e.message
728
+ );
729
+ await new Promise((resolve) => setTimeout(resolve, delay));
730
+ return this.call(userInput, _messages, retryCount + 1);
731
+ }
732
+
708
733
  }
709
734
 
710
735
  console.error("Agent failed", e);
@@ -6,6 +6,8 @@ import { spawn } from "child_process";
6
6
  interface StartAgentTaskParams {
7
7
  messageId?: string;
8
8
  syncFs?: boolean;
9
+ taskId?: string;
10
+ resume?: boolean;
9
11
  prompt: string;
10
12
  provider?: string;
11
13
  model?: string;
@@ -51,6 +53,8 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
51
53
  const {
52
54
  messageId,
53
55
  prompt,
56
+ taskId: providedTaskId,
57
+ resume,
54
58
  syncFs,
55
59
  provider,
56
60
  model,
@@ -62,8 +66,8 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
62
66
  throw new Error("prompt is required to create a chat task");
63
67
  }
64
68
 
65
- // Pre-generate taskId so we can return the agents dir path to the caller
66
- const taskId = generateTaskId(prompt);
69
+ // Use provided taskId if given, otherwise generate one from the prompt
70
+ const taskId = providedTaskId ?? generateTaskId(prompt);
67
71
  const agentTaskDir = path.join(AGENTS_DIR, taskId);
68
72
 
69
73
  // Build args array (no shell escaping needed - args are passed directly)
@@ -73,7 +77,10 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
73
77
  args.push("--message-id", messageId);
74
78
  } else if (syncFs) {
75
79
  args.push("--sync-fs");
76
- // Pass the pre-generated taskId so the agent dir path is predictable
80
+ }
81
+
82
+ if (syncFs || providedTaskId) {
83
+ // Pass --task-id whenever we have a known taskId (syncFs or explicit taskId)
77
84
  args.push("--task-id", taskId);
78
85
  }
79
86
 
@@ -96,6 +103,10 @@ export async function startAgentTask(params: StartAgentTaskParams): Promise<stri
96
103
  if (maxSpendLimit !== undefined) {
97
104
  args.push("--max-spend-limit", String(maxSpendLimit));
98
105
  }
106
+ if (resume) {
107
+ // --resume is a boolean flag; task ID is already passed via --task-id above
108
+ args.push("--resume");
109
+ }
99
110
 
100
111
  const timeoutMs = maxTimeLimit ? maxTimeLimit * 60 * 1000 : 60 * 60 * 1000;
101
112
 
@@ -211,6 +222,16 @@ export const startAgentTaskDefinition: Tool = {
211
222
  type: "number",
212
223
  description: "Cost limit for agent execution in dollars. Default: 10",
213
224
  },
225
+ taskId: {
226
+ type: "string",
227
+ description:
228
+ "Pre-generated task ID to use for this agent run. When provided with syncFs, the agent directory will use this ID for a predictable path. Required when using resume.",
229
+ },
230
+ resume: {
231
+ type: "boolean",
232
+ description:
233
+ "Resume a previously started task from where it left off. Must be used together with taskId which identifies the task to resume.",
234
+ },
214
235
  },
215
236
  required: ["prompt"],
216
237
  },
@@ -8,12 +8,14 @@ import {
8
8
  TaskRegistry,
9
9
  } from "../../services/index";
10
10
  import * as fs from "fs";
11
+ import * as fsPromises from "fs/promises";
11
12
  import * as path from "path";
12
13
 
13
14
  import { BaseChatModule } from "./BaseChatModule";
14
15
  import { services } from "../../services/index";
15
16
  import { BaseAgent } from "../../agents/index";
16
17
  import { ChatCommand, ChatMode, ChatContext, ChatService } from "../types";
18
+ import { Message } from "../../clients/types";
17
19
  import { ChatInteraction } from "../../types";
18
20
  import { Marked } from "../../utils/index";
19
21
  import { TokenCompressor } from "../../processors/TokenCompressor";
@@ -28,6 +30,7 @@ import { TaskInfo, ChatSession } from "../types";
28
30
  import { agents } from "../../agents";
29
31
  import { ToolCallEvent } from "../../agents/base/base";
30
32
  import { $Command } from "@aws-sdk/client-s3";
33
+ import { KnowhowSimpleClient } from "../../services/KnowhowClient";
31
34
 
32
35
  export class AgentModule extends BaseChatModule {
33
36
  name = "agent";
@@ -668,7 +671,10 @@ Please continue from where you left off and complete the original request.
668
671
  done = true;
669
672
  output = doneMsg || "No response from the AI";
670
673
  // Remove threadUpdate listener to prevent cost sharing across tasks
671
- agent.agentEvents.removeListener(agent.eventTypes.threadUpdate, threadUpdateHandler);
674
+ agent.agentEvents.removeListener(
675
+ agent.eventTypes.threadUpdate,
676
+ threadUpdateHandler
677
+ );
672
678
  // Update task info
673
679
  taskInfo = this.taskRegistry.get(taskId);
674
680
 
@@ -732,6 +738,117 @@ Please continue from where you left off and complete the original request.
732
738
  }
733
739
  }
734
740
 
741
+ public async loadThreadsForTask(taskId: string, messageId?: string) {
742
+ const resumeTaskId: string = taskId;
743
+ const localMetadataPath = path.join(
744
+ ".knowhow",
745
+ "processes",
746
+ "agents",
747
+ resumeTaskId,
748
+ "metadata.json"
749
+ );
750
+
751
+ let threads: Message[][] = [];
752
+
753
+ // Try local FS first
754
+ if (!messageId && fs.existsSync(localMetadataPath)) {
755
+ try {
756
+ const raw = await fsPromises.readFile(localMetadataPath, "utf-8");
757
+ const metadata = JSON.parse(raw);
758
+ threads = metadata.threads || [];
759
+ console.log(`📁 Loaded threads from local FS: ${localMetadataPath}`);
760
+ } catch (e) {
761
+ console.warn(`⚠️ Failed to parse local metadata: ${e.message}`);
762
+ }
763
+ } else {
764
+ // Try remote via KnowhowSimpleClient
765
+ try {
766
+ const client = new KnowhowSimpleClient();
767
+ threads = await client.getTaskThreads(resumeTaskId);
768
+ console.log(`🌐 Loaded threads from remote for task: ${resumeTaskId}`);
769
+ } catch (e) {
770
+ console.warn(`⚠️ Could not load threads from remote: ${e.message}`);
771
+ }
772
+ }
773
+
774
+ return threads;
775
+ }
776
+
777
+ /**
778
+ * Resume an agent from a set of existing message threads
779
+ * Used by the CLI --resume flag to continue crashed/failed tasks
780
+ */
781
+ public async resumeFromMessages(options: {
782
+ agentName: string;
783
+ input: string;
784
+ threads: Message[][];
785
+ messageId?: string;
786
+ taskId?: string;
787
+ }): Promise<{ taskCompleted: Promise<string> }> {
788
+ const { agentName, input, threads, messageId, taskId } = options;
789
+
790
+ // Try to extract the original request from the first user message in threads
791
+ let originalRequest = "";
792
+ if (threads && threads.length > 0) {
793
+ const firstThread = threads[0];
794
+ if (Array.isArray(firstThread)) {
795
+ const firstUserMsg = firstThread.find(
796
+ (m: any) => m.role === "user" && m.content
797
+ );
798
+ if (firstUserMsg) {
799
+ originalRequest =
800
+ typeof firstUserMsg.content === "string"
801
+ ? firstUserMsg.content
802
+ : JSON.stringify(firstUserMsg.content);
803
+ }
804
+ }
805
+ }
806
+
807
+ // Build the resume prompt
808
+ const resumePrompt = [
809
+ "You are resuming a previously started task.",
810
+ originalRequest ? `ORIGINAL REQUEST: ${originalRequest}` : "",
811
+ "Please continue from where you left off.",
812
+ input ? input : "",
813
+ ]
814
+ .filter(Boolean)
815
+ .join("\n");
816
+
817
+ // Flatten threads into a single messages array for the agent
818
+ const lastThread =
819
+ threads && threads.length > 0 ? threads[threads.length - 1] : [];
820
+ const resumeMessages = [...lastThread];
821
+
822
+ // find last user message index
823
+ const resumeIndex = lastThread
824
+ .reverse()
825
+ .findIndex((e) => e.role === "user" && typeof e.content === "string");
826
+
827
+ if (resumeIndex === -1) {
828
+ resumeMessages.push({
829
+ role: "user",
830
+ content: resumePrompt,
831
+ });
832
+ } else {
833
+ const actualIndex = lastThread.length - 1 - resumeIndex;
834
+ const lastUserMessage = resumeMessages[actualIndex];
835
+ lastUserMessage.content += `\n\n<Workflow>[RESUME CONTEXT]: ${resumePrompt}</Workflow>`;
836
+ }
837
+
838
+ const result = await this.setupAgent({
839
+ agentName,
840
+ input: resumePrompt,
841
+ messageId,
842
+ existingKnowhowTaskId: taskId,
843
+ run: false,
844
+ });
845
+
846
+ // Start agent with prior messages as context
847
+ result.agent.call(resumePrompt, resumeMessages);
848
+
849
+ return { taskCompleted: result.taskCompleted };
850
+ }
851
+
735
852
  /**
736
853
  * Get list of active agent tasks
737
854
  */
package/src/cli.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node --no-node-snapshot
2
2
  import "source-map-support/register";
3
3
  import * as fs from "fs";
4
+ import * as fsPromises from "fs/promises";
4
5
  import * as path from "path";
5
6
  import * as os from "os";
6
7
  import { Command } from "commander";
@@ -39,7 +40,6 @@ import { SetupModule } from "./chat/modules/SetupModule";
39
40
  import { CliChatService } from "./chat/CliChatService";
40
41
 
41
42
  async function setupServices() {
42
-
43
43
  const { Agents, Mcp, Clients, Tools: OldTools } = services();
44
44
  const Tools = new LazyToolsService();
45
45
 
@@ -206,12 +206,42 @@ async function main() {
206
206
  )
207
207
  .option("--message-id <messageId>", "Knowhow message ID for task tracking")
208
208
  .option("--sync-fs", "Enable filesystem-based synchronization")
209
- .option("--task-id <taskId>", "Pre-generated task ID (used with --sync-fs for predictable agent directory path)")
209
+ .option(
210
+ "--task-id <taskId>",
211
+ "Pre-generated task ID (used with --sync-fs for predictable agent directory path)"
212
+ )
210
213
  .option("--prompt-file <path>", "Custom prompt template file with {text}")
211
214
  .option("--input <text>", "Task input (fallback to stdin if not provided)")
215
+ .option(
216
+ "--resume",
217
+ "Resume a previously started task using the --task-id (local FS or remote)"
218
+ )
212
219
  .action(async (options) => {
213
220
  try {
214
221
  await setupServices();
222
+ const agentModule = new AgentModule();
223
+
224
+ // Handle --resume flag: load threads from local FS or remote using --task-id
225
+ if (options.resume) {
226
+ const threads = await agentModule.loadThreadsForTask(
227
+ options.taskId,
228
+ options.messageId
229
+ );
230
+ const resumeInput =
231
+ options.input || "Please continue from where you left off.";
232
+
233
+ await agentModule.initialize(chatService);
234
+ const { taskCompleted } = await agentModule.resumeFromMessages({
235
+ agentName: options.agentName || "Patcher",
236
+ input: resumeInput,
237
+ threads,
238
+ messageId: options.messageId,
239
+ taskId: options.taskId,
240
+ });
241
+ await taskCompleted;
242
+ return;
243
+ }
244
+
215
245
  let input = options.input;
216
246
 
217
247
  // Only read from stdin if we don't have input and don't have a standalone prompt file
@@ -230,7 +260,6 @@ async function main() {
230
260
  process.exit(1);
231
261
  }
232
262
 
233
- const agentModule = new AgentModule();
234
263
  await agentModule.initialize(chatService);
235
264
  const { taskCompleted } = await agentModule.setupAgent({
236
265
  ...options,
@@ -372,7 +401,9 @@ async function main() {
372
401
 
373
402
  program
374
403
  .command("files")
375
- .description("Sync files between local filesystem and Knowhow FS (uses fileMounts config)")
404
+ .description(
405
+ "Sync files between local filesystem and Knowhow FS (uses fileMounts config)"
406
+ )
376
407
  .option("--upload", "Force upload direction for all mounts")
377
408
  .option("--download", "Force download direction for all mounts")
378
409
  .option("--config <path>", "Path to knowhow.json", "./knowhow.json")
@@ -434,7 +465,10 @@ async function main() {
434
465
  .description(
435
466
  "Git credential helper for GitHub. Use as: git config credential.helper 'knowhow github-credentials'"
436
467
  )
437
- .option("--repo <repo>", "Repository in owner/repo format (e.g. myorg/myrepo)")
468
+ .option(
469
+ "--repo <repo>",
470
+ "Repository in owner/repo format (e.g. myorg/myrepo)"
471
+ )
438
472
  .action(async (action: string | undefined, options: { repo?: string }) => {
439
473
  const client = new KnowhowSimpleClient();
440
474
 
@@ -447,7 +481,10 @@ async function main() {
447
481
  // Read from stdin (git sends protocol/host/username)
448
482
  const lines: string[] = [];
449
483
  const readline = await import("readline");
450
- const rl = readline.createInterface({ input: process.stdin, terminal: false });
484
+ const rl = readline.createInterface({
485
+ input: process.stdin,
486
+ terminal: false,
487
+ });
451
488
  await new Promise<void>((resolve) => {
452
489
  rl.on("line", (line) => {
453
490
  if (line.trim()) lines.push(line.trim());
@@ -12,6 +12,41 @@ import path from "path";
12
12
  export class HttpClient implements GenericClient {
13
13
  constructor(private baseUrl: string, private headers = {}) {}
14
14
 
15
+ private async withRetry<T>(fn: () => Promise<T>, retries = 3): Promise<T> {
16
+ let lastError: any;
17
+ for (let attempt = 0; attempt <= retries; attempt++) {
18
+ try {
19
+ return await fn();
20
+ } catch (e: any) {
21
+ lastError = e;
22
+ const errorStr = e.toString();
23
+ const isNonRetriable =
24
+ errorStr.includes("401") ||
25
+ errorStr.includes("403") ||
26
+ errorStr.includes("404") ||
27
+ errorStr.includes("429");
28
+ const isRetriable =
29
+ !isNonRetriable &&
30
+ (errorStr.match(/5\d\d/) ||
31
+ errorStr.includes("timeout") ||
32
+ errorStr.includes("ECONNRESET") ||
33
+ errorStr.includes("ETIMEDOUT") ||
34
+ errorStr.includes("Invalid response format from MCP") ||
35
+ errorStr.includes("Failed to get models"));
36
+ if (!isRetriable || attempt >= retries) {
37
+ throw e;
38
+ }
39
+ const delay = 1000 * Math.pow(2, attempt);
40
+ console.warn(
41
+ `HTTP request failed (attempt ${attempt + 1}/${retries}), retrying in ${delay}ms...`,
42
+ e.message
43
+ );
44
+ await new Promise((resolve) => setTimeout(resolve, delay));
45
+ }
46
+ }
47
+ throw lastError;
48
+ }
49
+
15
50
  setJwt(jwt: string) {
16
51
  this.headers = {
17
52
  ...this.headers,
@@ -43,85 +78,91 @@ export class HttpClient implements GenericClient {
43
78
  async createChatCompletion(
44
79
  options: CompletionOptions
45
80
  ): Promise<CompletionResponse> {
46
- const body = {
47
- ...options,
48
- model: options.model,
49
- messages: options.messages,
50
- max_tokens: options.max_tokens || 3000,
51
-
52
- ...(options.tools && {
53
- tools: options.tools,
54
- tool_choice: "auto",
55
- }),
56
- };
57
-
58
- const response = await axios.post(
59
- `${this.baseUrl}/v1/chat/completions`,
60
- body,
61
- {
62
- headers: this.headers,
81
+ return this.withRetry(async () => {
82
+ const body = {
83
+ ...options,
84
+ model: options.model,
85
+ messages: options.messages,
86
+ max_tokens: options.max_tokens || 3000,
87
+
88
+ ...(options.tools && {
89
+ tools: options.tools,
90
+ tool_choice: "auto",
91
+ }),
92
+ };
93
+
94
+ const response = await axios.post(
95
+ `${this.baseUrl}/v1/chat/completions`,
96
+ body,
97
+ {
98
+ headers: this.headers,
99
+ }
100
+ );
101
+
102
+ const data = response.data;
103
+
104
+ // Since this uses a keepalive, we need to detect 200 with error in body
105
+ if (data.error) {
106
+ throw new Error(JSON.stringify(data.error, null, 2));
63
107
  }
64
- );
65
-
66
- const data = response.data;
67
-
68
- // Since this uses a keepalive, we need to detect 200 with error in body
69
- if (data.error) {
70
- throw new Error(JSON.stringify(data.error, null, 2));
71
- }
72
108
 
73
- return {
74
- choices: data.choices.map((choice: any) => ({
75
- message: {
76
- role: choice.message.role,
77
- content: choice.message.content,
78
- tool_calls: choice.message.tool_calls,
79
- },
80
- })),
81
- model: data.model,
82
- usage: data.usage,
83
- usd_cost: data.usd_cost,
84
- };
109
+ return {
110
+ choices: data.choices.map((choice: any) => ({
111
+ message: {
112
+ role: choice.message.role,
113
+ content: choice.message.content,
114
+ tool_calls: choice.message.tool_calls,
115
+ },
116
+ })),
117
+ model: data.model,
118
+ usage: data.usage,
119
+ usd_cost: data.usd_cost,
120
+ };
121
+ });
85
122
  }
86
123
 
87
124
  async createEmbedding(options: EmbeddingOptions): Promise<EmbeddingResponse> {
88
- const response = await axios.post(
89
- `${this.baseUrl}/v1/embeddings`,
90
- {
91
- model: options.model,
92
- input: options.input,
93
- },
94
- {
95
- headers: this.headers,
96
- }
97
- );
125
+ return this.withRetry(async () => {
126
+ const response = await axios.post(
127
+ `${this.baseUrl}/v1/embeddings`,
128
+ {
129
+ model: options.model,
130
+ input: options.input,
131
+ },
132
+ {
133
+ headers: this.headers,
134
+ }
135
+ );
98
136
 
99
- const data = response.data;
137
+ const data = response.data;
100
138
 
101
- // Since this uses a keepalive, we need to detect 200 with error in body
102
- if (data.error) {
103
- throw new Error(JSON.stringify(data.error, null, 2));
104
- }
139
+ // Since this uses a keepalive, we need to detect 200 with error in body
140
+ if (data.error) {
141
+ throw new Error(JSON.stringify(data.error, null, 2));
142
+ }
105
143
 
106
- return {
107
- data: data.data,
108
- model: options.model,
109
- usage: data.usage,
110
- usd_cost: data.usd_cost,
111
- };
144
+ return {
145
+ data: data.data,
146
+ model: options.model,
147
+ usage: data.usage,
148
+ usd_cost: data.usd_cost,
149
+ };
150
+ });
112
151
  }
113
152
 
114
153
  async getModels() {
115
- const response = await axios.get(`${this.baseUrl}/v1/models`, {
116
- headers: this.headers,
117
- });
154
+ return this.withRetry(async () => {
155
+ const response = await axios.get(`${this.baseUrl}/v1/models`, {
156
+ headers: this.headers,
157
+ });
118
158
 
119
- const data = response.data?.data;
159
+ const data = response.data?.data;
120
160
 
121
- return data.map((model: any) => ({
122
- id: model.id,
123
- object: model.object,
124
- owned_by: model.owned_by,
125
- }));
161
+ return data.map((model: any) => ({
162
+ id: model.id,
163
+ object: model.object,
164
+ owned_by: model.owned_by,
165
+ }));
166
+ });
126
167
  }
127
168
  }
package/src/index.ts CHANGED
@@ -135,6 +135,14 @@ export async function upload() {
135
135
  const url = await knowhowApiClient.getPresignedUploadUrl(source);
136
136
  console.log("Uploading to", url);
137
137
  await AwsS3.uploadToPresignedUrl(url, source.output);
138
+ // Sync config metadata back to the backend DB
139
+ await knowhowApiClient.updateEmbeddingMetadata(source.remoteId, {
140
+ inputGlob: source.input,
141
+ outputPath: source.output,
142
+ chunkSize: source.chunkSize,
143
+ remoteType: source.remoteType,
144
+ });
145
+ console.log("Synced metadata for", source.remoteId);
138
146
  } else {
139
147
  console.log(
140
148
  "Skipping upload to",
@@ -376,6 +384,9 @@ export async function download() {
376
384
  const preSignedUrl = await knowhowApiClient.getPresignedDownloadUrl(
377
385
  source
378
386
  );
387
+ // Ensure output directory exists
388
+ const outputDir = path.dirname(destinationPath);
389
+ if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
379
390
  await AwsS3.downloadFromPresignedUrl(preSignedUrl, destinationPath);
380
391
  } else {
381
392
  console.log("Unsupported remote type for", source.output);
package/src/login.ts CHANGED
@@ -111,9 +111,5 @@ export async function checkJwt(storedJwt: string) {
111
111
  return org.organizationId === orgId;
112
112
  });
113
113
 
114
- console.log(
115
- `Current user: ${user.email}, \nOrganization: ${currentOrg?.organization?.name} - ${orgId}`
116
- );
117
-
118
114
  return { user, currentOrg };
119
115
  }
@@ -53,7 +53,7 @@ export interface TaskDetailsResponse {
53
53
  inProgress: boolean;
54
54
  status: "running" | "paused" | "killed" | "completed";
55
55
  totalUsdCost: number;
56
- threads: any;
56
+ threads: any[][];
57
57
  createdAt: string;
58
58
  updatedAt: string;
59
59
  messageId?: string;
@@ -192,6 +192,25 @@ export class KnowhowSimpleClient {
192
192
  return presignedUrl;
193
193
  }
194
194
 
195
+ async updateEmbeddingMetadata(
196
+ id: string,
197
+ data: {
198
+ inputGlob?: string;
199
+ outputPath?: string;
200
+ chunkSize?: number;
201
+ remoteType?: string;
202
+ }
203
+ ) {
204
+ await this.checkJwt();
205
+ return axios.put(
206
+ `${this.baseUrl}/api/org-embeddings/${id}`,
207
+ data,
208
+ {
209
+ headers: this.headers,
210
+ }
211
+ );
212
+ }
213
+
195
214
  async createChatCompletion(options: CompletionOptions) {
196
215
  await this.checkJwt();
197
216
  return axios.post<CompletionResponse>(
@@ -431,6 +450,14 @@ export class KnowhowSimpleClient {
431
450
  );
432
451
  }
433
452
 
453
+ /**
454
+ * Get threads from a task by task ID
455
+ */
456
+ async getTaskThreads(taskId: string): Promise<any[][]> {
457
+ const response = await this.getTaskDetails(taskId);
458
+ return response.data.threads || [];
459
+ }
460
+
434
461
  /**
435
462
  * Kill/cancel a running or paused agent task
436
463
  */
@@ -29,7 +29,13 @@ export class MCPWebSocketTransport implements Transport {
29
29
  }
30
30
  console.log("MCPW Message received", JSON.stringify(parsed));
31
31
  const message = JSONRPCMessageSchema.parse(parsed);
32
- this.onmessage?.(message);
32
+ // Process message asynchronously to avoid blocking the WebSocket
33
+ // event loop while a long-running tool call is in progress.
34
+ // Without this, all subsequent messages are queued until the
35
+ // current tool call completes.
36
+ setImmediate(() => {
37
+ this.onmessage?.(message);
38
+ });
33
39
  } catch (error) {
34
40
  this.onerror?.(error as Error);
35
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.80",
3
+ "version": "0.0.82",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {