@launchchair/agent 0.4.7 → 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
 
@@ -130,7 +130,7 @@ LC_AGENT_CODEX_CWD = "/absolute/path/to/the-folder-codex-should-build-into"
130
130
  # LAUNCHCHAIR_AGENT_TOKEN = "<full lc_at token from Settings API Access>"
131
131
  ```
132
132
 
133
- 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, argument, and saved-config 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.
134
134
 
135
135
  ## What To Do First In A Fresh Chat
136
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`. 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.
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.7";
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 {
@@ -868,6 +903,96 @@ function mcpServerConfig(target, options = {}) {
868
903
  };
869
904
  }
870
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
+
871
996
  function renderMcpConfig(target, options = {}) {
872
997
  const normalized = normalizedMcpTarget(target);
873
998
  const server = mcpServerConfig(normalized, options);
@@ -933,11 +1058,22 @@ function copyToClipboard(text) {
933
1058
  }
934
1059
 
935
1060
  function claudeDesktopConfigPath() {
936
- 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");
937
1071
  }
938
1072
 
939
1073
  function codexConfigPath() {
940
- 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");
941
1077
  }
942
1078
 
943
1079
  function removeCodexLaunchChairTables(raw) {
@@ -972,7 +1108,7 @@ function writeCodexConfig() {
972
1108
  }
973
1109
  const preserved = removeCodexLaunchChairTables(current);
974
1110
  const next = `${preserved ? `${preserved}\n\n` : ""}${renderMcpConfig("codex", { includeTokenPlaceholder: false })}\n`;
975
- writeFileSync(configPath, next);
1111
+ writeFileAtomic(configPath, next);
976
1112
  return configPath;
977
1113
  }
978
1114
 
@@ -982,10 +1118,14 @@ function writeClaudeDesktopConfig() {
982
1118
  let current = {};
983
1119
  if (existsSync(configPath)) {
984
1120
  const raw = readFileSync(configPath, "utf8");
985
- current = raw.trim() ? JSON.parse(raw) : {};
986
1121
  const backupPath = `${configPath}.launchchair-backup-${Date.now()}`;
987
1122
  copyFileSync(configPath, backupPath);
988
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
+ }
989
1129
  }
990
1130
  const next = {
991
1131
  ...current,
@@ -994,10 +1134,34 @@ function writeClaudeDesktopConfig() {
994
1134
  launchchair: mcpServerConfig("claude", { includeTokenPlaceholder: false }),
995
1135
  },
996
1136
  };
997
- writeFileSync(configPath, `${JSON.stringify(next, null, 2)}\n`);
1137
+ writeFileAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`);
998
1138
  return configPath;
999
1139
  }
1000
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
+
1001
1165
  function printMcpConfig(target, args = {}) {
1002
1166
  const output = args["ui-fields"] === true || args["print-ui-fields"] === true ? renderMcpUiFields(target) : renderMcpConfig(target);
1003
1167
  print(output);
@@ -1102,6 +1266,9 @@ function installLaunchChairSkill(args) {
1102
1266
  async function setup(args) {
1103
1267
  const details = targetDetails(args._[0] || "codex");
1104
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);
1105
1272
  const incomingToken = typeof args.token === "string" ? args.token.trim() : "";
1106
1273
  const incomingBaseUrl = typeof args["base-url"] === "string" ? args["base-url"].trim().replace(/\/+$/, "") : undefined;
1107
1274
  const incomingProjectId = typeof args["project-id"] === "string" ? args["project-id"].trim() : "";
@@ -1168,6 +1335,7 @@ async function setup(args) {
1168
1335
  }
1169
1336
 
1170
1337
  print("");
1338
+ let claudeCodeSetup = null;
1171
1339
  if (details.terminalOnly) {
1172
1340
  title("terminal agent commands");
1173
1341
  print("Give a terminal agent the build folder and let it run these commands when needed:");
@@ -1178,14 +1346,27 @@ async function setup(args) {
1178
1346
  print(` ${paint("launchchair run-once", "cyan")}`);
1179
1347
  print(` ${paint("launchchair bridge", "cyan")}`);
1180
1348
  } else {
1181
- if (details.mcpTarget === "codex" && args["write-config"] === true) {
1349
+ if (details.mcpTarget === "codex" && writeConfig) {
1182
1350
  const configPath = writeCodexConfig();
1183
1351
  title("Codex MCP config updated");
1184
1352
  ok(`Wrote LaunchChair MCP server to ${configPath}`);
1185
- } else if (details.mcpTarget === "claude" && args["write-config"] === true) {
1186
- const configPath = writeClaudeDesktopConfig();
1187
- title("Claude MCP config updated");
1188
- 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
+ }
1189
1370
  } else {
1190
1371
  const output = details.mcpTarget === "codex" || args["ui-fields"] === true ? renderMcpUiFields(details.mcpTarget) : renderMcpConfig(details.mcpTarget);
1191
1372
  title(details.mcpTarget === "codex" || args["ui-fields"] === true ? "MCP fields to paste" : "MCP config to paste");
@@ -1197,6 +1378,17 @@ async function setup(args) {
1197
1378
  }
1198
1379
  }
1199
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
+
1200
1392
  print("");
1201
1393
  title("next steps");
1202
1394
  if (!token()) {
@@ -1210,23 +1402,28 @@ async function setup(args) {
1210
1402
  print("3. For product/project work, have it use launchchair projects/create/continue before local code discovery.");
1211
1403
  print("4. Keep launchchair bridge running only when LaunchChair UI Run with agent jobs should be picked up.");
1212
1404
  } else {
1213
- if (details.mcpTarget === "codex" && args["write-config"] === true) {
1405
+ if (details.mcpTarget === "codex" && writeConfig) {
1214
1406
  print("1. Restart or reload Codex Desktop so the MCP server is exposed to fresh chats.");
1215
1407
  print("2. Keep the generated config file under your user profile; it launches the saved LaunchChair CLI token.");
1216
1408
  } else if (details.mcpTarget === "codex") {
1217
1409
  print("1. Fill the Codex custom MCP form with the fields above.");
1218
1410
  print("2. Restart or reload Codex Desktop so the MCP server is exposed to fresh chats.");
1219
- } else if (details.mcpTarget === "claude" && args["write-config"] === true) {
1220
- print("1. Restart or reload Claude Desktop so the MCP server is exposed to fresh chats.");
1221
- 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.`);
1222
1419
  } else {
1223
1420
  print(`1. Paste the MCP config above into ${details.label}.`);
1224
1421
  print(`2. Restart or reload ${details.label} so the MCP server is exposed to fresh chats.`);
1225
1422
  }
1226
- print("3. Use the generated command and argument fields exactly as printed; setup resolves the local Node and CLI paths for this machine.");
1227
- 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:");
1228
1425
  print(` ${paint("launchchair bridge", "cyan")}`);
1229
- 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.");
1230
1427
  }
1231
1428
  print("");
1232
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.7";
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.7";
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.7",
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": {