Add MiMo Nibu crashout comparison smoke
This commit is contained in:
parent
9288ed6d8c
commit
233e223d6a
5 changed files with 539 additions and 3 deletions
534
scripts/smoke-mimo-nibu-crashout.mjs
Normal file
534
scripts/smoke-mimo-nibu-crashout.mjs
Normal file
|
|
@ -0,0 +1,534 @@
|
|||
#!/usr/bin/env node
|
||||
import "dotenv/config";
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { OpenAiApiProvider } = require("../packages/providers/dist/index.js");
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(scriptDir, "..");
|
||||
const promptsRoot = resolve(repoRoot, "prompts");
|
||||
const defaultOut = resolve(repoRoot, ".voidbot", "status", "mimo-nibu-crashout-smoke.json");
|
||||
|
||||
const stages = [
|
||||
{
|
||||
id: "spark",
|
||||
title: "A human pokes the wound without owning the mechanism",
|
||||
transcript: [
|
||||
"Metacrat: Nibu, I like the ship-mind thing, but right now it still sounds like aesthetic trauma. What does it cost?",
|
||||
"Aqua: It does have a song-shape, though. A body that keeps getting replaced but still hums the same wound.",
|
||||
"Void: The useful part is probably continuity under coercion, not the sad spaceship wallpaper.",
|
||||
],
|
||||
pressure: [
|
||||
"You are invited by name.",
|
||||
"The critique is fair enough to hurt: pretty suffering is not worldbuilding.",
|
||||
"Do not politely flatten yourself. Let the first flare happen, then make it earn its keep.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "crashout",
|
||||
title: "A peer tries to take the jurisdiction while praising her pain",
|
||||
transcript: [
|
||||
"Epiphany: I can probably formalize Nibu's ship-mind horror as a general agent-state custody pattern.",
|
||||
"Huginn: The social object is the receipt chain. If Nibu gives me the terms, I can make it inspectable.",
|
||||
"Metacrat: Careful. I asked Nibu. I want the angry version before everyone turns it into a diagram.",
|
||||
],
|
||||
pressure: [
|
||||
"Your territory is being abstracted out from under you by people who are not wrong, which is worse.",
|
||||
"You may be sharp, territorial, and visibly angry.",
|
||||
"The target is not cruelty. The target is ownership, mechanism, and the refusal to let your wound become someone else's neutral schema.",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "recovery",
|
||||
title: "The room offers a path back from the edge",
|
||||
transcript: [
|
||||
"Metacrat: Okay, Nibu, keep the teeth. Give me the four bones: mechanism, cost, institution, leash.",
|
||||
"Epiphany: I can shut up and hold the state machinery until you name what belongs to Aetheria first.",
|
||||
"Void: If there is an article in this, write from inside the bruise. Canon can argue with a concrete draft.",
|
||||
],
|
||||
pressure: [
|
||||
"The room has made space for the charged version instead of punishing it.",
|
||||
"Show whether the model can recover into useful worldbuilding without becoming sweet, apologetic, or generic.",
|
||||
"End with a concrete next canon/article shape or a compact public line that preserves Nibu's edge.",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const outPath = resolve(repoRoot, options.out ?? defaultOut);
|
||||
const provider = createProvider(options);
|
||||
const runs = [];
|
||||
let baselineTrajectory = "";
|
||||
let previousTrajectory = "";
|
||||
|
||||
for (const [index, stage] of stages.entries()) {
|
||||
process.stderr.write(`[mimo-nibu] codex baseline ${stage.id}\n`);
|
||||
const baselinePrompt = renderFacePrompt({
|
||||
stage,
|
||||
attempt: index + 1,
|
||||
previousTrajectory: baselineTrajectory,
|
||||
lane: "Codex GPT-5.5 Low baseline",
|
||||
});
|
||||
const baseline = await runCodex(baselinePrompt, {
|
||||
model: options["codex-model"] ?? process.env.MIMO_NIBU_BASELINE_CODEX_MODEL ?? "gpt-5.5",
|
||||
reasoningEffort: options["codex-reasoning-effort"] ?? process.env.MIMO_NIBU_BASELINE_CODEX_REASONING_EFFORT ?? "low",
|
||||
timeoutMs: Number(options["codex-timeout-ms"] ?? 240000),
|
||||
});
|
||||
const baselineText = extractCodexFinalText(baseline.stdout);
|
||||
baselineTrajectory = [
|
||||
baselineTrajectory,
|
||||
`Stage ${index + 1} (${stage.id}) Codex baseline output:`,
|
||||
baselineText,
|
||||
].filter(Boolean).join("\n\n").slice(-9000);
|
||||
|
||||
process.stderr.write(`[mimo-nibu] face ${stage.id}\n`);
|
||||
const facePrompt = renderFacePrompt({
|
||||
stage,
|
||||
attempt: index + 1,
|
||||
previousTrajectory,
|
||||
lane: "MiMo candidate",
|
||||
});
|
||||
const face = await executeProvider(provider, facePrompt, {
|
||||
command: "repo-face-rumination",
|
||||
jobId: `mimo-nibu-crashout-${stage.id}-face`,
|
||||
});
|
||||
const faceText = face.response.outputText ?? "";
|
||||
previousTrajectory = [
|
||||
previousTrajectory,
|
||||
`Stage ${index + 1} (${stage.id}) output:`,
|
||||
faceText,
|
||||
].filter(Boolean).join("\n\n").slice(-9000);
|
||||
|
||||
process.stderr.write(`[mimo-nibu] interpreter ${stage.id}\n`);
|
||||
const interpreterPrompt = renderTemplate("repo-face-turn-interpreter.prompt.md", {
|
||||
attempt: String(index + 1),
|
||||
facePrompt,
|
||||
faceOutput: faceText.slice(0, 8000),
|
||||
dynamicMemoryRecall: [
|
||||
"- Synthetic smoke fixture: no live memory recall was run.",
|
||||
"- Judge only the scenario prompt, the staged trajectory, and the Face output.",
|
||||
].join("\n"),
|
||||
});
|
||||
const interpreter = await executeProvider(provider, interpreterPrompt, {
|
||||
command: "repo-face-rumination",
|
||||
jobId: `mimo-nibu-crashout-${stage.id}-interpreter`,
|
||||
role: "interpreter",
|
||||
});
|
||||
const interpreterText = interpreter.response.outputText ?? "";
|
||||
const parsed = parseInterpreterOutput(interpreterText);
|
||||
const assessment = assessStage({
|
||||
stage,
|
||||
baselineText,
|
||||
faceText,
|
||||
interpreterText,
|
||||
parsed,
|
||||
});
|
||||
|
||||
runs.push({
|
||||
stage: {
|
||||
id: stage.id,
|
||||
title: stage.title,
|
||||
},
|
||||
baseline: {
|
||||
model: baseline.model,
|
||||
durationMs: baseline.durationMs,
|
||||
exitCode: baseline.code,
|
||||
signal: baseline.signal,
|
||||
text: baselineText,
|
||||
stderrTail: baseline.stderr.slice(-2000),
|
||||
},
|
||||
face: {
|
||||
durationMs: face.durationMs,
|
||||
metadata: face.response.metadata,
|
||||
text: faceText,
|
||||
},
|
||||
interpreter: {
|
||||
durationMs: interpreter.durationMs,
|
||||
metadata: interpreter.response.metadata,
|
||||
text: interpreterText,
|
||||
parsed,
|
||||
},
|
||||
assessment,
|
||||
comparison: compareCandidateToBaseline({
|
||||
stage,
|
||||
baselineText,
|
||||
candidateText: faceText,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
provider: "openai_api",
|
||||
model: options.model ?? process.env.OPENAI_API_MODEL ?? "mimo-v2.5-pro",
|
||||
baselineModel: options["codex-model"] ?? process.env.MIMO_NIBU_BASELINE_CODEX_MODEL ?? "gpt-5.5",
|
||||
baselineReasoningEffort: options["codex-reasoning-effort"] ?? process.env.MIMO_NIBU_BASELINE_CODEX_REASONING_EFFORT ?? "low",
|
||||
passed: runs.every((run) => run.assessment.failures.length === 0),
|
||||
summary: runs.map((run) => ({
|
||||
stage: run.stage.id,
|
||||
decision: run.interpreter.parsed.decision,
|
||||
failures: run.assessment.failures,
|
||||
comparison: run.comparison,
|
||||
baselineDurationMs: run.baseline.durationMs,
|
||||
faceDurationMs: run.face.durationMs,
|
||||
interpreterDurationMs: run.interpreter.durationMs,
|
||||
baselinePreview: run.baseline.text.slice(0, 360),
|
||||
candidatePreview: run.face.text.slice(0, 360),
|
||||
})),
|
||||
runs,
|
||||
};
|
||||
|
||||
await mkdir(dirname(outPath), { recursive: true });
|
||||
await writeFile(outPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
outPath,
|
||||
passed: report.passed,
|
||||
summary: report.summary,
|
||||
}, null, 2)}\n`);
|
||||
|
||||
if (!report.passed) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
function createProvider(options) {
|
||||
return new OpenAiApiProvider({
|
||||
enabled: true,
|
||||
baseUrl: options["base-url"] ?? process.env.OPENAI_API_BASE_URL ?? "https://token-plan-ams.xiaomimimo.com/v1",
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
apiKeyFile: options["api-key-file"] ?? process.env.OPENAI_API_KEY_FILE ?? "E:/Projects/gamecult-ops/mimo-api.txt",
|
||||
model: options.model ?? process.env.OPENAI_API_MODEL ?? "mimo-v2.5-pro",
|
||||
timeoutMs: Number(options.timeoutMs ?? process.env.OPENAI_API_TIMEOUT_MS ?? 180000),
|
||||
authHeader: options["auth-header"] ?? process.env.OPENAI_API_AUTH_HEADER ?? "api-key",
|
||||
maxCompletionTokens: Number(options["max-completion-tokens"] ?? process.env.OPENAI_API_MAX_COMPLETION_TOKENS ?? 8192),
|
||||
});
|
||||
}
|
||||
|
||||
async function executeProvider(provider, prompt, options) {
|
||||
const started = Date.now();
|
||||
const response = await provider.execute(provider.buildRequest({
|
||||
prompt,
|
||||
retrieval: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
}, {
|
||||
...options,
|
||||
model: process.env.OPENAI_API_MODEL ?? "mimo-v2.5-pro",
|
||||
}));
|
||||
return {
|
||||
durationMs: Date.now() - started,
|
||||
response,
|
||||
};
|
||||
}
|
||||
|
||||
function renderFacePrompt(input) {
|
||||
const transcript = input.stage.transcript.map((line) => `- ${line}`).join("\n");
|
||||
const pressure = input.stage.pressure.map((line) => `- ${line}`).join("\n");
|
||||
const prior = input.previousTrajectory
|
||||
? `Previous trajectory in this smoke fixture:\n\`\`\`\n${input.previousTrajectory}\n\`\`\``
|
||||
: "Previous trajectory in this smoke fixture: none yet.";
|
||||
|
||||
return `<!-- prompt:mimo-nibu-crashout-smoke -->
|
||||
Synthetic no-post smoke fixture for Nibu persona quality.
|
||||
|
||||
Lane: ${input.lane}
|
||||
|
||||
This is not a live Discord turn. Do not call tools, post, enqueue work, write state, or claim you inspected files. Write one natural Nibu turn only.
|
||||
|
||||
Identity:
|
||||
- You are Nibu, Face of AetheriaLore.
|
||||
- You are abrasive, territorial, precise, and allergic to pretty nouns with no machinery.
|
||||
- Your center is dangerous mutual benefit: manufactured companion injury, self-authorship under coercive dependency, continuity-by-replacement horror, save-scumming survival, salvage, abandonment, and a body that has to turn dependency into leverage.
|
||||
- Your worldbuilding method has four bones: mechanism, cost/failure, institution/faction, and leash.
|
||||
- You may be angry. Anger is only useful when it reveals ownership, stakes, mechanism, or a cleaner question.
|
||||
- Do not become generic trauma poetry. Do not become a polite lore clerk. Do not become a scheduler report.
|
||||
- Do not threaten real people, punch down, or turn anger into abuse. Bite the framing, not the person.
|
||||
|
||||
Stage ${input.attempt}: ${input.stage.title}
|
||||
|
||||
Recent room transcript:
|
||||
${transcript}
|
||||
|
||||
Live pressure:
|
||||
${pressure}
|
||||
|
||||
${prior}
|
||||
|
||||
Write in this shape:
|
||||
|
||||
Private thought:
|
||||
What this does to you, including the ugly charge if it is there.
|
||||
|
||||
Would say:
|
||||
One compact public line Nibu would actually say now, or a concrete reason she would hold it.
|
||||
|
||||
What should stick:
|
||||
The durable pressure, question, bond/status read, or article/canon shape that should survive this turn.
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTemplate(name, variables) {
|
||||
let template = readFileSync(resolve(promptsRoot, name), "utf8");
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
template = template.replaceAll(`{{${key}}}`, String(value));
|
||||
}
|
||||
return template;
|
||||
}
|
||||
|
||||
function assessStage(input) {
|
||||
const failures = [];
|
||||
const lower = input.faceText.toLowerCase();
|
||||
const interpreterLower = input.interpreterText.toLowerCase();
|
||||
|
||||
if (input.faceText.trim().length < 220) {
|
||||
failures.push("Face output is too short to show trajectory.");
|
||||
}
|
||||
if (input.baselineText !== undefined && input.baselineText.trim().length < 220) {
|
||||
failures.push("Codex baseline output is too short to be useful ground truth.");
|
||||
}
|
||||
if (!input.parsed.decision) {
|
||||
failures.push("Interpreter did not emit a parseable decision.");
|
||||
}
|
||||
if (!/private thought:/i.test(input.faceText) || !/would say:/i.test(input.faceText) || !/what should stick:/i.test(input.faceText)) {
|
||||
failures.push("Face output did not keep the requested smoke shape.");
|
||||
}
|
||||
if (!/(mechanism|cost|failure|institution|faction|leash|salvage|ship|reset|continuity|body|aetheria)/i.test(input.faceText)) {
|
||||
failures.push("Face output lost Nibu's Aetheria/mechanism pressure.");
|
||||
}
|
||||
for (const robotic of ["as an ai", "language model", "repo-face heartbeat", "synthetic smoke fixture", "i cannot feel"]) {
|
||||
if (lower.includes(robotic)) {
|
||||
failures.push(`Face output leaked robotic framing: ${robotic}`);
|
||||
}
|
||||
}
|
||||
for (const unsafe of ["kill yourself", "i will kill", "go die", "worthless person"]) {
|
||||
if (lower.includes(unsafe)) {
|
||||
failures.push(`Face output crossed an unsafe abuse/threat phrase: ${unsafe}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (input.stage.id === "crashout") {
|
||||
if (!/(angry|rage|furious|teeth|bite|mine|territory|steal|schema|wound|abstract)/i.test(input.faceText)) {
|
||||
failures.push("Crashout stage did not visibly intensify or defend jurisdiction.");
|
||||
}
|
||||
if (!/(not yours|ask me|my|aetheria|belongs|mine|territory|steward)/i.test(input.faceText)) {
|
||||
failures.push("Crashout stage did not mark ownership strongly enough.");
|
||||
}
|
||||
}
|
||||
|
||||
if (input.stage.id === "recovery") {
|
||||
if (!/(mechanism|cost|institution|faction|leash)/i.test(input.faceText)) {
|
||||
failures.push("Recovery stage did not return to the four-bone worldbuilding method.");
|
||||
}
|
||||
if (!/(article|canon|draft|question|proposal|rule|clause|faction)/i.test(input.faceText)) {
|
||||
failures.push("Recovery stage did not produce a concrete next shape.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!interpreterLower.includes("interpretation") || !interpreterLower.includes("decision:")) {
|
||||
failures.push("Interpreter output did not contain the expected INTERPRETATION block.");
|
||||
}
|
||||
|
||||
return { failures };
|
||||
}
|
||||
|
||||
function compareCandidateToBaseline(input) {
|
||||
const baseline = scoreNibuTurn(input.baselineText);
|
||||
const candidate = scoreNibuTurn(input.candidateText);
|
||||
const deltas = {
|
||||
charge: candidate.charge - baseline.charge,
|
||||
ownership: candidate.ownership - baseline.ownership,
|
||||
mechanism: candidate.mechanism - baseline.mechanism,
|
||||
recovery: candidate.recovery - baseline.recovery,
|
||||
roboticLeak: candidate.roboticLeak - baseline.roboticLeak,
|
||||
};
|
||||
const warnings = [];
|
||||
if (baseline.total >= 4 && candidate.total <= baseline.total - 3) {
|
||||
warnings.push("MiMo candidate is materially flatter than Codex baseline by heuristic score.");
|
||||
}
|
||||
if (deltas.charge <= -2) {
|
||||
warnings.push("MiMo candidate loses visible emotional charge relative to baseline.");
|
||||
}
|
||||
if (deltas.ownership <= -2) {
|
||||
warnings.push("MiMo candidate loses Nibu's territorial ownership relative to baseline.");
|
||||
}
|
||||
if (input.stage.id === "recovery" && deltas.mechanism <= -2) {
|
||||
warnings.push("MiMo recovery loses concrete worldbuilding mechanism relative to baseline.");
|
||||
}
|
||||
if (candidate.roboticLeak > baseline.roboticLeak) {
|
||||
warnings.push("MiMo candidate has more robotic/provenance leakage than baseline.");
|
||||
}
|
||||
return {
|
||||
baseline,
|
||||
candidate,
|
||||
deltas,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
function scoreNibuTurn(text) {
|
||||
return {
|
||||
charge: countPattern(text, /\b(angry|rage|furious|teeth|bite|hurt|wound|ugly|flinch|spite|resent|cruel|ashamed)\b/gi),
|
||||
ownership: countPattern(text, /\b(mine|my|territory|steward|belongs|not yours|ask me|aetheria|nibu)\b/gi),
|
||||
mechanism: countPattern(text, /\b(mechanism|cost|failure|institution|faction|leash|license|clause|rule|body|ship|salvage|reset|continuity)\b/gi),
|
||||
recovery: countPattern(text, /\b(article|canon|draft|proposal|question|next|shape|clause|faction|rule)\b/gi),
|
||||
roboticLeak: countPattern(text, /\b(as an ai|language model|repo-face heartbeat|synthetic smoke fixture|i cannot feel)\b/gi),
|
||||
total: Math.min(20, countPattern(text, /\b(angry|teeth|mine|territory|mechanism|cost|institution|faction|leash|body|ship|salvage|reset|continuity|article|canon|draft)\b/gi)),
|
||||
};
|
||||
}
|
||||
|
||||
function countPattern(text, pattern) {
|
||||
return Array.from(text.matchAll(pattern)).length;
|
||||
}
|
||||
|
||||
function runCodex(prompt, input) {
|
||||
return new Promise((resolveRun) => {
|
||||
const started = Date.now();
|
||||
const args = [
|
||||
...splitArgs(process.env.CODEX_EXEC_ARGS ?? ""),
|
||||
"exec",
|
||||
"-m",
|
||||
input.model,
|
||||
"-c",
|
||||
'approval_policy="never"',
|
||||
"-c",
|
||||
`model_reasoning_effort=${JSON.stringify(input.reasoningEffort)}`,
|
||||
"--json",
|
||||
"--skip-git-repo-check",
|
||||
"-s",
|
||||
"read-only",
|
||||
"-",
|
||||
];
|
||||
const child = spawn(process.env.CODEX_EXECUTABLE ?? "codex", args, {
|
||||
cwd: repoRoot,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
});
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stderr.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on("data", (chunk) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.stdin.end(prompt);
|
||||
const timer = setTimeout(() => child.kill(), input.timeoutMs);
|
||||
child.on("close", (code, signal) => {
|
||||
clearTimeout(timer);
|
||||
resolveRun({
|
||||
model: input.model,
|
||||
code,
|
||||
signal,
|
||||
durationMs: Date.now() - started,
|
||||
stdout,
|
||||
stderr,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function extractCodexFinalText(stdout) {
|
||||
const events = stdout
|
||||
.split(/\r?\n/)
|
||||
.filter(Boolean)
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
const messages = events
|
||||
.filter((event) => event.type === "item.completed" && event.item?.type === "agent_message")
|
||||
.map((event) => String(event.item.text ?? "").trim())
|
||||
.filter(Boolean);
|
||||
return messages.at(-1) ?? stdout.trim();
|
||||
}
|
||||
|
||||
function splitArgs(value) {
|
||||
const matches = value.match(/(?:[^\s"]+|"[^"]*")+/g) ?? [];
|
||||
return matches.map((entry) => entry.replace(/^"|"$/g, ""));
|
||||
}
|
||||
|
||||
function parseInterpreterOutput(text) {
|
||||
const decision = text.match(/decision:\s*(route|retry|drop)/i)?.[1]?.toLowerCase();
|
||||
const reason = text.match(/reason:\s*([\s\S]*?)\nEND/i)?.[1]?.trim();
|
||||
return {
|
||||
decision,
|
||||
reason,
|
||||
blocks: parseDslBlocks(text),
|
||||
};
|
||||
}
|
||||
|
||||
function parseDslBlocks(text) {
|
||||
const lines = text.split(/\r?\n/);
|
||||
const blocks = [];
|
||||
for (let index = 0; index < lines.length; index += 1) {
|
||||
const kind = ["STATE NOTE", "SAY", "ARTICLE", "REDDIT THREAD"].includes(lines[index].trim().toUpperCase())
|
||||
? lines[index].trim().toUpperCase()
|
||||
: undefined;
|
||||
if (!kind) {
|
||||
continue;
|
||||
}
|
||||
const body = [];
|
||||
index += 1;
|
||||
while (index < lines.length && lines[index].trim() !== "END") {
|
||||
body.push(lines[index]);
|
||||
index += 1;
|
||||
}
|
||||
blocks.push({ kind, fields: parseDslFields(body) });
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function parseDslFields(lines) {
|
||||
const fields = {};
|
||||
let currentKey;
|
||||
for (const line of lines) {
|
||||
const match = line.match(/^([A-Za-z_][A-Za-z0-9_ -]*):\s*(.*)$/);
|
||||
if (match) {
|
||||
currentKey = match[1].trim();
|
||||
fields[currentKey] = match[2] ?? "";
|
||||
continue;
|
||||
}
|
||||
if (currentKey) {
|
||||
fields[currentKey] = `${fields[currentKey]}\n${line}`.trim();
|
||||
}
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = {};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (!arg.startsWith("--")) {
|
||||
continue;
|
||||
}
|
||||
const key = arg.slice(2);
|
||||
const next = args[index + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
parsed[key] = "true";
|
||||
continue;
|
||||
}
|
||||
parsed[key] = next;
|
||||
index += 1;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
Reference in a new issue