@tyvm/knowhow 0.0.118 → 0.0.120
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -3
- package/src/agents/base/base.ts +72 -9
- package/src/agents/researcher/researcher.ts +9 -2
- package/src/agents/tools/list.ts +13 -2
- package/src/agents/tools/patch.ts +318 -32
- package/src/agents/tools/readFile.ts +48 -5
- package/src/chat/modules/AgentModule.ts +12 -0
- package/src/cli.ts +2 -0
- package/src/clients/anthropic.ts +12 -2
- package/src/clients/contextLimits.ts +77 -0
- package/src/commands/convert.ts +291 -0
- package/src/conversion.ts +15 -61
- package/src/index.ts +3 -0
- package/src/processors/CustomVariables.ts +45 -20
- package/src/processors/TokenCompressor.ts +95 -9
- package/src/services/AgentSyncFs.ts +26 -4
- package/src/services/AgentSyncKnowhowWeb.ts +26 -4
- package/src/services/SyncedAgentWatcher.ts +8 -0
- package/src/services/conversion/ConversionService.ts +763 -0
- package/src/services/conversion/index.ts +2 -0
- package/src/services/conversion/types.ts +79 -0
- package/src/services/index.ts +8 -1
- package/src/services/modules/types.ts +2 -0
- package/src/services/watchers/FsSyncer.ts +6 -0
- package/src/services/watchers/RemoteSyncer.ts +5 -0
- package/tests/agents/tools/readFile.test.ts +88 -0
- package/tests/clients/AIClient.test.ts +5 -0
- package/tests/clients/contextLimits.test.ts +71 -0
- package/tests/patching/patchFileOutput.test.ts +217 -0
- package/tests/patching/regression-2026.test.ts +278 -0
- package/tests/processors/CustomVariables.test.ts +4 -4
- package/tests/processors/TokenCompressor.test.ts +59 -1
- package/tests/processors/tools/grepToolResponse.test.ts +72 -0
- package/tests/services/ConversionService.test.ts +154 -0
- package/tests/test.spec.ts +1 -1
- package/tests/unit/clients/AIClient.test.ts +8 -0
- package/ts_build/package.json +1 -3
- package/ts_build/src/agents/base/base.d.ts +3 -0
- package/ts_build/src/agents/base/base.js +46 -3
- package/ts_build/src/agents/base/base.js.map +1 -1
- package/ts_build/src/agents/researcher/researcher.js +5 -2
- package/ts_build/src/agents/researcher/researcher.js.map +1 -1
- package/ts_build/src/agents/tools/list.js +10 -2
- package/ts_build/src/agents/tools/list.js.map +1 -1
- package/ts_build/src/agents/tools/patch.js +202 -24
- package/ts_build/src/agents/tools/patch.js.map +1 -1
- package/ts_build/src/agents/tools/readFile.d.ts +1 -1
- package/ts_build/src/agents/tools/readFile.js +17 -4
- package/ts_build/src/agents/tools/readFile.js.map +1 -1
- package/ts_build/src/chat/modules/AgentModule.js +12 -0
- package/ts_build/src/chat/modules/AgentModule.js.map +1 -1
- package/ts_build/src/cli.js +2 -0
- package/ts_build/src/cli.js.map +1 -1
- package/ts_build/src/clients/anthropic.js +7 -2
- package/ts_build/src/clients/anthropic.js.map +1 -1
- package/ts_build/src/clients/contextLimits.js +70 -0
- package/ts_build/src/clients/contextLimits.js.map +1 -1
- package/ts_build/src/commands/convert.d.ts +2 -0
- package/ts_build/src/commands/convert.js +275 -0
- package/ts_build/src/commands/convert.js.map +1 -0
- package/ts_build/src/conversion.js +6 -38
- package/ts_build/src/conversion.js.map +1 -1
- package/ts_build/src/index.d.ts +2 -0
- package/ts_build/src/index.js +4 -1
- package/ts_build/src/index.js.map +1 -1
- package/ts_build/src/processors/CustomVariables.js +14 -12
- package/ts_build/src/processors/CustomVariables.js.map +1 -1
- package/ts_build/src/processors/TokenCompressor.d.ts +2 -0
- package/ts_build/src/processors/TokenCompressor.js +57 -7
- package/ts_build/src/processors/TokenCompressor.js.map +1 -1
- package/ts_build/src/services/AgentSyncFs.d.ts +1 -0
- package/ts_build/src/services/AgentSyncFs.js +21 -4
- package/ts_build/src/services/AgentSyncFs.js.map +1 -1
- package/ts_build/src/services/AgentSyncKnowhowWeb.d.ts +1 -0
- package/ts_build/src/services/AgentSyncKnowhowWeb.js +21 -4
- package/ts_build/src/services/AgentSyncKnowhowWeb.js.map +1 -1
- package/ts_build/src/services/SyncedAgentWatcher.d.ts +3 -0
- package/ts_build/src/services/SyncedAgentWatcher.js +4 -0
- package/ts_build/src/services/SyncedAgentWatcher.js.map +1 -1
- package/ts_build/src/services/conversion/ConversionService.d.ts +18 -0
- package/ts_build/src/services/conversion/ConversionService.js +585 -0
- package/ts_build/src/services/conversion/ConversionService.js.map +1 -0
- package/ts_build/src/services/conversion/index.d.ts +2 -0
- package/ts_build/src/services/conversion/index.js +19 -0
- package/ts_build/src/services/conversion/index.js.map +1 -0
- package/ts_build/src/services/conversion/types.d.ts +49 -0
- package/ts_build/src/services/conversion/types.js +3 -0
- package/ts_build/src/services/conversion/types.js.map +1 -0
- package/ts_build/src/services/index.d.ts +3 -0
- package/ts_build/src/services/index.js +6 -1
- package/ts_build/src/services/index.js.map +1 -1
- package/ts_build/src/services/modules/index.d.ts +2 -0
- package/ts_build/src/services/modules/types.d.ts +2 -0
- package/ts_build/src/services/watchers/FsSyncer.d.ts +1 -0
- package/ts_build/src/services/watchers/FsSyncer.js +5 -0
- package/ts_build/src/services/watchers/FsSyncer.js.map +1 -1
- package/ts_build/src/services/watchers/RemoteSyncer.d.ts +1 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js +4 -0
- package/ts_build/src/services/watchers/RemoteSyncer.js.map +1 -1
- package/ts_build/tests/agents/tools/readFile.test.d.ts +1 -0
- package/ts_build/tests/agents/tools/readFile.test.js +90 -0
- package/ts_build/tests/agents/tools/readFile.test.js.map +1 -0
- package/ts_build/tests/clients/AIClient.test.js +1 -0
- package/ts_build/tests/clients/AIClient.test.js.map +1 -1
- package/ts_build/tests/clients/contextLimits.test.d.ts +1 -0
- package/ts_build/tests/clients/contextLimits.test.js +57 -0
- package/ts_build/tests/clients/contextLimits.test.js.map +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.d.ts +1 -0
- package/ts_build/tests/patching/patchFileOutput.test.js +187 -0
- package/ts_build/tests/patching/patchFileOutput.test.js.map +1 -0
- package/ts_build/tests/patching/regression-2026.test.js +214 -0
- package/ts_build/tests/patching/regression-2026.test.js.map +1 -1
- package/ts_build/tests/processors/CustomVariables.test.js +4 -4
- package/ts_build/tests/processors/CustomVariables.test.js.map +1 -1
- package/ts_build/tests/processors/TokenCompressor.test.js +37 -1
- package/ts_build/tests/processors/TokenCompressor.test.js.map +1 -1
- package/ts_build/tests/processors/tools/grepToolResponse.test.d.ts +1 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js +40 -0
- package/ts_build/tests/processors/tools/grepToolResponse.test.js.map +1 -0
- package/ts_build/tests/services/ConversionService.test.d.ts +1 -0
- package/ts_build/tests/services/ConversionService.test.js +154 -0
- package/ts_build/tests/services/ConversionService.test.js.map +1 -0
- package/ts_build/tests/test.spec.js +1 -1
- package/ts_build/tests/test.spec.js.map +1 -1
- package/ts_build/tests/unit/clients/AIClient.test.js +3 -0
- package/ts_build/tests/unit/clients/AIClient.test.js.map +1 -1
|
@@ -3,7 +3,6 @@ import { fileExists } from "../../utils";
|
|
|
3
3
|
import { services, ToolsService } from "../../services";
|
|
4
4
|
import { getConfiguredEmbeddings } from "../../embeddings";
|
|
5
5
|
import { fileSearch } from "./fileSearch";
|
|
6
|
-
import { createPatch } from "diff";
|
|
7
6
|
|
|
8
7
|
/*
|
|
9
8
|
*export function readFile(filePath: string): string {
|
|
@@ -18,7 +17,23 @@ import { createPatch } from "diff";
|
|
|
18
17
|
*}
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Reads the contents of a file and returns them as plain text.
|
|
22
|
+
*
|
|
23
|
+
* Optionally accepts a 1-based, inclusive line range so callers can pull just
|
|
24
|
+
* the region they care about in a single call. When a range is supplied the
|
|
25
|
+
* returned content is prefixed with real source line numbers so the output can
|
|
26
|
+
* be mapped straight back to an editable location.
|
|
27
|
+
*
|
|
28
|
+
* @param filePath The path to the file to read
|
|
29
|
+
* @param fromLine Optional 1-based start line (inclusive)
|
|
30
|
+
* @param toLine Optional 1-based end line (inclusive)
|
|
31
|
+
*/
|
|
32
|
+
export async function readFile(
|
|
33
|
+
filePath: string,
|
|
34
|
+
fromLine?: number,
|
|
35
|
+
toLine?: number
|
|
36
|
+
): Promise<string> {
|
|
22
37
|
// Get context from bound ToolsService
|
|
23
38
|
const toolService = (
|
|
24
39
|
this instanceof ToolsService ? this : services().Tools
|
|
@@ -48,9 +63,9 @@ export async function readFile(filePath: string): Promise<string> {
|
|
|
48
63
|
}
|
|
49
64
|
|
|
50
65
|
const text = fs.readFileSync(filePath, "utf8");
|
|
51
|
-
const patch = createPatch(filePath, "", text);
|
|
52
66
|
|
|
53
|
-
// Emit post-read non-blocking event
|
|
67
|
+
// Emit post-read non-blocking event with the full file content so listeners
|
|
68
|
+
// (e.g. indexers) always see the complete file regardless of any range slice.
|
|
54
69
|
if (context.Events) {
|
|
55
70
|
await context.Events.emitNonBlocking("file:post-read", {
|
|
56
71
|
filePath,
|
|
@@ -58,5 +73,33 @@ export async function readFile(filePath: string): Promise<string> {
|
|
|
58
73
|
});
|
|
59
74
|
}
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
const hasRange =
|
|
77
|
+
typeof fromLine === "number" || typeof toLine === "number";
|
|
78
|
+
|
|
79
|
+
if (!hasRange) {
|
|
80
|
+
return text;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Build a ranged read with real, 1-based source line numbers.
|
|
84
|
+
const lines = text.split("\n");
|
|
85
|
+
const totalLines = lines.length;
|
|
86
|
+
|
|
87
|
+
const start = Math.max(1, typeof fromLine === "number" ? fromLine : 1);
|
|
88
|
+
const end = Math.min(
|
|
89
|
+
totalLines,
|
|
90
|
+
typeof toLine === "number" ? toLine : totalLines
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
if (start > end) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Invalid line range for ${filePath}: fromLine (${start}) is greater than toLine (${end}). File has ${totalLines} lines.`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const numbered = [];
|
|
100
|
+
for (let i = start; i <= end; i++) {
|
|
101
|
+
numbered.push(`${i}: ${lines[i - 1]}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return numbered.join("\n");
|
|
62
105
|
}
|
|
@@ -122,6 +122,18 @@ export class AgentModule extends BaseChatModule {
|
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
name: "poke",
|
|
127
|
+
description: "Interrupt the agent's current tool call or AI completion, so it can continue with the next step",
|
|
128
|
+
modes: ["agent:attached"],
|
|
129
|
+
handler: async (args: string[]): Promise<void> => {
|
|
130
|
+
if (this.attachedAgent) {
|
|
131
|
+
const message = args.length > 0 ? args.join(" ") : undefined;
|
|
132
|
+
this.attachedAgent.interrupt(message);
|
|
133
|
+
console.log("Agent interrupted — it will continue with the next step.");
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
},
|
|
125
137
|
{
|
|
126
138
|
name: "detach",
|
|
127
139
|
description: "Detach from the currently attached agent",
|
package/src/cli.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
addChatCommand,
|
|
37
37
|
addGithubCredentialsCommand,
|
|
38
38
|
} from "./commands/misc";
|
|
39
|
+
import { addConvertCommand } from "./commands/convert";
|
|
39
40
|
|
|
40
41
|
// Handle unhandled promise rejections gracefully — particularly from MCP SDK
|
|
41
42
|
// which fires errors via event emitters that can bypass Promise.allSettled.
|
|
@@ -98,6 +99,7 @@ async function main() {
|
|
|
98
99
|
addGithubCredentialsCommand(program);
|
|
99
100
|
addModulesCommand(program);
|
|
100
101
|
addMcpCommands(program);
|
|
102
|
+
addConvertCommand(program);
|
|
101
103
|
|
|
102
104
|
// Load global modules early (before parse) so they can register CLI subcommands.
|
|
103
105
|
// We pass only the Program in context — no services are spun up at this stage.
|
package/src/clients/anthropic.ts
CHANGED
|
@@ -329,12 +329,22 @@ export class GenericAnthropicClient implements GenericClient {
|
|
|
329
329
|
},
|
|
330
330
|
} as Anthropic.ContentBlockParam;
|
|
331
331
|
} else {
|
|
332
|
+
// Data URL or raw base64. Anthropic's base64 source expects the
|
|
333
|
+
// media_type separately and the data WITHOUT the
|
|
334
|
+
// "data:<mime>;base64," prefix. Parse it out here.
|
|
335
|
+
const dataUrlMatch = e.image_url.url.match(
|
|
336
|
+
/^data:([^;]+);base64,(.*)$/s
|
|
337
|
+
);
|
|
338
|
+
const mediaType = dataUrlMatch ? dataUrlMatch[1] : "image/jpeg";
|
|
339
|
+
const data = dataUrlMatch
|
|
340
|
+
? dataUrlMatch[2]
|
|
341
|
+
: e.image_url.url.replace(/^data:[^,]*,/, "");
|
|
332
342
|
return {
|
|
333
343
|
type: "image",
|
|
334
344
|
source: {
|
|
335
345
|
type: "base64" as const,
|
|
336
|
-
media_type:
|
|
337
|
-
data
|
|
346
|
+
media_type: mediaType,
|
|
347
|
+
data,
|
|
338
348
|
},
|
|
339
349
|
} as Anthropic.ContentBlockParam;
|
|
340
350
|
}
|
|
@@ -10,6 +10,7 @@ import { Models, EmbeddingModels } from "../types";
|
|
|
10
10
|
*/
|
|
11
11
|
export const ContextLimits: Record<string, number> = {
|
|
12
12
|
// ─── OpenAI ───────────────────────────────────────────────────────────────
|
|
13
|
+
[Models.openai.GPT_55]: 1_000_000,
|
|
13
14
|
[Models.openai.GPT_54]: 1_000_000,
|
|
14
15
|
[Models.openai.GPT_54_Mini]: 400_000,
|
|
15
16
|
[Models.openai.GPT_54_Nano]: 400_000,
|
|
@@ -40,8 +41,49 @@ export const ContextLimits: Record<string, number> = {
|
|
|
40
41
|
[Models.openai.o3_Pro]: 200_000,
|
|
41
42
|
[Models.openai.o3_Mini]: 200_000,
|
|
42
43
|
[Models.openai.o4_Mini]: 200_000,
|
|
44
|
+
// ─── OpenAI (aliases / deprecated / non-text) ─────────────────────────────
|
|
45
|
+
[Models.openai.GPT_55_Pro]: 1_000_000,
|
|
46
|
+
[Models.openai.GPT_53_Codex_Spark]: 1_000_000,
|
|
47
|
+
[Models.openai.GPT_52_Chat]: 1_000_000,
|
|
48
|
+
[Models.openai.GPT_52_Codex]: 1_000_000,
|
|
49
|
+
[Models.openai.GPT_52_Pro]: 1_000_000,
|
|
50
|
+
[Models.openai.GPT_51_Chat]: 1_000_000,
|
|
51
|
+
[Models.openai.GPT_51_Codex]: 1_000_000,
|
|
52
|
+
[Models.openai.GPT_51_Codex_Max]: 1_000_000,
|
|
53
|
+
[Models.openai.GPT_51_Codex_Mini]: 1_000_000,
|
|
54
|
+
[Models.openai.GPT_5_Pro]: 1_000_000,
|
|
55
|
+
[Models.openai.GPT_5_Chat]: 1_000_000,
|
|
56
|
+
[Models.openai.GPT_5_Codex]: 1_000_000,
|
|
57
|
+
[Models.openai.o1_Preview]: 128_000,
|
|
58
|
+
[Models.openai.o3_Deep_Research]: 200_000,
|
|
59
|
+
[Models.openai.o4_Mini_Deep_Research]: 200_000,
|
|
60
|
+
[Models.openai.GPT_4o_Transcribe]: 128_000,
|
|
61
|
+
[Models.openai.GPT_4o_Mini_Transcribe]: 128_000,
|
|
62
|
+
[Models.openai.GPT_Realtime_15]: 128_000,
|
|
63
|
+
[Models.openai.GPT_Realtime_Mini]: 128_000,
|
|
64
|
+
[Models.openai.GPT_4o_2024_05_13]: 128_000,
|
|
65
|
+
[Models.openai.GPT_4o_2024_11_20]: 128_000,
|
|
66
|
+
[Models.openai.GPT_35_Turbo]: 16_385,
|
|
67
|
+
[Models.openai.GPT_4]: 8_192,
|
|
68
|
+
[Models.openai.GPT_4_Turbo]: 128_000,
|
|
69
|
+
// OpenAI image/video/audio models — no text context window
|
|
70
|
+
[Models.openai.GPT_Image_2]: 0,
|
|
71
|
+
[Models.openai.GPT_Image_15]: 0,
|
|
72
|
+
[Models.openai.GPT_Image_1]: 0,
|
|
73
|
+
[Models.openai.GPT_Image_1_Mini]: 0,
|
|
74
|
+
[Models.openai.ChatGPT_Image]: 0,
|
|
75
|
+
[Models.openai.TTS_1]: 0,
|
|
76
|
+
[Models.openai.Whisper_1]: 0,
|
|
77
|
+
[Models.openai.DALL_E_3]: 0,
|
|
78
|
+
[Models.openai.DALL_E_2]: 0,
|
|
79
|
+
[Models.openai.Sora]: 0,
|
|
80
|
+
[Models.openai.Sora_2]: 0,
|
|
81
|
+
[Models.openai.Sora_2_Pro]: 0,
|
|
43
82
|
|
|
44
83
|
// ─── Anthropic ────────────────────────────────────────────────────────────
|
|
84
|
+
[Models.anthropic.Opus4_8Fast]: 1_000_000,
|
|
85
|
+
[Models.anthropic.Opus4_8]: 1_000_000,
|
|
86
|
+
[Models.anthropic.Opus4_7]: 1_000_000,
|
|
45
87
|
[Models.anthropic.Opus4_6]: 1_000_000,
|
|
46
88
|
[Models.anthropic.Opus4_6Fast]: 1_000_000,
|
|
47
89
|
[Models.anthropic.Sonnet4_6]: 1_000_000,
|
|
@@ -57,10 +99,18 @@ export const ContextLimits: Record<string, number> = {
|
|
|
57
99
|
[Models.anthropic.Haiku3]: 200_000,
|
|
58
100
|
[Models.anthropic.Haiku3_5]: 200_000,
|
|
59
101
|
|
|
102
|
+
// ─── Anthropic (aliases / deprecated) ────────────────────────────────────
|
|
103
|
+
[Models.anthropic.Sonnet3_5_20240620]: 200_000,
|
|
104
|
+
[Models.anthropic.Haiku3_5_Latest]: 200_000,
|
|
105
|
+
[Models.anthropic.Sonnet3]: 200_000,
|
|
106
|
+
[Models.anthropic.Opus4_0]: 200_000,
|
|
107
|
+
[Models.anthropic.Sonnet4_0]: 200_000,
|
|
108
|
+
|
|
60
109
|
// ─── Google ───────────────────────────────────────────────────────────────
|
|
61
110
|
[Models.google.Gemini_31_Pro_Preview]: 1_000_000,
|
|
62
111
|
[Models.google.Gemini_31_Flash_Image_Preview]: 1_000_000,
|
|
63
112
|
[Models.google.Gemini_31_Flash_Lite_Preview]: 1_000_000,
|
|
113
|
+
[Models.google.Gemini_31_Flash_Live_Preview]: 1_000_000,
|
|
64
114
|
[Models.google.Gemini_3_Flash_Preview]: 1_000_000,
|
|
65
115
|
[Models.google.Gemini_3_Pro_Image_Preview]: 1_000_000,
|
|
66
116
|
[Models.google.Gemini_25_Pro]: 1_000_000,
|
|
@@ -70,9 +120,20 @@ export const ContextLimits: Record<string, number> = {
|
|
|
70
120
|
[Models.google.Gemini_25_Pro_Preview]: 1_000_000,
|
|
71
121
|
[Models.google.Gemini_25_Flash_Image]: 1_000_000,
|
|
72
122
|
[Models.google.Gemini_25_Flash_Live]: 1_000_000,
|
|
123
|
+
[Models.google.Gemini_25_Flash_Preview_0417]: 1_000_000,
|
|
124
|
+
[Models.google.Gemini_25_Flash_Image_Preview]: 1_000_000,
|
|
73
125
|
[Models.google.Gemini_25_Flash_Native_Audio]: 1_000_000,
|
|
74
126
|
[Models.google.Gemini_25_Flash_TTS]: 1_000_000,
|
|
75
127
|
[Models.google.Gemini_25_Pro_TTS]: 1_000_000,
|
|
128
|
+
// Google image/video generation models — no text context window; use 0
|
|
129
|
+
[Models.google.Imagen_3]: 0,
|
|
130
|
+
[Models.google.Imagen_4_Fast]: 0,
|
|
131
|
+
[Models.google.Imagen_4_Ultra]: 0,
|
|
132
|
+
[Models.google.Veo_2]: 0,
|
|
133
|
+
[Models.google.Veo_3]: 0,
|
|
134
|
+
[Models.google.Veo_3_Fast]: 0,
|
|
135
|
+
[Models.google.Veo_3_1]: 0,
|
|
136
|
+
[Models.google.Veo_3_1_Fast]: 0,
|
|
76
137
|
[Models.google.Gemini_20_Flash]: 1_000_000,
|
|
77
138
|
[Models.google.Gemini_20_Flash_Preview_Image_Generation]: 1_000_000,
|
|
78
139
|
[Models.google.Gemini_20_Flash_Live]: 1_000_000,
|
|
@@ -95,6 +156,22 @@ export const ContextLimits: Record<string, number> = {
|
|
|
95
156
|
[Models.xai.Grok3MiniFastBeta]: 131_072,
|
|
96
157
|
[Models.xai.Grok21212]: 131_072,
|
|
97
158
|
[Models.xai.Grok2Vision1212]: 131_072,
|
|
159
|
+
// ─── xAI (aliases / deprecated / image / video) ───────────────────────────
|
|
160
|
+
[Models.xai.Grok2Latest]: 131_072,
|
|
161
|
+
[Models.xai.Grok2VisionLatest]: 131_072,
|
|
162
|
+
[Models.xai.Grok3Latest]: 131_072,
|
|
163
|
+
[Models.xai.Grok3FastLatest]: 131_072,
|
|
164
|
+
[Models.xai.Grok3MiniLatest]: 131_072,
|
|
165
|
+
[Models.xai.Grok3MiniFastLatest]: 131_072,
|
|
166
|
+
[Models.xai.GrokBeta]: 131_072,
|
|
167
|
+
[Models.xai.GrokVisionBeta]: 131_072,
|
|
168
|
+
[Models.xai.Grok4_1_Fast]: 2_000_000,
|
|
169
|
+
[Models.xai.Grok4Fast]: 2_000_000,
|
|
170
|
+
[Models.xai.Grok4FastNonReasoning]: 2_000_000,
|
|
171
|
+
// xAI image/video models — no text context window
|
|
172
|
+
[Models.xai.GrokImagineImage]: 0,
|
|
173
|
+
[Models.xai.GrokImagineVideo]: 0,
|
|
174
|
+
[Models.xai.Grok2Image1212]: 0,
|
|
98
175
|
};
|
|
99
176
|
|
|
100
177
|
/** Default fallback context window limit (tokens) used when a model is not found. */
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import { services } from "../services";
|
|
5
|
+
import { ModulesService } from "../services/modules";
|
|
6
|
+
import { getConfig } from "../config";
|
|
7
|
+
import { Modality, ConvertOptions } from "../services/conversion/types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Load modules from config so that external converters (e.g. pdf-to-img) get
|
|
11
|
+
* registered into ConversionService before we run a conversion.
|
|
12
|
+
*/
|
|
13
|
+
async function setupConversionServices() {
|
|
14
|
+
const config = await getConfig();
|
|
15
|
+
const { Conversion } = services();
|
|
16
|
+
|
|
17
|
+
const allModulePaths = [
|
|
18
|
+
...(config.modules || []),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (allModulePaths.length) {
|
|
22
|
+
const modulesService = new ModulesService();
|
|
23
|
+
await modulesService.loadModulesFrom(
|
|
24
|
+
{ ...config, modules: allModulePaths },
|
|
25
|
+
{
|
|
26
|
+
Conversion,
|
|
27
|
+
Agents: services().Agents,
|
|
28
|
+
Embeddings: services().Embeddings,
|
|
29
|
+
Plugins: services().Plugins,
|
|
30
|
+
Clients: services().Clients,
|
|
31
|
+
Tools: services().Tools,
|
|
32
|
+
Events: services().Events,
|
|
33
|
+
MediaProcessor: services().MediaProcessor,
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { Conversion };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Modality short aliases accepted on the CLI.
|
|
43
|
+
* img, txt, vid, aud are short forms; full names also accepted.
|
|
44
|
+
*/
|
|
45
|
+
const MODALITY_ALIASES: Record<string, Modality> = {
|
|
46
|
+
img: "image",
|
|
47
|
+
image: "image",
|
|
48
|
+
txt: "text",
|
|
49
|
+
text: "text",
|
|
50
|
+
vid: "video",
|
|
51
|
+
video: "video",
|
|
52
|
+
aud: "audio",
|
|
53
|
+
audio: "audio",
|
|
54
|
+
html: "html",
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse a comma-separated modality chain like "img,txt" or "image,text".
|
|
59
|
+
* Returns { via: Modality[], target: Modality }.
|
|
60
|
+
* Single value (e.g. "text") → { via: [], target: "text" }.
|
|
61
|
+
*
|
|
62
|
+
* Examples:
|
|
63
|
+
* "text" → via=[], target="text"
|
|
64
|
+
* "img,txt" → via=["image"], target="text"
|
|
65
|
+
* "image,text"→ via=["image"], target="text"
|
|
66
|
+
*/
|
|
67
|
+
function parseModalityChain(raw: string): { via: Modality[]; target: Modality } {
|
|
68
|
+
const parts = raw.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
69
|
+
const modalities: Modality[] = parts.map((p) => {
|
|
70
|
+
const m = MODALITY_ALIASES[p];
|
|
71
|
+
if (!m) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Unknown modality "${p}". Valid values: text/txt, image/img, audio/aud, video/vid, html`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return m;
|
|
77
|
+
});
|
|
78
|
+
if (modalities.length === 0) throw new Error("--to requires at least one modality");
|
|
79
|
+
const target = modalities[modalities.length - 1];
|
|
80
|
+
const via = modalities.slice(0, -1);
|
|
81
|
+
return { via, target };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function addConvertCommand(program: Command): void {
|
|
85
|
+
program
|
|
86
|
+
.command("convert")
|
|
87
|
+
.description("Convert a file between modalities (pdf, image, audio, video, text)")
|
|
88
|
+
.option("-i, --input <path>", "Source file path (required unless --list)")
|
|
89
|
+
.option("-o, --output <path>", "Output file or directory (optional; defaults to stdout for text)")
|
|
90
|
+
.option(
|
|
91
|
+
"--to <chain>",
|
|
92
|
+
"Target modality or explicit chain. Single: text|txt|image|img|audio|aud|video|vid|html. Chain (comma-separated): img,txt forces pdf→image→text path.",
|
|
93
|
+
"text"
|
|
94
|
+
)
|
|
95
|
+
.option("--from <modality>", "Override inferred input modality")
|
|
96
|
+
.option("--prefer <names>", "Comma-separated list of preferred converter names (used when --to is a single modality)")
|
|
97
|
+
.option("--force", "Ignore cache and re-run converters")
|
|
98
|
+
.option("--model <name>", "Model to use (injected into converterOptions for all converters, e.g. tts-1, dall-e-3, gpt-4o)")
|
|
99
|
+
.option("--start-page <n>", "First page (documents)", parseInt)
|
|
100
|
+
.option("--end-page <n>", "Last page (documents)", parseInt)
|
|
101
|
+
.option("--start-line <n>", "First line (text output)", parseInt)
|
|
102
|
+
.option("--end-line <n>", "Last line (text output)", parseInt)
|
|
103
|
+
.option("--start-time <n>", "Start time in seconds (audio/video)", parseFloat)
|
|
104
|
+
.option("--end-time <n>", "End time in seconds (audio/video)", parseFloat)
|
|
105
|
+
.option("--opt <spec>", "Per-converter option in format name.key=value (repeatable)", collect, [])
|
|
106
|
+
.option("--list", "List all registered converters and exit")
|
|
107
|
+
.option("--json", "Emit machine-readable JSON result")
|
|
108
|
+
.action(async (opts) => {
|
|
109
|
+
try {
|
|
110
|
+
const { Conversion } = await setupConversionServices();
|
|
111
|
+
|
|
112
|
+
// --list mode: print registered converters
|
|
113
|
+
if (opts.list) {
|
|
114
|
+
const converters = Conversion.list();
|
|
115
|
+
if (opts.json) {
|
|
116
|
+
console.log(JSON.stringify(converters.map(c => ({
|
|
117
|
+
name: c.name,
|
|
118
|
+
inputExts: c.inputExts,
|
|
119
|
+
inputModality: c.inputModality,
|
|
120
|
+
outputType: c.outputType,
|
|
121
|
+
cache: c.cache ?? false,
|
|
122
|
+
})), null, 2));
|
|
123
|
+
} else {
|
|
124
|
+
console.log("\n📦 Registered converters:\n");
|
|
125
|
+
for (const c of converters) {
|
|
126
|
+
const input = c.inputExts
|
|
127
|
+
? c.inputExts.join(", ")
|
|
128
|
+
: c.inputModality ?? "?";
|
|
129
|
+
console.log(` • ${c.name.padEnd(24)} ${input} → ${c.outputType}${c.cache ? " (cached)" : ""}`);
|
|
130
|
+
}
|
|
131
|
+
console.log();
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate --input
|
|
137
|
+
if (!opts.input) {
|
|
138
|
+
console.error("Error: --input <path> is required (use --list to see converters)");
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const inputPath = path.resolve(opts.input);
|
|
143
|
+
if (!fs.existsSync(inputPath)) {
|
|
144
|
+
console.error(`Error: input file not found: ${inputPath}`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Parse --to chain (e.g. "img,txt" → via=["image"], target="text")
|
|
149
|
+
let targetModality: Modality;
|
|
150
|
+
let viaModalities: Modality[] = [];
|
|
151
|
+
try {
|
|
152
|
+
const parsed = parseModalityChain(opts.to || "text");
|
|
153
|
+
targetModality = parsed.target;
|
|
154
|
+
viaModalities = parsed.via;
|
|
155
|
+
} catch (e: any) {
|
|
156
|
+
console.error(`Error: ${e.message}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Parse --opt flags into converterOptions
|
|
161
|
+
const converterOptions: Record<string, Record<string, any>> = {};
|
|
162
|
+
for (const spec of (opts.opt as string[])) {
|
|
163
|
+
// format: converterName.key=value
|
|
164
|
+
const dotIdx = spec.indexOf(".");
|
|
165
|
+
const eqIdx = spec.indexOf("=");
|
|
166
|
+
if (dotIdx < 0 || eqIdx < 0 || eqIdx < dotIdx) {
|
|
167
|
+
console.error(`Error: --opt must be in format "converterName.key=value", got: ${spec}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
const converterName = spec.slice(0, dotIdx);
|
|
171
|
+
const key = spec.slice(dotIdx + 1, eqIdx);
|
|
172
|
+
const rawValue = spec.slice(eqIdx + 1);
|
|
173
|
+
// Try to coerce to number/boolean, else keep as string
|
|
174
|
+
let value: any = rawValue;
|
|
175
|
+
if (rawValue === "true") value = true;
|
|
176
|
+
else if (rawValue === "false") value = false;
|
|
177
|
+
else if (!isNaN(Number(rawValue)) && rawValue !== "") value = Number(rawValue);
|
|
178
|
+
|
|
179
|
+
if (!converterOptions[converterName]) converterOptions[converterName] = {};
|
|
180
|
+
converterOptions[converterName][key] = value;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// --model injects model into all converter option scopes
|
|
184
|
+
// It can be overridden by a more specific --opt converterName.model=xxx
|
|
185
|
+
if (opts.model) {
|
|
186
|
+
const modelInjectedNames = [
|
|
187
|
+
"whisper", "ffmpeg-whisper",
|
|
188
|
+
"image-to-text",
|
|
189
|
+
"text-to-audio", "text-to-image", "text-to-video",
|
|
190
|
+
"image-to-video",
|
|
191
|
+
];
|
|
192
|
+
for (const name of modelInjectedNames) {
|
|
193
|
+
if (!converterOptions[name]) converterOptions[name] = {};
|
|
194
|
+
if (!converterOptions[name].model) {
|
|
195
|
+
converterOptions[name].model = opts.model;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const preferredConverters = opts.prefer
|
|
201
|
+
? opts.prefer.split(",").map((s: string) => s.trim()).filter(Boolean)
|
|
202
|
+
: [];
|
|
203
|
+
|
|
204
|
+
const chainDisplay = [...viaModalities, targetModality].join(" → ");
|
|
205
|
+
console.error(`🔄 Converting ${inputPath} → ${chainDisplay}...`);
|
|
206
|
+
|
|
207
|
+
const convertOpts: ConvertOptions = {
|
|
208
|
+
force: opts.force ?? false,
|
|
209
|
+
preferredConverters,
|
|
210
|
+
startPage: opts.startPage,
|
|
211
|
+
endPage: opts.endPage,
|
|
212
|
+
startLine: opts.startLine,
|
|
213
|
+
endLine: opts.endLine,
|
|
214
|
+
startTime: opts.startTime,
|
|
215
|
+
endTime: opts.endTime,
|
|
216
|
+
converterOptions,
|
|
217
|
+
onProgress: (stage, fraction) => {
|
|
218
|
+
const pct = Math.round(fraction * 100);
|
|
219
|
+
process.stderr.write(`\r [${pct.toString().padStart(3)}%] ${stage} `);
|
|
220
|
+
},
|
|
221
|
+
...(viaModalities.length > 0 ? { via: viaModalities } : {}),
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const result = await Conversion.convert(inputPath, targetModality, convertOpts);
|
|
225
|
+
process.stderr.write("\n");
|
|
226
|
+
|
|
227
|
+
if (opts.json) {
|
|
228
|
+
console.log(JSON.stringify(result, null, 2));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Text / HTML output
|
|
233
|
+
if (result.outputType === "text" || result.outputType === "html") {
|
|
234
|
+
if (opts.output) {
|
|
235
|
+
fs.writeFileSync(path.resolve(opts.output), result.text ?? "");
|
|
236
|
+
console.error(`✅ Written to ${opts.output}`);
|
|
237
|
+
} else {
|
|
238
|
+
process.stdout.write(result.text ?? "");
|
|
239
|
+
process.stdout.write("\n");
|
|
240
|
+
}
|
|
241
|
+
if (result.usd_cost) {
|
|
242
|
+
console.error(`💰 Cost: $${result.usd_cost.toFixed(6)}`);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// File output (image, audio, video)
|
|
248
|
+
const files = result.files ?? [];
|
|
249
|
+
if (files.length === 0) {
|
|
250
|
+
console.error("⚠ Conversion produced no output files.");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (opts.output) {
|
|
255
|
+
const outPath = path.resolve(opts.output);
|
|
256
|
+
if (files.length === 1) {
|
|
257
|
+
fs.copyFileSync(files[0], outPath);
|
|
258
|
+
console.error(`✅ Written to ${outPath}`);
|
|
259
|
+
} else {
|
|
260
|
+
// Multiple files → write to directory
|
|
261
|
+
fs.mkdirSync(outPath, { recursive: true });
|
|
262
|
+
for (const f of files) {
|
|
263
|
+
const dest = path.join(outPath, path.basename(f));
|
|
264
|
+
fs.copyFileSync(f, dest);
|
|
265
|
+
console.log(dest);
|
|
266
|
+
}
|
|
267
|
+
console.error(`✅ ${files.length} files written to ${outPath}`);
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
// Print file paths to stdout
|
|
271
|
+
for (const f of files) {
|
|
272
|
+
console.log(f);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (result.usd_cost) {
|
|
277
|
+
console.error(`💰 Cost: $${result.usd_cost.toFixed(6)}`);
|
|
278
|
+
}
|
|
279
|
+
} catch (err: any) {
|
|
280
|
+
console.error(`❌ Conversion failed: ${err.message}`);
|
|
281
|
+
if (process.env.DEBUG) console.error(err);
|
|
282
|
+
process.exit(1);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Commander helper: collect repeated option values into an array */
|
|
288
|
+
function collect(val: string, acc: string[]): string[] {
|
|
289
|
+
acc.push(val);
|
|
290
|
+
return acc;
|
|
291
|
+
}
|
package/src/conversion.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import pdf from "pdf-parse";
|
|
2
|
-
import * as fs from "fs";
|
|
3
1
|
import * as path from "path";
|
|
4
2
|
import { readFile, fileExists } from "./utils";
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
|
-
* Get
|
|
8
|
-
* We import lazily to avoid circular dependency issues.
|
|
5
|
+
* Get services lazily to avoid circular dependency issues.
|
|
9
6
|
*/
|
|
10
|
-
function
|
|
7
|
+
function getServices() {
|
|
11
8
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
12
9
|
const { services } = require("./services") as typeof import("./services");
|
|
13
|
-
return services()
|
|
10
|
+
return services();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getMediaProcessor() {
|
|
14
|
+
return getServices().MediaProcessor;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export async function processAudio(
|
|
@@ -38,15 +39,10 @@ export async function convertAudioToText(
|
|
|
38
39
|
filePath: string,
|
|
39
40
|
reusePreviousTranscript = true,
|
|
40
41
|
chunkTime = 30
|
|
41
|
-
) {
|
|
42
|
-
const audios = await processAudio(
|
|
43
|
-
filePath,
|
|
44
|
-
reusePreviousTranscript,
|
|
45
|
-
chunkTime
|
|
46
|
-
);
|
|
42
|
+
): Promise<string> {
|
|
43
|
+
const audios = await processAudio(filePath, reusePreviousTranscript, chunkTime);
|
|
47
44
|
|
|
48
45
|
let fullString = "";
|
|
49
|
-
|
|
50
46
|
for (let i = 0; i < audios.length; i++) {
|
|
51
47
|
const audio = audios[i];
|
|
52
48
|
fullString += `[${i * chunkTime}:${(i + 1) * chunkTime}s] ${audio}`;
|
|
@@ -59,57 +55,15 @@ export async function processVideo(
|
|
|
59
55
|
filePath: string,
|
|
60
56
|
reusePreviousTranscript = true,
|
|
61
57
|
chunkTime = 30
|
|
62
|
-
) {
|
|
63
|
-
const parsed = path.parse(filePath);
|
|
64
|
-
const outputPath = `${parsed.dir}/${parsed.name}/video.json`;
|
|
65
|
-
|
|
58
|
+
): Promise<string[]> {
|
|
66
59
|
console.log("Processing audio...");
|
|
67
|
-
const transcriptions = await processAudio(
|
|
68
|
-
filePath,
|
|
69
|
-
reusePreviousTranscript,
|
|
70
|
-
chunkTime
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
// Return the transcriptions as text — keyframe extraction requires the
|
|
74
|
-
// @tyvm/knowhow-module-video-downloader module
|
|
60
|
+
const transcriptions = await processAudio(filePath, reusePreviousTranscript, chunkTime);
|
|
75
61
|
return transcriptions;
|
|
76
62
|
}
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
chunkTime = 30
|
|
82
|
-
): Promise<string> {
|
|
83
|
-
const transcriptions = await processVideo(filePath, reusePreviousTranscript, chunkTime);
|
|
84
|
-
if (Array.isArray(transcriptions)) {
|
|
85
|
-
return transcriptions.join("\n");
|
|
86
|
-
}
|
|
87
|
-
return String(transcriptions);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async function convertPdfToText(filePath: string) {
|
|
91
|
-
const existingPdfBytes = fs.readFileSync(filePath);
|
|
92
|
-
const data = await pdf(existingPdfBytes);
|
|
93
|
-
return data.text;
|
|
94
|
-
}
|
|
95
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Thin compat shim — delegates to ConversionService.
|
|
66
|
+
*/
|
|
96
67
|
export async function convertToText(filePath: string): Promise<string> {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
switch (extension) {
|
|
100
|
-
case "mp4":
|
|
101
|
-
case "webm":
|
|
102
|
-
case "mov":
|
|
103
|
-
case "mpeg":
|
|
104
|
-
return convertVideoToText(filePath);
|
|
105
|
-
case "mp3":
|
|
106
|
-
case "mpga":
|
|
107
|
-
case "m4a":
|
|
108
|
-
case "wav":
|
|
109
|
-
return convertAudioToText(filePath);
|
|
110
|
-
case "pdf":
|
|
111
|
-
return convertPdfToText(filePath);
|
|
112
|
-
default:
|
|
113
|
-
return ((await readFile(filePath, "utf8")) as string) || "";
|
|
114
|
-
}
|
|
68
|
+
return getServices().Conversion.convertToText(filePath);
|
|
115
69
|
}
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,9 @@ export * as ai from "./ai";
|
|
|
54
54
|
// Export module system types for external modules
|
|
55
55
|
export * from "./services/modules/types";
|
|
56
56
|
export { ModulesService } from "./services/modules";
|
|
57
|
+
// Export conversion types for external modules (e.g. knowhow-module-pdf)
|
|
58
|
+
export * from "./services/conversion/types";
|
|
59
|
+
export { ConversionService } from "./services/conversion/ConversionService";
|
|
57
60
|
// Export plugin types for external plugins
|
|
58
61
|
export { PluginBase } from "./plugins/PluginBase";
|
|
59
62
|
export { PluginMeta, Plugin, PluginContext } from "./plugins/types";
|