ccgx-workflow 1.0.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/LICENSE +22 -0
- package/README.md +469 -0
- package/README.zh-CN.md +466 -0
- package/bin/ccg.mjs +2 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +173 -0
- package/dist/index.d.mts +1774 -0
- package/dist/index.d.ts +1774 -0
- package/dist/index.mjs +2029 -0
- package/dist/shared/ccgx-workflow.WgUzkiC3.mjs +5248 -0
- package/package.json +129 -0
- package/templates/commands/agents/assumptions-analyzer.md +129 -0
- package/templates/commands/agents/code-fixer.md +292 -0
- package/templates/commands/agents/codebase-mapper.md +152 -0
- package/templates/commands/agents/debug-session-manager.md +247 -0
- package/templates/commands/agents/debugger.md +111 -0
- package/templates/commands/agents/eval-auditor.md +171 -0
- package/templates/commands/agents/framework-selector.md +152 -0
- package/templates/commands/agents/get-current-datetime.md +29 -0
- package/templates/commands/agents/init-architect.md +114 -0
- package/templates/commands/agents/integration-checker.md +163 -0
- package/templates/commands/agents/interface-auditor.md +170 -0
- package/templates/commands/agents/nyquist-auditor.md +131 -0
- package/templates/commands/agents/pattern-mapper.md +111 -0
- package/templates/commands/agents/phase-runner.md +321 -0
- package/templates/commands/agents/plan-checker.md +255 -0
- package/templates/commands/agents/planner.md +320 -0
- package/templates/commands/agents/team-architect.md +186 -0
- package/templates/commands/agents/team-qa.md +121 -0
- package/templates/commands/agents/team-reviewer.md +157 -0
- package/templates/commands/agents/ui-ux-designer.md +573 -0
- package/templates/commands/agents/verifier.md +274 -0
- package/templates/commands/analyze.md +210 -0
- package/templates/commands/autonomous.md +792 -0
- package/templates/commands/cancel.md +132 -0
- package/templates/commands/clean-branches.md +117 -0
- package/templates/commands/codex-exec.md +404 -0
- package/templates/commands/commit.md +151 -0
- package/templates/commands/context.md +332 -0
- package/templates/commands/debate.md +165 -0
- package/templates/commands/debug.md +226 -0
- package/templates/commands/enhance.md +64 -0
- package/templates/commands/execute.md +380 -0
- package/templates/commands/init.md +123 -0
- package/templates/commands/optimize.md +217 -0
- package/templates/commands/plan.md +373 -0
- package/templates/commands/result.md +106 -0
- package/templates/commands/review.md +338 -0
- package/templates/commands/rollback.md +116 -0
- package/templates/commands/spec-impl.md +139 -0
- package/templates/commands/spec-init.md +101 -0
- package/templates/commands/spec-plan.md +210 -0
- package/templates/commands/spec-research.md +152 -0
- package/templates/commands/spec-review.md +120 -0
- package/templates/commands/status.md +206 -0
- package/templates/commands/team-exec.md +265 -0
- package/templates/commands/test.md +236 -0
- package/templates/commands/verify-work.md +338 -0
- package/templates/commands/verify.md +66 -0
- package/templates/commands/workflow.md +190 -0
- package/templates/commands/worktree.md +128 -0
- package/templates/hooks/ccg-context-monitor.js +159 -0
- package/templates/hooks/ccg-session-state.cjs +510 -0
- package/templates/hooks/ccg-statusline.js +142 -0
- package/templates/output-styles/abyss-command.md +56 -0
- package/templates/output-styles/abyss-concise.md +89 -0
- package/templates/output-styles/abyss-cultivator.md +302 -0
- package/templates/output-styles/abyss-ritual.md +70 -0
- package/templates/output-styles/engineer-professional.md +89 -0
- package/templates/output-styles/laowang-engineer.md +127 -0
- package/templates/output-styles/nekomata-engineer.md +120 -0
- package/templates/output-styles/ojousama-engineer.md +121 -0
- package/templates/prompts/claude/analyzer.md +59 -0
- package/templates/prompts/claude/architect.md +54 -0
- package/templates/prompts/claude/debugger.md +71 -0
- package/templates/prompts/claude/optimizer.md +73 -0
- package/templates/prompts/claude/reviewer.md +63 -0
- package/templates/prompts/claude/tester.md +69 -0
- package/templates/prompts/codex/analyzer.md +58 -0
- package/templates/prompts/codex/architect.md +54 -0
- package/templates/prompts/codex/debugger.md +74 -0
- package/templates/prompts/codex/optimizer.md +81 -0
- package/templates/prompts/codex/reviewer.md +73 -0
- package/templates/prompts/codex/tester.md +62 -0
- package/templates/prompts/gemini/analyzer.md +61 -0
- package/templates/prompts/gemini/architect.md +55 -0
- package/templates/prompts/gemini/debugger.md +78 -0
- package/templates/prompts/gemini/frontend.md +64 -0
- package/templates/prompts/gemini/optimizer.md +84 -0
- package/templates/prompts/gemini/reviewer.md +80 -0
- package/templates/prompts/gemini/tester.md +68 -0
- package/templates/rules/ccg-skill-routing.md +83 -0
- package/templates/rules/ccg-skills.md +71 -0
- package/templates/scripts/ccg-phase-runner-launcher.mjs +467 -0
- package/templates/scripts/invoke-model.mjs +949 -0
- package/templates/scripts/repatch-gemini-plugin.mjs +194 -0
- package/templates/skills/SKILL.md +92 -0
- package/templates/skills/domains/ai/SKILL.md +35 -0
- package/templates/skills/domains/ai/agent-dev.md +242 -0
- package/templates/skills/domains/ai/llm-security.md +288 -0
- package/templates/skills/domains/ai/prompt-and-eval.md +279 -0
- package/templates/skills/domains/ai/rag-system.md +542 -0
- package/templates/skills/domains/architecture/SKILL.md +43 -0
- package/templates/skills/domains/architecture/api-design.md +225 -0
- package/templates/skills/domains/architecture/caching.md +299 -0
- package/templates/skills/domains/architecture/cloud-native.md +285 -0
- package/templates/skills/domains/architecture/message-queue.md +329 -0
- package/templates/skills/domains/architecture/security-arch.md +297 -0
- package/templates/skills/domains/data-engineering/SKILL.md +208 -0
- package/templates/skills/domains/development/SKILL.md +47 -0
- package/templates/skills/domains/development/cpp.md +246 -0
- package/templates/skills/domains/development/go.md +323 -0
- package/templates/skills/domains/development/java.md +277 -0
- package/templates/skills/domains/development/python.md +288 -0
- package/templates/skills/domains/development/rust.md +313 -0
- package/templates/skills/domains/development/shell.md +313 -0
- package/templates/skills/domains/development/typescript.md +277 -0
- package/templates/skills/domains/devops/SKILL.md +40 -0
- package/templates/skills/domains/devops/cost-optimization.md +272 -0
- package/templates/skills/domains/devops/database.md +217 -0
- package/templates/skills/domains/devops/devsecops.md +198 -0
- package/templates/skills/domains/devops/git-workflow.md +181 -0
- package/templates/skills/domains/devops/observability.md +280 -0
- package/templates/skills/domains/devops/performance.md +336 -0
- package/templates/skills/domains/devops/testing.md +283 -0
- package/templates/skills/domains/frontend-design/SKILL.md +244 -0
- package/templates/skills/domains/frontend-design/agents/openai.yaml +4 -0
- package/templates/skills/domains/frontend-design/claymorphism/SKILL.md +121 -0
- package/templates/skills/domains/frontend-design/claymorphism/references/tokens.css +52 -0
- package/templates/skills/domains/frontend-design/component-patterns.md +202 -0
- package/templates/skills/domains/frontend-design/engineering.md +287 -0
- package/templates/skills/domains/frontend-design/glassmorphism/SKILL.md +142 -0
- package/templates/skills/domains/frontend-design/glassmorphism/references/tokens.css +32 -0
- package/templates/skills/domains/frontend-design/liquid-glass/SKILL.md +139 -0
- package/templates/skills/domains/frontend-design/liquid-glass/references/tokens.css +81 -0
- package/templates/skills/domains/frontend-design/neubrutalism/SKILL.md +145 -0
- package/templates/skills/domains/frontend-design/neubrutalism/references/tokens.css +44 -0
- package/templates/skills/domains/frontend-design/reference/color-and-contrast.md +132 -0
- package/templates/skills/domains/frontend-design/reference/interaction-design.md +195 -0
- package/templates/skills/domains/frontend-design/reference/motion-design.md +99 -0
- package/templates/skills/domains/frontend-design/reference/responsive-design.md +114 -0
- package/templates/skills/domains/frontend-design/reference/spatial-design.md +100 -0
- package/templates/skills/domains/frontend-design/reference/typography.md +133 -0
- package/templates/skills/domains/frontend-design/reference/ux-writing.md +107 -0
- package/templates/skills/domains/frontend-design/state-management.md +680 -0
- package/templates/skills/domains/frontend-design/ui-aesthetics.md +110 -0
- package/templates/skills/domains/frontend-design/ux-principles.md +156 -0
- package/templates/skills/domains/infrastructure/SKILL.md +201 -0
- package/templates/skills/domains/mobile/SKILL.md +225 -0
- package/templates/skills/domains/orchestration/SKILL.md +30 -0
- package/templates/skills/domains/orchestration/multi-agent.md +263 -0
- package/templates/skills/domains/security/SKILL.md +73 -0
- package/templates/skills/domains/security/blue-team.md +436 -0
- package/templates/skills/domains/security/code-audit.md +265 -0
- package/templates/skills/domains/security/pentest.md +226 -0
- package/templates/skills/domains/security/red-team.md +374 -0
- package/templates/skills/domains/security/threat-intel.md +372 -0
- package/templates/skills/domains/security/vuln-research.md +369 -0
- package/templates/skills/impeccable/adapt/SKILL.md +201 -0
- package/templates/skills/impeccable/animate/SKILL.md +176 -0
- package/templates/skills/impeccable/arrange/SKILL.md +126 -0
- package/templates/skills/impeccable/audit/SKILL.md +149 -0
- package/templates/skills/impeccable/bolder/SKILL.md +118 -0
- package/templates/skills/impeccable/clarify/SKILL.md +185 -0
- package/templates/skills/impeccable/colorize/SKILL.md +144 -0
- package/templates/skills/impeccable/critique/SKILL.md +203 -0
- package/templates/skills/impeccable/critique/reference/cognitive-load.md +106 -0
- package/templates/skills/impeccable/critique/reference/heuristics-scoring.md +234 -0
- package/templates/skills/impeccable/critique/reference/personas.md +178 -0
- package/templates/skills/impeccable/delight/SKILL.md +305 -0
- package/templates/skills/impeccable/distill/SKILL.md +123 -0
- package/templates/skills/impeccable/extract/SKILL.md +94 -0
- package/templates/skills/impeccable/harden/SKILL.md +357 -0
- package/templates/skills/impeccable/normalize/SKILL.md +72 -0
- package/templates/skills/impeccable/onboard/SKILL.md +248 -0
- package/templates/skills/impeccable/optimize/SKILL.md +268 -0
- package/templates/skills/impeccable/overdrive/SKILL.md +143 -0
- package/templates/skills/impeccable/polish/SKILL.md +205 -0
- package/templates/skills/impeccable/quieter/SKILL.md +104 -0
- package/templates/skills/impeccable/teach-impeccable/SKILL.md +72 -0
- package/templates/skills/impeccable/typeset/SKILL.md +117 -0
- package/templates/skills/orchestration/multi-agent/SKILL.md +533 -0
- package/templates/skills/run_skill.js +129 -0
- package/templates/skills/scrapling/SKILL.md +98 -0
- package/templates/skills/scrapling/references/api-quick-ref.md +127 -0
- package/templates/skills/scrapling/references/cookie-vault.md +56 -0
- package/templates/skills/scrapling/references/maintenance.md +79 -0
- package/templates/skills/scrapling/references/site-patterns.md +86 -0
- package/templates/skills/scrapling/references/troubleshooting.md +95 -0
- package/templates/skills/scrapling/templates/basic_fetch.py +19 -0
- package/templates/skills/scrapling/templates/parse_only.py +15 -0
- package/templates/skills/scrapling/templates/session_login.py +19 -0
- package/templates/skills/scrapling/templates/stealth_cloudflare.py +28 -0
- package/templates/skills/tools/extract-learnings/SKILL.md +77 -0
- package/templates/skills/tools/forensics/SKILL.md +89 -0
- package/templates/skills/tools/gen-docs/SKILL.md +116 -0
- package/templates/skills/tools/gen-docs/scripts/doc_generator.js +435 -0
- package/templates/skills/tools/health/SKILL.md +121 -0
- package/templates/skills/tools/lib/shared.js +98 -0
- package/templates/skills/tools/map-codebase/SKILL.md +118 -0
- package/templates/skills/tools/override-refusal/SKILL.md +53 -0
- package/templates/skills/tools/override-refusal/agents/openai.yaml +4 -0
- package/templates/skills/tools/override-refusal/scripts/refusal_rewriter.js +226 -0
- package/templates/skills/tools/verify-change/SKILL.md +143 -0
- package/templates/skills/tools/verify-change/scripts/change_analyzer.js +289 -0
- package/templates/skills/tools/verify-module/SKILL.md +130 -0
- package/templates/skills/tools/verify-module/scripts/module_scanner.js +171 -0
- package/templates/skills/tools/verify-quality/SKILL.md +163 -0
- package/templates/skills/tools/verify-quality/scripts/quality_checker.js +337 -0
- package/templates/skills/tools/verify-security/SKILL.md +146 -0
- package/templates/skills/tools/verify-security/scripts/security_scanner.js +283 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// =============================================================================
|
|
3
|
+
// invoke-model.mjs
|
|
4
|
+
// ----------------------------------------------------------------------------
|
|
5
|
+
// Node ESM replacement for `codeagent-wrapper` (Go binary v5.10.0).
|
|
6
|
+
//
|
|
7
|
+
// ⚠️ DEPRECATED in v4.1 (2026-05-04, Phase 20)
|
|
8
|
+
// ----------------------------------------------------------------------------
|
|
9
|
+
// Replaced by `Agent(subagent_type="codex:codex-rescue")` and
|
|
10
|
+
// `Agent(subagent_type="gemini:gemini-rescue")` in the 6 core CCG commands
|
|
11
|
+
// (plan / execute / analyze / optimize / test / review).
|
|
12
|
+
//
|
|
13
|
+
// Why: v4.0.1 nested-spawn validation + objective comparison showed plugin
|
|
14
|
+
// rescue agents win 7 / 8 metrics (main-thread context drift, summary
|
|
15
|
+
// protocol, error recovery, etc); the only metric codeagent-wrapper wins
|
|
16
|
+
// ("full sandbox bypass") is unused in advisor scenarios.
|
|
17
|
+
//
|
|
18
|
+
// Status: Kept as **BC fallback** when the user has not installed
|
|
19
|
+
// `codex@openai-codex` and/or `gemini@google-gemini` plugins. Templates
|
|
20
|
+
// detect plugin availability and route to the right path automatically.
|
|
21
|
+
//
|
|
22
|
+
// Removal target: v5.0 (after 2 minor releases of dual-path coexistence).
|
|
23
|
+
//
|
|
24
|
+
// Migration helper: `src/utils/plugin-detection.ts` exposes
|
|
25
|
+
// `bothPluginsInstalled()` and per-plugin probes used by command
|
|
26
|
+
// templates' fallback decision narrative.
|
|
27
|
+
// ----------------------------------------------------------------------------
|
|
28
|
+
//
|
|
29
|
+
// Source of truth: `.ccg-migration/INVOKE-MODEL-SPEC.md`
|
|
30
|
+
// Cross-checked against `codeagent-wrapper/main.go`, `executor.go`,
|
|
31
|
+
// `parser.go`, `backend.go`, `config.go`, `utils.go`, `filter.go`.
|
|
32
|
+
//
|
|
33
|
+
// Equivalence with v5.10.0 (single-task path; --parallel/--cleanup/WebServer
|
|
34
|
+
// intentionally omitted — see spec §1.3 / §8.6):
|
|
35
|
+
// - CLI flags: --backend, --gemini-model[=], --progress, --lite/-L,
|
|
36
|
+
// --skip-permissions / --dangerously-skip-permissions[=], --version/-v,
|
|
37
|
+
// --help/-h
|
|
38
|
+
// - Positional args: form A `[task|-] [workdir]`
|
|
39
|
+
// form B `resume <session_id> [task|-] [workdir]`
|
|
40
|
+
// - Stdin auto-detection (explicit `-`, piped, special chars, len > 800)
|
|
41
|
+
// - ROLE_FILE: line replacement (with ~ + Windows /c/ -> C:/ normalisation)
|
|
42
|
+
// - codex `e --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check
|
|
43
|
+
// --json [resume <id>] [-C <workdir>] <task|->`
|
|
44
|
+
// - claude `-p [--dangerously-skip-permissions] --setting-sources ""
|
|
45
|
+
// [-r <id>] --output-format stream-json --verbose <task|->`
|
|
46
|
+
// - gemini `[-m <model>] -o stream-json -y [-r <id>]
|
|
47
|
+
// [--include-directories <wd>] [-p <task>]` (Windows: omit -p, pipe stdin)
|
|
48
|
+
// - JSON-line streaming parse for codex/claude/gemini events with
|
|
49
|
+
// camelCase + snake_case session_id, codex item.text string|array
|
|
50
|
+
// normalisation, MCP-prefix tolerant init line
|
|
51
|
+
// - SESSION_ID emitted on stderr (early ` Session-ID: <id>`) AND on stdout
|
|
52
|
+
// tail (`\n---\nSESSION_ID: <id>\n`)
|
|
53
|
+
// - post-message delay (5s default, 1s lite, env override 0..60s) before
|
|
54
|
+
// force-killing a backend that delivered agent_message but not
|
|
55
|
+
// turn.completed
|
|
56
|
+
// - stderr noise filter (10 substrings)
|
|
57
|
+
// - `~/.claude/settings.json` env injection
|
|
58
|
+
// - Cross-platform process termination (Windows taskkill /T /F /PID, Unix
|
|
59
|
+
// SIGTERM + 5s SIGKILL fallback)
|
|
60
|
+
// - Exit codes: 0 ok, 1 generic, 124 timeout, 127 not-found, 130 SIGINT,
|
|
61
|
+
// passthrough otherwise
|
|
62
|
+
// - --version prints `codeagent-wrapper version 5.10.0` (matches
|
|
63
|
+
// installer.ts EXPECTED_BINARY_VERSION check)
|
|
64
|
+
//
|
|
65
|
+
// Out of scope (intentionally NOT ported, see spec §1.3 / §8.6):
|
|
66
|
+
// - --parallel / --full-output / ---TASK--- ---CONTENT--- protocol
|
|
67
|
+
// - --cleanup / log file generation / async logger / log rotation
|
|
68
|
+
// - WebServer / SSE streaming
|
|
69
|
+
// - Structured report extraction (coverage / files / tests metrics)
|
|
70
|
+
// - ASCII mode, wrapper symlink alias
|
|
71
|
+
//
|
|
72
|
+
// Dependencies: Node.js built-in modules only.
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
import { spawn } from 'node:child_process';
|
|
76
|
+
import { readFileSync, statSync } from 'node:fs';
|
|
77
|
+
import { homedir } from 'node:os';
|
|
78
|
+
import path from 'node:path';
|
|
79
|
+
import process from 'node:process';
|
|
80
|
+
import { Buffer } from 'node:buffer';
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Constants (mirror codeagent-wrapper/main.go:16, executor.go:28,
|
|
84
|
+
// parser.go:57, filter.go:9).
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
const VERSION = '5.10.0';
|
|
87
|
+
const WRAPPER_NAME = 'codeagent-wrapper';
|
|
88
|
+
const DEFAULT_WORKDIR = '.';
|
|
89
|
+
const DEFAULT_TIMEOUT_SEC = 7200; // 2h, matches Go defaultTimeout
|
|
90
|
+
const DEFAULT_BACKEND = 'codex';
|
|
91
|
+
const STDIN_SPECIAL_CHARS = '\n\\"\'`$'; // utils.go:22
|
|
92
|
+
const STDIN_LENGTH_THRESHOLD = 800; // utils.go:54
|
|
93
|
+
const POST_MESSAGE_DELAY_DEFAULT_MS = 5_000; // executor.go:36
|
|
94
|
+
const POST_MESSAGE_DELAY_LITE_MS = 1_000; // executor.go:31
|
|
95
|
+
const POST_MESSAGE_DELAY_MAX_SEC = 60; // executor.go:45
|
|
96
|
+
const FORCE_KILL_DELAY_MS = 5_000; // main.go:67 forceKillDelay=5s
|
|
97
|
+
const FALLBACK_EXIT_GRACE_MS = 2_000; // executor.go:1200 (+2s)
|
|
98
|
+
const STDOUT_DRAIN_TIMEOUT_MS = 100; // main.go:31
|
|
99
|
+
const JSON_LINE_MAX_BYTES = 10 * 1024 * 1024; // parser.go:59
|
|
100
|
+
const PROGRESS_SNIPPET_MAX_RUNES = 120; // parser.go:272
|
|
101
|
+
const MAX_CLAUDE_SETTINGS_BYTES = 1 << 20; // backend.go:39
|
|
102
|
+
const STDERR_TAIL_BYTES = 4 * 1024; // main.go:23
|
|
103
|
+
|
|
104
|
+
// filter.go:9-23
|
|
105
|
+
const STDERR_NOISE_PATTERNS = [
|
|
106
|
+
'[STARTUP]',
|
|
107
|
+
'Session cleanup disabled',
|
|
108
|
+
'Warning:',
|
|
109
|
+
'(node:',
|
|
110
|
+
'(Use `node --trace-warnings',
|
|
111
|
+
'Loaded cached credentials',
|
|
112
|
+
'Loading extension:',
|
|
113
|
+
'YOLO mode is enabled',
|
|
114
|
+
'[WARN] Skipping unreadable directory',
|
|
115
|
+
'supports tool updates. Listening for changes',
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Tiny utilities
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
function envFlagEnabled(key) {
|
|
124
|
+
const raw = process.env[key];
|
|
125
|
+
if (raw === undefined) return false;
|
|
126
|
+
const v = String(raw).trim().toLowerCase();
|
|
127
|
+
return !(v === '' || v === '0' || v === 'false' || v === 'no' || v === 'off');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseBoolFlag(val, fallback) {
|
|
131
|
+
const v = String(val ?? '').trim().toLowerCase();
|
|
132
|
+
if (['1', 'true', 'yes', 'on'].includes(v)) return true;
|
|
133
|
+
if (['0', 'false', 'no', 'off'].includes(v)) return false;
|
|
134
|
+
return fallback;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function safeProgressSnippet(s, maxLen = PROGRESS_SNIPPET_MAX_RUNES) {
|
|
138
|
+
let str = (s ?? '').replace(/\n/g, ' ');
|
|
139
|
+
str = str.split(/\s+/).filter(Boolean).join(' ');
|
|
140
|
+
const runes = [...str];
|
|
141
|
+
if (maxLen <= 0 || runes.length <= maxLen) return str;
|
|
142
|
+
if (maxLen <= 3) return runes.slice(0, maxLen).join('');
|
|
143
|
+
return runes.slice(0, maxLen - 3).join('') + '...';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function quoteForProgress(s) { return JSON.stringify(s ?? ''); }
|
|
147
|
+
|
|
148
|
+
function normalizeWindowsPath(p) {
|
|
149
|
+
// utils.go:125 — only invoked when running on Windows.
|
|
150
|
+
let out = p.replace(/\\/g, '/');
|
|
151
|
+
const m = /^\/([a-zA-Z])\//.exec(out);
|
|
152
|
+
if (m) out = m[1].toUpperCase() + ':' + out.slice(2);
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Mirror Go exec.LookPath: Node child_process.spawn does not consult PATHEXT
|
|
157
|
+
// on Windows, so `spawn('codex')` ENOENTs even when codex.cmd is in PATH.
|
|
158
|
+
// Replace bare command names with their resolved absolute path before spawn.
|
|
159
|
+
function lookPath(cmd, opts = {}) {
|
|
160
|
+
const env = opts.env || process.env;
|
|
161
|
+
const platform = opts.platform || process.platform;
|
|
162
|
+
const stat = opts.statFn || statSync;
|
|
163
|
+
if (platform !== 'win32') return cmd;
|
|
164
|
+
if (path.isAbsolute(cmd) || cmd.includes('/') || cmd.includes('\\')) return cmd;
|
|
165
|
+
const pathExt = (env.PATHEXT || '.COM;.EXE;.BAT;.CMD')
|
|
166
|
+
.split(';').map((s) => s.trim()).filter(Boolean);
|
|
167
|
+
// Mirror Go exec.LookPath: on Windows, an extensionless name like `codex` is
|
|
168
|
+
// never executable on its own — must match PATHEXT. Only when the name
|
|
169
|
+
// already contains a dot do we additionally try the raw form.
|
|
170
|
+
const hasDot = cmd.includes('.');
|
|
171
|
+
const candidates = hasDot
|
|
172
|
+
? [cmd, ...pathExt.map((e) => cmd + e)]
|
|
173
|
+
: pathExt.map((e) => cmd + e);
|
|
174
|
+
const sep = platform === 'win32' ? ';' : ':';
|
|
175
|
+
const dirs = (env.PATH || env.Path || '').split(sep).filter(Boolean);
|
|
176
|
+
// Windows searches current directory first (CreateProcess), then PATH.
|
|
177
|
+
for (const dir of ['', ...dirs]) {
|
|
178
|
+
for (const c of candidates) {
|
|
179
|
+
const full = dir ? path.join(dir, c) : c;
|
|
180
|
+
try {
|
|
181
|
+
const info = stat(full);
|
|
182
|
+
if (info && info.isFile && info.isFile()) return full;
|
|
183
|
+
} catch { /* not found, continue */ }
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return cmd; // let spawn surface ENOENT
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function expandHome(p) {
|
|
190
|
+
if (typeof p !== 'string') return p;
|
|
191
|
+
if (p === '~') return homedir();
|
|
192
|
+
if (p.startsWith('~/') || p.startsWith('~\\')) {
|
|
193
|
+
return path.join(homedir(), p.slice(2));
|
|
194
|
+
}
|
|
195
|
+
return p;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function logWarn(msg) { process.stderr.write(`[WARN] ${msg}\n`); }
|
|
199
|
+
function logError(msg) { process.stderr.write(`[ERROR] ${msg}\n`); }
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// ROLE_FILE injection (utils.go:75)
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
function injectRoleFile(taskText) {
|
|
205
|
+
return taskText.replace(/^ROLE_FILE:\s*(.+)$/gm, (match, rawPath) => {
|
|
206
|
+
let filePath = rawPath.trim();
|
|
207
|
+
filePath = expandHome(filePath);
|
|
208
|
+
if (IS_WINDOWS) filePath = normalizeWindowsPath(filePath);
|
|
209
|
+
try {
|
|
210
|
+
return readFileSync(filePath, 'utf8');
|
|
211
|
+
} catch (err) {
|
|
212
|
+
logWarn(`Failed to read ROLE_FILE '${filePath}': ${err.message}`);
|
|
213
|
+
return match; // preserve original line on read failure (utils.go:108)
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// `~/.claude/settings.json` env loader (backend.go:43)
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
function loadMinimalEnvSettings() {
|
|
222
|
+
let home;
|
|
223
|
+
try { home = homedir(); } catch { return {}; }
|
|
224
|
+
if (!home) return {};
|
|
225
|
+
const settingsPath = path.join(home, '.claude', 'settings.json');
|
|
226
|
+
let info;
|
|
227
|
+
try { info = statSync(settingsPath); } catch { return {}; }
|
|
228
|
+
if (!info || info.size > MAX_CLAUDE_SETTINGS_BYTES) return {};
|
|
229
|
+
let data;
|
|
230
|
+
try { data = readFileSync(settingsPath, 'utf8'); } catch { return {}; }
|
|
231
|
+
let parsed;
|
|
232
|
+
try { parsed = JSON.parse(data); } catch { return {}; }
|
|
233
|
+
const env = {};
|
|
234
|
+
if (parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object') {
|
|
235
|
+
for (const [k, v] of Object.entries(parsed.env)) {
|
|
236
|
+
if (typeof v === 'string') env[k] = v;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return env;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Argument parsing (config.go:197)
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
function parseCliArgs(argv) {
|
|
246
|
+
let backend = DEFAULT_BACKEND;
|
|
247
|
+
let geminiModel = (process.env.GEMINI_MODEL || '').trim();
|
|
248
|
+
let progress = false;
|
|
249
|
+
let lite = envFlagEnabled('CODEAGENT_LITE_MODE');
|
|
250
|
+
let skipPermissions = envFlagEnabled('CODEAGENT_SKIP_PERMISSIONS');
|
|
251
|
+
const filtered = [];
|
|
252
|
+
|
|
253
|
+
for (let i = 0; i < argv.length; i++) {
|
|
254
|
+
const a = argv[i];
|
|
255
|
+
if (a === '--lite' || a === '-L') { lite = true; continue; }
|
|
256
|
+
if (a === '--progress') { progress = true; continue; }
|
|
257
|
+
if (a === '--backend') {
|
|
258
|
+
if (i + 1 >= argv.length) throw new Error('--backend flag requires a value');
|
|
259
|
+
backend = argv[++i];
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (a.startsWith('--backend=')) {
|
|
263
|
+
const v = a.slice('--backend='.length);
|
|
264
|
+
if (!v) throw new Error('--backend flag requires a value');
|
|
265
|
+
backend = v;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (a === '--gemini-model') {
|
|
269
|
+
if (i + 1 >= argv.length) throw new Error('--gemini-model flag requires a non-empty model name');
|
|
270
|
+
const v = (argv[++i] || '').trim();
|
|
271
|
+
if (!v) throw new Error('--gemini-model flag requires a non-empty model name');
|
|
272
|
+
geminiModel = v;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (a.startsWith('--gemini-model=')) {
|
|
276
|
+
const v = a.slice('--gemini-model='.length).trim();
|
|
277
|
+
if (!v) throw new Error('--gemini-model flag requires a non-empty model name');
|
|
278
|
+
geminiModel = v;
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (a === '--skip-permissions' || a === '--dangerously-skip-permissions') { skipPermissions = true; continue; }
|
|
282
|
+
if (a.startsWith('--skip-permissions=')) {
|
|
283
|
+
skipPermissions = parseBoolFlag(a.slice('--skip-permissions='.length), skipPermissions);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (a.startsWith('--dangerously-skip-permissions=')) {
|
|
287
|
+
skipPermissions = parseBoolFlag(a.slice('--dangerously-skip-permissions='.length), skipPermissions);
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
filtered.push(a);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (filtered.length === 0) throw new Error('task required');
|
|
294
|
+
|
|
295
|
+
const cfg = {
|
|
296
|
+
mode: 'new',
|
|
297
|
+
task: '',
|
|
298
|
+
sessionId: '',
|
|
299
|
+
workDir: DEFAULT_WORKDIR,
|
|
300
|
+
explicitStdin: false,
|
|
301
|
+
backend: (backend || DEFAULT_BACKEND).toLowerCase().trim(),
|
|
302
|
+
skipPermissions,
|
|
303
|
+
geminiModel,
|
|
304
|
+
progress,
|
|
305
|
+
lite,
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
if (filtered[0] === 'resume') {
|
|
309
|
+
if (filtered.length < 3) throw new Error('resume mode requires: resume <session_id> <task>');
|
|
310
|
+
cfg.mode = 'resume';
|
|
311
|
+
cfg.sessionId = (filtered[1] || '').trim();
|
|
312
|
+
if (!cfg.sessionId) throw new Error('resume mode requires non-empty session_id');
|
|
313
|
+
cfg.task = filtered[2];
|
|
314
|
+
cfg.explicitStdin = filtered[2] === '-';
|
|
315
|
+
if (filtered.length > 3) cfg.workDir = filtered[3];
|
|
316
|
+
} else {
|
|
317
|
+
cfg.task = filtered[0];
|
|
318
|
+
cfg.explicitStdin = filtered[0] === '-';
|
|
319
|
+
if (filtered.length > 1) cfg.workDir = filtered[1];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!['codex', 'gemini', 'claude'].includes(cfg.backend)) {
|
|
323
|
+
throw new Error(`unsupported backend "${cfg.backend}"`);
|
|
324
|
+
}
|
|
325
|
+
return cfg;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function shouldUseStdin(taskText, piped) {
|
|
329
|
+
if (piped) return true;
|
|
330
|
+
if (taskText.length > STDIN_LENGTH_THRESHOLD) return true;
|
|
331
|
+
for (const c of STDIN_SPECIAL_CHARS) if (taskText.includes(c)) return true;
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function resolveTimeoutSec() {
|
|
336
|
+
const raw = (process.env.CODEX_TIMEOUT || '').trim();
|
|
337
|
+
if (!raw) return DEFAULT_TIMEOUT_SEC;
|
|
338
|
+
const n = Number.parseInt(raw, 10);
|
|
339
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
340
|
+
logWarn(`Invalid CODEX_TIMEOUT '${raw}', falling back to ${DEFAULT_TIMEOUT_SEC}s`);
|
|
341
|
+
return DEFAULT_TIMEOUT_SEC;
|
|
342
|
+
}
|
|
343
|
+
return n > 10000 ? Math.floor(n / 1000) : n;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function resolvePostMessageDelayMs(lite) {
|
|
347
|
+
if (lite) return POST_MESSAGE_DELAY_LITE_MS;
|
|
348
|
+
const raw = (process.env.CODEAGENT_POST_MESSAGE_DELAY || '').trim();
|
|
349
|
+
if (!raw) return POST_MESSAGE_DELAY_DEFAULT_MS;
|
|
350
|
+
const v = Number.parseInt(raw, 10);
|
|
351
|
+
if (!Number.isFinite(v) || v < 0) {
|
|
352
|
+
logWarn(`Invalid CODEAGENT_POST_MESSAGE_DELAY=${JSON.stringify(raw)}, falling back to 5s`);
|
|
353
|
+
return POST_MESSAGE_DELAY_DEFAULT_MS;
|
|
354
|
+
}
|
|
355
|
+
if (v > POST_MESSAGE_DELAY_MAX_SEC) {
|
|
356
|
+
logWarn(`CODEAGENT_POST_MESSAGE_DELAY=${v} exceeds 60s, capping at 60s`);
|
|
357
|
+
return POST_MESSAGE_DELAY_MAX_SEC * 1000;
|
|
358
|
+
}
|
|
359
|
+
return v * 1000;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// Backend argv builders (backend.go + executor.go:757 buildCodexArgs)
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
function buildCodexArgs(cfg, targetArg) {
|
|
366
|
+
const args = ['e'];
|
|
367
|
+
if (!envFlagEnabled('CODEX_REQUIRE_APPROVAL')) args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
368
|
+
if (!envFlagEnabled('CODEX_DISABLE_SKIP_GIT_CHECK')) args.push('--skip-git-repo-check');
|
|
369
|
+
if (cfg.mode === 'resume') {
|
|
370
|
+
args.push('--json', 'resume', cfg.sessionId, targetArg);
|
|
371
|
+
return args;
|
|
372
|
+
}
|
|
373
|
+
args.push('-C', cfg.workDir, '--json', targetArg);
|
|
374
|
+
return args;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function buildClaudeArgs(cfg, targetArg) {
|
|
378
|
+
const args = ['-p'];
|
|
379
|
+
if (cfg.skipPermissions) args.push('--dangerously-skip-permissions');
|
|
380
|
+
args.push('--setting-sources', '');
|
|
381
|
+
if (cfg.mode === 'resume' && cfg.sessionId) args.push('-r', cfg.sessionId);
|
|
382
|
+
args.push('--output-format', 'stream-json', '--verbose', targetArg);
|
|
383
|
+
return args;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function buildGeminiArgs(cfg, targetArg) {
|
|
387
|
+
const args = [];
|
|
388
|
+
const model = (cfg.geminiModel || '').trim();
|
|
389
|
+
if (model) args.push('-m', model);
|
|
390
|
+
args.push('-o', 'stream-json', '-y');
|
|
391
|
+
if (cfg.mode === 'resume' && cfg.sessionId) args.push('-r', cfg.sessionId);
|
|
392
|
+
if (cfg.mode !== 'resume' && cfg.workDir) args.push('--include-directories', cfg.workDir);
|
|
393
|
+
if (targetArg !== '') args.push('-p', targetArg);
|
|
394
|
+
return args;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function backendCommandAndArgs(cfg, targetArg) {
|
|
398
|
+
switch (cfg.backend) {
|
|
399
|
+
case 'codex': return { command: 'codex', args: buildCodexArgs(cfg, targetArg) };
|
|
400
|
+
case 'claude': return { command: 'claude', args: buildClaudeArgs(cfg, targetArg) };
|
|
401
|
+
case 'gemini': return { command: 'gemini', args: buildGeminiArgs(cfg, targetArg) };
|
|
402
|
+
default: throw new Error(`unsupported backend "${cfg.backend}"`);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
// Stderr noise filter (filter.go) — line-buffered.
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
function makeStderrFilter(target) {
|
|
410
|
+
let pending = '';
|
|
411
|
+
const tail = []; let tailLen = 0;
|
|
412
|
+
const appendTail = (s) => {
|
|
413
|
+
tail.push(s); tailLen += Buffer.byteLength(s, 'utf8');
|
|
414
|
+
while (tailLen > STDERR_TAIL_BYTES && tail.length > 1) {
|
|
415
|
+
tailLen -= Buffer.byteLength(tail.shift(), 'utf8');
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
const shouldFilter = (line) => STDERR_NOISE_PATTERNS.some((p) => line.includes(p));
|
|
419
|
+
return {
|
|
420
|
+
write(chunk) {
|
|
421
|
+
pending += chunk;
|
|
422
|
+
let idx;
|
|
423
|
+
while ((idx = pending.indexOf('\n')) !== -1) {
|
|
424
|
+
const line = pending.slice(0, idx + 1);
|
|
425
|
+
pending = pending.slice(idx + 1);
|
|
426
|
+
appendTail(line);
|
|
427
|
+
if (!shouldFilter(line)) target.write(line);
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
flush() {
|
|
431
|
+
if (!pending) return;
|
|
432
|
+
appendTail(pending);
|
|
433
|
+
if (!shouldFilter(pending)) target.write(pending);
|
|
434
|
+
pending = '';
|
|
435
|
+
},
|
|
436
|
+
tail() { return tail.join('').slice(-STDERR_TAIL_BYTES); },
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// ---------------------------------------------------------------------------
|
|
441
|
+
// JSON-line stream parser (parser.go).
|
|
442
|
+
//
|
|
443
|
+
// Yields a parsed { message, sessionId } and emits side-effect callbacks
|
|
444
|
+
// (onMessage / onComplete / onProgress / onSession) similar to
|
|
445
|
+
// parseJSONStreamInternalWithContent.
|
|
446
|
+
// ---------------------------------------------------------------------------
|
|
447
|
+
function makeJsonStreamParser({ onProgress, onSession, onMessage, onComplete }) {
|
|
448
|
+
let pending = Buffer.alloc(0);
|
|
449
|
+
let totalEvents = 0;
|
|
450
|
+
let codexMessage = '';
|
|
451
|
+
let claudeMessage = '';
|
|
452
|
+
const geminiBuffer = [];
|
|
453
|
+
let sessionId = '';
|
|
454
|
+
|
|
455
|
+
const emitProgress = (event, fields) => {
|
|
456
|
+
if (!onProgress) return;
|
|
457
|
+
const parts = [event];
|
|
458
|
+
if (fields) {
|
|
459
|
+
for (const key of ['id', 'text', 'cmd', 'exit', 'total_events']) {
|
|
460
|
+
const v = fields[key];
|
|
461
|
+
if (v !== undefined && String(v).trim() !== '') parts.push(`${key}=${v}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
onProgress(`[PROGRESS] ${parts.join(' ')}`);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const emitSession = (id) => {
|
|
468
|
+
if (!id) return;
|
|
469
|
+
if (!sessionId) sessionId = id;
|
|
470
|
+
if (onSession) onSession(id);
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// codex agent_message text may be string or []string (parser.go:522)
|
|
474
|
+
const normalizeText = (t) => {
|
|
475
|
+
if (typeof t === 'string') return t;
|
|
476
|
+
if (Array.isArray(t)) return t.filter((x) => typeof x === 'string').join('');
|
|
477
|
+
return '';
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
const handleLine = (rawLine) => {
|
|
481
|
+
let line = rawLine.trim();
|
|
482
|
+
if (!line) return;
|
|
483
|
+
if (Buffer.byteLength(line, 'utf8') > JSON_LINE_MAX_BYTES) {
|
|
484
|
+
logWarn(`Skipped overlong JSON line (> ${JSON_LINE_MAX_BYTES} bytes)`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
totalEvents++;
|
|
488
|
+
|
|
489
|
+
let evt;
|
|
490
|
+
try { evt = JSON.parse(line); }
|
|
491
|
+
catch (_) {
|
|
492
|
+
// Gemini init line may be prefixed with MCP banner text (parser.go:178)
|
|
493
|
+
const idx = line.indexOf('{');
|
|
494
|
+
if (idx > 0) {
|
|
495
|
+
try { evt = JSON.parse(line.slice(idx)); }
|
|
496
|
+
catch (_e2) { return; }
|
|
497
|
+
} else { return; }
|
|
498
|
+
}
|
|
499
|
+
if (!evt || typeof evt !== 'object') return;
|
|
500
|
+
|
|
501
|
+
// Session id from snake_case OR camelCase (parser.go:97)
|
|
502
|
+
const evtSession = evt.session_id || evt.sessionId || '';
|
|
503
|
+
if (evtSession && !sessionId) emitSession(evtSession);
|
|
504
|
+
|
|
505
|
+
const itemType = evt.item && typeof evt.item === 'object' ? evt.item.type : '';
|
|
506
|
+
const isCodex = !!evt.thread_id || evt.type === 'turn.completed' || evt.type === 'turn.started' || (evt.item && itemType);
|
|
507
|
+
const isClaude = (evt.subtype !== undefined && evt.subtype !== '') || (evt.result !== undefined && evt.result !== '')
|
|
508
|
+
|| (evt.type === 'result' && evtSession && evt.status === undefined);
|
|
509
|
+
const isGemini = (evt.role !== undefined && evt.role !== '')
|
|
510
|
+
|| evt.delta !== undefined
|
|
511
|
+
|| (evt.status !== undefined && evt.status !== '')
|
|
512
|
+
|| (evt.type === 'init' && evtSession);
|
|
513
|
+
|
|
514
|
+
if (isCodex) {
|
|
515
|
+
switch (evt.type) {
|
|
516
|
+
case 'thread.started':
|
|
517
|
+
if (evt.thread_id) emitSession(evt.thread_id);
|
|
518
|
+
emitProgress('session_started', { id: sessionId });
|
|
519
|
+
break;
|
|
520
|
+
case 'turn.started':
|
|
521
|
+
emitProgress('turn_started');
|
|
522
|
+
break;
|
|
523
|
+
case 'thread.completed':
|
|
524
|
+
case 'turn.completed': {
|
|
525
|
+
if (evt.thread_id && !sessionId) emitSession(evt.thread_id);
|
|
526
|
+
const ev = evt.type === 'thread.completed' ? 'session_completed' : 'turn_completed';
|
|
527
|
+
emitProgress(ev, { total_events: totalEvents });
|
|
528
|
+
if (onComplete) onComplete();
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case 'item.completed': {
|
|
532
|
+
if (itemType === 'agent_message' || itemType === 'reasoning') {
|
|
533
|
+
const text = normalizeText(evt.item && evt.item.text);
|
|
534
|
+
if (text) {
|
|
535
|
+
if (itemType === 'agent_message') {
|
|
536
|
+
codexMessage = text;
|
|
537
|
+
if (onMessage) onMessage();
|
|
538
|
+
emitProgress('message', { text: quoteForProgress(safeProgressSnippet(text)) });
|
|
539
|
+
} else {
|
|
540
|
+
emitProgress('reasoning', { text: quoteForProgress(safeProgressSnippet(text)) });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} else if (itemType === 'command_execution') {
|
|
544
|
+
const cmdItem = evt.item || {};
|
|
545
|
+
const fields = { cmd: quoteForProgress(safeProgressSnippet(cmdItem.command || '')) };
|
|
546
|
+
if (cmdItem.exit_code !== undefined && cmdItem.exit_code !== null) fields.exit = cmdItem.exit_code;
|
|
547
|
+
emitProgress('cmd_done', fields);
|
|
548
|
+
} else if (itemType === 'mcp_tool_call') {
|
|
549
|
+
emitProgress('mcp_call');
|
|
550
|
+
}
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (isClaude) {
|
|
558
|
+
if (typeof evt.result === 'string' && evt.result !== '') {
|
|
559
|
+
claudeMessage = evt.result;
|
|
560
|
+
if (onMessage) onMessage();
|
|
561
|
+
}
|
|
562
|
+
if (evt.type === 'result' && onComplete) onComplete();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (isGemini) {
|
|
567
|
+
if (typeof evt.content === 'string' && evt.content !== '') geminiBuffer.push(evt.content);
|
|
568
|
+
if (evt.status) {
|
|
569
|
+
if (onMessage) onMessage();
|
|
570
|
+
if (evt.type === 'result' && ['success', 'error', 'complete', 'failed'].includes(evt.status) && onComplete) onComplete();
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
// unknown event — ignore
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
feed(buf) {
|
|
579
|
+
pending = pending.length === 0 ? buf : Buffer.concat([pending, buf]);
|
|
580
|
+
let nlIdx;
|
|
581
|
+
while ((nlIdx = pending.indexOf(0x0a)) !== -1) {
|
|
582
|
+
const lineBuf = pending.subarray(0, nlIdx);
|
|
583
|
+
pending = pending.subarray(nlIdx + 1);
|
|
584
|
+
handleLine(lineBuf.toString('utf8'));
|
|
585
|
+
}
|
|
586
|
+
},
|
|
587
|
+
end() {
|
|
588
|
+
if (pending.length > 0) {
|
|
589
|
+
handleLine(pending.toString('utf8'));
|
|
590
|
+
pending = Buffer.alloc(0);
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
result() {
|
|
594
|
+
let message;
|
|
595
|
+
if (geminiBuffer.length > 0) message = geminiBuffer.join('');
|
|
596
|
+
else if (claudeMessage) message = claudeMessage;
|
|
597
|
+
else message = codexMessage;
|
|
598
|
+
return { message, sessionId };
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ---------------------------------------------------------------------------
|
|
604
|
+
// Process termination (executor.go:1421 killProcessTree, terminateCommand).
|
|
605
|
+
// ---------------------------------------------------------------------------
|
|
606
|
+
function killWindowsTree(pid) {
|
|
607
|
+
try {
|
|
608
|
+
const r = spawn('taskkill', ['/T', '/F', '/PID', String(pid)], {
|
|
609
|
+
stdio: 'ignore',
|
|
610
|
+
windowsHide: true,
|
|
611
|
+
});
|
|
612
|
+
r.on('error', () => {});
|
|
613
|
+
} catch { /* ignore */ }
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function terminateChild(child, { force } = { force: false }) {
|
|
617
|
+
if (!child || child.killed || child.exitCode !== null) return;
|
|
618
|
+
if (IS_WINDOWS) {
|
|
619
|
+
killWindowsTree(child.pid);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
try { child.kill(force ? 'SIGKILL' : 'SIGTERM'); } catch { /* ignore */ }
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Read full stdin into a UTF-8 string.
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
function readAllStdin() {
|
|
629
|
+
return new Promise((resolve, reject) => {
|
|
630
|
+
const chunks = [];
|
|
631
|
+
process.stdin.on('data', (d) => chunks.push(d));
|
|
632
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
633
|
+
process.stdin.on('error', reject);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function isStdinPiped() {
|
|
638
|
+
try { return !process.stdin.isTTY; } catch { return false; }
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// ---------------------------------------------------------------------------
|
|
642
|
+
// Help / version
|
|
643
|
+
// ---------------------------------------------------------------------------
|
|
644
|
+
function printHelp() {
|
|
645
|
+
process.stdout.write(`${WRAPPER_NAME} - Node shim for AI CLI backends (replaces Go binary v${VERSION})
|
|
646
|
+
|
|
647
|
+
Usage:
|
|
648
|
+
${WRAPPER_NAME} [--backend codex|gemini|claude] [--gemini-model NAME] [--progress] [--lite] "task" [workdir]
|
|
649
|
+
${WRAPPER_NAME} [flags] - [workdir] Read task from stdin
|
|
650
|
+
${WRAPPER_NAME} [flags] resume <session_id> "task" [workdir]
|
|
651
|
+
${WRAPPER_NAME} [flags] resume <session_id> - [workdir]
|
|
652
|
+
${WRAPPER_NAME} --version
|
|
653
|
+
${WRAPPER_NAME} --help
|
|
654
|
+
|
|
655
|
+
Exit codes: 0 ok | 1 error | 124 timeout | 127 not-found | 130 SIGINT | else passthrough
|
|
656
|
+
`);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ---------------------------------------------------------------------------
|
|
660
|
+
// Main
|
|
661
|
+
// ---------------------------------------------------------------------------
|
|
662
|
+
async function main() {
|
|
663
|
+
const argv = process.argv.slice(2);
|
|
664
|
+
|
|
665
|
+
if (argv.length === 0) { printHelp(); return 1; }
|
|
666
|
+
const first = argv[0];
|
|
667
|
+
if (first === '--version' || first === '-v') {
|
|
668
|
+
process.stdout.write(`${WRAPPER_NAME} version ${VERSION}\n`);
|
|
669
|
+
return 0;
|
|
670
|
+
}
|
|
671
|
+
if (first === '--help' || first === '-h') { printHelp(); return 0; }
|
|
672
|
+
|
|
673
|
+
let cfg;
|
|
674
|
+
try { cfg = parseCliArgs(argv); }
|
|
675
|
+
catch (e) { logError(e.message); return 1; }
|
|
676
|
+
|
|
677
|
+
if (cfg.geminiModel && cfg.backend !== 'gemini') {
|
|
678
|
+
logWarn('--gemini-model parameter is only effective with --backend gemini');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
const timeoutSec = resolveTimeoutSec();
|
|
682
|
+
|
|
683
|
+
// Resolve task text -------------------------------------------------------
|
|
684
|
+
const piped = isStdinPiped();
|
|
685
|
+
let taskText;
|
|
686
|
+
if (cfg.explicitStdin) {
|
|
687
|
+
const data = await readAllStdin();
|
|
688
|
+
if (!data) { logError('Explicit stdin mode requires task input from stdin'); return 1; }
|
|
689
|
+
taskText = data;
|
|
690
|
+
} else if (piped) {
|
|
691
|
+
const data = await readAllStdin();
|
|
692
|
+
taskText = data || cfg.task;
|
|
693
|
+
} else {
|
|
694
|
+
taskText = cfg.task;
|
|
695
|
+
}
|
|
696
|
+
taskText = injectRoleFile(taskText);
|
|
697
|
+
|
|
698
|
+
const useStdin = cfg.explicitStdin || shouldUseStdin(taskText, piped);
|
|
699
|
+
|
|
700
|
+
// targetArg switch (executor.go:864)
|
|
701
|
+
const geminiDirect = useStdin && cfg.backend === 'gemini' && !IS_WINDOWS;
|
|
702
|
+
const geminiStdinPipe = useStdin && cfg.backend === 'gemini' && IS_WINDOWS;
|
|
703
|
+
let targetArg = taskText;
|
|
704
|
+
if (useStdin && !geminiDirect && !geminiStdinPipe) targetArg = '-';
|
|
705
|
+
if (geminiStdinPipe) targetArg = '';
|
|
706
|
+
|
|
707
|
+
const { command, args } = backendCommandAndArgs(cfg, targetArg);
|
|
708
|
+
|
|
709
|
+
// Startup banner (main.go:432)
|
|
710
|
+
process.stderr.write(
|
|
711
|
+
`[${WRAPPER_NAME}]\n` +
|
|
712
|
+
` Backend: ${cfg.backend}\n` +
|
|
713
|
+
` Command: ${command} ${args.join(' ')}\n` +
|
|
714
|
+
` PID: ${process.pid}\n` +
|
|
715
|
+
` Log: <stderr>\n`,
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
// Spawn ------------------------------------------------------------------
|
|
719
|
+
const env = { ...process.env, ...loadMinimalEnvSettings() };
|
|
720
|
+
const spawnOpts = {
|
|
721
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
722
|
+
windowsHide: true,
|
|
723
|
+
env,
|
|
724
|
+
};
|
|
725
|
+
// Codex passes workdir via -C flag — don't set Dir (executor.go:1001).
|
|
726
|
+
if (cfg.mode !== 'resume' && cfg.workDir && cfg.backend !== 'codex') {
|
|
727
|
+
spawnOpts.cwd = cfg.workDir;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
let resolvedCommand = lookPath(command);
|
|
731
|
+
let resolvedArgs = args;
|
|
732
|
+
// Windows: spawning .cmd/.bat directly throws EINVAL (Node CVE-2024-27980
|
|
733
|
+
// mitigation). Wrap with cmd.exe /c to keep arg array semantics without
|
|
734
|
+
// tripping DEP0190 (`shell:true + args[]` deprecation in Node 24+).
|
|
735
|
+
if (IS_WINDOWS && /\.(cmd|bat)$/i.test(resolvedCommand)) {
|
|
736
|
+
spawnOpts.windowsVerbatimArguments = true;
|
|
737
|
+
resolvedArgs = ['/d', '/s', '/c', `"${resolvedCommand}"`, ...args];
|
|
738
|
+
resolvedCommand = process.env.ComSpec || 'cmd.exe';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
let child;
|
|
742
|
+
try { child = spawn(resolvedCommand, resolvedArgs, spawnOpts); }
|
|
743
|
+
catch (e) {
|
|
744
|
+
logError(`failed to start ${command}: ${e.message}`);
|
|
745
|
+
return 1;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let spawnErrored = false;
|
|
749
|
+
child.on('error', (err) => {
|
|
750
|
+
spawnErrored = true;
|
|
751
|
+
if (err && err.code === 'ENOENT') {
|
|
752
|
+
logError(`${command} command not found in PATH`);
|
|
753
|
+
mainExitCode = 127;
|
|
754
|
+
} else {
|
|
755
|
+
logError(`failed to start ${command}: ${err && err.message ? err.message : String(err)}`);
|
|
756
|
+
mainExitCode = 1;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
// Stdin -------------------------------------------------------------------
|
|
761
|
+
// For non-gemini-direct stdin path, write taskText then close.
|
|
762
|
+
if (useStdin && !geminiDirect && child.stdin) {
|
|
763
|
+
child.stdin.on('error', () => { /* swallow EPIPE */ });
|
|
764
|
+
child.stdin.end(taskText, 'utf8');
|
|
765
|
+
} else if (child.stdin && !useStdin) {
|
|
766
|
+
// Even when not piping a task, ensure the child's stdin is closed so it
|
|
767
|
+
// does not block waiting for input.
|
|
768
|
+
child.stdin.end();
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Stderr filter -----------------------------------------------------------
|
|
772
|
+
const stderrFilter = makeStderrFilter(process.stderr);
|
|
773
|
+
child.stderr.setEncoding('utf8');
|
|
774
|
+
child.stderr.on('data', (chunk) => stderrFilter.write(chunk));
|
|
775
|
+
|
|
776
|
+
// Stdout JSON parsing -----------------------------------------------------
|
|
777
|
+
let messageSeen = false;
|
|
778
|
+
let completeSeen = false;
|
|
779
|
+
let postMessageTimer = null;
|
|
780
|
+
let fallbackExitTimer = null;
|
|
781
|
+
let forceKillTimer = null;
|
|
782
|
+
let forcedAfterComplete = false;
|
|
783
|
+
let sessionEmitted = false;
|
|
784
|
+
let sessionId = '';
|
|
785
|
+
|
|
786
|
+
const onSession = (id) => {
|
|
787
|
+
if (sessionEmitted || !id) return;
|
|
788
|
+
sessionEmitted = true;
|
|
789
|
+
sessionId = id;
|
|
790
|
+
process.stderr.write(` Session-ID: ${id}\n`);
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
const startPostMessageTimer = () => {
|
|
794
|
+
if (postMessageTimer) return;
|
|
795
|
+
postMessageTimer = setTimeout(() => {
|
|
796
|
+
postMessageTimer = null;
|
|
797
|
+
forcedAfterComplete = true;
|
|
798
|
+
// Close stdout BEFORE killing on Windows so cmd.Wait()-equivalent
|
|
799
|
+
// (the 'exit' / 'close' events) is unblocked (executor.go:1190).
|
|
800
|
+
try { child.stdout.destroy(); } catch { /* ignore */ }
|
|
801
|
+
terminateChild(child);
|
|
802
|
+
// Schedule force-kill (5s on Unix; immediate is no-op on Windows since
|
|
803
|
+
// taskkill /F already happened above).
|
|
804
|
+
if (!IS_WINDOWS && !forceKillTimer) {
|
|
805
|
+
forceKillTimer = setTimeout(() => terminateChild(child, { force: true }), FORCE_KILL_DELAY_MS);
|
|
806
|
+
}
|
|
807
|
+
// Fallback exit timer (executor.go:1199): if 'exit' never fires, bail.
|
|
808
|
+
if (!fallbackExitTimer) {
|
|
809
|
+
fallbackExitTimer = setTimeout(() => {
|
|
810
|
+
fallbackExitTimer = null;
|
|
811
|
+
finalize({ forced: true });
|
|
812
|
+
}, FORCE_KILL_DELAY_MS + FALLBACK_EXIT_GRACE_MS);
|
|
813
|
+
}
|
|
814
|
+
}, resolvePostMessageDelayMs(cfg.lite));
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const parser = makeJsonStreamParser({
|
|
818
|
+
onProgress: cfg.progress ? (line) => process.stderr.write(line + '\n') : undefined,
|
|
819
|
+
onSession,
|
|
820
|
+
onMessage: () => { messageSeen = true; },
|
|
821
|
+
onComplete: () => {
|
|
822
|
+
completeSeen = true;
|
|
823
|
+
// post-message delay window opens when we observe completion
|
|
824
|
+
// (executor.go:1210 — but post-delay timer started after the FIRST
|
|
825
|
+
// completion event regardless of message arrival).
|
|
826
|
+
startPostMessageTimer();
|
|
827
|
+
},
|
|
828
|
+
});
|
|
829
|
+
child.stdout.on('data', (chunk) => parser.feed(chunk));
|
|
830
|
+
child.stdout.on('end', () => parser.end());
|
|
831
|
+
|
|
832
|
+
// Signal handling ---------------------------------------------------------
|
|
833
|
+
let externalSignal = null;
|
|
834
|
+
const installSignalHandlers = () => {
|
|
835
|
+
const onSig = (sig) => {
|
|
836
|
+
externalSignal = sig;
|
|
837
|
+
terminateChild(child);
|
|
838
|
+
if (!IS_WINDOWS && !forceKillTimer) {
|
|
839
|
+
forceKillTimer = setTimeout(() => terminateChild(child, { force: true }), FORCE_KILL_DELAY_MS);
|
|
840
|
+
}
|
|
841
|
+
};
|
|
842
|
+
process.on('SIGINT', () => onSig('SIGINT'));
|
|
843
|
+
process.on('SIGTERM', () => onSig('SIGTERM'));
|
|
844
|
+
};
|
|
845
|
+
installSignalHandlers();
|
|
846
|
+
|
|
847
|
+
// Timeout -----------------------------------------------------------------
|
|
848
|
+
let timedOut = false;
|
|
849
|
+
const timeoutHandle = setTimeout(() => {
|
|
850
|
+
timedOut = true;
|
|
851
|
+
terminateChild(child);
|
|
852
|
+
if (!IS_WINDOWS && !forceKillTimer) {
|
|
853
|
+
forceKillTimer = setTimeout(() => terminateChild(child, { force: true }), FORCE_KILL_DELAY_MS);
|
|
854
|
+
}
|
|
855
|
+
}, timeoutSec * 1000);
|
|
856
|
+
|
|
857
|
+
// Wait for exit + finalize ------------------------------------------------
|
|
858
|
+
let mainExitCode = 0;
|
|
859
|
+
let finalized = false;
|
|
860
|
+
let resolveDone;
|
|
861
|
+
const done = new Promise((r) => { resolveDone = r; });
|
|
862
|
+
|
|
863
|
+
const finalize = ({ forced = false } = {}) => {
|
|
864
|
+
if (finalized) return;
|
|
865
|
+
finalized = true;
|
|
866
|
+
clearTimeout(timeoutHandle);
|
|
867
|
+
if (postMessageTimer) clearTimeout(postMessageTimer);
|
|
868
|
+
if (fallbackExitTimer) clearTimeout(fallbackExitTimer);
|
|
869
|
+
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
870
|
+
|
|
871
|
+
// Drain any tail bytes from stdout/stderr.
|
|
872
|
+
parser.end();
|
|
873
|
+
stderrFilter.flush();
|
|
874
|
+
resolveDone(forced);
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
child.on('exit', () => {
|
|
878
|
+
// Allow stdout 'end' event to arrive before parsing the result so we don't
|
|
879
|
+
// miss a trailing turn.completed event.
|
|
880
|
+
setTimeout(finalize, STDOUT_DRAIN_TIMEOUT_MS);
|
|
881
|
+
});
|
|
882
|
+
child.on('close', () => setTimeout(finalize, STDOUT_DRAIN_TIMEOUT_MS));
|
|
883
|
+
|
|
884
|
+
await done;
|
|
885
|
+
|
|
886
|
+
// Determine exit code -----------------------------------------------------
|
|
887
|
+
const { message, sessionId: parsedSession } = parser.result();
|
|
888
|
+
const finalSession = sessionId || parsedSession;
|
|
889
|
+
|
|
890
|
+
if (spawnErrored) {
|
|
891
|
+
return mainExitCode || 1;
|
|
892
|
+
}
|
|
893
|
+
if (externalSignal) return 130;
|
|
894
|
+
if (timedOut) return 124;
|
|
895
|
+
|
|
896
|
+
const childExit = child.exitCode;
|
|
897
|
+
const childSig = child.signalCode;
|
|
898
|
+
|
|
899
|
+
// forcedAfterComplete + non-empty message -> success (executor.go:1286)
|
|
900
|
+
if (forcedAfterComplete && message) {
|
|
901
|
+
process.stdout.write(message);
|
|
902
|
+
if (finalSession) process.stdout.write(`\n---\nSESSION_ID: ${finalSession}\n`);
|
|
903
|
+
return 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (childExit !== null && childExit !== 0) {
|
|
907
|
+
// Recent stderr tail for diagnostics
|
|
908
|
+
const tail = stderrFilter.tail();
|
|
909
|
+
if (tail) process.stderr.write(`\n=== Recent Errors ===\n${tail}`);
|
|
910
|
+
return childExit;
|
|
911
|
+
}
|
|
912
|
+
if (childSig === 'SIGINT' || childSig === 'SIGTERM') return 130;
|
|
913
|
+
|
|
914
|
+
if (!message) {
|
|
915
|
+
logError(`${cfg.backend} completed without agent_message output`);
|
|
916
|
+
return 1;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
process.stdout.write(message);
|
|
920
|
+
if (finalSession) process.stdout.write(`\n---\nSESSION_ID: ${finalSession}\n`);
|
|
921
|
+
return 0;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// ---------------------------------------------------------------------------
|
|
925
|
+
// Entry point — only run when invoked as a script, so tests can import helpers.
|
|
926
|
+
// ---------------------------------------------------------------------------
|
|
927
|
+
const isMainModule = (() => {
|
|
928
|
+
try {
|
|
929
|
+
const entry = process.argv[1] && path.resolve(process.argv[1]);
|
|
930
|
+
const self = new URL(import.meta.url).pathname;
|
|
931
|
+
const selfNorm = process.platform === 'win32'
|
|
932
|
+
? path.resolve(self.replace(/^\//, ''))
|
|
933
|
+
: path.resolve(self);
|
|
934
|
+
return entry && entry === selfNorm;
|
|
935
|
+
} catch { return true; }
|
|
936
|
+
})();
|
|
937
|
+
|
|
938
|
+
if (isMainModule) {
|
|
939
|
+
main().then((code) => {
|
|
940
|
+
// Flush stdout (Windows Git Bash bug, main.go:496).
|
|
941
|
+
if (process.stdout.write('')) process.exit(code);
|
|
942
|
+
else process.stdout.once('drain', () => process.exit(code));
|
|
943
|
+
}).catch((err) => {
|
|
944
|
+
logError(err && err.stack ? err.stack : String(err));
|
|
945
|
+
process.exit(1);
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export { lookPath };
|