@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 +6 -5
- package/README.md +17 -7
- package/launchchair-cli.mjs +224 -25
- 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
|
|
|
@@ -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 =
|
|
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
|
|
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
|
|
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`.
|
|
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 {
|
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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" &&
|
|
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" &&
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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" &&
|
|
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" &&
|
|
1218
|
-
|
|
1219
|
-
|
|
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("
|
|
1225
|
-
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:");
|
|
1226
1425
|
print(` ${paint("launchchair bridge", "cyan")}`);
|
|
1227
|
-
print("
|
|
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.
|
|
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