@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 +4 -4
- package/README.md +17 -7
- package/launchchair-cli.mjs +221 -24
- package/local-bridge.mjs +1 -1
- package/mcp-bridge-server.mjs +21 -33
- package/package.json +1 -1
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
|
|
68
|
-
launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
93
|
-
launchchair setup claude --cwd /absolute/path/to/repo-or-empty-folder
|
|
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
|
|
package/launchchair-cli.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
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" &&
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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" &&
|
|
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" &&
|
|
1220
|
-
|
|
1221
|
-
|
|
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("
|
|
1227
|
-
print("
|
|
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("
|
|
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.
|
|
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()
|
package/mcp-bridge-server.mjs
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1134
|
+
buffer += chunk;
|
|
1126
1135
|
while (true) {
|
|
1127
|
-
const
|
|
1128
|
-
if (
|
|
1129
|
-
const
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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