@launchchair/agent 0.4.10 → 0.4.12
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 +11 -23
- package/README.md +20 -19
- package/launchchair-cli.mjs +78 -26
- package/local-bridge.mjs +33 -3
- package/mcp-bridge-server.mjs +72 -14
- package/package.json +2 -1
- package/project-workspace.mjs +306 -0
- package/server.json +2 -2
package/AGENTS.md
CHANGED
|
@@ -6,7 +6,7 @@ This file is for fresh agent chats that need to use the LaunchChair Agent API or
|
|
|
6
6
|
|
|
7
7
|
Use LaunchChair first for product, research, validation, positioning, pricing, MVP blueprint, and phase-loop work. Do not inspect the local repo first unless the user is in Stack Setup, Build MVP, Design and Landing Page implementation, or another code-changing phase.
|
|
8
8
|
|
|
9
|
-
Fresh LaunchChair projects often do not have a local repo yet. Before Stack Setup,
|
|
9
|
+
Fresh LaunchChair projects often do not have a local repo yet. Before Stack Setup, each project uses its own isolated workspace under `~/.launchchair/projects`.
|
|
10
10
|
|
|
11
11
|
## Public Docs To Read
|
|
12
12
|
|
|
@@ -35,6 +35,7 @@ mkdir -p "$HOME/launchchair-agent"
|
|
|
35
35
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/local-bridge.mjs" -o "$HOME/launchchair-agent/local-bridge.mjs"
|
|
36
36
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/launchchair-cli.mjs" -o "$HOME/launchchair-agent/launchchair-cli.mjs"
|
|
37
37
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/mcp-bridge-server.mjs" -o "$HOME/launchchair-agent/mcp-bridge-server.mjs"
|
|
38
|
+
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/project-workspace.mjs" -o "$HOME/launchchair-agent/project-workspace.mjs"
|
|
38
39
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/runner-command.mjs" -o "$HOME/launchchair-agent/runner-command.mjs"
|
|
39
40
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/codex-runner.mjs" -o "$HOME/launchchair-agent/codex-runner.mjs"
|
|
40
41
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/claude-code-runner.mjs" -o "$HOME/launchchair-agent/claude-code-runner.mjs"
|
|
@@ -42,21 +43,8 @@ curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/chatgpt-runner.mjs" -
|
|
|
42
43
|
curl -fsSL "https://www.launchchair.io/api/agent/v1/bridge/claude-runner.mjs" -o "$HOME/launchchair-agent/claude-runner.mjs"
|
|
43
44
|
curl -fsSL "https://www.launchchair.io/agent-artifacts/AGENTS.md" -o "$HOME/launchchair-agent/AGENTS.md"
|
|
44
45
|
chmod +x "$HOME/launchchair-agent"/*.mjs
|
|
45
|
-
LAUNCHCHAIR_AGENT_WORKDIR="${LC_AGENT_CODEX_CWD:-${LC_AGENT_CLAUDE_CODE_CWD:-$PWD}}"
|
|
46
|
-
if [ -d "$LAUNCHCHAIR_AGENT_WORKDIR" ] && [ ! -f "$LAUNCHCHAIR_AGENT_WORKDIR/AGENTS.md" ]; then
|
|
47
|
-
printf '%s\n' \
|
|
48
|
-
'# LaunchChair Agent Bootstrap Pointer' \
|
|
49
|
-
'' \
|
|
50
|
-
'Before searching this repo, read ~/launchchair-agent/AGENTS.md.' \
|
|
51
|
-
'Use LaunchChair MCP/API first for product phases, project discovery, project creation, and phase loops.' \
|
|
52
|
-
'Do not ask for extra confirmation before using LaunchChair MCP/API when the user already requested the action and the token permits it.' \
|
|
53
|
-
'Only inspect this repo for Stack Setup, Build MVP, Design/Landing Page, SEO implementation, smoke testing, security review, or code remediation.' \
|
|
54
|
-
> "$LAUNCHCHAIR_AGENT_WORKDIR/AGENTS.md"
|
|
55
|
-
fi
|
|
56
46
|
```
|
|
57
47
|
|
|
58
|
-
The final block creates a tiny project-local `AGENTS.md` pointer only when the target folder does not already have one. It helps fresh Codex chats auto-load the LaunchChair instructions without overwriting a real project instruction file.
|
|
59
|
-
|
|
60
48
|
## CLI
|
|
61
49
|
|
|
62
50
|
The npm CLI is the recommended setup path for Codex Desktop and Claude Desktop. It can log in through LaunchChair, save the token locally, print paste-ready MCP config, write Claude Desktop config, and start the local bridge.
|
|
@@ -64,8 +52,9 @@ The npm CLI is the recommended setup path for Codex Desktop and Claude Desktop.
|
|
|
64
52
|
```bash
|
|
65
53
|
npm install -g @launchchair/agent
|
|
66
54
|
launchchair login
|
|
67
|
-
launchchair setup codex
|
|
68
|
-
launchchair setup claude
|
|
55
|
+
launchchair setup codex
|
|
56
|
+
launchchair setup claude
|
|
57
|
+
launchchair bind --project-id PROJECT_UUID --cwd /absolute/path/to/verified-clone
|
|
69
58
|
launchchair projects --query LeafLore
|
|
70
59
|
launchchair create --name SignalLift --one-liner "Distribution workspace for technical founders" --project-type b2b --idempotency-key signallift-v1
|
|
71
60
|
launchchair run-once
|
|
@@ -76,7 +65,7 @@ launchchair mcp-config codex --ui-fields
|
|
|
76
65
|
If Codex Desktop is being configured through the custom MCP form instead of `~/.codex/config.toml`, run:
|
|
77
66
|
|
|
78
67
|
```bash
|
|
79
|
-
launchchair setup codex --
|
|
68
|
+
launchchair setup codex --copy-config
|
|
80
69
|
```
|
|
81
70
|
|
|
82
71
|
## Environment Variables
|
|
@@ -89,12 +78,12 @@ LAUNCHCHAIR_PROJECT_ID="optional project UUID; omit or leave blank if creating o
|
|
|
89
78
|
`launchchair login` is the recommended token path. It saves the token to `~/.launchchair/config.json`, and `launchchair mcp-server` reads that saved token. Use `LAUNCHCHAIR_AGENT_TOKEN` only as a manual fallback when browser login is skipped or a per-client token override is needed.
|
|
90
79
|
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
80
|
|
|
92
|
-
For Codex Desktop local runs:
|
|
81
|
+
For Codex Desktop local runs, choose the runner globally and bind each project clone independently:
|
|
93
82
|
|
|
94
83
|
```bash
|
|
95
84
|
LC_AGENT_ROUTER_GPT_TARGET="codex"
|
|
96
85
|
LC_AGENT_ROUTER_CODEX_TARGET="codex"
|
|
97
|
-
|
|
86
|
+
launchchair bind --project-id PROJECT_UUID --cwd /absolute/path/to/verified-clone
|
|
98
87
|
```
|
|
99
88
|
|
|
100
89
|
For Claude Desktop plus Claude Code local runs:
|
|
@@ -102,10 +91,10 @@ For Claude Desktop plus Claude Code local runs:
|
|
|
102
91
|
```bash
|
|
103
92
|
LC_AGENT_ROUTER_GPT_TARGET="claude_code"
|
|
104
93
|
LC_AGENT_ROUTER_CODEX_TARGET="claude_code"
|
|
105
|
-
|
|
94
|
+
launchchair bind --project-id PROJECT_UUID --cwd /absolute/path/to/verified-clone
|
|
106
95
|
```
|
|
107
96
|
|
|
108
|
-
|
|
97
|
+
Pre-code work uses an isolated per-project scratch folder. After Stack Setup records the GitHub repository, the bridge auto-detects matching clones in standard repo folders and verifies the Git `origin`. If needed, clone the repo and run `launchchair bind`; code-changing runs fail closed until that verification succeeds.
|
|
109
98
|
|
|
110
99
|
## MCP Server Config Example
|
|
111
100
|
|
|
@@ -124,13 +113,12 @@ LAUNCHCHAIR_CONFIG_DIR = "/absolute/path/to/.launchchair"
|
|
|
124
113
|
LAUNCHCHAIR_PROJECT_ID = ""
|
|
125
114
|
LC_AGENT_ROUTER_GPT_TARGET = "codex"
|
|
126
115
|
LC_AGENT_ROUTER_CODEX_TARGET = "codex"
|
|
127
|
-
LC_AGENT_CODEX_CWD = "/absolute/path/to/the-folder-codex-should-build-into"
|
|
128
116
|
|
|
129
117
|
# Optional fallback if launchchair login was skipped:
|
|
130
118
|
# LAUNCHCHAIR_AGENT_TOKEN = "<full lc_at token from Settings API Access>"
|
|
131
119
|
```
|
|
132
120
|
|
|
133
|
-
Prefer `launchchair setup codex
|
|
121
|
+
Prefer `launchchair setup codex` so the CLI writes native config and verifies MCP tool discovery. Use `launchchair bind --project-id PROJECT_UUID --cwd /absolute/path/to/verified-clone` after cloning when automatic discovery cannot locate the repo. Use `launchchair mcp-config codex --ui-fields` only for manual custom-form setup.
|
|
134
122
|
|
|
135
123
|
## What To Do First In A Fresh Chat
|
|
136
124
|
|
package/README.md
CHANGED
|
@@ -49,59 +49,60 @@ 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
|
|
52
|
+
Use `setup` as the easiest path. It starts browser login when no token is saved yet, writes the global MCP config for your agent, creates isolated per-project workspaces, and then proves the server can complete MCP `initialize` and `tools/list` before reporting success.
|
|
53
53
|
|
|
54
54
|
Codex Desktop:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
launchchair setup codex
|
|
57
|
+
launchchair setup codex
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
If you prefer Codex Desktop's custom MCP form, copy paste-ready fields instead:
|
|
61
61
|
|
|
62
62
|
```bash
|
|
63
|
-
launchchair setup codex --
|
|
63
|
+
launchchair setup codex --copy-config
|
|
64
64
|
```
|
|
65
65
|
|
|
66
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
|
|
69
|
+
launchchair setup claude
|
|
70
70
|
```
|
|
71
71
|
|
|
72
72
|
Configure only one Claude client when needed:
|
|
73
73
|
|
|
74
74
|
```bash
|
|
75
|
-
launchchair setup claude-desktop
|
|
76
|
-
launchchair setup claude-code
|
|
75
|
+
launchchair setup claude-desktop
|
|
76
|
+
launchchair setup claude-code
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
Terminal / CLI agents:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
|
-
launchchair setup terminal
|
|
82
|
+
launchchair setup terminal
|
|
83
83
|
```
|
|
84
84
|
|
|
85
85
|
If you do not want browser login during setup, add `--skip-login`. You can also save an existing token during setup:
|
|
86
86
|
|
|
87
87
|
```bash
|
|
88
|
-
launchchair setup codex --
|
|
89
|
-
launchchair setup codex --
|
|
88
|
+
launchchair setup codex --skip-login
|
|
89
|
+
launchchair setup codex --token "<full lc_at token from LaunchChair Settings API Access>"
|
|
90
90
|
```
|
|
91
91
|
|
|
92
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
93
|
|
|
94
|
-
|
|
94
|
+
Fresh projects automatically use an isolated folder under `~/.launchchair/projects/<project-id>/workspace` before a repo exists. After Stack Setup, LaunchChair looks for the repo by its recorded Git URL in standard local repo folders and verifies the `origin` before switching. If it cannot find the clone, code work stops with an exact clone and `launchchair bind` instruction instead of using another project.
|
|
95
95
|
|
|
96
|
-
Use `setup terminal` when the agent runs inside a normal shell instead of Codex Desktop or Claude Desktop MCP. It
|
|
96
|
+
Use `setup terminal` when the agent runs inside a normal shell instead of Codex Desktop or Claude Desktop MCP. It installs the user-scoped bootstrap and prints the CLI commands the agent should use: `launchchair projects`, `launchchair create`, `launchchair continue`, `launchchair run-once`, and `launchchair bridge`.
|
|
97
97
|
|
|
98
98
|
## Common Commands
|
|
99
99
|
|
|
100
100
|
```bash
|
|
101
|
-
launchchair setup codex
|
|
102
|
-
launchchair setup claude
|
|
103
|
-
launchchair setup claude-code
|
|
104
|
-
launchchair setup terminal
|
|
101
|
+
launchchair setup codex
|
|
102
|
+
launchchair setup claude
|
|
103
|
+
launchchair setup claude-code
|
|
104
|
+
launchchair setup terminal
|
|
105
|
+
launchchair bind --project-id <project-id> --cwd /absolute/path/to/verified-clone
|
|
105
106
|
launchchair projects
|
|
106
107
|
launchchair projects --query LeafLore
|
|
107
108
|
launchchair scorecards --project-id <project-id>
|
|
@@ -114,7 +115,7 @@ launchchair bridge
|
|
|
114
115
|
launchchair mcp-config codex
|
|
115
116
|
launchchair mcp-config codex --ui-fields --copy-config
|
|
116
117
|
launchchair mcp-config claude
|
|
117
|
-
launchchair install-agent-md --cwd /absolute/path/to/repo
|
|
118
|
+
launchchair install-agent-md --cwd /absolute/path/to/repo
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
Feature projects can be created and planned from the CLI for every authenticated LaunchChair workspace:
|
|
@@ -175,17 +176,17 @@ launchchair run-once
|
|
|
175
176
|
|
|
176
177
|
## Runner Targets
|
|
177
178
|
|
|
178
|
-
Codex:
|
|
179
|
+
Codex routes are selected globally, while repository paths are bound per LaunchChair project:
|
|
179
180
|
|
|
180
181
|
```bash
|
|
181
|
-
launchchair
|
|
182
|
+
launchchair bind --project-id <project-id> --cwd /absolute/path/to/verified-clone
|
|
182
183
|
launchchair bridge
|
|
183
184
|
```
|
|
184
185
|
|
|
185
186
|
Claude Code:
|
|
186
187
|
|
|
187
188
|
```bash
|
|
188
|
-
launchchair
|
|
189
|
+
launchchair bind --project-id <project-id> --cwd /absolute/path/to/verified-clone
|
|
189
190
|
LC_AGENT_ROUTER_GPT_TARGET=claude_code LC_AGENT_ROUTER_CODEX_TARGET=claude_code launchchair bridge
|
|
190
191
|
```
|
|
191
192
|
|
package/launchchair-cli.mjs
CHANGED
|
@@ -14,8 +14,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
|
+
import {
|
|
18
|
+
bindProjectWorkspace,
|
|
19
|
+
listProjectWorkspaceBindings,
|
|
20
|
+
unbindProjectWorkspace,
|
|
21
|
+
} from "./project-workspace.mjs";
|
|
17
22
|
|
|
18
|
-
const VERSION = "0.4.
|
|
23
|
+
const VERSION = "0.4.12";
|
|
19
24
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
20
25
|
const DEFAULT_BASE_URL = "https://www.launchchair.io";
|
|
21
26
|
const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
|
|
@@ -179,9 +184,11 @@ Commands:
|
|
|
179
184
|
bridge Start the long-running local bridge for Run with Agent
|
|
180
185
|
mcp-server Run the stdio MCP server using saved CLI config
|
|
181
186
|
setup codex|claude|claude-desktop|claude-code|terminal [options]
|
|
182
|
-
Guided
|
|
187
|
+
Guided global CLI/MCP setup with isolated project workspaces
|
|
188
|
+
bind --project-id ID --cwd PATH Bind one LaunchChair project to a verified local Git clone
|
|
189
|
+
unbind --project-id ID Remove one project's saved local repository binding
|
|
183
190
|
config Show local CLI config without printing the token
|
|
184
|
-
config set KEY VALUE Save base-url
|
|
191
|
+
config set KEY VALUE Save base-url or project-id
|
|
185
192
|
mcp-config codex|claude [options]
|
|
186
193
|
Print MCP config or Codex custom MCP form fields
|
|
187
194
|
install-agent-md [--cwd PATH] Create a small AGENTS.md pointer in a fresh repo
|
|
@@ -215,7 +222,7 @@ Continue options:
|
|
|
215
222
|
--completion-target TEXT Natural-language target for the loop run
|
|
216
223
|
|
|
217
224
|
Setup options:
|
|
218
|
-
--cwd PATH
|
|
225
|
+
--cwd PATH Bind this repo only when --project-id is also supplied
|
|
219
226
|
--token TOKEN Optional manual lc_at token fallback to save during setup
|
|
220
227
|
--project-id ID Optional default LaunchChair project id
|
|
221
228
|
--base-url URL Optional LaunchChair base URL
|
|
@@ -241,8 +248,8 @@ Env:
|
|
|
241
248
|
LAUNCHCHAIR_AGENT_TOKEN Optional full lc_at token override
|
|
242
249
|
LAUNCHCHAIR_BASE_URL Default: https://www.launchchair.io
|
|
243
250
|
LAUNCHCHAIR_PROJECT_ID Optional default project id
|
|
244
|
-
LC_AGENT_CODEX_CWD
|
|
245
|
-
LC_AGENT_CLAUDE_CODE_CWD
|
|
251
|
+
LC_AGENT_CODEX_CWD Legacy candidate only; verified per project before use
|
|
252
|
+
LC_AGENT_CLAUDE_CODE_CWD Legacy candidate only; verified per project before use
|
|
246
253
|
|
|
247
254
|
MCP config options:
|
|
248
255
|
--ui-fields Print fields for Codex Desktop's custom MCP form
|
|
@@ -767,9 +774,9 @@ function buildBridgeEnv(projectId = defaultProjectId()) {
|
|
|
767
774
|
LAUNCHCHAIR_CONFIG_DIR: CONFIG_DIR,
|
|
768
775
|
LAUNCHCHAIR_AGENT_TOKEN: token(),
|
|
769
776
|
LAUNCHCHAIR_PROJECT_ID: projectId,
|
|
770
|
-
LC_AGENT_CODEX_CWD: configValue("codexCwd", ["LC_AGENT_CODEX_CWD"], ""),
|
|
771
|
-
LC_AGENT_CLAUDE_CODE_CWD: configValue("claudeCodeCwd", ["LC_AGENT_CLAUDE_CODE_CWD"], ""),
|
|
772
777
|
};
|
|
778
|
+
delete childEnv.LC_AGENT_CODEX_CWD;
|
|
779
|
+
delete childEnv.LC_AGENT_CLAUDE_CODE_CWD;
|
|
773
780
|
Object.keys(childEnv).forEach((key) => {
|
|
774
781
|
if (childEnv[key] === undefined || childEnv[key] === null || childEnv[key] === "") {
|
|
775
782
|
delete childEnv[key];
|
|
@@ -881,7 +888,6 @@ function mcpEnvForTarget(target, options = {}) {
|
|
|
881
888
|
const includeTokenPlaceholder = options.includeTokenPlaceholder !== false;
|
|
882
889
|
const details = targetDetails(target || "codex");
|
|
883
890
|
const projectId = defaultProjectId();
|
|
884
|
-
const cwd = configValue(details.cwdConfigKey, [details.cwdEnvName], "/absolute/path/to/repo-or-empty-folder");
|
|
885
891
|
const envBlock = {
|
|
886
892
|
LAUNCHCHAIR_BASE_URL: baseUrl(),
|
|
887
893
|
LAUNCHCHAIR_CONFIG_DIR: CONFIG_DIR,
|
|
@@ -889,7 +895,6 @@ function mcpEnvForTarget(target, options = {}) {
|
|
|
889
895
|
...(projectId ? { LAUNCHCHAIR_PROJECT_ID: projectId } : {}),
|
|
890
896
|
LC_AGENT_ROUTER_GPT_TARGET: details.routerTarget,
|
|
891
897
|
LC_AGENT_ROUTER_CODEX_TARGET: details.routerTarget,
|
|
892
|
-
[details.cwdEnvName]: cwd,
|
|
893
898
|
};
|
|
894
899
|
return Object.fromEntries(Object.entries(envBlock).filter(([, value]) => String(value ?? "").trim()));
|
|
895
900
|
}
|
|
@@ -1025,13 +1030,12 @@ ${envLines}`;
|
|
|
1025
1030
|
|
|
1026
1031
|
function renderMcpUiFields(target) {
|
|
1027
1032
|
const normalized = normalizedMcpTarget(target);
|
|
1028
|
-
const details = targetDetails(normalized);
|
|
1029
1033
|
const server = mcpServerConfig(normalized);
|
|
1030
1034
|
const envLines =
|
|
1031
1035
|
Object.entries(server.env)
|
|
1032
1036
|
.map(([key, value]) => ` ${key} = ${value}`)
|
|
1033
1037
|
.join("\n") || " (none)";
|
|
1034
|
-
const workingDir =
|
|
1038
|
+
const workingDir = path.join(CONFIG_DIR, "projects");
|
|
1035
1039
|
return `Name
|
|
1036
1040
|
launchchair
|
|
1037
1041
|
|
|
@@ -1173,14 +1177,20 @@ function printMcpConfig(target, args = {}) {
|
|
|
1173
1177
|
|
|
1174
1178
|
function showConfig() {
|
|
1175
1179
|
const config = loadConfig();
|
|
1180
|
+
const bindings = listProjectWorkspaceBindings(CONFIG_PATH);
|
|
1176
1181
|
title("CLI config");
|
|
1177
1182
|
row("Path", CONFIG_PATH, existsSync(CONFIG_PATH) ? "dim" : "yellow");
|
|
1178
1183
|
row("Base URL", baseUrl(), "cyan");
|
|
1179
1184
|
row("Token", maskToken(token()), token() ? "green" : "yellow");
|
|
1180
1185
|
row("Token source", tokenSource(), token() ? "green" : "yellow");
|
|
1181
1186
|
row("Project ID", defaultProjectId() || "not set", defaultProjectId() ? "cyan" : "dim");
|
|
1182
|
-
row("
|
|
1183
|
-
|
|
1187
|
+
row("Project bindings", Object.keys(bindings).length, Object.keys(bindings).length > 0 ? "cyan" : "dim");
|
|
1188
|
+
for (const [projectId, binding] of Object.entries(bindings)) {
|
|
1189
|
+
row(` ${projectId.slice(0, 12)}`, typeof binding === "string" ? binding : binding?.path ?? "invalid", "cyan");
|
|
1190
|
+
}
|
|
1191
|
+
if (config.codexCwd || config.claudeCodeCwd || env("LC_AGENT_CODEX_CWD") || env("LC_AGENT_CLAUDE_CODE_CWD")) {
|
|
1192
|
+
row("Legacy CWD", "ignored unless its Git origin matches the current project", "yellow");
|
|
1193
|
+
}
|
|
1184
1194
|
}
|
|
1185
1195
|
|
|
1186
1196
|
function setConfig(args) {
|
|
@@ -1189,16 +1199,50 @@ function setConfig(args) {
|
|
|
1189
1199
|
const allowed = {
|
|
1190
1200
|
"base-url": "baseUrl",
|
|
1191
1201
|
"project-id": "projectId",
|
|
1192
|
-
"codex-cwd": "codexCwd",
|
|
1193
|
-
"claude-code-cwd": "claudeCodeCwd",
|
|
1194
1202
|
};
|
|
1195
1203
|
if (!key || !value || !allowed[key]) {
|
|
1196
|
-
throw new Error("Usage: launchchair config set base-url|project-id
|
|
1204
|
+
throw new Error("Usage: launchchair config set base-url|project-id VALUE. Use launchchair bind for local repositories.");
|
|
1197
1205
|
}
|
|
1198
1206
|
saveConfig({ [allowed[key]]: key === "base-url" ? value.replace(/\/+$/, "") : value });
|
|
1199
1207
|
ok(`Saved ${key} to ${CONFIG_PATH}`);
|
|
1200
1208
|
}
|
|
1201
1209
|
|
|
1210
|
+
async function getRunnerWorkspaceContext(projectId) {
|
|
1211
|
+
const payload = await api(`/api/agent/v1/projects/${encodeURIComponent(projectId)}/runner-workspace`);
|
|
1212
|
+
return payload?.workspace && typeof payload.workspace === "object" ? payload.workspace : { projectId };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async function bindProject(args) {
|
|
1216
|
+
const projectId = String(args["project-id"] || defaultProjectId() || "").trim();
|
|
1217
|
+
const cwd = String(args.cwd || "").trim();
|
|
1218
|
+
if (!projectId || !cwd) {
|
|
1219
|
+
throw new Error("Usage: launchchair bind --project-id ID --cwd /absolute/path/to/repo");
|
|
1220
|
+
}
|
|
1221
|
+
const context = await getRunnerWorkspaceContext(projectId);
|
|
1222
|
+
const repository = context?.repository && typeof context.repository === "object" ? context.repository : {};
|
|
1223
|
+
const binding = bindProjectWorkspace({
|
|
1224
|
+
configPath: CONFIG_PATH,
|
|
1225
|
+
projectId,
|
|
1226
|
+
cwd,
|
|
1227
|
+
repositoryUrl: String(repository.repoUrl || repository.cloneUrl || "").trim(),
|
|
1228
|
+
});
|
|
1229
|
+
installAgentMd({ cwd: binding.path });
|
|
1230
|
+
title("project repository bound");
|
|
1231
|
+
row("Project ID", projectId, "cyan");
|
|
1232
|
+
row("Local repository", binding.path, "cyan");
|
|
1233
|
+
if (binding.repositoryUrl) row("Verified origin", binding.repositoryUrl, "green");
|
|
1234
|
+
ok("Future runs for this LaunchChair project will use this repository.");
|
|
1235
|
+
ok("Already-running bridges will pick up this binding on the next run.");
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function unbindProject(args) {
|
|
1239
|
+
const projectId = String(args["project-id"] || defaultProjectId() || "").trim();
|
|
1240
|
+
if (!projectId) throw new Error("Usage: launchchair unbind --project-id ID");
|
|
1241
|
+
const existed = unbindProjectWorkspace({ configPath: CONFIG_PATH, projectId });
|
|
1242
|
+
if (existed) ok(`Removed local repository binding for ${projectId}.`);
|
|
1243
|
+
else warn(`No local repository binding existed for ${projectId}.`);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1202
1246
|
function installAgentMd(args) {
|
|
1203
1247
|
const cwd = path.resolve(String(args.cwd || process.cwd()));
|
|
1204
1248
|
const target = path.join(cwd, "AGENTS.md");
|
|
@@ -1258,17 +1302,23 @@ function installLaunchChairSkill(args) {
|
|
|
1258
1302
|
|
|
1259
1303
|
async function setup(args) {
|
|
1260
1304
|
const details = targetDetails(args._[0] || "codex");
|
|
1261
|
-
const
|
|
1305
|
+
const requestedCwd = typeof args.cwd === "string" && args.cwd.trim() ? path.resolve(args.cwd.trim()) : "";
|
|
1262
1306
|
const writeConfig =
|
|
1263
1307
|
args["write-config"] === true ||
|
|
1264
1308
|
(args["copy-config"] !== true && args["ui-fields"] !== true && args["print-config"] !== true);
|
|
1265
1309
|
const incomingToken = typeof args.token === "string" ? args.token.trim() : "";
|
|
1266
1310
|
const incomingBaseUrl = typeof args["base-url"] === "string" ? args["base-url"].trim().replace(/\/+$/, "") : undefined;
|
|
1267
1311
|
const incomingProjectId = typeof args["project-id"] === "string" ? args["project-id"].trim() : "";
|
|
1312
|
+
const bindingProjectId = incomingProjectId || defaultProjectId();
|
|
1313
|
+
|
|
1314
|
+
if (requestedCwd && !bindingProjectId) {
|
|
1315
|
+
throw new Error("--cwd must be paired with --project-id. Global runner directories are no longer supported.");
|
|
1316
|
+
}
|
|
1268
1317
|
|
|
1269
1318
|
title(`guided setup for ${details.label}`);
|
|
1270
|
-
|
|
1271
|
-
|
|
1319
|
+
const projectWorkspaceRoot = path.join(CONFIG_DIR, "projects");
|
|
1320
|
+
mkdirSync(projectWorkspaceRoot, { recursive: true, mode: 0o700 });
|
|
1321
|
+
ok(`Isolated project workspace root ready: ${projectWorkspaceRoot}`);
|
|
1272
1322
|
|
|
1273
1323
|
if (incomingToken) {
|
|
1274
1324
|
if (!incomingToken.startsWith("lc_at_")) {
|
|
@@ -1287,10 +1337,6 @@ async function setup(args) {
|
|
|
1287
1337
|
});
|
|
1288
1338
|
}
|
|
1289
1339
|
|
|
1290
|
-
saveConfig({ [details.cwdConfigKey]: cwd });
|
|
1291
|
-
ok(`Saved ${details.cwdEnvName}: ${cwd}`);
|
|
1292
|
-
installAgentMd({ cwd });
|
|
1293
|
-
|
|
1294
1340
|
if (!token() && args["skip-login"] !== true) {
|
|
1295
1341
|
print("");
|
|
1296
1342
|
warn("No saved LaunchChair token found. Starting browser approval now.");
|
|
@@ -1306,8 +1352,12 @@ async function setup(args) {
|
|
|
1306
1352
|
});
|
|
1307
1353
|
}
|
|
1308
1354
|
|
|
1355
|
+
if (requestedCwd) {
|
|
1356
|
+
await bindProject({ "project-id": bindingProjectId, cwd: requestedCwd });
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1309
1359
|
if (details.mcpTarget === "codex") {
|
|
1310
|
-
installLaunchChairSkill({ cwd });
|
|
1360
|
+
installLaunchChairSkill({ cwd: path.dirname(CONFIG_DIR) });
|
|
1311
1361
|
}
|
|
1312
1362
|
|
|
1313
1363
|
print("");
|
|
@@ -1410,7 +1460,7 @@ async function setup(args) {
|
|
|
1410
1460
|
print("Start a fresh agent chat. The agent should discover the LaunchChair MCP tools automatically.");
|
|
1411
1461
|
}
|
|
1412
1462
|
print("");
|
|
1413
|
-
row("
|
|
1463
|
+
row("Workspace mode", requestedCwd ? `project-bound: ${requestedCwd}` : "isolated per LaunchChair project", "cyan");
|
|
1414
1464
|
row("Runner target", details.routerTarget, "cyan");
|
|
1415
1465
|
row("Config file", CONFIG_PATH, "dim");
|
|
1416
1466
|
|
|
@@ -1441,6 +1491,8 @@ async function main() {
|
|
|
1441
1491
|
if (command === "bridge") return spawnBridge(["--verbose"]);
|
|
1442
1492
|
if (command === "mcp-server") return spawnMcpServer();
|
|
1443
1493
|
if (command === "setup") return setup(args);
|
|
1494
|
+
if (command === "bind") return bindProject(args);
|
|
1495
|
+
if (command === "unbind") return unbindProject(args);
|
|
1444
1496
|
if (command === "config" && args._[0] === "set") return setConfig({ ...args, _: args._.slice(1) });
|
|
1445
1497
|
if (command === "config") return showConfig();
|
|
1446
1498
|
if (command === "mcp-config") return printMcpConfig(args._[0] || "codex", args);
|
package/local-bridge.mjs
CHANGED
|
@@ -15,9 +15,10 @@ import os from "node:os";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import process from "node:process";
|
|
17
17
|
import { fileURLToPath } from "node:url";
|
|
18
|
+
import { resolveProjectWorkspace } from "./project-workspace.mjs";
|
|
18
19
|
import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
|
|
19
20
|
|
|
20
|
-
const VERSION = "0.4.
|
|
21
|
+
const VERSION = "0.4.12";
|
|
21
22
|
const CLIENT_TYPES = ["codex", "chatgpt", "claude", "claude_code", "other"];
|
|
22
23
|
const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
|
|
@@ -130,10 +131,10 @@ Optional env:
|
|
|
130
131
|
AGENT_MOCK_RESPONSE If set, skips runner command and returns this payload
|
|
131
132
|
OPENAI_API_KEY Enables built-in ChatGPT runner fallback
|
|
132
133
|
ANTHROPIC_API_KEY Enables built-in Claude runner fallback
|
|
133
|
-
LC_AGENT_CODEX_CWD
|
|
134
|
+
LC_AGENT_CODEX_CWD Legacy search candidate; Git origin must match the project
|
|
134
135
|
LC_AGENT_CODEX_SANDBOX read-only|workspace-write|danger-full-access (default: workspace-write)
|
|
135
136
|
LC_AGENT_CODEX_MODEL Optional Codex model override
|
|
136
|
-
LC_AGENT_CLAUDE_CODE_CWD
|
|
137
|
+
LC_AGENT_CLAUDE_CODE_CWD Legacy search candidate; Git origin must match the project
|
|
137
138
|
LC_AGENT_CLAUDE_CODE_MODEL Optional Claude Code model override
|
|
138
139
|
LC_AGENT_CLAUDE_CODE_ALLOWED_TOOLS
|
|
139
140
|
Optional Claude Code allowedTools list
|
|
@@ -193,6 +194,10 @@ const config = {
|
|
|
193
194
|
gpt: normalizeTarget(env("LC_AGENT_ROUTER_GPT_TARGET", "codex"), "codex"),
|
|
194
195
|
codex: normalizeTarget(env("LC_AGENT_ROUTER_CODEX_TARGET", "codex"), "codex"),
|
|
195
196
|
},
|
|
197
|
+
legacyRepoPaths: {
|
|
198
|
+
codex: env("LC_AGENT_CODEX_CWD", savedConfigValue("codexCwd", process.cwd())),
|
|
199
|
+
claudeCode: env("LC_AGENT_CLAUDE_CODE_CWD", savedConfigValue("claudeCodeCwd", process.cwd())),
|
|
200
|
+
},
|
|
196
201
|
runnerCommands: {
|
|
197
202
|
generic: env("LC_AGENT_RUNNER_CMD"),
|
|
198
203
|
gpt: env("LC_AGENT_RUNNER_CMD_GPT"),
|
|
@@ -347,6 +352,25 @@ async function claimNextRun() {
|
|
|
347
352
|
return payload?.run ?? null;
|
|
348
353
|
}
|
|
349
354
|
|
|
355
|
+
function resolveWorkspaceForRun(run) {
|
|
356
|
+
const target = getTargetForRun(run) === "claude_code" ? "claude_code" : "codex";
|
|
357
|
+
const legacyPath = target === "claude_code" ? config.legacyRepoPaths.claudeCode : config.legacyRepoPaths.codex;
|
|
358
|
+
const context = run?.runnerWorkspace && typeof run.runnerWorkspace === "object" && !Array.isArray(run.runnerWorkspace)
|
|
359
|
+
? run.runnerWorkspace
|
|
360
|
+
: {};
|
|
361
|
+
return {
|
|
362
|
+
target,
|
|
363
|
+
workspace: resolveProjectWorkspace({
|
|
364
|
+
configDir: CONFIG_DIR,
|
|
365
|
+
configPath: CONFIG_PATH,
|
|
366
|
+
projectId: String(run?.projectId ?? context.projectId ?? "").trim(),
|
|
367
|
+
repository: context.repository,
|
|
368
|
+
requireRepository: context.requiresRepository === true,
|
|
369
|
+
legacyPaths: [legacyPath],
|
|
370
|
+
}),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
350
374
|
async function executeRunner(run) {
|
|
351
375
|
if (config.mockResponse) {
|
|
352
376
|
return {
|
|
@@ -367,6 +391,9 @@ async function executeRunner(run) {
|
|
|
367
391
|
}
|
|
368
392
|
const command = buildRunnerCommand(commandTemplate, run);
|
|
369
393
|
const parsedCommand = parseRunnerCommand(command);
|
|
394
|
+
const resolved = resolveWorkspaceForRun(run);
|
|
395
|
+
const workspaceEnvName = resolved.target === "claude_code" ? "LC_AGENT_CLAUDE_CODE_CWD" : "LC_AGENT_CODEX_CWD";
|
|
396
|
+
log(`Workspace ${resolved.workspace.path} (${resolved.workspace.source})`);
|
|
370
397
|
|
|
371
398
|
const startedAt = Date.now();
|
|
372
399
|
return new Promise((resolve, reject) => {
|
|
@@ -382,6 +409,7 @@ async function executeRunner(run) {
|
|
|
382
409
|
LC_AGENT_CARD_ID: String(run.cardId ?? ""),
|
|
383
410
|
LC_AGENT_PROMPT_MODE: String(run.promptMode ?? ""),
|
|
384
411
|
LC_AGENT_RUN_TYPE: String(run.runType ?? ""),
|
|
412
|
+
[workspaceEnvName]: resolved.workspace.path,
|
|
385
413
|
},
|
|
386
414
|
});
|
|
387
415
|
|
|
@@ -415,6 +443,7 @@ async function executeRunner(run) {
|
|
|
415
443
|
durationMs,
|
|
416
444
|
usedMock: false,
|
|
417
445
|
target: getTargetForRun(run),
|
|
446
|
+
workspace: resolved.workspace,
|
|
418
447
|
});
|
|
419
448
|
});
|
|
420
449
|
|
|
@@ -480,6 +509,7 @@ async function runOnce() {
|
|
|
480
509
|
runIntent: run.runIntent ?? "initial",
|
|
481
510
|
attemptNumber: run.attemptNumber ?? 1,
|
|
482
511
|
maxAttempts: run.maxAttempts ?? 1,
|
|
512
|
+
localWorkspaceSource: execution.workspace?.source ?? null,
|
|
483
513
|
},
|
|
484
514
|
error: null,
|
|
485
515
|
});
|
package/mcp-bridge-server.mjs
CHANGED
|
@@ -17,9 +17,10 @@ import os from "node:os";
|
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import process from "node:process";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { resolveProjectWorkspace } from "./project-workspace.mjs";
|
|
20
21
|
import { buildRunnerCommand, parseRunnerCommand } from "./runner-command.mjs";
|
|
21
22
|
|
|
22
|
-
const VERSION = "0.4.
|
|
23
|
+
const VERSION = "0.4.12";
|
|
23
24
|
const SERVER_NAME = "launchchair-agent-mcp";
|
|
24
25
|
const AGENT_SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
25
26
|
const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
|
|
@@ -27,7 +28,7 @@ const CONFIG_DIR = process.env.LAUNCHCHAIR_CONFIG_DIR?.trim()
|
|
|
27
28
|
: path.join(os.homedir() || process.cwd(), ".launchchair");
|
|
28
29
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
29
30
|
const BASE_SERVER_INSTRUCTIONS =
|
|
30
|
-
"Use LaunchChair MCP tools—not web browsing—for LaunchChair project discovery, creation, planning, validation scoring, and loops. Call the most specific available tool. When asked for validation scores, call launchchair_get_scorecards and report those scorecards; do not substitute generated direction-option scores. If a requested workflow is unavailable, explain the access or setup issue instead of silently substituting another project type or workflow.";
|
|
31
|
+
"Use LaunchChair MCP tools—not web browsing—for LaunchChair project discovery, creation, planning, validation scoring, and loops. Call the most specific available tool. When asked for validation scores, call launchchair_get_scorecards and report those scorecards; do not substitute generated direction-option scores. Always present completedPhaseLinks, selected choice URLs, and any upgradeUrl returned by a tool to the user as clickable links. If a requested workflow is unavailable, explain the access or setup issue instead of silently substituting another project type or workflow.";
|
|
31
32
|
|
|
32
33
|
function nodeScriptCommand(scriptName) {
|
|
33
34
|
return `${JSON.stringify(process.execPath)} ${JSON.stringify(path.join(AGENT_SCRIPT_DIR, scriptName))}`;
|
|
@@ -128,7 +129,7 @@ const config = {
|
|
|
128
129
|
codexTarget: env("LC_AGENT_RUNNER_CMD_CODEX", builtInRunnerCommand("codex")),
|
|
129
130
|
claudeCode: env("LC_AGENT_RUNNER_CMD_CLAUDE_CODE", builtInRunnerCommand("claude_code")),
|
|
130
131
|
},
|
|
131
|
-
|
|
132
|
+
legacyRepoPaths: {
|
|
132
133
|
codex: env("LC_AGENT_CODEX_CWD", savedConfigValue("codexCwd", process.cwd())),
|
|
133
134
|
claudeCode: env("LC_AGENT_CLAUDE_CODE_CWD", savedConfigValue("claudeCodeCwd", process.cwd())),
|
|
134
135
|
},
|
|
@@ -227,6 +228,32 @@ function commandForRun(run) {
|
|
|
227
228
|
return config.runnerCommands.generic;
|
|
228
229
|
}
|
|
229
230
|
|
|
231
|
+
function runnerTargetForRun(run) {
|
|
232
|
+
return targetForRun(run) === "claude_code" ? "claude_code" : "codex";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function resolveWorkspaceForRun(run, workspaceContext = run?.runnerWorkspace, options = {}) {
|
|
236
|
+
const target = runnerTargetForRun(run);
|
|
237
|
+
const context = workspaceContext && typeof workspaceContext === "object" && !Array.isArray(workspaceContext)
|
|
238
|
+
? workspaceContext
|
|
239
|
+
: {};
|
|
240
|
+
const legacyPath = target === "claude_code" ? config.legacyRepoPaths.claudeCode : config.legacyRepoPaths.codex;
|
|
241
|
+
return resolveProjectWorkspace({
|
|
242
|
+
configDir: CONFIG_DIR,
|
|
243
|
+
configPath: CONFIG_PATH,
|
|
244
|
+
projectId: String(run?.projectId ?? context.projectId ?? "").trim(),
|
|
245
|
+
repository: context.repository,
|
|
246
|
+
requireRepository: context.requiresRepository === true,
|
|
247
|
+
preferRepository: options.preferRepository === true,
|
|
248
|
+
legacyPaths: [legacyPath],
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function getRunnerWorkspaceContext(projectId) {
|
|
253
|
+
const payload = await api(`/api/agent/v1/projects/${encodeURIComponent(projectId)}/runner-workspace`);
|
|
254
|
+
return payload?.workspace && typeof payload.workspace === "object" ? payload.workspace : { projectId };
|
|
255
|
+
}
|
|
256
|
+
|
|
230
257
|
async function executeRunner(run) {
|
|
231
258
|
if (config.mockResponse) {
|
|
232
259
|
return {
|
|
@@ -245,6 +272,10 @@ async function executeRunner(run) {
|
|
|
245
272
|
}
|
|
246
273
|
const command = buildRunnerCommand(template, run);
|
|
247
274
|
const parsedCommand = parseRunnerCommand(command);
|
|
275
|
+
const workspace = resolveWorkspaceForRun(run);
|
|
276
|
+
const workspaceEnvName = runnerTargetForRun(run) === "claude_code"
|
|
277
|
+
? "LC_AGENT_CLAUDE_CODE_CWD"
|
|
278
|
+
: "LC_AGENT_CODEX_CWD";
|
|
248
279
|
const startedAt = Date.now();
|
|
249
280
|
return new Promise((resolve, reject) => {
|
|
250
281
|
const child = spawn(parsedCommand.file, parsedCommand.args, {
|
|
@@ -259,6 +290,7 @@ async function executeRunner(run) {
|
|
|
259
290
|
LC_AGENT_CARD_ID: String(run.cardId ?? ""),
|
|
260
291
|
LC_AGENT_PROMPT_MODE: String(run.promptMode ?? ""),
|
|
261
292
|
LC_AGENT_RUN_TYPE: String(run.runType ?? ""),
|
|
293
|
+
[workspaceEnvName]: workspace.path,
|
|
262
294
|
},
|
|
263
295
|
});
|
|
264
296
|
let stdout = "";
|
|
@@ -286,6 +318,7 @@ async function executeRunner(run) {
|
|
|
286
318
|
stderr,
|
|
287
319
|
exitCode: code ?? 0,
|
|
288
320
|
durationMs,
|
|
321
|
+
workspace,
|
|
289
322
|
});
|
|
290
323
|
});
|
|
291
324
|
child.stdin.write(String(run.prompt ?? ""));
|
|
@@ -366,6 +399,7 @@ async function runOnce(args = {}) {
|
|
|
366
399
|
runIntent: run.runIntent ?? "initial",
|
|
367
400
|
attemptNumber: run.attemptNumber ?? 1,
|
|
368
401
|
maxAttempts: run.maxAttempts ?? 1,
|
|
402
|
+
localWorkspaceSource: execution.workspace?.source ?? null,
|
|
369
403
|
},
|
|
370
404
|
error: null,
|
|
371
405
|
clientName: String(args.clientName ?? "").trim() || config.clientName,
|
|
@@ -520,22 +554,35 @@ async function continueProjectLoop(args = {}) {
|
|
|
520
554
|
typeof args.ideaSnapshot === "object" && args.ideaSnapshot !== null && !Array.isArray(args.ideaSnapshot)
|
|
521
555
|
? args.ideaSnapshot
|
|
522
556
|
: null;
|
|
523
|
-
const runnerTarget = config.modeTargets.codex === "claude_code" ? "claude_code" :
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
557
|
+
const runnerTarget = config.modeTargets.codex === "claude_code" ? "claude_code" : "codex";
|
|
558
|
+
let runnerWorkspaceContext = await getRunnerWorkspaceContext(projectId);
|
|
559
|
+
let agentSetup = null;
|
|
560
|
+
const refreshAgentSetup = async () => {
|
|
561
|
+
runnerWorkspaceContext = await getRunnerWorkspaceContext(projectId);
|
|
562
|
+
const resolvedAgentWorkspace = resolveWorkspaceForRun(
|
|
563
|
+
{ projectId, promptMode: "codex", runnerWorkspace: runnerWorkspaceContext },
|
|
564
|
+
runnerWorkspaceContext,
|
|
565
|
+
{ preferRepository: true },
|
|
566
|
+
);
|
|
567
|
+
agentSetup = {
|
|
568
|
+
...(typeof args.agentSetup === "object" && args.agentSetup !== null && !Array.isArray(args.agentSetup)
|
|
569
|
+
? args.agentSetup
|
|
570
|
+
: {}),
|
|
571
|
+
localRepoPath: resolvedAgentWorkspace.path,
|
|
572
|
+
runnerTarget,
|
|
573
|
+
clientType: config.clientType,
|
|
574
|
+
repositoryVerified: resolvedAgentWorkspace.repositoryVerified,
|
|
575
|
+
repositoryUrl: resolvedAgentWorkspace.repositoryUrl,
|
|
576
|
+
workspaceSource: resolvedAgentWorkspace.source,
|
|
577
|
+
};
|
|
533
578
|
};
|
|
579
|
+
await refreshAgentSetup();
|
|
534
580
|
let pendingChoiceSelections = normalizeChoiceSelections(args);
|
|
535
581
|
const steps = [];
|
|
536
582
|
let final = null;
|
|
537
583
|
|
|
538
584
|
for (let index = 0; index < maxSteps; index += 1) {
|
|
585
|
+
if (index > 0) await refreshAgentSetup();
|
|
539
586
|
const loop = await api(`/api/agent/v1/projects/${encodeURIComponent(projectId)}/loop/continue`, {
|
|
540
587
|
method: "POST",
|
|
541
588
|
body: {
|
|
@@ -553,7 +600,9 @@ async function continueProjectLoop(args = {}) {
|
|
|
553
600
|
break;
|
|
554
601
|
}
|
|
555
602
|
|
|
556
|
-
const run = loop.run
|
|
603
|
+
const run = loop.run
|
|
604
|
+
? { ...loop.run, runnerWorkspace: loop.runnerWorkspace ?? runnerWorkspaceContext }
|
|
605
|
+
: null;
|
|
557
606
|
if (!run?.id || !run?.prompt) {
|
|
558
607
|
final = loop;
|
|
559
608
|
break;
|
|
@@ -578,6 +627,7 @@ async function continueProjectLoop(args = {}) {
|
|
|
578
627
|
attemptNumber: run.attemptNumber ?? 1,
|
|
579
628
|
maxAttempts: run.maxAttempts ?? 1,
|
|
580
629
|
projectLoopStep: index + 1,
|
|
630
|
+
localWorkspaceSource: execution.workspace?.source ?? null,
|
|
581
631
|
},
|
|
582
632
|
error: null,
|
|
583
633
|
clientName: String(args.clientName ?? "").trim() || config.clientName,
|
|
@@ -1096,6 +1146,9 @@ async function handleRpc(request) {
|
|
|
1096
1146
|
}
|
|
1097
1147
|
writeError(id, -32601, `Unknown tool: ${toolName}`);
|
|
1098
1148
|
} catch (error) {
|
|
1149
|
+
const errorPayload = error?.payload && typeof error.payload === "object" && !Array.isArray(error.payload)
|
|
1150
|
+
? error.payload
|
|
1151
|
+
: {};
|
|
1099
1152
|
writeResult(id, {
|
|
1100
1153
|
content: [
|
|
1101
1154
|
{
|
|
@@ -1103,6 +1156,11 @@ async function handleRpc(request) {
|
|
|
1103
1156
|
text: JSON.stringify(
|
|
1104
1157
|
{
|
|
1105
1158
|
error: error instanceof Error ? error.message : String(error ?? "Tool failed"),
|
|
1159
|
+
...errorPayload,
|
|
1160
|
+
...(Number.isFinite(error?.status) ? { httpStatus: error.status } : {}),
|
|
1161
|
+
...(Number.isFinite(error?.retryAfterSeconds)
|
|
1162
|
+
? { retryAfterSeconds: error.retryAfterSeconds }
|
|
1163
|
+
: {}),
|
|
1106
1164
|
},
|
|
1107
1165
|
null,
|
|
1108
1166
|
2,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchchair/agent",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.12",
|
|
4
4
|
"mcpName": "io.launchchair/agent",
|
|
5
5
|
"description": "LaunchChair Agent CLI, MCP bridge, local bridge, and runners for Codex, Claude Code, ChatGPT, Claude, and Hermes workflows.",
|
|
6
6
|
"type": "module",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"launchchair-cli.mjs",
|
|
16
16
|
"local-bridge.mjs",
|
|
17
17
|
"mcp-bridge-server.mjs",
|
|
18
|
+
"project-workspace.mjs",
|
|
18
19
|
"runner-command.mjs",
|
|
19
20
|
"codex-runner.mjs",
|
|
20
21
|
"claude-code-runner.mjs",
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
renameSync,
|
|
7
|
+
statSync,
|
|
8
|
+
unlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
|
|
14
|
+
function asRecord(value) {
|
|
15
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function text(value) {
|
|
19
|
+
return typeof value === "string" ? value.trim() : "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readConfig(configPath) {
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(readFileSync(configPath, "utf8"));
|
|
25
|
+
return asRecord(parsed);
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeConfig(configPath, next) {
|
|
32
|
+
const directory = path.dirname(configPath);
|
|
33
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
34
|
+
const temporary = path.join(directory, `.${path.basename(configPath)}.${process.pid}.${Date.now()}.tmp`);
|
|
35
|
+
try {
|
|
36
|
+
writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
|
|
37
|
+
renameSync(temporary, configPath);
|
|
38
|
+
try {
|
|
39
|
+
chmodSync(configPath, 0o600);
|
|
40
|
+
} catch {
|
|
41
|
+
// Best effort for filesystems without POSIX permissions.
|
|
42
|
+
}
|
|
43
|
+
} finally {
|
|
44
|
+
if (existsSync(temporary)) unlinkSync(temporary);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeProjectKey(projectId) {
|
|
49
|
+
const value = text(projectId);
|
|
50
|
+
if (!value) throw new Error("LaunchChair projectId is required to resolve a local workspace.");
|
|
51
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function normalizeGitRemote(value) {
|
|
55
|
+
let remote = text(value);
|
|
56
|
+
if (!remote) return "";
|
|
57
|
+
if (remote.startsWith("git@")) {
|
|
58
|
+
const match = remote.match(/^git@([^:]+):(.+)$/);
|
|
59
|
+
if (match) remote = `https://${match[1]}/${match[2]}`;
|
|
60
|
+
} else if (remote.startsWith("ssh://git@")) {
|
|
61
|
+
remote = remote.replace(/^ssh:\/\/git@/, "https://");
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const parsed = new URL(remote);
|
|
65
|
+
const pathname = parsed.pathname.replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "");
|
|
66
|
+
return `${parsed.hostname.toLowerCase()}/${pathname.toLowerCase()}`;
|
|
67
|
+
} catch {
|
|
68
|
+
return remote.replace(/\.git$/i, "").replace(/\/+$/, "").toLowerCase();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function repositoryNameFromUrl(value) {
|
|
73
|
+
const normalized = normalizeGitRemote(value);
|
|
74
|
+
return normalized.split("/").filter(Boolean).at(-1) ?? "";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function gitDirectory(repoPath) {
|
|
78
|
+
const dotGit = path.join(repoPath, ".git");
|
|
79
|
+
if (!existsSync(dotGit)) return "";
|
|
80
|
+
try {
|
|
81
|
+
if (statSync(dotGit).isDirectory()) return dotGit;
|
|
82
|
+
const pointer = readFileSync(dotGit, "utf8").match(/^gitdir:\s*(.+)$/im)?.[1]?.trim();
|
|
83
|
+
if (!pointer) return "";
|
|
84
|
+
return path.resolve(repoPath, pointer);
|
|
85
|
+
} catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function readGitOrigin(repoPath) {
|
|
91
|
+
const gitDir = gitDirectory(repoPath);
|
|
92
|
+
if (!gitDir) return "";
|
|
93
|
+
try {
|
|
94
|
+
const raw = readFileSync(path.join(gitDir, "config"), "utf8");
|
|
95
|
+
const remoteBlock = raw.match(/\[remote\s+"origin"\]([\s\S]*?)(?=\n\[|$)/i)?.[1] ?? "";
|
|
96
|
+
return remoteBlock.match(/^\s*url\s*=\s*(.+)$/im)?.[1]?.trim() ?? "";
|
|
97
|
+
} catch {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function projectBindings(config) {
|
|
103
|
+
return asRecord(config.projectWorkspaces);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function bindingPath(config, projectId) {
|
|
107
|
+
const value = projectBindings(config)[projectId];
|
|
108
|
+
if (typeof value === "string") return text(value);
|
|
109
|
+
return text(asRecord(value).path);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function repositoryMatches(repoPath, expectedRepositoryUrl) {
|
|
113
|
+
const origin = readGitOrigin(repoPath);
|
|
114
|
+
if (!origin) return false;
|
|
115
|
+
const expected = normalizeGitRemote(expectedRepositoryUrl);
|
|
116
|
+
return !expected || normalizeGitRemote(origin) === expected;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function standardSearchRoots(config, legacyPaths = [], extraRoots = []) {
|
|
120
|
+
const home = os.homedir();
|
|
121
|
+
const configuredRoots = Array.isArray(config.repoSearchRoots) ? config.repoSearchRoots.map(text) : [];
|
|
122
|
+
const bindingParents = Object.values(projectBindings(config))
|
|
123
|
+
.map((value) => (typeof value === "string" ? value : asRecord(value).path))
|
|
124
|
+
.map(text)
|
|
125
|
+
.filter(Boolean)
|
|
126
|
+
.map((value) => path.dirname(path.resolve(value)));
|
|
127
|
+
const legacyParents = legacyPaths.map(text).filter(Boolean).map((value) => path.dirname(path.resolve(value)));
|
|
128
|
+
const legacyRoots = legacyPaths.map(text).filter(Boolean).map((value) => path.resolve(value));
|
|
129
|
+
return Array.from(
|
|
130
|
+
new Set(
|
|
131
|
+
[
|
|
132
|
+
...extraRoots,
|
|
133
|
+
...configuredRoots,
|
|
134
|
+
...bindingParents,
|
|
135
|
+
...legacyRoots,
|
|
136
|
+
...legacyParents,
|
|
137
|
+
home ? path.join(home, "Documents", "GitHub") : "",
|
|
138
|
+
home ? path.join(home, "Developer") : "",
|
|
139
|
+
home ? path.join(home, "Projects") : "",
|
|
140
|
+
home ? path.join(home, "repos") : "",
|
|
141
|
+
]
|
|
142
|
+
.map(text)
|
|
143
|
+
.filter(Boolean)
|
|
144
|
+
.map((value) => path.resolve(value)),
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function saveBinding(configPath, projectId, repoPath, repositoryUrl, source) {
|
|
150
|
+
const config = readConfig(configPath);
|
|
151
|
+
const bindings = projectBindings(config);
|
|
152
|
+
const next = {
|
|
153
|
+
...config,
|
|
154
|
+
projectWorkspaces: {
|
|
155
|
+
...bindings,
|
|
156
|
+
[projectId]: {
|
|
157
|
+
path: path.resolve(repoPath),
|
|
158
|
+
repositoryUrl: text(repositoryUrl) || text(asRecord(bindings[projectId]).repositoryUrl) || undefined,
|
|
159
|
+
source,
|
|
160
|
+
updatedAt: new Date().toISOString(),
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
writeConfig(configPath, next);
|
|
165
|
+
return next.projectWorkspaces[projectId];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function bindProjectWorkspace({ configPath, projectId, cwd, repositoryUrl = "", source = "explicit" }) {
|
|
169
|
+
const id = text(projectId);
|
|
170
|
+
safeProjectKey(id);
|
|
171
|
+
const resolved = path.resolve(text(cwd));
|
|
172
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
173
|
+
throw new Error(`Local project directory does not exist: ${resolved}`);
|
|
174
|
+
}
|
|
175
|
+
const expected = text(repositoryUrl);
|
|
176
|
+
const origin = readGitOrigin(resolved);
|
|
177
|
+
if (expected && !origin) {
|
|
178
|
+
throw new Error(`The selected directory is not a Git clone. Expected ${expected}.`);
|
|
179
|
+
}
|
|
180
|
+
if (expected && normalizeGitRemote(origin) !== normalizeGitRemote(expected)) {
|
|
181
|
+
throw new Error(`Git origin mismatch. Expected ${expected}, found ${origin || "no origin"}.`);
|
|
182
|
+
}
|
|
183
|
+
return saveBinding(configPath, id, resolved, expected || origin, source);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function unbindProjectWorkspace({ configPath, projectId }) {
|
|
187
|
+
const id = text(projectId);
|
|
188
|
+
safeProjectKey(id);
|
|
189
|
+
const config = readConfig(configPath);
|
|
190
|
+
const bindings = { ...projectBindings(config) };
|
|
191
|
+
const existed = Object.prototype.hasOwnProperty.call(bindings, id);
|
|
192
|
+
delete bindings[id];
|
|
193
|
+
writeConfig(configPath, { ...config, projectWorkspaces: bindings });
|
|
194
|
+
return existed;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function listProjectWorkspaceBindings(configPath) {
|
|
198
|
+
return projectBindings(readConfig(configPath));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function isolatedProjectWorkspace(configDir, projectId) {
|
|
202
|
+
const key = safeProjectKey(projectId);
|
|
203
|
+
const target = path.join(configDir, "projects", key, "workspace");
|
|
204
|
+
mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
205
|
+
const instructionsPath = path.join(target, "AGENTS.md");
|
|
206
|
+
if (!existsSync(instructionsPath)) {
|
|
207
|
+
writeFileSync(
|
|
208
|
+
instructionsPath,
|
|
209
|
+
[
|
|
210
|
+
"# LaunchChair isolated project workspace",
|
|
211
|
+
"",
|
|
212
|
+
`This folder belongs only to LaunchChair project ${projectId}.`,
|
|
213
|
+
"Use the LaunchChair prompt and MCP/API context for pre-code work.",
|
|
214
|
+
"Do not inspect, edit, or execute against unrelated repositories.",
|
|
215
|
+
"Before code-changing work, bind a verified local Git clone with `launchchair bind --project-id <id> --cwd <path>`.",
|
|
216
|
+
"",
|
|
217
|
+
].join("\n"),
|
|
218
|
+
{ mode: 0o600 },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
return target;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function resolveProjectWorkspace({
|
|
225
|
+
configDir,
|
|
226
|
+
configPath,
|
|
227
|
+
projectId,
|
|
228
|
+
repository = {},
|
|
229
|
+
requireRepository = false,
|
|
230
|
+
preferRepository = false,
|
|
231
|
+
legacyPaths = [],
|
|
232
|
+
searchRoots = [],
|
|
233
|
+
}) {
|
|
234
|
+
const id = text(projectId);
|
|
235
|
+
safeProjectKey(id);
|
|
236
|
+
const config = readConfig(configPath);
|
|
237
|
+
const repo = asRecord(repository);
|
|
238
|
+
const repositoryUrl = text(repo.repoUrl) || text(repo.cloneUrl);
|
|
239
|
+
const cloneUrl = text(repo.cloneUrl) || repositoryUrl;
|
|
240
|
+
const repoName = text(repo.repoName) || repositoryNameFromUrl(repositoryUrl);
|
|
241
|
+
const boundPath = bindingPath(config, id);
|
|
242
|
+
const scratchPath = isolatedProjectWorkspace(configDir, id);
|
|
243
|
+
|
|
244
|
+
if (!requireRepository && !preferRepository) {
|
|
245
|
+
return {
|
|
246
|
+
path: scratchPath,
|
|
247
|
+
source: "isolated",
|
|
248
|
+
repositoryVerified: false,
|
|
249
|
+
repositoryUrl,
|
|
250
|
+
cloneUrl,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!repositoryUrl) {
|
|
255
|
+
if (requireRepository) {
|
|
256
|
+
throw new Error(
|
|
257
|
+
`LaunchChair project ${id} does not have repository metadata yet. Finish Stack Setup before running code-changing work.`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
path: scratchPath,
|
|
262
|
+
source: "isolated",
|
|
263
|
+
repositoryVerified: false,
|
|
264
|
+
repositoryUrl: "",
|
|
265
|
+
cloneUrl: "",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const candidates = [boundPath, scratchPath, ...legacyPaths.map(text).filter(Boolean)];
|
|
270
|
+
|
|
271
|
+
if (repoName) {
|
|
272
|
+
for (const root of standardSearchRoots(config, legacyPaths, searchRoots)) {
|
|
273
|
+
candidates.push(path.join(root, repoName));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (const candidate of Array.from(new Set(candidates.filter(Boolean).map((value) => path.resolve(value))))) {
|
|
278
|
+
if (!existsSync(candidate)) continue;
|
|
279
|
+
if (!repositoryMatches(candidate, repositoryUrl)) continue;
|
|
280
|
+
const source = candidate === boundPath ? "binding" : "auto_detected";
|
|
281
|
+
if (candidate !== boundPath) saveBinding(configPath, id, candidate, repositoryUrl || readGitOrigin(candidate), source);
|
|
282
|
+
return {
|
|
283
|
+
path: candidate,
|
|
284
|
+
source,
|
|
285
|
+
repositoryVerified: true,
|
|
286
|
+
repositoryUrl: repositoryUrl || readGitOrigin(candidate),
|
|
287
|
+
cloneUrl,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (requireRepository) {
|
|
292
|
+
const expected = repositoryUrl ? ` for ${repositoryUrl}` : "";
|
|
293
|
+
const cloneStep = cloneUrl && repoName ? ` Clone it with: git clone ${cloneUrl} \"${repoName}\".` : "";
|
|
294
|
+
throw new Error(
|
|
295
|
+
`No verified local Git clone is bound to LaunchChair project ${id}${expected}.${cloneStep} Then run: launchchair bind --project-id ${id} --cwd \"/absolute/path/to/${repoName || "repo"}\"`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
path: scratchPath,
|
|
301
|
+
source: "isolated",
|
|
302
|
+
repositoryVerified: false,
|
|
303
|
+
repositoryUrl,
|
|
304
|
+
cloneUrl,
|
|
305
|
+
};
|
|
306
|
+
}
|
package/server.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"name": "io.launchchair/agent",
|
|
4
4
|
"title": "LaunchChair",
|
|
5
5
|
"description": "Plan, validate, and build products and features with LaunchChair.",
|
|
6
|
-
"version": "0.4.
|
|
6
|
+
"version": "0.4.12",
|
|
7
7
|
"websiteUrl": "https://www.launchchair.io/docs/mcp",
|
|
8
8
|
"packages": [
|
|
9
9
|
{
|
|
10
10
|
"registryType": "npm",
|
|
11
11
|
"identifier": "@launchchair/agent",
|
|
12
|
-
"version": "0.4.
|
|
12
|
+
"version": "0.4.12",
|
|
13
13
|
"runtimeHint": "npx",
|
|
14
14
|
"runtimeArguments": [
|
|
15
15
|
{
|