niceeval 0.1.0
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/README.md +164 -0
- package/README.zh.md +160 -0
- package/bin/niceeval.js +11 -0
- package/package.json +96 -0
- package/src/agents/bub.ts +223 -0
- package/src/agents/builtin.ts +6 -0
- package/src/agents/claude-code.ts +96 -0
- package/src/agents/codex.ts +122 -0
- package/src/agents/index.ts +25 -0
- package/src/agents/shared.ts +145 -0
- package/src/cli.ts +421 -0
- package/src/context/context.test.ts +96 -0
- package/src/context/context.ts +427 -0
- package/src/context/control-flow.ts +27 -0
- package/src/context/session.ts +124 -0
- package/src/define.ts +112 -0
- package/src/expect/index.ts +243 -0
- package/src/i18n/en.ts +131 -0
- package/src/i18n/index.ts +39 -0
- package/src/i18n/zh-CN.ts +132 -0
- package/src/index.ts +39 -0
- package/src/loaders/index.ts +56 -0
- package/src/o11y/cost.ts +57 -0
- package/src/o11y/derive.ts +304 -0
- package/src/o11y/otlp/canonical.ts +112 -0
- package/src/o11y/otlp/mappers/bub.ts +30 -0
- package/src/o11y/otlp/mappers/codex.ts +38 -0
- package/src/o11y/otlp/mappers/index.ts +25 -0
- package/src/o11y/otlp/parse.ts +330 -0
- package/src/o11y/otlp/receiver.ts +100 -0
- package/src/o11y/otlp/sandbox-receiver.ts +113 -0
- package/src/o11y/otlp/select.ts +82 -0
- package/src/o11y/parsers/bub.ts +240 -0
- package/src/o11y/parsers/claude-code.ts +270 -0
- package/src/o11y/parsers/codex.ts +480 -0
- package/src/o11y/parsers/index.ts +37 -0
- package/src/o11y/prices.json +9873 -0
- package/src/runner/discover.ts +77 -0
- package/src/runner/reporters/artifacts.ts +81 -0
- package/src/runner/reporters/console.ts +76 -0
- package/src/runner/reporters/index.ts +5 -0
- package/src/runner/reporters/json.ts +52 -0
- package/src/runner/reporters/live.ts +178 -0
- package/src/runner/reporters/table.ts +328 -0
- package/src/runner/run.ts +860 -0
- package/src/runner/sandbox-prep.ts +91 -0
- package/src/sandbox/checkpoint.ts +39 -0
- package/src/sandbox/docker.ts +539 -0
- package/src/sandbox/e2b.ts +203 -0
- package/src/sandbox/index.ts +25 -0
- package/src/sandbox/registry.ts +60 -0
- package/src/sandbox/resolve.ts +131 -0
- package/src/sandbox/source-files.ts +30 -0
- package/src/sandbox/vercel.ts +236 -0
- package/src/scoring/collector.ts +113 -0
- package/src/scoring/judge.ts +236 -0
- package/src/scoring/scoped.ts +289 -0
- package/src/scoring/verdict.ts +20 -0
- package/src/source-loc.ts +53 -0
- package/src/types.ts +913 -0
- package/src/util.test.ts +31 -0
- package/src/util.ts +50 -0
- package/src/view/app/App.tsx +189 -0
- package/src/view/app/components/AttemptModal.tsx +66 -0
- package/src/view/app/components/CodeView.tsx +272 -0
- package/src/view/app/components/CopyControls.tsx +89 -0
- package/src/view/app/components/ExperimentTable.tsx +266 -0
- package/src/view/app/components/GroupSelector.tsx +61 -0
- package/src/view/app/components/LazyArtifact.tsx +50 -0
- package/src/view/app/components/Trace.tsx +100 -0
- package/src/view/app/components/Transcript.tsx +130 -0
- package/src/view/app/components/primitives.tsx +43 -0
- package/src/view/app/components/ui/badge.tsx +21 -0
- package/src/view/app/components/ui/dialog.tsx +34 -0
- package/src/view/app/components/ui/tabs.tsx +30 -0
- package/src/view/app/i18n.ts +341 -0
- package/src/view/app/index.html +13 -0
- package/src/view/app/lib/cn.ts +7 -0
- package/src/view/app/lib/format.ts +73 -0
- package/src/view/app/lib/guards.ts +61 -0
- package/src/view/app/lib/outcome.ts +96 -0
- package/src/view/app/lib/rows.ts +63 -0
- package/src/view/app/lib/transcript-data.tsx +121 -0
- package/src/view/app/main.tsx +17 -0
- package/src/view/app/pages/RunsPage.tsx +83 -0
- package/src/view/app/pages/TracesPage.tsx +40 -0
- package/src/view/app/shared.ts +10 -0
- package/src/view/app/types.ts +114 -0
- package/src/view/app/vite.config.ts +26 -0
- package/src/view/client-dist/app.css +2 -0
- package/src/view/client-dist/app.js +56 -0
- package/src/view/index.ts +406 -0
- package/src/view/styles.css +1074 -0
- package/src/view/template.html +15 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineSandboxAgent } from "../define.ts";
|
|
2
|
+
import { requireEnv, getEnv } from "../util.ts";
|
|
3
|
+
import { shared } from "./shared.ts";
|
|
4
|
+
import type { Agent, McpServer } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Claude Code 的 agent adapter(沙箱型)。
|
|
8
|
+
//
|
|
9
|
+
// 连接方式:在沙箱里 spawn `claude` CLI,跑完读回 transcript JSONL → 标准事件流。
|
|
10
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export interface ClaudeCodeConfig {
|
|
13
|
+
/** Anthropic API key。省略时读 ANTHROPIC_API_KEY env。 */
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
/**
|
|
16
|
+
* 自定义 API base URL(代理 / 内网端点)。省略时读 ANTHROPIC_BASE_URL env;
|
|
17
|
+
* 两者都没有则用 Anthropic 官方端点(claude CLI 默认行为)。
|
|
18
|
+
*/
|
|
19
|
+
baseUrl?: string;
|
|
20
|
+
/**
|
|
21
|
+
* 最多跑几个 tool-use 轮次(→ `--max-turns`)。
|
|
22
|
+
* 控制 eval 成本上限;省略时用 CLI 原生默认(无限制)。
|
|
23
|
+
*/
|
|
24
|
+
maxTurns?: number;
|
|
25
|
+
/**
|
|
26
|
+
* 额外 MCP server(每个沙箱 setup 时写进 ~/.claude/claude.json)。
|
|
27
|
+
* 示例:{ name: "browser", command: "npx", args: ["-y", "@anthropic/mcp-browser"] }
|
|
28
|
+
*/
|
|
29
|
+
mcpServers?: McpServer[];
|
|
30
|
+
/**
|
|
31
|
+
* 额外安装的 skill,格式为 GitHub `"org/repo"`(如 `"Effect-TS/skills"`)。
|
|
32
|
+
* setup 阶段在沙箱里执行 `npx skills add <org/repo>`;
|
|
33
|
+
* 结果写进沙箱工作区的 skills-lock.json,claude CLI 启动时自动读取。
|
|
34
|
+
*
|
|
35
|
+
* @example skills: ["Effect-TS/skills"]
|
|
36
|
+
*/
|
|
37
|
+
skills?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function claudeCodeAgent(config?: ClaudeCodeConfig): Agent {
|
|
41
|
+
const getApiKey = () => config?.apiKey ?? requireEnv("ANTHROPIC_API_KEY");
|
|
42
|
+
const getBaseUrl = () => config?.baseUrl ?? getEnv("ANTHROPIC_BASE_URL");
|
|
43
|
+
|
|
44
|
+
return defineSandboxAgent({
|
|
45
|
+
name: "claude-code",
|
|
46
|
+
capabilities: { conversation: true, toolObservability: true, workspace: true, compactionObservability: true },
|
|
47
|
+
|
|
48
|
+
async setup(sb) {
|
|
49
|
+
// 预制模板已把 claude 烘焙进镜像(PATH 上)就跳过安装;否则 npm 全局装。
|
|
50
|
+
await sb.runShell("command -v claude >/dev/null 2>&1 || npm install -g @anthropic-ai/claude-code");
|
|
51
|
+
|
|
52
|
+
if (config?.mcpServers?.length) {
|
|
53
|
+
const servers: Record<string, object> = {};
|
|
54
|
+
for (const s of config.mcpServers) {
|
|
55
|
+
servers[s.name] = {
|
|
56
|
+
command: s.command,
|
|
57
|
+
...(s.args?.length && { args: s.args }),
|
|
58
|
+
...(s.env && { env: s.env }),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
await shared.writeFile(sb, "~/.claude/claude.json", JSON.stringify({ mcpServers: servers }, null, 2));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (config?.skills?.length) {
|
|
65
|
+
for (const source of config.skills) {
|
|
66
|
+
// source = "Effect-TS/skills"(GitHub org/repo)
|
|
67
|
+
// `npx skills add` 拉 repo、读 manifest、写 skills-lock.json,claude CLI 自动读取。
|
|
68
|
+
await sb.runShell(`npx skills add ${source}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async send(input, ctx) {
|
|
74
|
+
const sb = ctx.sandbox;
|
|
75
|
+
const args = ["--print", "--dangerously-skip-permissions"];
|
|
76
|
+
if (ctx.model) args.push("--model", ctx.model);
|
|
77
|
+
if (config?.maxTurns != null) args.push("--max-turns", String(config.maxTurns));
|
|
78
|
+
if (ctx.flags.webResearch) args.push("--allowedTools", "WebSearch,WebFetch");
|
|
79
|
+
if (!ctx.session.isNew && ctx.session.id) args.push("--resume", ctx.session.id);
|
|
80
|
+
args.push(input.text);
|
|
81
|
+
|
|
82
|
+
const env: Record<string, string> = { ANTHROPIC_API_KEY: getApiKey() };
|
|
83
|
+
const baseUrl = getBaseUrl();
|
|
84
|
+
if (baseUrl) env["ANTHROPIC_BASE_URL"] = baseUrl;
|
|
85
|
+
|
|
86
|
+
const res = await sb.runCommand("claude", args, { env, stream: true });
|
|
87
|
+
|
|
88
|
+
const raw = await shared.captureLatestJsonl(sb, "~/.claude/projects");
|
|
89
|
+
ctx.session.id = shared.sessionIdFromClaudeTranscript(raw) ?? ctx.session.id;
|
|
90
|
+
const parsed = shared.parseClaudeCode(raw);
|
|
91
|
+
return { events: parsed.events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export default claudeCodeAgent();
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { defineSandboxAgent } from "../define.ts";
|
|
2
|
+
import { requireEnv, getEnv } from "../util.ts";
|
|
3
|
+
import { shared } from "./shared.ts";
|
|
4
|
+
import type { Agent, McpServer } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
7
|
+
// OpenAI Codex CLI 的 agent adapter(沙箱型)。
|
|
8
|
+
//
|
|
9
|
+
// 连接方式:在沙箱里 spawn `codex exec --json`,stdout JSONL → parseCodex → 标准事件流。
|
|
10
|
+
// 配置:鉴权本地(config / env),模型交给实验(ctx.model),feature flags 经 ctx.flags。
|
|
11
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface CodexConfig {
|
|
14
|
+
/** 代理 / OpenAI API key。省略时读 CODEX_API_KEY env。 */
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
/** OpenAI 兼容代理 base URL(如 https://s2a.example.com/v1)。省略时读 CODEX_BASE_URL env。 */
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* 额外 MCP server(每个沙箱 setup 时追加进 ~/.codex/config.toml)。
|
|
20
|
+
* 格式对应 codex config.toml 的 [mcp_server.<name>] 表。
|
|
21
|
+
*/
|
|
22
|
+
mcpServers?: McpServer[];
|
|
23
|
+
/**
|
|
24
|
+
* 额外安装的 skill,格式为 GitHub `"org/repo"`(如 `"Effect-TS/skills"`)。
|
|
25
|
+
* setup 阶段执行 `npx skills add <org/repo>`,结果写进 skills-lock.json。
|
|
26
|
+
*
|
|
27
|
+
* @example skills: ["Effect-TS/skills"]
|
|
28
|
+
*/
|
|
29
|
+
skills?: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function codexAgent(config?: CodexConfig): Agent {
|
|
33
|
+
const getApiKey = () => config?.apiKey ?? requireEnv("CODEX_API_KEY");
|
|
34
|
+
const getBaseUrl = () => config?.baseUrl ?? getEnv("CODEX_BASE_URL");
|
|
35
|
+
|
|
36
|
+
return defineSandboxAgent({
|
|
37
|
+
name: "codex",
|
|
38
|
+
capabilities: { conversation: true, toolObservability: true, workspace: true, compactionObservability: true, tracing: true },
|
|
39
|
+
|
|
40
|
+
async setup(sb, ctx) {
|
|
41
|
+
// 预制模板已把 codex 烘焙进镜像(PATH 上)就跳过安装;否则 npm 全局装。
|
|
42
|
+
await sb.runShell("command -v codex >/dev/null 2>&1 || npm install -g @openai/codex");
|
|
43
|
+
|
|
44
|
+
const model = ctx.model ?? "gpt-5.4";
|
|
45
|
+
const effort = (ctx.flags.effort as string | undefined) ?? "medium";
|
|
46
|
+
const base = getBaseUrl();
|
|
47
|
+
|
|
48
|
+
if (base) {
|
|
49
|
+
await shared.writeFile(
|
|
50
|
+
sb,
|
|
51
|
+
"~/.codex/config.toml",
|
|
52
|
+
`model = "${model}"\n` +
|
|
53
|
+
`model_provider = "s2a"\n` +
|
|
54
|
+
`model_reasoning_effort = "${effort}"\n\n` +
|
|
55
|
+
`[model_providers.s2a]\n` +
|
|
56
|
+
`name = "s2a"\n` +
|
|
57
|
+
`base_url = "${base}"\n` +
|
|
58
|
+
`env_key = "CODEX_API_KEY"\n` +
|
|
59
|
+
`wire_api = "responses"\n`,
|
|
60
|
+
);
|
|
61
|
+
} else {
|
|
62
|
+
await shared.writeFile(sb, "~/.codex/config.toml", `model = "${model}"\nmodel_reasoning_effort = "${effort}"\n`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (config?.mcpServers?.length) {
|
|
66
|
+
const mcpToml = config.mcpServers
|
|
67
|
+
.map((s) => {
|
|
68
|
+
const lines: string[] = [`[mcp_server.${s.name}]`, `command = "${s.command}"`];
|
|
69
|
+
if (s.args?.length) lines.push(`args = [${s.args.map((a) => `"${a}"`).join(", ")}]`);
|
|
70
|
+
if (s.env && Object.keys(s.env).length) {
|
|
71
|
+
lines.push(`[mcp_server.${s.name}.env]`);
|
|
72
|
+
for (const [k, v] of Object.entries(s.env)) lines.push(`${k} = "${v}"`);
|
|
73
|
+
}
|
|
74
|
+
return lines.join("\n");
|
|
75
|
+
})
|
|
76
|
+
.join("\n\n");
|
|
77
|
+
await sb.runShell(`cat >> ~/.codex/config.toml <<'MCPEOF'\n\n${mcpToml}\nMCPEOF\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (config?.skills?.length) {
|
|
81
|
+
for (const source of config.skills) {
|
|
82
|
+
await sb.runShell(`npx skills add ${source}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
tracing: {
|
|
88
|
+
protocol: "http/json",
|
|
89
|
+
async configure(sb, ctx) {
|
|
90
|
+
const endpoint = ctx.telemetry!.endpoint;
|
|
91
|
+
const otel =
|
|
92
|
+
`\n[otel]\n` +
|
|
93
|
+
`environment = "niceeval"\n` +
|
|
94
|
+
`exporter = "none"\n` +
|
|
95
|
+
`metrics_exporter = "none"\n\n` +
|
|
96
|
+
`[otel.trace_exporter.otlp-http]\n` +
|
|
97
|
+
`endpoint = "${endpoint}"\n` +
|
|
98
|
+
`protocol = "json"\n`;
|
|
99
|
+
await sb.runShell(`cat >> ~/.codex/config.toml <<'EOF'\n${otel}EOF\n`);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
async send(input, ctx) {
|
|
104
|
+
const sb = ctx.sandbox;
|
|
105
|
+
const flags = "--json --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check";
|
|
106
|
+
const escaped = input.text.replace(/'/g, "'\\''");
|
|
107
|
+
const resuming = !ctx.session.isNew && ctx.session.id;
|
|
108
|
+
const cmd = resuming
|
|
109
|
+
? `codex exec resume ${ctx.session.id} ${flags} '${escaped}'`
|
|
110
|
+
: `codex exec ${flags} '${escaped}'`;
|
|
111
|
+
|
|
112
|
+
const res = await sb.runShell(cmd, { env: { CODEX_API_KEY: getApiKey() }, stream: true });
|
|
113
|
+
|
|
114
|
+
const raw = shared.extractJsonlFromStdout(res.stdout);
|
|
115
|
+
ctx.session.id = shared.codexThreadId(res.stdout) ?? ctx.session.id;
|
|
116
|
+
const parsed = shared.parseCodex(raw);
|
|
117
|
+
return { events: parsed.events, usage: parsed.usage, status: res.exitCode === 0 ? "completed" : "failed" };
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default codexAgent();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// niceeval/adapter 公开导出:「连到哪个 AI」相关的类型 + 内置 adapter + 自定义 adapter 的入口。
|
|
2
|
+
|
|
3
|
+
export { defineAgent, defineSandboxAgent } from "../define.ts";
|
|
4
|
+
export { shared } from "./shared.ts";
|
|
5
|
+
export type { Shared } from "./shared.ts";
|
|
6
|
+
|
|
7
|
+
export { BUILTIN_AGENTS } from "./builtin.ts";
|
|
8
|
+
export { claudeCodeAgent } from "./claude-code.ts";
|
|
9
|
+
export { codexAgent } from "./codex.ts";
|
|
10
|
+
export { bubAgent } from "./bub.ts";
|
|
11
|
+
export type { ClaudeCodeConfig } from "./claude-code.ts";
|
|
12
|
+
export type { CodexConfig } from "./codex.ts";
|
|
13
|
+
export type { BubConfig } from "./bub.ts";
|
|
14
|
+
|
|
15
|
+
export type {
|
|
16
|
+
Agent,
|
|
17
|
+
AgentContext,
|
|
18
|
+
AgentCapabilities,
|
|
19
|
+
AgentSession,
|
|
20
|
+
AgentTracing,
|
|
21
|
+
Telemetry,
|
|
22
|
+
SandboxAgentDef,
|
|
23
|
+
RemoteAgentDef,
|
|
24
|
+
McpServer,
|
|
25
|
+
} from "../types.ts";
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// 沙箱型 adapter 的共享工具(对应 agent-eval 的 shared.ts)。
|
|
2
|
+
//
|
|
3
|
+
// adapter 真正要写的只有「装 CLI / 鉴权 / 拼调用 / 模型 / 读 transcript」那几行,
|
|
4
|
+
// 其余跨 agent 一致的活(装包去重、定位最新 transcript、写配置文件、从 stdout
|
|
5
|
+
// 抠 JSONL、解析成标准事件流)都收在这里,经 `shared` 暴露给用户写的 adapter。
|
|
6
|
+
|
|
7
|
+
import type { ParsedTranscript } from "../o11y/parsers/index.ts";
|
|
8
|
+
import {
|
|
9
|
+
parseCodexTranscript,
|
|
10
|
+
parseClaudeCodeTranscript,
|
|
11
|
+
parseBubTranscript,
|
|
12
|
+
} from "../o11y/parsers/index.ts";
|
|
13
|
+
import type { Sandbox } from "../types.ts";
|
|
14
|
+
import { t } from "../i18n/index.ts";
|
|
15
|
+
|
|
16
|
+
/** 每个沙箱里「已经装过的全局包」去重,避免每轮 send 重复 npm i -g。 */
|
|
17
|
+
const installedBySandbox = new WeakMap<Sandbox, Set<string>>();
|
|
18
|
+
|
|
19
|
+
function installKey(cmd: string, args: string[]): string {
|
|
20
|
+
return [cmd, ...args].join(" ");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function ensureInstalled(sandbox: Sandbox, cmd: string, args: string[]): Promise<void> {
|
|
24
|
+
let set = installedBySandbox.get(sandbox);
|
|
25
|
+
if (!set) {
|
|
26
|
+
set = new Set();
|
|
27
|
+
installedBySandbox.set(sandbox, set);
|
|
28
|
+
}
|
|
29
|
+
const key = installKey(cmd, args);
|
|
30
|
+
if (set.has(key)) return;
|
|
31
|
+
const res = await sandbox.runCommand(cmd, args);
|
|
32
|
+
if (res.exitCode !== 0) {
|
|
33
|
+
const tail = (res.stdout + res.stderr).trim().split("\n").slice(-12).join("\n");
|
|
34
|
+
throw new Error(t("agent.installFailed", { key, tail }));
|
|
35
|
+
}
|
|
36
|
+
set.add(key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 在 dir(可含 ~)下找最新的 *.jsonl,读回其内容;没有则 undefined。 */
|
|
40
|
+
async function captureLatestJsonl(sandbox: Sandbox, dir: string): Promise<string | undefined> {
|
|
41
|
+
try {
|
|
42
|
+
const find = await sandbox.runShell(
|
|
43
|
+
`find ${dir} -type f -name '*.jsonl' -printf '%T@\\t%p\\n' 2>/dev/null | sort -nr | head -1 | cut -f2-`,
|
|
44
|
+
);
|
|
45
|
+
const path = find.stdout.trim();
|
|
46
|
+
if (!path) return undefined;
|
|
47
|
+
return await sandbox.readFile(path);
|
|
48
|
+
} catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 写一个文件到沙箱任意路径(含 ~ / 绝对路径),用随机定界符的 heredoc,内容不被解释。 */
|
|
54
|
+
async function writeFile(sandbox: Sandbox, path: string, content: string): Promise<void> {
|
|
55
|
+
const delim = `FE_EOF_${Date.now().toString(36)}_${Math.floor(Math.random() * 1e9).toString(36)}`;
|
|
56
|
+
// path 不加引号,以便 bash 展开 ~;dirname 也走 $() 不加引号同理。这些是受信内部路径。
|
|
57
|
+
await sandbox.runShell(`mkdir -p $(dirname ${path}) && cat > ${path} <<'${delim}'\n${content}\n${delim}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Claude Code 的 transcript JSONL 里抠 sessionId(取最后一个,供下一轮 --resume)。 */
|
|
61
|
+
function sessionIdFromClaudeTranscript(raw: string | undefined): string | undefined {
|
|
62
|
+
if (!raw) return undefined;
|
|
63
|
+
let last: string | undefined;
|
|
64
|
+
for (const line of raw.split("\n")) {
|
|
65
|
+
const t = line.trim();
|
|
66
|
+
if (!t) continue;
|
|
67
|
+
try {
|
|
68
|
+
const obj = JSON.parse(t) as { sessionId?: unknown; session_id?: unknown };
|
|
69
|
+
const id = (obj.sessionId ?? obj.session_id) as unknown;
|
|
70
|
+
if (typeof id === "string" && id) last = id;
|
|
71
|
+
} catch {
|
|
72
|
+
// 跳过非 JSON 行
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return last;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 从混了普通日志的 stdout 里抠出 JSONL(只留看起来是 JSON 对象的行)。 */
|
|
79
|
+
function extractJsonlFromStdout(stdout: string | undefined): string | undefined {
|
|
80
|
+
if (!stdout || !stdout.trim()) return undefined;
|
|
81
|
+
const lines = stdout.split("\n").filter((line) => {
|
|
82
|
+
const t = line.trim();
|
|
83
|
+
return t.startsWith("{") && t.endsWith("}");
|
|
84
|
+
});
|
|
85
|
+
return lines.length ? lines.join("\n") : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** 从 codex 的 --json stdout 里取 thread_id(thread.started 事件),供下一轮 resume。 */
|
|
89
|
+
function codexThreadId(stdout: string | undefined): string | undefined {
|
|
90
|
+
if (!stdout) return undefined;
|
|
91
|
+
for (const line of stdout.split("\n")) {
|
|
92
|
+
const t = line.trim();
|
|
93
|
+
if (!t) continue;
|
|
94
|
+
try {
|
|
95
|
+
const e = JSON.parse(t) as { type?: string; thread_id?: unknown };
|
|
96
|
+
if (e.type === "thread.started" && typeof e.thread_id === "string") return e.thread_id;
|
|
97
|
+
} catch {
|
|
98
|
+
// 跳过
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 扫 JSONL,返回第一个出现的 field 的字符串值。 */
|
|
105
|
+
function firstJsonField(raw: string | undefined, field: string): string | undefined {
|
|
106
|
+
if (!raw) return undefined;
|
|
107
|
+
for (const line of raw.split("\n")) {
|
|
108
|
+
const t = line.trim();
|
|
109
|
+
if (!t) continue;
|
|
110
|
+
try {
|
|
111
|
+
const obj = JSON.parse(t) as Record<string, unknown>;
|
|
112
|
+
const v = obj[field];
|
|
113
|
+
if (typeof v === "string" && v) return v;
|
|
114
|
+
} catch {
|
|
115
|
+
// 跳过
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `shared`:用户写的沙箱 adapter 唯一要碰的工具袋。解析器把各 agent 的原始 JSONL
|
|
123
|
+
* 归一化成标准事件流 + token 用量(ParsedTranscript),adapter 直接 return 即可。
|
|
124
|
+
*/
|
|
125
|
+
export const shared = {
|
|
126
|
+
ensureInstalled,
|
|
127
|
+
captureLatestJsonl,
|
|
128
|
+
writeFile,
|
|
129
|
+
sessionIdFromClaudeTranscript,
|
|
130
|
+
extractJsonlFromStdout,
|
|
131
|
+
codexThreadId,
|
|
132
|
+
firstJsonField,
|
|
133
|
+
/** 原始 codex JSONL → 标准事件流 + 用量 + 压缩计数。 */
|
|
134
|
+
parseCodex(raw: string | undefined): ParsedTranscript {
|
|
135
|
+
return parseCodexTranscript(raw);
|
|
136
|
+
},
|
|
137
|
+
parseClaudeCode(raw: string | undefined): ParsedTranscript {
|
|
138
|
+
return parseClaudeCodeTranscript(raw);
|
|
139
|
+
},
|
|
140
|
+
parseBub(raw: string | undefined): ParsedTranscript {
|
|
141
|
+
return parseBubTranscript(raw);
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export type Shared = typeof shared;
|