@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.
- package/assets/dashboard/app.css +3 -0
- package/assets/dashboard/app.js +9 -0
- package/assets/dashboard/index.html +15 -0
- package/dist/build-info.json +3 -3
- package/dist/{checks-RDU56OGI.js → checks-FMIUVJ73.js} +2 -2
- package/dist/{twinTape-5P3HQ27N.js → chunk-4EPQF6CR.js} +261 -31
- package/dist/{chunk-IZFM7W2V.js → chunk-GBMPR32X.js} +34 -10
- package/dist/{chunk-DX2KCFJM.js → chunk-GTLGXFLQ.js} +1 -1
- package/dist/{chunk-HEEFOMSZ.js → chunk-J5IF4T3C.js} +7 -4
- package/dist/{chunk-BXYPNEJY.js → chunk-LDDR6XRN.js} +45 -22
- package/dist/{chunk-EZYVICDB.js → chunk-QO6QJ6MN.js} +3 -1
- package/dist/chunk-TG6GFWA3.js +48 -0
- package/dist/{runTrialGroup-MMTTTDWY.js → runTrialGroup-FZWKPVI5.js} +6 -6
- package/dist/src/cli/main.js +121 -154
- package/dist/{src-STAXYPD3.js → src-F5FHPGJP.js} +1 -1
- package/dist/{src-46YGR2WY.js → src-L54OSFDK.js} +1 -1
- package/dist/{src-LPTWNBO5.js → src-LYV6LLKW.js} +1 -1
- package/dist/{src-D3PKLT54.js → src-MRJ73S46.js} +1 -1
- package/dist/{src-CV7QDV5E.js → src-PQ35TNOF.js} +1 -1
- package/dist/twinHarness-VEIXQJ4Y.js +6 -0
- package/dist/{twinSeed-ZFZO7BL7.js → twinSeed-LDKR3NPY.js} +1 -1
- package/dist/{twinStart-XN3BYOUF.js → twinStart-44W63TRH.js} +379 -93
- package/dist/twinTape-433A4IC4.js +10 -0
- package/package.json +2 -1
- package/dist/chunk-AGZOWSLG.js +0 -142
- package/dist/twinHarness-I2JGKQYD.js +0 -6
- package/dist/{run-WONFTZS3.js → run-CFA4LZWQ.js} +1 -1
|
@@ -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-
|
|
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-
|
|
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-
|
|
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-
|
|
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';
|
|
@@ -1,17 +1,25 @@
|
|
|
1
|
-
import { snapshotStandaloneInitialState, updateStandaloneStatusFile } from './chunk-
|
|
2
|
-
export { STANDALONE_STATUS_PATH, mergeStandaloneStatus, readStandaloneStatusFile, standaloneStatusEntries, updateStandaloneStatusFile, writeStandaloneStatusFile } from './chunk-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
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(
|
|
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
|
-
|
|
313
|
-
|
|
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
|
-
|
|
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.
|
|
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": "*",
|