@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 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.132",
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.132",
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 { claudeSetupStatus, ensureClaudeSetup, isPermissionMode, permissionMode, setPermissionMode, setPrivacy, type PermissionMode, type SetupDeps } from './setup.js';
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
- if (body.privacy !== undefined && typeof body.privacy !== 'boolean') throw new ApiError('privacy must be true or false', 400);
151
- if (body.permissionMode !== undefined && !isPermissionMode(body.permissionMode)) throw new ApiError('permissionMode must be auto or bypass', 400);
152
- if (body.privacy === undefined && body.permissionMode === undefined) throw new ApiError('nothing to change', 400);
153
- return { ...(typeof body.privacy === 'boolean' ? { privacy: body.privacy } : {}), ...(isPermissionMode(body.permissionMode) ? { permissionMode: body.permissionMode } : {}) };
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.permissionMode !== undefined && change.permissionMode !== permissionMode(setup.agents)) {
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)),
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.132"
2
+ "version": "0.1.0-beta.133"
3
3
  }