@tyvm/knowhow 0.0.102 → 0.0.104

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 (62) hide show
  1. package/package.json +2 -2
  2. package/src/agents/base/base.ts +3 -0
  3. package/src/chat/modules/AgentModule.ts +24 -10
  4. package/src/chat/modules/InternalChatModule.ts +6 -0
  5. package/src/chat/modules/RemoteSyncModule.ts +447 -0
  6. package/src/chat/types.ts +2 -0
  7. package/src/clients/pricing/catalog.ts +287 -0
  8. package/src/clients/pricing/index.ts +2 -0
  9. package/src/config.ts +2 -0
  10. package/src/fileSync.ts +15 -2
  11. package/src/hashes.ts +33 -0
  12. package/src/services/AgentSyncFs.ts +44 -14
  13. package/src/services/AgentSyncKnowhowWeb.ts +27 -5
  14. package/src/services/KnowhowClient.ts +61 -0
  15. package/src/services/SessionManager.ts +2 -0
  16. package/src/services/script-execution/ScriptPolicy.ts +0 -44
  17. package/src/types.ts +3 -0
  18. package/src/worker.ts +70 -4
  19. package/ts_build/package.json +2 -2
  20. package/ts_build/src/agents/base/base.js +1 -0
  21. package/ts_build/src/agents/base/base.js.map +1 -1
  22. package/ts_build/src/chat/modules/AgentModule.d.ts +2 -1
  23. package/ts_build/src/chat/modules/AgentModule.js +12 -7
  24. package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
  25. package/ts_build/src/chat/modules/InternalChatModule.d.ts +1 -0
  26. package/ts_build/src/chat/modules/InternalChatModule.js +6 -0
  27. package/ts_build/src/chat/modules/InternalChatModule.js.map +1 -1
  28. package/ts_build/src/chat/modules/RemoteSyncModule.d.ts +27 -0
  29. package/ts_build/src/chat/modules/RemoteSyncModule.js +282 -0
  30. package/ts_build/src/chat/modules/RemoteSyncModule.js.map +1 -0
  31. package/ts_build/src/chat/types.d.ts +2 -0
  32. package/ts_build/src/clients/pricing/catalog.d.ts +28 -0
  33. package/ts_build/src/clients/pricing/catalog.js +179 -0
  34. package/ts_build/src/clients/pricing/catalog.js.map +1 -0
  35. package/ts_build/src/clients/pricing/index.d.ts +2 -0
  36. package/ts_build/src/clients/pricing/index.js +8 -1
  37. package/ts_build/src/clients/pricing/index.js.map +1 -1
  38. package/ts_build/src/config.js +1 -0
  39. package/ts_build/src/config.js.map +1 -1
  40. package/ts_build/src/fileSync.js +8 -1
  41. package/ts_build/src/fileSync.js.map +1 -1
  42. package/ts_build/src/hashes.d.ts +2 -0
  43. package/ts_build/src/hashes.js +23 -0
  44. package/ts_build/src/hashes.js.map +1 -1
  45. package/ts_build/src/services/AgentSyncFs.d.ts +6 -3
  46. package/ts_build/src/services/AgentSyncFs.js +30 -13
  47. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  48. package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -0
  49. package/ts_build/src/services/AgentSyncKnowhowWeb.js +13 -2
  50. package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
  51. package/ts_build/src/services/KnowhowClient.d.ts +21 -0
  52. package/ts_build/src/services/KnowhowClient.js +10 -0
  53. package/ts_build/src/services/KnowhowClient.js.map +1 -1
  54. package/ts_build/src/services/Mcp.d.ts +219 -406
  55. package/ts_build/src/services/SessionManager.js +2 -0
  56. package/ts_build/src/services/SessionManager.js.map +1 -1
  57. package/ts_build/src/services/script-execution/ScriptPolicy.js +0 -35
  58. package/ts_build/src/services/script-execution/ScriptPolicy.js.map +1 -1
  59. package/ts_build/src/types.d.ts +2 -0
  60. package/ts_build/src/types.js.map +1 -1
  61. package/ts_build/src/worker.js +51 -2
  62. package/ts_build/src/worker.js.map +1 -1
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Model catalog: a single source of truth for all supported AI models,
3
+ * their providers, types, display names, and pricing.
4
+ *
5
+ * Pricing is in USD per 1M tokens (or per-image / per-second for media models).
6
+ * This is exported so the Knowhow backend (and other consumers) can import
7
+ * model/pricing data without duplicating it.
8
+ */
9
+
10
+ import { Models, EmbeddingModels } from "../../types";
11
+ import { OpenAiTextPricing } from "./openai";
12
+ import { AnthropicTextPricing } from "./anthropic";
13
+ import { GeminiPricing } from "./google";
14
+ import { XaiTextPricing, XaiImagePricing, XaiVideoPricing } from "./xai";
15
+
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
+ }
47
+
48
+ // ─── Platform markup ──────────────────────────────────────────────────────────
49
+
50
+ /** 2.5% platform markup applied on top of all provider base rates */
51
+ export const USAGE_MARKUP_PERCENT = 2.5 / 100;
52
+
53
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
54
+
55
+ function completion(
56
+ id: string,
57
+ provider: string,
58
+ displayName: string,
59
+ pricingOverride?: Partial<ModelPricing>
60
+ ): ModelCatalogEntry {
61
+ const base =
62
+ (OpenAiTextPricing as Record<string, Partial<ModelPricing>>)[id] ||
63
+ (AnthropicTextPricing as Record<string, Partial<ModelPricing>>)[id] ||
64
+ (GeminiPricing as Record<string, Partial<ModelPricing>>)[id] ||
65
+ (XaiTextPricing as Record<string, Partial<ModelPricing>>)[id] ||
66
+ {};
67
+ return {
68
+ id,
69
+ provider,
70
+ type: "completion",
71
+ displayName,
72
+ markupPercent: USAGE_MARKUP_PERCENT,
73
+ pricing: {
74
+ input: 0,
75
+ output: 0,
76
+ ...base,
77
+ ...pricingOverride,
78
+ },
79
+ };
80
+ }
81
+
82
+ function embedding(
83
+ id: string,
84
+ provider: string,
85
+ displayName: string,
86
+ input: number
87
+ ): ModelCatalogEntry {
88
+ return {
89
+ id,
90
+ provider,
91
+ type: "embedding",
92
+ displayName,
93
+ markupPercent: USAGE_MARKUP_PERCENT,
94
+ pricing: { input, output: 0 },
95
+ };
96
+ }
97
+
98
+ function image(
99
+ id: string,
100
+ provider: string,
101
+ displayName: string,
102
+ pricing: Partial<ModelPricing>
103
+ ): ModelCatalogEntry {
104
+ return {
105
+ id,
106
+ provider,
107
+ type: "image",
108
+ displayName,
109
+ markupPercent: USAGE_MARKUP_PERCENT,
110
+ pricing: { input: 0, output: 0, ...pricing },
111
+ };
112
+ }
113
+
114
+ function video(
115
+ id: string,
116
+ provider: string,
117
+ displayName: string,
118
+ video_generation: number
119
+ ): ModelCatalogEntry {
120
+ return {
121
+ id,
122
+ provider,
123
+ type: "video",
124
+ displayName,
125
+ markupPercent: USAGE_MARKUP_PERCENT,
126
+ pricing: { input: 0, output: 0, video_generation },
127
+ };
128
+ }
129
+
130
+ function audio(
131
+ id: string,
132
+ provider: string,
133
+ displayName: string,
134
+ pricing: Partial<ModelPricing>
135
+ ): ModelCatalogEntry {
136
+ return {
137
+ id,
138
+ provider,
139
+ type: "audio",
140
+ displayName,
141
+ markupPercent: USAGE_MARKUP_PERCENT,
142
+ pricing: { input: 0, output: 0, ...pricing },
143
+ };
144
+ }
145
+
146
+ function transaction(
147
+ id: string,
148
+ provider: string,
149
+ displayName: string,
150
+ pricing: Partial<ModelPricing>
151
+ ): ModelCatalogEntry {
152
+ return {
153
+ id,
154
+ provider,
155
+ type: "transaction",
156
+ displayName,
157
+ markupPercent: USAGE_MARKUP_PERCENT,
158
+ pricing: { input: 0, output: 0, ...pricing },
159
+ };
160
+ }
161
+
162
+ // ─── OpenAI ───────────────────────────────────────────────────────────────────
163
+
164
+ export const OPENAI_MODEL_CATALOG: ModelCatalogEntry[] = [
165
+ // Completion
166
+ completion(Models.openai.GPT_4o, "openai", "GPT-4o"),
167
+ completion(Models.openai.GPT_4o_Mini, "openai", "GPT-4o Mini"),
168
+ completion(Models.openai.GPT_41, "openai", "GPT-4.1"),
169
+ completion(Models.openai.GPT_41_Mini, "openai", "GPT-4.1 Mini"),
170
+ completion(Models.openai.GPT_41_Nano, "openai", "GPT-4.1 Nano"),
171
+ completion(Models.openai.GPT_45, "openai", "GPT-4.5 Preview"),
172
+ completion(Models.openai.o1, "openai", "o1"),
173
+ completion(Models.openai.o1_Mini, "openai", "o1 Mini"),
174
+ completion(Models.openai.o3, "openai", "o3"),
175
+ completion(Models.openai.o3_Mini, "openai", "o3 Mini"),
176
+ completion(Models.openai.o4_Mini, "openai", "o4 Mini"),
177
+ // Embedding
178
+ embedding(EmbeddingModels.openai.EmbeddingAda2, "openai", "Embedding Ada 002", OpenAiTextPricing[EmbeddingModels.openai.EmbeddingAda2]?.input ?? 0.1),
179
+ embedding(EmbeddingModels.openai.EmbeddingLarge3, "openai", "Embedding 3 Large", OpenAiTextPricing[EmbeddingModels.openai.EmbeddingLarge3]?.input ?? 0.13),
180
+ embedding(EmbeddingModels.openai.EmbeddingSmall3, "openai", "Embedding 3 Small", OpenAiTextPricing[EmbeddingModels.openai.EmbeddingSmall3]?.input ?? 0.02),
181
+ // Image generation
182
+ image(Models.openai.DALL_E_3, "openai", "DALL-E 3", { image_generation: 0.04 }),
183
+ image(Models.openai.DALL_E_2, "openai", "DALL-E 2", { image_generation: 0.02 }),
184
+ image(Models.openai.GPT_Image_15, "openai", "GPT Image 1.5", { input: OpenAiTextPricing[Models.openai.GPT_Image_15]?.input ?? 5.0, output: OpenAiTextPricing[Models.openai.GPT_Image_15]?.output ?? 10.0 }),
185
+ image(Models.openai.GPT_Image_1_Mini, "openai", "GPT Image 1 Mini", { input: OpenAiTextPricing[Models.openai.GPT_Image_1_Mini]?.input ?? 2.0 }),
186
+ // Video generation
187
+ video(Models.openai.Sora, "openai", "Sora", 0.012),
188
+ video(Models.openai.Sora_2, "openai", "Sora 2", 0.015),
189
+ // Audio
190
+ audio(Models.openai.TTS_1, "openai", "TTS-1", { input: 15.0 }),
191
+ audio(Models.openai.Whisper_1, "openai", "Whisper 1", { input: 0.006 }),
192
+ // Transaction / Search
193
+ transaction(Models.openai.GPT_4o_Mini_Search, "openai", "GPT-4o Mini Search", { input: OpenAiTextPricing[Models.openai.GPT_4o_Mini_Search]?.input ?? 0.15, output: OpenAiTextPricing[Models.openai.GPT_4o_Mini_Search]?.output ?? 0.6 }),
194
+ transaction(Models.openai.GPT_4o_Search, "openai", "GPT-4o Search", { input: OpenAiTextPricing[Models.openai.GPT_4o_Search]?.input ?? 2.5, output: OpenAiTextPricing[Models.openai.GPT_4o_Search]?.output ?? 10.0 }),
195
+ ];
196
+
197
+ // ─── Anthropic ────────────────────────────────────────────────────────────────
198
+
199
+ export const ANTHROPIC_MODEL_CATALOG: ModelCatalogEntry[] = [
200
+ completion(Models.anthropic.Opus4_5, "anthropic", "Claude Opus 4.5"),
201
+ completion(Models.anthropic.Sonnet4_5, "anthropic", "Claude Sonnet 4.5"),
202
+ completion(Models.anthropic.Opus4, "anthropic", "Claude Opus 4"),
203
+ completion(Models.anthropic.Sonnet4, "anthropic", "Claude Sonnet 4"),
204
+ completion(Models.anthropic.Haiku4_5, "anthropic", "Claude Haiku 4.5"),
205
+ completion(Models.anthropic.Sonnet3_7, "anthropic", "Claude Sonnet 3.7"),
206
+ completion(Models.anthropic.Sonnet3_5, "anthropic", "Claude Sonnet 3.5"),
207
+ completion(Models.anthropic.Haiku3, "anthropic", "Claude Haiku 3"),
208
+ completion(Models.anthropic.Opus3, "anthropic", "Claude Opus 3"),
209
+ ];
210
+
211
+ // ─── Google ───────────────────────────────────────────────────────────────────
212
+
213
+ export const GOOGLE_MODEL_CATALOG: ModelCatalogEntry[] = [
214
+ // Completion
215
+ completion(Models.google.Gemini_25_Pro, "google", "Gemini 2.5 Pro"),
216
+ completion(Models.google.Gemini_25_Flash, "google", "Gemini 2.5 Flash"),
217
+ completion(Models.google.Gemini_25_Flash_Lite, "google", "Gemini 2.5 Flash Lite"),
218
+ completion(Models.google.Gemini_20_Flash, "google", "Gemini 2.0 Flash"),
219
+ completion(Models.google.Gemini_15_Pro, "google", "Gemini 1.5 Pro"),
220
+ completion(Models.google.Gemini_15_Flash, "google", "Gemini 1.5 Flash"),
221
+ completion(Models.google.Gemini_15_Flash_8B, "google", "Gemini 1.5 Flash 8B"),
222
+ // Embedding
223
+ embedding(EmbeddingModels.google.Gemini_Embedding, "google", "Gemini Embedding", GeminiPricing[EmbeddingModels.google.Gemini_Embedding]?.input ?? 0),
224
+ // Image generation
225
+ image(Models.google.Gemini_20_Flash_Preview_Image_Generation, "google", "Gemini 2.0 Flash Image", {
226
+ input: GeminiPricing[Models.google.Gemini_20_Flash_Preview_Image_Generation]?.input ?? 0.1,
227
+ output: GeminiPricing[Models.google.Gemini_20_Flash_Preview_Image_Generation]?.output ?? 0.4,
228
+ image_generation: GeminiPricing[Models.google.Gemini_20_Flash_Preview_Image_Generation]?.image_generation ?? 0.039,
229
+ }),
230
+ image(Models.google.Gemini_25_Flash_Image, "google", "Gemini 2.5 Flash Image", {
231
+ input: GeminiPricing[Models.google.Gemini_25_Flash_Image]?.input ?? 0.3,
232
+ output: GeminiPricing[Models.google.Gemini_25_Flash_Image]?.output ?? 0.039,
233
+ image_generation: GeminiPricing[Models.google.Gemini_25_Flash_Image]?.image_generation ?? 0.039,
234
+ }),
235
+ image(Models.google.Gemini_31_Flash_Image_Preview, "google", "Gemini 3.1 Flash Image", {
236
+ input: GeminiPricing[Models.google.Gemini_31_Flash_Image_Preview]?.input ?? 0.5,
237
+ output: GeminiPricing[Models.google.Gemini_31_Flash_Image_Preview]?.output ?? 3.0,
238
+ image_generation: GeminiPricing[Models.google.Gemini_31_Flash_Image_Preview]?.image_generation ?? 0.045,
239
+ }),
240
+ image(Models.google.Gemini_3_Pro_Image_Preview, "google", "Gemini 3 Pro Image", {
241
+ input: GeminiPricing[Models.google.Gemini_3_Pro_Image_Preview]?.input ?? 2.0,
242
+ output: GeminiPricing[Models.google.Gemini_3_Pro_Image_Preview]?.output ?? 12.0,
243
+ image_generation: GeminiPricing[Models.google.Gemini_3_Pro_Image_Preview]?.image_generation ?? 0.134,
244
+ }),
245
+ image(Models.google.Imagen_3, "google", "Imagen 4", { image_generation: GeminiPricing[Models.google.Imagen_3]?.image_generation ?? 0.04 }),
246
+ image(Models.google.Imagen_4_Fast, "google", "Imagen 4 Fast", { image_generation: GeminiPricing[Models.google.Imagen_4_Fast]?.image_generation ?? 0.02 }),
247
+ image(Models.google.Imagen_4_Ultra, "google", "Imagen 4 Ultra", { image_generation: GeminiPricing[Models.google.Imagen_4_Ultra]?.image_generation ?? 0.06 }),
248
+ // Video generation
249
+ video(Models.google.Veo_2, "google", "Veo 2", GeminiPricing[Models.google.Veo_2]?.video_generation ?? 0.35),
250
+ video(Models.google.Veo_3, "google", "Veo 3", GeminiPricing[Models.google.Veo_3]?.video_generation ?? 0.4),
251
+ video(Models.google.Veo_3_Fast, "google", "Veo 3 Fast", GeminiPricing[Models.google.Veo_3_Fast]?.video_generation ?? 0.1),
252
+ // Audio (TTS)
253
+ audio(Models.google.Gemini_25_Flash_TTS, "google", "Gemini 2.5 Flash TTS", {
254
+ input: GeminiPricing[Models.google.Gemini_25_Flash_TTS]?.input ?? 0.5,
255
+ output_audio: GeminiPricing[Models.google.Gemini_25_Flash_TTS]?.output_audio ?? 10.0,
256
+ output: GeminiPricing[Models.google.Gemini_25_Flash_TTS]?.output_audio ?? 10.0,
257
+ }),
258
+ audio(Models.google.Gemini_25_Pro_TTS, "google", "Gemini 2.5 Pro TTS", {
259
+ input: GeminiPricing[Models.google.Gemini_25_Pro_TTS]?.input ?? 1.0,
260
+ output_audio: GeminiPricing[Models.google.Gemini_25_Pro_TTS]?.output_audio ?? 20.0,
261
+ output: GeminiPricing[Models.google.Gemini_25_Pro_TTS]?.output_audio ?? 20.0,
262
+ }),
263
+ ];
264
+
265
+ // ─── xAI ──────────────────────────────────────────────────────────────────────
266
+
267
+ export const XAI_MODEL_CATALOG: ModelCatalogEntry[] = [
268
+ completion(Models.xai.Grok4, "xai", "Grok 4"),
269
+ completion(Models.xai.Grok3Beta, "xai", "Grok 3 Beta"),
270
+ completion(Models.xai.Grok3MiniBeta, "xai", "Grok 3 Mini Beta"),
271
+ completion(Models.xai.Grok3FastBeta, "xai", "Grok 3 Fast Beta"),
272
+ completion(Models.xai.Grok21212, "xai", "Grok 2"),
273
+ // 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 }),
276
+ // Video generation
277
+ video(Models.xai.GrokImagineVideo, "xai", "Grok Imagine Video", XaiVideoPricing["grok-imagine-video"] ?? 0.05),
278
+ ];
279
+
280
+ // ─── Combined catalog ─────────────────────────────────────────────────────────
281
+
282
+ export const ALL_MODEL_CATALOG: ModelCatalogEntry[] = [
283
+ ...OPENAI_MODEL_CATALOG,
284
+ ...ANTHROPIC_MODEL_CATALOG,
285
+ ...GOOGLE_MODEL_CATALOG,
286
+ ...XAI_MODEL_CATALOG,
287
+ ];
@@ -2,3 +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";
package/src/config.ts CHANGED
@@ -109,6 +109,8 @@ const defaultConfig = {
109
109
  allowedPorts: [],
110
110
  },
111
111
  },
112
+
113
+ syncRemote: false,
112
114
  } as Config;
113
115
 
114
116
  const defaultLanguage = {
package/src/fileSync.ts CHANGED
@@ -5,7 +5,7 @@ import { loadJwt } from "./login";
5
5
  import { getConfig } from "./config";
6
6
  import { services } from "./services";
7
7
  import { S3Service } from "./services/S3";
8
- import { getHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote } from "./hashes";
8
+ import { getHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote, isLocalFileMatchingDownloadHash, saveDownloadHash } from "./hashes";
9
9
 
10
10
  export interface FileSyncOptions {
11
11
  upload?: boolean;
@@ -145,12 +145,21 @@ async function downloadFile(
145
145
  }
146
146
 
147
147
  try {
148
+ // Fast-path: check stored download hash before hitting the API
149
+ const hashes = await getHashes();
150
+ if (await isLocalFileMatchingDownloadHash(localPath, hashes)) {
151
+ console.log(` ✓ Skipping ${localPath} (matches stored download hash)`);
152
+ return;
153
+ }
154
+
148
155
  // Get presigned download URL + remote checksum
149
156
  const { downloadUrl, checksumSHA256 } = await client.getOrgFilePresignedDownloadUrl(remotePath);
150
157
 
151
158
  // Skip if local file matches remote checksum
152
159
  if (isLocalFileMatchingRemote(localPath, checksumSHA256)) {
153
160
  console.log(` ✓ Skipping ${localPath} (matches remote checksum)`);
161
+ // Store the hash so future syncs can skip without hitting the API
162
+ await saveDownloadHash(localPath);
154
163
  return;
155
164
  }
156
165
 
@@ -163,6 +172,9 @@ async function downloadFile(
163
172
  // Download file using presigned URL
164
173
  await s3Service.downloadFromPresignedUrl(downloadUrl, localPath);
165
174
 
175
+ // Save download hash so we can skip unchanged files next time
176
+ await saveDownloadHash(localPath);
177
+
166
178
  // Get file size for logging
167
179
  const stats = fs.statSync(localPath);
168
180
  console.log(` ✓ Downloaded ${stats.size} bytes`);
@@ -284,7 +296,8 @@ async function downloadDirectory(
284
296
  const fullPath = f.folderPath.endsWith("/")
285
297
  ? f.folderPath + f.fileName
286
298
  : f.folderPath + "/" + f.fileName;
287
- return fullPath.startsWith(remoteDir);
299
+ // Exclude directory placeholder entries (empty fileName) and only include real files
300
+ return f.fileName !== "" && fullPath.startsWith(remoteDir);
288
301
  });
289
302
 
290
303
  if (matchingFiles.length === 0) {
package/src/hashes.ts CHANGED
@@ -73,6 +73,8 @@ export async function saveAllFileHashes(files: string[], promptHash: string) {
73
73
 
74
74
  const UPLOAD_KEY = "upload";
75
75
 
76
+ const DOWNLOAD_KEY = "download";
77
+
76
78
  /**
77
79
  * Returns true if the file has changed since the last successful upload
78
80
  * (or if it has never been uploaded before)
@@ -101,6 +103,37 @@ export async function saveUploadHash(localPath: string) {
101
103
  await saveHashes(hashes);
102
104
  }
103
105
 
106
+ /**
107
+ * Returns true if the local file already matches the hash stored from
108
+ * the last successful download, meaning we can skip the download.
109
+ */
110
+ export async function isLocalFileMatchingDownloadHash(
111
+ localPath: string,
112
+ hashes: any
113
+ ): Promise<boolean> {
114
+ if (!fs.existsSync(localPath)) return false;
115
+ const storedHash = hashes[localPath]?.[DOWNLOAD_KEY];
116
+ if (!storedHash) return false;
117
+ const content = fs.readFileSync(localPath);
118
+ const currentHash = crypto.createHash("sha256").update(content).digest("base64");
119
+ return storedHash === currentHash;
120
+ }
121
+
122
+ /**
123
+ * Saves the SHA-256 hash of the file after a successful download so we can
124
+ * skip unchanged files on the next sync.
125
+ */
126
+ export async function saveDownloadHash(localPath: string) {
127
+ const hashes = await getHashes();
128
+ const content = fs.readFileSync(localPath);
129
+ const currentHash = crypto.createHash("sha256").update(content).digest("base64");
130
+ if (!hashes[localPath]) {
131
+ hashes[localPath] = { fileHash: currentHash, promptHash: "" };
132
+ }
133
+ hashes[localPath][DOWNLOAD_KEY] = currentHash;
134
+ await saveHashes(hashes);
135
+ }
136
+
104
137
  /**
105
138
  * Compute SHA-256 of a local file, returned as base64 (matches S3 encoding)
106
139
  */
@@ -18,21 +18,31 @@ export interface FsSyncOptions {
18
18
  * Creates files in .knowhow/processes/agents/{taskId}/ for status and input
19
19
  */
20
20
  export class AgentSyncFs {
21
+ /** Shared cleanup interval across all instances to avoid duplicate cleanup runs */
22
+ private static sharedCleanupInterval: NodeJS.Timeout | null = null;
23
+ private static sharedBasePath: string = ".knowhow/processes/agents";
24
+ private static cleanupStarted: boolean = false;
25
+
21
26
  private taskId: string | undefined;
22
27
  private basePath: string = ".knowhow/processes/agents";
23
28
  private taskPath: string | undefined;
24
29
  private eventHandlersSetup: boolean = false;
25
30
  private watcher: ReturnType<typeof watch> | null = null;
26
31
  private lastInputContent: string = "";
27
- private cleanupInterval: NodeJS.Timeout | null = null;
28
32
  private finalizationPromise: Promise<void> | null = null;
29
33
  private agent: BaseAgent | undefined;
30
34
  private threadUpdateHandler: ((...args: any[]) => void) | undefined;
31
35
  private doneHandler: ((...args: any[]) => void) | undefined;
36
+ /**
37
+ * Tracks the most recent in-flight filesystem metadata update.
38
+ * The done handler awaits this before finalizing, preventing a race where
39
+ * the completion call writes before the last thread sync finishes.
40
+ */
41
+ private pendingThreadUpdatePromise: Promise<void> | null = null;
32
42
 
33
43
  constructor() {
34
44
  // Start cleanup process when created
35
- this.startCleanupProcess();
45
+ AgentSyncFs.startSharedCleanupProcess();
36
46
  }
37
47
 
38
48
  /**
@@ -294,8 +304,12 @@ export class AgentSyncFs {
294
304
  if (!this.taskId) return;
295
305
 
296
306
  try {
297
- await this.updateMetadata(agent, true);
298
- await this.checkForChanges(agent);
307
+ // Track the pending update so the done handler can await it.
308
+ this.pendingThreadUpdatePromise = (async () => {
309
+ await this.updateMetadata(agent, true);
310
+ await this.checkForChanges(agent);
311
+ })();
312
+ await this.pendingThreadUpdatePromise;
299
313
  } catch (error) {
300
314
  console.error(`❌ Error during threadUpdate sync:`, error);
301
315
  }
@@ -314,6 +328,17 @@ export class AgentSyncFs {
314
328
  // Store finalization promise so callers can await it (same pattern as AgentSyncKnowhowWeb)
315
329
  this.finalizationPromise = (async () => {
316
330
  try {
331
+ // Flush any in-flight thread update before finalizing.
332
+ // This prevents the race where a pending "inProgress: true" metadata write
333
+ // overwrites the finalization write.
334
+ if (this.pendingThreadUpdatePromise) {
335
+ console.log(`⏳ [AgentSyncFs] Awaiting pending thread update before finalizing...`);
336
+ await this.pendingThreadUpdatePromise.catch(() => {
337
+ // Ignore errors in pending update — we still want to finalize
338
+ });
339
+ this.pendingThreadUpdatePromise = null;
340
+ }
341
+
317
342
  await this.updateMetadata(agent, false, result);
318
343
  console.log(`✅ Completed filesystem sync for task: ${this.taskId}`);
319
344
  await this.cleanup();
@@ -351,9 +376,9 @@ export class AgentSyncFs {
351
376
  /**
352
377
  * Clean up old task directories (older than 3 days)
353
378
  */
354
- private async cleanupOldTasks(): Promise<void> {
379
+ private static async cleanupOldTasks(): Promise<void> {
355
380
  try {
356
- const agentsPath = this.basePath;
381
+ const agentsPath = AgentSyncFs.sharedBasePath;
357
382
 
358
383
  // Check if directory exists
359
384
  try {
@@ -393,23 +418,27 @@ export class AgentSyncFs {
393
418
  /**
394
419
  * Start periodic cleanup process
395
420
  */
396
- private startCleanupProcess(): void {
397
- // Run cleanup every hour
398
- this.cleanupInterval = setInterval(() => {
399
- this.cleanupOldTasks();
421
+ private static startSharedCleanupProcess(): void {
422
+ if (AgentSyncFs.cleanupStarted) return;
423
+ AgentSyncFs.cleanupStarted = true;
424
+
425
+ // Run cleanup every hour (shared across all instances)
426
+ AgentSyncFs.sharedCleanupInterval = setInterval(() => {
427
+ AgentSyncFs.cleanupOldTasks();
400
428
  }, 60 * 60 * 1000);
401
429
 
402
430
  // Also run once on startup
403
- this.cleanupOldTasks();
431
+ AgentSyncFs.cleanupOldTasks();
404
432
  }
405
433
 
406
434
  /**
407
435
  * Stop cleanup process
408
436
  */
409
437
  stopCleanup(): void {
410
- if (this.cleanupInterval) {
411
- clearInterval(this.cleanupInterval);
412
- this.cleanupInterval = null;
438
+ if (AgentSyncFs.sharedCleanupInterval) {
439
+ clearInterval(AgentSyncFs.sharedCleanupInterval);
440
+ AgentSyncFs.sharedCleanupInterval = null;
441
+ AgentSyncFs.cleanupStarted = false;
413
442
  }
414
443
  }
415
444
 
@@ -435,5 +464,6 @@ export class AgentSyncFs {
435
464
  this.eventHandlersSetup = false;
436
465
  this.lastInputContent = "";
437
466
  this.finalizationPromise = null;
467
+ this.pendingThreadUpdatePromise = null;
438
468
  }
439
469
  }
@@ -36,6 +36,12 @@ export class AgentSyncKnowhowWeb {
36
36
  private agent: BaseAgent | undefined;
37
37
  private threadUpdateHandler: ((...args: any[]) => void) | undefined;
38
38
  private doneHandler: ((...args: any[]) => void) | undefined;
39
+ /**
40
+ * Tracks the most recent in-flight thread update API call.
41
+ * The done handler awaits this before sending the finalization call,
42
+ * preventing a race where the completion overwrites a later in-progress update.
43
+ */
44
+ private pendingThreadUpdatePromise: Promise<void> | null = null;
39
45
 
40
46
  constructor(baseUrl: string = KNOWHOW_API_URL) {
41
47
  this.baseUrl = baseUrl;
@@ -240,11 +246,15 @@ export class AgentSyncKnowhowWeb {
240
246
  }
241
247
 
242
248
  try {
243
- // Update task with current state
244
- await this.updateChatTask(this.knowhowTaskId, agent, true);
245
-
246
- // Check for pending messages, pause, or kill status
247
- await this.checkAndProcessPendingMessages(agent, this.knowhowTaskId);
249
+ // Track the pending thread update so the done handler can await it.
250
+ this.pendingThreadUpdatePromise = (async () => {
251
+ // Update task with current state
252
+ await this.updateChatTask(this.knowhowTaskId!, agent, true);
253
+
254
+ // Check for pending messages, pause, or kill status
255
+ await this.checkAndProcessPendingMessages(agent, this.knowhowTaskId!);
256
+ })();
257
+ await this.pendingThreadUpdatePromise;
248
258
  } catch (error) {
249
259
  console.error(`❌ Error during threadUpdate sync:`, error);
250
260
  // Continue execution even if synchronization fails
@@ -264,6 +274,17 @@ export class AgentSyncKnowhowWeb {
264
274
  // Create a promise that tracks finalization
265
275
  this.finalizationPromise = (async () => {
266
276
  try {
277
+ // Flush any in-flight thread update before sending the completion call.
278
+ // This prevents the race where a "inProgress: true" update overtakes
279
+ // the "inProgress: false" finalization call.
280
+ if (this.pendingThreadUpdatePromise) {
281
+ console.log(`⏳ [AgentSync] Awaiting pending thread update before finalizing...`);
282
+ await this.pendingThreadUpdatePromise.catch(() => {
283
+ // Ignore errors in the pending update — we still want to finalize
284
+ });
285
+ this.pendingThreadUpdatePromise = null;
286
+ }
287
+
267
288
  console.log(
268
289
  `Updating Knowhow chat task on completion..., ${this.knowhowTaskId}`
269
290
  );
@@ -306,6 +327,7 @@ export class AgentSyncKnowhowWeb {
306
327
  this.knowhowTaskId = undefined;
307
328
  this.eventHandlersSetup = false;
308
329
  this.finalizationPromise = null;
330
+ this.pendingThreadUpdatePromise = null;
309
331
  }
310
332
 
311
333
  /**
@@ -24,6 +24,30 @@ import {
24
24
  } from "../clients";
25
25
  import { Config } from "../types";
26
26
 
27
+ // Remote sync placeholder interfaces
28
+ export interface CreateSessionPlaceholderRequest {
29
+ title?: string;
30
+ workerId?: string;
31
+ metadata?: Record<string, any>;
32
+ }
33
+
34
+ export interface CreateSessionPlaceholderResponse {
35
+ sessionId: string;
36
+ orgId: string;
37
+ }
38
+
39
+ export interface CreateMessagePlaceholderRequest {
40
+ content: string;
41
+ agentName?: string;
42
+ modelName?: string;
43
+ metadata?: Record<string, any>;
44
+ }
45
+
46
+ export interface CreateMessagePlaceholderResponse {
47
+ messageId: string;
48
+ taskId?: string;
49
+ }
50
+
27
51
  // Chat Task interfaces
28
52
  export interface CreateMessageTaskRequest {
29
53
  messageId: string;
@@ -697,4 +721,41 @@ export class KnowhowSimpleClient {
697
721
  { headers: this.headers }
698
722
  );
699
723
  }
724
+
725
+ // ============================================
726
+ // Remote Sync Placeholder Methods
727
+ // ============================================
728
+
729
+ /**
730
+ * Create a bare session stub without triggering AI inference.
731
+ * Used by the CLI remote sync feature to establish a remote session.
732
+ */
733
+ async createSessionPlaceholder(
734
+ request: CreateSessionPlaceholderRequest = {}
735
+ ): Promise<CreateSessionPlaceholderResponse> {
736
+ await this.checkJwt();
737
+ const response = await http.post<CreateSessionPlaceholderResponse>(
738
+ `${this.baseUrl}/api/chat/sessions/placeholder`,
739
+ request,
740
+ { headers: this.headers }
741
+ );
742
+ return response.data;
743
+ }
744
+
745
+ /**
746
+ * Create a message placeholder in a session without triggering AI inference.
747
+ * Used by the CLI remote sync feature to register a message before syncing threads.
748
+ */
749
+ async createMessagePlaceholder(
750
+ sessionId: string,
751
+ request: CreateMessagePlaceholderRequest
752
+ ): Promise<CreateMessagePlaceholderResponse> {
753
+ await this.checkJwt();
754
+ const response = await http.post<CreateMessagePlaceholderResponse>(
755
+ `${this.baseUrl}/api/chat/sessions/${sessionId}/messages/placeholder`,
756
+ request,
757
+ { headers: this.headers }
758
+ );
759
+ return response.data;
760
+ }
700
761
  }
@@ -54,6 +54,7 @@ export class SessionManager {
54
54
  sessionId: taskId,
55
55
  knowhowMessageId: taskInfo.knowhowMessageId,
56
56
  knowhowTaskId: taskInfo.knowhowTaskId,
57
+ chatSessionId: taskInfo.chatSessionId,
57
58
  taskId,
58
59
  agentName: taskInfo.agentName,
59
60
  initialInput: taskInfo.initialInput,
@@ -99,6 +100,7 @@ export class SessionManager {
99
100
  // Update Knowhow task fields if they exist in TaskInfo
100
101
  session.knowhowMessageId = taskInfo.knowhowMessageId;
101
102
  session.knowhowTaskId = taskInfo.knowhowTaskId;
103
+ session.chatSessionId = taskInfo.chatSessionId;
102
104
  }
103
105
 
104
106
  fs.writeFileSync(sessionPath, JSON.stringify(session, null, 2));