@stage-labs/metro 0.1.0-beta.132 → 0.1.0-beta.133
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/dist/claude.js +8 -4
- package/dist/route.js +12 -0
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +58 -10
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +25 -1
- package/runtime/runtime.json +1 -1
package/dist/claude.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn, spawnSync } from 'node:child_process';
|
|
2
2
|
import { markOnboardingDone, seedChannels } from './onboarding.js';
|
|
3
3
|
import { writeMcpConfig } from './mcp-config.js';
|
|
4
|
-
import { currentRoute, permissionMode, routeModelEnv } from './route.js';
|
|
4
|
+
import { currentRoute, permissionMode, routeModelEnv, systemPrompt } from './route.js';
|
|
5
5
|
import { settingsConflicts, settingsFiles } from './claude-settings.js';
|
|
6
6
|
import { localAgents, pickLocalAgent } from './local.js';
|
|
7
7
|
import { PROVIDER_FLAGS } from './provider-flags.js';
|
|
@@ -10,11 +10,12 @@ const CHANNEL_FLAGS = ['--dangerously-load-development-channels', 'server:metro'
|
|
|
10
10
|
const PERMISSION_MODE_FLAG = { auto: 'auto', bypass: 'bypassPermissions' };
|
|
11
11
|
const KEY_HEADER = 'x-metro-key';
|
|
12
12
|
const PROBE_MS = 3_000;
|
|
13
|
-
export const claudeArgs = (extra, mcpConfig, mode = 'auto') => [
|
|
13
|
+
export const claudeArgs = (extra, mcpConfig, mode = 'auto', prompt = null) => [
|
|
14
14
|
...CHANNEL_FLAGS,
|
|
15
15
|
'--permission-mode',
|
|
16
16
|
PERMISSION_MODE_FLAG[mode],
|
|
17
17
|
...(mcpConfig === undefined ? [] : ['--mcp-config', mcpConfig]),
|
|
18
|
+
...(prompt === null ? [] : ['--append-system-prompt', prompt]),
|
|
18
19
|
...extra,
|
|
19
20
|
];
|
|
20
21
|
const set = (env, name) => (env[name] ?? '').trim() !== '';
|
|
@@ -157,6 +158,9 @@ export async function launchClaude(extra) {
|
|
|
157
158
|
const port = localPort();
|
|
158
159
|
const mcp = mcpConfigFor(await servedKey(decision), port);
|
|
159
160
|
const mode = permissionMode();
|
|
161
|
+
const prompt = systemPrompt();
|
|
162
|
+
if (prompt !== null)
|
|
163
|
+
process.stderr.write('metro claude: the system prompt from the Harness page is appended to this session\n');
|
|
160
164
|
const sandbox = (env) => {
|
|
161
165
|
const out = bypassSandboxEnv(env, mode, process.getuid?.());
|
|
162
166
|
if (out !== env)
|
|
@@ -166,9 +170,9 @@ export async function launchClaude(extra) {
|
|
|
166
170
|
try {
|
|
167
171
|
if ('skip' in decision) {
|
|
168
172
|
process.stderr.write(`metro claude: ${decision.skip}\n`);
|
|
169
|
-
return await runClaude(claudeArgs(extra, mcp?.path, mode), sandbox(process.env));
|
|
173
|
+
return await runClaude(claudeArgs(extra, mcp?.path, mode, prompt), sandbox(process.env));
|
|
170
174
|
}
|
|
171
|
-
return await runClaude(claudeArgs(extra, mcp?.path, mode), sandbox(gatewayLaunchEnv(decision.key, port)));
|
|
175
|
+
return await runClaude(claudeArgs(extra, mcp?.path, mode, prompt), sandbox(gatewayLaunchEnv(decision.key, port)));
|
|
172
176
|
}
|
|
173
177
|
finally {
|
|
174
178
|
mcp?.cleanup();
|
package/dist/route.js
CHANGED
|
@@ -36,3 +36,15 @@ export function permissionMode(dir = agentsDir()) {
|
|
|
36
36
|
return 'auto';
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
export function systemPrompt(dir = agentsDir()) {
|
|
40
|
+
const path = join(dir, 'system-prompt.md');
|
|
41
|
+
if (!existsSync(path))
|
|
42
|
+
return null;
|
|
43
|
+
try {
|
|
44
|
+
const text = readFileSync(path, 'utf8').trim();
|
|
45
|
+
return text === '' ? null : text;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stage-labs/metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.133",
|
|
4
4
|
"description": "The metro command line. Sign in once per machine, then hand your MCP connector list to Claude Code without the credentials touching disk, argv or shell history.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.133",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, the standing rules loaded at each session start, and a refresh skill for when the daemon could not keep the server list current. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -21,7 +21,19 @@ import {
|
|
|
21
21
|
startClaudeLogin,
|
|
22
22
|
type LoginDeps,
|
|
23
23
|
} from './login.js';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
claudeSetupStatus,
|
|
26
|
+
ensureClaudeSetup,
|
|
27
|
+
isPermissionMode,
|
|
28
|
+
permissionMode,
|
|
29
|
+
setPermissionMode,
|
|
30
|
+
setPrivacy,
|
|
31
|
+
setSystemPrompt,
|
|
32
|
+
systemPrompt,
|
|
33
|
+
SYSTEM_PROMPT_MAX,
|
|
34
|
+
type PermissionMode,
|
|
35
|
+
type SetupDeps,
|
|
36
|
+
} from './setup.js';
|
|
25
37
|
import {
|
|
26
38
|
ensureSession,
|
|
27
39
|
sessionRunning,
|
|
@@ -138,28 +150,64 @@ async function created(req: IncomingMessage, path: string, dir: string): Promise
|
|
|
138
150
|
const LOGIN = 'login';
|
|
139
151
|
const SESSION = 'session';
|
|
140
152
|
const SETUP = 'setup';
|
|
153
|
+
const SETUP_BODY_MAX = SYSTEM_PROMPT_MAX * 2;
|
|
141
154
|
const VERSION = 'version';
|
|
142
155
|
|
|
143
156
|
interface SetupChange {
|
|
144
157
|
privacy?: boolean;
|
|
145
158
|
permissionMode?: PermissionMode;
|
|
159
|
+
systemPrompt?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function promptChange(raw: unknown): string | undefined {
|
|
163
|
+
if (raw === undefined) return undefined;
|
|
164
|
+
if (typeof raw !== 'string') throw new ApiError('systemPrompt must be text', 400);
|
|
165
|
+
if (Buffer.byteLength(raw) > SYSTEM_PROMPT_MAX) throw new ApiError(`systemPrompt must be under ${String(SYSTEM_PROMPT_MAX / 1024)} KiB`, 400);
|
|
166
|
+
return raw;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function privacyChange(raw: unknown): boolean | undefined {
|
|
170
|
+
if (raw === undefined) return undefined;
|
|
171
|
+
if (typeof raw !== 'boolean') throw new ApiError('privacy must be true or false', 400);
|
|
172
|
+
return raw;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function modeChange(raw: unknown): PermissionMode | undefined {
|
|
176
|
+
if (raw === undefined) return undefined;
|
|
177
|
+
if (!isPermissionMode(raw)) throw new ApiError('permissionMode must be auto or bypass', 400);
|
|
178
|
+
return raw;
|
|
146
179
|
}
|
|
147
180
|
|
|
148
181
|
function setupChange(body: unknown): SetupChange {
|
|
149
182
|
if (!isRecord(body)) throw new ApiError('a body is required', 400);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
183
|
+
const privacy = privacyChange(body.privacy);
|
|
184
|
+
const permissionMode = modeChange(body.permissionMode);
|
|
185
|
+
const systemPrompt = promptChange(body.systemPrompt);
|
|
186
|
+
if (privacy === undefined && permissionMode === undefined && systemPrompt === undefined) throw new ApiError('nothing to change', 400);
|
|
187
|
+
return {
|
|
188
|
+
...(privacy === undefined ? {} : { privacy }),
|
|
189
|
+
...(permissionMode === undefined ? {} : { permissionMode }),
|
|
190
|
+
...(systemPrompt === undefined ? {} : { systemPrompt }),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function restartsSession(change: SetupChange, agents: string | undefined): boolean {
|
|
195
|
+
let restart = false;
|
|
196
|
+
if (change.permissionMode !== undefined && change.permissionMode !== permissionMode(agents)) {
|
|
197
|
+
setPermissionMode(change.permissionMode, agents);
|
|
198
|
+
restart = true;
|
|
199
|
+
}
|
|
200
|
+
if (change.systemPrompt !== undefined && change.systemPrompt.trim() !== systemPrompt(agents)) {
|
|
201
|
+
setSystemPrompt(change.systemPrompt, agents);
|
|
202
|
+
restart = true;
|
|
203
|
+
}
|
|
204
|
+
return restart;
|
|
154
205
|
}
|
|
155
206
|
|
|
156
207
|
function applySetupChange(change: SetupChange, deps: ClaudeApiDeps): void {
|
|
157
208
|
const setup = deps.setup ?? {};
|
|
158
209
|
if (change.privacy !== undefined) setPrivacy(change.privacy, setup.agents);
|
|
159
|
-
if (change.
|
|
160
|
-
setPermissionMode(change.permissionMode, setup.agents);
|
|
161
|
-
if (sessionRunning(deps.session?.tmux ?? 'tmux')) stopSession(deps.session ?? {});
|
|
162
|
-
}
|
|
210
|
+
if (restartsSession(change, setup.agents) && sessionRunning(deps.session?.tmux ?? 'tmux')) stopSession(deps.session ?? {});
|
|
163
211
|
ensureClaudeSetup(setup);
|
|
164
212
|
}
|
|
165
213
|
|
|
@@ -168,7 +216,7 @@ async function setupAnswer(req: IncomingMessage, deps: ClaudeApiDeps): Promise<u
|
|
|
168
216
|
const method = req.method ?? 'GET';
|
|
169
217
|
if (method === 'GET') return claudeSetupStatus(setup);
|
|
170
218
|
if (method !== 'POST') throw new ApiError('method not allowed', 405);
|
|
171
|
-
applySetupChange(setupChange(await readJsonBody(req)), deps);
|
|
219
|
+
applySetupChange(setupChange(await readJsonBody(req, SETUP_BODY_MAX)), deps);
|
|
172
220
|
return claudeSetupStatus(setup);
|
|
173
221
|
}
|
|
174
222
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
@@ -19,6 +19,8 @@ export const RETENTION_DAYS = 7;
|
|
|
19
19
|
export const SKILL_NAME = 'metro-orchestrator';
|
|
20
20
|
const STATE_FILE = 'claude-setup.json';
|
|
21
21
|
const GUIDANCE = 'orchestrator.md';
|
|
22
|
+
export const SYSTEM_PROMPT_FILE = 'system-prompt.md';
|
|
23
|
+
export const SYSTEM_PROMPT_MAX = 64 * 1024;
|
|
22
24
|
|
|
23
25
|
export const WORKER_AGENT = `---
|
|
24
26
|
name: worker
|
|
@@ -56,6 +58,7 @@ export interface SetupReport {
|
|
|
56
58
|
export interface SetupStatus {
|
|
57
59
|
privacy: boolean;
|
|
58
60
|
permissionMode: PermissionMode;
|
|
61
|
+
systemPrompt: string;
|
|
59
62
|
guard: 'plugin';
|
|
60
63
|
worker: boolean;
|
|
61
64
|
skill: boolean;
|
|
@@ -95,6 +98,26 @@ export function setPermissionMode(mode: PermissionMode, agents = agentsDir()): v
|
|
|
95
98
|
writeState(agents, { permissionMode: mode });
|
|
96
99
|
}
|
|
97
100
|
|
|
101
|
+
const promptPath = (agents: string): string => join(agents, SYSTEM_PROMPT_FILE);
|
|
102
|
+
|
|
103
|
+
export function systemPrompt(agents = agentsDir()): string {
|
|
104
|
+
const path = promptPath(agents);
|
|
105
|
+
if (!existsSync(path)) return '';
|
|
106
|
+
return readFileSync(path, 'utf8').trim();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function setSystemPrompt(text: string, agents = agentsDir()): void {
|
|
110
|
+
const trimmed = text.trim();
|
|
111
|
+
if (trimmed === '') {
|
|
112
|
+
rmSync(promptPath(agents), { force: true });
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
mkdirSync(agents, { recursive: true });
|
|
116
|
+
const tmp = `${promptPath(agents)}.${String(process.pid)}.tmp`;
|
|
117
|
+
writeFileSync(tmp, `${trimmed}\n`, { mode: 0o600 });
|
|
118
|
+
renameSync(tmp, promptPath(agents));
|
|
119
|
+
}
|
|
120
|
+
|
|
98
121
|
export function guidancePath(env: NodeJS.ProcessEnv = process.env): string {
|
|
99
122
|
const staged = stagedMarketplaceDir(env);
|
|
100
123
|
if (staged !== null) {
|
|
@@ -178,6 +201,7 @@ export function claudeSetupStatus(deps: SetupDeps = {}): SetupStatus {
|
|
|
178
201
|
return {
|
|
179
202
|
privacy: privacyEnabled(deps.agents ?? agentsDir()),
|
|
180
203
|
permissionMode: permissionMode(deps.agents ?? agentsDir()),
|
|
204
|
+
systemPrompt: systemPrompt(deps.agents ?? agentsDir()),
|
|
181
205
|
guard: 'plugin',
|
|
182
206
|
worker: existsSync(workerPath(dir)),
|
|
183
207
|
skill: existsSync(skillPath(dir)),
|
package/runtime/runtime.json
CHANGED