@launchchair/agent 0.4.12 → 0.5.1

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 CHANGED
@@ -35,6 +35,9 @@ mkdir -p "$HOME/launchchair-agent"
35
35
  curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/local-bridge.mjs" -o "$HOME/launchchair-agent/local-bridge.mjs"
36
36
  curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/launchchair-cli.mjs" -o "$HOME/launchchair-agent/launchchair-cli.mjs"
37
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/mcp-capabilities.mjs" -o "$HOME/launchchair-agent/mcp-capabilities.mjs"
39
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/mcp-widgets.mjs" -o "$HOME/launchchair-agent/mcp-widgets.mjs"
40
+ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/mcp-widget.html" -o "$HOME/launchchair-agent/mcp-widget.html"
38
41
  curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/project-workspace.mjs" -o "$HOME/launchchair-agent/project-workspace.mjs"
39
42
  curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/runner-command.mjs" -o "$HOME/launchchair-agent/runner-command.mjs"
40
43
  curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/codex-runner.mjs" -o "$HOME/launchchair-agent/codex-runner.mjs"
@@ -75,7 +78,7 @@ LAUNCHCHAIR_BASE_URL="https://www.launchchair.io"
75
78
  LAUNCHCHAIR_PROJECT_ID="optional project UUID; omit or leave blank if creating or discovering a project"
76
79
  ```
77
80
 
78
- `launchchair login` is the recommended token path. It saves the token to `~/.launchchair/config.json`, and `launchchair mcp-server` reads that saved token. Use `LAUNCHCHAIR_AGENT_TOKEN` only as a manual fallback when browser login is skipped or a per-client token override is needed.
81
+ `launchchair login` saves the token to `~/.launchchair/config.json`, shared by the CLI and MCP. Use `LAUNCHCHAIR_AGENT_TOKEN` as a manual fallback when browser login is skipped. A fresh saved login takes precedence over stale environment credentials; logout disables authentication until login. Use a separate `LAUNCHCHAIR_CONFIG_DIR` for a separate per-client account.
79
82
  The packaged MCP and local bridges also read the same saved CLI config as a fallback, so a desktop MCP launch should not require pasting a token into Codex or Claude config after browser login. Guided setup verifies standards-compliant MCP initialize and tool discovery before it reports success.
80
83
 
81
84
  For Codex Desktop local runs, choose the runner globally and bind each project clone independently:
@@ -122,14 +125,15 @@ Prefer `launchchair setup codex` so the CLI writes native config and verifies MC
122
125
 
123
126
  ## What To Do First In A Fresh Chat
124
127
 
125
- 1. If LaunchChair MCP tools are available, call `launchchair_heartbeat`.
126
- 2. Call `launchchair_status` to confirm token, bridge, runner, and project context.
128
+ 1. Call `launchchair_status`. If login is required, call `launchchair_login`, present the approval URL, then poll `launchchair_login_status` at its returned interval.
129
+ 2. If paid access is required, present the returned upgrade URL and wait for the user to upgrade before resuming the blocked operation.
127
130
  3. If `LAUNCHCHAIR_PROJECT_ID` is blank and the user named a project, call `launchchair_list_projects` and select the matching project.
128
131
  4. If the user asks to create a new LaunchChair project, call `launchchair_create_project`.
129
- 5. If the user clicked Run with agent in LaunchChair, call `launchchair_run_once`.
130
- 6. If the user asks to advance the product workflow, call `launchchair_continue_project`.
132
+ 5. To pick up queued work or advance the workflow, call `launchchair_next_step`, execute its claimed prompt in the current host agent, and submit the result with `launchchair_complete_run`.
133
+ 6. Use `launchchair_run_once` or `executionMode=local_runner` only when the user explicitly requests a separate local runner.
131
134
  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.
132
135
  8. When the user asks for validation scores or wants to compare ideas, call `launchchair_get_scorecards`. Report validation scorecards separately from generated direction-option scores.
136
+ 9. In hosts that support MCP Apps, present `launchchair_show_choices` at a choice step and after a selection, `launchchair_show_phase_synthesis` after phase completion, and `launchchair_show_validation_scores` for validation results. Their Next action controls return work to the host agent. If UI is unsupported, summarize the structured result and show the returned URLs.
133
137
 
134
138
  ## Consent Behavior
135
139
 
@@ -218,7 +222,7 @@ Avoid long explanations of local file searching when LaunchChair MCP tools are a
218
222
  Run a full project loop:
219
223
 
220
224
  ```text
221
- Call launchchair_continue_project with projectId, maxSteps, and optional stopPhaseKey.
225
+ Call launchchair_next_step with projectId and optional stopPhaseKey, execute the claimed prompt with the host's tools, and call launchchair_complete_run. Repeat until the requested target or a genuine checkpoint.
222
226
  ```
223
227
 
224
228
  Create a project from a new idea:
@@ -233,18 +237,20 @@ Find a project by name:
233
237
  Call launchchair_list_projects with query set to the user-provided project name.
234
238
  ```
235
239
 
236
- Run one queued UI job:
240
+ Claim one queued UI job for execution by the current agent:
237
241
 
238
242
  ```text
239
- Call launchchair_run_once.
243
+ Call launchchair_next_step with projectId.
240
244
  ```
241
245
 
242
- Run one queued UI job from CLI:
246
+ Run one queued UI job through an explicitly requested local runner:
243
247
 
244
248
  ```bash
245
249
  launchchair run-once
246
250
  ```
247
251
 
252
+ Every MCP tool is also accessible through `launchchair call TOOL --json '{...}'` or `--input FILE`. Use `launchchair tools --json` for schemas. Tool results are JSON, tool errors exit nonzero, and token secrets remain in private files. Prefer host execution for agents with connected GitHub/Vercel/Supabase tools.
253
+
248
254
  Select a generated wedge:
249
255
 
250
256
  ```text
@@ -259,3 +265,15 @@ Call launchchair_select_choice with domain="wedge" and optionTitle or optionId.
259
265
  - Do not run production SQL without explicit user authorization and a known project ref.
260
266
  - Do not mark QA complete without matching evidence. For Feature projects, produce that evidence by running the repository-native or browser checks whenever possible.
261
267
  - Do not search the whole filesystem for LaunchChair source when the public docs and MCP tools are available.
268
+
269
+ ## MCP login and host execution
270
+
271
+ The MCP can start without a token. Call `launchchair_login`, show the returned LaunchChair approval URL, and call `launchchair_login_status` after approval. Respect its poll interval. Tokens are stored privately in the local config and never returned to the conversation. Existing tokens need a fresh owner-approved login for `tokens.manage`. Use `launchchair_create_token` to save a scoped credential file, `launchchair_list_tokens` for redacted metadata, and `launchchair_revoke_token`/`launchchair_logout` for cleanup.
272
+
273
+ Prefer `launchchair_next_step`, execute the claimed prompt in the current agent with its already-connected plugins, call `launchchair_complete_run`, and repeat. Do not spawn another model or local CLI unless explicitly requested (`executionMode=local_runner`). A waiting run is already claimed; poll it instead of executing it twice. Inspect forms/checklists with `launchchair_get_bucket`, save allowed patches with `launchchair_update_bucket`, and supply factual `executionEvidence` for setup and verification. Use `launchchair_get_starter` after validation to read the starter files and the host's GitHub/Vercel/Supabase tools to create and deploy the user's app. Bind the verified clone with `launchchair_bind_repository`. Use `stopPhaseKey=launch_sales` for the complete Product workflow.
274
+
275
+ Preserve a supplied idea and specified requirements, even with low validation scores. `ideaIntent=preserve` is the default. Report risks and continue the requested work when no score gate was requested; never pivot merely to improve a score. Choose a generated direction consistent with the supplied idea. Only use `ideaIntent=explore` for explicit discovery or pivot requests.
276
+
277
+ For “do not build until VERY high validation,” create candidates with `validationGate=very_high` (pain >=85, opportunity >=85, saturation <=30), validate through `market_validation`, and compare with `launchchair_compare_projects`. The gate is enforced on LaunchChair build/setup actions and cannot be weakened through generic mutations. Do not provision external resources until it passes. If no candidate qualifies, report the unmet thresholds honestly. Scores are research estimates, not a guarantee of market demand. Do not fabricate evidence or replace validation scores with direction-option scores.
278
+
279
+ Research sources, competitor notes, Pexels assets, logo upload, form/checklist data, prompts, build cards, run management, and markdown exports are available as MCP tools. Tool descriptions distinguish data writes from actual external execution. Existing user authorization carries forward; ask only at a genuine missing authorization or information boundary.
package/README.md CHANGED
@@ -4,24 +4,37 @@ LaunchChair Agent packages the LaunchChair CLI, MCP bridge, local bridge, and ru
4
4
 
5
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
6
 
7
- ## Install Locally For Testing
8
-
9
- From this package directory:
7
+ ## Install or update
10
8
 
11
9
  ```bash
12
- npm pack
13
- npm install -g ./launchchair-agent-*.tgz
14
- launchchair help
10
+ npm install -g @launchchair/agent@latest
11
+ launchchair --version
15
12
  launchchair doctor
16
13
  ```
17
14
 
18
- After publishing:
15
+ Version 0.5.1 adds tabbed MCP Apps widgets for saved choices, rich phase synthesis, and Product/Feature validation scorecards to the expanded MCP, browser login, private token management, host-agent execution, idea preservation, and optional validation gates. Existing installations must update the package and restart their MCP connection to load the new tools.
19
16
 
20
- ```bash
21
- npm install -g @launchchair/agent
22
- launchchair help
23
- launchchair doctor
24
- ```
17
+ After updating, run `launchchair setup codex --skip-login` or `launchchair setup claude --skip-login` to refresh the installed path and verify tool discovery. Then restart/reload that client. Your saved account login and project bindings remain in `~/.launchchair/config.json`. Older tokens still work within their existing scopes; run `launchchair login` again with workspace-owner approval if you need the new `tokens.manage` scope.
18
+
19
+ For local release testing, run `npm pack ./packages/launchchair-agent --pack-destination /tmp` from the repository root and install the resulting tarball into an isolated npm prefix before publishing.
20
+
21
+ ## Agent response widgets
22
+
23
+ The bridge supports LaunchChair AUX through MCP Apps. Three read-only tools return tabbed widgets using the app's Chakra theme, shared choice cards, and existing strategic scorecards:
24
+
25
+ - `launchchair_show_choices` (`projectId`, `phaseKey`, `bucketKey`): generated options and the saved choice.
26
+ - `launchchair_show_phase_synthesis` (`projectId`, `phaseKey`): the app's phase-specific synthesis cards, native tabs and charts, with verified completion state.
27
+ - `launchchair_show_validation_scores` (`projectId`): validation scorecards, evidence inputs, and separate direction scores for Product and Feature projects.
28
+
29
+ Every widget includes **Next action** controls. Saving a choice calls the existing `launchchair_select_choice` tool, then rereads the saved selection. Continue/review actions send a message to the host agent; the host executes queued prompts with its tools. The iframe never receives a token or calls the API directly. Existing scopes, entitlements, idea policies, validation gates, and downstream resets still apply.
30
+
31
+ The content pane scrolls at 560px on desktop and 440px on smaller screens using the app's scrollbar styles. The next-action footer stays outside that pane. Selected choices reuse the same choice cards, and validation thresholds live inside the scorecard footer. Synthesis reads the sanitized spec through the existing export permission; competitor editing and workspace notes remain in the authenticated app.
32
+
33
+ Rendering uses `_meta.ui.resourceUri`, `text/html;profile=mcp-app` resources, and the MCP Apps JSON-RPC host bridge. Compatible ChatGPT hosts can also use the feature-detected `window.openai` aliases. Text-only clients receive the full structured result and URLs. Static templates can be discovered while logged out; project reads still require authentication.
34
+
35
+ From the repository root, run `npm run agent:widgets:build` after changing widget components or the theme. This rebuilds the self-contained HTML in both bridge distributions. `npm run agent:widgets:preview -- /absolute/path/examples.html` builds interactive local examples with labeled sample data. Nothing is published by these commands.
36
+
37
+ The choice widget requires the bucket API's additive `choice` fields. Synthesis requests the existing export endpoint with `?format=json`; default Markdown downloads are unchanged. Test against a matching local API before releasing. Release the matching API changes before publishing the bridge package so installed clients can read the new choice and synthesis payloads.
25
38
 
26
39
  ## Login
27
40
 
@@ -49,7 +62,7 @@ Tokens are masked in CLI output. Do not paste real tokens into public docs, scre
49
62
 
50
63
  ## Guided Setup
51
64
 
52
- Use `setup` as the easiest path. It starts browser login when no token is saved yet, writes the global MCP config for your agent, creates isolated per-project workspaces, and then proves the server can complete MCP `initialize` and `tools/list` before reporting success.
65
+ Use `setup` as the easiest path. It starts browser login when no token is saved yet, writes the global MCP config for your agent, creates isolated per-project workspaces, and then verifies MCP discovery and an authenticated status call before reporting authenticated success. Unauthenticated discovery is reported separately as login required.
53
66
 
54
67
  Codex Desktop:
55
68
 
@@ -118,6 +131,29 @@ launchchair mcp-config claude
118
131
  launchchair install-agent-md --cwd /absolute/path/to/repo
119
132
  ```
120
133
 
134
+ ## Use any MCP tool from the terminal
135
+
136
+ `tools` discovers the same catalog as your MCP connection (47 tools when authenticated). `call` uses the bundled MCP implementation, including its login, permission checks, private credential storage, and host-execution defaults. Tool errors return a nonzero exit status and preserve details such as an upgrade URL.
137
+
138
+ ```bash
139
+ launchchair tools --json
140
+ launchchair call status
141
+ launchchair call next_step --json '{"projectId":"PROJECT_UUID"}'
142
+ launchchair call complete_run --input completed-run.json
143
+ launchchair call list_tokens
144
+ launchchair call create_token --json '{"name":"Build agent","scopes":["spec.read"]}'
145
+ ```
146
+
147
+ Tool names work with or without the `launchchair_` prefix. Use `--input -` to read JSON arguments from stdin. `next_step` returns a claimed prompt for your current agent to execute with its connected plugins; it does not start another agent. `complete_run` submits the actual result and execution evidence. Token creation returns a private credential-file path, never the secret in terminal output.
148
+
149
+ Project creation preserves a supplied idea by default, even when validation is low. Only request exploration and a score gate when that matches the user's intent:
150
+
151
+ ```bash
152
+ launchchair create --name "Groomer candidate" --one-liner "Reduce missed appointments" --idea-intent explore --validation-gate very_high --save-project
153
+ ```
154
+
155
+ The gate requires pain >=85, opportunity >=85, and saturation <=30 before build/setup. For custom thresholds or the full Feature input schema, use `launchchair call create_project` or `launchchair call create_feature_project` with JSON arguments.
156
+
121
157
  Feature projects can be created and planned from the CLI for every authenticated LaunchChair workspace:
122
158
 
123
159
  ```bash
@@ -158,7 +194,7 @@ launchchair mcp-config claude
158
194
 
159
195
  The recommended config launches the MCP server through the exact Node executable and CLI file path generated by `launchchair setup` or `launchchair mcp-config`. It also pins `LAUNCHCHAIR_CONFIG_DIR` to the exact directory used by `launchchair login`, so Codex and Claude users do not need to paste tokens into MCP config after browser login—even when the desktop app starts with a different home or shell environment. The server uses standard newline-delimited JSON-RPC over stdio, as required by MCP clients.
160
196
 
161
- Use `LAUNCHCHAIR_AGENT_TOKEN` only as a manual fallback when you skip `launchchair login`, need a per-client token override, or your desktop app cannot read the saved CLI config. If you use a desktop custom MCP form, run `launchchair mcp-config codex --ui-fields` or `launchchair mcp-config claude --ui-fields` and paste the printed command, each argument, env vars, and working directory exactly as shown.
197
+ Use `LAUNCHCHAIR_AGENT_TOKEN` as a manual fallback when you skip `launchchair login` or your desktop app cannot read the saved config. A saved browser login takes precedence over stale environment credentials. Logout disables both CLI and MCP authentication until another login. For a separate per-client account, use a separate `LAUNCHCHAIR_CONFIG_DIR`. If you use a desktop custom MCP form, run `launchchair mcp-config codex --ui-fields` or `launchchair mcp-config claude --ui-fields` and paste the printed command, each argument, env vars, and working directory exactly as shown.
162
198
 
163
199
  ## Local Bridge
164
200
 
@@ -207,3 +243,15 @@ This package includes `AGENTS.md`. Fresh Codex, Claude, and Hermes chats should
207
243
  - MCP docs: https://www.launchchair.io/docs/mcp
208
244
  - Agent API docs: https://www.launchchair.io/docs/agent-api
209
245
  - Machine-readable catalog: https://www.launchchair.io/api/agent/v1/docs
246
+
247
+ ## MCP login and host execution
248
+
249
+ The MCP can start without a token. Call `launchchair_login`, show the returned LaunchChair approval URL, and call `launchchair_login_status` after approval. Respect its poll interval. Tokens are stored privately in the local config and never returned to the conversation. Existing tokens need a fresh owner-approved login for `tokens.manage`. Use `launchchair_create_token` to save a scoped credential file, `launchchair_list_tokens` for redacted metadata, and `launchchair_revoke_token`/`launchchair_logout` for cleanup.
250
+
251
+ Prefer `launchchair_next_step`, execute the claimed prompt in the current agent with its already-connected plugins, call `launchchair_complete_run`, and repeat. Do not spawn another model or local CLI unless explicitly requested (`executionMode=local_runner`). A waiting run is already claimed; poll it instead of executing it twice. Inspect forms/checklists with `launchchair_get_bucket`, save allowed patches with `launchchair_update_bucket`, and supply factual `executionEvidence` for setup and verification. Use `launchchair_get_starter` after validation to read the starter files and the host's GitHub/Vercel/Supabase tools to create and deploy the user's app. Bind the verified clone with `launchchair_bind_repository`. Use `stopPhaseKey=launch_sales` for the complete Product workflow.
252
+
253
+ Preserve a supplied idea and specified requirements, even with low validation scores. `ideaIntent=preserve` is the default. Report risks and continue the requested work when no score gate was requested; never pivot merely to improve a score. Choose a generated direction consistent with the supplied idea. Only use `ideaIntent=explore` for explicit discovery or pivot requests.
254
+
255
+ For “do not build until VERY high validation,” create candidates with `validationGate=very_high` (pain >=85, opportunity >=85, saturation <=30), validate through `market_validation`, and compare with `launchchair_compare_projects`. The gate is enforced on LaunchChair build/setup actions and cannot be weakened through generic mutations. Do not provision external resources until it passes. If no candidate qualifies, report the unmet thresholds honestly. Scores are research estimates, not a guarantee of market demand. Do not fabricate evidence or replace validation scores with direction-option scores.
256
+
257
+ Research sources, competitor notes, Pexels assets, logo upload, form/checklist data, prompts, build cards, run management, and markdown exports are available as MCP tools. Tool descriptions distinguish data writes from actual external execution. Existing user authorization carries forward; ask only at a genuine missing authorization or information boundary.
@@ -20,7 +20,7 @@ import {
20
20
  unbindProjectWorkspace,
21
21
  } from "./project-workspace.mjs";
22
22
 
23
- const VERSION = "0.4.12";
23
+ const VERSION = "0.5.1";
24
24
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
25
25
  const DEFAULT_BASE_URL = "https://www.launchchair.io";
26
26
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -109,10 +109,16 @@ function configValue(key, envNames = [], fallback = "") {
109
109
  }
110
110
 
111
111
  function token() {
112
+ const config = loadConfig();
113
+ if (config.mcpLoggedOut === true) return "";
114
+ if (config.mcpPreferSavedToken === true && config.agentToken) return config.agentToken;
112
115
  return configValue("agentToken", ["LAUNCHCHAIR_AGENT_TOKEN", "LAUNCHCHAIR_API_KEY"]);
113
116
  }
114
117
 
115
118
  function tokenSource() {
119
+ const config = loadConfig();
120
+ if (config.mcpLoggedOut === true) return "logged out";
121
+ if (config.mcpPreferSavedToken === true && config.agentToken) return CONFIG_PATH;
116
122
  if (env("LAUNCHCHAIR_AGENT_TOKEN")) return "LAUNCHCHAIR_AGENT_TOKEN";
117
123
  if (env("LAUNCHCHAIR_API_KEY")) return "LAUNCHCHAIR_API_KEY";
118
124
  if (loadConfig().agentToken) return CONFIG_PATH;
@@ -171,6 +177,7 @@ Use MCP-only when you want agent tools inside Codex/Claude/Hermes.
171
177
  Use CLI + MCP when you also want quick terminal commands, diagnostics, and bridge control.
172
178
 
173
179
  Commands:
180
+ --version Show the installed package version
174
181
  doctor Check token, docs, API reachability, and local bridge files
175
182
  login [--token TOKEN] Open browser login and save a LaunchChair Agent API token locally
176
183
  logout Remove the saved local token
@@ -183,6 +190,9 @@ Commands:
183
190
  run-once Claim and run one queued LaunchChair job through the local bridge
184
191
  bridge Start the long-running local bridge for Run with Agent
185
192
  mcp-server Run the stdio MCP server using saved CLI config
193
+ tools [--json] List the available MCP tools and their input schemas
194
+ call TOOL [--json JSON|--input FILE]
195
+ Call any MCP tool; JSON output, nonzero exit on tool errors
186
196
  setup codex|claude|claude-desktop|claude-code|terminal [options]
187
197
  Guided global CLI/MCP setup with isolated project workspaces
188
198
  bind --project-id ID --cwd PATH Bind one LaunchChair project to a verified local Git clone
@@ -202,6 +212,8 @@ Create options:
202
212
  --start-loop
203
213
  --save-project Save the created project id as the CLI default
204
214
  --idempotency-key KEY
215
+ --idea-intent preserve|explore Default: preserve the supplied idea, even with low scores
216
+ --validation-gate very_high Require pain >=85, opportunity >=85, saturation <=30 before build
205
217
 
206
218
  Feature create options:
207
219
  --project-kind feature
@@ -245,7 +257,7 @@ Login options:
245
257
  --timeout SECONDS Default: 600
246
258
 
247
259
  Env:
248
- LAUNCHCHAIR_AGENT_TOKEN Optional full lc_at token override
260
+ LAUNCHCHAIR_AGENT_TOKEN Optional token fallback when no saved login takes precedence
249
261
  LAUNCHCHAIR_BASE_URL Default: https://www.launchchair.io
250
262
  LAUNCHCHAIR_PROJECT_ID Optional default project id
251
263
  LC_AGENT_CODEX_CWD Legacy candidate only; verified per project before use
@@ -255,6 +267,11 @@ MCP config options:
255
267
  --ui-fields Print fields for Codex Desktop's custom MCP form
256
268
  --copy-config Copy generated output to clipboard on macOS
257
269
 
270
+ Tool call options:
271
+ --json JSON Tool arguments as a JSON object; defaults to {}
272
+ --input FILE Read tool arguments from a JSON file; use - for stdin
273
+ --timeout SECONDS Request timeout. Default: 60, max: 3600
274
+
258
275
  Config:
259
276
  ${CONFIG_PATH}
260
277
  `);
@@ -308,7 +325,13 @@ async function api(pathname, options = {}) {
308
325
  if (!response.ok) {
309
326
  const retryAfter = response.headers.get("retry-after");
310
327
  const suffix = retryAfter ? ` Retry after ${retryAfter}s.` : "";
311
- throw new Error(`${body?.error ?? response.statusText}${suffix}`);
328
+ const next = body?.upgradeUrl
329
+ ? ` Upgrade: ${body.upgradeUrl}`
330
+ : response.status === 401 ? " Run launchchair login to reconnect." : "";
331
+ const error = new Error(`${body?.error ?? response.statusText}${suffix}${next}`);
332
+ error.status = response.status;
333
+ error.retryAfterSeconds = Number(retryAfter) || 10;
334
+ throw error;
312
335
  }
313
336
  return body;
314
337
  }
@@ -331,7 +354,10 @@ async function publicApi(pathname, options = {}) {
331
354
  if (!response.ok) {
332
355
  const retryAfter = response.headers.get("retry-after");
333
356
  const suffix = retryAfter ? ` Retry after ${retryAfter}s.` : "";
334
- throw new Error(`${body?.error ?? response.statusText}${suffix}`);
357
+ const error = new Error(`${body?.error ?? response.statusText}${suffix}`);
358
+ error.status = response.status;
359
+ error.retryAfterSeconds = Number(retryAfter) || 10;
360
+ throw error;
335
361
  }
336
362
  return body;
337
363
  }
@@ -391,6 +417,8 @@ async function login(args) {
391
417
  }
392
418
  saveConfig({
393
419
  agentToken: incomingToken,
420
+ mcpLoggedOut: false,
421
+ mcpPreferSavedToken: true,
394
422
  baseUrl: incomingBaseUrl ?? baseUrl(),
395
423
  projectId: incomingProjectId || defaultProjectId() || undefined,
396
424
  });
@@ -434,7 +462,7 @@ async function login(args) {
434
462
 
435
463
  const timeoutSeconds =
436
464
  typeof args.timeout === "string" && Number.isFinite(Number(args.timeout)) ? Math.max(10, Number(args.timeout)) : Number(start.expiresIn ?? 600);
437
- const intervalSeconds = Math.max(2, Number(start.interval ?? 2));
465
+ let intervalSeconds = Math.max(5, Number(start.interval ?? 5));
438
466
  const startedAt = Date.now();
439
467
  print("");
440
468
  print("Waiting for approval. Leave this terminal open.");
@@ -449,10 +477,12 @@ async function login(args) {
449
477
  });
450
478
  } catch (error) {
451
479
  const message = error instanceof Error ? error.message : String(error);
480
+ if (error.status === 429) { intervalSeconds = Math.max(5, error.retryAfterSeconds); continue; }
452
481
  if (/pending/i.test(message)) continue;
453
482
  throw error;
454
483
  }
455
484
  if (poll?.status === "pending") {
485
+ intervalSeconds = Math.max(5, Number(poll.interval) || intervalSeconds);
456
486
  write(paint(".", "dim"));
457
487
  continue;
458
488
  }
@@ -460,6 +490,8 @@ async function login(args) {
460
490
  print("");
461
491
  saveConfig({
462
492
  agentToken: poll.token,
493
+ mcpLoggedOut: false,
494
+ mcpPreferSavedToken: true,
463
495
  baseUrl: baseUrl(),
464
496
  projectId: incomingProjectId || defaultProjectId() || undefined,
465
497
  });
@@ -478,11 +510,12 @@ async function login(args) {
478
510
  }
479
511
 
480
512
  function logout() {
481
- deleteConfigKeys(["agentToken"]);
513
+ deleteConfigKeys(["agentToken", "pendingMcpLogin"]);
514
+ saveConfig({ mcpLoggedOut: true });
482
515
  title("CLI logout");
483
516
  ok(`Removed saved token from ${CONFIG_PATH}`);
484
517
  if (env("LAUNCHCHAIR_AGENT_TOKEN") || env("LAUNCHCHAIR_API_KEY")) {
485
- warn("An environment token is still set, so CLI commands may remain authenticated in this shell.");
518
+ warn("An environment token is still set; it remains ignored until you log in again.");
486
519
  }
487
520
  }
488
521
 
@@ -606,6 +639,14 @@ async function createProject(args) {
606
639
  throw new Error("--project-kind must be product or feature.");
607
640
  }
608
641
  const featureProject = projectKind === "feature";
642
+ const ideaIntent = args["idea-intent"] ?? "preserve";
643
+ if (!["preserve", "explore"].includes(ideaIntent)) {
644
+ throw new Error("--idea-intent must be preserve or explore.");
645
+ }
646
+ const validationGate = args["validation-gate"];
647
+ if (validationGate !== undefined && validationGate !== "very_high") {
648
+ throw new Error("--validation-gate must be very_high. For custom thresholds, use launchchair call create_project --input FILE.");
649
+ }
609
650
  const featureDescription = String(args["feature-description"] ?? args["one-liner"] ?? "").trim();
610
651
  const featureProblem = String(args.problem ?? "").trim();
611
652
  if (featureProject && (!featureDescription || !featureProblem)) {
@@ -664,7 +705,7 @@ async function createProject(args) {
664
705
  const body = await api("/api/agent/v1/projects", {
665
706
  method: "POST",
666
707
  headers,
667
- body: JSON.stringify(requestBody),
708
+ body: JSON.stringify({ ...requestBody, ideaIntent, validationGate }),
668
709
  });
669
710
  const projectId = body?.project?.id ?? body?.projectId ?? "";
670
711
  title("Project created");
@@ -876,6 +917,100 @@ function spawnMcpServer(extraArgs = []) {
876
917
  });
877
918
  }
878
919
 
920
+ // Use the bundled MCP so terminal calls share its login, scope checks, and host workflow.
921
+ async function requestMcp(method, params, timeoutMs = 60_000) {
922
+ const child = spawn(process.execPath, [path.join(SCRIPT_DIR, "mcp-bridge-server.mjs")], {
923
+ env: buildBridgeEnv(),
924
+ stdio: ["pipe", "pipe", "pipe"],
925
+ });
926
+ let buffer = "";
927
+ let serial = 0;
928
+ const pending = new Map();
929
+ const failPending = (error) => {
930
+ for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(error); }
931
+ pending.clear();
932
+ };
933
+ child.on("error", failPending);
934
+ child.on("exit", (code) => failPending(new Error(`MCP server exited before replying (code ${code ?? "unknown"}).`)));
935
+ child.stdin.on("error", failPending);
936
+ child.stderr.on("data", (chunk) => process.stderr.write(chunk));
937
+ child.stdout.setEncoding("utf8");
938
+ child.stdout.on("data", (chunk) => {
939
+ buffer += chunk;
940
+ let newline;
941
+ while ((newline = buffer.indexOf("\n")) !== -1) {
942
+ const line = buffer.slice(0, newline).trim();
943
+ buffer = buffer.slice(newline + 1);
944
+ if (!line) continue;
945
+ let message;
946
+ try { message = JSON.parse(line); }
947
+ catch { failPending(new Error("MCP server returned invalid JSON.")); return; }
948
+ const entry = pending.get(message.id);
949
+ if (!entry) continue;
950
+ pending.delete(message.id);
951
+ clearTimeout(entry.timer);
952
+ if (message.error) entry.reject(new Error(message.error.message || "MCP request failed."));
953
+ else entry.resolve(message.result);
954
+ }
955
+ });
956
+ const rpc = (rpcMethod, rpcParams) => new Promise((resolve, reject) => {
957
+ const id = ++serial;
958
+ const timer = setTimeout(() => {
959
+ pending.delete(id);
960
+ reject(new Error("MCP request timed out. Check the project's run status before retrying a write."));
961
+ }, timeoutMs);
962
+ pending.set(id, { resolve, reject, timer });
963
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method: rpcMethod, params: rpcParams })}\n`);
964
+ });
965
+ try {
966
+ await rpc("initialize", { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "launchchair-cli", version: VERSION } });
967
+ child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })}\n`);
968
+ return await rpc(method, params);
969
+ } finally {
970
+ child.stdin.end();
971
+ child.kill();
972
+ failPending(new Error("MCP client closed."));
973
+ }
974
+ }
975
+
976
+ async function listTools(args) {
977
+ const result = await requestMcp("tools/list", {});
978
+ if (args.json === true) return print(JSON.stringify(result, null, 2));
979
+ title("MCP tools");
980
+ for (const tool of result.tools ?? []) print(`${tool.name}\n ${tool.description}`);
981
+ print("\nUse launchchair tools --json for input schemas, then launchchair call TOOL --json '{...}'.");
982
+ }
983
+
984
+ async function callTool(args) {
985
+ const requestedName = args._[0];
986
+ if (!requestedName) throw new Error("call requires a tool name. Run launchchair tools to list them.");
987
+ if (args.json !== undefined && args.input !== undefined) throw new Error("Use either --json or --input, not both.");
988
+ let input = "{}";
989
+ if (args.json !== undefined) {
990
+ if (typeof args.json !== "string") throw new Error("--json requires a JSON object.");
991
+ input = args.json;
992
+ } else if (args.input !== undefined) {
993
+ if (typeof args.input !== "string") throw new Error("--input requires a file path or - for stdin.");
994
+ input = readFileSync(args.input === "-" ? 0 : path.resolve(args.input), "utf8");
995
+ }
996
+ let toolArgs;
997
+ try { toolArgs = JSON.parse(input); }
998
+ catch { throw new Error("Tool arguments must be valid JSON."); }
999
+ if (!toolArgs || typeof toolArgs !== "object" || Array.isArray(toolArgs)) throw new Error("Tool arguments must be a JSON object.");
1000
+ const timeoutSeconds = args.timeout === undefined ? 60 : Number(args.timeout);
1001
+ if (typeof args.timeout === "boolean" || !Number.isFinite(timeoutSeconds) || timeoutSeconds < 1 || timeoutSeconds > 3600) {
1002
+ throw new Error("--timeout must be between 1 and 3600 seconds.");
1003
+ }
1004
+ const result = await requestMcp("tools/call", {
1005
+ name: requestedName.startsWith("launchchair_") ? requestedName : `launchchair_${requestedName}`,
1006
+ arguments: toolArgs,
1007
+ }, timeoutSeconds * 1000);
1008
+ // LaunchChair tools return one JSON text block. Keep other MCP content intact.
1009
+ print(result.content?.length === 1 && result.content[0].type === "text"
1010
+ ? result.content[0].text : JSON.stringify(result, null, 2));
1011
+ if (result.isError) process.exitCode = 1;
1012
+ }
1013
+
879
1014
  function normalizedMcpTarget(target) {
880
1015
  return targetDetails(target || "codex").mcpTarget || "codex";
881
1016
  }
@@ -885,13 +1020,11 @@ function tomlString(value) {
885
1020
  }
886
1021
 
887
1022
  function mcpEnvForTarget(target, options = {}) {
888
- const includeTokenPlaceholder = options.includeTokenPlaceholder !== false;
889
1023
  const details = targetDetails(target || "codex");
890
1024
  const projectId = defaultProjectId();
891
1025
  const envBlock = {
892
1026
  LAUNCHCHAIR_BASE_URL: baseUrl(),
893
1027
  LAUNCHCHAIR_CONFIG_DIR: CONFIG_DIR,
894
- ...(token() || !includeTokenPlaceholder ? {} : { LAUNCHCHAIR_AGENT_TOKEN: "<optional full lc_at token when launchchair login was not used>" }),
895
1028
  ...(projectId ? { LAUNCHCHAIR_PROJECT_ID: projectId } : {}),
896
1029
  LC_AGENT_ROUTER_GPT_TARGET: details.routerTarget,
897
1030
  LC_AGENT_ROUTER_CODEX_TARGET: details.routerTarget,
@@ -916,6 +1049,7 @@ function verifyMcpServer(target, timeoutMs = 20_000) {
916
1049
  stdio: ["pipe", "pipe", "pipe"],
917
1050
  });
918
1051
  let settled = false;
1052
+ let discoveredTools = [];
919
1053
  let stdout = "";
920
1054
  let stderr = "";
921
1055
 
@@ -966,13 +1100,29 @@ function verifyMcpServer(target, timeoutMs = 20_000) {
966
1100
  const toolNames = Array.isArray(response?.result?.tools)
967
1101
  ? response.result.tools.map((tool) => String(tool?.name || "")).filter(Boolean)
968
1102
  : [];
969
- const requiredTools = ["launchchair_status", "launchchair_create_project", "launchchair_list_projects"];
1103
+ const requiredTools = [
1104
+ "launchchair_status", "launchchair_create_project", "launchchair_list_projects",
1105
+ "launchchair_login", "launchchair_login_status", "launchchair_create_token",
1106
+ "launchchair_next_step", "launchchair_complete_run", "launchchair_get_validation_gate",
1107
+ "launchchair_update_bucket", "launchchair_get_starter", "launchchair_export_mvp_spec",
1108
+ ];
970
1109
  const missingTools = requiredTools.filter((name) => !toolNames.includes(name));
971
1110
  if (missingTools.length) {
972
1111
  finish(new Error(`MCP verification did not discover required tools: ${missingTools.join(", ")}`));
973
1112
  return;
974
1113
  }
975
- finish(null, toolNames);
1114
+ discoveredTools = toolNames;
1115
+ if (!token()) { finish(null, toolNames); continue; }
1116
+ send({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "launchchair_status", arguments: {} } });
1117
+ }
1118
+ if (response?.id === 3) {
1119
+ let status = {};
1120
+ try { status = JSON.parse(response.result?.content?.[0]?.text || "{}"); } catch {}
1121
+ if (response.error || response.result?.isError || status.authenticated !== true) {
1122
+ finish(new Error(`MCP authentication verification failed: ${status.error || response.error?.message || "not authenticated"}`));
1123
+ return;
1124
+ }
1125
+ finish(null, discoveredTools);
976
1126
  }
977
1127
  }
978
1128
  });
@@ -1286,7 +1436,10 @@ function installLaunchChairSkill(args) {
1286
1436
  "# LaunchChair workflow",
1287
1437
  "",
1288
1438
  "Use the installed LaunchChair MCP tools before web search or local repo discovery for LaunchChair project work.",
1289
- "Call launchchair_status if capability context is unclear.",
1439
+ "Call launchchair_status if capability context is unclear. If login is required, use launchchair_login and launchchair_login_status; never print token secrets.",
1440
+ "Prefer launchchair_next_step and execute its claimed prompt in this host agent using existing plugins, then complete_run. Use local runners only when explicitly requested.",
1441
+ "Preserve supplied ideas and requirements even when validation is low. ideaIntent=preserve is the default; exploration and pivots require an explicit user request.",
1442
+ "Only impose a validation gate when requested. VERY high means validationGate=very_high (pain>=85, opportunity>=85, saturation<=30). Never fabricate evidence or weaken the gate.",
1290
1443
  "For a new product, use launchchair_create_project.",
1291
1444
  "For a feature in an existing product, use launchchair_create_feature_project. Never substitute a Product project if that tool is unavailable; report the setup or authentication issue instead.",
1292
1445
  "Use planToBuildBoard=true for planning through the Feature build board.",
@@ -1326,6 +1479,8 @@ async function setup(args) {
1326
1479
  }
1327
1480
  saveConfig({
1328
1481
  agentToken: incomingToken,
1482
+ mcpLoggedOut: false,
1483
+ mcpPreferSavedToken: true,
1329
1484
  baseUrl: incomingBaseUrl ?? baseUrl(),
1330
1485
  projectId: incomingProjectId || defaultProjectId() || undefined,
1331
1486
  });
@@ -1412,11 +1567,11 @@ async function setup(args) {
1412
1567
  }
1413
1568
  }
1414
1569
 
1415
- if (!details.terminalOnly && token() && args["skip-mcp-check"] !== true) {
1570
+ if (!details.terminalOnly && args["skip-mcp-check"] !== true) {
1416
1571
  const toolNames = await verifyMcpServer(details.mcpTarget);
1417
1572
  print("");
1418
- title("MCP verification passed");
1419
- ok(`Initialize and tools/list succeeded; discovered ${toolNames.length} LaunchChair tools.`);
1573
+ title(token() ? "MCP verification passed" : "MCP discovery verified — login required");
1574
+ ok(`Discovered ${toolNames.length} LaunchChair tools.${token() ? " Authenticated status verified." : " Call launchchair_login in your agent."}`);
1420
1575
  } else if (!details.terminalOnly && !token()) {
1421
1576
  warn("MCP verification was skipped because no LaunchChair token is configured.");
1422
1577
  } else if (!details.terminalOnly && args["skip-mcp-check"] === true) {
@@ -1455,7 +1610,8 @@ async function setup(args) {
1455
1610
  print(`2. Restart or reload ${details.label} so the MCP server is exposed to fresh chats.`);
1456
1611
  }
1457
1612
  print("Use the generated command and argument fields exactly as printed; setup resolves the local Node and CLI paths for this machine.");
1458
- print("Keep this bridge running when you want LaunchChair's Run with agent button to work:");
1613
+ print("Your agent can use launchchair_next_step and launchchair_complete_run with its connected plugins.");
1614
+ print("For unattended local-runner jobs queued from LaunchChair's Run with agent button, optionally run:");
1459
1615
  print(` ${paint("launchchair bridge", "cyan")}`);
1460
1616
  print("Start a fresh agent chat. The agent should discover the LaunchChair MCP tools automatically.");
1461
1617
  }
@@ -1479,6 +1635,7 @@ async function main() {
1479
1635
  const [command = "help", ...rest] = process.argv.slice(2);
1480
1636
  const args = parseArgs(rest);
1481
1637
  if (command === "help" || command === "--help" || command === "-h") return help();
1638
+ if (command === "--version" || command === "-v" || command === "version") return print(VERSION);
1482
1639
  if (command === "doctor") return doctor();
1483
1640
  if (command === "login") return login(args);
1484
1641
  if (command === "logout") return logout();
@@ -1490,6 +1647,8 @@ async function main() {
1490
1647
  if (command === "run-once") return spawnBridge(["--once", "--verbose"]);
1491
1648
  if (command === "bridge") return spawnBridge(["--verbose"]);
1492
1649
  if (command === "mcp-server") return spawnMcpServer();
1650
+ if (command === "tools") return listTools(args);
1651
+ if (command === "call") return callTool(args);
1493
1652
  if (command === "setup") return setup(args);
1494
1653
  if (command === "bind") return bindProject(args);
1495
1654
  if (command === "unbind") return unbindProject(args);
package/local-bridge.mjs CHANGED
@@ -18,7 +18,7 @@ import { fileURLToPath } from "node:url";
18
18
  import { resolveProjectWorkspace } from "./project-workspace.mjs";
19
19
  import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
20
20
 
21
- const VERSION = "0.4.12";
21
+ const VERSION = "0.5.1";
22
22
  const CLIENT_TYPES = ["codex", "chatgpt", "claude", "claude_code", "other"];
23
23
  const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
24
24
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -402,6 +402,7 @@ async function executeRunner(run) {
402
402
  stdio: ["pipe", "pipe", "pipe"],
403
403
  env: {
404
404
  ...process.env,
405
+ ...(resolved.workspace.source === "isolated" && run?.runnerWorkspace?.requiresRepository !== true ? { LC_AGENT_CODEX_SKIP_GIT_CHECK: "1" } : {}),
405
406
  LC_AGENT_RUN_ID: String(run.id ?? ""),
406
407
  LC_AGENT_PROJECT_ID: String(run.projectId ?? ""),
407
408
  LC_AGENT_PHASE_KEY: String(run.phaseKey ?? ""),