@pome-sh/cli 0.45.1 → 0.46.0

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.
@@ -1,7 +1,7 @@
1
1
  import './chunk-C4BVTUA3.js';
2
2
  import { defaultSeedState, parseSeed } from './chunk-NBOQN5VX.js';
3
3
  export { defaultSeedState, parseSeed, seedSchema } from './chunk-NBOQN5VX.js';
4
- import { routeInputDeclarer, integerInput, mountDeclaredRoute, UndeclaredInputError, MalformedBodyError } from './chunk-IZFM7W2V.js';
4
+ import { routeInputDeclarer, integerInput, mountDeclaredRoute, UndeclaredInputError, MalformedBodyError } from './chunk-GBMPR32X.js';
5
5
  import { loadMcpToolFixture, defineTwin, twinBuildInfo, deriveMcpToolTable, UnknownToolError, openTwinDatabase, createApp, typeDisagreements } from './chunk-TWURH7YM.js';
6
6
  import './chunk-7VZBAHQ2.js';
7
7
  import './chunk-SG6ZTIMT.js';
@@ -2,7 +2,7 @@ export { SLACK_CHECKS } from './chunk-2ZGVDTJC.js';
2
2
  import './chunk-JWJYNAWI.js';
3
3
  import { defaultSeedState, parseSeed } from './chunk-YBWG5JK2.js';
4
4
  export { defaultSeedState, loadSeedFromEnv, parseSeed, seedSchema } from './chunk-YBWG5JK2.js';
5
- import { routeInputDeclarer, booleanInput, integerInput, mountDeclaredRoute, UndeclaredInputError } from './chunk-IZFM7W2V.js';
5
+ import { routeInputDeclarer, booleanInput, integerInput, mountDeclaredRoute, UndeclaredInputError } from './chunk-GBMPR32X.js';
6
6
  import { loadMcpToolFixture, defineTwin, queryTokenResolver, formTokenResolver, deriveMcpToolTable, UnknownToolError, openTwinDatabase, createApp, typeDisagreements } from './chunk-TWURH7YM.js';
7
7
  import './chunk-7VZBAHQ2.js';
8
8
  import './chunk-SG6ZTIMT.js';
@@ -2,7 +2,7 @@ export { LINEAR_CHECKS } from './chunk-TSOSDKXP.js';
2
2
  import { MCP_PAGE_MAX, defaultSeedState, linearSeedSchema, notFound, badUserInput, MCP_PAGE_DEFAULT, DEFAULT_LINEAR_EMAIL, RELAY_PAGE_MAX, RELAY_PAGE_DEFAULT, STATE_EXPORT_CAP, parseSeed, DEFAULT_LINEAR_CLOCK, LINEAR_PROVIDER_TOKEN_PREFIX, DEFAULT_LINEAR_SID, DEFAULT_SCOPES, linearErrorEnvelope, unauthorizedEnvelope, unsupportedEnvelope, forbiddenEnvelope, assertWebhookUrl, ACCESS_TOKEN_TTL_SECONDS, OAUTH_CODE_TTL_SECONDS, TITLE_MAX_BYTES, BODY_MAX_BYTES, LinearTwinError, GRAPHQL_QUERY_MAX_BYTES, GRAPHQL_SELECTION_DEPTH_MAX } from './chunk-3FZY376K.js';
3
3
  export { DEFAULT_LINEAR_CLOCK, DEFAULT_LINEAR_EMAIL, DEFAULT_LINEAR_PORT, DEFAULT_LINEAR_SID, DEFAULT_LINEAR_TOKEN, LINEAR_PROVIDER_TOKEN_PREFIX, LinearTwinError, STATE_EXPORT_CAP, assertWebhookUrl, defaultSeedState, linearErrorEnvelope, linearSeedSchema, loadSeedFromEnv, parseSeed, unauthorizedEnvelope, unsupportedEnvelope, webhookUrlError } from './chunk-3FZY376K.js';
4
4
  import './chunk-JWJYNAWI.js';
5
- import { routeInputDeclarer, mountDeclaredRoute, UndeclaredInputError } from './chunk-IZFM7W2V.js';
5
+ import { routeInputDeclarer, mountDeclaredRoute, UndeclaredInputError } from './chunk-GBMPR32X.js';
6
6
  import { loadMcpToolFixture, deriveMcpToolTable, openTwinDatabase, defineTwin, createApp } from './chunk-TWURH7YM.js';
7
7
  import './chunk-7VZBAHQ2.js';
8
8
  import './chunk-SG6ZTIMT.js';
@@ -2,7 +2,7 @@ export { GMAIL_CHECKS } from './chunk-JM6VS62R.js';
2
2
  import { gmailErrorEnvelope, gmailSeedSchema, parseSeed, defaultSeedState, notFound, invalidArgument, checkFault, unsupported, validateSearchQuery, SEARCH_MAILBOX_MESSAGE_BUDGET, GmailError, parseSearchQuery, CATEGORY_LABELS, parseDate, parseSize, KNOWN_FIELDS, parseDuration, assertHasOperator } from './chunk-PASFBRK4.js';
3
3
  export { DEFAULT_GMAIL_AGENT_EMAIL, GmailError, SEARCH_MAILBOX_MESSAGE_BUDGET, agentPathInboxMailbox, defaultSeedState, gmailErrorEnvelope, gmailSeedSchema, loadSeedFromEnv, parseSearchQuery, parseSeed, validateSearchQuery } from './chunk-PASFBRK4.js';
4
4
  import './chunk-JWJYNAWI.js';
5
- import { routeInputDeclarer, integerInput, booleanInput, repeatedInput, mountDeclaredRoute, UndeclaredInputError, MalformedBodyError } from './chunk-IZFM7W2V.js';
5
+ import { routeInputDeclarer, integerInput, booleanInput, repeatedInput, mountDeclaredRoute, UndeclaredInputError, MalformedBodyError } from './chunk-GBMPR32X.js';
6
6
  import { loadMcpToolFixture, deriveMcpToolTable, defineTwin, openTwinDatabase, createApp } from './chunk-TWURH7YM.js';
7
7
  import './chunk-7VZBAHQ2.js';
8
8
  import './chunk-SG6ZTIMT.js';
@@ -0,0 +1,6 @@
1
+ export { UnsupportedTwinError, bootTwin } from './chunk-QO6QJ6MN.js';
2
+ export { STRIPE_LOCAL_ACCOUNT_ID } from './chunk-LDDR6XRN.js';
3
+ import './chunk-TWURH7YM.js';
4
+ import './chunk-7VZBAHQ2.js';
5
+ import './chunk-SG6ZTIMT.js';
6
+ import './chunk-FBSA5L36.js';
@@ -1,4 +1,4 @@
1
- import { TWIN_NAMES, isTwinName, TWIN_REGISTRY } from './chunk-BXYPNEJY.js';
1
+ import { TWIN_NAMES, isTwinName, TWIN_REGISTRY } from './chunk-LDDR6XRN.js';
2
2
  import { writeFile } from 'node:fs/promises';
3
3
 
4
4
  async function generateSeedFile(twins) {
@@ -1,17 +1,25 @@
1
- import { snapshotStandaloneInitialState, updateStandaloneStatusFile } from './chunk-AGZOWSLG.js';
2
- export { STANDALONE_STATUS_PATH, mergeStandaloneStatus, readStandaloneStatusFile, standaloneStatusEntries, updateStandaloneStatusFile, writeStandaloneStatusFile } from './chunk-AGZOWSLG.js';
3
- import { readSeedFileText, seedForTwin, parseSeedFileText, soleTwinOf, twinsNamedBy } from './chunk-DX2KCFJM.js';
4
- import { bootTwin } from './chunk-EZYVICDB.js';
5
- import { defaultPortFor, TWIN_REGISTRY, isTwinName, TWIN_NAMES } from './chunk-BXYPNEJY.js';
1
+ import { snapshotStandaloneInitialState, updateStandaloneStatusFile, tapeRows, censusState, tapeSummary } from './chunk-4EPQF6CR.js';
2
+ export { STANDALONE_STATUS_PATH, mergeStandaloneStatus, readStandaloneStatusFile, standaloneStatusEntries, updateStandaloneStatusFile, writeStandaloneStatusFile } from './chunk-4EPQF6CR.js';
3
+ import { openBrowser, assetPath } from './chunk-TG6GFWA3.js';
4
+ import { readSeedFileText, seedForTwin, parseSeedFileText, soleTwinOf, twinsNamedBy } from './chunk-GTLGXFLQ.js';
5
+ import './chunk-NW7HGA2K.js';
6
+ import { bootTwin } from './chunk-QO6QJ6MN.js';
7
+ import './chunk-4LQ4IJOC.js';
8
+ import './chunk-M7ATJ423.js';
9
+ import { TWIN_REGISTRY, defaultPortFor, isTwinName, TWIN_NAMES } from './chunk-LDDR6XRN.js';
10
+ import './chunk-NBOQN5VX.js';
11
+ import './chunk-YBWG5JK2.js';
6
12
  import './chunk-TWURH7YM.js';
7
13
  import './chunk-7VZBAHQ2.js';
8
- import './chunk-SG6ZTIMT.js';
14
+ import { redactSecrets } from './chunk-SG6ZTIMT.js';
15
+ import './chunk-2K6BJ3PI.js';
9
16
  import './chunk-FBSA5L36.js';
10
- import { randomBytes } from 'node:crypto';
17
+ import { randomBytes, timingSafeEqual } from 'node:crypto';
11
18
  import { readFileSync } from 'node:fs';
12
19
  import { join } from 'node:path';
13
20
  import { serve } from '@hono/node-server';
14
21
  import { sign } from 'hono/jwt';
22
+ import { Hono } from 'hono';
15
23
  import { createServer } from 'node:net';
16
24
 
17
25
  // src/twin/connectSnippets.ts
@@ -39,6 +47,9 @@ function mcpJsonStanzaFor(inputs) {
39
47
  }
40
48
  return JSON.stringify({ mcpServers }, null, 2);
41
49
  }
50
+ function mcpJsonStanza(input) {
51
+ return mcpJsonStanzaFor([input]);
52
+ }
42
53
  function sdkLines(input) {
43
54
  const rest = `process.env.POME_${input.envName}_REST_URL`;
44
55
  switch (input.name) {
@@ -89,6 +100,9 @@ function exportLineFor(inputs) {
89
100
  }
90
101
  return `export ${pairs.join(" ")}`;
91
102
  }
103
+ function exportLine(input) {
104
+ return exportLineFor([input]);
105
+ }
92
106
  function indent(text) {
93
107
  return text.split("\n").map((line) => ` ${line}`).join("\n");
94
108
  }
@@ -116,6 +130,319 @@ function renderConnectSnippets(input) {
116
130
  sdkSections.join("\n\n")
117
131
  ].join("\n");
118
132
  }
133
+
134
+ // src/dashboard/snapshot.ts
135
+ var TAPE_WINDOW = 200;
136
+ var readTwinOverHttp = async (twin) => {
137
+ const get = async (leaf) => {
138
+ const res = await fetch(`${twin.restUrl}/_pome/${leaf}`, {
139
+ headers: { Authorization: `Bearer ${twin.token}` },
140
+ signal: AbortSignal.timeout(2e3)
141
+ });
142
+ if (!res.ok) throw new Error(`${leaf} answered ${res.status}`);
143
+ return await res.json();
144
+ };
145
+ try {
146
+ const [events, state] = await Promise.all([get("events"), get("state")]);
147
+ return { events, state };
148
+ } catch {
149
+ return null;
150
+ }
151
+ };
152
+ function connectSnippetFor(twin) {
153
+ const input = {
154
+ name: twin.name,
155
+ envName: twin.envName,
156
+ port: Number(new URL(twin.restUrl).port),
157
+ restUrl: twin.restUrl,
158
+ mcpUrl: twin.mcpUrl,
159
+ token: twin.token,
160
+ ...twin.tokenEnvName ? { tokenEnvName: twin.tokenEnvName } : {}
161
+ };
162
+ const line = exportLine(input);
163
+ const claude = claudeCodeCommand(input);
164
+ return {
165
+ exportLine: line,
166
+ exportLineMasked: maskTokens(line, twin.token),
167
+ mcpJson: mcpJsonStanza(input),
168
+ claudeCode: claude,
169
+ claudeCodeMasked: maskTokens(claude, twin.token),
170
+ codexToml: codexConfigBlock(input)
171
+ };
172
+ }
173
+ function maskTokens(line, ...tokens) {
174
+ let out = line;
175
+ for (const token of tokens) {
176
+ if (token.length === 0) continue;
177
+ out = out.split(token).join("\u2022".repeat(28));
178
+ }
179
+ return out;
180
+ }
181
+ function entriesFor(rows, events) {
182
+ const start = Math.max(0, rows.length - TAPE_WINDOW);
183
+ return rows.slice(start).map((row, offset) => ({
184
+ ...row,
185
+ // `tapeRows` maps the event list one-to-one and in order, so index i on
186
+ // both sides is the same request. Read defensively anyway: a row without
187
+ // its event is a row whose detail panel says the twin reported nothing,
188
+ // which is a state the page already has to render honestly.
189
+ delta: deltaOf(events[start + offset])
190
+ }));
191
+ }
192
+ function deltaOf(event) {
193
+ if (event === null || typeof event !== "object") return null;
194
+ const delta = event.state_delta;
195
+ if (delta === null || typeof delta !== "object") return null;
196
+ const { before, after } = delta;
197
+ return {
198
+ before: isRecord(before) ? before : null,
199
+ after: isRecord(after) ? after : null
200
+ };
201
+ }
202
+ function isRecord(value) {
203
+ return value !== null && typeof value === "object" && !Array.isArray(value);
204
+ }
205
+ async function snapshotTwin(twin, read) {
206
+ const connect = connectSnippetFor(twin);
207
+ const base = {
208
+ name: twin.name,
209
+ url: twin.restUrl,
210
+ connect
211
+ };
212
+ const got = await read(twin);
213
+ if (got === null) {
214
+ return {
215
+ ...base,
216
+ reachable: false,
217
+ entries: [],
218
+ dropped: 0,
219
+ summary: { requests: 0, changed_state: 0, writes_not_landed: 0, unsupported: 0, reads: 0 },
220
+ world: []
221
+ };
222
+ }
223
+ const sessionPath = new URL(twin.restUrl).pathname;
224
+ const rows = tapeRows(got.events, sessionPath);
225
+ const events = Array.isArray(got.events) ? got.events : [];
226
+ return {
227
+ ...base,
228
+ reachable: true,
229
+ entries: entriesFor(rows, events),
230
+ dropped: Math.max(0, rows.length - TAPE_WINDOW),
231
+ // Over every row, not the windowed slice: the verdict line is a claim
232
+ // about the session, and a capped count would quietly under-report the
233
+ // one number this page exists to show.
234
+ summary: tapeSummary(rows),
235
+ world: censusState(twin.initialState, got.state)
236
+ };
237
+ }
238
+ async function buildSnapshot(twins, read = readTwinOverHttp) {
239
+ return {
240
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
241
+ twins: await Promise.all(twins.map((twin) => snapshotTwin(twin, read)))
242
+ };
243
+ }
244
+
245
+ // src/dashboard/server.ts
246
+ var PAGE_FILES = {
247
+ "/": { file: "index.html", type: "text/html; charset=utf-8" },
248
+ "/index.html": { file: "index.html", type: "text/html; charset=utf-8" },
249
+ "/app.js": { file: "app.js", type: "text/javascript; charset=utf-8" },
250
+ "/app.css": { file: "app.css", type: "text/css; charset=utf-8" }
251
+ };
252
+ var SCROLL_AREA_STYLE_HASH = "sha256-vGQdhYJbTuF+M8iCn1IZCHpdkiICocWHDq4qnQF4Rjw=";
253
+ var DASHBOARD_CSP = [
254
+ "default-src 'none'",
255
+ "script-src 'self'",
256
+ `style-src 'self' '${SCROLL_AREA_STYLE_HASH}'`,
257
+ "font-src data:",
258
+ "img-src 'self' data:",
259
+ "connect-src 'self'",
260
+ "base-uri 'none'",
261
+ "form-action 'none'"
262
+ ].join("; ");
263
+ function keyMatches(presented, expected) {
264
+ const a = Buffer.from(presented);
265
+ const b = Buffer.from(expected);
266
+ if (a.length !== b.length) return false;
267
+ return timingSafeEqual(a, b);
268
+ }
269
+ function isLoopbackHost(host) {
270
+ if (host === void 0) return false;
271
+ const name = host.replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
272
+ return name === "127.0.0.1" || name === "localhost" || name === "::1";
273
+ }
274
+ function createDashboardApp(options) {
275
+ const app = new Hono();
276
+ app.use("*", async (c, next) => {
277
+ if (!isLoopbackHost(c.req.header("host"))) {
278
+ return c.text("This dashboard serves 127.0.0.1 only.", 403);
279
+ }
280
+ await next();
281
+ });
282
+ app.get("/api/snapshot", async (c) => {
283
+ if (!keyMatches(c.req.query("k") ?? "", options.key)) {
284
+ return c.json({ error: "not authorized for this dashboard" }, 401);
285
+ }
286
+ const snapshot = await buildSnapshot(options.twins, options.read);
287
+ c.header("cache-control", "no-store");
288
+ return c.json(snapshot);
289
+ });
290
+ app.get("*", (c) => {
291
+ const wanted = PAGE_FILES[new URL(c.req.url).pathname];
292
+ if (wanted === void 0) return c.text("Not found.", 404);
293
+ try {
294
+ const body = readFileSync(assetPath("dashboard", wanted.file));
295
+ c.header("content-type", wanted.type);
296
+ c.header("cache-control", "no-store");
297
+ c.header("content-security-policy", DASHBOARD_CSP);
298
+ return c.body(body);
299
+ } catch (err) {
300
+ return c.text(
301
+ `The dashboard page is not in this install.
302
+ ${err.message}
303
+ Build it with \`npm run build -w @pome-sh/dashboard\`.`,
304
+ 500
305
+ );
306
+ }
307
+ });
308
+ return app;
309
+ }
310
+ async function startDashboard(options) {
311
+ const key = randomBytes(16).toString("hex");
312
+ const app = createDashboardApp({ key, twins: options.twins, read: options.read });
313
+ const server = serve({
314
+ fetch: app.fetch,
315
+ port: options.port ?? 0,
316
+ hostname: "127.0.0.1"
317
+ });
318
+ const port = await new Promise((resolve, reject) => {
319
+ const onError = (err) => {
320
+ reject(
321
+ err.code === "EADDRINUSE" ? new Error(
322
+ `pome twin start: dashboard port ${options.port} is already in use \u2014 pass --dashboard-port <port>, or --no-dashboard.`
323
+ ) : err
324
+ );
325
+ };
326
+ server.once("error", onError);
327
+ server.once("listening", () => {
328
+ server.off("error", onError);
329
+ server.on("error", (err) => console.error(`pome twin start: dashboard error: ${err}`));
330
+ const address = server.address();
331
+ resolve(typeof address === "object" && address !== null ? address.port : 0);
332
+ });
333
+ });
334
+ return {
335
+ port,
336
+ url: `http://127.0.0.1:${port}/?k=${key}`,
337
+ close: async () => {
338
+ server.closeAllConnections?.();
339
+ await new Promise((resolve) => server.close(() => resolve()));
340
+ }
341
+ };
342
+ }
343
+
344
+ // src/twin/twinStartSeed.ts
345
+ async function resolveStandaloneSeed(twin, seedPath, env = process.env, seedText, noSeed = false) {
346
+ if (seedPath !== void 0) {
347
+ const raw = seedText ?? readSeedFileText(seedPath, "pome twin start --seed");
348
+ const origin = `--seed ${seedPath}`;
349
+ return {
350
+ seedState: await seedForTwin(parseSeedFileText(raw, origin), twin, origin),
351
+ source: "file",
352
+ path: seedPath
353
+ };
354
+ }
355
+ const fromEnv = noSeed ? void 0 : env.POME_SEED_JSON;
356
+ if (fromEnv !== void 0 && fromEnv !== "") {
357
+ return {
358
+ seedState: await seedForTwin(
359
+ parseSeedFileText(fromEnv, "POME_SEED_JSON"),
360
+ twin,
361
+ "POME_SEED_JSON"
362
+ ),
363
+ source: "env"
364
+ };
365
+ }
366
+ return { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" };
367
+ }
368
+ async function resolveStandaloneSeeds(twins, seedPath, env = process.env, seedText, noSeed = () => false) {
369
+ const out = /* @__PURE__ */ new Map();
370
+ if (twins.length === 1) {
371
+ const only = twins[0];
372
+ out.set(only, await resolveStandaloneSeed(only, seedPath, env, seedText, noSeed(only)));
373
+ return out;
374
+ }
375
+ for (const twin of twins.filter(noSeed)) {
376
+ out.set(twin, { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" });
377
+ }
378
+ const seeded = twins.filter((twin) => !noSeed(twin));
379
+ if (seeded.length === 0) return out;
380
+ let authored;
381
+ if (seedPath !== void 0) {
382
+ authored = {
383
+ raw: seedText ?? readSeedFileText(seedPath, "pome twin start --seed"),
384
+ origin: `--seed ${seedPath}`,
385
+ source: "file",
386
+ path: seedPath
387
+ };
388
+ } else if (env.POME_SEED_JSON !== void 0 && env.POME_SEED_JSON !== "") {
389
+ authored = { raw: env.POME_SEED_JSON, origin: "POME_SEED_JSON", source: "env" };
390
+ }
391
+ if (authored === void 0) {
392
+ for (const twin of seeded) {
393
+ out.set(twin, { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" });
394
+ }
395
+ return out;
396
+ }
397
+ const file = parseSeedFileText(authored.raw, authored.origin);
398
+ if (file.shape === "flat") {
399
+ throw new Error(
400
+ `${authored.origin} is a flat seed for one twin, and pome twin start was given ${twins.length} names (${twins.join(", ")}), so nothing says which one it is for. Name one twin, or wrap it in a per-twin envelope: { "${twins[0]}": { \u2026 } }.`
401
+ );
402
+ }
403
+ for (const twin of seeded) {
404
+ out.set(twin, {
405
+ seedState: await seedForTwin(file, twin, authored.origin),
406
+ source: authored.source,
407
+ ...authored.path !== void 0 ? { path: authored.path } : {}
408
+ });
409
+ }
410
+ return out;
411
+ }
412
+
413
+ // src/twin/twinDb.ts
414
+ function resolveStandaloneDb(twin, seedPath, env = process.env) {
415
+ const { dbEnvName, noSeedEnvName } = TWIN_REGISTRY[twin];
416
+ const noSeed = env[noSeedEnvName] === "1";
417
+ if (noSeed && seedPath !== void 0) {
418
+ throw new Error(
419
+ `pome twin start: --seed ${seedPath} and ${noSeedEnvName}=1 contradict each other \u2014 one says apply this seed, the other says apply none. Drop one.`
420
+ );
421
+ }
422
+ return {
423
+ dbPath: env[dbEnvName] || ":memory:",
424
+ noSeed,
425
+ dbEnvName,
426
+ noSeedEnvName
427
+ };
428
+ }
429
+ function resolveStandaloneDbs(twins, seedPath, env = process.env) {
430
+ return new Map(twins.map((twin) => [twin, resolveStandaloneDb(twin, seedPath, env)]));
431
+ }
432
+ function renderStateLines(twin, seed, db) {
433
+ const inMemory = db.dbPath === ":memory:";
434
+ const seedLine = db.noSeed ? `Seed: not applied (${db.noSeedEnvName}=1).` : seed.source === "file" ? `Seed: ${seed.path} (replaces the ${twin} twin's default).` : seed.source === "env" ? `Seed: POME_SEED_JSON (replaces the ${twin} twin's default; --seed <path> overrides it).` : `Seed: the ${twin} twin's default (pass --seed <path>, or write one with \`pome twin new-seed ${twin}\`).`;
435
+ const stateLine = db.noSeed ? inMemory ? `State: in memory and unseeded, so this twin starts empty \u2014 point ${db.dbEnvName} at a saved db to serve one.` : `State: ${db.dbPath} (${db.dbEnvName}), served as it stands.` : inMemory ? `State: in memory \u2014 nothing survives Ctrl-C (set ${db.dbEnvName}=<path> to keep it).` : `State: ${db.dbPath} (${db.dbEnvName}), re-seeded on every boot \u2014 ${db.noSeedEnvName}=1 keeps what is there.`;
436
+ return `${seedLine}
437
+ ${stateLine}`;
438
+ }
439
+ function parseListenPort(raw, flag) {
440
+ const port = Number(raw);
441
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
442
+ throw new Error(`pome twin start: invalid ${flag} "${raw}"`);
443
+ }
444
+ return port;
445
+ }
119
446
  async function isLoopbackPortFree(port) {
120
447
  return await new Promise((resolve) => {
121
448
  const probe = createServer();
@@ -124,13 +451,7 @@ async function isLoopbackPortFree(port) {
124
451
  });
125
452
  }
126
453
  async function chooseStandalonePorts(twins, portOption, env = process.env, isFree = isLoopbackPortFree) {
127
- const parse = (raw) => {
128
- const port = Number(raw);
129
- if (!Number.isInteger(port) || port < 1 || port > 65535) {
130
- throw new Error(`pome twin start: invalid --port "${raw}"`);
131
- }
132
- return port;
133
- };
454
+ const parse = (raw) => parseListenPort(raw, "--port");
134
455
  if (twins.length === 1) return [parse(portOption ?? defaultPortFor(twins[0], env))];
135
456
  const chosen = [];
136
457
  for (const [index, twin] of twins.entries()) {
@@ -154,6 +475,9 @@ async function chooseStandalonePorts(twins, portOption, env = process.env, isFre
154
475
  }
155
476
 
156
477
  // src/twin/twinStart.ts
478
+ async function bootBaseline(harness) {
479
+ return redactSecrets(await harness.exportState());
480
+ }
157
481
  var STANDALONE_SID = "standalone";
158
482
  function resolveStandaloneAuthSecret(twin, env = process.env) {
159
483
  const injected = env.TWIN_AUTH_SECRET;
@@ -177,29 +501,6 @@ function resolveStandaloneAuthSecret(twin, env = process.env) {
177
501
  }
178
502
  return { secret: randomBytes(32).toString("hex"), source: "ephemeral" };
179
503
  }
180
- async function resolveStandaloneSeed(twin, seedPath, env = process.env, seedText) {
181
- if (seedPath !== void 0) {
182
- const raw = seedText ?? readSeedFileText(seedPath, "pome twin start --seed");
183
- const origin = `--seed ${seedPath}`;
184
- return {
185
- seedState: await seedForTwin(parseSeedFileText(raw, origin), twin, origin),
186
- source: "file",
187
- path: seedPath
188
- };
189
- }
190
- const fromEnv = env.POME_SEED_JSON;
191
- if (fromEnv !== void 0 && fromEnv !== "") {
192
- return {
193
- seedState: await seedForTwin(
194
- parseSeedFileText(fromEnv, "POME_SEED_JSON"),
195
- twin,
196
- "POME_SEED_JSON"
197
- ),
198
- source: "env"
199
- };
200
- }
201
- return { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" };
202
- }
203
504
  function resolveStandaloneTwin(name, seedPath, seedText) {
204
505
  if (name !== void 0) {
205
506
  if (!isTwinName(name)) {
@@ -237,45 +538,6 @@ function resolveStandaloneTwins(names, seedPath, seedText) {
237
538
  }
238
539
  return twins;
239
540
  }
240
- async function resolveStandaloneSeeds(twins, seedPath, env = process.env, seedText) {
241
- const out = /* @__PURE__ */ new Map();
242
- if (twins.length === 1) {
243
- const only = twins[0];
244
- out.set(only, await resolveStandaloneSeed(only, seedPath, env, seedText));
245
- return out;
246
- }
247
- let authored;
248
- if (seedPath !== void 0) {
249
- authored = {
250
- raw: seedText ?? readSeedFileText(seedPath, "pome twin start --seed"),
251
- origin: `--seed ${seedPath}`,
252
- source: "file",
253
- path: seedPath
254
- };
255
- } else if (env.POME_SEED_JSON !== void 0 && env.POME_SEED_JSON !== "") {
256
- authored = { raw: env.POME_SEED_JSON, origin: "POME_SEED_JSON", source: "env" };
257
- }
258
- if (authored === void 0) {
259
- for (const twin of twins) {
260
- out.set(twin, { seedState: await TWIN_REGISTRY[twin].defaultSeed(), source: "default" });
261
- }
262
- return out;
263
- }
264
- const file = parseSeedFileText(authored.raw, authored.origin);
265
- if (file.shape === "flat") {
266
- throw new Error(
267
- `${authored.origin} is a flat seed for one twin, and pome twin start was given ${twins.length} names (${twins.join(", ")}), so nothing says which one it is for. Name one twin, or wrap it in a per-twin envelope: { "${twins[0]}": { \u2026 } }.`
268
- );
269
- }
270
- for (const twin of twins) {
271
- out.set(twin, {
272
- seedState: await seedForTwin(file, twin, authored.origin),
273
- source: authored.source,
274
- ...authored.path !== void 0 ? { path: authored.path } : {}
275
- });
276
- }
277
- return out;
278
- }
279
541
  function resolveStandaloneAuthSecretFor(twins, env = process.env, resolveOne = resolveStandaloneAuthSecret) {
280
542
  if (twins.length === 1) return resolveOne(twins[0], env);
281
543
  if (env.TWIN_AUTH_SECRET) return { secret: env.TWIN_AUTH_SECRET, source: "env" };
@@ -294,8 +556,16 @@ async function runTwinStartCommand(namesArg, options) {
294
556
  const names = namesArg === void 0 ? [] : typeof namesArg === "string" ? [namesArg] : [...namesArg];
295
557
  const seedText = options.seed === void 0 ? void 0 : readSeedFileText(options.seed, "pome twin start --seed");
296
558
  const twins = resolveStandaloneTwins(names, options.seed, seedText);
559
+ const dbs = resolveStandaloneDbs(twins, options.seed, process.env);
560
+ const dashboardPort = options.dashboardPort === void 0 ? void 0 : parseListenPort(options.dashboardPort, "--dashboard-port");
297
561
  const ports = await chooseStandalonePorts(twins, options.port, process.env);
298
- const seeds = await resolveStandaloneSeeds(twins, options.seed, process.env, seedText);
562
+ const seeds = await resolveStandaloneSeeds(
563
+ twins,
564
+ options.seed,
565
+ process.env,
566
+ seedText,
567
+ (twin) => dbs.get(twin).noSeed
568
+ );
299
569
  const resolved = resolveStandaloneAuthSecretFor(twins);
300
570
  process.env.TWIN_AUTH_SECRET = resolved.secret;
301
571
  const booted = [];
@@ -307,10 +577,13 @@ async function runTwinStartCommand(namesArg, options) {
307
577
  twin,
308
578
  seedState: seeds.get(twin).seedState,
309
579
  runId: STANDALONE_SID,
310
- twinBaseUrl: baseUrl
580
+ twinBaseUrl: baseUrl,
581
+ dbPath: dbs.get(twin).dbPath,
582
+ noSeed: dbs.get(twin).noSeed
311
583
  });
312
- booted.push({ twin, port, baseUrl, harness });
313
- await snapshotStandaloneInitialState(twin, () => harness.exportState());
584
+ const initialState = await bootBaseline(harness);
585
+ booted.push({ twin, port, baseUrl, harness, initialState });
586
+ await snapshotStandaloneInitialState(twin, () => initialState);
314
587
  }
315
588
  } catch (err) {
316
589
  for (const entry of booted) await entry.harness.close();
@@ -329,7 +602,9 @@ async function runTwinStartCommand(namesArg, options) {
329
602
  resolved.secret
330
603
  );
331
604
  const servers = [];
605
+ let dashboard;
332
606
  const closeAll = async () => {
607
+ await dashboard?.close();
333
608
  for (const server of servers) {
334
609
  server.closeAllConnections?.();
335
610
  await new Promise((resolve) => server.close(() => resolve()));
@@ -370,19 +645,8 @@ async function runTwinStartCommand(namesArg, options) {
370
645
  const { twin: name, port, baseUrl, harness } = entry;
371
646
  const restUrl = `${baseUrl}/s/${STANDALONE_SID}`;
372
647
  const mcpUrl = `${restUrl}/mcp`;
373
- const world = seeds.get(name);
374
648
  console.log(`Pome ${name} twin listening at ${restUrl}`);
375
- if (world.source === "file") {
376
- console.log(`Seed: ${world.path} (replaces the ${name} twin's default).`);
377
- } else if (world.source === "env") {
378
- console.log(
379
- `Seed: POME_SEED_JSON (replaces the ${name} twin's default; --seed <path> overrides it).`
380
- );
381
- } else {
382
- console.log(
383
- `Seed: the ${name} twin's default (pass --seed <path>, or write one with \`pome twin new-seed ${name}\`).`
384
- );
385
- }
649
+ console.log(renderStateLines(name, seeds.get(name), dbs.get(name)));
386
650
  if (index === 0 && resolved.source === "persisted") {
387
651
  console.log(
388
652
  `Auth: using the persisted secret from ${resolved.path} (an env-injected TWIN_AUTH_SECRET overrides it).`
@@ -405,6 +669,28 @@ async function runTwinStartCommand(namesArg, options) {
405
669
  }
406
670
  console.log("");
407
671
  console.log(renderConnectSnippets(connect));
672
+ if (options.dashboard !== false) {
673
+ try {
674
+ dashboard = await startDashboard({
675
+ twins: booted.map((entry) => ({
676
+ name: entry.twin,
677
+ restUrl: `${entry.baseUrl}/s/${STANDALONE_SID}`,
678
+ mcpUrl: `${entry.baseUrl}/s/${STANDALONE_SID}/mcp`,
679
+ token,
680
+ envName: entry.harness.envName,
681
+ ...entry.harness.tokenEnvName ? { tokenEnvName: entry.harness.tokenEnvName } : {},
682
+ initialState: entry.initialState
683
+ })),
684
+ ...dashboardPort === void 0 ? {} : { port: dashboardPort }
685
+ });
686
+ console.log("");
687
+ console.log(`Dashboard: ${dashboard.url}`);
688
+ console.log(" Open it in a browser to watch the tape while the agent works.");
689
+ if (options.open === true) void openBrowser(dashboard.url);
690
+ } catch (err) {
691
+ console.error(`Dashboard not started: ${err.message}`);
692
+ }
693
+ }
408
694
  console.log("");
409
695
  console.log("Ctrl-C to stop.");
410
696
  const shutdown = () => {
@@ -419,4 +705,4 @@ async function runTwinStartCommand(namesArg, options) {
419
705
  process.once("SIGTERM", shutdown);
420
706
  }
421
707
 
422
- export { chooseStandalonePorts, isLoopbackPortFree, resolveStandaloneAuthSecret, resolveStandaloneAuthSecretFor, resolveStandaloneSeed, resolveStandaloneSeeds, resolveStandaloneTwin, resolveStandaloneTwins, runTwinStartCommand };
708
+ export { bootBaseline, chooseStandalonePorts, isLoopbackPortFree, parseListenPort, resolveStandaloneAuthSecret, resolveStandaloneAuthSecretFor, resolveStandaloneSeed, resolveStandaloneSeeds, resolveStandaloneTwin, resolveStandaloneTwins, runTwinStartCommand };
@@ -0,0 +1,10 @@
1
+ export { graphqlOperationKind, pickRecordedTwin, renderTape, requestKind, requestLabel, runTwinTapeCommand, tapeRows, tapeSummary } from './chunk-4EPQF6CR.js';
2
+ import './chunk-4LQ4IJOC.js';
3
+ import './chunk-M7ATJ423.js';
4
+ import './chunk-LDDR6XRN.js';
5
+ import './chunk-NBOQN5VX.js';
6
+ import './chunk-YBWG5JK2.js';
7
+ import './chunk-7VZBAHQ2.js';
8
+ import './chunk-SG6ZTIMT.js';
9
+ import './chunk-2K6BJ3PI.js';
10
+ import './chunk-FBSA5L36.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pome-sh/cli",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "description": "Test mode for your integrations, built for the way agents build. Local, stateful twins of GitHub, Slack, Stripe, Gmail and Linear, over REST and MCP.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -75,6 +75,7 @@
75
75
  "zod": "^4.6.5"
76
76
  },
77
77
  "devDependencies": {
78
+ "@pome-sh/dashboard": "*",
78
79
  "@pome-sh/sdk": "*",
79
80
  "@pome-sh/wire": "*",
80
81
  "@pome-sh/twin-github": "*",