@tyvm/knowhow 0.0.103 → 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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/clients/index.ts +100 -6
  3. package/src/clients/pricing/index.ts +2 -0
  4. package/src/clients/pricing/models.ts +252 -0
  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/fileSync.ts +15 -2
  9. package/src/hashes.ts +33 -0
  10. package/src/services/AgentSyncFs.ts +20 -12
  11. package/src/services/SyncedAgentWatcher.ts +13 -298
  12. package/src/services/index.ts +1 -0
  13. package/src/services/watchers/FsSyncer.ts +155 -0
  14. package/src/services/watchers/RemoteSyncer.ts +153 -0
  15. package/src/services/watchers/index.ts +2 -0
  16. package/src/types.ts +2 -2
  17. package/ts_build/package.json +1 -1
  18. package/ts_build/src/clients/index.d.ts +8 -0
  19. package/ts_build/src/clients/index.js +57 -2
  20. package/ts_build/src/clients/index.js.map +1 -1
  21. package/ts_build/src/clients/pricing/index.d.ts +2 -0
  22. package/ts_build/src/clients/pricing/index.js +8 -1
  23. package/ts_build/src/clients/pricing/index.js.map +1 -1
  24. package/ts_build/src/clients/pricing/models.d.ts +8 -0
  25. package/ts_build/src/clients/pricing/models.js +173 -0
  26. package/ts_build/src/clients/pricing/models.js.map +1 -0
  27. package/ts_build/src/clients/pricing/types.d.ts +21 -0
  28. package/ts_build/src/clients/pricing/types.js +3 -0
  29. package/ts_build/src/clients/pricing/types.js.map +1 -0
  30. package/ts_build/src/clients/pricing/xai.d.ts +3 -8
  31. package/ts_build/src/clients/pricing/xai.js +4 -4
  32. package/ts_build/src/clients/pricing/xai.js.map +1 -1
  33. package/ts_build/src/clients/xai.d.ts +9 -5
  34. package/ts_build/src/clients/xai.js +2 -2
  35. package/ts_build/src/clients/xai.js.map +1 -1
  36. package/ts_build/src/fileSync.js +8 -1
  37. package/ts_build/src/fileSync.js.map +1 -1
  38. package/ts_build/src/hashes.d.ts +2 -0
  39. package/ts_build/src/hashes.js +23 -0
  40. package/ts_build/src/hashes.js.map +1 -1
  41. package/ts_build/src/services/AgentSyncFs.d.ts +5 -3
  42. package/ts_build/src/services/AgentSyncFs.js +17 -11
  43. package/ts_build/src/services/AgentSyncFs.js.map +1 -1
  44. package/ts_build/src/services/SyncedAgentWatcher.d.ts +0 -51
  45. package/ts_build/src/services/SyncedAgentWatcher.js +1 -282
  46. package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
  47. package/ts_build/src/services/index.d.ts +1 -0
  48. package/ts_build/src/services/index.js +1 -0
  49. package/ts_build/src/services/index.js.map +1 -1
  50. package/ts_build/src/services/watchers/FsSyncer.d.ts +27 -0
  51. package/ts_build/src/services/watchers/FsSyncer.js +135 -0
  52. package/ts_build/src/services/watchers/FsSyncer.js.map +1 -0
  53. package/ts_build/src/services/watchers/RemoteSyncer.d.ts +28 -0
  54. package/ts_build/src/services/watchers/RemoteSyncer.js +126 -0
  55. package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -0
  56. package/ts_build/src/services/watchers/index.d.ts +2 -0
  57. package/ts_build/src/services/watchers/index.js +19 -0
  58. package/ts_build/src/services/watchers/index.js.map +1 -0
  59. package/ts_build/src/types.d.ts +2 -2
  60. package/ts_build/src/types.js +2 -2
  61. package/ts_build/src/types.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.103",
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,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 "./models";
6
+ export type { ModelCatalogEntry, ModelType, ModelPricing as CatalogModelPricing } from "./types";
@@ -0,0 +1,252 @@
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
+ import { ModelPricing, ModelType, ModelCatalogEntry } from "./types";
16
+
17
+ export { ModelPricing, ModelType, ModelCatalogEntry };
18
+
19
+ // ─── Platform markup ──────────────────────────────────────────────────────────
20
+
21
+ /** 2.5% platform markup applied on top of all provider base rates */
22
+ export const USAGE_MARKUP_PERCENT = 2.5 / 100;
23
+
24
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
25
+
26
+ function completion(
27
+ id: string,
28
+ provider: string,
29
+ displayName: string,
30
+ pricingOverride?: Partial<ModelPricing>
31
+ ): ModelCatalogEntry {
32
+ const base =
33
+ (OpenAiTextPricing as Record<string, Partial<ModelPricing>>)[id] ||
34
+ (AnthropicTextPricing as Record<string, Partial<ModelPricing>>)[id] ||
35
+ (GeminiPricing as Record<string, Partial<ModelPricing>>)[id] ||
36
+ (XaiTextPricing as Record<string, Partial<ModelPricing>>)[id] ||
37
+ {};
38
+ return {
39
+ id,
40
+ provider,
41
+ type: "completion",
42
+ displayName,
43
+ pricing: {
44
+ input: 0,
45
+ output: 0,
46
+ ...base,
47
+ ...pricingOverride,
48
+ },
49
+ };
50
+ }
51
+
52
+ function embedding(
53
+ id: string,
54
+ provider: string,
55
+ displayName: string,
56
+ input: number
57
+ ): ModelCatalogEntry {
58
+ return {
59
+ id,
60
+ provider,
61
+ type: "embedding",
62
+ displayName,
63
+ pricing: { input, output: 0 },
64
+ };
65
+ }
66
+
67
+ function image(
68
+ id: string,
69
+ provider: string,
70
+ displayName: string,
71
+ pricing: Partial<ModelPricing>
72
+ ): ModelCatalogEntry {
73
+ return {
74
+ id,
75
+ provider,
76
+ type: "image",
77
+ displayName,
78
+ pricing: { input: 0, output: 0, ...pricing },
79
+ };
80
+ }
81
+
82
+ function video(
83
+ id: string,
84
+ provider: string,
85
+ displayName: string,
86
+ video_generation: number
87
+ ): ModelCatalogEntry {
88
+ return {
89
+ id,
90
+ provider,
91
+ type: "video",
92
+ displayName,
93
+ pricing: { input: 0, output: 0, video_generation },
94
+ };
95
+ }
96
+
97
+ function audio(
98
+ id: string,
99
+ provider: string,
100
+ displayName: string,
101
+ pricing: Partial<ModelPricing>
102
+ ): ModelCatalogEntry {
103
+ return {
104
+ id,
105
+ provider,
106
+ type: "audio",
107
+ displayName,
108
+ pricing: { input: 0, output: 0, ...pricing },
109
+ };
110
+ }
111
+
112
+ function transaction(
113
+ id: string,
114
+ provider: string,
115
+ displayName: string,
116
+ pricing: Partial<ModelPricing>
117
+ ): ModelCatalogEntry {
118
+ return {
119
+ id,
120
+ provider,
121
+ type: "transaction",
122
+ displayName,
123
+ pricing: { input: 0, output: 0, ...pricing },
124
+ };
125
+ }
126
+
127
+ // ─── OpenAI ───────────────────────────────────────────────────────────────────
128
+
129
+ export const OPENAI_MODEL_CATALOG: ModelCatalogEntry[] = [
130
+ // Completion
131
+ completion(Models.openai.GPT_4o, "openai", "GPT-4o"),
132
+ completion(Models.openai.GPT_4o_Mini, "openai", "GPT-4o Mini"),
133
+ completion(Models.openai.GPT_41, "openai", "GPT-4.1"),
134
+ completion(Models.openai.GPT_41_Mini, "openai", "GPT-4.1 Mini"),
135
+ completion(Models.openai.GPT_41_Nano, "openai", "GPT-4.1 Nano"),
136
+ completion(Models.openai.GPT_45, "openai", "GPT-4.5 Preview"),
137
+ completion(Models.openai.o1, "openai", "o1"),
138
+ completion(Models.openai.o1_Mini, "openai", "o1 Mini"),
139
+ completion(Models.openai.o3, "openai", "o3"),
140
+ completion(Models.openai.o3_Mini, "openai", "o3 Mini"),
141
+ completion(Models.openai.o4_Mini, "openai", "o4 Mini"),
142
+ // Embedding
143
+ embedding(EmbeddingModels.openai.EmbeddingAda2, "openai", "Embedding Ada 002", OpenAiTextPricing[EmbeddingModels.openai.EmbeddingAda2]?.input ?? 0.1),
144
+ embedding(EmbeddingModels.openai.EmbeddingLarge3, "openai", "Embedding 3 Large", OpenAiTextPricing[EmbeddingModels.openai.EmbeddingLarge3]?.input ?? 0.13),
145
+ embedding(EmbeddingModels.openai.EmbeddingSmall3, "openai", "Embedding 3 Small", OpenAiTextPricing[EmbeddingModels.openai.EmbeddingSmall3]?.input ?? 0.02),
146
+ // Image generation
147
+ image(Models.openai.DALL_E_3, "openai", "DALL-E 3", { image_generation: 0.04 }),
148
+ image(Models.openai.DALL_E_2, "openai", "DALL-E 2", { image_generation: 0.02 }),
149
+ 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 }),
150
+ image(Models.openai.GPT_Image_1_Mini, "openai", "GPT Image 1 Mini", { input: OpenAiTextPricing[Models.openai.GPT_Image_1_Mini]?.input ?? 2.0 }),
151
+ // Video generation
152
+ video(Models.openai.Sora, "openai", "Sora", 0.012),
153
+ video(Models.openai.Sora_2, "openai", "Sora 2", 0.015),
154
+ // Audio
155
+ audio(Models.openai.TTS_1, "openai", "TTS-1", { input: 15.0 }),
156
+ audio(Models.openai.Whisper_1, "openai", "Whisper 1", { input: 0.006 }),
157
+ // Transaction / Search
158
+ 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 }),
159
+ 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 }),
160
+ ];
161
+
162
+ // ─── Anthropic ────────────────────────────────────────────────────────────────
163
+
164
+ export const ANTHROPIC_MODEL_CATALOG: ModelCatalogEntry[] = [
165
+ completion(Models.anthropic.Opus4_5, "anthropic", "Claude Opus 4.5"),
166
+ completion(Models.anthropic.Sonnet4_5, "anthropic", "Claude Sonnet 4.5"),
167
+ completion(Models.anthropic.Opus4, "anthropic", "Claude Opus 4"),
168
+ completion(Models.anthropic.Sonnet4, "anthropic", "Claude Sonnet 4"),
169
+ completion(Models.anthropic.Haiku4_5, "anthropic", "Claude Haiku 4.5"),
170
+ completion(Models.anthropic.Sonnet3_7, "anthropic", "Claude Sonnet 3.7"),
171
+ completion(Models.anthropic.Sonnet3_5, "anthropic", "Claude Sonnet 3.5"),
172
+ completion(Models.anthropic.Haiku3, "anthropic", "Claude Haiku 3"),
173
+ completion(Models.anthropic.Opus3, "anthropic", "Claude Opus 3"),
174
+ ];
175
+
176
+ // ─── Google ───────────────────────────────────────────────────────────────────
177
+
178
+ export const GOOGLE_MODEL_CATALOG: ModelCatalogEntry[] = [
179
+ // Completion
180
+ completion(Models.google.Gemini_25_Pro, "google", "Gemini 2.5 Pro"),
181
+ completion(Models.google.Gemini_25_Flash, "google", "Gemini 2.5 Flash"),
182
+ completion(Models.google.Gemini_25_Flash_Lite, "google", "Gemini 2.5 Flash Lite"),
183
+ completion(Models.google.Gemini_20_Flash, "google", "Gemini 2.0 Flash"),
184
+ completion(Models.google.Gemini_15_Pro, "google", "Gemini 1.5 Pro"),
185
+ completion(Models.google.Gemini_15_Flash, "google", "Gemini 1.5 Flash"),
186
+ completion(Models.google.Gemini_15_Flash_8B, "google", "Gemini 1.5 Flash 8B"),
187
+ // Embedding
188
+ embedding(EmbeddingModels.google.Gemini_Embedding, "google", "Gemini Embedding", GeminiPricing[EmbeddingModels.google.Gemini_Embedding]?.input ?? 0),
189
+ // Image generation
190
+ image(Models.google.Gemini_20_Flash_Preview_Image_Generation, "google", "Gemini 2.0 Flash Image", {
191
+ input: GeminiPricing[Models.google.Gemini_20_Flash_Preview_Image_Generation]?.input ?? 0.1,
192
+ output: GeminiPricing[Models.google.Gemini_20_Flash_Preview_Image_Generation]?.output ?? 0.4,
193
+ image_generation: GeminiPricing[Models.google.Gemini_20_Flash_Preview_Image_Generation]?.image_generation ?? 0.039,
194
+ }),
195
+ image(Models.google.Gemini_25_Flash_Image, "google", "Gemini 2.5 Flash Image", {
196
+ input: GeminiPricing[Models.google.Gemini_25_Flash_Image]?.input ?? 0.3,
197
+ output: GeminiPricing[Models.google.Gemini_25_Flash_Image]?.output ?? 0.039,
198
+ image_generation: GeminiPricing[Models.google.Gemini_25_Flash_Image]?.image_generation ?? 0.039,
199
+ }),
200
+ image(Models.google.Gemini_31_Flash_Image_Preview, "google", "Gemini 3.1 Flash Image", {
201
+ input: GeminiPricing[Models.google.Gemini_31_Flash_Image_Preview]?.input ?? 0.5,
202
+ output: GeminiPricing[Models.google.Gemini_31_Flash_Image_Preview]?.output ?? 3.0,
203
+ image_generation: GeminiPricing[Models.google.Gemini_31_Flash_Image_Preview]?.image_generation ?? 0.045,
204
+ }),
205
+ image(Models.google.Gemini_3_Pro_Image_Preview, "google", "Gemini 3 Pro Image", {
206
+ input: GeminiPricing[Models.google.Gemini_3_Pro_Image_Preview]?.input ?? 2.0,
207
+ output: GeminiPricing[Models.google.Gemini_3_Pro_Image_Preview]?.output ?? 12.0,
208
+ image_generation: GeminiPricing[Models.google.Gemini_3_Pro_Image_Preview]?.image_generation ?? 0.134,
209
+ }),
210
+ image(Models.google.Imagen_3, "google", "Imagen 4", { image_generation: GeminiPricing[Models.google.Imagen_3]?.image_generation ?? 0.04 }),
211
+ image(Models.google.Imagen_4_Fast, "google", "Imagen 4 Fast", { image_generation: GeminiPricing[Models.google.Imagen_4_Fast]?.image_generation ?? 0.02 }),
212
+ image(Models.google.Imagen_4_Ultra, "google", "Imagen 4 Ultra", { image_generation: GeminiPricing[Models.google.Imagen_4_Ultra]?.image_generation ?? 0.06 }),
213
+ // Video generation
214
+ video(Models.google.Veo_2, "google", "Veo 2", GeminiPricing[Models.google.Veo_2]?.video_generation ?? 0.35),
215
+ video(Models.google.Veo_3, "google", "Veo 3", GeminiPricing[Models.google.Veo_3]?.video_generation ?? 0.4),
216
+ video(Models.google.Veo_3_Fast, "google", "Veo 3 Fast", GeminiPricing[Models.google.Veo_3_Fast]?.video_generation ?? 0.1),
217
+ // Audio (TTS)
218
+ audio(Models.google.Gemini_25_Flash_TTS, "google", "Gemini 2.5 Flash TTS", {
219
+ input: GeminiPricing[Models.google.Gemini_25_Flash_TTS]?.input ?? 0.5,
220
+ output_audio: GeminiPricing[Models.google.Gemini_25_Flash_TTS]?.output_audio ?? 10.0,
221
+ output: GeminiPricing[Models.google.Gemini_25_Flash_TTS]?.output_audio ?? 10.0,
222
+ }),
223
+ audio(Models.google.Gemini_25_Pro_TTS, "google", "Gemini 2.5 Pro TTS", {
224
+ input: GeminiPricing[Models.google.Gemini_25_Pro_TTS]?.input ?? 1.0,
225
+ output_audio: GeminiPricing[Models.google.Gemini_25_Pro_TTS]?.output_audio ?? 20.0,
226
+ output: GeminiPricing[Models.google.Gemini_25_Pro_TTS]?.output_audio ?? 20.0,
227
+ }),
228
+ ];
229
+
230
+ // ─── xAI ──────────────────────────────────────────────────────────────────────
231
+
232
+ export const XAI_MODEL_CATALOG: ModelCatalogEntry[] = [
233
+ completion(Models.xai.Grok4, "xai", "Grok 4"),
234
+ completion(Models.xai.Grok3Beta, "xai", "Grok 3 Beta"),
235
+ completion(Models.xai.Grok3MiniBeta, "xai", "Grok 3 Mini Beta"),
236
+ completion(Models.xai.Grok3FastBeta, "xai", "Grok 3 Fast Beta"),
237
+ completion(Models.xai.Grok21212, "xai", "Grok 2"),
238
+ // Image generation
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 }),
241
+ // Video generation
242
+ video(Models.xai.GrokImagineVideo, "xai", "Grok Imagine Video", XaiVideoPricing["grok-imagine-video"]?.video_generation ?? 0.05),
243
+ ];
244
+
245
+ // ─── Combined catalog ─────────────────────────────────────────────────────────
246
+
247
+ export const ALL_MODEL_CATALOG: ModelCatalogEntry[] = [
248
+ ...OPENAI_MODEL_CATALOG,
249
+ ...ANTHROPIC_MODEL_CATALOG,
250
+ ...GOOGLE_MODEL_CATALOG,
251
+ ...XAI_MODEL_CATALOG,
252
+ ];
@@ -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 {
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,13 +18,17 @@ 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;
@@ -38,7 +42,7 @@ export class AgentSyncFs {
38
42
 
39
43
  constructor() {
40
44
  // Start cleanup process when created
41
- this.startCleanupProcess();
45
+ AgentSyncFs.startSharedCleanupProcess();
42
46
  }
43
47
 
44
48
  /**
@@ -372,9 +376,9 @@ export class AgentSyncFs {
372
376
  /**
373
377
  * Clean up old task directories (older than 3 days)
374
378
  */
375
- private async cleanupOldTasks(): Promise<void> {
379
+ private static async cleanupOldTasks(): Promise<void> {
376
380
  try {
377
- const agentsPath = this.basePath;
381
+ const agentsPath = AgentSyncFs.sharedBasePath;
378
382
 
379
383
  // Check if directory exists
380
384
  try {
@@ -414,23 +418,27 @@ export class AgentSyncFs {
414
418
  /**
415
419
  * Start periodic cleanup process
416
420
  */
417
- private startCleanupProcess(): void {
418
- // Run cleanup every hour
419
- this.cleanupInterval = setInterval(() => {
420
- 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();
421
428
  }, 60 * 60 * 1000);
422
429
 
423
430
  // Also run once on startup
424
- this.cleanupOldTasks();
431
+ AgentSyncFs.cleanupOldTasks();
425
432
  }
426
433
 
427
434
  /**
428
435
  * Stop cleanup process
429
436
  */
430
437
  stopCleanup(): void {
431
- if (this.cleanupInterval) {
432
- clearInterval(this.cleanupInterval);
433
- this.cleanupInterval = null;
438
+ if (AgentSyncFs.sharedCleanupInterval) {
439
+ clearInterval(AgentSyncFs.sharedCleanupInterval);
440
+ AgentSyncFs.sharedCleanupInterval = null;
441
+ AgentSyncFs.cleanupStarted = false;
434
442
  }
435
443
  }
436
444