@launchchair/agent 0.4.6 → 0.4.8

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
@@ -64,8 +64,8 @@ The npm CLI is the recommended setup path for Codex Desktop and Claude Desktop.
64
64
  ```bash
65
65
  npm install -g @launchchair/agent
66
66
  launchchair login
67
- launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder --write-config
68
- launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder --write-config
67
+ launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder
68
+ launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
69
69
  launchchair projects --query LeafLore
70
70
  launchchair create --name SignalLift --one-liner "Distribution workspace for technical founders" --project-type b2b --idempotency-key signallift-v1
71
71
  launchchair run-once
@@ -87,7 +87,7 @@ LAUNCHCHAIR_PROJECT_ID="optional project UUID; omit or leave blank if creating o
87
87
  ```
88
88
 
89
89
  `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.
90
- 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.
90
+ 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.
91
91
 
92
92
  For Codex Desktop local runs:
93
93
 
@@ -114,12 +114,13 @@ The build folder can be an empty scratch folder until Stack Setup creates or rec
114
114
  command = "/absolute/path/to/node"
115
115
  args = ["/absolute/path/to/launchchair-cli.mjs", "mcp-server"]
116
116
  required = false
117
- startup_timeout_sec = 20
117
+ startup_timeout_sec = 45
118
118
  tool_timeout_sec = 3600
119
119
  default_tools_approval_mode = "approve"
120
120
 
121
121
  [mcp_servers.launchchair.env]
122
122
  LAUNCHCHAIR_BASE_URL = "https://www.launchchair.io"
123
+ LAUNCHCHAIR_CONFIG_DIR = "/absolute/path/to/.launchchair"
123
124
  LAUNCHCHAIR_PROJECT_ID = ""
124
125
  LC_AGENT_ROUTER_GPT_TARGET = "codex"
125
126
  LC_AGENT_ROUTER_CODEX_TARGET = "codex"
@@ -129,7 +130,7 @@ LC_AGENT_CODEX_CWD = "/absolute/path/to/the-folder-codex-should-build-into"
129
130
  # LAUNCHCHAIR_AGENT_TOKEN = "<full lc_at token from Settings API Access>"
130
131
  ```
131
132
 
132
- Prefer `launchchair setup codex --cwd /absolute/path/to/build-folder --write-config` or `launchchair mcp-config codex --ui-fields` so the CLI prints exact local command and argument paths for this machine.
133
+ Prefer `launchchair setup codex --cwd /absolute/path/to/build-folder` so the CLI writes native config and verifies MCP tool discovery. Use `launchchair mcp-config codex --ui-fields` only for manual custom-form setup.
133
134
 
134
135
  ## What To Do First In A Fresh Chat
135
136
 
package/README.md CHANGED
@@ -49,12 +49,12 @@ Tokens are masked in CLI output. Do not paste real tokens into public docs, scre
49
49
 
50
50
  ## Guided Setup
51
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, writes or prints the MCP config for your desktop agent, and tells you exactly what to do next.
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, writes the MCP config for your agent, and then proves the server can complete MCP `initialize` and `tools/list` before reporting success.
53
53
 
54
54
  Codex Desktop:
55
55
 
56
56
  ```bash
57
- launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder --write-config
57
+ launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder
58
58
  ```
59
59
 
60
60
  If you prefer Codex Desktop's custom MCP form, copy paste-ready fields instead:
@@ -63,10 +63,17 @@ If you prefer Codex Desktop's custom MCP form, copy paste-ready fields instead:
63
63
  launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder --copy-config
64
64
  ```
65
65
 
66
- Claude Desktop + Claude Code:
66
+ Claude Desktop + Claude Code (Claude Code is registered at user scope when its CLI is installed):
67
67
 
68
68
  ```bash
69
- launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder --write-config
69
+ launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
70
+ ```
71
+
72
+ Configure only one Claude client when needed:
73
+
74
+ ```bash
75
+ launchchair setup claude-desktop --cwd /absolute/path/to/repo-or-empty-folder
76
+ launchchair setup claude-code --cwd /absolute/path/to/repo-or-empty-folder
70
77
  ```
71
78
 
72
79
  Terminal / CLI agents:
@@ -82,6 +89,8 @@ launchchair setup codex --cwd . --skip-login
82
89
  launchchair setup codex --cwd . --token "<full lc_at token from LaunchChair Settings API Access>"
83
90
  ```
84
91
 
92
+ Setup writes client configuration by default. Use `--copy-config`, `--ui-fields`, or `--print-config` only when you intentionally want manual configuration. Authenticated setup exits nonzero if the MCP handshake or required tool discovery fails; `--skip-mcp-check` is an explicit diagnostic escape hatch, not the recommended first-run path.
93
+
85
94
  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.
86
95
 
87
96
  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`.
@@ -89,8 +98,9 @@ Use `setup terminal` when the agent runs inside a normal shell instead of Codex
89
98
  ## Common Commands
90
99
 
91
100
  ```bash
92
- launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder --write-config
93
- launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder --write-config
101
+ launchchair setup codex --cwd /absolute/path/to/repo-or-empty-folder
102
+ launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
103
+ launchchair setup claude-code --cwd /absolute/path/to/repo-or-empty-folder
94
104
  launchchair setup terminal --cwd /absolute/path/to/repo-or-empty-folder
95
105
  launchchair projects
96
106
  launchchair projects --query LeafLore
@@ -145,7 +155,7 @@ Print a Claude Desktop MCP config example:
145
155
  launchchair mcp-config claude
146
156
  ```
147
157
 
148
- The recommended config launches the MCP server through the exact Node executable and CLI file path generated by `launchchair setup` or `launchchair mcp-config`. That process reads the token saved by `launchchair login`, so Codex and Claude users do not need to paste tokens into MCP config after browser login.
158
+ 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.
149
159
 
150
160
  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.
151
161
 
@@ -9,13 +9,13 @@
9
9
  */
10
10
 
11
11
  import { spawn, spawnSync } from "node:child_process";
12
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
12
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
13
13
  import os from "node:os";
14
14
  import path from "node:path";
15
15
  import process from "node:process";
16
16
  import { fileURLToPath } from "node:url";
17
17
 
18
- const VERSION = "0.4.6";
18
+ const VERSION = "0.4.8";
19
19
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
20
20
  const DEFAULT_BASE_URL = "https://www.launchchair.io";
21
21
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -40,6 +40,18 @@ function env(name, fallback = "") {
40
40
  return typeof value === "string" && value.trim() ? value.trim() : fallback;
41
41
  }
42
42
 
43
+ function writeFileAtomic(target, contents, options = {}) {
44
+ const directory = path.dirname(target);
45
+ mkdirSync(directory, { recursive: true });
46
+ const temporary = path.join(directory, `.${path.basename(target)}.${process.pid}.${Date.now()}.tmp`);
47
+ try {
48
+ writeFileSync(temporary, contents, options);
49
+ renameSync(temporary, target);
50
+ } finally {
51
+ if (existsSync(temporary)) unlinkSync(temporary);
52
+ }
53
+ }
54
+
43
55
  function paint(value, tone) {
44
56
  if (!COLORS_ENABLED || !color[tone]) return value;
45
57
  return `${color[tone]}${value}${color.reset}`;
@@ -62,7 +74,7 @@ function saveConfig(patch) {
62
74
  ...Object.fromEntries(Object.entries(patch).filter(([, value]) => value !== undefined)),
63
75
  };
64
76
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
65
- writeFileSync(CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
77
+ writeFileAtomic(CONFIG_PATH, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
66
78
  try {
67
79
  chmodSync(CONFIG_PATH, 0o600);
68
80
  } catch {
@@ -77,7 +89,7 @@ function deleteConfigKeys(keys) {
77
89
  delete current[key];
78
90
  });
79
91
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
80
- writeFileSync(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`, { mode: 0o600 });
92
+ writeFileAtomic(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`, { mode: 0o600 });
81
93
  return current;
82
94
  }
83
95
 
@@ -166,7 +178,7 @@ Commands:
166
178
  run-once Claim and run one queued LaunchChair job through the local bridge
167
179
  bridge Start the long-running local bridge for Run with Agent
168
180
  mcp-server Run the stdio MCP server using saved CLI config
169
- setup codex|claude|terminal [options]
181
+ setup codex|claude|claude-desktop|claude-code|terminal [options]
170
182
  Guided setup for CLI, CWD, AGENTS.md, MCP config, and bridge
171
183
  config Show local CLI config without printing the token
172
184
  config set KEY VALUE Save base-url, project-id, codex-cwd, or claude-code-cwd
@@ -213,7 +225,9 @@ Setup options:
213
225
  --start-bridge Start the local bridge after setup
214
226
  --copy-config Copy generated MCP setup output to clipboard on macOS
215
227
  --ui-fields Print Codex custom MCP form fields instead of raw config
216
- --write-config Write/update Codex or Claude Desktop MCP config when supported
228
+ --write-config Explicitly write/update client config (the setup default)
229
+ --print-config Print raw MCP config instead of writing client config
230
+ --skip-mcp-check Write config without verifying MCP initialize + tools/list
217
231
 
218
232
  Login options:
219
233
  --token TOKEN Manual fallback: save an existing lc_at token
@@ -707,13 +721,34 @@ function targetDetails(target) {
707
721
  terminalOnly: true,
708
722
  };
709
723
  }
710
- if (target === "claude" || target === "claude-code" || target === "claude_code") {
724
+ if (target === "claude-code" || target === "claude_code") {
725
+ return {
726
+ label: "Claude Code",
727
+ mcpTarget: "claude",
728
+ cwdConfigKey: "claudeCodeCwd",
729
+ cwdEnvName: "LC_AGENT_CLAUDE_CODE_CWD",
730
+ routerTarget: "claude_code",
731
+ claudeClients: ["code"],
732
+ };
733
+ }
734
+ if (target === "claude-desktop" || target === "claude_desktop") {
735
+ return {
736
+ label: "Claude Desktop",
737
+ mcpTarget: "claude",
738
+ cwdConfigKey: "claudeCodeCwd",
739
+ cwdEnvName: "LC_AGENT_CLAUDE_CODE_CWD",
740
+ routerTarget: "claude_code",
741
+ claudeClients: ["desktop"],
742
+ };
743
+ }
744
+ if (target === "claude") {
711
745
  return {
712
746
  label: "Claude Desktop + Claude Code",
713
747
  mcpTarget: "claude",
714
748
  cwdConfigKey: "claudeCodeCwd",
715
749
  cwdEnvName: "LC_AGENT_CLAUDE_CODE_CWD",
716
750
  routerTarget: "claude_code",
751
+ claudeClients: ["desktop", "code"],
717
752
  };
718
753
  }
719
754
  return {
@@ -729,6 +764,7 @@ function buildBridgeEnv(projectId = defaultProjectId()) {
729
764
  const childEnv = {
730
765
  ...process.env,
731
766
  LAUNCHCHAIR_BASE_URL: baseUrl(),
767
+ LAUNCHCHAIR_CONFIG_DIR: CONFIG_DIR,
732
768
  LAUNCHCHAIR_AGENT_TOKEN: token(),
733
769
  LAUNCHCHAIR_PROJECT_ID: projectId,
734
770
  LC_AGENT_CODEX_CWD: configValue("codexCwd", ["LC_AGENT_CODEX_CWD"], ""),
@@ -848,6 +884,7 @@ function mcpEnvForTarget(target, options = {}) {
848
884
  const cwd = configValue(details.cwdConfigKey, [details.cwdEnvName], "/absolute/path/to/repo-or-empty-folder");
849
885
  const envBlock = {
850
886
  LAUNCHCHAIR_BASE_URL: baseUrl(),
887
+ LAUNCHCHAIR_CONFIG_DIR: CONFIG_DIR,
851
888
  ...(token() || !includeTokenPlaceholder ? {} : { LAUNCHCHAIR_AGENT_TOKEN: "<optional full lc_at token when launchchair login was not used>" }),
852
889
  ...(projectId ? { LAUNCHCHAIR_PROJECT_ID: projectId } : {}),
853
890
  LC_AGENT_ROUTER_GPT_TARGET: details.routerTarget,
@@ -866,6 +903,96 @@ function mcpServerConfig(target, options = {}) {
866
903
  };
867
904
  }
868
905
 
906
+ function verifyMcpServer(target, timeoutMs = 20_000) {
907
+ const server = mcpServerConfig(target, { includeTokenPlaceholder: false });
908
+ return new Promise((resolve, reject) => {
909
+ const child = spawn(server.command, server.args, {
910
+ env: { ...process.env, ...server.env },
911
+ stdio: ["pipe", "pipe", "pipe"],
912
+ });
913
+ let settled = false;
914
+ let stdout = "";
915
+ let stderr = "";
916
+
917
+ const finish = (error, toolNames = []) => {
918
+ if (settled) return;
919
+ settled = true;
920
+ clearTimeout(timeout);
921
+ child.kill();
922
+ if (error) reject(error);
923
+ else resolve(toolNames);
924
+ };
925
+
926
+ const timeout = setTimeout(() => {
927
+ finish(new Error(`MCP verification timed out after ${Math.round(timeoutMs / 1000)} seconds.${stderr.trim() ? ` ${stderr.trim()}` : ""}`));
928
+ }, timeoutMs);
929
+
930
+ const send = (message) => child.stdin.write(`${JSON.stringify(message)}\n`);
931
+ child.stdout.setEncoding("utf8");
932
+ child.stdout.on("data", (chunk) => {
933
+ stdout += chunk;
934
+ while (true) {
935
+ const newlineIndex = stdout.indexOf("\n");
936
+ if (newlineIndex === -1) break;
937
+ const line = stdout.slice(0, newlineIndex).replace(/\r$/, "");
938
+ stdout = stdout.slice(newlineIndex + 1);
939
+ if (!line.trim()) continue;
940
+ let response = null;
941
+ try {
942
+ response = JSON.parse(line);
943
+ } catch {
944
+ finish(new Error("MCP verification received non-JSON output on stdout."));
945
+ return;
946
+ }
947
+ if (response?.id === 1) {
948
+ if (response.error) {
949
+ finish(new Error(`MCP initialize failed: ${response.error.message || "unknown error"}`));
950
+ return;
951
+ }
952
+ send({ jsonrpc: "2.0", method: "notifications/initialized", params: {} });
953
+ send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
954
+ continue;
955
+ }
956
+ if (response?.id === 2) {
957
+ if (response.error) {
958
+ finish(new Error(`MCP tools/list failed: ${response.error.message || "unknown error"}`));
959
+ return;
960
+ }
961
+ const toolNames = Array.isArray(response?.result?.tools)
962
+ ? response.result.tools.map((tool) => String(tool?.name || "")).filter(Boolean)
963
+ : [];
964
+ const requiredTools = ["launchchair_status", "launchchair_create_project", "launchchair_list_projects"];
965
+ const missingTools = requiredTools.filter((name) => !toolNames.includes(name));
966
+ if (missingTools.length) {
967
+ finish(new Error(`MCP verification did not discover required tools: ${missingTools.join(", ")}`));
968
+ return;
969
+ }
970
+ finish(null, toolNames);
971
+ }
972
+ }
973
+ });
974
+ child.stderr.setEncoding("utf8");
975
+ child.stderr.on("data", (chunk) => {
976
+ stderr = `${stderr}${chunk}`.slice(-4_000);
977
+ });
978
+ child.on("error", (error) => finish(new Error(`Could not start the MCP server: ${error.message}`)));
979
+ child.on("exit", (code) => {
980
+ if (!settled) finish(new Error(`MCP server exited before verification completed (code ${code ?? "unknown"}).${stderr.trim() ? ` ${stderr.trim()}` : ""}`));
981
+ });
982
+
983
+ send({
984
+ jsonrpc: "2.0",
985
+ id: 1,
986
+ method: "initialize",
987
+ params: {
988
+ protocolVersion: "2024-11-05",
989
+ capabilities: {},
990
+ clientInfo: { name: "launchchair-setup-check", version: VERSION },
991
+ },
992
+ });
993
+ });
994
+ }
995
+
869
996
  function renderMcpConfig(target, options = {}) {
870
997
  const normalized = normalizedMcpTarget(target);
871
998
  const server = mcpServerConfig(normalized, options);
@@ -888,7 +1015,7 @@ function renderMcpConfig(target, options = {}) {
888
1015
  command = ${tomlString(server.command)}
889
1016
  args = [${argsLine}]
890
1017
  required = false
891
- startup_timeout_sec = 20
1018
+ startup_timeout_sec = 45
892
1019
  tool_timeout_sec = 3600
893
1020
  default_tools_approval_mode = "approve"
894
1021
 
@@ -931,11 +1058,22 @@ function copyToClipboard(text) {
931
1058
  }
932
1059
 
933
1060
  function claudeDesktopConfigPath() {
934
- return path.join(os.homedir() || process.cwd(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
1061
+ const configuredPath = env("CLAUDE_DESKTOP_CONFIG_PATH");
1062
+ if (configuredPath) return path.resolve(configuredPath);
1063
+ const home = os.homedir() || process.cwd();
1064
+ if (process.platform === "darwin") {
1065
+ return path.join(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
1066
+ }
1067
+ if (process.platform === "win32") {
1068
+ return path.join(env("APPDATA", path.join(home, "AppData", "Roaming")), "Claude", "claude_desktop_config.json");
1069
+ }
1070
+ return path.join(env("XDG_CONFIG_HOME", path.join(home, ".config")), "Claude", "claude_desktop_config.json");
935
1071
  }
936
1072
 
937
1073
  function codexConfigPath() {
938
- return path.join(os.homedir() || process.cwd(), ".codex", "config.toml");
1074
+ const configuredHome = env("CODEX_HOME");
1075
+ const codexHome = configuredHome ? path.resolve(configuredHome) : path.join(os.homedir() || process.cwd(), ".codex");
1076
+ return path.join(codexHome, "config.toml");
939
1077
  }
940
1078
 
941
1079
  function removeCodexLaunchChairTables(raw) {
@@ -970,7 +1108,7 @@ function writeCodexConfig() {
970
1108
  }
971
1109
  const preserved = removeCodexLaunchChairTables(current);
972
1110
  const next = `${preserved ? `${preserved}\n\n` : ""}${renderMcpConfig("codex", { includeTokenPlaceholder: false })}\n`;
973
- writeFileSync(configPath, next);
1111
+ writeFileAtomic(configPath, next);
974
1112
  return configPath;
975
1113
  }
976
1114
 
@@ -980,10 +1118,14 @@ function writeClaudeDesktopConfig() {
980
1118
  let current = {};
981
1119
  if (existsSync(configPath)) {
982
1120
  const raw = readFileSync(configPath, "utf8");
983
- current = raw.trim() ? JSON.parse(raw) : {};
984
1121
  const backupPath = `${configPath}.launchchair-backup-${Date.now()}`;
985
1122
  copyFileSync(configPath, backupPath);
986
1123
  ok(`Backed up existing Claude config to ${backupPath}`);
1124
+ try {
1125
+ current = raw.trim() ? JSON.parse(raw) : {};
1126
+ } catch (error) {
1127
+ throw new Error(`Claude Desktop config is not valid JSON. The original was preserved at ${backupPath}. ${error instanceof Error ? error.message : String(error)}`);
1128
+ }
987
1129
  }
988
1130
  const next = {
989
1131
  ...current,
@@ -992,10 +1134,34 @@ function writeClaudeDesktopConfig() {
992
1134
  launchchair: mcpServerConfig("claude", { includeTokenPlaceholder: false }),
993
1135
  },
994
1136
  };
995
- writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`);
1137
+ writeFileAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`);
996
1138
  return configPath;
997
1139
  }
998
1140
 
1141
+ function registerClaudeCodeConfig() {
1142
+ const executable = env("CLAUDE_CODE_PATH", "claude");
1143
+ const version = spawnSync(executable, ["--version"], { encoding: "utf8" });
1144
+ if (version.error?.code === "ENOENT") {
1145
+ return { configured: false, reason: "Claude Code is not installed or is not on PATH." };
1146
+ }
1147
+ if (version.status !== 0) {
1148
+ throw new Error(`Claude Code is installed but could not run: ${(version.stderr || version.stdout || `exit ${version.status}`).trim()}`);
1149
+ }
1150
+
1151
+ const server = { type: "stdio", ...mcpServerConfig("claude", { includeTokenPlaceholder: false }) };
1152
+ spawnSync(executable, ["mcp", "remove", "launchchair", "--scope", "user"], { encoding: "utf8" });
1153
+ const added = spawnSync(
1154
+ executable,
1155
+ ["mcp", "add-json", "launchchair", JSON.stringify(server), "--scope", "user"],
1156
+ { encoding: "utf8" },
1157
+ );
1158
+ if (added.error) throw new Error(`Could not run Claude Code MCP setup: ${added.error.message}`);
1159
+ if (added.status !== 0) {
1160
+ throw new Error(`Claude Code MCP setup failed: ${(added.stderr || added.stdout || `exit ${added.status}`).trim()}`);
1161
+ }
1162
+ return { configured: true, output: String(added.stdout || "").trim() };
1163
+ }
1164
+
999
1165
  function printMcpConfig(target, args = {}) {
1000
1166
  const output = args["ui-fields"] === true || args["print-ui-fields"] === true ? renderMcpUiFields(target) : renderMcpConfig(target);
1001
1167
  print(output);
@@ -1100,6 +1266,9 @@ function installLaunchChairSkill(args) {
1100
1266
  async function setup(args) {
1101
1267
  const details = targetDetails(args._[0] || "codex");
1102
1268
  const cwd = path.resolve(String(args.cwd || process.cwd()));
1269
+ const writeConfig =
1270
+ args["write-config"] === true ||
1271
+ (args["copy-config"] !== true && args["ui-fields"] !== true && args["print-config"] !== true);
1103
1272
  const incomingToken = typeof args.token === "string" ? args.token.trim() : "";
1104
1273
  const incomingBaseUrl = typeof args["base-url"] === "string" ? args["base-url"].trim().replace(/\/+$/, "") : undefined;
1105
1274
  const incomingProjectId = typeof args["project-id"] === "string" ? args["project-id"].trim() : "";
@@ -1166,6 +1335,7 @@ async function setup(args) {
1166
1335
  }
1167
1336
 
1168
1337
  print("");
1338
+ let claudeCodeSetup = null;
1169
1339
  if (details.terminalOnly) {
1170
1340
  title("terminal agent commands");
1171
1341
  print("Give a terminal agent the build folder and let it run these commands when needed:");
@@ -1176,14 +1346,27 @@ async function setup(args) {
1176
1346
  print(` ${paint("launchchair run-once", "cyan")}`);
1177
1347
  print(` ${paint("launchchair bridge", "cyan")}`);
1178
1348
  } else {
1179
- if (details.mcpTarget === "codex" && args["write-config"] === true) {
1349
+ if (details.mcpTarget === "codex" && writeConfig) {
1180
1350
  const configPath = writeCodexConfig();
1181
1351
  title("Codex MCP config updated");
1182
1352
  ok(`Wrote LaunchChair MCP server to ${configPath}`);
1183
- } else if (details.mcpTarget === "claude" && args["write-config"] === true) {
1184
- const configPath = writeClaudeDesktopConfig();
1185
- title("Claude MCP config updated");
1186
- ok(`Wrote LaunchChair MCP server to ${configPath}`);
1353
+ } else if (details.mcpTarget === "claude" && writeConfig) {
1354
+ if (details.claudeClients?.includes("desktop")) {
1355
+ const configPath = writeClaudeDesktopConfig();
1356
+ title("Claude Desktop MCP config updated");
1357
+ ok(`Wrote LaunchChair MCP server to ${configPath}`);
1358
+ }
1359
+ if (details.claudeClients?.includes("code")) {
1360
+ claudeCodeSetup = registerClaudeCodeConfig();
1361
+ title("Claude Code MCP config updated");
1362
+ if (claudeCodeSetup.configured) {
1363
+ ok("Registered LaunchChair as a user-scoped MCP server for Claude Code.");
1364
+ } else if (details.claudeClients.length === 1) {
1365
+ throw new Error(`${claudeCodeSetup.reason} Install Claude Code, then rerun setup claude-code.`);
1366
+ } else {
1367
+ warn(`${claudeCodeSetup.reason} Claude Desktop is configured; rerun setup claude-code after installing Claude Code.`);
1368
+ }
1369
+ }
1187
1370
  } else {
1188
1371
  const output = details.mcpTarget === "codex" || args["ui-fields"] === true ? renderMcpUiFields(details.mcpTarget) : renderMcpConfig(details.mcpTarget);
1189
1372
  title(details.mcpTarget === "codex" || args["ui-fields"] === true ? "MCP fields to paste" : "MCP config to paste");
@@ -1195,6 +1378,17 @@ async function setup(args) {
1195
1378
  }
1196
1379
  }
1197
1380
 
1381
+ if (!details.terminalOnly && token() && args["skip-mcp-check"] !== true) {
1382
+ const toolNames = await verifyMcpServer(details.mcpTarget);
1383
+ print("");
1384
+ title("MCP verification passed");
1385
+ ok(`Initialize and tools/list succeeded; discovered ${toolNames.length} LaunchChair tools.`);
1386
+ } else if (!details.terminalOnly && !token()) {
1387
+ warn("MCP verification was skipped because no LaunchChair token is configured.");
1388
+ } else if (!details.terminalOnly && args["skip-mcp-check"] === true) {
1389
+ warn("MCP verification was skipped by --skip-mcp-check.");
1390
+ }
1391
+
1198
1392
  print("");
1199
1393
  title("next steps");
1200
1394
  if (!token()) {
@@ -1208,23 +1402,28 @@ async function setup(args) {
1208
1402
  print("3. For product/project work, have it use launchchair projects/create/continue before local code discovery.");
1209
1403
  print("4. Keep launchchair bridge running only when LaunchChair UI Run with agent jobs should be picked up.");
1210
1404
  } else {
1211
- if (details.mcpTarget === "codex" && args["write-config"] === true) {
1405
+ if (details.mcpTarget === "codex" && writeConfig) {
1212
1406
  print("1. Restart or reload Codex Desktop so the MCP server is exposed to fresh chats.");
1213
1407
  print("2. Keep the generated config file under your user profile; it launches the saved LaunchChair CLI token.");
1214
1408
  } else if (details.mcpTarget === "codex") {
1215
1409
  print("1. Fill the Codex custom MCP form with the fields above.");
1216
1410
  print("2. Restart or reload Codex Desktop so the MCP server is exposed to fresh chats.");
1217
- } else if (details.mcpTarget === "claude" && args["write-config"] === true) {
1218
- print("1. Restart or reload Claude Desktop so the MCP server is exposed to fresh chats.");
1219
- print("2. Keep the generated config file under your user profile; it launches the saved LaunchChair CLI token.");
1411
+ } else if (details.mcpTarget === "claude" && writeConfig) {
1412
+ if (details.claudeClients?.includes("desktop")) {
1413
+ print("1. Restart or reload Claude Desktop so the MCP server is exposed to fresh chats.");
1414
+ }
1415
+ if (details.claudeClients?.includes("code") && claudeCodeSetup?.configured) {
1416
+ print(`${details.claudeClients.includes("desktop") ? "2" : "1"}. Start a fresh Claude Code session; LaunchChair is registered at user scope for every project.`);
1417
+ }
1418
+ print(`${details.claudeClients?.includes("desktop") && claudeCodeSetup?.configured ? "3" : "2"}. Keep the generated config under your user profile; it launches the saved LaunchChair CLI token.`);
1220
1419
  } else {
1221
1420
  print(`1. Paste the MCP config above into ${details.label}.`);
1222
1421
  print(`2. Restart or reload ${details.label} so the MCP server is exposed to fresh chats.`);
1223
1422
  }
1224
- print("3. Use the generated command and argument fields exactly as printed; setup resolves the local Node and CLI paths for this machine.");
1225
- print("4. Keep this bridge running when you want LaunchChair's Run with agent button to work:");
1423
+ print("Use the generated command and argument fields exactly as printed; setup resolves the local Node and CLI paths for this machine.");
1424
+ print("Keep this bridge running when you want LaunchChair's Run with agent button to work:");
1226
1425
  print(` ${paint("launchchair bridge", "cyan")}`);
1227
- print("5. Start a fresh agent chat. The agent should discover the LaunchChair skill and MCP server instructions automatically.");
1426
+ print("Start a fresh agent chat. The agent should discover the LaunchChair MCP tools automatically.");
1228
1427
  }
1229
1428
  print("");
1230
1429
  row("Build folder", cwd, "cyan");
package/local-bridge.mjs CHANGED
@@ -17,7 +17,7 @@ import process from "node:process";
17
17
  import { fileURLToPath } from "node:url";
18
18
  import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
19
19
 
20
- const VERSION = "0.4.6";
20
+ const VERSION = "0.4.8";
21
21
  const CLIENT_TYPES = ["codex", "chatgpt", "claude", "claude_code", "other"];
22
22
  const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
23
23
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -19,7 +19,7 @@ import process from "node:process";
19
19
  import { fileURLToPath } from "node:url";
20
20
  import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
21
21
 
22
- const VERSION = "0.4.6";
22
+ const VERSION = "0.4.8";
23
23
  const SERVER_NAME = "launchchair-agent-mcp";
24
24
  const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
25
25
  const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
@@ -970,9 +970,7 @@ function mergeToolOverrides(baseTools, overrides) {
970
970
  }
971
971
 
972
972
  function writeMessage(obj) {
973
- const body = Buffer.from(JSON.stringify(obj), "utf8");
974
- process.stdout.write(`Content-Length: ${body.length}\r\n\r\n`);
975
- process.stdout.write(body);
973
+ process.stdout.write(`${JSON.stringify(obj)}\n`);
976
974
  }
977
975
 
978
976
  function writeResult(id, result) {
@@ -1120,36 +1118,26 @@ async function handleRpc(request) {
1120
1118
  writeError(id, -32601, `Method not found: ${method}`);
1121
1119
  }
1122
1120
 
1123
- let buffer = Buffer.alloc(0);
1121
+ function dispatchMessage(body) {
1122
+ let parsed = null;
1123
+ try {
1124
+ parsed = JSON.parse(body);
1125
+ } catch (_error) {
1126
+ return;
1127
+ }
1128
+ void handleRpc(parsed);
1129
+ }
1130
+
1131
+ let buffer = "";
1132
+ process.stdin.setEncoding("utf8");
1124
1133
  process.stdin.on("data", (chunk) => {
1125
- buffer = Buffer.concat([buffer, chunk]);
1134
+ buffer += chunk;
1126
1135
  while (true) {
1127
- const headerEnd = buffer.indexOf("\r\n\r\n");
1128
- if (headerEnd === -1) break;
1129
- const headerRaw = buffer.slice(0, headerEnd).toString("utf8");
1130
- const lines = headerRaw.split("\r\n");
1131
- let contentLength = -1;
1132
- for (const line of lines) {
1133
- const [key, value] = line.split(":");
1134
- if (key && value && key.toLowerCase() === "content-length") {
1135
- contentLength = Number.parseInt(value.trim(), 10);
1136
- }
1137
- }
1138
- if (!Number.isFinite(contentLength) || contentLength < 0) {
1139
- buffer = Buffer.alloc(0);
1140
- break;
1141
- }
1142
- const bodyStart = headerEnd + 4;
1143
- const bodyEnd = bodyStart + contentLength;
1144
- if (buffer.length < bodyEnd) break;
1145
- const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
1146
- buffer = buffer.slice(bodyEnd);
1147
- let parsed = null;
1148
- try {
1149
- parsed = JSON.parse(body);
1150
- } catch (_error) {
1151
- continue;
1152
- }
1153
- void handleRpc(parsed);
1136
+ const newlineIndex = buffer.indexOf("\n");
1137
+ if (newlineIndex === -1) break;
1138
+ const body = buffer.slice(0, newlineIndex).replace(/\r$/, "");
1139
+ buffer = buffer.slice(newlineIndex + 1);
1140
+ if (!body.trim()) continue;
1141
+ dispatchMessage(body);
1154
1142
  }
1155
1143
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchchair/agent",
3
- "version": "0.4.6",
3
+ "version": "0.4.8",
4
4
  "description": "LaunchChair Agent CLI, MCP bridge, local bridge, and runners for Codex, Claude Code, ChatGPT, Claude, and Hermes workflows.",
5
5
  "type": "module",
6
6
  "bin": {