@superpanel-sdk/runtime 0.5.0 → 0.7.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/README.md +3 -3
- package/dist/builder/codegen/emit.d.ts +5 -10
- package/dist/builder/codegen/emit.js +100 -106
- package/dist/builder/codegen/generate.d.ts +3 -5
- package/dist/builder/codegen/generate.js +13 -32
- package/dist/builder/company-imports.d.ts +5 -0
- package/dist/builder/company-imports.js +9 -1
- package/dist/builder/compile/compile-sequence.d.ts +13 -11
- package/dist/builder/compile/compile-sequence.js +135 -83
- package/dist/builder/compile/function-code.d.ts +15 -9
- package/dist/builder/compile/function-code.js +59 -29
- package/dist/builder/compile/registry.d.ts +4 -4
- package/dist/builder/compile/registry.js +14 -12
- package/dist/builder/errors.d.ts +9 -0
- package/dist/builder/errors.js +17 -0
- package/dist/builder/function-params.d.ts +10 -0
- package/dist/builder/function-params.js +203 -0
- package/dist/builder/load.d.ts +17 -17
- package/dist/builder/load.js +27 -25
- package/dist/builder/paths.d.ts +6 -1
- package/dist/builder/paths.js +16 -2
- package/dist/builder/schedules.d.ts +9 -7
- package/dist/builder/schedules.js +20 -21
- package/dist/builder/typecheck.d.ts +1 -1
- package/dist/builder/typecheck.js +3 -6
- package/dist/builder/types.d.ts +41 -5
- package/dist/builder/types.js +19 -0
- package/dist/cli/calls/index.d.ts +177 -0
- package/dist/cli/calls/index.js +96 -0
- package/dist/cli/debug/index.d.ts +31 -0
- package/dist/cli/debug/index.js +119 -0
- package/dist/cli/exec/index.d.ts +39 -0
- package/dist/cli/exec/index.js +138 -0
- package/dist/cli/exec/session.d.ts +48 -0
- package/dist/cli/exec/session.js +82 -0
- package/dist/cli/exec/trace.d.ts +18 -0
- package/dist/cli/exec/trace.js +52 -0
- package/dist/cli/index.js +84 -1
- package/dist/cli/logs/index.d.ts +47 -0
- package/dist/cli/logs/index.js +113 -0
- package/dist/cli/mobile/index.d.ts +12 -0
- package/dist/cli/mobile/index.js +405 -0
- package/dist/cli/serve/index.d.ts +40 -4
- package/dist/cli/serve/index.js +167 -28
- package/dist/cli/turns/index.d.ts +105 -0
- package/dist/cli/turns/index.js +128 -0
- package/dist/cli/turns/run.d.ts +13 -0
- package/dist/cli/turns/run.js +57 -0
- package/dist/cli/turns/shadow.d.ts +112 -0
- package/dist/cli/turns/shadow.js +54 -0
- package/dist/cli/type-gen/index.js +0 -1
- package/dist/compute-host/compute-host.d.ts +5 -6
- package/dist/compute-host/compute-host.js +4 -13
- package/dist/compute-host/source-compute-host.d.ts +5 -7
- package/dist/compute-host/source-compute-host.js +12 -33
- package/dist/core/case-orchestrator/contact-hours.d.ts +26 -6
- package/dist/core/case-orchestrator/contact-hours.js +27 -8
- package/dist/core/case-orchestrator/index.d.ts +1 -4
- package/dist/core/case-orchestrator/index.js +1 -4
- package/dist/core/case-orchestrator/render.d.ts +1 -1
- package/dist/core/case-orchestrator/render.js +15 -10
- package/dist/core/case-orchestrator/types.d.ts +83 -128
- package/dist/core/case-orchestrator/types.js +0 -4
- package/dist/core/context/email-message.d.ts +40 -1
- package/dist/core/context/email-message.js +27 -0
- package/dist/core/context/email.d.ts +13 -1
- package/dist/core/context/email.js +2 -3
- package/dist/core/context/exchanges.d.ts +25 -0
- package/dist/core/context/exchanges.js +25 -7
- package/dist/core/context/instances.d.ts +31 -2
- package/dist/core/context/instances.js +18 -4
- package/dist/core/context/platform-registry.d.ts +35 -0
- package/dist/core/context/sms.d.ts +30 -0
- package/dist/core/context/sms.js +20 -0
- package/dist/core/context/types.d.ts +29 -1
- package/dist/core/conversation/index.d.ts +58 -4
- package/dist/core/conversation/index.js +123 -4
- package/dist/core/functions/errors.d.ts +74 -0
- package/dist/core/functions/errors.js +26 -0
- package/dist/core/functions/events.d.ts +23 -78
- package/dist/core/functions/events.js +8 -17
- package/dist/core/functions/schedules.d.ts +9 -10
- package/dist/core/functions/schedules.js +2 -2
- package/dist/core/functions/types.d.ts +17 -72
- package/dist/core/functions/types.js +1 -9
- package/dist/core/page/define.d.ts +230 -160
- package/dist/core/page/define.js +145 -104
- package/dist/core/page/envelope.d.ts +55 -0
- package/dist/core/page/envelope.js +4 -0
- package/dist/core/page/fns.d.ts +448 -0
- package/dist/core/page/fns.js +539 -0
- package/dist/core/page/memory-reader.d.ts +27 -0
- package/dist/core/page/memory-reader.js +299 -0
- package/dist/core/page/page-host.d.ts +20 -0
- package/dist/core/page/page-host.js +57 -0
- package/dist/core/page/records-query.d.ts +234 -0
- package/dist/core/page/records-query.js +780 -0
- package/dist/core/page/render.d.ts +67 -25
- package/dist/core/page/render.js +492 -272
- package/dist/core/page/sandbox.d.ts +85 -0
- package/dist/core/page/sandbox.js +357 -0
- package/dist/core/page/time-format.d.ts +27 -0
- package/dist/core/page/time-format.js +84 -0
- package/dist/core/page/typed.d.ts +165 -159
- package/dist/core/page/workspace.d.ts +25 -1
- package/dist/core/page/workspace.js +48 -13
- package/dist/core/party/schema.d.ts +74 -3
- package/dist/core/party/schema.js +39 -6
- package/dist/core/party/types.d.ts +8 -13
- package/dist/core/sequence/define.d.ts +1 -2
- package/dist/core/slot/company-metadata.d.ts +11 -22
- package/dist/core/slot/company-metadata.js +12 -39
- package/dist/core/slot/compile-schema.js +6 -6
- package/dist/core/slot/field.d.ts +1 -2
- package/dist/core/slot/field.js +0 -2
- package/dist/core/slot/index.d.ts +3 -3
- package/dist/core/slot/index.js +3 -3
- package/dist/core/slot/naming.d.ts +1 -1
- package/dist/core/slot/naming.js +1 -1
- package/dist/core/slot/platform-fields.d.ts +4 -4
- package/dist/core/slot/platform-fields.js +5 -5
- package/dist/core/slot/schema-compat.d.ts +8 -7
- package/dist/core/slot/schema-compat.js +22 -31
- package/dist/core/slot/schema.d.ts +3 -59
- package/dist/core/slot/schema.js +0 -15
- package/dist/core/slot/types.d.ts +4 -10
- package/dist/core/slot/value.d.ts +2 -0
- package/dist/core/slot/value.js +6 -2
- package/dist/dev-overlay/protocol.d.ts +38 -5
- package/dist/fn-context/build-context.js +10 -4
- package/dist/fn-context/fake/index.d.ts +2 -0
- package/dist/fn-context/fake/index.js +5 -3
- package/dist/fn-context/fn-params.d.ts +2 -5
- package/dist/fn-context/fn-params.js +6 -9
- package/dist/fn-context/index.d.ts +1 -1
- package/dist/fn-context/make-ctx.js +5 -4
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +11 -3
- package/dist/fn-context/rpc/case-orchestrator-tools.js +0 -1
- package/dist/fn-context/rpc/errors.d.ts +13 -0
- package/dist/fn-context/rpc/errors.js +22 -0
- package/dist/fn-context/rpc/index.d.ts +1 -1
- package/dist/fn-context/rpc/index.js +1 -1
- package/dist/fn-context/rpc/integration/index.d.ts +11 -1
- package/dist/fn-context/rpc/log.d.ts +11 -1
- package/dist/fn-context/rpc/platform.d.ts +11 -1
- package/dist/fn-context/rpc/records.d.ts +13 -6
- package/dist/fn-context/rpc/records.js +2 -4
- package/dist/fn-context/rpc/trpc-client.d.ts +42 -0
- package/dist/fn-context/rpc/trpc-client.js +151 -22
- package/dist/fn-context/rpc/trpc-init.d.ts +19 -1
- package/dist/fn-context/rpc/trpc-init.js +10 -4
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +66 -8
- package/dist/fn-context/run-scope.js +1 -1
- package/dist/fn-context/types.d.ts +20 -6
- package/dist/index.d.ts +11 -12
- package/dist/index.js +5 -8
- package/dist/logger.d.ts +4 -0
- package/dist/logger.js +8 -0
- package/dist/platform.d.ts +32 -33
- package/dist/platform.js +23 -22
- package/dist/testing-compile-child.d.ts +2 -0
- package/dist/testing-compile-child.js +20 -0
- package/dist/testing.d.ts +368 -0
- package/dist/testing.js +439 -0
- package/dist/voice-service/actions.d.ts +12 -35
- package/dist/voice-service/index.d.ts +2 -0
- package/dist/voice-service/index.js +2 -0
- package/dist/voice-service/protocol.d.ts +604 -0
- package/dist/voice-service/protocol.js +67 -0
- package/dist/voice-service/types.d.ts +16 -6
- package/dist/worker.d.ts +2 -1
- package/dist/worker.js +2 -1
- package/package.json +20 -16
- package/dist/core/case-orchestrator/follow-ups.d.ts +0 -31
- package/dist/core/case-orchestrator/follow-ups.js +0 -103
- package/dist/core/case-orchestrator/tools.d.ts +0 -29
- package/dist/core/case-orchestrator/tools.js +0 -31
- package/dist/core/case-orchestrator/validate.d.ts +0 -11
- package/dist/core/case-orchestrator/validate.js +0 -63
- package/dist/core/functions/define/bind.d.ts +0 -25
- package/dist/core/functions/define/bind.js +0 -14
- package/dist/core/functions/define/case.d.ts +0 -9
- package/dist/core/functions/define/case.js +0 -13
- package/dist/core/functions/define/events.d.ts +0 -20
- package/dist/core/functions/define/events.js +0 -21
- package/dist/core/functions/define/no-case.d.ts +0 -4
- package/dist/core/functions/define/no-case.js +0 -9
- package/dist/core/functions/func-ref.d.ts +0 -25
- package/dist/core/functions/func-ref.js +0 -20
package/dist/cli/serve/index.js
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
2
3
|
import { cancel, isCancel, select } from '@clack/prompts';
|
|
3
4
|
import chokidar from 'chokidar';
|
|
4
|
-
import { compileAll,
|
|
5
|
+
import { compileAll, compileCompanyPart, runtimePackageSource, runtimePackageVersion, } from '../../builder/compile/compile-sequence.js';
|
|
5
6
|
import { withCompanyImportGraph } from '../../builder/company-import-graph.js';
|
|
6
7
|
import { findTsModule, requireCompanyResolveModule, requireResolveModule } from '../../builder/paths.js';
|
|
7
8
|
import { listSequenceIds, loadCompanyWebhook, loadSequenceCaseOrchestrator, WEBHOOK_MODULE, loadCompanyResolver, loadSequenceResolver, loadSequenceEvents, loadSequenceFunctions, } from '../../builder/load.js';
|
|
8
9
|
import { loadSchedules } from '../../builder/schedules.js';
|
|
9
|
-
import {
|
|
10
|
+
import { loadOnError } from '../../builder/errors.js';
|
|
11
|
+
import { ORCHESTRATOR_FUNCTION } from '../../core/case-orchestrator/types.js';
|
|
10
12
|
import { WEBHOOK_RECEIVED_FUNCTION } from '../../core/functions/events.js';
|
|
11
13
|
import { COMPANY_RESOLVE_FUNCTION, RESOLVE_FUNCTION } from '../../core/party/types.js';
|
|
12
|
-
import { COMPANY_SCOPE } from '../../builder/types.js';
|
|
14
|
+
import { COMPANY_SCOPE, } from '../../builder/types.js';
|
|
15
|
+
import { realPath } from '../../builder/company-imports.js';
|
|
13
16
|
import { createSourceComputeHost } from '../../compute-host/source-compute-host.js';
|
|
14
17
|
import { formatRuntimeVersion } from '../../fn-context/rpc/request-id.js';
|
|
15
18
|
import { encodeOverlayMessage, OVERLAY_REPLACED_CLOSE_CODE, parseOverlayMessage, } from '../../dev-overlay/protocol.js';
|
|
@@ -67,22 +70,18 @@ async function loadOne(repoRoot, sequenceId, generation) {
|
|
|
67
70
|
throw new Error(`Failed to compile sequence ${sequenceId}`);
|
|
68
71
|
}
|
|
69
72
|
const sequenceDir = path.join(repoRoot, 'sequences', sequenceId);
|
|
73
|
+
// A sequence without `functions/` has none (76 §5.5), as the compile above takes it.
|
|
70
74
|
const functionsPath = findTsModule(sequenceDir, 'functions');
|
|
71
|
-
if (!functionsPath) {
|
|
72
|
-
throw new Error(`No functions module for sequence ${sequenceId}`);
|
|
73
|
-
}
|
|
74
75
|
const caseOrchestratorPath = findTsModule(sequenceDir, 'orchestrator');
|
|
75
|
-
const authored = await loadSequenceFunctions(functionsPath);
|
|
76
|
+
const authored = functionsPath ? await loadSequenceFunctions(functionsPath) : {};
|
|
76
77
|
const functions = {
|
|
77
78
|
...authored,
|
|
78
|
-
[RESOLVE_FUNCTION]: await loadSequenceResolver(requireResolveModule(sequenceDir, sequenceId)),
|
|
79
|
-
...(caseOrchestratorPath ? await
|
|
80
|
-
// The jobs of its schedules.ts run here too while serving (25 §6.4).
|
|
81
|
-
...(await loadSchedules(sequenceDir, `sequences/${sequenceId}/schedules.ts`))?.functions,
|
|
79
|
+
[RESOLVE_FUNCTION]: (await loadSequenceResolver(requireResolveModule(sequenceDir, sequenceId))),
|
|
80
|
+
...(caseOrchestratorPath ? { [ORCHESTRATOR_FUNCTION]: await loadSequenceCaseOrchestrator(caseOrchestratorPath) } : {}),
|
|
82
81
|
};
|
|
83
82
|
const eventsPath = findTsModule(sequenceDir, 'events');
|
|
84
83
|
const events = eventsPath ? await loadSequenceEvents(eventsPath) : {};
|
|
85
|
-
return { build: compiled.build, functions, events, generation };
|
|
84
|
+
return { build: compiled.build, sourceMaps: compiled.sourceMaps, functions, events, generation };
|
|
86
85
|
});
|
|
87
86
|
}
|
|
88
87
|
export async function loadCompany(repoRoot) {
|
|
@@ -90,23 +89,15 @@ export async function loadCompany(repoRoot) {
|
|
|
90
89
|
const webhookPath = findTsModule(repoRoot, WEBHOOK_MODULE);
|
|
91
90
|
return {
|
|
92
91
|
part: await compileCompanyPart(repoRoot, { includeCode: false }),
|
|
93
|
-
companyMetadata: await compileCompanyMetadataFromRepo(repoRoot),
|
|
94
92
|
functions: {
|
|
95
|
-
[COMPANY_RESOLVE_FUNCTION]: await loadCompanyResolver(requireCompanyResolveModule(repoRoot)),
|
|
96
|
-
...(webhookPath ? { [WEBHOOK_RECEIVED_FUNCTION]: await loadCompanyWebhook(webhookPath) } : {}),
|
|
97
|
-
...(await loadSchedules(repoRoot
|
|
93
|
+
[COMPANY_RESOLVE_FUNCTION]: (await loadCompanyResolver(requireCompanyResolveModule(repoRoot))),
|
|
94
|
+
...(webhookPath ? { [WEBHOOK_RECEIVED_FUNCTION]: (await loadCompanyWebhook(webhookPath)) } : {}),
|
|
95
|
+
...(await loadSchedules(repoRoot))?.functions,
|
|
96
|
+
...(await loadOnError(repoRoot))?.functions,
|
|
98
97
|
},
|
|
99
98
|
};
|
|
100
99
|
});
|
|
101
100
|
}
|
|
102
|
-
/** orchestrator.ts's two functions, as the build names them. */
|
|
103
|
-
async function caseOrchestratorFunctions(path) {
|
|
104
|
-
const { orchestrator, contacts } = await loadSequenceCaseOrchestrator(path);
|
|
105
|
-
return {
|
|
106
|
-
[ORCHESTRATOR_FUNCTION]: { kind: 'orchestrator', main: orchestrator },
|
|
107
|
-
[ORCHESTRATOR_CONTACTS_FUNCTION]: { kind: 'orchestrator', main: contacts },
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
101
|
/** The repository's sequences, compiled; a listed sequence missing from the repository is left out. */
|
|
111
102
|
async function loadSequences(repoRoot, sequenceIds, generation) {
|
|
112
103
|
return withCompanyImportGraph(repoRoot, async () => {
|
|
@@ -209,7 +200,54 @@ export function pushOverlay(ws, company, sequences, generation) {
|
|
|
209
200
|
company: company.part,
|
|
210
201
|
sequences: Object.fromEntries([...sequences].map(([sequenceId, sequence]) => [sequenceId, sequence.build])),
|
|
211
202
|
};
|
|
212
|
-
|
|
203
|
+
const sourceMaps = Object.assign({}, ...[...sequences.values()].map((sequence) => sequence.sourceMaps));
|
|
204
|
+
send(ws, { type: 'overlay', generation, build, sourceMaps });
|
|
205
|
+
}
|
|
206
|
+
/** A stack frame as V8 prints it: `at name (file:line:col)` or `at file:line:col`. */
|
|
207
|
+
const STACK_FRAME = /^\s*at (?:(.*?) \()?(.+?):(\d+):(\d+)\)?$/;
|
|
208
|
+
/**
|
|
209
|
+
* What the company is shown of a failed function's stack (the backend's company logs): the error's first
|
|
210
|
+
* line and the frames of the repository's own files, by their path from its root. tsx runs the original
|
|
211
|
+
* files, so their lines are already the company's. Undefined when no frame is the repository's.
|
|
212
|
+
*/
|
|
213
|
+
export function repositoryStack(error, repoRoot) {
|
|
214
|
+
if (!(error instanceof Error) || !error.stack)
|
|
215
|
+
return undefined;
|
|
216
|
+
const lines = error.stack.split('\n');
|
|
217
|
+
const firstFrame = lines.findIndex((line) => /^\s*at /.test(line));
|
|
218
|
+
if (firstFrame === -1)
|
|
219
|
+
return undefined;
|
|
220
|
+
const roots = [...new Set([path.resolve(repoRoot), realPath(repoRoot)])];
|
|
221
|
+
const frames = lines.slice(firstFrame).flatMap((line) => {
|
|
222
|
+
const frame = STACK_FRAME.exec(line);
|
|
223
|
+
const file = frame ? repositoryFile(frame[2], roots) : null;
|
|
224
|
+
if (!frame || !file)
|
|
225
|
+
return [];
|
|
226
|
+
return [` at ${frame[1] ? `${frame[1]} (` : ''}${file}:${frame[3]}:${frame[4]}${frame[1] ? ')' : ''}`];
|
|
227
|
+
});
|
|
228
|
+
return frames.length > 0 ? [...lines.slice(0, firstFrame), ...frames].join('\n') : undefined;
|
|
229
|
+
}
|
|
230
|
+
/** A frame's file as a path from the repository root; null outside its own files (node_modules included). */
|
|
231
|
+
function repositoryFile(location, roots) {
|
|
232
|
+
let file = location;
|
|
233
|
+
if (file.startsWith('file://')) {
|
|
234
|
+
try {
|
|
235
|
+
// The import graph stamps the URLs it loads (`?spgen=…`).
|
|
236
|
+
file = fileURLToPath(file.replace(/[?#].*$/, ''));
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (!path.isAbsolute(file))
|
|
243
|
+
return null;
|
|
244
|
+
for (const root of roots) {
|
|
245
|
+
const relative = path.relative(root, file);
|
|
246
|
+
if (relative && !relative.startsWith('..') && !path.isAbsolute(relative) && !relative.split(path.sep).includes('node_modules')) {
|
|
247
|
+
return relative.split(path.sep).join('/');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
213
251
|
}
|
|
214
252
|
function closeDetail(event) {
|
|
215
253
|
return event.reason ? `${event.code} ${event.reason}` : String(event.code);
|
|
@@ -318,7 +356,12 @@ function waitAuthOk(ws) {
|
|
|
318
356
|
ws.addEventListener('close', onClose);
|
|
319
357
|
});
|
|
320
358
|
}
|
|
321
|
-
|
|
359
|
+
/**
|
|
360
|
+
* Runs each `execute` the backend sends, unless its deadline has passed: the backend has then reported
|
|
361
|
+
* it as failed (a paused process, a laptop that slept), and running it would repeat its effects when
|
|
362
|
+
* the call is retried. One already started is not interrupted.
|
|
363
|
+
*/
|
|
364
|
+
export function attachExecuteHandler(ws, find, rpcUrl, repoRoot) {
|
|
322
365
|
ws.addEventListener('message', (event) => {
|
|
323
366
|
void (async () => {
|
|
324
367
|
let message;
|
|
@@ -331,6 +374,22 @@ function attachExecuteHandler(ws, find, rpcUrl) {
|
|
|
331
374
|
}
|
|
332
375
|
if (message.type !== 'execute')
|
|
333
376
|
return;
|
|
377
|
+
const lateMs = Date.now() - message.deadline;
|
|
378
|
+
if (lateMs > 0) {
|
|
379
|
+
send(ws, {
|
|
380
|
+
type: 'execute.result',
|
|
381
|
+
requestId: message.requestId,
|
|
382
|
+
ok: false,
|
|
383
|
+
error: `expired: the backend stopped waiting ${lateMs} ms ago, so it was not run`,
|
|
384
|
+
notStarted: true,
|
|
385
|
+
});
|
|
386
|
+
logger.warn('execute skipped: it arrived after the backend stopped waiting', {
|
|
387
|
+
sequenceId: message.sequenceId,
|
|
388
|
+
name: message.name,
|
|
389
|
+
lateMs,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
334
393
|
const loaded = find(message.sequenceId);
|
|
335
394
|
if (!loaded) {
|
|
336
395
|
const error = `unknown sequence ${message.sequenceId}`;
|
|
@@ -339,6 +398,7 @@ function attachExecuteHandler(ws, find, rpcUrl) {
|
|
|
339
398
|
requestId: message.requestId,
|
|
340
399
|
ok: false,
|
|
341
400
|
error,
|
|
401
|
+
notStarted: true,
|
|
342
402
|
});
|
|
343
403
|
logger.error('execute failed', { sequenceId: message.sequenceId, name: message.name, error });
|
|
344
404
|
return;
|
|
@@ -350,18 +410,21 @@ function attachExecuteHandler(ws, find, rpcUrl) {
|
|
|
350
410
|
}
|
|
351
411
|
catch (error) {
|
|
352
412
|
const text = error instanceof Error ? error.message : String(error);
|
|
413
|
+
const stack = repositoryStack(error, repoRoot);
|
|
353
414
|
send(ws, {
|
|
354
415
|
type: 'execute.result',
|
|
355
416
|
requestId: message.requestId,
|
|
356
417
|
ok: false,
|
|
357
418
|
error: text,
|
|
419
|
+
...(stack ? { stack } : {}),
|
|
420
|
+
...(error instanceof Error && error.name === 'PlatformUnavailableError' ? { platform: true } : {}),
|
|
358
421
|
});
|
|
359
422
|
logger.error('execute failed', {
|
|
360
423
|
sequenceId: message.sequenceId,
|
|
361
424
|
name: message.name,
|
|
362
425
|
durationMs: Date.now() - started,
|
|
363
426
|
error: text,
|
|
364
|
-
});
|
|
427
|
+
}, ...(stack ? [`\n${stack}`] : []));
|
|
365
428
|
return;
|
|
366
429
|
}
|
|
367
430
|
send(ws, { type: 'execute.result', requestId: message.requestId, ok: true, result });
|
|
@@ -611,7 +674,7 @@ export async function runServe(repoRoot, options = {}) {
|
|
|
611
674
|
const authOk = waitAuthOk(ws);
|
|
612
675
|
send(ws, { type: 'auth', token: authorization.token, environment });
|
|
613
676
|
await authOk;
|
|
614
|
-
attachExecuteHandler(ws, find, rpcUrl);
|
|
677
|
+
attachExecuteHandler(ws, find, rpcUrl, repoRoot);
|
|
615
678
|
attachNoticeHandler(ws);
|
|
616
679
|
pushOverlay(ws, company, sequences, generation);
|
|
617
680
|
delayMs = 500;
|
|
@@ -647,4 +710,80 @@ export async function runServe(repoRoot, options = {}) {
|
|
|
647
710
|
delayMs = Math.min(delayMs * 2, 5_000);
|
|
648
711
|
}
|
|
649
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* The repository served on `environmentId` as `serve` serves it, once (no file watch, no reconnect): its
|
|
715
|
+
* code loaded through the company import graph (an import the compile refuses fails here), pushed, and
|
|
716
|
+
* every function the environment runs meanwhile run here. Resolves once the environment took it.
|
|
717
|
+
*/
|
|
718
|
+
export async function openOverlay(repoRoot, options) {
|
|
719
|
+
if (listSequenceIds(repoRoot).length === 0)
|
|
720
|
+
throw new Error(`No sequences found under ${path.join(repoRoot, 'sequences')}`);
|
|
721
|
+
const company = await loadCompany(repoRoot);
|
|
722
|
+
const sequences = await loadSequences(repoRoot, listSequenceIds(repoRoot), 1);
|
|
723
|
+
const runtimeSource = runtimePackageSource(repoRoot);
|
|
724
|
+
const find = (scope) => {
|
|
725
|
+
if (scope === COMPANY_SCOPE)
|
|
726
|
+
return { functions: company.functions, events: {}, runtimeVersion: runtimePackageVersion(), runtimeSource };
|
|
727
|
+
const loaded = sequences.get(scope);
|
|
728
|
+
return loaded
|
|
729
|
+
? { functions: loaded.functions, events: loaded.events, runtimeVersion: loaded.build.engineVersion, runtimeSource: loaded.build.runtimeSource }
|
|
730
|
+
: undefined;
|
|
731
|
+
};
|
|
732
|
+
const ws = new WebSocket(overlayWebSocketUrl(options.backendUrl));
|
|
733
|
+
try {
|
|
734
|
+
await waitOpen(ws);
|
|
735
|
+
const authOk = waitAuthOk(ws);
|
|
736
|
+
send(ws, { type: 'auth', token: options.token, environment: options.environmentId });
|
|
737
|
+
await authOk;
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
ws.close();
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
attachExecuteHandler(ws, find, overlayRpcUrl(options.backendUrl), repoRoot);
|
|
744
|
+
attachNoticeHandler(ws);
|
|
745
|
+
// Served once the backend says so (`overlay.ok`): a call made before could still run the release.
|
|
746
|
+
const served = new Promise((resolve, reject) => {
|
|
747
|
+
const timer = setTimeout(() => reject(new Error('the environment did not take the code within 60 s')), 60_000);
|
|
748
|
+
ws.addEventListener('message', (event) => {
|
|
749
|
+
try {
|
|
750
|
+
const message = parseOverlayMessage(String(event.data));
|
|
751
|
+
if (message.type === 'overlay.ok' && message.generation === 1) {
|
|
752
|
+
clearTimeout(timer);
|
|
753
|
+
resolve();
|
|
754
|
+
}
|
|
755
|
+
else if (message.type === 'auth.error') {
|
|
756
|
+
clearTimeout(timer);
|
|
757
|
+
reject(new Error(`serve: ${message.error}`));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// not a message of ours
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
ws.addEventListener('close', () => {
|
|
765
|
+
clearTimeout(timer);
|
|
766
|
+
reject(new Error('the environment closed the connection before it took the code'));
|
|
767
|
+
});
|
|
768
|
+
});
|
|
769
|
+
pushOverlay(ws, company, sequences, 1);
|
|
770
|
+
try {
|
|
771
|
+
await served;
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
ws.close();
|
|
775
|
+
throw error;
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
sequences: [...sequences.keys()],
|
|
779
|
+
close: async () => {
|
|
780
|
+
if (ws.readyState === WebSocket.CLOSED)
|
|
781
|
+
return;
|
|
782
|
+
await new Promise((resolve) => {
|
|
783
|
+
ws.addEventListener('close', () => resolve(), { once: true });
|
|
784
|
+
ws.close();
|
|
785
|
+
});
|
|
786
|
+
},
|
|
787
|
+
};
|
|
788
|
+
}
|
|
650
789
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { PageChannel } from '../../core/page/define.js';
|
|
2
|
+
export type TurnSummary = {
|
|
3
|
+
id: string;
|
|
4
|
+
seq: number;
|
|
5
|
+
runId: string;
|
|
6
|
+
callId: string | null;
|
|
7
|
+
instanceId: string;
|
|
8
|
+
/** customer | assistant | silence | wrap_up | run */
|
|
9
|
+
kind: string;
|
|
10
|
+
channel: string;
|
|
11
|
+
heard: string | null;
|
|
12
|
+
said: string[];
|
|
13
|
+
/** The model's thought at each step: what it heard, what it does, what comes next. */
|
|
14
|
+
thoughts: string[];
|
|
15
|
+
actions: Array<{
|
|
16
|
+
key: string;
|
|
17
|
+
name: string;
|
|
18
|
+
kind: string;
|
|
19
|
+
status: string;
|
|
20
|
+
args: unknown;
|
|
21
|
+
result: unknown;
|
|
22
|
+
error: string | null;
|
|
23
|
+
}>;
|
|
24
|
+
writes: Array<{
|
|
25
|
+
rowId: string;
|
|
26
|
+
table: string;
|
|
27
|
+
field: string;
|
|
28
|
+
value: unknown;
|
|
29
|
+
state: string | null;
|
|
30
|
+
}>;
|
|
31
|
+
outcome: string | null;
|
|
32
|
+
error: string | null;
|
|
33
|
+
startedAt: string;
|
|
34
|
+
endedAt: string | null;
|
|
35
|
+
/** Whether its snapshot is still kept (30 days). */
|
|
36
|
+
snapshot: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type TurnRecord = TurnSummary & {
|
|
39
|
+
format: 1;
|
|
40
|
+
sequence: string;
|
|
41
|
+
steps: Array<{
|
|
42
|
+
thought: string | null;
|
|
43
|
+
say: string;
|
|
44
|
+
actions: unknown[];
|
|
45
|
+
outcomes: Array<{
|
|
46
|
+
action: string;
|
|
47
|
+
status: string;
|
|
48
|
+
detail?: string;
|
|
49
|
+
}>;
|
|
50
|
+
}>;
|
|
51
|
+
platformRules: string;
|
|
52
|
+
};
|
|
53
|
+
export type TurnSnapshot = {
|
|
54
|
+
pages: Array<{
|
|
55
|
+
seq: number;
|
|
56
|
+
step: number;
|
|
57
|
+
auto: boolean;
|
|
58
|
+
text: string;
|
|
59
|
+
}>;
|
|
60
|
+
data: Record<string, Array<Record<string, unknown>>>;
|
|
61
|
+
context: Record<string, unknown> | null;
|
|
62
|
+
history: unknown;
|
|
63
|
+
start: unknown;
|
|
64
|
+
};
|
|
65
|
+
export type TurnRead = {
|
|
66
|
+
turn: TurnRecord;
|
|
67
|
+
snapshot: TurnSnapshot | null;
|
|
68
|
+
};
|
|
69
|
+
export type TurnsQuery = {
|
|
70
|
+
instanceId?: string;
|
|
71
|
+
runId?: string;
|
|
72
|
+
callId?: string;
|
|
73
|
+
since?: string;
|
|
74
|
+
limit?: number;
|
|
75
|
+
};
|
|
76
|
+
/** POST to an exec route with the run token; the backend's error as the message. */
|
|
77
|
+
export declare function postExec<T>(backendUrl: string, runToken: string, what: string, body: unknown): Promise<T>;
|
|
78
|
+
/** The turn's directory: `turn.json`, `page/<n>.txt`, `data.json`, `context.json`, `history.json`, `start.json`. */
|
|
79
|
+
export declare function writeTurnExport(dir: string, read: TurnRead): string;
|
|
80
|
+
/** A turn's directory read back. Its snapshot is null when it was exported after the snapshot was dropped. */
|
|
81
|
+
export declare function readTurnExport(dir: string): TurnRead;
|
|
82
|
+
/** One line: where it is in the run, who spoke, what was said, what it did. */
|
|
83
|
+
export declare function formatTurnLine(turn: TurnSummary): string;
|
|
84
|
+
/** The channel of a page a turn's context names. */
|
|
85
|
+
export declare function channelOf(turn: TurnRecord): PageChannel;
|
|
86
|
+
type TurnsOptions = {
|
|
87
|
+
env?: string;
|
|
88
|
+
instance?: string;
|
|
89
|
+
call?: string;
|
|
90
|
+
run?: string;
|
|
91
|
+
since?: string;
|
|
92
|
+
limit?: number;
|
|
93
|
+
json?: boolean;
|
|
94
|
+
url?: string;
|
|
95
|
+
};
|
|
96
|
+
/** `turns`: the turns, one line each (or one JSON each with `--json`). */
|
|
97
|
+
export declare function runTurns(repoRoot: string, options: TurnsOptions): Promise<void>;
|
|
98
|
+
/** `turns export <turnId>`: the turn written to `.superpanel/turns/<turnId>/`; prints the directory. */
|
|
99
|
+
export declare function runTurnExport(repoRoot: string, turnId: string, options: {
|
|
100
|
+
env?: string;
|
|
101
|
+
url?: string;
|
|
102
|
+
out?: string;
|
|
103
|
+
}): Promise<void>;
|
|
104
|
+
export {};
|
|
105
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `turns`: the turns of a case, a run or a call in a non-prod environment (68 §7.4), one line each, and
|
|
3
|
+
* `turns export <turnId>`: one turn written to `.superpanel/turns/<turnId>/` (its record and snapshot),
|
|
4
|
+
* which `fromTurn` reads back for `renderPage`. The same files an integration test's `exportTurn` writes.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { loadCachedServeAuthorization, resolveBackendUrl } from '../auth/auth.js';
|
|
9
|
+
import { apiUrl, requestJson } from '../auth/http.js';
|
|
10
|
+
import { requestExecRun } from '../exec/session.js';
|
|
11
|
+
import { timeOf } from '../logs/index.js';
|
|
12
|
+
/** POST to an exec route with the run token; the backend's error as the message. */
|
|
13
|
+
export async function postExec(backendUrl, runToken, what, body) {
|
|
14
|
+
const { response, payload } = await requestJson(apiUrl(backendUrl, `cli-auth/exec/${what}`), {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: { Authorization: `Bearer ${runToken}`, 'Content-Type': 'application/json' },
|
|
17
|
+
body: JSON.stringify(body),
|
|
18
|
+
});
|
|
19
|
+
if (!response.ok)
|
|
20
|
+
throw new Error(`${what}: ${payload.error ?? `the backend answered ${response.status}`}`);
|
|
21
|
+
return payload;
|
|
22
|
+
}
|
|
23
|
+
/** The turn's directory: `turn.json`, `page/<n>.txt`, `data.json`, `context.json`, `history.json`, `start.json`. */
|
|
24
|
+
export function writeTurnExport(dir, read) {
|
|
25
|
+
mkdirSync(dir, { recursive: true });
|
|
26
|
+
const json = (name, value) => writeFileSync(path.join(dir, name), `${JSON.stringify(value, null, 2)}\n`);
|
|
27
|
+
json('turn.json', read.turn);
|
|
28
|
+
if (read.snapshot) {
|
|
29
|
+
mkdirSync(path.join(dir, 'page'), { recursive: true });
|
|
30
|
+
for (const page of read.snapshot.pages)
|
|
31
|
+
if (!page.auto)
|
|
32
|
+
writeFileSync(path.join(dir, 'page', `${page.seq}.txt`), page.text);
|
|
33
|
+
json('data.json', read.snapshot.data);
|
|
34
|
+
json('context.json', read.snapshot.context);
|
|
35
|
+
json('history.json', read.snapshot.history);
|
|
36
|
+
json('start.json', read.snapshot.start);
|
|
37
|
+
}
|
|
38
|
+
return dir;
|
|
39
|
+
}
|
|
40
|
+
/** A turn's directory read back. Its snapshot is null when it was exported after the snapshot was dropped. */
|
|
41
|
+
export function readTurnExport(dir) {
|
|
42
|
+
const file = (name) => path.join(dir, name);
|
|
43
|
+
if (!existsSync(file('turn.json')))
|
|
44
|
+
throw new Error(`${dir}: no turn.json (export a turn with \`turns export <turnId>\`)`);
|
|
45
|
+
const turn = JSON.parse(readFileSync(file('turn.json'), 'utf8'));
|
|
46
|
+
if (turn.format !== 1)
|
|
47
|
+
throw new Error(`${dir}: turn.json format ${String(turn.format)}; this runtime reads format 1`);
|
|
48
|
+
if (!existsSync(file('data.json')))
|
|
49
|
+
return { turn, snapshot: null };
|
|
50
|
+
const json = (name) => JSON.parse(readFileSync(file(name), 'utf8'));
|
|
51
|
+
const pages = existsSync(file('page'))
|
|
52
|
+
? readdirSync(file('page'))
|
|
53
|
+
.filter((name) => /^\d+\.txt$/.test(name))
|
|
54
|
+
.map((name) => ({ seq: Number(name.slice(0, -4)), step: 0, auto: false, text: readFileSync(path.join(file('page'), name), 'utf8') }))
|
|
55
|
+
.sort((a, b) => a.seq - b.seq)
|
|
56
|
+
: [];
|
|
57
|
+
return {
|
|
58
|
+
turn,
|
|
59
|
+
snapshot: {
|
|
60
|
+
pages,
|
|
61
|
+
data: json('data.json'),
|
|
62
|
+
context: json('context.json'),
|
|
63
|
+
history: json('history.json'),
|
|
64
|
+
start: json('start.json'),
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const clip = (text, max = 80) => {
|
|
69
|
+
const one = (text ?? '').replace(/\s+/g, ' ').trim();
|
|
70
|
+
return one.length > max ? `${one.slice(0, max - 1)}…` : one;
|
|
71
|
+
};
|
|
72
|
+
/** One line: where it is in the run, who spoke, what was said, what it did. */
|
|
73
|
+
export function formatTurnLine(turn) {
|
|
74
|
+
const parts = [`#${turn.seq} ${turn.kind}`, turn.id];
|
|
75
|
+
if (turn.heard)
|
|
76
|
+
parts.push(`heard "${clip(turn.heard)}"`);
|
|
77
|
+
if (turn.said.length)
|
|
78
|
+
parts.push(`said "${clip(turn.said.join(' '))}"`);
|
|
79
|
+
if (turn.actions.length)
|
|
80
|
+
parts.push(`did ${turn.actions.map((a) => `${a.key}:${a.status}`).join(', ')}`);
|
|
81
|
+
if (turn.writes.length)
|
|
82
|
+
parts.push(`wrote ${turn.writes.map((w) => `${w.table}.${w.field}`).join(', ')}`);
|
|
83
|
+
if (turn.outcome && turn.outcome !== 'done')
|
|
84
|
+
parts.push(turn.outcome);
|
|
85
|
+
if (turn.error)
|
|
86
|
+
parts.push(`error: ${clip(turn.error)}`);
|
|
87
|
+
return parts.join(' ');
|
|
88
|
+
}
|
|
89
|
+
/** The channel of a page a turn's context names. */
|
|
90
|
+
export function channelOf(turn) {
|
|
91
|
+
return turn.channel === 'call' || turn.channel === 'sms' || turn.channel === 'email' ? turn.channel : 'call';
|
|
92
|
+
}
|
|
93
|
+
async function execToken(repoRoot, options) {
|
|
94
|
+
const backendUrl = resolveBackendUrl(repoRoot, options.url);
|
|
95
|
+
const authorization = await loadCachedServeAuthorization(backendUrl);
|
|
96
|
+
if (!authorization)
|
|
97
|
+
throw new Error('not logged in: run `superpanel-sdk-runtime login` first');
|
|
98
|
+
const run = await requestExecRun(backendUrl, authorization.token, options.env ?? 'sandbox', undefined);
|
|
99
|
+
return { backendUrl, runToken: run.runToken };
|
|
100
|
+
}
|
|
101
|
+
/** `turns`: the turns, one line each (or one JSON each with `--json`). */
|
|
102
|
+
export async function runTurns(repoRoot, options) {
|
|
103
|
+
if (!options.instance && !options.call && !options.run)
|
|
104
|
+
throw new Error('turns: name the case (--instance), the call (--call) or the run (--run)');
|
|
105
|
+
const { backendUrl, runToken } = await execToken(repoRoot, options);
|
|
106
|
+
const since = timeOf(options.since);
|
|
107
|
+
const { turns } = await postExec(backendUrl, runToken, 'turns', {
|
|
108
|
+
...(options.instance ? { instanceId: options.instance } : {}),
|
|
109
|
+
...(options.call ? { callId: options.call } : {}),
|
|
110
|
+
...(options.run ? { runId: options.run } : {}),
|
|
111
|
+
...(since ? { since } : {}),
|
|
112
|
+
...(options.limit ? { limit: options.limit } : {}),
|
|
113
|
+
});
|
|
114
|
+
if (!turns.length)
|
|
115
|
+
console.error('no turns');
|
|
116
|
+
for (const turn of turns)
|
|
117
|
+
console.log(options.json ? JSON.stringify(turn) : formatTurnLine(turn));
|
|
118
|
+
}
|
|
119
|
+
/** `turns export <turnId>`: the turn written to `.superpanel/turns/<turnId>/`; prints the directory. */
|
|
120
|
+
export async function runTurnExport(repoRoot, turnId, options) {
|
|
121
|
+
const { backendUrl, runToken } = await execToken(repoRoot, options);
|
|
122
|
+
const read = await postExec(backendUrl, runToken, 'turns/read', { turnId });
|
|
123
|
+
const dir = writeTurnExport(path.resolve(repoRoot, options.out ?? path.join('.superpanel', 'turns', turnId)), read);
|
|
124
|
+
if (!read.snapshot)
|
|
125
|
+
console.error(`turn ${turnId}: its snapshot is no longer kept (30 days); only turn.json was written`);
|
|
126
|
+
console.log(dir);
|
|
127
|
+
}
|
|
128
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type TurnCliOptions = {
|
|
2
|
+
env?: string;
|
|
3
|
+
url?: string;
|
|
4
|
+
from?: string;
|
|
5
|
+
instance?: string;
|
|
6
|
+
channel?: 'sms' | 'email' | 'call';
|
|
7
|
+
say?: string;
|
|
8
|
+
caller?: string;
|
|
9
|
+
repeat?: number;
|
|
10
|
+
json?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function runTurnCli(repoRoot: string, options: TurnCliOptions): Promise<void>;
|
|
13
|
+
//# sourceMappingURL=run.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `superpanel-sdk-runtime turn` (68 §8.1): one turn as a shadow, from the command line — on a case now, or
|
|
3
|
+
* from a turn that ran (its id or its exported directory), with this repository's code serving the
|
|
4
|
+
* environment while it runs. Each run's words, buttons, writes and functions, one block per run.
|
|
5
|
+
*/
|
|
6
|
+
import { connect } from '../../testing.js';
|
|
7
|
+
function describe(run, n) {
|
|
8
|
+
const lines = [`── run ${n}`];
|
|
9
|
+
for (const thought of run.turns.flatMap((t) => t.thoughts ?? []))
|
|
10
|
+
lines.push(` (thought) ${thought}`);
|
|
11
|
+
for (const said of run.said)
|
|
12
|
+
lines.push(`AI: ${said}`);
|
|
13
|
+
if (run.outcome && run.outcome.kind !== 'reply')
|
|
14
|
+
lines.push(`ended: ${run.outcome.kind}${'reason' in run.outcome ? ` (${run.outcome.reason})` : ''}`);
|
|
15
|
+
for (const action of run.actions)
|
|
16
|
+
lines.push(` ${action.name}: ${action.status}${action.error ? ` (${action.error})` : ''}`);
|
|
17
|
+
for (const write of run.writes)
|
|
18
|
+
lines.push(` wrote ${write.table}.${write.field} = ${JSON.stringify(write.value)}${write.state && write.state !== 'filled' ? ` (${write.state})` : ''}`);
|
|
19
|
+
for (const event of run.events)
|
|
20
|
+
lines.push(` on the line: ${JSON.stringify(event)}`);
|
|
21
|
+
for (const call of run.functions)
|
|
22
|
+
lines.push(` function ${call.name}: ${call.how}${call.error ? ` (${call.error})` : ''}`);
|
|
23
|
+
for (const warning of run.warnings)
|
|
24
|
+
lines.push(` ! ${warning}`);
|
|
25
|
+
return lines.join('\n');
|
|
26
|
+
}
|
|
27
|
+
export async function runTurnCli(repoRoot, options) {
|
|
28
|
+
if (!options.from && !options.instance)
|
|
29
|
+
throw new Error('turn: --from <turnId|dir>, or --instance <case id> with --say');
|
|
30
|
+
const sandbox = await connect({ env: options.env ?? 'sandbox', repoRoot, ...(options.url ? { url: options.url } : {}) });
|
|
31
|
+
let result;
|
|
32
|
+
try {
|
|
33
|
+
result = await sandbox.turn({
|
|
34
|
+
...(options.from ? { from: options.from } : {}),
|
|
35
|
+
...(options.instance ? { instanceId: options.instance } : {}),
|
|
36
|
+
...(options.channel ? { channel: options.channel } : {}),
|
|
37
|
+
...(options.say ? { say: options.say } : {}),
|
|
38
|
+
...(options.caller ? { caller: options.caller } : {}),
|
|
39
|
+
...(options.repeat ? { repeat: options.repeat } : {}),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await sandbox.close();
|
|
44
|
+
}
|
|
45
|
+
if (options.json) {
|
|
46
|
+
for (const run of result.runs)
|
|
47
|
+
console.log(JSON.stringify(run));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
if (result.from)
|
|
51
|
+
console.log(`from turn ${result.from.turnId} (${result.from.kind})${result.from.heard ? `: "${result.from.heard}"` : ''}`);
|
|
52
|
+
result.runs.forEach((run, i) => console.log(describe(run, i + 1)));
|
|
53
|
+
console.log('(a shadow: nothing was kept, sent or booked)');
|
|
54
|
+
}
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=run.js.map
|