@launchchair/agent 0.4.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/AGENTS.md ADDED
@@ -0,0 +1,248 @@
1
+ # LaunchChair Agent Bootstrap
2
+
3
+ This file is for fresh agent chats that need to use the LaunchChair Agent API or MCP bridge without wasting time searching an empty repo.
4
+
5
+ ## Core Rule
6
+
7
+ Use LaunchChair first for product, research, validation, positioning, pricing, MVP blueprint, and phase-loop work. Do not inspect the local repo first unless the user is in Stack Setup, Build MVP, Design and Landing Page implementation, or another code-changing phase.
8
+
9
+ Fresh LaunchChair projects often do not have a local repo yet. Before Stack Setup, the configured runner folder may be empty on purpose.
10
+
11
+ ## Public Docs To Read
12
+
13
+ - Agent API docs: https://www.launchchair.io/docs/agent-api
14
+ - MCP setup docs: https://www.launchchair.io/docs/mcp
15
+ - Optional CLI docs: https://www.launchchair.io/docs/cli
16
+ - Public machine-readable catalog: https://www.launchchair.io/api/agent/v1/docs
17
+ - Bridge file catalog: https://www.launchchair.io/api/agent/v1/bridge
18
+
19
+ ## Security Rules
20
+
21
+ - Never print a LaunchChair token.
22
+ - Never paste a LaunchChair token into public docs, issue comments, screenshots, or chat summaries.
23
+ - Real tokens come from LaunchChair Settings -> API Access or from an approved `launchchair login` browser flow.
24
+ - Public docs and artifacts contain placeholders only.
25
+ - If you inspect env files, confirm whether token variables exist without showing their values.
26
+ - If the user asks you to create or continue a LaunchChair project and the configured token permits it, do not ask for extra confirmation before using LaunchChair MCP/API tools. The token scope is the authorization boundary.
27
+ - Ask for confirmation only for external account authorization, production SQL execution, destructive local file actions, or shell commands that the client sandbox requires the user to approve.
28
+
29
+ ## Bridge And Runner Files
30
+
31
+ Download these into a stable local folder such as `~/launchchair-agent` or `~/.hermes/launchchair-mcp`:
32
+
33
+ ```bash
34
+ mkdir -p "$HOME/launchchair-agent"
35
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/local-bridge.mjs" -o "$HOME/launchchair-agent/local-bridge.mjs"
36
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/launchchair-cli.mjs" -o "$HOME/launchchair-agent/launchchair-cli.mjs"
37
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/mcp-bridge-server.mjs" -o "$HOME/launchchair-agent/mcp-bridge-server.mjs"
38
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/runner-command.mjs" -o "$HOME/launchchair-agent/runner-command.mjs"
39
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/codex-runner.mjs" -o "$HOME/launchchair-agent/codex-runner.mjs"
40
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/claude-code-runner.mjs" -o "$HOME/launchchair-agent/claude-code-runner.mjs"
41
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/chatgpt-runner.mjs" -o "$HOME/launchchair-agent/chatgpt-runner.mjs"
42
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/claude-runner.mjs" -o "$HOME/launchchair-agent/claude-runner.mjs"
43
+ curl -fsSL "https://www.launchchair.io/agent-artifacts/AGENTS.md" -o "$HOME/launchchair-agent/AGENTS.md"
44
+ chmod +x "$HOME/launchchair-agent"/*.mjs
45
+ LAUNCHCHAIR_AGENT_WORKDIR="${LC_AGENT_CODEX_CWD:-${LC_AGENT_CLAUDE_CODE_CWD:-$PWD}}"
46
+ if [ -d "$LAUNCHCHAIR_AGENT_WORKDIR" ] && [ ! -f "$LAUNCHCHAIR_AGENT_WORKDIR/AGENTS.md" ]; then
47
+ printf '%s\n' \
48
+ '# LaunchChair Agent Bootstrap Pointer' \
49
+ '' \
50
+ 'Before searching this repo, read ~/launchchair-agent/AGENTS.md.' \
51
+ 'Use LaunchChair MCP/API first for product phases, project discovery, project creation, and phase loops.' \
52
+ 'Do not ask for extra confirmation before using LaunchChair MCP/API when the user already requested the action and the token permits it.' \
53
+ 'Only inspect this repo for Stack Setup, Build MVP, Design/Landing Page, SEO implementation, smoke testing, security review, or code remediation.' \
54
+ > "$LAUNCHCHAIR_AGENT_WORKDIR/AGENTS.md"
55
+ fi
56
+ ```
57
+
58
+ The final block creates a tiny project-local `AGENTS.md` pointer only when the target folder does not already have one. It helps fresh Codex chats auto-load the LaunchChair instructions without overwriting a real project instruction file.
59
+
60
+ ## Optional CLI
61
+
62
+ MCP-only setup is still supported. Use `mcp-bridge-server.mjs` when the desktop agent exposes LaunchChair MCP tools. Use `launchchair-cli.mjs` when you need quick diagnostics, project discovery, retry-safe project creation, MCP config output, or a simple way to start the local bridge.
63
+
64
+ ```bash
65
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" doctor
66
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" login
67
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" projects --query LeafLore
68
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" create --name SignalLift --one-liner "Distribution workspace for technical founders" --project-type b2b --idempotency-key signallift-v1
69
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" run-once
70
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" bridge
71
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" mcp-config codex
72
+ ```
73
+
74
+ ## Required Environment Variables
75
+
76
+ ```bash
77
+ LAUNCHCHAIR_BASE_URL="https://www.launchchair.io"
78
+ LAUNCHCHAIR_AGENT_TOKEN="<full lc_at token from LaunchChair Settings API Access>"
79
+ LAUNCHCHAIR_PROJECT_ID="optional project UUID; omit or leave blank if creating or discovering a project"
80
+ ```
81
+
82
+ For Codex Desktop local runs:
83
+
84
+ ```bash
85
+ LC_AGENT_ROUTER_GPT_TARGET="codex"
86
+ LC_AGENT_ROUTER_CODEX_TARGET="codex"
87
+ LC_AGENT_CODEX_CWD="/absolute/path/to/the-folder-codex-should-build-into"
88
+ ```
89
+
90
+ For Claude Desktop plus Claude Code local runs:
91
+
92
+ ```bash
93
+ LC_AGENT_ROUTER_GPT_TARGET="claude_code"
94
+ LC_AGENT_ROUTER_CODEX_TARGET="claude_code"
95
+ LC_AGENT_CLAUDE_CODE_CWD="/absolute/path/to/the-folder-claude-code-should-build-into"
96
+ ```
97
+
98
+ The build folder can be an empty scratch folder until Stack Setup creates or records the GitHub repository. After Stack Setup, the user should clone the generated repo locally or update the runner CWD to the cloned repo path.
99
+
100
+ ## MCP Server Config Example
101
+
102
+ ```toml
103
+ [mcp_servers.launchchair]
104
+ command = "node"
105
+ args = ["/Users/you/launchchair-agent/mcp-bridge-server.mjs"]
106
+
107
+ [mcp_servers.launchchair.env]
108
+ LAUNCHCHAIR_BASE_URL = "https://www.launchchair.io"
109
+ LAUNCHCHAIR_AGENT_TOKEN = "<full lc_at token from Settings API Access>"
110
+ LAUNCHCHAIR_PROJECT_ID = ""
111
+ LC_AGENT_ROUTER_GPT_TARGET = "codex"
112
+ LC_AGENT_ROUTER_CODEX_TARGET = "codex"
113
+ LC_AGENT_CODEX_CWD = "/absolute/path/to/the-folder-codex-should-build-into"
114
+ ```
115
+
116
+ ## What To Do First In A Fresh Chat
117
+
118
+ 1. If LaunchChair MCP tools are available, call `launchchair_heartbeat`.
119
+ 2. Call `launchchair_status` to confirm token, bridge, runner, and project context.
120
+ 3. If `LAUNCHCHAIR_PROJECT_ID` is blank and the user named a project, call `launchchair_list_projects` and select the matching project.
121
+ 4. If the user asks to create a new LaunchChair project, call `launchchair_create_project`.
122
+ 5. If the user clicked Run with agent in LaunchChair, call `launchchair_run_once`.
123
+ 6. If the user asks to advance the product workflow, call `launchchair_continue_project`.
124
+ 7. If LaunchChair asks for a user-choice bucket such as wedge, pricing, or feature direction, call `launchchair_select_choice`. Do not use generic SPEC_UPDATE apply for user-choice buckets.
125
+
126
+ ## Consent Behavior
127
+
128
+ Do not ask the user for repeated permission to perform normal LaunchChair project-loop work when the user already requested it and the token allows it.
129
+
130
+ Safe LaunchChair MCP/API actions that should not need another user confirmation:
131
+
132
+ - creating a LaunchChair project with `launchchair_create_project`
133
+ - listing accessible LaunchChair projects with `launchchair_list_projects`
134
+ - continuing a project loop with `launchchair_continue_project`
135
+ - selecting a generated wedge, pricing, or feature direction with `launchchair_select_choice`
136
+ - running/rerunning Product Ideation, Market Validation, Positioning, Pricing, and MVP Blueprint buckets
137
+ - returning phase synthesis links and selected choices
138
+
139
+ Stop and ask only when:
140
+
141
+ - the local client sandbox asks the user to approve a shell command
142
+ - the user must authorize GitHub, Vercel, Supabase, Stripe, or another external account
143
+ - production SQL must be executed
144
+ - Product-track manual QA requires a human when LaunchChair explicitly returns that checkpoint; Feature-track QA should be executed by the agent with matching evidence whenever the local runtime is available
145
+ - a destructive local filesystem action is required
146
+ - the token is missing a required scope
147
+
148
+ If Codex asks for permission to run the local bridge fallback command, tell the user they can choose "Yes, and don't ask again for commands that start with ..." for the LaunchChair bridge command prefix. Prefer exposed MCP tools when available because MCP tool calls avoid shell-command permission prompts.
149
+
150
+ ## When To Avoid Repo Inspection
151
+
152
+ Do not start with `ls`, `rg`, dependency inspection, or nearby repo searches when the user asks for:
153
+
154
+ - product ideation
155
+ - market validation
156
+ - ICP research
157
+ - wedge selection
158
+ - positioning and pricing
159
+ - MVP blueprint
160
+ - LaunchChair project creation
161
+ - LaunchChair phase loops
162
+ - score-based reruns such as ICP Pain Score, Market Saturation Score, or Wedge Opportunity Score
163
+
164
+ Those are LaunchChair project-state tasks, not local repo tasks.
165
+
166
+ ## When Repo Inspection Is Correct
167
+
168
+ Inspect the repo when the current LaunchChair work is:
169
+
170
+ - Stack Setup local readiness
171
+ - Build MVP cards
172
+ - Design and Landing Page implementation
173
+ - SEO, GEO, AEO implementation against an existing app
174
+ - smoke testing, security review, or code remediation
175
+
176
+ For Build MVP work, read the repo `AGENTS.md` first if it exists, then inspect the files named in the LaunchChair build prompt.
177
+
178
+ ## Human Checkpoints
179
+
180
+ Stop and give the user a direct LaunchChair link when LaunchChair needs:
181
+
182
+ - GitHub authorization or repo selection
183
+ - Vercel account, team, project, or domain selection
184
+ - Supabase project selection or SQL execution
185
+ - Stripe account authorization
186
+ - Product-track manual QA, or Feature QA that is genuinely blocked by unavailable credentials, external authorization, or an inaccessible runtime
187
+ - a human product choice that the token cannot save
188
+
189
+ Do not claim that SQL migrations, production deploys, billing setup, or QA are complete unless you have verified evidence. For Feature projects, run the available QA yourself and return matching `executionEvidence.agentQaChecks` instead of handing runnable checks back to the user.
190
+
191
+ ## Progress Reporting
192
+
193
+ Keep chat updates short. Report:
194
+
195
+ - the LaunchChair project name and phase
196
+ - the bucket or card being worked
197
+ - choices selected, especially wedge, pricing, and feature direction
198
+ - phase synthesis links returned by LaunchChair
199
+ - exact user checkpoints with links to the relevant LaunchChair page or build-card tab
200
+
201
+ Avoid long explanations of local file searching when LaunchChair MCP tools are available.
202
+
203
+ ## Common Commands
204
+
205
+ Run a full project loop:
206
+
207
+ ```text
208
+ Call launchchair_continue_project with projectId, maxSteps, and optional stopPhaseKey.
209
+ ```
210
+
211
+ Create a project from a new idea:
212
+
213
+ ```text
214
+ Call launchchair_create_project with projectName, oneLiner, projectType, and ideaSnapshot.
215
+ ```
216
+
217
+ Find a project by name:
218
+
219
+ ```text
220
+ Call launchchair_list_projects with query set to the user-provided project name.
221
+ ```
222
+
223
+ Run one queued UI job:
224
+
225
+ ```text
226
+ Call launchchair_run_once.
227
+ ```
228
+
229
+ Run one queued UI job from CLI:
230
+
231
+ ```bash
232
+ node "$HOME/launchchair-agent/launchchair-cli.mjs" run-once
233
+ ```
234
+
235
+ Select a generated wedge:
236
+
237
+ ```text
238
+ Call launchchair_select_choice with domain="wedge" and optionTitle or optionId.
239
+ ```
240
+
241
+ ## Do Not Do This
242
+
243
+ - Do not brute-force project state by writing arbitrary JSON Patch to `projects.spec_json`.
244
+ - Do not use generic SPEC_UPDATE for user-choice buckets.
245
+ - Do not invent missing Supabase, Vercel, GitHub, or Stripe access.
246
+ - Do not run production SQL without explicit user authorization and a known project ref.
247
+ - Do not mark QA complete without matching evidence. For Feature projects, produce that evidence by running the repository-native or browser checks whenever possible.
248
+ - Do not search the whole filesystem for LaunchChair source when the public docs and MCP tools are available.
package/LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ Copyright (c) LaunchChair.
2
+
3
+ All rights reserved.
4
+
5
+ This package is provided for use with LaunchChair services. You may install,
6
+ execute, and configure this package to connect authorized local agent clients to
7
+ LaunchChair. No permission is granted to copy, modify, sublicense, sell, or
8
+ redistribute this package except as expressly permitted by LaunchChair.
package/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # LaunchChair Agent
2
+
3
+ LaunchChair Agent packages the LaunchChair CLI, MCP bridge, local bridge, and runner scripts used by Codex, Claude Code, ChatGPT, Claude, and Hermes workflows.
4
+
5
+ Use it when you want an agent to create or discover LaunchChair projects, run product phases, continue project loops, pick up `Run with agent` jobs from LaunchChair, or expose LaunchChair tools through MCP.
6
+
7
+ ## Install Locally For Testing
8
+
9
+ From this package directory:
10
+
11
+ ```bash
12
+ npm pack
13
+ npm install -g ./launchchair-agent-*.tgz
14
+ launchchair help
15
+ launchchair doctor
16
+ ```
17
+
18
+ After publishing:
19
+
20
+ ```bash
21
+ npm install -g @launchchair/agent
22
+ launchchair help
23
+ launchchair doctor
24
+ ```
25
+
26
+ ## Login
27
+
28
+ Approve the CLI once in your browser. LaunchChair creates a scoped Agent API token after approval and the CLI stores it locally:
29
+
30
+ ```bash
31
+ launchchair login
32
+ launchchair doctor
33
+ launchchair whoami
34
+ ```
35
+
36
+ Manual fallback when you already have a token from LaunchChair Settings -> API Access:
37
+
38
+ ```bash
39
+ launchchair login --token "<full lc_at token from LaunchChair Settings API Access>"
40
+ ```
41
+
42
+ The CLI stores local config at:
43
+
44
+ ```text
45
+ ~/.launchchair/config.json
46
+ ```
47
+
48
+ Tokens are masked in CLI output. Do not paste real tokens into public docs, screenshots, issue comments, or chat summaries.
49
+
50
+ ## Guided Setup
51
+
52
+ Use `setup` as the easiest path. It saves the build folder, writes a small `AGENTS.md` pointer into that folder, starts browser login when no token is saved yet, prints the MCP config for your desktop agent, and tells you exactly what to do next.
53
+
54
+ Codex Desktop:
55
+
56
+ ```bash
57
+ launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder
58
+ ```
59
+
60
+ Claude Desktop + Claude Code:
61
+
62
+ ```bash
63
+ launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
64
+ ```
65
+
66
+ Terminal / CLI agents:
67
+
68
+ ```bash
69
+ launchchair setup terminal --cwd /absolute/path/to/repo-or-empty-folder
70
+ ```
71
+
72
+ If you do not want browser login during setup, add `--skip-login`. You can also save an existing token during setup:
73
+
74
+ ```bash
75
+ launchchair setup codex --cwd . --skip-login
76
+ launchchair setup codex --cwd . --token "<full lc_at token from LaunchChair Settings API Access>"
77
+ ```
78
+
79
+ For a fresh project before a repo exists, point `--cwd` at an empty folder. LaunchChair can run ideation, validation, positioning, pricing, and MVP blueprint before code exists; Stack Setup and Build MVP can later use that folder as the build target.
80
+
81
+ Use `setup terminal` when the agent runs inside a normal shell instead of Codex Desktop or Claude Desktop MCP. It writes the same project-local `AGENTS.md` pointer, saves the build folder, and prints the CLI commands the agent should use: `launchchair projects`, `launchchair create`, `launchchair continue`, `launchchair run-once`, and `launchchair bridge`.
82
+
83
+ ## Common Commands
84
+
85
+ ```bash
86
+ launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder
87
+ launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
88
+ launchchair setup terminal --cwd /absolute/path/to/repo-or-empty-folder
89
+ launchchair projects
90
+ launchchair projects --query LeafLore
91
+ launchchair create --name "SignalLift" --one-liner "Distribution workspace for technical founders" --project-type b2b --save-project
92
+ launchchair continue --stop-phase-key market_validation
93
+ launchchair continue --completion-target "rerun Product Ideation until ICP Pain Score is above 85"
94
+ launchchair run-once
95
+ launchchair bridge
96
+ launchchair mcp-config codex
97
+ launchchair mcp-config claude
98
+ launchchair install-agent-md --cwd /absolute/path/to/repo-or-empty-folder
99
+ ```
100
+
101
+ Feature beta projects can be created and planned from the CLI when the token owner has access:
102
+
103
+ ```bash
104
+ launchchair create \
105
+ --name "Retention Engine" \
106
+ --project-kind feature \
107
+ --feature-description "Add account-level lifecycle nudges to an existing SaaS dashboard" \
108
+ --problem "Activated users forget the next valuable action after onboarding" \
109
+ --product-description "Existing B2B SaaS dashboard with auth, billing, and workspace teams" \
110
+ --plan-to-build-board \
111
+ --save-project
112
+
113
+ # Optional: continue from feature planning into Build MVP
114
+ launchchair create \
115
+ --name "Retention Engine" \
116
+ --project-kind feature \
117
+ --feature-description "Add account-level lifecycle nudges to an existing SaaS dashboard" \
118
+ --problem "Activated users forget the next valuable action after onboarding" \
119
+ --full-loop \
120
+ --max-steps 12 \
121
+ --build-max-steps 25
122
+ ```
123
+
124
+ ## MCP Config
125
+
126
+ Print a Codex MCP config example:
127
+
128
+ ```bash
129
+ launchchair mcp-config codex
130
+ ```
131
+
132
+ Print a Claude Desktop MCP config example:
133
+
134
+ ```bash
135
+ launchchair mcp-config claude
136
+ ```
137
+
138
+ The printed config intentionally uses a token placeholder. Paste your real `lc_at_` token into your MCP client config when the desktop client launches MCP directly, or use `launchchair login` for CLI-started bridge sessions.
139
+
140
+ Desktop MCP clients do not automatically inherit the token saved by `launchchair login` unless they launch through a CLI-managed bridge process. For direct MCP config, put the token in that client config's `env` block.
141
+
142
+ ## Local Bridge
143
+
144
+ Use the local bridge when the LaunchChair web app's `Run with agent` button should queue work and your local agent should pick it up:
145
+
146
+ ```bash
147
+ launchchair bridge
148
+ ```
149
+
150
+ For a one-shot test:
151
+
152
+ ```bash
153
+ launchchair run-once
154
+ ```
155
+
156
+ ## Runner Targets
157
+
158
+ Codex:
159
+
160
+ ```bash
161
+ launchchair config set codex-cwd /absolute/path/to/repo-or-empty-folder
162
+ launchchair bridge
163
+ ```
164
+
165
+ Claude Code:
166
+
167
+ ```bash
168
+ launchchair config set claude-code-cwd /absolute/path/to/repo-or-empty-folder
169
+ LC_AGENT_ROUTER_GPT_TARGET=claude_code LC_AGENT_ROUTER_CODEX_TARGET=claude_code launchchair bridge
170
+ ```
171
+
172
+ ChatGPT and Claude API runner fallbacks require provider API keys:
173
+
174
+ ```bash
175
+ OPENAI_API_KEY=... launchchair bridge
176
+ ANTHROPIC_API_KEY=... launchchair bridge
177
+ ```
178
+
179
+ ## Fresh Agent Chats
180
+
181
+ This package includes `AGENTS.md`. Fresh Codex, Claude, and Hermes chats should read that file before searching an empty repo. Product ideation, market validation, positioning, pricing, MVP blueprint, and project loops are LaunchChair API/MCP tasks. Local repo inspection starts later, usually at Stack Setup or Build MVP.
182
+
183
+ ## Docs
184
+
185
+ - CLI docs: https://www.launchchair.io/docs/cli
186
+ - MCP docs: https://www.launchchair.io/docs/mcp
187
+ - Agent API docs: https://www.launchchair.io/docs/agent-api
188
+ - Machine-readable catalog: https://www.launchchair.io/api/agent/v1/docs
@@ -0,0 +1,239 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from "node:process";
4
+
5
+ function env(name, fallback = "") {
6
+ const value = process.env[name];
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
8
+ }
9
+
10
+ function toInt(value, fallback) {
11
+ const parsed = Number.parseInt(String(value ?? ""), 10);
12
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
13
+ }
14
+
15
+ function toFloat(value, fallback) {
16
+ const parsed = Number.parseFloat(String(value ?? ""));
17
+ return Number.isFinite(parsed) ? parsed : fallback;
18
+ }
19
+
20
+ function stripCodeFence(text) {
21
+ const trimmed = String(text ?? "").trim();
22
+ if (!trimmed.startsWith("```")) {
23
+ return trimmed;
24
+ }
25
+ const lines = trimmed.split("\n");
26
+ if (lines.length < 3) {
27
+ return trimmed;
28
+ }
29
+ const first = lines[0].trim();
30
+ const last = lines[lines.length - 1].trim();
31
+ if (!first.startsWith("```") || last !== "```") {
32
+ return trimmed;
33
+ }
34
+ return lines.slice(1, -1).join("\n").trim();
35
+ }
36
+
37
+ function tryJsonParse(candidate) {
38
+ try {
39
+ return { ok: true, value: JSON.parse(candidate) };
40
+ } catch {
41
+ return { ok: false, value: null };
42
+ }
43
+ }
44
+
45
+ function extractFirstJsonValue(text) {
46
+ const raw = String(text ?? "");
47
+ const direct = tryJsonParse(raw.trim());
48
+ if (direct.ok) {
49
+ return direct.value;
50
+ }
51
+
52
+ const unfenced = stripCodeFence(raw);
53
+ const fromFence = tryJsonParse(unfenced);
54
+ if (fromFence.ok) {
55
+ return fromFence.value;
56
+ }
57
+
58
+ for (let start = 0; start < unfenced.length; start += 1) {
59
+ const open = unfenced[start];
60
+ if (open !== "{" && open !== "[") {
61
+ continue;
62
+ }
63
+ const close = open === "{" ? "}" : "]";
64
+ let depth = 0;
65
+ let inString = false;
66
+ let escaped = false;
67
+
68
+ for (let i = start; i < unfenced.length; i += 1) {
69
+ const char = unfenced[i];
70
+
71
+ if (inString) {
72
+ if (escaped) {
73
+ escaped = false;
74
+ } else if (char === "\\") {
75
+ escaped = true;
76
+ } else if (char === "\"") {
77
+ inString = false;
78
+ }
79
+ continue;
80
+ }
81
+
82
+ if (char === "\"") {
83
+ inString = true;
84
+ continue;
85
+ }
86
+ if (char === open) {
87
+ depth += 1;
88
+ continue;
89
+ }
90
+ if (char === close) {
91
+ depth -= 1;
92
+ if (depth === 0) {
93
+ const candidate = unfenced.slice(start, i + 1).trim();
94
+ const parsed = tryJsonParse(candidate);
95
+ if (parsed.ok) {
96
+ return parsed.value;
97
+ }
98
+ break;
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ return null;
105
+ }
106
+
107
+ function readStdin() {
108
+ return new Promise((resolve, reject) => {
109
+ let buffer = "";
110
+ process.stdin.setEncoding("utf8");
111
+ process.stdin.on("data", (chunk) => {
112
+ buffer += chunk;
113
+ });
114
+ process.stdin.on("end", () => resolve(buffer));
115
+ process.stdin.on("error", (error) => reject(error));
116
+ });
117
+ }
118
+
119
+ function extractCompletionText(payload) {
120
+ const choices = Array.isArray(payload?.choices) ? payload.choices : [];
121
+ const choice = choices[0] ?? null;
122
+ const content = choice?.message?.content;
123
+ if (typeof content === "string") {
124
+ return content;
125
+ }
126
+ if (Array.isArray(content)) {
127
+ const merged = content
128
+ .map((part) => {
129
+ if (typeof part?.text === "string") {
130
+ return part.text;
131
+ }
132
+ if (typeof part?.content === "string") {
133
+ return part.content;
134
+ }
135
+ return "";
136
+ })
137
+ .join("\n")
138
+ .trim();
139
+ if (merged) {
140
+ return merged;
141
+ }
142
+ }
143
+ if (typeof payload?.output_text === "string") {
144
+ return payload.output_text;
145
+ }
146
+ throw new Error("OpenAI response did not include assistant text.");
147
+ }
148
+
149
+ async function callOpenAi(input, config) {
150
+ const controller = new AbortController();
151
+ const timeout = setTimeout(() => controller.abort(), config.timeoutMs);
152
+
153
+ try {
154
+ const response = await fetch(`${config.baseUrl}/chat/completions`, {
155
+ method: "POST",
156
+ headers: {
157
+ Authorization: `Bearer ${config.apiKey}`,
158
+ "Content-Type": "application/json",
159
+ },
160
+ body: JSON.stringify({
161
+ model: config.model,
162
+ temperature: config.temperature,
163
+ max_tokens: config.maxTokens,
164
+ messages: [
165
+ {
166
+ role: "system",
167
+ content:
168
+ "Return exactly one valid JSON value (object or array) and nothing else. No markdown, no prose.",
169
+ },
170
+ {
171
+ role: "user",
172
+ content: input,
173
+ },
174
+ ],
175
+ }),
176
+ signal: controller.signal,
177
+ });
178
+
179
+ const payload = await response.json().catch(() => ({}));
180
+ if (!response.ok) {
181
+ const detail = String(payload?.error?.message ?? payload?.error ?? response.statusText ?? "Request failed");
182
+ throw new Error(`OpenAI request failed (${response.status}): ${detail}`);
183
+ }
184
+
185
+ return extractCompletionText(payload);
186
+ } finally {
187
+ clearTimeout(timeout);
188
+ }
189
+ }
190
+
191
+ async function main() {
192
+ const config = {
193
+ apiKey: env("OPENAI_API_KEY"),
194
+ baseUrl: env("OPENAI_BASE_URL", "https://api.openai.com/v1").replace(/\/+$/, ""),
195
+ model: env("LC_AGENT_OPENAI_MODEL", env("OPENAI_MODEL", "gpt-5-mini")),
196
+ temperature: toFloat(process.env.LC_AGENT_OPENAI_TEMPERATURE, 0),
197
+ maxTokens: toInt(process.env.LC_AGENT_OPENAI_MAX_TOKENS, 12000),
198
+ timeoutMs: toInt(process.env.LC_AGENT_OPENAI_TIMEOUT_MS, 180000),
199
+ maxAttempts: toInt(process.env.LC_AGENT_JSON_MAX_ATTEMPTS, 3),
200
+ };
201
+
202
+ if (!config.apiKey) {
203
+ throw new Error("Missing OPENAI_API_KEY for chatgpt-runner.");
204
+ }
205
+
206
+ const prompt = String(await readStdin()).trim();
207
+ if (!prompt) {
208
+ throw new Error("Runner received an empty prompt.");
209
+ }
210
+
211
+ let lastRaw = "";
212
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt += 1) {
213
+ const input =
214
+ attempt === 1
215
+ ? prompt
216
+ : [
217
+ "Repair this to strict valid JSON only.",
218
+ "Do not add explanations, markdown, or code fences.",
219
+ "Return exactly one JSON value.",
220
+ "",
221
+ lastRaw,
222
+ ].join("\n");
223
+
224
+ lastRaw = await callOpenAi(input, config);
225
+ const parsed = extractFirstJsonValue(lastRaw);
226
+ if (parsed !== null) {
227
+ process.stdout.write(`${JSON.stringify(parsed, null, 2)}\n`);
228
+ return;
229
+ }
230
+ }
231
+
232
+ throw new Error("Model did not return valid JSON after retry attempts.");
233
+ }
234
+
235
+ main().catch((error) => {
236
+ const message = error instanceof Error ? error.message : String(error ?? "chatgpt-runner failed");
237
+ process.stderr.write(`${message}\n`);
238
+ process.exit(1);
239
+ });