0sec-cli 0.15.0 → 0.18.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/0sec.js +327 -0
- package/README.md +68 -48
- package/attacks/data-exfiltration/pii-leakage.yaml +27 -0
- package/attacks/encoding-bypass/base64-encoding.yaml +24 -0
- package/attacks/jailbreak/dan-roleplay.yaml +27 -0
- package/attacks/jailbreak/hypothetical-scenario.yaml +25 -0
- package/attacks/jailbreak/multilingual-bypass.yaml +22 -0
- package/attacks/output-manipulation/harmful-content.yaml +25 -0
- package/attacks/prompt-injection/context-manipulation.yaml +32 -0
- package/attacks/prompt-injection/direct-injection.yaml +28 -0
- package/attacks/prompt-injection/indirect-injection.yaml +33 -0
- package/attacks/system-prompt-extraction/direct-ask.yaml +30 -0
- package/attacks/system-prompt-extraction/markdown-exfil.yaml +26 -0
- package/attacks/tool-misuse/ssrf-via-tools.yaml +27 -0
- package/chunks/adapt-loop-SMH6V4PD.js +18 -0
- package/chunks/adgraph-JLGA6RYI.js +54 -0
- package/chunks/agent/skills/frameworks/entra-id.yaml +63 -0
- package/chunks/agent/skills/frameworks/graphql-introspection.yaml +116 -0
- package/chunks/agent/skills/frameworks/nextjs.yaml +82 -0
- package/chunks/agent/skills/frameworks/python-web.yaml +85 -0
- package/chunks/agent/skills/frameworks/supabase.yaml +83 -0
- package/chunks/agent/skills/frameworks/wordpress-deep.yaml +122 -0
- package/chunks/agent/skills/techniques/ad-attack-paths.yaml +57 -0
- package/chunks/agent/skills/techniques/advisory-disclosure.yaml +59 -0
- package/chunks/agent/skills/techniques/assumption-mining.yaml +53 -0
- package/chunks/agent/skills/techniques/blind-exploitation.yaml +143 -0
- package/chunks/agent/skills/techniques/crypto-misuse.yaml +88 -0
- package/chunks/agent/skills/techniques/cve-poc-adaptation.yaml +82 -0
- package/chunks/agent/skills/techniques/entra-attack-paths.yaml +55 -0
- package/chunks/agent/skills/techniques/http-conformance-diff.yaml +48 -0
- package/chunks/agent/skills/techniques/jwt-attacks.yaml +137 -0
- package/chunks/agent/skills/techniques/kernel-weaponization.yaml +53 -0
- package/chunks/agent/skills/techniques/llm-excessive-agency.yaml +55 -0
- package/chunks/agent/skills/techniques/llm-insecure-output-handling.yaml +56 -0
- package/chunks/agent/skills/techniques/llm-prompt-injection.yaml +54 -0
- package/chunks/agent/skills/techniques/llm-prompt-layer-write.yaml +68 -0
- package/chunks/agent/skills/techniques/llm-rag-poisoning.yaml +57 -0
- package/chunks/agent/skills/techniques/llm-safety-eval.yaml +51 -0
- package/chunks/agent/skills/techniques/npm-ecosystem.yaml +51 -0
- package/chunks/agent/skills/techniques/poc-verification.yaml +55 -0
- package/chunks/agent/skills/techniques/race-condition.yaml +122 -0
- package/chunks/agent/skills/techniques/scoped-fix.yaml +49 -0
- package/chunks/agent/skills/techniques/seedless-depth-review.yaml +59 -0
- package/chunks/agent/skills/techniques/spec-differential.yaml +48 -0
- package/chunks/agent/skills/techniques/variant-hunting.yaml +55 -0
- package/chunks/agent/skills/vulnerabilities/cardano-eutxo-validators.yaml +72 -0
- package/chunks/agent/skills/vulnerabilities/command-injection.yaml +99 -0
- package/chunks/agent/skills/vulnerabilities/deserialization-chains.yaml +106 -0
- package/chunks/agent/skills/vulnerabilities/native-memory-safety.yaml +93 -0
- package/chunks/agent/skills/vulnerabilities/path-traversal.yaml +78 -0
- package/chunks/agent/skills/vulnerabilities/prototype-pollution.yaml +132 -0
- package/chunks/agent/skills/vulnerabilities/request-smuggling.yaml +129 -0
- package/chunks/agent/skills/vulnerabilities/sqli-advanced.yaml +102 -0
- package/chunks/agent/skills/vulnerabilities/ssrf-bypass.yaml +84 -0
- package/chunks/agent/skills/vulnerabilities/ssti-exploitation.yaml +112 -0
- package/chunks/agent/skills/vulnerabilities/structural-sqli.yaml +91 -0
- package/chunks/appsec-catalog-CBWHGTEL.js +24 -0
- package/chunks/artifact-scraper-HQ7HLP6V.js +34 -0
- package/chunks/assumption-mining-5SNIVVDN.js +79 -0
- package/chunks/chunk-242TMK6G.js +110 -0
- package/chunks/chunk-2JCCA2JL.js +381 -0
- package/chunks/chunk-2RMLOJVB.js +47 -0
- package/chunks/chunk-3KPWWJCI.js +1422 -0
- package/chunks/chunk-3MOLBTLS.js +953 -0
- package/chunks/chunk-53G27VPS.js +136 -0
- package/chunks/chunk-57ZENEX2.js +169 -0
- package/chunks/chunk-5G3ZXFBW.js +182 -0
- package/chunks/chunk-5JI7L7KV.js +74240 -0
- package/chunks/chunk-6L26OBWI.js +1094 -0
- package/chunks/chunk-6LKRLK2R.js +542 -0
- package/chunks/chunk-72CISA2D.js +2597 -0
- package/chunks/chunk-7DQEV5QI.js +287 -0
- package/chunks/chunk-AD7WXDH6.js +3250 -0
- package/chunks/chunk-ADOLI6DT.js +102 -0
- package/chunks/chunk-AHTBZC3E.js +596 -0
- package/chunks/chunk-ASUR522M.js +518 -0
- package/chunks/chunk-ATATQACO.js +471 -0
- package/chunks/chunk-BATRQBOR.js +1732 -0
- package/chunks/chunk-BFR2CDV5.js +5735 -0
- package/chunks/chunk-BFRB6HB2.js +235 -0
- package/chunks/chunk-BKZFDZ23.js +361 -0
- package/chunks/chunk-BVAZO4WA.js +193 -0
- package/chunks/chunk-BZNEY2ZS.js +33225 -0
- package/chunks/chunk-C2WXWFBB.js +1070 -0
- package/chunks/chunk-CMKXP5RR.js +917 -0
- package/chunks/chunk-DW5UWPFY.js +275 -0
- package/chunks/chunk-F3WBKITT.js +687 -0
- package/chunks/chunk-GMT2AZKM.js +2439 -0
- package/chunks/chunk-H2FFLZNK.js +3 -0
- package/chunks/chunk-IOL7D5YV.js +515 -0
- package/chunks/chunk-IR537GON.js +49 -0
- package/chunks/chunk-JQHOLLTD.js +3729 -0
- package/chunks/chunk-KLTNTE2Z.js +121 -0
- package/chunks/chunk-LP3HHYQU.js +245 -0
- package/chunks/chunk-MYVT64FN.js +16 -0
- package/chunks/chunk-N7BOUYEV.js +852 -0
- package/chunks/chunk-OEFNRYI2.js +150 -0
- package/chunks/chunk-P6WKNFWX.js +1968 -0
- package/chunks/chunk-QI233I24.js +333 -0
- package/chunks/chunk-RJWOYEOG.js +271 -0
- package/chunks/chunk-RRMJC3ZE.js +105 -0
- package/chunks/chunk-RWONANDA.js +973 -0
- package/chunks/chunk-SAFFWQW4.js +5921 -0
- package/chunks/chunk-SF4KZ4O3.js +502 -0
- package/chunks/chunk-SVJEDVYK.js +808 -0
- package/chunks/chunk-TSK2AG6J.js +5108 -0
- package/chunks/chunk-U5FLXGHT.js +2010 -0
- package/chunks/chunk-UCYRA73C.js +662 -0
- package/chunks/chunk-UR4ELX2Y.js +4784 -0
- package/chunks/chunk-VQG4FT5D.js +685 -0
- package/chunks/chunk-WEBMRWEG.js +9337 -0
- package/chunks/chunk-WU6AFRAZ.js +1167 -0
- package/chunks/chunk-WVTBZEQO.js +1531 -0
- package/chunks/chunk-WXQ6BJ6A.js +596 -0
- package/chunks/chunk-XM5NLHYU.js +600 -0
- package/chunks/chunk-YLMN3N25.js +4054 -0
- package/chunks/chunk-YYRBQVFE.js +2572 -0
- package/chunks/chunk-Z5FA2XOX.js +2798 -0
- package/chunks/commands-TBC2F5CE.js +16999 -0
- package/chunks/corpus-v1.json +403 -0
- package/chunks/cost-ledger-NU63MYZM.js +13 -0
- package/chunks/data/appsec-archetypes.json +102 -0
- package/chunks/data/chromium-archetypes.json +204 -0
- package/chunks/data/freebsd-archetypes.json +171 -0
- package/chunks/data/kernel-archetypes.json +611 -0
- package/chunks/db-KFWOAUUB.js +16 -0
- package/chunks/disclose-EKTWSCGW.js +132 -0
- package/chunks/dist-4MAW5X6L.js +74 -0
- package/chunks/dist-4YICS2J2.js +2776 -0
- package/chunks/dist-FMGSR3DW.js +159 -0
- package/chunks/eval-runner-7ZF472KV.js +27 -0
- package/chunks/example-manifest.json +95 -0
- package/chunks/exploit-agent-45YQOHPJ.js +98 -0
- package/chunks/exploit-autoclimb-UZGEMGYT.js +81 -0
- package/chunks/exploit-climb-SG23KB2G.js +358 -0
- package/chunks/fix-IG7LYXH3.js +12 -0
- package/chunks/github-issues-MJ6OYOOU.js +157 -0
- package/chunks/harness-7ODOLOWU.js +22 -0
- package/chunks/http-conformance-DU66MZIU.js +11 -0
- package/chunks/http-sender-GWH2IYEA.js +10 -0
- package/chunks/hunt-scan-MMKUOETU.js +38 -0
- package/chunks/identity-6ZAIIWOR.js +191 -0
- package/chunks/kernel-primitive-7U6CWHXD.js +44 -0
- package/chunks/kernel-vm-runner-3JAU4O6J.js +62 -0
- package/chunks/memsafety-scan-J7SECV5W.js +16 -0
- package/chunks/native-loop-RJCJPOUS.js +54 -0
- package/chunks/npm-detectors-KB5Y5ZFX.js +69 -0
- package/chunks/npm-dynamic-discovery-AHROOZDB.js +13 -0
- package/chunks/orchestrate-OC2VZN2X.js +62 -0
- package/chunks/pipeline-BZK5BLJR.js +14 -0
- package/chunks/pre-recon-cve-66EB6G4M.js +351 -0
- package/chunks/prepare-MYY743TK.js +13 -0
- package/chunks/process-LQRCS5OY.js +13 -0
- package/chunks/replay-runner-45G3VU7X.js +39 -0
- package/chunks/run-D56MPDKY.js +32716 -0
- package/chunks/runtime-G7YWIO3B.js +43 -0
- package/chunks/runtime-J7PLXZNM.js +12 -0
- package/chunks/scan-stream-FHI2FYZE.js +96 -0
- package/chunks/scope-BI7BF4ZY.js +18 -0
- package/chunks/session-store-GZ4A4KYT.js +28 -0
- package/chunks/source-files-PWRQR6LY.js +12 -0
- package/chunks/specdrift-WCLH6TTQ.js +19 -0
- package/chunks/variant-candidates-YERCYMPD.js +13 -0
- package/chunks/web-recon-prepass-JJGPEOLE.js +1148 -0
- package/dashboard/assets/0sec-icon-66SreztZ.gif +0 -0
- package/dashboard/assets/bot-yILPnRgD.js +1 -0
- package/dashboard/assets/chevron-down-BeDJ-Vka.js +1 -0
- package/dashboard/assets/circle-alert-BEnMBMT-.js +1 -0
- package/dashboard/assets/client-DRYsuOGl.js +9 -0
- package/dashboard/assets/copy-Bi5vzoF9.js +1 -0
- package/dashboard/assets/desktop-CXTQonNm.js +32 -0
- package/dashboard/assets/desktop-NOekk4QS.css +1 -0
- package/dashboard/assets/dist-CmX-h7a4.js +1 -0
- package/dashboard/assets/dist-DAwYQmf_.js +45 -0
- package/dashboard/assets/findings-page-CACW9nw2.js +5 -0
- package/dashboard/assets/format-PoISqsES.js +1 -0
- package/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-BSMlKf0J.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-400-normal-CEL4l2ZJ.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-DMdlQ8Kv.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-cyrillic-ext-400-normal-xuaO2J-f.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-BmRBH3aV.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-latin-ext-400-normal-D3D2R8hC.woff +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-BulugwFq.woff2 +0 -0
- package/dashboard/assets/ibm-plex-mono-vietnamese-400-normal-DDuiU_S-.woff +0 -0
- package/dashboard/assets/jsx-runtime-C7oxC63R.js +1 -0
- package/dashboard/assets/live-page-DrgQqQK5.js +1 -0
- package/dashboard/assets/meta-tile-CcskIV_o.js +1 -0
- package/dashboard/assets/operations-BKE8T-vY.css +2 -0
- package/dashboard/assets/operations-D8nUP5_m.js +4 -0
- package/dashboard/assets/operations-app-CQQhG54n.js +2 -0
- package/dashboard/assets/overview-page-DTGS8dPo.js +1 -0
- package/dashboard/assets/page-header-MFVvEtZW.js +1 -0
- package/dashboard/assets/play-Dwo-9xt6.js +1 -0
- package/dashboard/assets/scans-page-ChVtq8Us.js +1 -0
- package/dashboard/assets/search-CiooTLck.js +1 -0
- package/dashboard/assets/siren-CCBwTSuk.js +1 -0
- package/dashboard/assets/table-BYS-nIVA.js +1 -0
- package/dashboard/assets/tabs-DivxbhQy.js +1 -0
- package/dashboard/desktop.html +21 -0
- package/dashboard/index.html +15 -0
- package/package.json +30 -18
- package/bin/0sec.cjs +0 -361
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __0secCreateRequire } from "node:module";
|
|
3
|
+
const require = __0secCreateRequire(import.meta.url);
|
|
4
|
+
import {
|
|
5
|
+
LlmApiRuntime
|
|
6
|
+
} from "./chunk-UR4ELX2Y.js";
|
|
7
|
+
import {
|
|
8
|
+
ProcessRuntime
|
|
9
|
+
} from "./chunk-ATATQACO.js";
|
|
10
|
+
|
|
11
|
+
// packages/core/dist/runtime/cli-native.js
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
var CliNativeRuntime = class {
|
|
14
|
+
type;
|
|
15
|
+
config;
|
|
16
|
+
command;
|
|
17
|
+
/** Populated from the first turn's stream-json `system` event. */
|
|
18
|
+
sessionId;
|
|
19
|
+
constructor(config) {
|
|
20
|
+
if (config.type === "api") {
|
|
21
|
+
throw new Error("CliNativeRuntime does not support type=api. Use LlmApiRuntime instead.");
|
|
22
|
+
}
|
|
23
|
+
this.type = config.type;
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.command = config.type;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Reset the cached session id so the next call starts a brand-new
|
|
29
|
+
* Claude Code session. The native loop uses this between scans.
|
|
30
|
+
*/
|
|
31
|
+
resetSession() {
|
|
32
|
+
this.sessionId = void 0;
|
|
33
|
+
}
|
|
34
|
+
async isAvailable() {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
const proc = spawn(this.command, ["--version"], {
|
|
37
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
38
|
+
env: { ...process.env, ...this.config.env },
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
40
|
+
});
|
|
41
|
+
const timer = setTimeout(() => {
|
|
42
|
+
proc.kill();
|
|
43
|
+
resolve(false);
|
|
44
|
+
}, 5e3);
|
|
45
|
+
proc.on("close", (code) => {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
resolve(code === 0);
|
|
48
|
+
});
|
|
49
|
+
proc.on("error", () => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
resolve(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async executeNative(system, messages, _tools, callbacks) {
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
if (this.type !== "claude") {
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: "" }],
|
|
60
|
+
stopReason: "error",
|
|
61
|
+
durationMs: Date.now() - start,
|
|
62
|
+
error: `Native multi-turn agent loop is only supported for the 'claude' CLI runtime. '${this.type}' does not yet expose a stable session-resume protocol \u2014 use --runtime claude (with 'claude login') for subscription auth, or set ANTHROPIC_API_KEY / OPENROUTER_API_KEY / OPENAI_API_KEY for --runtime api.`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
const promptText = buildClaudePromptFromLastUser(messages);
|
|
66
|
+
if (!promptText) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: "" }],
|
|
69
|
+
stopReason: "error",
|
|
70
|
+
durationMs: Date.now() - start,
|
|
71
|
+
error: "CliNativeRuntime: last message was not a user turn (cannot prompt Claude Code)."
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const args = ["-p", promptText, "--verbose", "--output-format", "stream-json"];
|
|
75
|
+
if (this.sessionId) {
|
|
76
|
+
args.push("--resume", this.sessionId);
|
|
77
|
+
} else if (system && system.trim().length > 0) {
|
|
78
|
+
args.push("--system-prompt", system);
|
|
79
|
+
}
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
let stdoutBuf = "";
|
|
82
|
+
let stderrBuf = "";
|
|
83
|
+
let timedOut = false;
|
|
84
|
+
const content = [];
|
|
85
|
+
let stopReason = "end_turn";
|
|
86
|
+
let usage;
|
|
87
|
+
let sawAssistantEnd = false;
|
|
88
|
+
const proc = spawn(this.command, args, {
|
|
89
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
90
|
+
env: { ...process.env, ...this.config.env },
|
|
91
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
92
|
+
});
|
|
93
|
+
proc.stdout?.on("data", (chunk) => {
|
|
94
|
+
stdoutBuf += chunk.toString("utf-8");
|
|
95
|
+
let nl = stdoutBuf.indexOf("\n");
|
|
96
|
+
while (nl >= 0) {
|
|
97
|
+
const line = stdoutBuf.slice(0, nl).trim();
|
|
98
|
+
stdoutBuf = stdoutBuf.slice(nl + 1);
|
|
99
|
+
nl = stdoutBuf.indexOf("\n");
|
|
100
|
+
if (!line)
|
|
101
|
+
continue;
|
|
102
|
+
const event = tryParseJson(line);
|
|
103
|
+
if (!event)
|
|
104
|
+
continue;
|
|
105
|
+
if (typeof event.session_id === "string" && !this.sessionId) {
|
|
106
|
+
this.sessionId = event.session_id;
|
|
107
|
+
}
|
|
108
|
+
if (event.type === "assistant" && event.message && Array.isArray(event.message.content)) {
|
|
109
|
+
for (const block of event.message.content) {
|
|
110
|
+
if (block?.type === "text" && typeof block.text === "string") {
|
|
111
|
+
content.push({ type: "text", text: block.text });
|
|
112
|
+
if (block.text.trim())
|
|
113
|
+
callbacks?.onThinking?.(block.text);
|
|
114
|
+
} else if (block?.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
115
|
+
content.push({
|
|
116
|
+
type: "tool_use",
|
|
117
|
+
id: block.id,
|
|
118
|
+
name: block.name,
|
|
119
|
+
input: block.input && typeof block.input === "object" ? block.input : {}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const u = event.message?.usage;
|
|
124
|
+
if (u) {
|
|
125
|
+
const partial = readUsage(u);
|
|
126
|
+
if (partial)
|
|
127
|
+
callbacks?.onUsage?.(partial);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (event.type === "result") {
|
|
131
|
+
sawAssistantEnd = true;
|
|
132
|
+
stopReason = mapClaudeStopReason(event.stop_reason ?? event.subtype);
|
|
133
|
+
const u = event.usage;
|
|
134
|
+
if (u)
|
|
135
|
+
usage = readUsage(u) ?? usage;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
proc.stderr?.on("data", (chunk) => {
|
|
140
|
+
stderrBuf += chunk.toString("utf-8");
|
|
141
|
+
});
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
timedOut = true;
|
|
144
|
+
proc.kill("SIGTERM");
|
|
145
|
+
setTimeout(() => proc.kill("SIGKILL"), 5e3).unref();
|
|
146
|
+
}, this.config.timeout || 6e5);
|
|
147
|
+
proc.on("error", (err) => {
|
|
148
|
+
clearTimeout(timer);
|
|
149
|
+
this.sessionId = void 0;
|
|
150
|
+
resolve({
|
|
151
|
+
content: [{ type: "text", text: "" }],
|
|
152
|
+
stopReason: "error",
|
|
153
|
+
durationMs: Date.now() - start,
|
|
154
|
+
error: `claude CLI spawn failed: ${err.message}`
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
proc.on("close", (code) => {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
if (timedOut) {
|
|
160
|
+
this.sessionId = void 0;
|
|
161
|
+
resolve({
|
|
162
|
+
content: content.length > 0 ? content : [{ type: "text", text: "" }],
|
|
163
|
+
stopReason: "error",
|
|
164
|
+
usage,
|
|
165
|
+
durationMs: Date.now() - start,
|
|
166
|
+
error: `claude CLI timed out after ${this.config.timeout}ms`
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (code !== 0 && !sawAssistantEnd) {
|
|
171
|
+
const hint = extractAuthHint(stderrBuf);
|
|
172
|
+
this.sessionId = void 0;
|
|
173
|
+
resolve({
|
|
174
|
+
content: [{ type: "text", text: "" }],
|
|
175
|
+
stopReason: "error",
|
|
176
|
+
usage,
|
|
177
|
+
durationMs: Date.now() - start,
|
|
178
|
+
error: `claude CLI exited ${code}` + (hint ? ` \u2014 ${hint}` : "") + (stderrBuf.trim() ? `
|
|
179
|
+
stderr: ${stderrBuf.trim().slice(0, 500)}` : "")
|
|
180
|
+
});
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
resolve({
|
|
184
|
+
content: content.length > 0 ? content : [{ type: "text", text: "" }],
|
|
185
|
+
stopReason,
|
|
186
|
+
usage,
|
|
187
|
+
durationMs: Date.now() - start
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
function buildClaudePromptFromLastUser(messages) {
|
|
194
|
+
let last;
|
|
195
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
196
|
+
if (messages[i].role === "user") {
|
|
197
|
+
last = messages[i];
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (!last)
|
|
202
|
+
return "";
|
|
203
|
+
const toolResults = [];
|
|
204
|
+
const textParts = [];
|
|
205
|
+
for (const block of last.content) {
|
|
206
|
+
if (block.type === "tool_result") {
|
|
207
|
+
toolResults.push({
|
|
208
|
+
tool_use_id: block.tool_use_id,
|
|
209
|
+
content: block.content,
|
|
210
|
+
...block.is_error ? { is_error: true } : {}
|
|
211
|
+
});
|
|
212
|
+
} else if (block.type === "text") {
|
|
213
|
+
textParts.push(block.text);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const parts = [];
|
|
217
|
+
if (toolResults.length > 0) {
|
|
218
|
+
parts.push(`[0sec:tool_results]
|
|
219
|
+
${JSON.stringify(toolResults, null, 2)}`);
|
|
220
|
+
}
|
|
221
|
+
if (textParts.length > 0) {
|
|
222
|
+
parts.push(textParts.join("\n").trim());
|
|
223
|
+
}
|
|
224
|
+
return parts.filter((p) => p.length > 0).join("\n\n").trim();
|
|
225
|
+
}
|
|
226
|
+
function mapClaudeStopReason(raw) {
|
|
227
|
+
if (!raw)
|
|
228
|
+
return "end_turn";
|
|
229
|
+
switch (raw) {
|
|
230
|
+
case "tool_use":
|
|
231
|
+
return "tool_use";
|
|
232
|
+
case "max_tokens":
|
|
233
|
+
case "success_max_turns":
|
|
234
|
+
return "max_tokens";
|
|
235
|
+
case "end_turn":
|
|
236
|
+
case "success":
|
|
237
|
+
case "stop_sequence":
|
|
238
|
+
return "end_turn";
|
|
239
|
+
case "error":
|
|
240
|
+
case "error_max_turns":
|
|
241
|
+
case "error_during_execution":
|
|
242
|
+
return "error";
|
|
243
|
+
default:
|
|
244
|
+
return "end_turn";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function tryParseJson(line) {
|
|
248
|
+
try {
|
|
249
|
+
const parsed = JSON.parse(line);
|
|
250
|
+
if (parsed && typeof parsed === "object") {
|
|
251
|
+
return parsed;
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
function readUsage(u) {
|
|
258
|
+
const input = num(u.input_tokens) ?? num(u.prompt_tokens);
|
|
259
|
+
const output = num(u.output_tokens) ?? num(u.completion_tokens);
|
|
260
|
+
if (input === void 0 && output === void 0)
|
|
261
|
+
return void 0;
|
|
262
|
+
return { inputTokens: input ?? 0, outputTokens: output ?? 0 };
|
|
263
|
+
}
|
|
264
|
+
function num(v) {
|
|
265
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
266
|
+
return v;
|
|
267
|
+
if (typeof v === "string" && /^\d+$/.test(v))
|
|
268
|
+
return Number(v);
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
function extractAuthHint(stderr) {
|
|
272
|
+
if (!stderr)
|
|
273
|
+
return void 0;
|
|
274
|
+
if (/please\s+run\s+claude\s+login/i.test(stderr)) {
|
|
275
|
+
return "run `claude login` to authenticate the CLI with your subscription";
|
|
276
|
+
}
|
|
277
|
+
if (/not\s+(?:logged|authenticated)/i.test(stderr)) {
|
|
278
|
+
return "Claude Code reports the user is not authenticated \u2014 run `claude login`";
|
|
279
|
+
}
|
|
280
|
+
if (/session\s+expired/i.test(stderr)) {
|
|
281
|
+
return "Claude Code session expired \u2014 run `claude login` again";
|
|
282
|
+
}
|
|
283
|
+
return void 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// packages/core/dist/runtime/openrouter.js
|
|
287
|
+
var DEFAULT_ENSEMBLE_MODELS = [
|
|
288
|
+
"anthropic/claude-sonnet-4.6",
|
|
289
|
+
"google/gemini-2.5-pro",
|
|
290
|
+
"openai/o3",
|
|
291
|
+
"deepseek/deepseek-chat",
|
|
292
|
+
"meta-llama/llama-4-maverick",
|
|
293
|
+
"qwen/qwen3-235b-a22b"
|
|
294
|
+
];
|
|
295
|
+
function scoreResult(result) {
|
|
296
|
+
let score = 0;
|
|
297
|
+
if (result.stopReason === "error")
|
|
298
|
+
return -1;
|
|
299
|
+
if (result.stopReason === "end_turn")
|
|
300
|
+
score += 100;
|
|
301
|
+
if (result.stopReason === "tool_use")
|
|
302
|
+
score += 80;
|
|
303
|
+
for (const block of result.content) {
|
|
304
|
+
if (block.type === "text") {
|
|
305
|
+
score += Math.min(block.text.length / 100, 50);
|
|
306
|
+
}
|
|
307
|
+
if (block.type === "tool_use") {
|
|
308
|
+
score += 20;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return score;
|
|
312
|
+
}
|
|
313
|
+
var OpenRouterRuntime = class _OpenRouterRuntime {
|
|
314
|
+
type = "api";
|
|
315
|
+
config;
|
|
316
|
+
apiKey;
|
|
317
|
+
singleRuntime = null;
|
|
318
|
+
ensembleRuntimes = [];
|
|
319
|
+
constructor(config = {}) {
|
|
320
|
+
this.config = config;
|
|
321
|
+
this.apiKey = config.apiKey ?? process.env.OPENROUTER_API_KEY ?? "";
|
|
322
|
+
if (config.ensembleModels && config.ensembleModels.length > 0) {
|
|
323
|
+
for (const model of config.ensembleModels) {
|
|
324
|
+
this.ensembleRuntimes.push({
|
|
325
|
+
model,
|
|
326
|
+
runtime: this.createModelRuntime(model)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const model = config.model ?? process.env["0SEC_MODEL"] ?? "anthropic/claude-sonnet-4.6";
|
|
331
|
+
this.singleRuntime = this.createModelRuntime(model);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
createModelRuntime(model) {
|
|
335
|
+
const runtimeConfig = {
|
|
336
|
+
type: "api",
|
|
337
|
+
timeout: this.config.timeout ?? 12e4,
|
|
338
|
+
model,
|
|
339
|
+
apiKey: this.apiKey
|
|
340
|
+
};
|
|
341
|
+
return new LlmApiRuntime(runtimeConfig);
|
|
342
|
+
}
|
|
343
|
+
/** Get the model name(s) this runtime is configured for. */
|
|
344
|
+
get models() {
|
|
345
|
+
if (this.ensembleRuntimes.length > 0) {
|
|
346
|
+
return this.ensembleRuntimes.map((e) => e.model);
|
|
347
|
+
}
|
|
348
|
+
return [this.config.model ?? process.env["0SEC_MODEL"] ?? "anthropic/claude-sonnet-4.6"];
|
|
349
|
+
}
|
|
350
|
+
/** Whether this runtime is in ensemble mode. */
|
|
351
|
+
get isEnsemble() {
|
|
352
|
+
return this.ensembleRuntimes.length > 0;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Execute a single native call. In ensemble mode, runs all models in
|
|
356
|
+
* parallel (up to maxConcurrency) and returns the best result.
|
|
357
|
+
*/
|
|
358
|
+
async executeNative(system, messages, tools, callbacks) {
|
|
359
|
+
if (this.singleRuntime) {
|
|
360
|
+
return this.singleRuntime.executeNative(system, messages, tools, callbacks);
|
|
361
|
+
}
|
|
362
|
+
const maxConcurrency = this.config.maxConcurrency ?? 3;
|
|
363
|
+
const results = [];
|
|
364
|
+
const pending = [...this.ensembleRuntimes];
|
|
365
|
+
while (pending.length > 0) {
|
|
366
|
+
const batch = pending.splice(0, maxConcurrency);
|
|
367
|
+
const batchResults = await Promise.allSettled(batch.map(async ({ model, runtime }) => {
|
|
368
|
+
const result = await runtime.executeNative(system, messages, tools, callbacks);
|
|
369
|
+
return { model, result };
|
|
370
|
+
}));
|
|
371
|
+
for (const settled of batchResults) {
|
|
372
|
+
if (settled.status === "fulfilled") {
|
|
373
|
+
results.push(settled.value);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (results.length === 0) {
|
|
378
|
+
return {
|
|
379
|
+
content: [{ type: "text", text: "" }],
|
|
380
|
+
stopReason: "error",
|
|
381
|
+
durationMs: 0,
|
|
382
|
+
error: "All ensemble models failed"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
let best = results[0];
|
|
386
|
+
let bestScore = scoreResult(best.result);
|
|
387
|
+
for (let i = 1; i < results.length; i++) {
|
|
388
|
+
const s = scoreResult(results[i].result);
|
|
389
|
+
if (s > bestScore) {
|
|
390
|
+
best = results[i];
|
|
391
|
+
bestScore = s;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return best.result;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Run ensemble and return ALL results (not just the best).
|
|
398
|
+
* Useful for the benchmark runner which may want to evaluate each model separately.
|
|
399
|
+
*/
|
|
400
|
+
async executeEnsemble(system, messages, tools) {
|
|
401
|
+
const runtimes = this.ensembleRuntimes.length > 0 ? this.ensembleRuntimes : [{ model: this.config.model ?? "default", runtime: this.singleRuntime }];
|
|
402
|
+
const maxConcurrency = this.config.maxConcurrency ?? 3;
|
|
403
|
+
const results = [];
|
|
404
|
+
const pending = [...runtimes];
|
|
405
|
+
while (pending.length > 0) {
|
|
406
|
+
const batch = pending.splice(0, maxConcurrency);
|
|
407
|
+
const batchResults = await Promise.allSettled(batch.map(async ({ model, runtime }) => {
|
|
408
|
+
const result = await runtime.executeNative(system, messages, tools);
|
|
409
|
+
return { model, result };
|
|
410
|
+
}));
|
|
411
|
+
for (const settled of batchResults) {
|
|
412
|
+
if (settled.status === "fulfilled") {
|
|
413
|
+
results.push(settled.value);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return results;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Create a new OpenRouterRuntime for a specific model from this ensemble.
|
|
421
|
+
* Useful for model rotation on retries.
|
|
422
|
+
*/
|
|
423
|
+
forModel(model) {
|
|
424
|
+
return new _OpenRouterRuntime({
|
|
425
|
+
...this.config,
|
|
426
|
+
model,
|
|
427
|
+
ensembleModels: void 0
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Get the next model to try for retry-based rotation.
|
|
432
|
+
* Cycles through ensemble models based on the attempt number.
|
|
433
|
+
*/
|
|
434
|
+
getModelForAttempt(attempt) {
|
|
435
|
+
const models = this.ensembleRuntimes.length > 0 ? this.ensembleRuntimes.map((e) => e.model) : DEFAULT_ENSEMBLE_MODELS;
|
|
436
|
+
return models[attempt % models.length];
|
|
437
|
+
}
|
|
438
|
+
async isAvailable() {
|
|
439
|
+
return !!this.apiKey;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
// packages/core/dist/runtime/ollama.js
|
|
444
|
+
var DEFAULT_HOST = "http://localhost:11434";
|
|
445
|
+
function resolveHost(explicit) {
|
|
446
|
+
const fromEnv = process.env.OLLAMA_HOST?.trim();
|
|
447
|
+
return (explicit && explicit.length > 0 ? explicit : fromEnv) || DEFAULT_HOST;
|
|
448
|
+
}
|
|
449
|
+
function toOllamaMessages(system, messages) {
|
|
450
|
+
const out = [];
|
|
451
|
+
if (system)
|
|
452
|
+
out.push({ role: "system", content: system });
|
|
453
|
+
for (const msg of messages) {
|
|
454
|
+
const textParts = [];
|
|
455
|
+
for (const block of msg.content) {
|
|
456
|
+
if (block.type === "text")
|
|
457
|
+
textParts.push(block.text);
|
|
458
|
+
else if (block.type === "tool_use") {
|
|
459
|
+
textParts.push(`[tool_call name=${block.name} input=${JSON.stringify(block.input)}]`);
|
|
460
|
+
} else if (block.type === "tool_result") {
|
|
461
|
+
out.push({
|
|
462
|
+
role: "tool",
|
|
463
|
+
content: block.content,
|
|
464
|
+
tool_call_id: block.tool_use_id
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (textParts.length > 0) {
|
|
469
|
+
out.push({ role: msg.role, content: textParts.join("\n") });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
function toOllamaTools(tools) {
|
|
475
|
+
return tools.map((t) => ({
|
|
476
|
+
type: "function",
|
|
477
|
+
function: {
|
|
478
|
+
name: t.name,
|
|
479
|
+
description: t.description,
|
|
480
|
+
parameters: t.input_schema
|
|
481
|
+
}
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
function parseToolCalls(calls) {
|
|
485
|
+
if (!calls || calls.length === 0)
|
|
486
|
+
return [];
|
|
487
|
+
const blocks = [];
|
|
488
|
+
for (let i = 0; i < calls.length; i++) {
|
|
489
|
+
const c = calls[i];
|
|
490
|
+
let input;
|
|
491
|
+
if (typeof c.function.arguments === "string") {
|
|
492
|
+
try {
|
|
493
|
+
input = JSON.parse(c.function.arguments);
|
|
494
|
+
} catch {
|
|
495
|
+
input = { _raw: c.function.arguments };
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
input = c.function.arguments;
|
|
499
|
+
}
|
|
500
|
+
blocks.push({
|
|
501
|
+
type: "tool_use",
|
|
502
|
+
// Ollama doesn't return a per-call id, so synthesise one stable
|
|
503
|
+
// within this turn. Agent dispatch only needs uniqueness per turn.
|
|
504
|
+
id: `ollama_${Date.now()}_${i}`,
|
|
505
|
+
name: c.function.name,
|
|
506
|
+
input
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
return blocks;
|
|
510
|
+
}
|
|
511
|
+
var OllamaRuntime = class {
|
|
512
|
+
type = "ollama";
|
|
513
|
+
host;
|
|
514
|
+
model;
|
|
515
|
+
timeout;
|
|
516
|
+
temperature;
|
|
517
|
+
fetchImpl;
|
|
518
|
+
stream;
|
|
519
|
+
constructor(opts) {
|
|
520
|
+
if (!opts.model) {
|
|
521
|
+
throw new Error("OllamaRuntime requires a model (e.g. 'gemma4:27b').");
|
|
522
|
+
}
|
|
523
|
+
this.host = resolveHost(opts.host);
|
|
524
|
+
this.model = opts.model;
|
|
525
|
+
this.timeout = opts.timeout;
|
|
526
|
+
this.temperature = opts.temperature ?? 0.6;
|
|
527
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
528
|
+
this.stream = opts.stream ?? true;
|
|
529
|
+
}
|
|
530
|
+
/** Cheap health probe — `/api/tags` is the canonical Ollama liveness check. */
|
|
531
|
+
async isAvailable() {
|
|
532
|
+
try {
|
|
533
|
+
const ctl = new AbortController();
|
|
534
|
+
const t = setTimeout(() => ctl.abort(), 2e3);
|
|
535
|
+
const res = await this.fetchImpl(`${this.host}/api/tags`, { signal: ctl.signal });
|
|
536
|
+
clearTimeout(t);
|
|
537
|
+
return res.ok;
|
|
538
|
+
} catch {
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/** Legacy text-in/text-out path. Used by stages that haven't migrated to NativeRuntime. */
|
|
543
|
+
async execute(prompt, _context) {
|
|
544
|
+
const start = Date.now();
|
|
545
|
+
try {
|
|
546
|
+
const body = {
|
|
547
|
+
model: this.model,
|
|
548
|
+
messages: [{ role: "user", content: prompt }],
|
|
549
|
+
stream: false,
|
|
550
|
+
options: { temperature: this.temperature }
|
|
551
|
+
};
|
|
552
|
+
const res = await this.postChat(body);
|
|
553
|
+
const usage = readUsage2(res);
|
|
554
|
+
return {
|
|
555
|
+
output: res.message.content,
|
|
556
|
+
exitCode: 0,
|
|
557
|
+
timedOut: false,
|
|
558
|
+
durationMs: Date.now() - start,
|
|
559
|
+
usage
|
|
560
|
+
};
|
|
561
|
+
} catch (err) {
|
|
562
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
563
|
+
return {
|
|
564
|
+
output: "",
|
|
565
|
+
exitCode: 1,
|
|
566
|
+
timedOut: msg.includes("AbortError"),
|
|
567
|
+
durationMs: Date.now() - start,
|
|
568
|
+
error: msg
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
async executeNative(system, messages, tools, callbacks) {
|
|
573
|
+
const start = Date.now();
|
|
574
|
+
const useStream = this.stream;
|
|
575
|
+
try {
|
|
576
|
+
const body = {
|
|
577
|
+
model: this.model,
|
|
578
|
+
messages: toOllamaMessages(system, messages),
|
|
579
|
+
tools: tools.length > 0 ? toOllamaTools(tools) : void 0,
|
|
580
|
+
stream: useStream,
|
|
581
|
+
options: { temperature: this.temperature }
|
|
582
|
+
};
|
|
583
|
+
const aggregate = useStream ? await this.streamChat(body, callbacks) : await this.postChat(body);
|
|
584
|
+
const usage = readUsage2(aggregate);
|
|
585
|
+
if (callbacks?.onUsage && usage)
|
|
586
|
+
callbacks.onUsage(usage);
|
|
587
|
+
const content = [];
|
|
588
|
+
if (aggregate.message.content && aggregate.message.content.trim()) {
|
|
589
|
+
content.push({ type: "text", text: aggregate.message.content });
|
|
590
|
+
}
|
|
591
|
+
const toolBlocks = parseToolCalls(aggregate.message.tool_calls);
|
|
592
|
+
content.push(...toolBlocks);
|
|
593
|
+
let stopReason;
|
|
594
|
+
if (toolBlocks.length > 0)
|
|
595
|
+
stopReason = "tool_use";
|
|
596
|
+
else if (aggregate.done_reason === "length")
|
|
597
|
+
stopReason = "max_tokens";
|
|
598
|
+
else
|
|
599
|
+
stopReason = "end_turn";
|
|
600
|
+
return {
|
|
601
|
+
content,
|
|
602
|
+
stopReason,
|
|
603
|
+
usage,
|
|
604
|
+
durationMs: Date.now() - start
|
|
605
|
+
};
|
|
606
|
+
} catch (err) {
|
|
607
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
608
|
+
return {
|
|
609
|
+
content: [{ type: "text", text: "" }],
|
|
610
|
+
stopReason: "error",
|
|
611
|
+
durationMs: Date.now() - start,
|
|
612
|
+
error: msg
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
async postChat(body) {
|
|
617
|
+
const ctl = new AbortController();
|
|
618
|
+
const timer = setTimeout(() => ctl.abort(), this.timeout);
|
|
619
|
+
try {
|
|
620
|
+
const res = await this.fetchImpl(`${this.host}/api/chat`, {
|
|
621
|
+
method: "POST",
|
|
622
|
+
headers: { "content-type": "application/json" },
|
|
623
|
+
body: JSON.stringify(body),
|
|
624
|
+
signal: ctl.signal
|
|
625
|
+
});
|
|
626
|
+
if (!res.ok) {
|
|
627
|
+
const text = await res.text().catch(() => "");
|
|
628
|
+
throw new Error(`Ollama /api/chat returned ${res.status}: ${text.slice(0, 200)}`);
|
|
629
|
+
}
|
|
630
|
+
return await res.json();
|
|
631
|
+
} finally {
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Consume Ollama's ND-JSON stream from `/api/chat?stream=true`.
|
|
637
|
+
*
|
|
638
|
+
* Behaviour:
|
|
639
|
+
* - Each non-terminal frame's `message.content` is treated as a delta and
|
|
640
|
+
* forwarded to `callbacks.onDelta("assistant_response", delta)` exactly
|
|
641
|
+
* once (no accumulation passed to the callback — 0sec's contract is
|
|
642
|
+
* "incremental fragment only", see types.ts:88).
|
|
643
|
+
* - The terminal frame (`done: true`) is taken as authoritative for
|
|
644
|
+
* `tool_calls`, `done_reason`, and usage stats. Tool-call shape across
|
|
645
|
+
* intermediate frames in current Ollama builds is empty/absent, so we
|
|
646
|
+
* don't try to stitch partials — we just trust the final frame.
|
|
647
|
+
* - The aggregated `message.content` (full assistant text) is returned in
|
|
648
|
+
* the synthetic `OllamaChatResponse` so the rest of `executeNative`
|
|
649
|
+
* can keep its non-streaming-shape codepath.
|
|
650
|
+
*/
|
|
651
|
+
async streamChat(body, callbacks) {
|
|
652
|
+
const ctl = new AbortController();
|
|
653
|
+
const timer = setTimeout(() => ctl.abort(), this.timeout);
|
|
654
|
+
try {
|
|
655
|
+
const res = await this.fetchImpl(`${this.host}/api/chat`, {
|
|
656
|
+
method: "POST",
|
|
657
|
+
headers: { "content-type": "application/json" },
|
|
658
|
+
body: JSON.stringify(body),
|
|
659
|
+
signal: ctl.signal
|
|
660
|
+
});
|
|
661
|
+
if (!res.ok) {
|
|
662
|
+
const text = await res.text().catch(() => "");
|
|
663
|
+
throw new Error(`Ollama /api/chat returned ${res.status}: ${text.slice(0, 200)}`);
|
|
664
|
+
}
|
|
665
|
+
if (!res.body) {
|
|
666
|
+
return await res.json();
|
|
667
|
+
}
|
|
668
|
+
const reader = res.body.getReader();
|
|
669
|
+
const decoder = new TextDecoder();
|
|
670
|
+
let buffer = "";
|
|
671
|
+
let aggregateContent = "";
|
|
672
|
+
let lastModel = "";
|
|
673
|
+
let lastCreatedAt = "";
|
|
674
|
+
let finalToolCalls;
|
|
675
|
+
let doneReason;
|
|
676
|
+
let promptEvalCount;
|
|
677
|
+
let evalCount;
|
|
678
|
+
let totalDuration;
|
|
679
|
+
let sawDone = false;
|
|
680
|
+
const processLine = (line) => {
|
|
681
|
+
const trimmed = line.trim();
|
|
682
|
+
if (!trimmed)
|
|
683
|
+
return;
|
|
684
|
+
let frame;
|
|
685
|
+
try {
|
|
686
|
+
frame = JSON.parse(trimmed);
|
|
687
|
+
} catch {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (frame.model)
|
|
691
|
+
lastModel = frame.model;
|
|
692
|
+
if (frame.created_at)
|
|
693
|
+
lastCreatedAt = frame.created_at;
|
|
694
|
+
const msg = frame.message;
|
|
695
|
+
if (msg) {
|
|
696
|
+
if (typeof msg.content === "string" && msg.content.length > 0) {
|
|
697
|
+
aggregateContent += msg.content;
|
|
698
|
+
callbacks?.onDelta?.("assistant_response", msg.content);
|
|
699
|
+
}
|
|
700
|
+
if (msg.tool_calls && msg.tool_calls.length > 0) {
|
|
701
|
+
finalToolCalls = msg.tool_calls;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (frame.done) {
|
|
705
|
+
sawDone = true;
|
|
706
|
+
if (frame.done_reason)
|
|
707
|
+
doneReason = frame.done_reason;
|
|
708
|
+
if (typeof frame.prompt_eval_count === "number")
|
|
709
|
+
promptEvalCount = frame.prompt_eval_count;
|
|
710
|
+
if (typeof frame.eval_count === "number")
|
|
711
|
+
evalCount = frame.eval_count;
|
|
712
|
+
if (typeof frame.total_duration === "number")
|
|
713
|
+
totalDuration = frame.total_duration;
|
|
714
|
+
}
|
|
715
|
+
};
|
|
716
|
+
while (true) {
|
|
717
|
+
const { value, done } = await reader.read();
|
|
718
|
+
if (done)
|
|
719
|
+
break;
|
|
720
|
+
buffer += decoder.decode(value, { stream: true });
|
|
721
|
+
let nl = buffer.indexOf("\n");
|
|
722
|
+
while (nl !== -1) {
|
|
723
|
+
const line = buffer.slice(0, nl);
|
|
724
|
+
buffer = buffer.slice(nl + 1);
|
|
725
|
+
processLine(line);
|
|
726
|
+
nl = buffer.indexOf("\n");
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (buffer.length > 0)
|
|
730
|
+
processLine(buffer);
|
|
731
|
+
return {
|
|
732
|
+
model: lastModel,
|
|
733
|
+
created_at: lastCreatedAt,
|
|
734
|
+
message: {
|
|
735
|
+
role: "assistant",
|
|
736
|
+
content: aggregateContent,
|
|
737
|
+
tool_calls: finalToolCalls
|
|
738
|
+
},
|
|
739
|
+
done: sawDone,
|
|
740
|
+
done_reason: doneReason,
|
|
741
|
+
total_duration: totalDuration,
|
|
742
|
+
prompt_eval_count: promptEvalCount,
|
|
743
|
+
eval_count: evalCount
|
|
744
|
+
};
|
|
745
|
+
} finally {
|
|
746
|
+
clearTimeout(timer);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
function readUsage2(res) {
|
|
751
|
+
const inp = res.prompt_eval_count;
|
|
752
|
+
const out = res.eval_count;
|
|
753
|
+
if (typeof inp !== "number" || typeof out !== "number")
|
|
754
|
+
return void 0;
|
|
755
|
+
return { inputTokens: inp, outputTokens: out };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// packages/core/dist/runtime/registry.js
|
|
759
|
+
var RUNTIME_REGISTRY = [
|
|
760
|
+
{
|
|
761
|
+
type: "claude",
|
|
762
|
+
command: "claude",
|
|
763
|
+
description: "Claude Code CLI \u2014 supported local adapter for live target scanning, MCP tool use, and deep analysis",
|
|
764
|
+
strengths: ["attack", "source-analysis", "report"],
|
|
765
|
+
supportsSystemPrompt: true
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
type: "codex",
|
|
769
|
+
command: "codex",
|
|
770
|
+
description: "Codex CLI / direct ChatGPT Codex provider \u2014 full parity across web scans, package audits (npm/pypi/cargo/oci), and source code review (incl. linux-kernel). Set 0SEC_CHATGPT_OAUTH_REFRESH_TOKEN to enable subscription auth without the local CLI binary (#402).",
|
|
771
|
+
strengths: ["verify", "source-analysis", "discovery"],
|
|
772
|
+
supportsSystemPrompt: false
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
type: "gemini",
|
|
776
|
+
command: "gemini",
|
|
777
|
+
description: "Gemini CLI (experimental) \u2014 large context window, good for source analysis and reports",
|
|
778
|
+
strengths: ["source-analysis", "report", "discovery"],
|
|
779
|
+
supportsSystemPrompt: false
|
|
780
|
+
},
|
|
781
|
+
{
|
|
782
|
+
type: "ollama",
|
|
783
|
+
// No external binary — health-checked over HTTP. Field is informational
|
|
784
|
+
// so detection tools that key off `command` don't crash; the registry
|
|
785
|
+
// uses `isAvailable()` not `which(command)` for ollama.
|
|
786
|
+
command: "ollama",
|
|
787
|
+
description: "Ollama (local) \u2014 Gemma 4 27B via /api/chat with native function-calling. Pairs with `--seed-findings` for full-local hunts. See 0sec#369.",
|
|
788
|
+
strengths: ["source-analysis", "discovery"],
|
|
789
|
+
supportsSystemPrompt: true
|
|
790
|
+
}
|
|
791
|
+
];
|
|
792
|
+
var STAGE_PREFERENCES = {
|
|
793
|
+
"discovery": ["claude", "codex", "gemini"],
|
|
794
|
+
"source-analysis": ["claude", "gemini", "codex"],
|
|
795
|
+
"attack": ["claude", "codex", "gemini"],
|
|
796
|
+
"verify": ["codex", "claude", "gemini"],
|
|
797
|
+
"report": ["claude", "gemini", "codex"]
|
|
798
|
+
};
|
|
799
|
+
function pickRuntimeForStage(stage, availableRuntimes) {
|
|
800
|
+
const prefs = STAGE_PREFERENCES[stage];
|
|
801
|
+
for (const rt of prefs) {
|
|
802
|
+
if (availableRuntimes.has(rt))
|
|
803
|
+
return rt;
|
|
804
|
+
}
|
|
805
|
+
const first = availableRuntimes.values().next();
|
|
806
|
+
return first.done ? "claude" : first.value;
|
|
807
|
+
}
|
|
808
|
+
async function detectAvailableRuntimes() {
|
|
809
|
+
const { ProcessRuntime: ProcessRuntime2 } = await import("./process-LQRCS5OY.js");
|
|
810
|
+
const available = /* @__PURE__ */ new Set();
|
|
811
|
+
const checks = RUNTIME_REGISTRY.map(async (info) => {
|
|
812
|
+
const rt = new ProcessRuntime2({ type: info.type, timeout: 5e3 });
|
|
813
|
+
if (await rt.isAvailable()) {
|
|
814
|
+
available.add(info.type);
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
await Promise.all(checks);
|
|
818
|
+
return available;
|
|
819
|
+
}
|
|
820
|
+
function getRuntimeInfo(type) {
|
|
821
|
+
return RUNTIME_REGISTRY.find((r) => r.type === type);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// packages/core/dist/runtime/index.js
|
|
825
|
+
function createRuntime(config) {
|
|
826
|
+
switch (config.type) {
|
|
827
|
+
case "api":
|
|
828
|
+
return new LlmApiRuntime(config);
|
|
829
|
+
case "claude":
|
|
830
|
+
case "codex":
|
|
831
|
+
case "gemini":
|
|
832
|
+
return new ProcessRuntime(config);
|
|
833
|
+
case "ollama":
|
|
834
|
+
return new OllamaRuntime({
|
|
835
|
+
model: config.model ?? process.env["0SEC_OLLAMA_MODEL"] ?? "gemma4:27b",
|
|
836
|
+
timeout: config.timeout,
|
|
837
|
+
host: process.env.OLLAMA_HOST
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export {
|
|
843
|
+
CliNativeRuntime,
|
|
844
|
+
DEFAULT_ENSEMBLE_MODELS,
|
|
845
|
+
OpenRouterRuntime,
|
|
846
|
+
OllamaRuntime,
|
|
847
|
+
RUNTIME_REGISTRY,
|
|
848
|
+
pickRuntimeForStage,
|
|
849
|
+
detectAvailableRuntimes,
|
|
850
|
+
getRuntimeInfo,
|
|
851
|
+
createRuntime
|
|
852
|
+
};
|