@stage-labs/metro 0.1.0-beta.131 → 0.1.0-beta.132

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
@@ -82,7 +82,7 @@ export function agentKey(agents, wanted) {
82
82
  catch {
83
83
  return {
84
84
  skip: wanted === undefined || wanted === ''
85
- ? 'several agents live here; set METRO_AGENT=<name> to route through the daemon, Claude Code talks to Anthropic directly for now'
85
+ ? 'this box has no agent yet (or several old ones), so Claude Code talks to Anthropic directly for now'
86
86
  : `no agent named '${wanted}' lives here, so Claude Code talks to Anthropic directly`,
87
87
  };
88
88
  }
@@ -102,7 +102,7 @@ async function verdict() {
102
102
  const conflicts = settingsConflicts(settingsFiles());
103
103
  if (conflicts.length > 0)
104
104
  return { skip: `a settings file pins the provider (${conflicts.join(', ')}), so Claude Code keeps it` };
105
- const picked = agentKey(localAgentList(), process.env.METRO_AGENT);
105
+ const picked = agentKey(localAgentList(), undefined);
106
106
  if ('skip' in picked)
107
107
  return picked;
108
108
  if (await daemonServing())
@@ -128,7 +128,7 @@ export function runClaude(args, env) {
128
128
  async function servedKey(decision) {
129
129
  if ('key' in decision)
130
130
  return decision.key;
131
- const picked = agentKey(localAgentList(), process.env.METRO_AGENT);
131
+ const picked = agentKey(localAgentList(), undefined);
132
132
  if ('skip' in picked)
133
133
  return null;
134
134
  return (await daemonServing()) ? picked.key : null;
package/dist/cli.js CHANGED
@@ -25,11 +25,9 @@ const USAGE = `metro — run your agent on this machine
25
25
  metro stop stop metro on this machine, metro serve included
26
26
  metro tail <agent-id>
27
27
  follow this machine's inbound events, one JSON line each
28
- metro whoami [agent]
29
- print the agent this machine runs
30
- metro mcp [agent]
31
- print the mcpServers block of the agent's connectors, served through the
32
- daemon's own relay (name the agent if several live here)
28
+ metro whoami print the agent this machine runs
29
+ metro mcp print the mcpServers block of the agent's connectors, served through the
30
+ daemon's own relay
33
31
  metro plugin set up the Claude Code plugin (connector servers + /metro:refresh)
34
32
  metro claude [args...]
35
33
  open Claude Code with the metro channel; every argument is passed through
package/dist/local.js CHANGED
@@ -8,20 +8,24 @@ export function agentsDir() {
8
8
  const explicit = process.env.METRO_AGENTS_DIR?.trim();
9
9
  return explicit !== undefined && explicit !== '' ? explicit : join(homedir(), '.metro', 'agents');
10
10
  }
11
- export function localAgents(dir = agentsDir()) {
11
+ function agentFiles(dir) {
12
+ const fixed = join(dir, 'agent.json');
13
+ if (existsSync(fixed))
14
+ return [fixed];
12
15
  if (!existsSync(dir))
13
16
  return [];
17
+ return readdirSync(dir, { withFileTypes: true })
18
+ .filter((entry) => entry.isDirectory())
19
+ .map((entry) => join(dir, entry.name, 'agent.json'))
20
+ .filter((path) => existsSync(path));
21
+ }
22
+ export function localAgents(dir = agentsDir()) {
14
23
  const out = [];
15
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
16
- if (!entry.isDirectory())
17
- continue;
18
- const path = join(dir, entry.name, 'agent.json');
19
- if (!existsSync(path))
20
- continue;
24
+ for (const path of agentFiles(dir)) {
21
25
  try {
22
26
  const file = JSON.parse(readFileSync(path, 'utf8'));
23
- if (typeof file.id === 'string' && typeof file.name === 'string' && typeof file.key === 'string')
24
- out.push({ id: file.id, name: file.name, key: file.key });
27
+ if (typeof file.id === 'string' && typeof file.key === 'string')
28
+ out.push({ id: file.id, name: typeof file.name === 'string' ? file.name : 'agent', key: file.key });
25
29
  }
26
30
  catch {
27
31
  continue;
@@ -30,15 +34,8 @@ export function localAgents(dir = agentsDir()) {
30
34
  return out.sort((a, b) => a.name.localeCompare(b.name));
31
35
  }
32
36
  export function localStations(dir = agentsDir()) {
33
- if (!existsSync(dir))
34
- return [];
35
37
  const out = new Set();
36
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
37
- if (!entry.isDirectory())
38
- continue;
39
- const path = join(dir, entry.name, 'agent.json');
40
- if (!existsSync(path))
41
- continue;
38
+ for (const path of agentFiles(dir)) {
42
39
  try {
43
40
  const file = JSON.parse(readFileSync(path, 'utf8'));
44
41
  if (!Array.isArray(file.stations))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.131",
3
+ "version": "0.1.0-beta.132",
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.131",
3
+ "version": "0.1.0-beta.132",
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",
@@ -15,18 +15,23 @@ function agentsDir() {
15
15
  return explicit ? explicit : join(homedir(), '.metro', 'agents');
16
16
  }
17
17
 
18
- function localAgents() {
19
- const dir = agentsDir();
18
+ function agentFiles(dir) {
19
+ const fixed = join(dir, 'agent.json');
20
+ if (existsSync(fixed)) return [fixed];
20
21
  if (!existsSync(dir)) return [];
22
+ return readdirSync(dir, { withFileTypes: true })
23
+ .filter((entry) => entry.isDirectory())
24
+ .map((entry) => join(dir, entry.name, 'agent.json'))
25
+ .filter((path) => existsSync(path));
26
+ }
27
+
28
+ function localAgents() {
21
29
  const out = [];
22
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
23
- if (!entry.isDirectory()) continue;
24
- const path = join(dir, entry.name, 'agent.json');
25
- if (!existsSync(path)) continue;
30
+ for (const path of agentFiles(agentsDir())) {
26
31
  try {
27
32
  const file = JSON.parse(readFileSync(path, 'utf8'));
28
- if (typeof file.id === 'string' && typeof file.name === 'string' && typeof file.key === 'string')
29
- out.push({ id: file.id, name: file.name, key: file.key });
33
+ if (typeof file.id === 'string' && typeof file.key === 'string')
34
+ out.push({ id: file.id, name: typeof file.name === 'string' ? file.name : 'agent', key: file.key });
30
35
  } catch {
31
36
  continue;
32
37
  }
@@ -45,7 +50,7 @@ function pickAgent() {
45
50
  if (agents.length === 1) return agents[0];
46
51
  throw new Error(
47
52
  agents.length === 0
48
- ? 'no agent on this machine yet — start metro serve and create or restore one in the web UI'
53
+ ? 'no agent on this machine yet — start metro serve, it creates one at boot'
49
54
  : `several agents on this machine — set METRO_AGENT to one of: ${agents.map((a) => a.name).join(', ')}`,
50
55
  );
51
56
  }
@@ -6,20 +6,20 @@ export class AgentAdminError extends ApiError {}
6
6
 
7
7
  export interface AgentSummary {
8
8
  id: string;
9
- name: string;
9
+ name: string | null;
10
10
  owned: boolean;
11
11
  key: string | null;
12
12
  }
13
13
 
14
14
  export interface CreatedAgent {
15
15
  id: string;
16
- name: string;
16
+ name: string | null;
17
17
  key: string;
18
18
  }
19
19
 
20
20
  export interface OwnedAgent {
21
21
  id: string;
22
- name: string;
22
+ name: string | null;
23
23
  }
24
24
 
25
25
  export type DeletedAgent = OwnedAgent;
@@ -7,7 +7,6 @@ import {
7
7
  apiSession,
8
8
  projectParam,
9
9
  cors,
10
- readJsonBody,
11
10
  sendJson,
12
11
  type ApiSession,
13
12
  } from '@metro-labs/http/api-http';
@@ -19,7 +18,6 @@ import {
19
18
  } from './account-routes.js';
20
19
  import {
21
20
  type AgentSummary,
22
- type CreatedAgent,
23
21
  type DeletedAgent,
24
22
  type ResetAgentKey,
25
23
  } from './admin.js';
@@ -29,11 +27,6 @@ const SERVER_NAME = 'metro';
29
27
 
30
28
  export interface AgentApiDeps extends AccountApiDeps {
31
29
  listAgents: (subject: string, project: string) => Promise<AgentSummary[]>;
32
- createAgent: (
33
- subject: string,
34
- project: string,
35
- name: string,
36
- ) => Promise<CreatedAgent>;
37
30
  deleteAgent: (
38
31
  subject: string,
39
32
  id: string,
@@ -166,31 +159,6 @@ async function handleList(
166
159
  sendJson(req, res, 200, { ...base, accounts, unavailable });
167
160
  }
168
161
 
169
- async function handleCreate(
170
- req: IncomingMessage,
171
- res: ServerResponse,
172
- deps: AgentApiDeps,
173
- session: ApiSession,
174
- ): Promise<void> {
175
- const body = await readJsonBody(req);
176
- const name = (body as { name?: unknown }).name;
177
- const project = projectParam(req);
178
- if (project === null) {
179
- sendJson(req, res, 400, { error: 'a project is required' });
180
- return;
181
- }
182
- const created = await deps.createAgent(session.subject, project, name as string);
183
- log.info(
184
- { agent: created.name, id: created.id, owner: session.subject },
185
- 'agent-api: created agent',
186
- );
187
- sendJson(req, res, 201, {
188
- id: created.id,
189
- name: created.name,
190
- ...credentials(created.key),
191
- });
192
- }
193
-
194
162
  async function handleResetKey(
195
163
  req: IncomingMessage,
196
164
  res: ServerResponse,
@@ -239,15 +207,14 @@ async function routeAgent(
239
207
  if (tgt.kind === 'key') await handleResetKey(req, res, deps, session, tgt.id);
240
208
  else if (tgt.kind === 'agent')
241
209
  await handleDelete(req, res, deps, session, tgt.id);
242
- else if (req.method === 'GET') await handleList(req, res, deps, session);
243
- else await handleCreate(req, res, deps, session);
210
+ else await handleList(req, res, deps, session);
244
211
  } catch (err) {
245
212
  apiFailure(req, res, err);
246
213
  }
247
214
  }
248
215
 
249
216
  const ALLOWED: Record<AgentTarget['kind'], string[]> = {
250
- collection: ['GET', 'POST'],
217
+ collection: ['GET'],
251
218
  agent: ['DELETE'],
252
219
  key: ['POST'],
253
220
  };
@@ -69,9 +69,9 @@ function agentOf(raw: unknown): AgentBundle['agent'] {
69
69
  if (!isRecord(raw)) throw bad('not a v1 agent bundle');
70
70
  const { id, name, key, stations } = raw;
71
71
  if (typeof id !== 'string' || parseId(id) === null) throw bad('agent id is not an id');
72
- if (typeof name !== 'string' || typeof key !== 'string' || key === '') throw bad('agent has no name or key');
72
+ if (typeof key !== 'string' || key === '') throw bad('agent has no key');
73
73
  if (!Array.isArray(stations)) throw bad('agent has no station list');
74
- return { id, name, key, stations: stations.map(stationOf) };
74
+ return { id, name: typeof name === 'string' ? name : '', key, stations: stations.map(stationOf) };
75
75
  }
76
76
 
77
77
  export function parseBundle(raw: unknown): AgentBundle {
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, rmdirSync, rmSync } from 'node:fs';
1
+ import { readFileSync, rmdirSync, rmSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { ApiError } from '@metro-labs/http/api-error';
4
4
  import { ensureSecureDir, writeSecure } from '@metro-labs/core/secure-fs';
@@ -14,7 +14,7 @@ import {
14
14
  } from './admin.js';
15
15
  import type { AccountRef } from './account-attach.js';
16
16
  import {
17
- AGENT_FILE,
17
+ agentFilePath,
18
18
  AgentFileError,
19
19
  agentsDir,
20
20
  listAgentFiles,
@@ -94,7 +94,7 @@ export async function localListAgents(
94
94
  return Promise.resolve(
95
95
  storedAgents(dir)
96
96
  .map(({ file }) => ({ id: file.id, name: file.name, owned: true, key: file.key }))
97
- .sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)),
97
+ .sort((a, b) => a.id.localeCompare(b.id)),
98
98
  );
99
99
  }
100
100
 
@@ -109,30 +109,35 @@ function freshId(taken: Set<string>): string {
109
109
  export async function localCreateAgent(
110
110
  subject: string,
111
111
  project: string,
112
- rawName: string,
112
+ rawName?: string,
113
113
  dir = agentsDir(),
114
114
  ): Promise<CreatedAgent> {
115
115
  if (project !== LOCAL_PROJECT_ID || !isOwner(subject, dir))
116
116
  throw new AgentAdminError('no such project', 404);
117
- const name = normalizeAgentName(rawName);
118
- const folder = join(dir, name);
119
- if (existsSync(join(folder, AGENT_FILE)))
120
- throw new AgentAdminError(
121
- `an agent named '${name}' already exists on this machine`,
122
- 409,
123
- );
117
+ const name = rawName === undefined ? null : normalizeAgentName(rawName);
124
118
  const existing = storedAgents(dir);
125
- const id = freshId(new Set(existing.map((s) => s.file.id)));
119
+ if (existing.length > 0) throw new AgentAdminError('this box already has its agent', 409);
120
+ const id = freshId(new Set());
126
121
  const key = newApiKey();
127
- ensureSecureDir(folder);
122
+ ensureSecureDir(dir);
128
123
  save({
129
- path: join(folder, AGENT_FILE),
124
+ path: agentFilePath(dir),
130
125
  file: { version: 1, id, name, key, owner: localOwner(dir), stations: [], connectors: [] },
131
126
  });
132
127
  registerKey(key, id);
133
128
  return Promise.resolve({ id, name, key });
134
129
  }
135
130
 
131
+ export type Ensured = 'created' | 'present' | 'no-owner';
132
+
133
+ export async function ensureLocalAgent(dir = agentsDir()): Promise<Ensured> {
134
+ if (storedAgents(dir).length > 0) return 'present';
135
+ const owner = localOwner(dir);
136
+ if (owner === null) return 'no-owner';
137
+ await localCreateAgent(owner, LOCAL_PROJECT_ID, undefined, dir);
138
+ return 'created';
139
+ }
140
+
136
141
  export async function localResetAgentKey(
137
142
  subject: string,
138
143
  id: string,
@@ -155,7 +160,7 @@ export async function localDeleteAgent(
155
160
  const attached = stored.file.stations.length;
156
161
  if (attached > 0)
157
162
  throw new AgentAdminError(
158
- `agent '${stored.file.name}' still has ${String(attached)} station account(s) attached — detach them first`,
163
+ `the agent still has ${String(attached)} station account(s) attached — detach them first`,
159
164
  409,
160
165
  );
161
166
  rmSync(stored.path);
@@ -196,14 +201,10 @@ interface ImportTarget {
196
201
  function importTarget(dir: string, agent: LoadedAgent & { key: string }): ImportTarget {
197
202
  const existing = storedAgents(dir);
198
203
  const same = existing.find((s) => s.file.id === agent.id);
199
- const fresh = join(dir, agent.name, AGENT_FILE);
200
- if (same === undefined && existsSync(fresh))
201
- throw new AgentAdminError(
202
- `an agent named '${agent.name}' already exists on this machine`,
203
- 409,
204
- );
204
+ if (same === undefined && existing.length > 0)
205
+ throw new AgentAdminError('this box already has its agent; import into it rather than bringing another', 409);
205
206
  assertUnclaimed(existing.filter((s) => s !== same), agent);
206
- return { path: same?.path ?? fresh, previous: same?.file };
207
+ return { path: same?.path ?? agentFilePath(dir), previous: same?.file };
207
208
  }
208
209
 
209
210
  export type ImportMode = 'append' | 'overwrite';
@@ -256,7 +257,7 @@ function fileFor(
256
257
  JSON.stringify({
257
258
  version: 1,
258
259
  id: agent.id,
259
- name: previous?.name ?? agent.name,
260
+ name: previous?.name ?? null,
260
261
  key: agent.key,
261
262
  owner,
262
263
  stations: mergedStations(agent, previous, mode),
@@ -1,4 +1,5 @@
1
- import { existsSync, readdirSync, readFileSync } from 'node:fs';
1
+ import { existsSync, readdirSync, readFileSync, renameSync, rmdirSync } from 'node:fs';
2
+ import { log } from '@metro-labs/core/log';
2
3
  import { homedir } from 'node:os';
3
4
  import { join } from 'node:path';
4
5
  import { AGENT_NAME_RE, ID_RE } from '@metro-labs/core/ids';
@@ -17,13 +18,15 @@ export class AgentFileError extends Error {}
17
18
  export interface AgentFile {
18
19
  version: 1;
19
20
  id: string;
20
- name: string;
21
+ name: string | null;
21
22
  key: string | null;
22
23
  owner: string | null;
23
24
  stations: LoadedAccount[];
24
25
  connectors: string[];
25
26
  }
26
27
 
28
+ export const agentFilePath = (dir = agentsDir()): string => join(dir, AGENT_FILE);
29
+
27
30
  export function agentsDir(): string {
28
31
  const explicit = process.env.METRO_AGENTS_DIR?.trim();
29
32
  return explicit !== undefined && explicit !== ''
@@ -73,6 +76,12 @@ function optionalMatch(
73
76
  const connectorsOf = (raw: unknown): string[] =>
74
77
  Array.isArray(raw) ? raw.filter((id): id is string => typeof id === 'string' && ID_RE.test(id)) : [];
75
78
 
79
+ function nameOf(raw: unknown, path: string): string | null {
80
+ if (raw === undefined || raw === null) return null;
81
+ if (typeof raw !== 'string' || !AGENT_NAME_RE.test(raw)) fail(path, 'name is not a valid agent name');
82
+ return raw;
83
+ }
84
+
76
85
  export function parseAgentFile(raw: string, path: string): AgentFile {
77
86
  let parsed: unknown;
78
87
  try {
@@ -85,8 +94,7 @@ export function parseAgentFile(raw: string, path: string): AgentFile {
85
94
  const { id, name, stations } = parsed;
86
95
  if (typeof id !== 'string' || !ID_RE.test(id))
87
96
  fail(path, 'id is not an 11-character id');
88
- if (typeof name !== 'string' || !AGENT_NAME_RE.test(name))
89
- fail(path, 'name is not a valid agent name');
97
+ const label = nameOf(name, path);
90
98
  const key = optionalMatch(parsed.key, KEY_RE, path, 'key is not an agent key');
91
99
  const owner = optionalMatch(
92
100
  parsed.owner,
@@ -98,7 +106,7 @@ export function parseAgentFile(raw: string, path: string): AgentFile {
98
106
  return {
99
107
  version: 1,
100
108
  id,
101
- name,
109
+ name: label,
102
110
  key,
103
111
  owner,
104
112
  stations: stations.map((s, i) => stationOf(s, path, i)),
@@ -116,7 +124,7 @@ export function readAgentFile(path: string): AgentFile {
116
124
  return parseAgentFile(raw, path);
117
125
  }
118
126
 
119
- export function listAgentFiles(dir = agentsDir()): string[] {
127
+ function legacyAgentFiles(dir: string): string[] {
120
128
  if (!existsSync(dir)) return [];
121
129
  return readdirSync(dir, { withFileTypes: true })
122
130
  .filter((entry) => entry.isDirectory())
@@ -125,6 +133,27 @@ export function listAgentFiles(dir = agentsDir()): string[] {
125
133
  .sort();
126
134
  }
127
135
 
136
+ export function listAgentFiles(dir = agentsDir()): string[] {
137
+ const fixed = agentFilePath(dir);
138
+ return existsSync(fixed) ? [fixed] : legacyAgentFiles(dir);
139
+ }
140
+
141
+ export function migrateAgentLayout(dir = agentsDir()): 'moved' | 'kept' | 'none' {
142
+ const fixed = agentFilePath(dir);
143
+ if (existsSync(fixed)) return 'kept';
144
+ const [first, ...rest] = legacyAgentFiles(dir);
145
+ if (first === undefined) return 'none';
146
+ renameSync(first, fixed);
147
+ try {
148
+ rmdirSync(join(first, '..'));
149
+ } catch {
150
+ log.warn({ folder: join(first, '..') }, 'agent layout: the old agent folder is not empty and was left in place');
151
+ }
152
+ for (const extra of rest) log.warn({ path: extra }, 'agent layout: a second agent file is ignored; a box holds one agent');
153
+ log.info({ from: first, to: fixed }, 'agent layout: moved the agent file to its fixed place');
154
+ return 'moved';
155
+ }
156
+
128
157
  function assertUnique(agents: AgentFile[], paths: string[]): void {
129
158
  const ids = new Map<string, string>();
130
159
  const keys = new Map<string, string>();
@@ -146,7 +175,7 @@ export function loadFileAgents(dir = agentsDir()): LoadedAgent[] {
146
175
  assertUnique(agents, paths);
147
176
  return agents.map((agent) => ({
148
177
  id: agent.id,
149
- name: agent.name,
178
+ name: agent.name ?? agent.id,
150
179
  key: agent.key,
151
180
  accounts: agent.stations,
152
181
  }));
@@ -26,8 +26,8 @@ import { ensureMetroPlugin } from '../claude/plugin-install.js';
26
26
  import { unwatchSession, watchSession } from '../claude/session.js';
27
27
  import { tryClaudeSetup } from '../claude/setup.js';
28
28
  import { applyLocalOwner } from './local-owner.js';
29
- import { localOwner } from '../agents/file-admin.js';
30
- import { seedAgent } from '../agents/seed.js';
29
+ import { ensureLocalAgent, localOwner } from '../agents/file-admin.js';
30
+ import { migrateAgentLayout } from '../agents/files.js';
31
31
  import { ensureStationDeps } from '../stations/runtime-deps.js';
32
32
  import { localSessionApis } from '../routes/local-mode.js';
33
33
  import type { SessionApis } from '../routes/session-apis.js';
@@ -135,7 +135,8 @@ function startConnectors(): void {
135
135
 
136
136
  async function main(): Promise<void> {
137
137
  applyLocalOwner();
138
- await seedAgent();
138
+ migrateAgentLayout();
139
+ log.info({ agent: await ensureLocalAgent() }, 'local daemon: agent');
139
140
  await materializeFrom(fileSource, { allowEmpty: true });
140
141
  supervisor.start();
141
142
  const metroMcp = await createMetroMcp();
@@ -33,7 +33,6 @@ import {
33
33
  assertLocalOwner,
34
34
  LOCAL_PROJECT_ID,
35
35
  localAttachAccount,
36
- localCreateAgent,
37
36
  localDeleteAgent,
38
37
  localDetachAccount,
39
38
  localSetAllowlist,
@@ -87,7 +86,6 @@ function agentApi(deps: LocalModeDeps): AgentApiDeps {
87
86
  return {
88
87
  attachSessions: attachSessions(deps),
89
88
  listAgents: localListAgents,
90
- createAgent: localCreateAgent,
91
89
  deleteAgent: localDeleteAgent,
92
90
  resetKey: async (subject, id) => {
93
91
  const reset = await localResetAgentKey(subject, id);
@@ -158,10 +156,10 @@ function bundleApi(deps: LocalModeDeps): BundleApiDeps {
158
156
  const file = readLocalAgentFile(agentId);
159
157
  const bundle: AgentBundle = {
160
158
  version: 1,
161
- agent: { id: file.id, name: file.name, key: file.key ?? '', stations: file.stations },
159
+ agent: { id: file.id, name: file.name ?? '', key: file.key ?? '', stations: file.stations },
162
160
  connectors: readLocalConnectors().map((c) => ({ id: c.id, name: c.name, url: c.url, transport: c.transport, config: { ...c.config } })),
163
161
  };
164
- if (bundle.agent.key === '') throw new ApiError(`agent '${agent.name}' has no key to bundle`, 400);
162
+ if (bundle.agent.key === '') throw new ApiError(`agent ${agent.id} has no key to bundle`, 400);
165
163
  return bundle;
166
164
  },
167
165
  restore: async (subject, bundle, mode) => {
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.131"
2
+ "version": "0.1.0-beta.132"
3
3
  }
@@ -1,33 +0,0 @@
1
- import { existsSync, readFileSync, rmSync } from 'node:fs';
2
- import { join } from 'node:path';
3
- import { errMsg, log } from '@metro-labs/core/log';
4
- import { agentsDir } from './files.js';
5
- import { LOCAL_PROJECT_ID, localCreateAgent, localOwner, storedAgents } from './file-admin.js';
6
-
7
- export const SEED_FILE = '.agent';
8
-
9
- export type Seeded = 'created' | 'present' | 'none' | 'failed';
10
-
11
- export async function seedAgent(dir = agentsDir()): Promise<Seeded> {
12
- const path = join(dir, SEED_FILE);
13
- if (!existsSync(path)) return 'none';
14
- const name = readFileSync(path, 'utf8').trim();
15
- const owner = localOwner(dir);
16
- if (owner === null) {
17
- log.warn({ name }, 'agent seed: no owner yet, leaving the seed for the next boot');
18
- return 'failed';
19
- }
20
- if (storedAgents(dir).length > 0) {
21
- rmSync(path, { force: true });
22
- return 'present';
23
- }
24
- try {
25
- const made = await localCreateAgent(owner, LOCAL_PROJECT_ID, name, dir);
26
- rmSync(path, { force: true });
27
- log.info({ id: made.id, name: made.name }, 'agent seed: agent created from the launch name');
28
- return 'created';
29
- } catch (err) {
30
- log.warn({ name, err: errMsg(err) }, 'agent seed: could not create the agent');
31
- return 'failed';
32
- }
33
- }