@tyvm/knowhow 0.0.120 → 0.0.121
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 -1
- package/src/agents/base/base.ts +11 -1
- package/src/cli.ts +2 -0
- package/src/clients/fireworks.ts +54 -0
- package/src/clients/index.ts +21 -10
- package/src/clients/pricing/fireworks.ts +3 -0
- package/src/commands/replay.ts +424 -0
- package/src/processors/Base64ImageDetector.ts +44 -22
- package/src/services/modules/index.ts +5 -4
- package/tests/processors/Base64ImageDetector.test.ts +135 -0
- package/ts_build/package.json +1 -1
- package/ts_build/src/agents/base/base.js +8 -1
- package/ts_build/src/agents/base/base.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/fireworks.d.ts +9 -0
- package/ts_build/src/clients/fireworks.js +29 -0
- package/ts_build/src/clients/fireworks.js.map +1 -1
- package/ts_build/src/clients/index.js +12 -7
- package/ts_build/src/clients/index.js.map +1 -1
- package/ts_build/src/clients/pricing/fireworks.js +3 -0
- package/ts_build/src/clients/pricing/fireworks.js.map +1 -1
- package/ts_build/src/commands/replay.d.ts +2 -0
- package/ts_build/src/commands/replay.js +324 -0
- package/ts_build/src/commands/replay.js.map +1 -0
- package/ts_build/src/processors/Base64ImageDetector.js +19 -12
- package/ts_build/src/processors/Base64ImageDetector.js.map +1 -1
- package/ts_build/src/services/modules/index.js.map +1 -1
- package/ts_build/tests/processors/Base64ImageDetector.test.js +88 -0
- package/ts_build/tests/processors/Base64ImageDetector.test.js.map +1 -1
package/package.json
CHANGED
package/src/agents/base/base.ts
CHANGED
|
@@ -668,11 +668,15 @@ export abstract class BaseAgent implements IAgent {
|
|
|
668
668
|
promise: Promise<T>,
|
|
669
669
|
interruptValue: T
|
|
670
670
|
): Promise<T> {
|
|
671
|
-
return new Promise<T>((resolve) => {
|
|
671
|
+
return new Promise<T>((resolve, reject) => {
|
|
672
|
+
let interrupted = false;
|
|
673
|
+
|
|
672
674
|
this._interruptResolve = (value: any) => {
|
|
675
|
+
interrupted = true;
|
|
673
676
|
this._interruptResolve = null;
|
|
674
677
|
resolve(value);
|
|
675
678
|
};
|
|
679
|
+
|
|
676
680
|
promise.then((result) => {
|
|
677
681
|
if (this._interruptResolve) {
|
|
678
682
|
this._interruptResolve = null;
|
|
@@ -681,7 +685,13 @@ export abstract class BaseAgent implements IAgent {
|
|
|
681
685
|
}).catch((err) => {
|
|
682
686
|
if (this._interruptResolve) {
|
|
683
687
|
this._interruptResolve = null;
|
|
688
|
+
}
|
|
689
|
+
// Only swallow the error if interrupt() was explicitly called.
|
|
690
|
+
// Otherwise re-throw so callers see the real error.
|
|
691
|
+
if (interrupted) {
|
|
684
692
|
resolve(interruptValue);
|
|
693
|
+
} else {
|
|
694
|
+
reject(err);
|
|
685
695
|
}
|
|
686
696
|
});
|
|
687
697
|
});
|
package/src/cli.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
addGithubCredentialsCommand,
|
|
38
38
|
} from "./commands/misc";
|
|
39
39
|
import { addConvertCommand } from "./commands/convert";
|
|
40
|
+
import { addReplayCommand } from "./commands/replay";
|
|
40
41
|
|
|
41
42
|
// Handle unhandled promise rejections gracefully — particularly from MCP SDK
|
|
42
43
|
// which fires errors via event emitters that can bypass Promise.allSettled.
|
|
@@ -100,6 +101,7 @@ async function main() {
|
|
|
100
101
|
addModulesCommand(program);
|
|
101
102
|
addMcpCommands(program);
|
|
102
103
|
addConvertCommand(program);
|
|
104
|
+
addReplayCommand(program);
|
|
103
105
|
|
|
104
106
|
// Load global modules early (before parse) so they can register CLI subcommands.
|
|
105
107
|
// We pass only the Program in context — no services are spun up at this stage.
|
package/src/clients/fireworks.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { HttpClient } from "./http";
|
|
2
2
|
import { FireworksTextPricing } from "./pricing/fireworks";
|
|
3
|
+
import { CompletionOptions, CompletionResponse } from "./types";
|
|
4
|
+
|
|
5
|
+
type ModelInfo = { id: string; object: string; owned_by: string };
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Fireworks AI client — OpenAI-compatible API (fast serverless inference)
|
|
@@ -12,4 +15,55 @@ export class GenericFireworksClient extends HttpClient {
|
|
|
12
15
|
if (apiKey) this.setJwt(apiKey);
|
|
13
16
|
this.setPrices(FireworksTextPricing);
|
|
14
17
|
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Supplement the live /v1/models response with any models we have in the
|
|
21
|
+
* pricing table. The Fireworks API sometimes doesn't return newly-released
|
|
22
|
+
* models (e.g. minimax-m3, kimi-k2p7-code) even though they are available
|
|
23
|
+
* for inference — so we use the pricing map as the source of truth for
|
|
24
|
+
* "models we know exist on this provider".
|
|
25
|
+
*/
|
|
26
|
+
async getModels(type = "all"): Promise<ModelInfo[]> {
|
|
27
|
+
let liveModels: ModelInfo[] = [];
|
|
28
|
+
try {
|
|
29
|
+
liveModels = await super.getModels(type);
|
|
30
|
+
} catch (_err) {
|
|
31
|
+
// Live API call failed — fall back to pricing map only
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const liveIds = new Set(liveModels.map((m) => m.id));
|
|
35
|
+
const pricingModels: ModelInfo[] = Object.keys(FireworksTextPricing)
|
|
36
|
+
.filter((id) => !liveIds.has(id))
|
|
37
|
+
.map((id) => ({ id, object: "model", owned_by: "fireworks" }));
|
|
38
|
+
|
|
39
|
+
return [...liveModels, ...pricingModels];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Sanitize the request before sending to Fireworks.
|
|
44
|
+
* Some models (e.g. kimi-k2p7-code) reject extra fields like:
|
|
45
|
+
* - tools[N].function.returns (non-standard extension)
|
|
46
|
+
* - long_ttl_cache (Anthropic-specific cache flag)
|
|
47
|
+
*/
|
|
48
|
+
async createChatCompletion(
|
|
49
|
+
options: CompletionOptions
|
|
50
|
+
): Promise<CompletionResponse> {
|
|
51
|
+
const sanitized: CompletionOptions = {
|
|
52
|
+
...options,
|
|
53
|
+
// Strip Anthropic-specific field not accepted by Fireworks
|
|
54
|
+
long_ttl_cache: undefined,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (sanitized.tools) {
|
|
58
|
+
sanitized.tools = sanitized.tools.map((tool) => {
|
|
59
|
+
const { returns, ...fnRest } = tool.function as any;
|
|
60
|
+
return {
|
|
61
|
+
...tool,
|
|
62
|
+
function: fnRest,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return super.createChatCompletion(sanitized);
|
|
68
|
+
}
|
|
15
69
|
}
|
package/src/clients/index.ts
CHANGED
|
@@ -397,9 +397,10 @@ export class AIClient {
|
|
|
397
397
|
const hasModel = this.providerHasModel(provider, model);
|
|
398
398
|
|
|
399
399
|
if (!hasModel) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
)
|
|
400
|
+
// Model not in local registry — pass it through anyway so the provider
|
|
401
|
+
// API can accept or reject it directly (e.g. newly-released models that
|
|
402
|
+
// haven't been fetched into our local model list yet).
|
|
403
|
+
console.warn(`⚠️ Model '${model}' not in local registry for provider '${provider}', attempting anyway.`);
|
|
403
404
|
}
|
|
404
405
|
|
|
405
406
|
return { client: this.clients[provider], provider, model };
|
|
@@ -609,6 +610,11 @@ export class AIClient {
|
|
|
609
610
|
return { provider, model };
|
|
610
611
|
}
|
|
611
612
|
|
|
613
|
+
// If an explicit provider was given, don't fall through to fuzzy cross-provider
|
|
614
|
+
// search — that would silently pick a completely different provider (e.g. nvidia
|
|
615
|
+
// instead of fireworks). Just pass through and let the API accept/reject the model.
|
|
616
|
+
const hasExplicitProvider = !!provider;
|
|
617
|
+
|
|
612
618
|
if (model?.includes("/")) {
|
|
613
619
|
const split = model.split("/");
|
|
614
620
|
|
|
@@ -620,16 +626,21 @@ export class AIClient {
|
|
|
620
626
|
return { provider: inferredProvider, model: inferredModel };
|
|
621
627
|
}
|
|
622
628
|
|
|
623
|
-
// Starts with match
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
629
|
+
// Starts with match — only if no explicit provider was given
|
|
630
|
+
if (!hasExplicitProvider) {
|
|
631
|
+
const foundBySplit = this.findModel(inferredModel);
|
|
632
|
+
if (foundBySplit) {
|
|
633
|
+
return foundBySplit;
|
|
634
|
+
}
|
|
627
635
|
}
|
|
628
636
|
}
|
|
629
637
|
|
|
630
|
-
|
|
631
|
-
if (
|
|
632
|
-
|
|
638
|
+
// Fuzzy cross-provider search — only if no explicit provider was given
|
|
639
|
+
if (!hasExplicitProvider) {
|
|
640
|
+
const foundByModel = this.findModel(model);
|
|
641
|
+
if (foundByModel) {
|
|
642
|
+
return foundByModel;
|
|
643
|
+
}
|
|
633
644
|
}
|
|
634
645
|
|
|
635
646
|
const allModels = this.listAllModels();
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const FireworksTextPricing: Record<string, { input: number; output: number; cache_hit?: number }> = {
|
|
6
6
|
// Moonshot AI
|
|
7
|
+
"accounts/fireworks/models/kimi-k2p7-code": { input: 0.95, cache_hit: 0.19, output: 4.0 },
|
|
7
8
|
"accounts/fireworks/models/kimi-k2-6": { input: 0.95, cache_hit: 0.16, output: 4.0 },
|
|
8
9
|
"accounts/fireworks/models/kimi-k2-5": { input: 0.60, cache_hit: 0.10, output: 3.0 },
|
|
9
10
|
|
|
10
11
|
// MiniMax
|
|
12
|
+
"accounts/fireworks/models/minimax-m3": { input: 0.30, cache_hit: 0.06, output: 1.20 },
|
|
11
13
|
"accounts/fireworks/models/minimax-m2-7": { input: 0.30, cache_hit: 0.06, output: 1.20 },
|
|
12
14
|
"accounts/fireworks/models/minimax-m2-5": { input: 0.30, cache_hit: 0.03, output: 1.20 },
|
|
13
15
|
|
|
@@ -19,6 +21,7 @@ export const FireworksTextPricing: Record<string, { input: number; output: numbe
|
|
|
19
21
|
"accounts/fireworks/models/qwen3-8b": { input: 0.20, cache_hit: 0.10, output: 0.20 },
|
|
20
22
|
|
|
21
23
|
// Z.ai
|
|
24
|
+
"accounts/fireworks/models/glm-5-2": { input: 1.40, cache_hit: 0.26, output: 4.40 },
|
|
22
25
|
"accounts/fireworks/models/glm-5-1": { input: 1.40, cache_hit: 0.26, output: 4.40 },
|
|
23
26
|
"accounts/fireworks/models/glm-5": { input: 1.00, cache_hit: 0.20, output: 3.20 },
|
|
24
27
|
"accounts/fireworks/models/glm-4-7": { input: 0.60, cache_hit: 0.30, output: 2.20 },
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as readline from "readline";
|
|
3
|
+
import { KnowhowSimpleClient, KNOWHOW_API_URL } from "../services/KnowhowClient";
|
|
4
|
+
import { ToolCall } from "../clients/types";
|
|
5
|
+
|
|
6
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
interface ReplayResult {
|
|
9
|
+
toolName: string;
|
|
10
|
+
toolId: string;
|
|
11
|
+
status: "success" | "failed" | "skipped";
|
|
12
|
+
error?: string;
|
|
13
|
+
output?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
/** Flatten all tool_use blocks from all threads in order */
|
|
19
|
+
function extractToolCalls(threads: any[][]): { toolCall: ToolCall; threadIdx: number; msgIdx: number }[] {
|
|
20
|
+
const calls: { toolCall: ToolCall; threadIdx: number; msgIdx: number }[] = [];
|
|
21
|
+
|
|
22
|
+
for (let ti = 0; ti < threads.length; ti++) {
|
|
23
|
+
const thread = threads[ti];
|
|
24
|
+
for (let mi = 0; mi < thread.length; mi++) {
|
|
25
|
+
const msg = thread[mi];
|
|
26
|
+
if (msg.role !== "assistant") continue;
|
|
27
|
+
|
|
28
|
+
const content = msg.content;
|
|
29
|
+
|
|
30
|
+
// Handle Anthropic-style: content is an array of blocks with type "tool_use"
|
|
31
|
+
if (Array.isArray(content)) {
|
|
32
|
+
for (const block of content) {
|
|
33
|
+
if (block.type !== "tool_use") continue;
|
|
34
|
+
// Convert from Anthropic tool_use block to our ToolCall shape
|
|
35
|
+
const toolCall: ToolCall = {
|
|
36
|
+
id: block.id || `replay_${Date.now()}_${Math.random()}`,
|
|
37
|
+
type: "function",
|
|
38
|
+
function: {
|
|
39
|
+
name: block.name,
|
|
40
|
+
arguments:
|
|
41
|
+
typeof block.input === "string"
|
|
42
|
+
? block.input
|
|
43
|
+
: JSON.stringify(block.input ?? {}),
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
calls.push({ toolCall, threadIdx: ti, msgIdx: mi });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Handle OpenAI-style: tool_calls array on the message (content may be a string)
|
|
51
|
+
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
52
|
+
for (const tc of msg.tool_calls) {
|
|
53
|
+
calls.push({ toolCall: tc, threadIdx: ti, msgIdx: mi });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return calls;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fetch threads by taskId (direct task) */
|
|
63
|
+
async function fetchThreadsByTaskId(
|
|
64
|
+
client: KnowhowSimpleClient,
|
|
65
|
+
taskId: string
|
|
66
|
+
): Promise<any[][]> {
|
|
67
|
+
return client.getTaskThreads(taskId);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Fetch threads from a session's messages and find the task embedded within */
|
|
71
|
+
async function fetchThreadsBySessionMessage(
|
|
72
|
+
client: KnowhowSimpleClient,
|
|
73
|
+
sessionId: string,
|
|
74
|
+
messageId: string
|
|
75
|
+
): Promise<any[][]> {
|
|
76
|
+
// GET /api/chat/sessions/:sessionId/messages
|
|
77
|
+
const baseUrl = (client as any).baseUrl || KNOWHOW_API_URL;
|
|
78
|
+
const headers = (client as any).headers || {};
|
|
79
|
+
|
|
80
|
+
const resp = await fetch(`${baseUrl}/api/chat/sessions/${sessionId}/messages`, {
|
|
81
|
+
headers,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (!resp.ok) {
|
|
85
|
+
throw new Error(`Failed to fetch session messages: ${resp.status} ${resp.statusText}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const messages: any[] = await resp.json();
|
|
89
|
+
|
|
90
|
+
// Find the message with our target messageId
|
|
91
|
+
let targetMsg = messages.find((m: any) => m.id === messageId);
|
|
92
|
+
if (!targetMsg) {
|
|
93
|
+
// messageId might be on a task inside an assistant message
|
|
94
|
+
for (const m of messages) {
|
|
95
|
+
if (m.tasks) {
|
|
96
|
+
for (const task of m.tasks) {
|
|
97
|
+
if (task.id === messageId) {
|
|
98
|
+
targetMsg = m;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (targetMsg) break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!targetMsg) {
|
|
108
|
+
throw new Error(`Message ${messageId} not found in session ${sessionId}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extract threads - they may be directly on a task inside the message
|
|
112
|
+
if (targetMsg.tasks && targetMsg.tasks.length > 0) {
|
|
113
|
+
// Find task that contains our messageId, or use first task
|
|
114
|
+
let task = targetMsg.tasks.find((t: any) => t.id === messageId);
|
|
115
|
+
if (!task) task = targetMsg.tasks[0];
|
|
116
|
+
|
|
117
|
+
if (task.threads && Array.isArray(task.threads)) {
|
|
118
|
+
return task.threads;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// If task has an id but no inline threads, fetch via task API
|
|
122
|
+
if (task.id) {
|
|
123
|
+
return client.getTaskThreads(task.id);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Fallback: if threads are directly on the message
|
|
128
|
+
if (targetMsg.threads) return targetMsg.threads;
|
|
129
|
+
|
|
130
|
+
throw new Error(`No threads found for message ${messageId}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Prompt user for approval (y/n/s for skip/q for quit) */
|
|
134
|
+
async function promptApproval(toolName: string, args: string): Promise<"run" | "skip" | "quit"> {
|
|
135
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const preview = args.length > 200 ? args.slice(0, 200) + "…" : args;
|
|
138
|
+
rl.question(
|
|
139
|
+
`\n🔧 Tool: ${toolName}\n Args: ${preview}\n Run this? [y/n/q] (y=run, n=skip, q=quit): `,
|
|
140
|
+
(answer) => {
|
|
141
|
+
rl.close();
|
|
142
|
+
const a = answer.trim().toLowerCase();
|
|
143
|
+
if (a === "q" || a === "quit") resolve("quit");
|
|
144
|
+
else if (a === "n" || a === "skip" || a === "s") resolve("skip");
|
|
145
|
+
else resolve("run");
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Parse --replace-path options like "/workspace/repo:./" into a list of [from, to] pairs.
|
|
153
|
+
* Multiple --replace-path flags are supported.
|
|
154
|
+
*/
|
|
155
|
+
function parseReplacePaths(replacePaths: string[]): Array<[string, string]> {
|
|
156
|
+
return replacePaths.map((rp) => {
|
|
157
|
+
const colonIdx = rp.indexOf(":");
|
|
158
|
+
if (colonIdx === -1) {
|
|
159
|
+
throw new Error(`Invalid --replace-path value "${rp}": expected format "from:to" (e.g. /workspace/repo:./)`);
|
|
160
|
+
}
|
|
161
|
+
const from = rp.slice(0, colonIdx);
|
|
162
|
+
const to = rp.slice(colonIdx + 1);
|
|
163
|
+
return [from, to];
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Apply all path replacements to a tool arguments string */
|
|
168
|
+
function applyPathReplacements(argsStr: string, replacements: Array<[string, string]>): string {
|
|
169
|
+
let result = argsStr;
|
|
170
|
+
for (const [from, to] of replacements) {
|
|
171
|
+
// Replace all occurrences globally
|
|
172
|
+
result = result.split(from).join(to);
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Main replay logic ────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
async function runReplay(options: {
|
|
180
|
+
taskId?: string;
|
|
181
|
+
sessionId?: string;
|
|
182
|
+
messageId?: string;
|
|
183
|
+
approve: boolean;
|
|
184
|
+
fromStep?: number;
|
|
185
|
+
toStep?: number;
|
|
186
|
+
only?: string[];
|
|
187
|
+
ignore?: string[];
|
|
188
|
+
dryRun: boolean;
|
|
189
|
+
replacePaths: Array<[string, string]>;
|
|
190
|
+
}) {
|
|
191
|
+
// 1. Setup services + tools
|
|
192
|
+
console.log("🔧 Setting up services...");
|
|
193
|
+
const { setupServices } = await import("./services");
|
|
194
|
+
const { Tools } = await setupServices();
|
|
195
|
+
|
|
196
|
+
// 2. Fetch the threads from the remote API
|
|
197
|
+
const client = new KnowhowSimpleClient(KNOWHOW_API_URL);
|
|
198
|
+
|
|
199
|
+
let threads: any[][] = [];
|
|
200
|
+
|
|
201
|
+
if (options.taskId && !options.sessionId) {
|
|
202
|
+
console.log(`\n📡 Fetching threads for task: ${options.taskId}`);
|
|
203
|
+
threads = await fetchThreadsByTaskId(client, options.taskId);
|
|
204
|
+
} else if (options.sessionId && options.messageId) {
|
|
205
|
+
console.log(`\n📡 Fetching threads for session=${options.sessionId} message=${options.messageId}`);
|
|
206
|
+
threads = await fetchThreadsBySessionMessage(client, options.sessionId, options.messageId);
|
|
207
|
+
} else if (options.taskId) {
|
|
208
|
+
// taskId might actually be a task UUID from a message
|
|
209
|
+
console.log(`\n📡 Fetching threads for task: ${options.taskId}`);
|
|
210
|
+
threads = await fetchThreadsByTaskId(client, options.taskId);
|
|
211
|
+
} else {
|
|
212
|
+
throw new Error("Provide --task-id or --session-id + --message-id");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!threads || threads.length === 0) {
|
|
216
|
+
console.log("⚠️ No threads found. Nothing to replay.");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`✅ Loaded ${threads.length} thread(s)`);
|
|
221
|
+
|
|
222
|
+
// 3. Extract all tool calls
|
|
223
|
+
const allToolCalls = extractToolCalls(threads);
|
|
224
|
+
|
|
225
|
+
if (allToolCalls.length === 0) {
|
|
226
|
+
console.log("⚠️ No tool calls found in threads. Nothing to replay.");
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
console.log(`\n📋 Found ${allToolCalls.length} tool call(s) total`);
|
|
231
|
+
|
|
232
|
+
// 4. Apply filters
|
|
233
|
+
let filtered = allToolCalls;
|
|
234
|
+
|
|
235
|
+
if (options.fromStep != null) {
|
|
236
|
+
filtered = filtered.slice(options.fromStep - 1);
|
|
237
|
+
}
|
|
238
|
+
if (options.toStep != null) {
|
|
239
|
+
const end = options.toStep - (options.fromStep ?? 1) + 1;
|
|
240
|
+
filtered = filtered.slice(0, end);
|
|
241
|
+
}
|
|
242
|
+
if (options.only && options.only.length > 0) {
|
|
243
|
+
filtered = filtered.filter(({ toolCall }) =>
|
|
244
|
+
options.only!.some((n) => toolCall.function.name.includes(n))
|
|
245
|
+
);
|
|
246
|
+
console.log(` Filtered to tools matching: ${options.only.join(", ")} → ${filtered.length} call(s)`);
|
|
247
|
+
}
|
|
248
|
+
if (options.ignore && options.ignore.length > 0) {
|
|
249
|
+
filtered = filtered.filter(({ toolCall }) =>
|
|
250
|
+
!options.ignore!.some((n) => toolCall.function.name.includes(n))
|
|
251
|
+
);
|
|
252
|
+
console.log(` Ignoring tools matching: ${options.ignore.join(", ")} → ${filtered.length} call(s)`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (options.replacePaths.length > 0) {
|
|
256
|
+
const pairs = options.replacePaths.map(([f, t]) => `"${f}" → "${t}"`).join(", ");
|
|
257
|
+
console.log(` Path replacements: ${pairs}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (filtered.length === 0) {
|
|
261
|
+
console.log("⚠️ No tool calls remain after filtering. Nothing to replay.");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 5. Print summary table
|
|
266
|
+
console.log("\n┌─────────────────────────────────────────────────────────────┐");
|
|
267
|
+
console.log(`│ Replay Plan: ${filtered.length} tool call(s)${" ".repeat(Math.max(0, 47 - String(filtered.length).length))}│`);
|
|
268
|
+
console.log("├──────┬──────────────────────────────────────────────────────┤");
|
|
269
|
+
console.log("│ # │ Tool Name │");
|
|
270
|
+
console.log("├──────┼──────────────────────────────────────────────────────┤");
|
|
271
|
+
for (let i = 0; i < filtered.length; i++) {
|
|
272
|
+
const name = filtered[i].toolCall.function.name.padEnd(52).slice(0, 52);
|
|
273
|
+
const num = String(i + 1).padStart(4);
|
|
274
|
+
console.log(`│ ${num} │ ${name}│`);
|
|
275
|
+
}
|
|
276
|
+
console.log("└──────┴──────────────────────────────────────────────────────┘\n");
|
|
277
|
+
|
|
278
|
+
if (options.dryRun) {
|
|
279
|
+
console.log("🏃 Dry-run mode: no tools will be executed.");
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 6. Execute tool calls
|
|
284
|
+
const results: ReplayResult[] = [];
|
|
285
|
+
let stepNum = 0;
|
|
286
|
+
|
|
287
|
+
for (const { toolCall } of filtered) {
|
|
288
|
+
stepNum++;
|
|
289
|
+
const toolName = toolCall.function.name;
|
|
290
|
+
const argsStr =
|
|
291
|
+
typeof toolCall.function.arguments === "string"
|
|
292
|
+
? toolCall.function.arguments
|
|
293
|
+
: JSON.stringify(toolCall.function.arguments);
|
|
294
|
+
|
|
295
|
+
// Apply path replacements to the arguments string before execution
|
|
296
|
+
const patchedArgsStr =
|
|
297
|
+
options.replacePaths.length > 0
|
|
298
|
+
? applyPathReplacements(argsStr, options.replacePaths)
|
|
299
|
+
: argsStr;
|
|
300
|
+
|
|
301
|
+
const patchedToolCall: ToolCall = {
|
|
302
|
+
...toolCall,
|
|
303
|
+
function: { ...toolCall.function, arguments: patchedArgsStr },
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Prompt for approval if requested
|
|
307
|
+
if (options.approve) {
|
|
308
|
+
const decision = await promptApproval(toolName, patchedArgsStr);
|
|
309
|
+
if (decision === "quit") {
|
|
310
|
+
console.log("\n⛔ Quit by user.");
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
if (decision === "skip") {
|
|
314
|
+
console.log(` ⏭ Skipped: ${toolName}`);
|
|
315
|
+
results.push({ toolName, toolId: toolCall.id, status: "skipped" });
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
process.stdout.write(`[${stepNum}/${filtered.length}] Running ${toolName}... `);
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const { functionResp } = await Tools.callTool(patchedToolCall, Tools.getToolNames());
|
|
324
|
+
const output = typeof functionResp === "string" ? functionResp : JSON.stringify(functionResp);
|
|
325
|
+
const preview = output.length > 120 ? output.slice(0, 120) + "…" : output;
|
|
326
|
+
console.log(`✅`);
|
|
327
|
+
if (output) {
|
|
328
|
+
console.log(` → ${preview}`);
|
|
329
|
+
}
|
|
330
|
+
results.push({ toolName, toolId: toolCall.id, status: "success", output });
|
|
331
|
+
} catch (err: any) {
|
|
332
|
+
console.log(`❌`);
|
|
333
|
+
const errMsg = err?.message || String(err);
|
|
334
|
+
console.log(` ⚠ ${errMsg}`);
|
|
335
|
+
results.push({ toolName, toolId: toolCall.id, status: "failed", error: errMsg });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// 7. Print summary
|
|
340
|
+
const succeeded = results.filter((r) => r.status === "success").length;
|
|
341
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
342
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
343
|
+
|
|
344
|
+
console.log("\n═══════════════════════════════════════════════════════════════");
|
|
345
|
+
console.log(" Replay Complete");
|
|
346
|
+
console.log(` ✅ ${succeeded} succeeded ❌ ${failed} failed ⏭ ${skipped} skipped`);
|
|
347
|
+
|
|
348
|
+
if (failed > 0) {
|
|
349
|
+
console.log("\n Failed tool calls:");
|
|
350
|
+
results
|
|
351
|
+
.filter((r) => r.status === "failed")
|
|
352
|
+
.forEach((r) => {
|
|
353
|
+
console.log(` • ${r.toolName}: ${r.error}`);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log("═══════════════════════════════════════════════════════════════\n");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ─── Command registration ─────────────────────────────────────────────────────
|
|
361
|
+
|
|
362
|
+
export function addReplayCommand(program: Command): void {
|
|
363
|
+
program
|
|
364
|
+
.command("replay")
|
|
365
|
+
.description(
|
|
366
|
+
"Replay tool calls from a remote session/task locally. " +
|
|
367
|
+
"Useful for recreating agent work from a cloud sandbox on your local machine."
|
|
368
|
+
)
|
|
369
|
+
.option("--task-id <taskId>", "Task UUID to replay tool calls from")
|
|
370
|
+
.option("--session-id <sessionId>", "Chat session UUID (use with --message-id)")
|
|
371
|
+
.option("--message-id <messageId>", "Message UUID within a session to replay")
|
|
372
|
+
.option(
|
|
373
|
+
"--approve",
|
|
374
|
+
"Approval mode: prompt before each tool call (y=run, n=skip, q=quit)"
|
|
375
|
+
)
|
|
376
|
+
.option("--dry-run", "List tool calls without executing them")
|
|
377
|
+
.option(
|
|
378
|
+
"--from-step <n>",
|
|
379
|
+
"Start replaying from step N (1-based)",
|
|
380
|
+
(v) => parseInt(v, 10)
|
|
381
|
+
)
|
|
382
|
+
.option(
|
|
383
|
+
"--to-step <n>",
|
|
384
|
+
"Stop replaying after step N (1-based)",
|
|
385
|
+
(v) => parseInt(v, 10)
|
|
386
|
+
)
|
|
387
|
+
.option(
|
|
388
|
+
"--only <tools>",
|
|
389
|
+
"Comma-separated list of tool name substrings to include (e.g. writeFile,patchFile)",
|
|
390
|
+
(v) => v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
391
|
+
)
|
|
392
|
+
.option(
|
|
393
|
+
"--ignore <tools>",
|
|
394
|
+
"Comma-separated list of tool name substrings to skip (e.g. execCommand,readFile)",
|
|
395
|
+
(v) => v.split(",").map((s) => s.trim()).filter(Boolean)
|
|
396
|
+
)
|
|
397
|
+
.option(
|
|
398
|
+
"--replace-path <mapping>",
|
|
399
|
+
'Replace a path prefix in all tool call arguments. Format: "from:to" (e.g. /workspace/repo:./). ' +
|
|
400
|
+
"Can be specified multiple times.",
|
|
401
|
+
(v: string, acc: string[]) => { acc.push(v); return acc; },
|
|
402
|
+
[] as string[]
|
|
403
|
+
)
|
|
404
|
+
.action(async (options) => {
|
|
405
|
+
try {
|
|
406
|
+
const replacePaths = parseReplacePaths(options.replacePath || []);
|
|
407
|
+
await runReplay({
|
|
408
|
+
taskId: options.taskId,
|
|
409
|
+
sessionId: options.sessionId,
|
|
410
|
+
messageId: options.messageId,
|
|
411
|
+
approve: options.approve || false,
|
|
412
|
+
fromStep: options.fromStep,
|
|
413
|
+
toStep: options.toStep,
|
|
414
|
+
only: options.only,
|
|
415
|
+
ignore: options.ignore,
|
|
416
|
+
dryRun: options.dryRun || false,
|
|
417
|
+
replacePaths,
|
|
418
|
+
});
|
|
419
|
+
} catch (err: any) {
|
|
420
|
+
console.error(`\n❌ Replay failed: ${err?.message || err}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
}
|