@tyvm/knowhow 0.0.104 → 0.0.105

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 (51) hide show
  1. package/package.json +1 -1
  2. package/src/clients/index.ts +100 -6
  3. package/src/clients/pricing/index.ts +2 -2
  4. package/src/clients/pricing/{catalog.ts → models.ts} +5 -40
  5. package/src/clients/pricing/types.ts +29 -0
  6. package/src/clients/pricing/xai.ts +7 -6
  7. package/src/clients/xai.ts +2 -2
  8. package/src/services/SyncedAgentWatcher.ts +13 -298
  9. package/src/services/index.ts +1 -0
  10. package/src/services/watchers/FsSyncer.ts +155 -0
  11. package/src/services/watchers/RemoteSyncer.ts +153 -0
  12. package/src/services/watchers/index.ts +2 -0
  13. package/src/types.ts +2 -2
  14. package/ts_build/package.json +1 -1
  15. package/ts_build/src/clients/index.d.ts +8 -0
  16. package/ts_build/src/clients/index.js +57 -2
  17. package/ts_build/src/clients/index.js.map +1 -1
  18. package/ts_build/src/clients/pricing/index.d.ts +2 -2
  19. package/ts_build/src/clients/pricing/index.js +7 -7
  20. package/ts_build/src/clients/pricing/index.js.map +1 -1
  21. package/ts_build/src/clients/pricing/models.d.ts +8 -0
  22. package/ts_build/src/clients/pricing/{catalog.js → models.js} +4 -10
  23. package/ts_build/src/clients/pricing/models.js.map +1 -0
  24. package/ts_build/src/clients/pricing/{catalog.d.ts → types.d.ts} +2 -9
  25. package/ts_build/src/clients/pricing/types.js +3 -0
  26. package/ts_build/src/clients/pricing/types.js.map +1 -0
  27. package/ts_build/src/clients/pricing/xai.d.ts +3 -8
  28. package/ts_build/src/clients/pricing/xai.js +4 -4
  29. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  30. package/ts_build/src/clients/xai.d.ts +9 -5
  31. package/ts_build/src/clients/xai.js +2 -2
  32. package/ts_build/src/clients/xai.js.map +1 -1
  33. package/ts_build/src/services/SyncedAgentWatcher.d.ts +0 -51
  34. package/ts_build/src/services/SyncedAgentWatcher.js +1 -282
  35. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  36. package/ts_build/src/services/index.d.ts +1 -0
  37. package/ts_build/src/services/index.js +1 -0
  38. package/ts_build/src/services/index.js.map +1 -1
  39. package/ts_build/src/services/watchers/FsSyncer.d.ts +27 -0
  40. package/ts_build/src/services/watchers/FsSyncer.js +135 -0
  41. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -0
  42. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +28 -0
  43. package/ts_build/src/services/watchers/RemoteSyncer.js +126 -0
  44. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -0
  45. package/ts_build/src/services/watchers/index.d.ts +2 -0
  46. package/ts_build/src/services/watchers/index.js +19 -0
  47. package/ts_build/src/services/watchers/index.js.map +1 -0
  48. package/ts_build/src/types.d.ts +2 -2
  49. package/ts_build/src/types.js +2 -2
  50. package/ts_build/src/types.js.map +1 -1
  51. package/ts_build/src/clients/pricing/catalog.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.104",
3
+ "version": "0.0.105",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -30,6 +30,32 @@ import { ModelProvider } from "../types";
30
30
  import { getConfig } from "../config";
31
31
  import { loadKnowhowJwt, KNOWHOW_API_URL } from "../services/KnowhowClient";
32
32
  import { ContextLimits } from "./contextLimits";
33
+ import { OpenAiTextPricing } from "./pricing/openai";
34
+ import { AnthropicTextPricing } from "./pricing/anthropic";
35
+ import { GeminiPricing } from "./pricing/google";
36
+ import {
37
+ XaiTextPricing,
38
+ XaiImagePricing,
39
+ XaiVideoPricing,
40
+ } from "./pricing/xai";
41
+ import type {
42
+ ModelPricing,
43
+ ModelType,
44
+ ModelCatalogEntry,
45
+ } from "./pricing/types";
46
+ export {
47
+ OpenAiTextPricing,
48
+ AnthropicTextPricing,
49
+ GeminiPricing,
50
+ XaiTextPricing,
51
+ XaiImagePricing,
52
+ XaiVideoPricing,
53
+ };
54
+ export type {
55
+ ModelPricing,
56
+ ModelType,
57
+ ModelCatalogEntry,
58
+ } from "./pricing/types";
33
59
 
34
60
  // ---------------------------------------------------------------------------
35
61
  // Built-in provider registry
@@ -219,7 +245,9 @@ export class AIClient {
219
245
 
220
246
  if (!client) {
221
247
  if (entry.provider === "knowhow") {
222
- console.warn(`⚠️ Knowhow provider is not logged in. Run 'knowhow login' to enable Knowhow models.`);
248
+ console.warn(
249
+ `⚠️ Knowhow provider is not logged in. Run 'knowhow login' to enable Knowhow models.`
250
+ );
223
251
  }
224
252
  continue;
225
253
  }
@@ -499,17 +527,22 @@ export class AIClient {
499
527
  }
500
528
 
501
529
  const allModels = this.listAllModels();
502
- const hasKnowhowModels =
503
- allModels["knowhow"] && allModels["knowhow"].length > 0;
530
+ const hasKnowhowModels = allModels.knowhow && allModels.knowhow.length > 0;
504
531
  const knowhowIsConfigured = Object.keys(allModels).includes("knowhow");
505
532
 
506
- console.warn(`⚠️ Unable to find model '${model}' for provider '${provider}'.`);
507
- console.warn(` Available providers: ${Object.keys(allModels).join(", ") || "(none)"}`);
533
+ console.warn(
534
+ `⚠️ Unable to find model '${model}' for provider '${provider}'.`
535
+ );
536
+ console.warn(
537
+ ` Available providers: ${Object.keys(allModels).join(", ") || "(none)"}`
538
+ );
508
539
 
509
540
  if (!hasKnowhowModels && !knowhowIsConfigured) {
510
541
  console.warn(` Tip: Run 'knowhow login' to enable Knowhow models.`);
511
542
  } else if (!hasKnowhowModels) {
512
- console.warn(` Tip: The Knowhow provider returned no models. Try running 'knowhow login' to re-authenticate.`);
543
+ console.warn(
544
+ ` Tip: The Knowhow provider returned no models. Try running 'knowhow login' to re-authenticate.`
545
+ );
513
546
  }
514
547
 
515
548
  return { provider, model };
@@ -763,6 +796,67 @@ export class AIClient {
763
796
  if (contextLimit === undefined) return undefined;
764
797
  return { contextLimit, threshold: contextLimit };
765
798
  }
799
+
800
+ /**
801
+ * Returns pricing information for all known models, derived from the
802
+ * provider pricing maps.
803
+ *
804
+ * @param modelId Optional model id filter (without provider prefix).
805
+ * If omitted, all models across all providers are returned.
806
+ */
807
+ getPrices(modelId?: string): ModelCatalogEntry[] {
808
+ const results: ModelCatalogEntry[] = [];
809
+
810
+ const addModels = (
811
+ models: Record<string, string[]>,
812
+ type: ModelType,
813
+ pricingMap: Record<string, ModelPricing>
814
+ ) => {
815
+ for (const [provider, ids] of Object.entries(models)) {
816
+ for (const id of ids) {
817
+ if (modelId && id !== modelId) continue;
818
+ if (!pricingMap[id]) continue;
819
+
820
+ const p = pricingMap[id];
821
+ results.push({
822
+ id,
823
+ provider,
824
+ type,
825
+ displayName: id,
826
+ pricing: p,
827
+ });
828
+ }
829
+ }
830
+ };
831
+
832
+ // Build a combined pricing map across all providers
833
+ const allTextPricing: Record<string, ModelPricing> = {
834
+ ...OpenAiTextPricing,
835
+ ...AnthropicTextPricing,
836
+ ...GeminiPricing,
837
+ ...XaiTextPricing,
838
+ };
839
+ const allImagePricing: Record<string, ModelPricing> = {
840
+ ...XaiImagePricing,
841
+ };
842
+ const allVideoPricing: Record<string, ModelPricing> = {
843
+ ...XaiVideoPricing,
844
+ };
845
+
846
+ addModels(this.completionModels, "completion", allTextPricing);
847
+ addModels(this.embeddingModels, "embedding", allTextPricing);
848
+ addModels(this.imageModels, "image", {
849
+ ...allTextPricing,
850
+ ...allImagePricing,
851
+ });
852
+ addModels(this.audioModels, "audio", allTextPricing);
853
+ addModels(this.videoModels, "video", {
854
+ ...allTextPricing,
855
+ ...allVideoPricing,
856
+ });
857
+
858
+ return results;
859
+ }
766
860
  }
767
861
 
768
862
  export const Clients = new AIClient();
@@ -2,5 +2,5 @@ export { OpenAiTextPricing } from "./openai";
2
2
  export { GeminiTextPricing } from "./google";
3
3
  export { AnthropicTextPricing } from "./anthropic";
4
4
  export { XaiTextPricing, XaiImagePricing, XaiVideoPricing } from "./xai";
5
- export { ALL_MODEL_CATALOG, OPENAI_MODEL_CATALOG, ANTHROPIC_MODEL_CATALOG, GOOGLE_MODEL_CATALOG, XAI_MODEL_CATALOG, USAGE_MARKUP_PERCENT } from "./catalog";
6
- export type { ModelCatalogEntry, ModelType, ModelPricing as CatalogModelPricing } from "./catalog";
5
+ export { ALL_MODEL_CATALOG, OPENAI_MODEL_CATALOG, ANTHROPIC_MODEL_CATALOG, GOOGLE_MODEL_CATALOG, XAI_MODEL_CATALOG, USAGE_MARKUP_PERCENT } from "./models";
6
+ export type { ModelCatalogEntry, ModelType, ModelPricing as CatalogModelPricing } from "./types";
@@ -12,38 +12,9 @@ import { OpenAiTextPricing } from "./openai";
12
12
  import { AnthropicTextPricing } from "./anthropic";
13
13
  import { GeminiPricing } from "./google";
14
14
  import { XaiTextPricing, XaiImagePricing, XaiVideoPricing } from "./xai";
15
+ import { ModelPricing, ModelType, ModelCatalogEntry } from "./types";
15
16
 
16
- export type ModelType =
17
- | "completion"
18
- | "embedding"
19
- | "image"
20
- | "audio"
21
- | "video"
22
- | "transaction";
23
-
24
- export interface ModelPricing {
25
- input: number;
26
- output: number;
27
- cached_input?: number;
28
- cache_write?: number;
29
- cache_hit?: number;
30
- input_audio?: number;
31
- output_audio?: number;
32
- input_gt_200k?: number;
33
- output_gt_200k?: number;
34
- image_generation?: number;
35
- video_generation?: number;
36
- }
37
-
38
- export interface ModelCatalogEntry {
39
- id: string;
40
- provider: string;
41
- type: ModelType;
42
- displayName: string;
43
- pricing: ModelPricing;
44
- /** Markup applied on top of base pricing (as a fraction, e.g. 0.025 = 2.5%) */
45
- markupPercent: number;
46
- }
17
+ export { ModelPricing, ModelType, ModelCatalogEntry };
47
18
 
48
19
  // ─── Platform markup ──────────────────────────────────────────────────────────
49
20
 
@@ -69,7 +40,6 @@ function completion(
69
40
  provider,
70
41
  type: "completion",
71
42
  displayName,
72
- markupPercent: USAGE_MARKUP_PERCENT,
73
43
  pricing: {
74
44
  input: 0,
75
45
  output: 0,
@@ -90,7 +60,6 @@ function embedding(
90
60
  provider,
91
61
  type: "embedding",
92
62
  displayName,
93
- markupPercent: USAGE_MARKUP_PERCENT,
94
63
  pricing: { input, output: 0 },
95
64
  };
96
65
  }
@@ -106,7 +75,6 @@ function image(
106
75
  provider,
107
76
  type: "image",
108
77
  displayName,
109
- markupPercent: USAGE_MARKUP_PERCENT,
110
78
  pricing: { input: 0, output: 0, ...pricing },
111
79
  };
112
80
  }
@@ -122,7 +90,6 @@ function video(
122
90
  provider,
123
91
  type: "video",
124
92
  displayName,
125
- markupPercent: USAGE_MARKUP_PERCENT,
126
93
  pricing: { input: 0, output: 0, video_generation },
127
94
  };
128
95
  }
@@ -138,7 +105,6 @@ function audio(
138
105
  provider,
139
106
  type: "audio",
140
107
  displayName,
141
- markupPercent: USAGE_MARKUP_PERCENT,
142
108
  pricing: { input: 0, output: 0, ...pricing },
143
109
  };
144
110
  }
@@ -154,7 +120,6 @@ function transaction(
154
120
  provider,
155
121
  type: "transaction",
156
122
  displayName,
157
- markupPercent: USAGE_MARKUP_PERCENT,
158
123
  pricing: { input: 0, output: 0, ...pricing },
159
124
  };
160
125
  }
@@ -271,10 +236,10 @@ export const XAI_MODEL_CATALOG: ModelCatalogEntry[] = [
271
236
  completion(Models.xai.Grok3FastBeta, "xai", "Grok 3 Fast Beta"),
272
237
  completion(Models.xai.Grok21212, "xai", "Grok 2"),
273
238
  // Image generation
274
- image(Models.xai.GrokImagineImage, "xai", "Grok Imagine Image", { image_generation: XaiImagePricing["grok-imagine-image"] ?? 0.02 }),
275
- image("grok-2-image-1212", "xai", "Grok 2 Image", { image_generation: XaiImagePricing["grok-2-image-1212"] ?? 0.07 }),
239
+ image(Models.xai.GrokImagineImage, "xai", "Grok Imagine Image", { image_generation: XaiImagePricing["grok-imagine-image"]?.image_generation ?? 0.02 }),
240
+ image("grok-2-image-1212", "xai", "Grok 2 Image", { image_generation: XaiImagePricing["grok-2-image-1212"]?.image_generation ?? 0.07 }),
276
241
  // Video generation
277
- video(Models.xai.GrokImagineVideo, "xai", "Grok Imagine Video", XaiVideoPricing["grok-imagine-video"] ?? 0.05),
242
+ video(Models.xai.GrokImagineVideo, "xai", "Grok Imagine Video", XaiVideoPricing["grok-imagine-video"]?.video_generation ?? 0.05),
278
243
  ];
279
244
 
280
245
  // ─── Combined catalog ─────────────────────────────────────────────────────────
@@ -0,0 +1,29 @@
1
+ export type ModelType =
2
+ | "completion"
3
+ | "embedding"
4
+ | "image"
5
+ | "audio"
6
+ | "video"
7
+ | "transaction";
8
+
9
+ export interface ModelPricing {
10
+ input?: number;
11
+ output?: number;
12
+ cached_input?: number;
13
+ cache_write?: number;
14
+ cache_hit?: number;
15
+ input_audio?: number;
16
+ output_audio?: number;
17
+ input_gt_200k?: number;
18
+ output_gt_200k?: number;
19
+ image_generation?: number;
20
+ video_generation?: number;
21
+ }
22
+
23
+ export interface ModelCatalogEntry {
24
+ id: string;
25
+ provider: string;
26
+ type: ModelType;
27
+ displayName: string;
28
+ pricing: ModelPricing;
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { Models } from "../../types";
2
+ import { ModelPricing } from "./types";
2
3
 
3
4
  export const XaiTextPricing = {
4
5
 
@@ -60,14 +61,14 @@ export const XaiTextPricing = {
60
61
 
61
62
  // Image generation pricing: per image
62
63
  // Based on https://docs.x.ai/developers/models
63
- export const XaiImagePricing = {
64
- "grok-imagine-image-pro": 0.07,
65
- "grok-imagine-image": 0.02,
66
- "grok-2-image-1212": 0.07,
64
+ export const XaiImagePricing: Record<string, ModelPricing> = {
65
+ "grok-imagine-image-pro": { image_generation: 0.07 },
66
+ "grok-imagine-image": { image_generation: 0.02 },
67
+ "grok-2-image-1212": { image_generation: 0.07 },
67
68
  };
68
69
 
69
70
  // Video generation pricing: $0.05 per second
70
71
  // Based on https://docs.x.ai/developers/models
71
- export const XaiVideoPricing = {
72
- "grok-imagine-video": 0.05, // per second
72
+ export const XaiVideoPricing: Record<string, ModelPricing> = {
73
+ "grok-imagine-video": { video_generation: 0.05 }, // per second
73
74
  };
@@ -175,7 +175,7 @@ export class GenericXAIClient implements GenericClient {
175
175
  // Calculate cost based on model name
176
176
  const imageModel = options.model || "grok-imagine-image";
177
177
  const costPerImage =
178
- XaiImagePricing[imageModel as keyof typeof XaiImagePricing] || 0.02;
178
+ XaiImagePricing[imageModel as keyof typeof XaiImagePricing]?.image_generation || 0.02;
179
179
  const usdCost = (options.n || 1) * costPerImage;
180
180
 
181
181
  return {
@@ -250,7 +250,7 @@ export class GenericXAIClient implements GenericClient {
250
250
  // Return immediately with the jobId – do NOT poll here.
251
251
  // Use getVideoStatus() to poll and downloadVideo() to fetch the result.
252
252
  const duration = options.duration || 5;
253
- const pricePerSecond = XaiVideoPricing[model] || 0.07;
253
+ const pricePerSecond = XaiVideoPricing[model]?.video_generation || 0.07;
254
254
  const usdCost = duration * pricePerSecond;
255
255
 
256
256
  return {
@@ -5,11 +5,6 @@
5
5
 
6
6
  import { Message } from "../clients/types";
7
7
  import { EventService } from "./EventService";
8
- import * as fs from "fs";
9
- import * as fsPromises from "fs/promises";
10
- import * as path from "path";
11
- import { messagesToRenderEvents } from "../chat/renderer/messagesToRenderEvents";
12
- import { KnowhowSimpleClient } from "./KnowhowClient";
13
8
 
14
9
  export interface SyncedAgentWatcher {
15
10
  /** Start watching for changes, emitting agent events */
@@ -27,7 +22,13 @@ export interface SyncedAgentWatcher {
27
22
  /** EventService that emits agent lifecycle events (toolCall, toolUsed, agentSay, threadUpdate, done) */
28
23
  agentEvents: EventService;
29
24
  /** Event type constants mirroring BaseAgent.eventTypes */
30
- eventTypes: { done: string; toolCall: string; toolUsed: string; agentSay: string; threadUpdate: string };
25
+ eventTypes: {
26
+ done: string;
27
+ toolCall: string;
28
+ toolUsed: string;
29
+ agentSay: string;
30
+ threadUpdate: string;
31
+ };
31
32
  /** Pause the remote agent */
32
33
  pause(): Promise<void>;
33
34
  /** Unpause/resume the remote agent */
@@ -44,7 +45,12 @@ export interface SyncedAgentWatcher {
44
45
  export interface AttachableAgent {
45
46
  name: string;
46
47
  agentEvents: EventService;
47
- eventTypes: { done: string; toolCall?: string; toolUsed?: string; agentSay?: string };
48
+ eventTypes: {
49
+ done: string;
50
+ toolCall?: string;
51
+ toolUsed?: string;
52
+ agentSay?: string;
53
+ };
48
54
  getTotalCostUsd(): number;
49
55
  pause(): void | Promise<void>;
50
56
  unpause(): void | Promise<void>;
@@ -104,294 +110,3 @@ export class WatcherBackedAgent implements AttachableAgent {
104
110
  });
105
111
  }
106
112
  }
107
-
108
- /**
109
- * Watches an agent running in another process via the filesystem.
110
- * Reads .knowhow/processes/agents/<taskId>/metadata.json for changes.
111
- * Sends messages by writing to .knowhow/processes/agents/<taskId>/input.txt
112
- */
113
- export class FsSyncedAgentWatcher implements SyncedAgentWatcher {
114
- public taskId: string = "";
115
- private taskPath: string = "";
116
- private watcher: fs.FSWatcher | null = null;
117
- private lastThreadLength: number = 0;
118
- public agentName: string = "unknown";
119
- private debounceTimer: NodeJS.Timeout | null = null;
120
- public agentEvents = new EventService();
121
- public eventTypes = {
122
- done: "done",
123
- toolCall: "tool:pre_call",
124
- toolUsed: "tool:post_call",
125
- agentSay: "agent:say",
126
- threadUpdate: "thread_update",
127
- };
128
-
129
- async startWatching(taskId: string): Promise<void> {
130
- this.taskId = taskId;
131
- this.taskPath = path.join(".knowhow/processes/agents", taskId);
132
-
133
- // Load initial state to track current thread length (for delta rendering)
134
- const metadata = await this.readMetadata();
135
- if (metadata) {
136
- const threads: any[][] = metadata.threads || [];
137
- const lastThread = threads[threads.length - 1] || [];
138
- this.agentName = metadata.agentName || taskId;
139
- this.lastThreadLength = lastThread.length;
140
- }
141
-
142
- // Watch the directory for metadata.json changes
143
- try {
144
- this.watcher = fs.watch(this.taskPath, (event, filename) => {
145
- if (filename === "metadata.json" || filename === null) {
146
- // Debounce rapid file writes
147
- if (this.debounceTimer) clearTimeout(this.debounceTimer);
148
- this.debounceTimer = setTimeout(() => {
149
- this.onMetadataChanged().catch(() => {});
150
- }, 200);
151
- }
152
- });
153
- } catch (err: any) {
154
- console.warn(`⚠️ Could not watch ${this.taskPath}: ${err.message}`);
155
- }
156
-
157
- console.log(`👁️ Watching fs-synced agent: ${taskId} (${this.agentName})`);
158
- console.log(
159
- ` Type /logs 20 to see recent messages, or type to send a message`
160
- );
161
- }
162
-
163
- private async onMetadataChanged(): Promise<void> {
164
- const metadata = await this.readMetadata();
165
- if (!metadata?.threads) return;
166
-
167
- const threads: any[][] = metadata.threads;
168
- const lastThread = threads[threads.length - 1] || [];
169
-
170
- // Only render NEW messages since last check
171
- const newMessages = lastThread.slice(this.lastThreadLength);
172
- if (newMessages.length > 0) {
173
- const renderEvents = messagesToRenderEvents(
174
- newMessages,
175
- this.taskId,
176
- this.agentName
177
- );
178
- for (const event of renderEvents) {
179
- if (event.type === "toolCall") {
180
- this.agentEvents.emit(this.eventTypes.toolCall, {
181
- toolCall: (event as any).toolCall,
182
- });
183
- } else if (event.type === "toolResult") {
184
- this.agentEvents.emit(this.eventTypes.toolUsed, {
185
- toolCall: (event as any).toolCall,
186
- functionResp: (event as any).result,
187
- });
188
- } else if (event.type === "agentMessage") {
189
- this.agentEvents.emit(this.eventTypes.agentSay, {
190
- message: (event as any).message,
191
- });
192
- }
193
- }
194
- this.agentEvents.emit(this.eventTypes.threadUpdate, lastThread);
195
- this.lastThreadLength = lastThread.length;
196
- }
197
-
198
- // Emit done if the agent has completed and has a result
199
- const status = metadata.status;
200
- const result = metadata.result;
201
- if ((status === "completed" || status === "killed") && result != null) {
202
- this.stopWatching();
203
- this.agentEvents.emit(this.eventTypes.done, result);
204
- }
205
- }
206
-
207
- async sendMessage(message: string): Promise<void> {
208
- const inputPath = path.join(this.taskPath, "input.txt");
209
- await fsPromises.writeFile(inputPath, message, "utf8");
210
- }
211
-
212
- async getThreads(): Promise<any[][]> {
213
- const metadata = await this.readMetadata();
214
- return metadata?.threads || [];
215
- }
216
-
217
- stopWatching(): void {
218
- if (this.debounceTimer) {
219
- clearTimeout(this.debounceTimer);
220
- this.debounceTimer = null;
221
- }
222
- this.watcher?.close();
223
- this.watcher = null;
224
- console.log(`🔌 Stopped watching agent: ${this.taskId}`);
225
- }
226
-
227
- async pause(): Promise<void> {
228
- const statusPath = path.join(this.taskPath, "status.txt");
229
- await fsPromises.writeFile(statusPath, "paused", "utf8");
230
- console.log(`⏸️ Paused remote agent: ${this.taskId}`);
231
- }
232
-
233
- async unpause(): Promise<void> {
234
- const statusPath = path.join(this.taskPath, "status.txt");
235
- await fsPromises.writeFile(statusPath, "running", "utf8");
236
- console.log(`▶️ Unpaused remote agent: ${this.taskId}`);
237
- }
238
-
239
- async kill(): Promise<void> {
240
- const statusPath = path.join(this.taskPath, "status.txt");
241
- await fsPromises.writeFile(statusPath, "killed", "utf8");
242
- console.log(`🛑 Killed remote agent: ${this.taskId}`);
243
- }
244
-
245
- private async readMetadata(): Promise<any> {
246
- try {
247
- const metaPath = path.join(this.taskPath, "metadata.json");
248
- const content = await fsPromises.readFile(metaPath, "utf8");
249
- return JSON.parse(content);
250
- } catch {
251
- return null;
252
- }
253
- }
254
- }
255
-
256
- /**
257
- * Watches an agent running on Knowhow Web via polling the API.
258
- * Polls GET /tasks/<taskId> every 3 seconds for thread updates.
259
- * Sends messages via the client's sendMessageToAgent method.
260
- */
261
- export class WebSyncedAgentWatcher implements SyncedAgentWatcher {
262
- public taskId: string = "";
263
- private client: KnowhowSimpleClient;
264
- private pollInterval: NodeJS.Timeout | null = null;
265
- private lastThreadLength: number = 0;
266
- public agentName: string = "remote-agent";
267
- private stopped: boolean = false;
268
- public agentEvents = new EventService();
269
- public eventTypes = {
270
- done: "done",
271
- toolCall: "tool:pre_call",
272
- toolUsed: "tool:post_call",
273
- agentSay: "agent:say",
274
- threadUpdate: "thread_update",
275
- };
276
-
277
- constructor(client?: KnowhowSimpleClient) {
278
- this.client = client || new KnowhowSimpleClient();
279
- }
280
-
281
- async startWatching(taskId: string): Promise<void> {
282
- this.taskId = taskId;
283
- this.stopped = false;
284
-
285
- // Load initial state to track current thread length
286
- try {
287
- const details = await this.client.getTaskDetails(taskId);
288
- const threads: any[][] = details?.data?.threads || [];
289
- const lastThread = threads[threads.length - 1] || [];
290
- this.agentName = "remote-agent";
291
- this.lastThreadLength = lastThread.length;
292
- } catch (err: any) {
293
- console.warn(
294
- `⚠️ Could not load initial state for task ${taskId}: ${err.message}`
295
- );
296
- }
297
-
298
- // Poll every 3 seconds for updates
299
- this.pollInterval = setInterval(async () => {
300
- if (!this.stopped) {
301
- await this.onPoll().catch(() => {});
302
- }
303
- }, 3000);
304
-
305
- console.log(`🌐 Watching web-synced agent: ${taskId} (${this.agentName})`);
306
- console.log(
307
- ` Type /logs 20 to see recent messages, or type to send a message`
308
- );
309
- }
310
-
311
- private async onPoll(): Promise<void> {
312
- if (this.stopped) return;
313
- try {
314
- const details = await this.client.getTaskDetails(this.taskId);
315
- const threads: any[][] = details?.data?.threads || [];
316
- const lastThread = threads[threads.length - 1] || [];
317
-
318
- const newMessages = lastThread.slice(this.lastThreadLength);
319
- if (newMessages.length > 0) {
320
- const renderEvents = messagesToRenderEvents(
321
- newMessages,
322
- this.taskId,
323
- this.agentName
324
- );
325
- for (const event of renderEvents) {
326
- if (event.type === "toolCall") {
327
- this.agentEvents.emit(this.eventTypes.toolCall, {
328
- toolCall: (event as any).toolCall,
329
- });
330
- } else if (event.type === "toolResult") {
331
- this.agentEvents.emit(this.eventTypes.toolUsed, {
332
- toolCall: (event as any).toolCall,
333
- functionResp: (event as any).result,
334
- });
335
- } else if (event.type === "agentMessage") {
336
- this.agentEvents.emit(this.eventTypes.agentSay, {
337
- message: (event as any).message,
338
- });
339
- }
340
- }
341
- this.agentEvents.emit(this.eventTypes.threadUpdate, lastThread);
342
- this.lastThreadLength = lastThread.length;
343
- }
344
-
345
- // Stop polling and emit done if task is complete with a result
346
- const status = details?.data?.status;
347
- const result = details?.data?.result;
348
- if (status === "completed" || status === "killed") {
349
- this.stopWatching();
350
- if (result != null) {
351
- this.agentEvents.emit(this.eventTypes.done, result);
352
- } else {
353
- console.log(`\n✅ Remote agent ${this.taskId} status: ${status} (no result)`);
354
- }
355
- }
356
- } catch {
357
- // Silently continue on poll errors
358
- }
359
- }
360
-
361
- async sendMessage(message: string): Promise<void> {
362
- await this.client.sendMessageToAgent(this.taskId, message);
363
- }
364
-
365
- async getThreads(): Promise<any[][]> {
366
- try {
367
- const details = await this.client.getTaskDetails(this.taskId);
368
- return details?.data?.threads || [];
369
- } catch {
370
- return [];
371
- }
372
- }
373
-
374
- stopWatching(): void {
375
- this.stopped = true;
376
- if (this.pollInterval) {
377
- clearInterval(this.pollInterval);
378
- this.pollInterval = null;
379
- }
380
- console.log(`🔌 Stopped watching web agent: ${this.taskId}`);
381
- }
382
-
383
- async pause(): Promise<void> {
384
- await this.client.pauseAgent(this.taskId);
385
- console.log(`⏸️ Paused remote web agent: ${this.taskId}`);
386
- }
387
-
388
- async unpause(): Promise<void> {
389
- await this.client.resumeAgent(this.taskId);
390
- console.log(`▶️ Unpaused remote web agent: ${this.taskId}`);
391
- }
392
-
393
- async kill(): Promise<void> {
394
- await this.client.killAgent(this.taskId);
395
- console.log(`🛑 Killed remote web agent: ${this.taskId}`);
396
- }
397
- }
@@ -32,6 +32,7 @@ export * from "./SessionManager";
32
32
  export * from "./TaskRegistry";
33
33
  export * from "./SyncedAgentWatcher";
34
34
  export * from "./SyncerService";
35
+ export * from "./watchers";
35
36
  export { Clients } from "../clients";
36
37
 
37
38
  let Singletons = {} as {