@stage-labs/metro 0.1.0-beta.201 → 0.1.0-beta.202
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 +4 -15
- package/dist/cli.js +5 -3
- package/dist/hold.js +1 -2
- package/dist/runtime-install.js +4 -5
- package/dist/runtime.js +1 -4
- package/dist/serve.js +4 -5
- package/dist/service.js +82 -21
- package/dist/update.js +1 -1
- package/package.json +2 -2
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/events.ts +4 -2
- package/runtime/node_modules/@metro-labs/core/src/lines.ts +12 -25
- package/runtime/node_modules/@metro-labs/core/src/stations/account-store.ts +45 -22
- package/runtime/node_modules/@metro-labs/core/src/stations/attachments.ts +0 -1
- package/runtime/node_modules/@metro-labs/core/src/stations/messaging-normalize.ts +8 -80
- package/runtime/node_modules/@metro-labs/core/src/stations/station-runtime.ts +3 -5
- package/runtime/node_modules/@metro-labs/core/src/stations/train-events.ts +4 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/files.ts +9 -39
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/job-env.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/provision.ts +9 -28
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules-api.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedules.ts +8 -116
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/unit-files.ts +11 -35
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/user.ts +7 -9
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/view.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/account-lookups.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/accounts-api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/allowlist.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/agents/bundle.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/agents/file-admin.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/agents/files.ts +3 -29
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/pending.ts +5 -8
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/preview.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/boot/boot.ts +5 -8
- package/runtime/node_modules/@metro-labs/daemon/src/boot/connect-hint.ts +3 -6
- package/runtime/node_modules/@metro-labs/daemon/src/channels/inbound.ts +25 -3
- package/runtime/node_modules/@metro-labs/daemon/src/channels/media-note.ts +2 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/files.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/login.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/memory.ts +6 -56
- package/runtime/node_modules/@metro-labs/daemon/src/claude/plugin-install.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session-files.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +7 -6
- package/runtime/node_modules/@metro-labs/daemon/src/claude/settings.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +7 -7
- package/runtime/node_modules/@metro-labs/daemon/src/claude/skills.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/claude/version.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/api.ts +2 -4
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/config.ts +19 -22
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/gates.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-discovery.ts +6 -8
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/oauth-routes.ts +3 -6
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/plugin-sync.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay.ts +2 -3
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/store.ts +28 -1
- package/runtime/node_modules/@metro-labs/daemon/src/files/attach-serve.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/bedrock.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-auth.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-device.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-stream.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex-translate.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/codex.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/effort.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/eventstream.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/forward.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/frames.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gateway.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-auth.ts +6 -6
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-client.ts +7 -7
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-stream.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini-translate.ts +3 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/gemini.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-config.ts +10 -31
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-signin.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/model-store.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/openrouter.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/provider-models.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/text.ts +1 -2
- package/runtime/node_modules/@metro-labs/daemon/src/gateway/usage.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +5 -6
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-snapshot.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/profile-lookup.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/read-tool.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/helper-script.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/install.ts +37 -0
- package/runtime/node_modules/@metro-labs/daemon/src/net/tunnel.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/policy/policy.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/routes/local-mode.ts +1 -4
- package/runtime/node_modules/@metro-labs/daemon/src/routes/session-apis.ts +3 -8
- package/runtime/node_modules/@metro-labs/daemon/src/server/machine.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/server/update.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-session.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/stations/attach-xmtp.ts +6 -7
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor-io.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/stations/supervisor.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/api.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/terminal/socket.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/vault/firewall.ts +5 -44
- package/runtime/node_modules/@metro-labs/daemon/src/vault/index.ts +4 -4
- package/runtime/node_modules/@metro-labs/daemon/src/vault/proxy.ts +6 -7
- package/runtime/node_modules/@metro-labs/daemon/src/vault/store.ts +12 -13
- package/runtime/node_modules/@metro-labs/daemon/src/vault/trust.ts +5 -22
- package/runtime/node_modules/@metro-labs/discord-bot/src/accounts.ts +1 -19
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +7 -35
- package/runtime/node_modules/@metro-labs/discord-bot/src/format.ts +5 -10
- package/runtime/node_modules/@metro-labs/discord-bot/src/group-actions.ts +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/index.ts +1 -2
- package/runtime/node_modules/@metro-labs/discord-bot/src/profile.ts +1 -1
- package/runtime/node_modules/@metro-labs/discord-bot/src/send-files.ts +11 -12
- package/runtime/node_modules/@metro-labs/http/src/api-http.ts +2 -5
- package/runtime/node_modules/@metro-labs/http/src/mode-api.ts +0 -1
- package/runtime/node_modules/@metro-labs/outlook/src/accounts.ts +1 -15
- package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +0 -6
- package/runtime/node_modules/@metro-labs/outlook/src/format.ts +0 -1
- package/runtime/node_modules/@metro-labs/outlook/src/state.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/accounts.ts +4 -22
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +2 -2
- package/runtime/node_modules/@metro-labs/telegram/src/format.ts +1 -3
- package/runtime/node_modules/@metro-labs/telegram/src/inbound.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram/src/media-actions.ts +1 -2
- package/runtime/node_modules/@metro-labs/telegram/src/reactions.ts +1 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/accounts.ts +4 -37
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +7 -23
- package/runtime/node_modules/@metro-labs/telegram-bot/src/format.ts +1 -4
- package/runtime/node_modules/@metro-labs/telegram-bot/src/media-actions.ts +4 -9
- package/runtime/node_modules/@metro-labs/telegram-bot/src/profile.ts +3 -3
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +0 -11
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +2 -12
- package/runtime/node_modules/@metro-labs/threema/src/files.ts +4 -15
- package/runtime/node_modules/@metro-labs/threema/src/format.ts +2 -7
- package/runtime/node_modules/@metro-labs/whatsapp/package.json +1 -2
- package/runtime/node_modules/@metro-labs/whatsapp/src/accounts.ts +5 -26
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +5 -6
- package/runtime/node_modules/@metro-labs/whatsapp/src/format.ts +1 -3
- package/runtime/node_modules/@metro-labs/whatsapp/src/inbound.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/accounts.ts +1 -12
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-close.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-conv.ts +14 -5
- package/runtime/node_modules/@metro-labs/xmtp/src/actions-meta.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +31 -78
- package/runtime/node_modules/@metro-labs/xmtp/src/emit.ts +3 -4
- package/runtime/node_modules/@metro-labs/xmtp/src/index.ts +1 -6
- package/runtime/node_modules/@metro-labs/xmtp/src/profile.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/sender.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/tools-handlers.ts +1 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/transcribe.ts +2 -2
- package/runtime/node_modules/@metro-labs/xmtp/src/wire.ts +0 -2
- package/runtime/runtime.json +1 -1
- package/runtime/node_modules/@metro-labs/core/src/identity.ts +0 -7
- package/runtime/node_modules/@metro-labs/daemon/src/approvals/needs.ts +0 -2
- package/runtime/node_modules/@metro-labs/daemon/src/channels/dedupe.ts +0 -12
- package/runtime/node_modules/@metro-labs/daemon/src/channels/react-note.ts +0 -13
- package/runtime/node_modules/@metro-labs/daemon/src/channels/reply-id.ts +0 -1
- package/runtime/node_modules/@metro-labs/daemon/src/connectors/relay-target.ts +0 -29
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/api.ts +0 -43
- package/runtime/node_modules/@metro-labs/daemon/src/metro-user/move.ts +0 -165
- package/runtime/node_modules/@metro-labs/discord-bot/src/wire.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram/src/normalize.ts +0 -22
- package/runtime/node_modules/@metro-labs/telegram/src/wire.ts +0 -1
- package/runtime/node_modules/@metro-labs/telegram-bot/src/wire.ts +0 -1
- package/runtime/node_modules/@metro-labs/whatsapp/src/normalize.ts +0 -22
- package/runtime/node_modules/@metro-labs/whatsapp/src/wire.ts +0 -1
|
@@ -13,12 +13,11 @@ export interface AgentUser {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface UserHost {
|
|
16
|
-
|
|
17
|
-
uid: number | undefined;
|
|
16
|
+
metro: boolean;
|
|
18
17
|
lookup: (name: string) => AgentUser | null;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
function lookupUser(name: string): AgentUser | null {
|
|
22
21
|
const run = spawnSync('getent', ['passwd', name], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
23
22
|
if (run.error !== undefined || run.status !== 0) return null;
|
|
24
23
|
const [, , uid = '', gid = '', , home = ''] = run.stdout.trim().split(':');
|
|
@@ -27,13 +26,13 @@ export function lookupUser(name: string): AgentUser | null {
|
|
|
27
26
|
return Number.isInteger(u) && u > 0 && Number.isInteger(g) && home.startsWith('/') ? { name, uid: u, gid: g, home } : null;
|
|
28
27
|
}
|
|
29
28
|
|
|
30
|
-
const realHost: UserHost
|
|
29
|
+
const realHost = (): UserHost => ({ metro: runningAsMetro(), lookup: lookupUser });
|
|
31
30
|
|
|
32
|
-
export const agentUserExpected = (host: UserHost = realHost): boolean => host.
|
|
31
|
+
export const agentUserExpected = (host: UserHost = realHost()): boolean => host.metro;
|
|
33
32
|
|
|
34
33
|
let cached: AgentUser | null = null;
|
|
35
34
|
|
|
36
|
-
export function agentUser(host: UserHost = realHost): AgentUser | null {
|
|
35
|
+
export function agentUser(host: UserHost = realHost()): AgentUser | null {
|
|
37
36
|
if (!agentUserExpected(host)) return null;
|
|
38
37
|
cached ??= host.lookup(AGENT_NAME);
|
|
39
38
|
return cached;
|
|
@@ -51,7 +50,7 @@ export function setAgentExtraEnv(env: Record<string, string>): void {
|
|
|
51
50
|
|
|
52
51
|
export const agentExtraEnv = (): Record<string, string> => ({ ...extraEnv });
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
function agentEnv(user: AgentUser, extra: Record<string, string> = {}): Record<string, string> {
|
|
55
54
|
return {
|
|
56
55
|
HOME: user.home,
|
|
57
56
|
USER: user.name,
|
|
@@ -68,8 +67,7 @@ export function agentEnv(user: AgentUser, extra: Record<string, string> = {}): R
|
|
|
68
67
|
export function asUser(user: AgentUser | null, file: string, args: readonly string[], extra: Record<string, string> = {}): [string, string[]] {
|
|
69
68
|
if (user === null) return [file, [...args]];
|
|
70
69
|
const env = Object.entries(agentEnv(user, extra)).map(([k, v]) => `${k}=${v}`);
|
|
71
|
-
|
|
72
|
-
return ['setpriv', [`--reuid=${String(user.uid)}`, `--regid=${String(user.gid)}`, '--init-groups', 'env', '-i', ...env, file, ...args]];
|
|
70
|
+
return ['sudo', ['-n', '-u', user.name, '--', 'env', '-i', ...env, file, ...args]];
|
|
73
71
|
}
|
|
74
72
|
|
|
75
73
|
export const asAgent = (file: string, args: readonly string[], extra: Record<string, string> = {}): [string, string[]] =>
|
|
@@ -54,7 +54,7 @@ export function viewFiles(dir = agentsDir()): Map<string, string | null> {
|
|
|
54
54
|
|
|
55
55
|
const written = new Map<string, string | null>();
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
function syncAgentView(user: AgentUser | null = agentUser(), dir = agentsDir()): number {
|
|
58
58
|
if (user === null) return 0;
|
|
59
59
|
const target = agentViewDir(user);
|
|
60
60
|
let changed = 0;
|
|
@@ -27,21 +27,21 @@ export const loadedAgentOf = (bundle: AgentBundle): LoadedAgent => ({
|
|
|
27
27
|
accounts: bundle.agent.stations,
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
interface RestoredAgent {
|
|
31
31
|
id: string;
|
|
32
32
|
name: string;
|
|
33
33
|
stations: number;
|
|
34
34
|
connectors: number;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
type ImportMode = 'append' | 'overwrite';
|
|
38
38
|
|
|
39
39
|
export interface BundleApiDeps {
|
|
40
40
|
bundle: (agentId: string) => Promise<AgentBundle>;
|
|
41
41
|
restore: (bundle: AgentBundle, mode: ImportMode) => Promise<RestoredAgent>;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
function parseMode(raw: unknown): ImportMode {
|
|
45
45
|
const mode = isRecord(raw) && typeof raw.mode === 'string' ? raw.mode : 'overwrite';
|
|
46
46
|
if (mode !== 'append' && mode !== 'overwrite') throw bad("mode is 'append' or 'overwrite'");
|
|
47
47
|
return mode;
|
|
@@ -35,7 +35,7 @@ interface Stored {
|
|
|
35
35
|
|
|
36
36
|
const missing = (): AgentAdminError => new AgentAdminError('no such agent', 404);
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
function parseOwner(raw: string): string | null {
|
|
39
39
|
const text = raw.trim();
|
|
40
40
|
return isOrganizationId(text) ? text : null;
|
|
41
41
|
}
|
|
@@ -56,7 +56,7 @@ export function setLocalOwner(raw: string, dir = agentsDir()): string {
|
|
|
56
56
|
return owner;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
function storedAgents(dir: string): Stored[] {
|
|
60
60
|
return listAgentFiles(dir).map((path) => ({ path, file: readAgentFile(path) }));
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
2
|
-
import { log } from '@metro-labs/core/log';
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
2
|
import { homedir } from 'node:os';
|
|
4
3
|
import { join } from 'node:path';
|
|
5
4
|
import { AGENT_NAME_RE, ID_RE } from '@metro-labs/core/ids';
|
|
@@ -8,7 +7,7 @@ import type { LoadedAccount, LoadedAgent, StationSource } from '../stations/mate
|
|
|
8
7
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
9
8
|
import { parsePolicy } from '../policy/policy.js';
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
const AGENT_FILE = 'agent.json';
|
|
12
11
|
|
|
13
12
|
const KEY_RE = /^[A-Za-z0-9_-]{16,128}$/;
|
|
14
13
|
const STATION_NAMES = new Set<string>(STATIONS);
|
|
@@ -126,34 +125,9 @@ export function readAgentFile(path: string): AgentFile {
|
|
|
126
125
|
return parseAgentFile(raw, path);
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
function legacyAgentFiles(dir: string): string[] {
|
|
130
|
-
if (!existsSync(dir)) return [];
|
|
131
|
-
return readdirSync(dir, { withFileTypes: true })
|
|
132
|
-
.filter((entry) => entry.isDirectory())
|
|
133
|
-
.map((entry) => join(dir, entry.name, AGENT_FILE))
|
|
134
|
-
.filter((path) => existsSync(path))
|
|
135
|
-
.sort();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
128
|
export function listAgentFiles(dir = agentsDir()): string[] {
|
|
139
129
|
const fixed = agentFilePath(dir);
|
|
140
|
-
return existsSync(fixed) ? [fixed] :
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
export function migrateAgentLayout(dir = agentsDir()): 'moved' | 'kept' | 'none' {
|
|
144
|
-
const fixed = agentFilePath(dir);
|
|
145
|
-
if (existsSync(fixed)) return 'kept';
|
|
146
|
-
const [first, ...rest] = legacyAgentFiles(dir);
|
|
147
|
-
if (first === undefined) return 'none';
|
|
148
|
-
renameSync(first, fixed);
|
|
149
|
-
try {
|
|
150
|
-
rmdirSync(join(first, '..'));
|
|
151
|
-
} catch {
|
|
152
|
-
log.warn({ folder: join(first, '..') }, 'agent layout: the old agent folder is not empty and was left in place');
|
|
153
|
-
}
|
|
154
|
-
for (const extra of rest) log.warn({ path: extra }, 'agent layout: a second agent file is ignored; a box holds one agent');
|
|
155
|
-
log.info({ from: first, to: fixed }, 'agent layout: moved the agent file to its fixed place');
|
|
156
|
-
return 'moved';
|
|
130
|
+
return existsSync(fixed) ? [fixed] : [];
|
|
157
131
|
}
|
|
158
132
|
|
|
159
133
|
export function loadFileAgents(dir = agentsDir()): LoadedAgent[] {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
2
2
|
import { previewMatches } from './preview.js';
|
|
3
3
|
|
|
4
|
+
export const NEEDS_APPROVAL = (where: string, tool: string): string =>
|
|
5
|
+
`Needs the owner's approval for ${where} (${tool}). Make this exact call from a background worker so Claude Code asks the owner in chat, and wait for the answer. An approval given only in the terminal does not count.`;
|
|
6
|
+
|
|
4
7
|
export type Behavior = 'allow' | 'deny';
|
|
5
8
|
|
|
6
9
|
export type AnsweredVia = 'chat' | 'page' | 'expiry';
|
|
@@ -22,8 +25,7 @@ interface Held extends PendingPrompt {
|
|
|
22
25
|
|
|
23
26
|
const PENDING_MAX = 500;
|
|
24
27
|
const SWEEP_MS = 60_000;
|
|
25
|
-
const
|
|
26
|
-
const HOUR_MS = 3_600_000;
|
|
28
|
+
const APPROVAL_TTL_MS = 24 * 3_600_000;
|
|
27
29
|
|
|
28
30
|
const held = new Map<string, Held>();
|
|
29
31
|
|
|
@@ -48,11 +50,6 @@ export function takeGrant(toolMatches: (tool: string) => boolean, args: Record<s
|
|
|
48
50
|
return true;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
export function approvalTtlMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
52
|
-
const hours = Number(env.METRO_APPROVAL_TTL_H);
|
|
53
|
-
return (Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_TTL_H) * HOUR_MS;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
53
|
const shown = ({ requestId, tool, description, preview, line, at }: Held): PendingPrompt => ({
|
|
57
54
|
requestId,
|
|
58
55
|
tool,
|
|
@@ -122,7 +119,7 @@ export function settlePromptsFor(name: string, args: Record<string, unknown>): v
|
|
|
122
119
|
}
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
export async function expirePrompts(now = Date.now(), ttl =
|
|
122
|
+
export async function expirePrompts(now = Date.now(), ttl = APPROVAL_TTL_MS): Promise<number> {
|
|
126
123
|
const overdue = [...held.values()].filter((entry) => now - entry.at >= ttl);
|
|
127
124
|
for (const entry of overdue)
|
|
128
125
|
await answerPrompt(entry.requestId, 'deny', 'expiry').catch((err: unknown) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from 'node:util';
|
|
2
2
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const ELIDED = '\u0000';
|
|
5
5
|
|
|
6
6
|
const CODE_POINTS = /(?:\\n|\n)⋯ \d+ code points elided ⋯(?:\\n|\n)/g;
|
|
7
7
|
const FIELD_BLOCK = /[\s,]*(?:\\n|\n)?\s*⋯[^]*$/;
|
|
@@ -23,17 +23,14 @@ import { agentsDir, fileSource } from '../agents/files.js';
|
|
|
23
23
|
import { syncPluginServers } from '../connectors/plugin-sync.js';
|
|
24
24
|
import { loadConnectorPolicies, readLocalConnectors } from '../connectors/store.js';
|
|
25
25
|
import { ensureMetroPlugin } from '../claude/plugin-install.js';
|
|
26
|
+
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
26
27
|
import { provisionAgentUser } from '../agent-user/provision.js';
|
|
27
|
-
import { convertRootJobs } from '../agent-user/schedules.js';
|
|
28
28
|
import { applyVault } from '../vault/index.js';
|
|
29
|
-
import { agentUser as currentAgentUser } from '../agent-user/user.js';
|
|
30
|
-
import { ensureServiceOomPolicy } from '../claude/memory.js';
|
|
31
29
|
import { unwatchSession, watchSession } from '../claude/session.js';
|
|
32
30
|
import { tryClaudeSetup } from '../claude/setup.js';
|
|
33
31
|
import { applyLocalOwner } from './local-owner.js';
|
|
34
32
|
import { installBearerSessions } from '../routes/bearer.js';
|
|
35
33
|
import { ensureLocalAgent, localOwner } from '../agents/file-admin.js';
|
|
36
|
-
import { migrateAgentLayout } from '../agents/files.js';
|
|
37
34
|
import { ensureStationDeps } from '../stations/runtime-deps.js';
|
|
38
35
|
import { localSessionApis } from '../routes/local-mode.js';
|
|
39
36
|
import type { SessionApis } from '../routes/session-apis.js';
|
|
@@ -124,7 +121,6 @@ function sessionApis(): SessionApis {
|
|
|
124
121
|
async function main(): Promise<void> {
|
|
125
122
|
applyLocalOwner();
|
|
126
123
|
installBearerSessions(agentsDir(), localOwner);
|
|
127
|
-
migrateAgentLayout();
|
|
128
124
|
watchPolicySnapshot();
|
|
129
125
|
loadConnectorPolicies().catch((err: unknown) => {
|
|
130
126
|
log.warn({ err: errMsg(err) }, 'connector policy: loading the policies failed');
|
|
@@ -158,13 +154,14 @@ installBearerSessions(agentsDir(), localOwner);
|
|
|
158
154
|
}
|
|
159
155
|
|
|
160
156
|
async function startClaude(): Promise<void> {
|
|
161
|
-
|
|
157
|
+
if (runningAsRoot()) {
|
|
158
|
+
log.error('claude: Metro runs as root, so Claude Code never starts here; reinstall Metro as the metro user');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
162
161
|
const agentUser = await provisionAgentUser();
|
|
163
162
|
if (agentUser !== 'off') log.info({ agentUser }, 'agent-user: Claude Code runs as its own user');
|
|
164
163
|
if (agentUser === 'failed') return;
|
|
165
164
|
if (agentUser === 'ready') {
|
|
166
|
-
const switched = convertRootJobs(currentAgentUser());
|
|
167
|
-
if (switched > 0) log.info({ switched }, "schedules: jobs that pointed into root's home now run as the agent user");
|
|
168
165
|
const vault = (await applyVault()).status;
|
|
169
166
|
if (vault.enabled) log.info({ running: vault.running, problem: vault.problem }, 'vault: the agent reaches the internet through the vault');
|
|
170
167
|
}
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import { hostname } from 'node:os';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
const connectLink = (base: string): string =>
|
|
6
|
-
`${uiBase()}/#/${new URL(base).host}`;
|
|
3
|
+
const connectLink = (base: string): string => `https://metro.box/#/${new URL(base).host}`;
|
|
7
4
|
|
|
8
5
|
const ownerLine = (owner: string | null): string =>
|
|
9
6
|
owner === null
|
|
10
|
-
? 'No owner is set, so
|
|
11
|
-
: `Only ${owner} can sign in.`;
|
|
7
|
+
? 'No owner is set, so nobody can sign in. Restart with: metro serve --owner <organization id>'
|
|
8
|
+
: `Only members of the organization ${owner} can sign in.`;
|
|
12
9
|
|
|
13
10
|
export function localConnectHint(port: number, owner: string | null): string {
|
|
14
11
|
const here = `http://127.0.0.1:${String(port)}`;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
2
|
import { str } from '@metro-labs/core/str';
|
|
3
|
-
import { dedupeKey } from './dedupe.js';
|
|
4
|
-
import { PERMISSION_REPLY_RE } from './reply-id.js';
|
|
5
3
|
import {
|
|
6
4
|
buildMediaFailureNote,
|
|
7
5
|
buildMediaNote,
|
|
@@ -9,7 +7,6 @@ import {
|
|
|
9
7
|
type SavedMedia,
|
|
10
8
|
} from './media-note.js';
|
|
11
9
|
import { buildWebhookNote } from './webhook-note.js';
|
|
12
|
-
import { reactContent, reactionEmoji } from './react-note.js';
|
|
13
10
|
import { replyMeta } from './addressed.js';
|
|
14
11
|
import {
|
|
15
12
|
capSet,
|
|
@@ -23,6 +20,31 @@ import {
|
|
|
23
20
|
} from './pending.js';
|
|
24
21
|
import { agentUser } from '../agent-user/user.js';
|
|
25
22
|
|
|
23
|
+
const PERMISSION_REPLY_RE = /^\s*(y|yes|n|no)\s+([a-km-z]{5})\s*$/i;
|
|
24
|
+
|
|
25
|
+
const accountStrippedLine = (line: string): string => {
|
|
26
|
+
const parts = line.split('/');
|
|
27
|
+
if (parts.length < 5) return line;
|
|
28
|
+
return [parts[0], parts[1], parts[2], ...parts.slice(4)].join('/');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const dedupeKey = (station: string, line: string, kind: string, messageId: string): string =>
|
|
32
|
+
`${station} ${accountStrippedLine(line)} ${kind} ${messageId}`;
|
|
33
|
+
|
|
34
|
+
const shortId = (id: string): string => (id.length > 10 ? `${id.slice(0, 6)}…` : id);
|
|
35
|
+
|
|
36
|
+
function reactionEmoji(raw: unknown): string {
|
|
37
|
+
if (typeof raw === 'string') return raw;
|
|
38
|
+
const obj = raw as { name?: string; reaction?: string } | undefined;
|
|
39
|
+
return obj?.name ?? obj?.reaction ?? '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function reactContent(emoji: string, target: string, removed: boolean): string {
|
|
43
|
+
const verb = removed ? 'removed from' : 'reacted to';
|
|
44
|
+
const label = removed ? emoji || 'reaction' : emoji || 'reacted';
|
|
45
|
+
return `${label} ${verb} message ${shortId(target)}`.trim();
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
interface InboundDeps {
|
|
27
49
|
mcp: Server;
|
|
28
50
|
log: (...a: unknown[]) => void;
|
|
@@ -6,7 +6,6 @@ export interface SavedMedia {
|
|
|
6
6
|
contentType?: string;
|
|
7
7
|
attachmentFor?: string;
|
|
8
8
|
attachmentPath?: string;
|
|
9
|
-
localPath?: string;
|
|
10
9
|
url?: string;
|
|
11
10
|
mime?: string;
|
|
12
11
|
name?: string;
|
|
@@ -22,7 +21,7 @@ export interface MediaNote {
|
|
|
22
21
|
path: string;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
function mediaKind(mime?: string, name?: string): string {
|
|
26
25
|
const m = (mime ?? '').toLowerCase();
|
|
27
26
|
if (m.startsWith('image/')) return 'image';
|
|
28
27
|
if (m.startsWith('video/')) return 'video';
|
|
@@ -61,7 +60,7 @@ export async function buildMediaNote(
|
|
|
61
60
|
caption: string,
|
|
62
61
|
showPath = true,
|
|
63
62
|
): Promise<MediaNote | null> {
|
|
64
|
-
const path = p.attachmentPath
|
|
63
|
+
const path = p.attachmentPath;
|
|
65
64
|
if (!path) return null;
|
|
66
65
|
const kind = p.kind ?? mediaKind(p.mime, p.name);
|
|
67
66
|
const name = p.name ?? path.split('/').pop() ?? 'attachment';
|
|
@@ -167,14 +167,14 @@ export function listClaudeSessions(project: string, dir = claudeDir()): ClaudeSe
|
|
|
167
167
|
);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
|
|
170
|
+
type Block =
|
|
171
171
|
| { kind: 'text'; text: string }
|
|
172
172
|
| { kind: 'tool_use'; name: string; input: string }
|
|
173
173
|
| { kind: 'tool_result'; text: string; isError: boolean }
|
|
174
174
|
| { kind: 'thinking' }
|
|
175
175
|
| { kind: 'image' };
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
interface TranscriptEntry {
|
|
178
178
|
uuid: string;
|
|
179
179
|
at: string | null;
|
|
180
180
|
role: 'user' | 'assistant';
|
|
@@ -16,7 +16,7 @@ const ESC = String.fromCharCode(27);
|
|
|
16
16
|
const CSI = /^\[[0-9;?]*[a-zA-Z]/;
|
|
17
17
|
const URL_RE = /https:\/\/[^\s"'`]+/g;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
type LoginState = 'pending' | 'done' | 'failed';
|
|
20
20
|
|
|
21
21
|
export interface LoginView {
|
|
22
22
|
id: string;
|
|
@@ -1,27 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
3
2
|
import { totalmem } from 'node:os';
|
|
4
|
-
import { dirname } from 'node:path';
|
|
5
|
-
import { errMsg, log } from '@metro-labs/core/log';
|
|
6
3
|
import { helperArgv, runningAsMetro } from '../metro-user/privilege.js';
|
|
7
4
|
|
|
8
|
-
const UNIT = '/etc/systemd/system/metro.service';
|
|
9
|
-
const DROP_IN = '/etc/systemd/system/metro.service.d/10-metro-memory.conf';
|
|
10
|
-
const DROP_IN_TEXT = '[Service]\nOOMPolicy=continue\n';
|
|
11
5
|
const GIB = 1024 ** 3;
|
|
12
6
|
const RESERVE = 1.5 * GIB;
|
|
13
7
|
const FLOOR = GIB;
|
|
14
8
|
|
|
15
9
|
export interface ScopeHost {
|
|
16
|
-
|
|
17
|
-
uid: number | undefined;
|
|
10
|
+
metro: boolean;
|
|
18
11
|
systemd: boolean;
|
|
19
12
|
total: number;
|
|
20
13
|
}
|
|
21
14
|
|
|
22
15
|
const realHost = (): ScopeHost => ({
|
|
23
|
-
|
|
24
|
-
uid: process.getuid?.(),
|
|
16
|
+
metro: runningAsMetro(),
|
|
25
17
|
systemd: existsSync('/run/systemd/system'),
|
|
26
18
|
total: totalmem(),
|
|
27
19
|
});
|
|
@@ -31,52 +23,10 @@ export function sessionMemoryLimit(total: number): number {
|
|
|
31
23
|
return Math.max(FLOOR, Math.floor(kept / (1024 * 1024)) * 1024 * 1024);
|
|
32
24
|
}
|
|
33
25
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function metroScope(command: [string, string[]], total: number): [string, string[]] {
|
|
26
|
+
export function inSessionScope(command: [string, string[]], host: ScopeHost = realHost()): [string, string[]] {
|
|
37
27
|
const [file, args] = command;
|
|
38
28
|
const asAgent = file === 'sudo' && args[0] === '-n' && args[1] === '-u' && args[3] === '--';
|
|
39
|
-
if (!asAgent) return command;
|
|
40
|
-
const limit = sessionMemoryLimit(total);
|
|
41
|
-
return helperArgv(['as-agent-scope', String(limit), String(Math.floor(limit * 0.9)), ...args.slice(4)]);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function inSessionScope(command: [string, string[]], host: ScopeHost = realHost(), now = Date.now()): [string, string[]] {
|
|
45
|
-
if (host.systemd && runningAsMetro()) return metroScope(command, host.total);
|
|
46
|
-
if (!usesScopes(host)) return command;
|
|
29
|
+
if (!host.metro || !host.systemd || !asAgent) return command;
|
|
47
30
|
const limit = sessionMemoryLimit(host.total);
|
|
48
|
-
|
|
49
|
-
return [
|
|
50
|
-
'systemd-run',
|
|
51
|
-
[
|
|
52
|
-
'--scope',
|
|
53
|
-
'--quiet',
|
|
54
|
-
'--collect',
|
|
55
|
-
`--unit=metro-claude-${String(now)}`,
|
|
56
|
-
'-p',
|
|
57
|
-
`MemoryMax=${String(limit)}`,
|
|
58
|
-
'-p',
|
|
59
|
-
`MemoryHigh=${String(Math.floor(limit * 0.9))}`,
|
|
60
|
-
'-p',
|
|
61
|
-
'OOMPolicy=continue',
|
|
62
|
-
'--',
|
|
63
|
-
file,
|
|
64
|
-
...args,
|
|
65
|
-
],
|
|
66
|
-
];
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function ensureServiceOomPolicy(host: ScopeHost = realHost(), env: NodeJS.ProcessEnv = process.env): 'written' | 'present' | 'skipped' {
|
|
70
|
-
if (!usesScopes(host) || (env.INVOCATION_ID ?? '') === '' || !existsSync(UNIT)) return 'skipped';
|
|
71
|
-
try {
|
|
72
|
-
if (existsSync(DROP_IN) && readFileSync(DROP_IN, 'utf8') === DROP_IN_TEXT) return 'present';
|
|
73
|
-
mkdirSync(dirname(DROP_IN), { recursive: true });
|
|
74
|
-
writeFileSync(DROP_IN, DROP_IN_TEXT, { mode: 0o644 });
|
|
75
|
-
const reload = spawnSync('systemctl', ['daemon-reload'], { stdio: 'ignore' });
|
|
76
|
-
log.info({ reloaded: reload.status === 0 }, 'claude-memory: the metro service no longer stops as a whole when the kernel kills one process for memory');
|
|
77
|
-
return 'written';
|
|
78
|
-
} catch (err) {
|
|
79
|
-
log.warn({ err: errMsg(err) }, 'claude-memory: could not set the service memory policy');
|
|
80
|
-
return 'skipped';
|
|
81
|
-
}
|
|
31
|
+
return helperArgv(['as-agent-scope', String(limit), String(Math.floor(limit * 0.9)), ...args.slice(4)]);
|
|
82
32
|
}
|
|
@@ -26,7 +26,7 @@ export interface PluginInstallDeps {
|
|
|
26
26
|
|
|
27
27
|
export type PluginOutcome = 'skipped' | 'unchanged' | 'installed' | 'updated' | 'failed';
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
function storeMarketplaceDir(env: NodeJS.ProcessEnv = process.env): string | null {
|
|
30
30
|
const store = env.METRO_RUNTIME_STORE?.trim() ?? '';
|
|
31
31
|
if (store === '') return null;
|
|
32
32
|
const dir = join(store, 'marketplace');
|
|
@@ -60,7 +60,7 @@ const runner =
|
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
async function findClaude(candidates = agentUser() === null ? ['claude', join(homedir(), '.local', 'bin', 'claude')] : [claudeBin()]): Promise<Run | null> {
|
|
64
64
|
for (const bin of candidates) {
|
|
65
65
|
const run = runner(bin);
|
|
66
66
|
if ((await run(['--version'])).status === 0) return run;
|
|
@@ -129,7 +129,7 @@ function explainMissingMarketplace(env: NodeJS.ProcessEnv = process.env): void {
|
|
|
129
129
|
if (store === '') return;
|
|
130
130
|
log.warn(
|
|
131
131
|
{ store },
|
|
132
|
-
'plugin: the runtime store holds no plugin marketplace, so metro cannot install its Claude Code plugin;
|
|
132
|
+
'plugin: the runtime store holds no plugin marketplace, so metro cannot install its Claude Code plugin; restart the metro service once so `metro serve` stages it again',
|
|
133
133
|
);
|
|
134
134
|
}
|
|
135
135
|
|
|
@@ -6,7 +6,7 @@ import { ApiError } from '@metro-labs/http/api-error';
|
|
|
6
6
|
import { isRecord } from '@metro-labs/core/is-record';
|
|
7
7
|
import { claudeDir, PROJECT_RE, safeName, SESSION_RE } from './files.js';
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
const SESSION_FILE_MAX = 512 * 1024 * 1024;
|
|
10
10
|
const FILE_MODE = 0o644;
|
|
11
11
|
|
|
12
12
|
export interface SessionFile {
|
|
@@ -12,7 +12,7 @@ import { claudeDir, listClaudeProjects } from './files.js';
|
|
|
12
12
|
import { claudeAccount, claudeInstalled } from './login.js';
|
|
13
13
|
import { trustFolder } from './onboarding.js';
|
|
14
14
|
import { inSessionScope } from './memory.js';
|
|
15
|
-
import {
|
|
15
|
+
import { runningAsRoot } from '../metro-user/privilege.js';
|
|
16
16
|
import { agentExtraEnv, agentUser, agentUserExpected, agentViewDir, asAgent, claudeHome } from '../agent-user/user.js';
|
|
17
17
|
|
|
18
18
|
function sessionEnv(): Record<string, string> {
|
|
@@ -22,7 +22,7 @@ function sessionEnv(): Record<string, string> {
|
|
|
22
22
|
return { ...agentExtraEnv(), METRO_AGENTS_DIR: agentViewDir(user), ...(port === '' ? {} : { METRO_WEBHOOK_PORT: port }) };
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
const SESSION_NAME = 'metro';
|
|
26
26
|
const STATE_FILE = 'claude-session.json';
|
|
27
27
|
const WARNING = 'WARNING: Loading development channels';
|
|
28
28
|
const CONFIRM_POLL_MS = 500;
|
|
@@ -78,7 +78,7 @@ export function setAutostart(enabled: boolean, agents = agentsDir()): void {
|
|
|
78
78
|
writeState(agents, { autostart: enabled });
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
function startedVersion(agents = agentsDir()): string | null {
|
|
82
82
|
const version = readState(agents).version;
|
|
83
83
|
return typeof version === 'string' ? version : null;
|
|
84
84
|
}
|
|
@@ -136,7 +136,10 @@ function credentialReady(deps: SessionDeps): string | null {
|
|
|
136
136
|
const agentUserMissing = (deps: SessionDeps): boolean =>
|
|
137
137
|
deps.metro === undefined && agentUserExpected() && agentUser() === null;
|
|
138
138
|
|
|
139
|
+
const ROOT_REFUSED = 'Metro runs as root; reinstall it as the metro user';
|
|
140
|
+
|
|
139
141
|
export function sessionBlocked(deps: SessionDeps = {}): string | null {
|
|
142
|
+
if (runningAsRoot()) return ROOT_REFUSED;
|
|
140
143
|
const agents = deps.agents ?? agentsDir();
|
|
141
144
|
if (listAgentFiles(agents).length === 0) return 'no agent on this machine yet';
|
|
142
145
|
if (agentUserMissing(deps))
|
|
@@ -176,8 +179,6 @@ function recordStart(deps: SessionDeps, tmux: string, now: number, run: { error?
|
|
|
176
179
|
confirmChannels(tmux, now + CONFIRM_WAIT_MS);
|
|
177
180
|
}
|
|
178
181
|
|
|
179
|
-
const spawnCwd = (home: string): string => (runningAsMetro() ? '/' : home);
|
|
180
|
-
|
|
181
182
|
export function startSession(deps: SessionDeps = {}): SessionStatus {
|
|
182
183
|
const tmux = deps.tmux ?? 'tmux';
|
|
183
184
|
const home = deps.home ?? claudeHome() ?? homedir();
|
|
@@ -186,7 +187,7 @@ export function startSession(deps: SessionDeps = {}): SessionStatus {
|
|
|
186
187
|
const [command = 'metro', ...args] = metroCommand(deps, home);
|
|
187
188
|
const tmuxArgs = ['new-session', '-d', '-s', SESSION_NAME, '-c', home, '-x', '200', '-y', '50', command, ...args];
|
|
188
189
|
const launch = deps.tmux === undefined && !tmuxServerUp(tmux) ? inSessionScope(asAgent(tmux, tmuxArgs)) : asAgent(tmux, tmuxArgs);
|
|
189
|
-
const run = spawnSync(...launch, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd:
|
|
190
|
+
const run = spawnSync(...launch, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], cwd: '/' });
|
|
190
191
|
recordStart(deps, tmux, now, run);
|
|
191
192
|
if (memory.lastError === null) log.info({ home, trusted, command: [command, ...args].join(' ') }, 'claude-session: started Claude Code in tmux');
|
|
192
193
|
return sessionStatus(deps);
|
|
@@ -10,7 +10,7 @@ export const SETTINGS_MAX = 256 * 1024;
|
|
|
10
10
|
const USER_ID = 'user';
|
|
11
11
|
const LOCAL_SUFFIX = '.local';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
type SettingsScope = 'user' | 'project' | 'local';
|
|
14
14
|
|
|
15
15
|
export interface ClaudeSettingsFile {
|
|
16
16
|
id: string;
|
|
@@ -19,13 +19,13 @@ export const PRIVACY_ENV: Record<string, string> = {
|
|
|
19
19
|
CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF: '1',
|
|
20
20
|
};
|
|
21
21
|
export const RETENTION_DAYS = 7;
|
|
22
|
-
|
|
22
|
+
const SKILL_NAME = 'metro-orchestrator';
|
|
23
23
|
const STATE_FILE = 'claude-setup.json';
|
|
24
24
|
const GUIDANCE = 'orchestrator.md';
|
|
25
|
-
|
|
25
|
+
const SYSTEM_PROMPT_FILE = 'system-prompt.md';
|
|
26
26
|
export const SYSTEM_PROMPT_MAX = 64 * 1024;
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
const WORKER_AGENT = `---
|
|
29
29
|
name: worker
|
|
30
30
|
description: Default execution agent for delegated work. Full tool access and maximum reasoning effort. Use for any substantive task the orchestrator main thread cannot perform itself - reading and writing code, running commands, research, analysis.
|
|
31
31
|
effort: xhigh
|
|
@@ -73,7 +73,7 @@ export interface SetupStatus {
|
|
|
73
73
|
|
|
74
74
|
const statePath = (agents: string): string => join(agents, STATE_FILE);
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
const PERMISSION_MODES = ['auto', 'bypass'] as const;
|
|
77
77
|
export type PermissionMode = (typeof PERMISSION_MODES)[number];
|
|
78
78
|
|
|
79
79
|
function readState(agents: string): Record<string, unknown> {
|
|
@@ -86,7 +86,7 @@ function writeState(agents: string, patch: Record<string, unknown>): void {
|
|
|
86
86
|
writeJson(statePath(agents), { ...readState(agents), ...patch });
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
const privacyEnabled = (agents = agentsDir()): boolean => readState(agents).privacy !== false;
|
|
90
90
|
|
|
91
91
|
export function setPrivacy(enabled: boolean, agents = agentsDir()): void {
|
|
92
92
|
writeState(agents, { privacy: enabled });
|
|
@@ -120,7 +120,7 @@ export function setSystemPrompt(text: string, agents = agentsDir()): void {
|
|
|
120
120
|
writeAtomic(promptPath(agents), `${trimmed}\n`, 0o600);
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
|
|
123
|
+
function guidancePath(env: NodeJS.ProcessEnv = process.env): string {
|
|
124
124
|
const staged = stagedMarketplaceDir(env);
|
|
125
125
|
if (staged !== null) {
|
|
126
126
|
const path = join(staged, 'plugin', GUIDANCE);
|
|
@@ -177,7 +177,7 @@ function mergeSettings(dir: string, change: (settings: Record<string, unknown>)
|
|
|
177
177
|
return 'written';
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
function applyPrivacy(dir: string, enabled: boolean): SettingsOutcome {
|
|
181
181
|
return mergeSettings(dir, (current) => withPrivacy(current, enabled));
|
|
182
182
|
}
|
|
183
183
|
|