octwin-cli 0.1.7 → 0.1.9
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 +19 -2
- package/dist/index.js +432 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -66,13 +66,30 @@ octwin status # "✓ live and current" once it's warm
|
|
|
66
66
|
| `octwin whoami` | Verify the saved/passed token is valid for a tenant. `--url`, `--tenant`. |
|
|
67
67
|
| `octwin deploy` | Upload + install the pack onto your tenant's project. `--seed` also runs the pack's demo seed. |
|
|
68
68
|
| `octwin status` | Report what the platform has live for this pack — installed vs. loaded version, and its flows. |
|
|
69
|
+
| `octwin chat "msg"` | Drive a turn through the dev web channel and print **every render with its tap ids**. `--as <handle>` picks the test user; `--tap "<tap-id>"` presses a rendered button/list row; `--json` dumps the raw envelopes. |
|
|
70
|
+
| `octwin logs` | List recent conversations (handle, status, last activity; `--as` filters), or show one conversation's full event timeline — including what each turn rendered. `--json` for raw payloads. |
|
|
71
|
+
| `octwin records` | Inspect the pack's XRM data (needs a `records:read` token). No args = list entities. |
|
|
72
|
+
| `octwin cases` | Inspect casework (support tickets): the inbox, one case + its timeline and decisions, or `--queues` for queue keys + open counts. |
|
|
69
73
|
| `octwin platform-kb pull` | Pull the platform's capability reference (built-ins, primitives, render intents, flow-DSL — as markdown + JSON) into `.octwin/platform-kb/`, for the **`octwin-pack`** Claude Code authoring plugin to consult. |
|
|
70
|
-
| `octwin test` |
|
|
71
|
-
| `octwin help` | Show usage. |
|
|
74
|
+
| `octwin test` | Alias for `octwin validate --remote` — the platform's full manifest + flow-DSL check. |
|
|
75
|
+
| `octwin help` | Show usage. Every subcommand also answers `--help`. |
|
|
72
76
|
|
|
73
77
|
Every command that talks to the platform accepts `--dir <path>` (the pack directory; defaults to
|
|
74
78
|
the current directory) plus the target overrides `--url` / `--tenant` / `--project` / `--token`.
|
|
75
79
|
|
|
80
|
+
### Debugging a live conversation
|
|
81
|
+
|
|
82
|
+
The platform keeps **one open conversation per `--as` handle**, so consecutive `octwin chat` calls
|
|
83
|
+
with the same handle **continue the same conversation** — agent memory, suspended flows, and all:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
octwin chat "hi" --as tester1 # turn 1 — prints the menu with each row's tap id
|
|
87
|
+
octwin chat --tap "t:invoke:my-flow:x=1" --as tester1 # turn 2 — press a rendered row
|
|
88
|
+
octwin chat "3 bedrooms" --as tester1 # turn 3 — free text into the running flow
|
|
89
|
+
octwin logs --as tester1 # find the conversation, then:
|
|
90
|
+
octwin logs <conversationId> # the full timeline (taps, renders, tool events)
|
|
91
|
+
```
|
|
92
|
+
|
|
76
93
|
## Configuration
|
|
77
94
|
|
|
78
95
|
The deploy target has four settings. Three live in a committed **`pack.json`** at the root of your
|
package/dist/index.js
CHANGED
|
@@ -15,11 +15,18 @@
|
|
|
15
15
|
* octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
16
16
|
* octwin status [--dir .] # did my deploy land? which version is live?
|
|
17
17
|
* octwin records [entity] [id] # inspect the pack's XRM data (records:read token)
|
|
18
|
-
* octwin
|
|
19
|
-
* octwin
|
|
18
|
+
* octwin cases [caseId] [--queues] # inspect casework (support tickets) — list / one case + timeline
|
|
19
|
+
* octwin logs [conversationId] [--as h] [--json] # list conversations / show one's timeline
|
|
20
|
+
* octwin chat "msg" [--as h] [--tap <tap-id>] [--json] # drive a turn via the web channel
|
|
20
21
|
* octwin platform-kb [pull] [--dir .] # pull the platform capability reference for the authoring skill
|
|
21
22
|
* octwin test [--dir .] # = validate --remote (the full platform check)
|
|
22
23
|
*
|
|
24
|
+
* Multi-turn testing: the platform keeps ONE open conversation per `--as` handle,
|
|
25
|
+
* so consecutive `octwin chat --as <h>` calls CONTINUE the same conversation
|
|
26
|
+
* (same agent memory, resumable flows). Renders print every row/button with its
|
|
27
|
+
* tap id; replay a tap with `--tap "<tap-id>"`. `--help` on any subcommand
|
|
28
|
+
* prints its usage without touching the network.
|
|
29
|
+
*
|
|
23
30
|
* Config resolution (deploy): flags > pack.json (in the pack dir) > env
|
|
24
31
|
* (PACK_PLATFORM_URL / PACK_TENANT / PACK_PROJECT / PACK_TOKEN) > saved login.
|
|
25
32
|
*
|
|
@@ -72,6 +79,32 @@ function die(msg) {
|
|
|
72
79
|
console.error(`✗ ${msg}`);
|
|
73
80
|
process.exit(1);
|
|
74
81
|
}
|
|
82
|
+
// ── network helpers ─────────────────────────────────────────────────────────
|
|
83
|
+
/** `fetch` that dies with the TARGET URL on a network failure — a bare
|
|
84
|
+
* `✗ fetch failed` with no address helps nobody (author-feedback A9). */
|
|
85
|
+
async function fetchOrDie(url, init, what) {
|
|
86
|
+
try {
|
|
87
|
+
return await fetch(url, init);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
die(`${what} — platform unreachable at ${url} (${err?.message ?? err})`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** One consistent explanation for auth failures on admin reads. A 401 can also
|
|
94
|
+
* be a one-off transient (the platform treats an auth-backend hiccup as
|
|
95
|
+
* unauthenticated), so say so instead of sending the author on a re-login hunt. */
|
|
96
|
+
function authFailureHint(status, url) {
|
|
97
|
+
return status === 401
|
|
98
|
+
? `the token was rejected — invalid / expired / revoked. If it JUST worked, this can be a one-off platform hiccup: retry once before re-logging in (octwin login --url ${url} --token oct_…)`
|
|
99
|
+
: `the token is valid but not authorized here (missing scope, plan feature, or role)`;
|
|
100
|
+
}
|
|
101
|
+
/** Pretty-print a JSON error body (or raw text) for an HTTP failure line. */
|
|
102
|
+
function errDetail(json) {
|
|
103
|
+
if (json == null)
|
|
104
|
+
return '';
|
|
105
|
+
const msg = typeof json === 'string' ? json : (json.error ?? json.message ?? JSON.stringify(json));
|
|
106
|
+
return msg ? ` — ${msg}` : '';
|
|
107
|
+
}
|
|
75
108
|
// ── bundle collection ───────────────────────────────────────────────────────
|
|
76
109
|
const SKIP_DIRS = new Set(['.git', 'node_modules', '.pack-bundles', 'dist', '.mastra']);
|
|
77
110
|
/** Collect every text file under `packDir` into a `{ relPath: content }` map. */
|
|
@@ -177,11 +210,22 @@ async function latestPublishedVersion() {
|
|
|
177
210
|
return null;
|
|
178
211
|
}
|
|
179
212
|
}
|
|
213
|
+
/** True when running via `npx` — the CLI's own file lives in npx's cache dir, or npm
|
|
214
|
+
* ran it as `npm exec`. Under npx there is no persistent install to upgrade
|
|
215
|
+
* (`@latest` already resolves the newest), so an upgrade notice would be
|
|
216
|
+
* misleading — stay silent. The notice is for a GLOBAL install (`npm i -g`). */
|
|
217
|
+
function isNpx() {
|
|
218
|
+
try {
|
|
219
|
+
return fileURLToPath(import.meta.url).includes('_npx') || process.env.npm_command === 'exec';
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
180
225
|
/** Print a one-line upgrade notice (to stderr) when a newer octwin-cli is published.
|
|
181
|
-
* Skipped when not
|
|
182
|
-
* Never throws. */
|
|
226
|
+
* Skipped when piped/CI (not a TTY) or run via npx (nothing to upgrade). Never throws. */
|
|
183
227
|
async function notifyIfOutdated() {
|
|
184
|
-
if (!process.stdout.isTTY)
|
|
228
|
+
if (!process.stdout.isTTY || isNpx())
|
|
185
229
|
return;
|
|
186
230
|
try {
|
|
187
231
|
const latest = await latestPublishedVersion();
|
|
@@ -257,11 +301,11 @@ async function cmdValidate(flags) {
|
|
|
257
301
|
// every flow (schema/expression/structure) — returning ALL errors at once.
|
|
258
302
|
const { url, tenant, project, token } = resolveTarget(flags, packDir);
|
|
259
303
|
console.log(`→ Validating against ${tenant}/${project} @ ${url} …`);
|
|
260
|
-
const res = await
|
|
304
|
+
const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/projects/${project}/packs/validate`, {
|
|
261
305
|
method: 'POST',
|
|
262
306
|
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
263
307
|
body: JSON.stringify({ files }),
|
|
264
|
-
});
|
|
308
|
+
}, 'remote validate');
|
|
265
309
|
const text = await res.text();
|
|
266
310
|
let json;
|
|
267
311
|
try {
|
|
@@ -323,7 +367,7 @@ function resolveTarget(flags, packDir) {
|
|
|
323
367
|
async function cmdWhoami(flags) {
|
|
324
368
|
const packDir = resolve(flags.dir ?? '.');
|
|
325
369
|
const { url, tenant, token } = resolveTarget(flags, packDir);
|
|
326
|
-
const res = await
|
|
370
|
+
const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/packs`, { headers: { authorization: `Bearer ${token}` } }, 'token check');
|
|
327
371
|
if (res.ok) {
|
|
328
372
|
console.log(`✓ Token valid for tenant '${tenant}' at ${url} (${token.startsWith('oct_') ? 'deploy token' : 'session token'})`);
|
|
329
373
|
return;
|
|
@@ -400,13 +444,13 @@ async function cmdDeploy(flags) {
|
|
|
400
444
|
const endpoint = `${url}/api/admin/tenants/${tenant}/projects/${project}/packs/deploy`;
|
|
401
445
|
const seed = flags.seed === true;
|
|
402
446
|
console.log(`→ Deploying ${id}@${version} (${Object.keys(files).length} files) to ${tenant}/${project}${seed ? ' — with demo seed' : ''} …`);
|
|
403
|
-
const res = await
|
|
447
|
+
const res = await fetchOrDie(endpoint, {
|
|
404
448
|
method: 'POST',
|
|
405
449
|
// Ask for a progress stream; the platform falls back to plain JSON if it
|
|
406
450
|
// (or an error before any progress) can't stream — handled below.
|
|
407
451
|
headers: { 'content-type': 'application/json', accept: 'text/event-stream', authorization: `Bearer ${token}` },
|
|
408
452
|
body: JSON.stringify({ files, seed }),
|
|
409
|
-
});
|
|
453
|
+
}, 'deploy');
|
|
410
454
|
// Streaming path — live install + seed progress (image generation can take a
|
|
411
455
|
// while, so `--seed` prints per-record / per-image lines as they happen).
|
|
412
456
|
if (res.ok && (res.headers.get('content-type') ?? '').includes('text/event-stream') && res.body) {
|
|
@@ -444,9 +488,9 @@ async function cmdStatus(flags) {
|
|
|
444
488
|
die('manifest.yaml must declare a string `id`');
|
|
445
489
|
const id = doc.id;
|
|
446
490
|
const localVersion = typeof doc?.version === 'string' ? doc.version : '?';
|
|
447
|
-
const res = await
|
|
491
|
+
const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/projects/${project}/packs/${id}/runtime`, {
|
|
448
492
|
headers: { authorization: `Bearer ${token}` },
|
|
449
|
-
});
|
|
493
|
+
}, 'status check');
|
|
450
494
|
const text = await res.text();
|
|
451
495
|
let json;
|
|
452
496
|
try {
|
|
@@ -482,9 +526,9 @@ async function cmdStatus(flags) {
|
|
|
482
526
|
async function cmdPlatformKb(flags) {
|
|
483
527
|
const packDir = resolve(flags.dir ?? '.');
|
|
484
528
|
const { url, tenant, token } = resolveTarget(flags, packDir);
|
|
485
|
-
const res = await
|
|
529
|
+
const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/octwin-platform-kb`, {
|
|
486
530
|
headers: { authorization: `Bearer ${token}` },
|
|
487
|
-
});
|
|
531
|
+
}, 'platform-kb pull');
|
|
488
532
|
const text = await res.text();
|
|
489
533
|
if (!res.ok) {
|
|
490
534
|
let j;
|
|
@@ -522,10 +566,12 @@ async function cmdPlatformKb(flags) {
|
|
|
522
566
|
console.log(` ${mdCount} markdown docs + ${jsonCount} JSON catalogs (reference version ${bundle.version ?? '?'})`);
|
|
523
567
|
console.log(' The octwin-pack authoring skill reads these as the source of truth for what the platform supports.');
|
|
524
568
|
}
|
|
525
|
-
// ── records / logs / chat — headless inspect + test with the deploy token ────
|
|
526
|
-
/** GET an admin endpoint with the deploy token; returns `{ status, json }`.
|
|
569
|
+
// ── records / cases / logs / chat — headless inspect + test with the deploy token ────
|
|
570
|
+
/** GET an admin endpoint with the deploy token; returns `{ status, json }`.
|
|
571
|
+
* Dies (with the URL) on a network failure; auth failures return so the
|
|
572
|
+
* caller can add command-specific context on top of `authFailureHint`. */
|
|
527
573
|
async function apiGet(endpoint, token) {
|
|
528
|
-
const res = await
|
|
574
|
+
const res = await fetchOrDie(endpoint, { headers: { authorization: `Bearer ${token}` } }, 'request');
|
|
529
575
|
const text = await res.text();
|
|
530
576
|
let json;
|
|
531
577
|
try {
|
|
@@ -567,8 +613,14 @@ async function cmdRecords(flags) {
|
|
|
567
613
|
const { status, json } = await apiGet(`${base}/xrm/records?entity=${encodeURIComponent(entity)}&limit=${limit}`, token);
|
|
568
614
|
if (status === 403)
|
|
569
615
|
die('forbidden — the paged record list needs the `records` plan feature on this tenant');
|
|
570
|
-
if (status !== 200)
|
|
571
|
-
|
|
616
|
+
if (status !== 200) {
|
|
617
|
+
// Always show the server's reason (it names the unknown entity). Cases are
|
|
618
|
+
// casework (worklist), not pack-declared XRM — point at the right command.
|
|
619
|
+
if (entity === 'case' || entity === 'cases') {
|
|
620
|
+
console.error(` '${entity}' is casework (worklist), not a pack-declared XRM entity — inspect tickets with: octwin cases`);
|
|
621
|
+
}
|
|
622
|
+
die(`could not read records (HTTP ${status})${errDetail(json)}${status === 401 ? ` — ${authFailureHint(status, url)}` : ''}`);
|
|
623
|
+
}
|
|
572
624
|
const rows = (json?.records ?? []);
|
|
573
625
|
console.log(`${entity}: ${json?.total ?? rows.length} record(s)`);
|
|
574
626
|
if (rows.length === 0)
|
|
@@ -586,44 +638,191 @@ async function cmdRecords(flags) {
|
|
|
586
638
|
die(`could not read record (HTTP ${status})`);
|
|
587
639
|
console.log(JSON.stringify(json?.record ?? json, null, 2));
|
|
588
640
|
}
|
|
589
|
-
/** `octwin logs [conversationId]` — list conversations
|
|
641
|
+
/** `octwin logs [conversationId] [--as <handle>] [--json]` — list conversations
|
|
642
|
+
* or show one's event timeline (full text + the renders each turn produced). */
|
|
590
643
|
async function cmdLogs(flags) {
|
|
591
644
|
const packDir = resolve(flags.dir ?? '.');
|
|
592
645
|
const { url, tenant, project, token } = resolveTarget(flags, packDir);
|
|
593
646
|
const base = `${url}/api/admin/tenants/${tenant}/projects/${project}`;
|
|
594
647
|
const convId = flags._[0];
|
|
648
|
+
const asJson = flags.json === true;
|
|
649
|
+
const asHandle = typeof flags.as === 'string' ? flags.as : undefined;
|
|
595
650
|
if (!convId) {
|
|
596
|
-
const { status, json } = await apiGet(`${base}/conversations?limit=
|
|
651
|
+
const { status, json } = await apiGet(`${base}/conversations?limit=50`, token);
|
|
597
652
|
if (status !== 200)
|
|
598
|
-
die(`could not read conversations (HTTP ${status})`);
|
|
599
|
-
|
|
653
|
+
die(`could not read conversations (HTTP ${status})${errDetail(json)} — ${authFailureHint(status, url)}`);
|
|
654
|
+
let convs = (json?.conversations ?? []);
|
|
655
|
+
if (asHandle)
|
|
656
|
+
convs = convs.filter((c) => c.contact?.channel_contact_handle === asHandle);
|
|
600
657
|
if (convs.length === 0) {
|
|
601
|
-
console.log('No conversations yet — try `octwin chat "hi"` first.');
|
|
658
|
+
console.log(asHandle ? `No conversations for handle '${asHandle}'.` : 'No conversations yet — try `octwin chat "hi"` first.');
|
|
602
659
|
return;
|
|
603
660
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
661
|
+
if (asJson) {
|
|
662
|
+
console.log(JSON.stringify(convs, null, 2));
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
console.log(`Recent conversations in ${tenant}/${project}${asHandle ? ` (handle: ${asHandle})` : ''}:`);
|
|
666
|
+
for (const c of convs) {
|
|
667
|
+
const handle = c.contact?.channel_contact_handle ?? '?';
|
|
668
|
+
const name = c.contact?.display_name && c.contact.display_name !== handle ? ` (${c.contact.display_name})` : '';
|
|
669
|
+
const when = c.last_event_at ?? c.created_at ?? '';
|
|
670
|
+
console.log(` ${c.id} ${handle}${name} [${c.status}] ${when}`);
|
|
671
|
+
}
|
|
672
|
+
console.log('\nView a timeline: octwin logs <conversationId> (add --json for full payloads)');
|
|
608
673
|
return;
|
|
609
674
|
}
|
|
610
675
|
const { status, json } = await apiGet(`${base}/conversations/${encodeURIComponent(convId)}`, token);
|
|
611
676
|
if (status === 404)
|
|
612
677
|
die(`conversation '${convId}' not found`);
|
|
613
678
|
if (status !== 200)
|
|
614
|
-
die(`could not read conversation (HTTP ${status})`);
|
|
679
|
+
die(`could not read conversation (HTTP ${status})${errDetail(json)} — ${authFailureHint(status, url)}`);
|
|
615
680
|
const events = (json?.events ?? []);
|
|
681
|
+
if (asJson) {
|
|
682
|
+
console.log(JSON.stringify(events, null, 2));
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
616
685
|
console.log(`Timeline for ${convId} (${events.length} events):`);
|
|
617
686
|
for (const e of events) {
|
|
618
687
|
const isErr = e.type === 'tool' && e.subtype === 'platform_error';
|
|
619
688
|
const tag = isErr ? '⚠ ERROR' : `${e.type}${e.subtype ? `/${e.subtype}` : ''}`;
|
|
620
689
|
const body = typeof e.content === 'string' ? e.content : JSON.stringify(e.content ?? '');
|
|
621
|
-
console.log(` ${e.ts ?? ''} ${tag}: ${
|
|
690
|
+
console.log(` ${e.ts ?? ''} ${tag}: ${body ?? ''}`);
|
|
691
|
+
// The renders this turn actually sent (captured outbound payloads) — the
|
|
692
|
+
// other half of the story next to the structured tap lines.
|
|
693
|
+
const captured = Array.isArray(e.metadata?.raw) ? e.metadata.raw : [];
|
|
694
|
+
for (const p of captured) {
|
|
695
|
+
if (!p?.rendered)
|
|
696
|
+
continue;
|
|
697
|
+
console.log(` ↳ rendered ${p.rendered.type ?? '?'}`);
|
|
698
|
+
printHint(p.rendered, ' ');
|
|
699
|
+
}
|
|
622
700
|
}
|
|
701
|
+
console.log('\nVerbatim LLM-thread payloads: --json, or GET …/conversations/<id>/mastra-messages');
|
|
623
702
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
*
|
|
703
|
+
// ── chat: render printing + SSE frame plumbing ──────────────────────────────
|
|
704
|
+
/** Print a render hint's FULL interactive content — every row/button with its
|
|
705
|
+
* tap id, so a follow-up `octwin chat --tap "<id>"` can press it. */
|
|
706
|
+
function printHint(hint, indent = ' ') {
|
|
707
|
+
if (!hint || typeof hint !== 'object')
|
|
708
|
+
return;
|
|
709
|
+
const out = (s) => console.log(`${indent}${s}`);
|
|
710
|
+
if (hint.header)
|
|
711
|
+
out(`─ ${hint.header}`);
|
|
712
|
+
switch (hint.type) {
|
|
713
|
+
case 'buttons':
|
|
714
|
+
for (const b of hint.buttons ?? [])
|
|
715
|
+
out(`[btn] ${b.title} tap: ${b.id}`);
|
|
716
|
+
break;
|
|
717
|
+
case 'list':
|
|
718
|
+
for (const s of hint.sections ?? []) {
|
|
719
|
+
if (s.title)
|
|
720
|
+
out(`── ${s.title}`);
|
|
721
|
+
for (const r of s.rows ?? []) {
|
|
722
|
+
out(`[row] ${r.title}${r.description ? ` — ${r.description}` : ''}`);
|
|
723
|
+
out(` tap: ${r.id}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
if (hint.button_text)
|
|
727
|
+
out(`(list button: ${hint.button_text})`);
|
|
728
|
+
break;
|
|
729
|
+
case 'carousel':
|
|
730
|
+
(hint.cards ?? []).forEach((c, i) => {
|
|
731
|
+
out(`[card ${i + 1}] ${c.body}`);
|
|
732
|
+
for (const b of c.buttons ?? []) {
|
|
733
|
+
if (b.type === 'url')
|
|
734
|
+
out(` [link] ${b.title} → ${b.url}`);
|
|
735
|
+
else
|
|
736
|
+
out(` [btn] ${b.title} tap: ${b.id}`);
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
break;
|
|
740
|
+
case 'cta_url':
|
|
741
|
+
out(`[link] ${hint.label} → ${hint.url}`);
|
|
742
|
+
break;
|
|
743
|
+
case 'flow':
|
|
744
|
+
out(`[flow form] ${hint.cta_text} (flow_id: ${hint.flow_id})`);
|
|
745
|
+
break;
|
|
746
|
+
default:
|
|
747
|
+
// text / commerce variants — the body line already carries the substance.
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
if (hint.footer)
|
|
751
|
+
out(`─ ${hint.footer}`);
|
|
752
|
+
}
|
|
753
|
+
/** Incremental SSE reader: buffers chunks, yields parsed `{ id, ev }` frames,
|
|
754
|
+
* and supports racing a read against a settle-window timeout WITHOUT losing
|
|
755
|
+
* data (the pending read is kept and re-raced — never issued twice). */
|
|
756
|
+
class SseFrameReader {
|
|
757
|
+
reader;
|
|
758
|
+
buf = '';
|
|
759
|
+
decoder = new TextDecoder();
|
|
760
|
+
pending = null;
|
|
761
|
+
queue = [];
|
|
762
|
+
constructor(reader) {
|
|
763
|
+
this.reader = reader;
|
|
764
|
+
}
|
|
765
|
+
/** Release the underlying stream (the body is locked to our reader). */
|
|
766
|
+
async cancel() {
|
|
767
|
+
try {
|
|
768
|
+
await this.reader.cancel();
|
|
769
|
+
}
|
|
770
|
+
catch { /* already closed */ }
|
|
771
|
+
}
|
|
772
|
+
/** Next frame, or 'timeout' after `ms` of silence, or 'done' when the stream ends. */
|
|
773
|
+
async next(ms) {
|
|
774
|
+
for (;;) {
|
|
775
|
+
const queued = this.queue.shift();
|
|
776
|
+
if (queued)
|
|
777
|
+
return queued;
|
|
778
|
+
this.pending ??= this.reader.read();
|
|
779
|
+
const TIMEOUT = Symbol('timeout');
|
|
780
|
+
const winner = await Promise.race([
|
|
781
|
+
this.pending,
|
|
782
|
+
new Promise((res) => setTimeout(() => res(TIMEOUT), ms).unref?.()),
|
|
783
|
+
]);
|
|
784
|
+
if (winner === TIMEOUT)
|
|
785
|
+
return 'timeout';
|
|
786
|
+
this.pending = null;
|
|
787
|
+
const { done, value } = winner;
|
|
788
|
+
if (done)
|
|
789
|
+
return 'done';
|
|
790
|
+
this.buf += this.decoder.decode(value, { stream: true });
|
|
791
|
+
let idx;
|
|
792
|
+
while ((idx = this.buf.indexOf('\n\n')) >= 0) {
|
|
793
|
+
const frame = this.buf.slice(0, idx);
|
|
794
|
+
this.buf = this.buf.slice(idx + 2);
|
|
795
|
+
const lines = frame.split('\n');
|
|
796
|
+
const dataLine = lines.find((l) => l.startsWith('data:'));
|
|
797
|
+
if (!dataLine)
|
|
798
|
+
continue; // heartbeat comments etc.
|
|
799
|
+
const idLine = lines.find((l) => l.startsWith('id:'));
|
|
800
|
+
const id = idLine ? parseInt(idLine.slice(3).trim(), 10) : null;
|
|
801
|
+
let ev;
|
|
802
|
+
try {
|
|
803
|
+
ev = JSON.parse(dataLine.slice(5).trim());
|
|
804
|
+
}
|
|
805
|
+
catch {
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
this.queue.push({ id: Number.isFinite(id) ? id : null, ev });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const REPLAY_SETTLE_MS = 400; // quiet gap that marks the end of the connect replay burst
|
|
814
|
+
const TURN_SETTLE_MS = 2_000; // quiet gap after a render = the turn finished sending
|
|
815
|
+
const REPLY_TIMEOUT_MS = 45_000; // hard cap waiting for the first render of the turn
|
|
816
|
+
/** `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--json]` — drive one
|
|
817
|
+
* turn through the dev web channel and print everything it rendered.
|
|
818
|
+
*
|
|
819
|
+
* Multi-turn works: the platform keeps ONE open conversation per handle, so the
|
|
820
|
+
* same `--as` continues the same conversation. Two traps this command handles:
|
|
821
|
+
* (1) the platform dedups inbound on `local_id` for 5 minutes — so we mint a
|
|
822
|
+
* FRESH id per call (a constant id made consecutive calls silent no-ops);
|
|
823
|
+
* (2) a fresh SSE connect REPLAYS recent history (the ring buffer) — so we
|
|
824
|
+
* drain the replay first and only accept frames newer than it as the reply
|
|
825
|
+
* (naively printing the first render showed LAST turn's message again). */
|
|
627
826
|
async function cmdChat(flags) {
|
|
628
827
|
const packDir = resolve(flags.dir ?? '.');
|
|
629
828
|
const cfg = readPackConfig(packDir);
|
|
@@ -635,84 +834,162 @@ async function cmdChat(flags) {
|
|
|
635
834
|
if (!tenant)
|
|
636
835
|
die('no tenant — set it in pack.json, --tenant, or PACK_TENANT');
|
|
637
836
|
const from = flags.as ?? 'cli-tester';
|
|
837
|
+
const asJson = flags.json === true;
|
|
838
|
+
const tapId = typeof flags.tap === 'string' ? flags.tap : undefined;
|
|
638
839
|
const message = flags._[0];
|
|
639
|
-
if (!message)
|
|
640
|
-
die('usage: octwin chat "your message" [--as <handle>]');
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
evRes = await fetch(`${url}/api/web/events/${tenant}/${project}/${encodeURIComponent(from)}`, { headers: { accept: 'text/event-stream' }, signal: ctrl.signal });
|
|
646
|
-
}
|
|
647
|
-
catch (err) {
|
|
648
|
-
clearTimeout(timer);
|
|
649
|
-
die(`could not open chat stream: ${err?.message ?? err}`);
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if (!evRes.ok || !evRes.body) {
|
|
653
|
-
clearTimeout(timer);
|
|
840
|
+
if (!message && !tapId)
|
|
841
|
+
die('usage: octwin chat "your message" [--as <handle>] [--tap <tap-id>] [--json]');
|
|
842
|
+
// Fresh idempotency key per call (see the command doc above).
|
|
843
|
+
const localId = `cli-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
844
|
+
const evRes = await fetchOrDie(`${url}/api/web/events/${tenant}/${project}/${encodeURIComponent(from)}`, { headers: { accept: 'text/event-stream' } }, 'open chat stream');
|
|
845
|
+
if (!evRes.ok || !evRes.body)
|
|
654
846
|
die(`could not open chat stream (HTTP ${evRes.status})`);
|
|
655
|
-
|
|
847
|
+
const frames = new SseFrameReader(evRes.body.getReader());
|
|
848
|
+
const cancel = () => frames.cancel();
|
|
849
|
+
// Phase 1 — drain the connect replay; remember the highest sequence id.
|
|
850
|
+
// Frames at or below it are history, not this turn's reply.
|
|
851
|
+
let boundary = 0;
|
|
852
|
+
for (;;) {
|
|
853
|
+
const f = await frames.next(REPLAY_SETTLE_MS);
|
|
854
|
+
if (f === 'timeout')
|
|
855
|
+
break;
|
|
856
|
+
if (f === 'done') {
|
|
857
|
+
await cancel();
|
|
858
|
+
die('chat stream closed before the message was sent');
|
|
859
|
+
}
|
|
860
|
+
if (f.id != null && f.id > boundary)
|
|
861
|
+
boundary = f.id;
|
|
656
862
|
}
|
|
657
|
-
|
|
658
|
-
|
|
863
|
+
// Phase 2 — send the inbound (text, or an interactive tap).
|
|
864
|
+
console.log(`→ [${from}] ${tapId ? `(tap) ${tapId}` : message}`);
|
|
865
|
+
const body = tapId
|
|
866
|
+
? { type: 'interactive', from, tap_id: tapId, ...(message ? { raw_title: message } : {}), local_id: localId }
|
|
867
|
+
: { type: 'text', from, text: message, local_id: localId };
|
|
868
|
+
const postRes = await fetchOrDie(`${url}/api/web/inbound/${tenant}/${project}`, {
|
|
659
869
|
method: 'POST',
|
|
660
870
|
headers: { 'content-type': 'application/json' },
|
|
661
|
-
body: JSON.stringify(
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
871
|
+
body: JSON.stringify(body),
|
|
872
|
+
}, 'send message');
|
|
873
|
+
if (!postRes.ok) {
|
|
874
|
+
await cancel();
|
|
875
|
+
die(`send rejected (HTTP ${postRes.status}): ${await postRes.text()}`);
|
|
876
|
+
}
|
|
877
|
+
// Phase 3 — collect THIS turn's renders (id > boundary). A turn can send
|
|
878
|
+
// several messages, so keep reading until a quiet gap after the last render.
|
|
879
|
+
const deadline = Date.now() + REPLY_TIMEOUT_MS;
|
|
880
|
+
let rendersSeen = 0;
|
|
881
|
+
for (;;) {
|
|
882
|
+
const remaining = deadline - Date.now();
|
|
883
|
+
if (remaining <= 0)
|
|
884
|
+
break;
|
|
885
|
+
const f = await frames.next(rendersSeen > 0 ? TURN_SETTLE_MS : Math.min(remaining, REPLY_TIMEOUT_MS));
|
|
886
|
+
if (f === 'timeout') {
|
|
887
|
+
if (rendersSeen > 0)
|
|
671
888
|
break;
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
catch {
|
|
685
|
-
continue;
|
|
686
|
-
}
|
|
687
|
-
if (ev.kind === 'render') {
|
|
688
|
-
console.log(`← ${ev.body ?? '(no text body)'}`);
|
|
689
|
-
const hint = ev.hint;
|
|
690
|
-
if (hint?.buttons?.length)
|
|
691
|
-
console.log(` [buttons: ${hint.buttons.map((b) => b.title).join(' | ')}]`);
|
|
692
|
-
if (hint?.sections?.length || hint?.rows?.length)
|
|
693
|
-
console.log(' [a list picker was rendered]');
|
|
694
|
-
gotReply = true;
|
|
695
|
-
break outer;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
889
|
+
else
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (f === 'done')
|
|
893
|
+
break;
|
|
894
|
+
if (f.id != null && f.id <= boundary)
|
|
895
|
+
continue; // late replay stragglers
|
|
896
|
+
if (asJson) {
|
|
897
|
+
console.log(JSON.stringify(f.ev));
|
|
898
|
+
if (f.ev?.kind === 'render')
|
|
899
|
+
rendersSeen++;
|
|
900
|
+
continue;
|
|
698
901
|
}
|
|
902
|
+
if (f.ev?.kind !== 'render')
|
|
903
|
+
continue; // status/typing noise
|
|
904
|
+
rendersSeen++;
|
|
905
|
+
console.log(`← ${f.ev.body ?? '(no text body)'}${f.ev.hint?.type && f.ev.hint.type !== 'text' ? ` (render: ${f.ev.hint.type})` : ''}`);
|
|
906
|
+
printHint(f.ev.hint);
|
|
699
907
|
}
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
console.error(` (stream error: ${err?.message ?? err})`);
|
|
908
|
+
await cancel();
|
|
909
|
+
if (rendersSeen === 0) {
|
|
910
|
+
console.error(` no reply after ${Math.round(REPLY_TIMEOUT_MS / 1000)}s — the pack may not be warm yet, or the turn produced no render.`);
|
|
911
|
+
process.exit(1);
|
|
705
912
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
913
|
+
console.log(`\n(same --as '${from}' continues this conversation — timeline: octwin logs --as ${from})`);
|
|
914
|
+
}
|
|
915
|
+
/** `octwin cases [caseId] [--queues]` — inspect casework (support tickets):
|
|
916
|
+
* the aggregate inbox, one case + its timeline, or the queue list. */
|
|
917
|
+
async function cmdCases(flags) {
|
|
918
|
+
const packDir = resolve(flags.dir ?? '.');
|
|
919
|
+
const { url, tenant, project, token } = resolveTarget(flags, packDir);
|
|
920
|
+
const base = `${url}/api/admin/tenants/${tenant}/projects/${project}`;
|
|
921
|
+
const caseId = flags._[0];
|
|
922
|
+
const asJson = flags.json === true;
|
|
923
|
+
const caseFail = (what, status, json) => {
|
|
924
|
+
if (status === 403)
|
|
925
|
+
die(`forbidden — casework needs the 'cases' plan feature on this tenant, and a role whose grants reach the queue`);
|
|
926
|
+
die(`could not read ${what} (HTTP ${status})${errDetail(json)}${status === 401 ? ` — ${authFailureHint(status, url)}` : ''}`);
|
|
927
|
+
};
|
|
928
|
+
if (flags.queues === true) {
|
|
929
|
+
const { status, json } = await apiGet(`${base}/case-queues`, token);
|
|
930
|
+
if (status !== 200)
|
|
931
|
+
caseFail('case queues', status, json);
|
|
932
|
+
if (asJson) {
|
|
933
|
+
console.log(JSON.stringify(json, null, 2));
|
|
934
|
+
return;
|
|
710
935
|
}
|
|
711
|
-
|
|
936
|
+
const queues = (json?.queues ?? []);
|
|
937
|
+
console.log(`Case queues in ${tenant}/${project}:`);
|
|
938
|
+
for (const q of queues)
|
|
939
|
+
console.log(` ${q.key}${q.name ? ` (${q.name})` : ''} ${q.open_count} open`);
|
|
940
|
+
if (json?.unrouted_open_count)
|
|
941
|
+
console.log(` (unrouted: ${json.unrouted_open_count} open)`);
|
|
942
|
+
return;
|
|
712
943
|
}
|
|
713
|
-
if (!
|
|
714
|
-
|
|
715
|
-
|
|
944
|
+
if (!caseId) {
|
|
945
|
+
const limit = flags.limit ?? '50';
|
|
946
|
+
const { status, json } = await apiGet(`${base}/cases?limit=${limit}`, token);
|
|
947
|
+
if (status !== 200)
|
|
948
|
+
caseFail('cases', status, json);
|
|
949
|
+
if (asJson) {
|
|
950
|
+
console.log(JSON.stringify(json, null, 2));
|
|
951
|
+
return;
|
|
952
|
+
}
|
|
953
|
+
const rows = (json?.cases ?? []);
|
|
954
|
+
console.log(`Cases in ${tenant}/${project}: ${json?.total ?? rows.length} total`);
|
|
955
|
+
if (rows.length === 0)
|
|
956
|
+
console.log(' (none)');
|
|
957
|
+
for (const c of rows) {
|
|
958
|
+
const sla = c.sla_due_at ? ` sla:${c.sla_due_at}` : '';
|
|
959
|
+
console.log(` #${c.case_number ?? '?'} ${c.type} [${c.status}] ${c.priority}${c.queue_key ? ` q:${c.queue_key}` : ''}${sla} ${c.id}`);
|
|
960
|
+
}
|
|
961
|
+
console.log('\nOne case + timeline: octwin cases <caseId> queues: octwin cases --queues');
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const { status, json } = await apiGet(`${base}/cases/${encodeURIComponent(caseId)}`, token);
|
|
965
|
+
if (status === 404)
|
|
966
|
+
die(`case '${caseId}' not found`);
|
|
967
|
+
if (status !== 200)
|
|
968
|
+
caseFail('case', status, json);
|
|
969
|
+
if (asJson) {
|
|
970
|
+
console.log(JSON.stringify(json, null, 2));
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
const c = json?.case ?? {};
|
|
974
|
+
console.log(`Case #${c.case_number ?? '?'} ${c.type} [${c.status}] ${c.priority}`);
|
|
975
|
+
console.log(` id: ${c.id} queue: ${c.queue_key ?? '(unrouted)'} assignee: ${c.assignee_principal ?? '(none)'}`);
|
|
976
|
+
if (json?.contact)
|
|
977
|
+
console.log(` contact: ${json.contact.display_name ?? json.contact.channel_contact_handle ?? json.contact.id}`);
|
|
978
|
+
if (c.conversation_id)
|
|
979
|
+
console.log(` conversation: ${c.conversation_id} (octwin logs ${c.conversation_id})`);
|
|
980
|
+
if (c.sla_due_at)
|
|
981
|
+
console.log(` sla due: ${c.sla_due_at}`);
|
|
982
|
+
if (c.fields && Object.keys(c.fields).length > 0)
|
|
983
|
+
console.log(` fields: ${JSON.stringify(c.fields)}`);
|
|
984
|
+
const events = (json?.events ?? []);
|
|
985
|
+
console.log(` Timeline (${events.length}):`);
|
|
986
|
+
for (const e of events) {
|
|
987
|
+
const payload = e.payload && Object.keys(e.payload).length > 0 ? ` ${JSON.stringify(e.payload)}` : '';
|
|
988
|
+
console.log(` ${e.ts ?? ''} ${e.kind}${e.actor ? ` (${e.actor})` : ''}${payload}`);
|
|
989
|
+
}
|
|
990
|
+
const dispositions = (json?.dispositions ?? []);
|
|
991
|
+
if (dispositions.length > 0) {
|
|
992
|
+
console.log(` Decisions: ${dispositions.map((d) => `${d.action}${d.next_status ? `→${d.next_status}` : ''}`).join(', ')}`);
|
|
716
993
|
}
|
|
717
994
|
}
|
|
718
995
|
function help() {
|
|
@@ -726,18 +1003,67 @@ function help() {
|
|
|
726
1003
|
octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
727
1004
|
octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>]
|
|
728
1005
|
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
729
|
-
octwin
|
|
730
|
-
octwin
|
|
1006
|
+
octwin cases [caseId] [--queues] [--json] # inspect casework (support tickets) + timelines
|
|
1007
|
+
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
1008
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--json] # drive a turn + print every render
|
|
731
1009
|
octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
|
|
732
1010
|
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
733
1011
|
|
|
1012
|
+
Multi-turn: the platform keeps ONE open conversation per --as handle — consecutive
|
|
1013
|
+
\`octwin chat --as <h>\` calls continue the same conversation; press a rendered
|
|
1014
|
+
button/row with \`--tap "<tap-id>"\` (chat prints every tap id).
|
|
734
1015
|
Get a deploy token: console → your workspace → Settings → API tokens → Generate (tick records:read to inspect data).
|
|
735
1016
|
octwin platform-kb pull → writes the platform capability reference into .octwin/platform-kb/ (for the octwin-pack skill).
|
|
736
|
-
Config (deploy): flags > pack.json > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login
|
|
1017
|
+
Config (deploy): flags > pack.json > env (PACK_PLATFORM_URL/PACK_TENANT/PACK_PROJECT/PACK_TOKEN) > saved login.
|
|
1018
|
+
Per-command usage: octwin <command> --help`);
|
|
737
1019
|
}
|
|
1020
|
+
/** Per-subcommand usage — printed for `octwin <cmd> --help|-h` BEFORE any
|
|
1021
|
+
* network/auth work (a --help that 401s is worse than no help at all). */
|
|
1022
|
+
const COMMAND_HELP = {
|
|
1023
|
+
init: `octwin init <dir> [--id my-pack] [--description "..."] [--display-name "..."]
|
|
1024
|
+
Scaffold a pure-YAML starter pack into <dir>.`,
|
|
1025
|
+
validate: `octwin validate [--dir .] [--remote]
|
|
1026
|
+
Offline structural check; --remote additionally runs the platform's FULL
|
|
1027
|
+
manifest + flow-DSL validation (all errors at once) — same check as deploy.`,
|
|
1028
|
+
login: `octwin login --url <platformUrl> --token oct_…
|
|
1029
|
+
Save a deploy token (console → Settings → API tokens) for that platform url.`,
|
|
1030
|
+
whoami: `octwin whoami [--url <url>] [--tenant <slug>]
|
|
1031
|
+
Verify the resolved token authenticates against the tenant.`,
|
|
1032
|
+
deploy: `octwin deploy [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>] [--token <t>] [--seed]
|
|
1033
|
+
Upload the pack bundle, validate server-side, install onto the project.
|
|
1034
|
+
--seed additionally applies the pack's demo seed (streams progress).`,
|
|
1035
|
+
status: `octwin status [--dir .] [--url <url>] [--tenant <slug>] [--project <slug>]
|
|
1036
|
+
Show installed vs live version + the flow list for this pack.`,
|
|
1037
|
+
records: `octwin records [entity] [id] [--limit 50]
|
|
1038
|
+
Inspect the pack's XRM data. No args = list entities. Cases/tickets are
|
|
1039
|
+
casework, not XRM — use \`octwin cases\` for those.`,
|
|
1040
|
+
cases: `octwin cases [caseId] [--queues] [--limit 50] [--json]
|
|
1041
|
+
Inspect casework (support tickets): the inbox, one case + its timeline
|
|
1042
|
+
(+ applicable decisions), or --queues for queue keys + open counts.`,
|
|
1043
|
+
logs: `octwin logs [conversationId] [--as <handle>] [--json]
|
|
1044
|
+
No id = recent conversations (handle, status, last activity; --as filters).
|
|
1045
|
+
With id = the full event timeline including what each turn rendered.
|
|
1046
|
+
--json = raw events (verbatim payloads).`,
|
|
1047
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--json]
|
|
1048
|
+
Drive one turn through the dev web channel and print every render with its
|
|
1049
|
+
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
1050
|
+
--tap presses a rendered button/list row instead of sending text.
|
|
1051
|
+
--json dumps the raw SSE envelopes for the turn.`,
|
|
1052
|
+
'platform-kb': `octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
|
|
1053
|
+
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
1054
|
+
.octwin/platform-kb/ for the octwin-pack authoring skill.`,
|
|
1055
|
+
test: `octwin test [--dir .]
|
|
1056
|
+
Alias for \`octwin validate --remote\` — the full platform check.`,
|
|
1057
|
+
};
|
|
738
1058
|
async function main() {
|
|
739
1059
|
const [command, ...rest] = process.argv.slice(2);
|
|
740
1060
|
const flags = parseFlags(rest);
|
|
1061
|
+
// Per-subcommand --help/-h — intercepted BEFORE the command runs, so help can
|
|
1062
|
+
// never hit the network or die on auth (author-feedback A8).
|
|
1063
|
+
if (command && command in COMMAND_HELP && (flags.help === true || flags._.includes('-h'))) {
|
|
1064
|
+
console.log(COMMAND_HELP[command]);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
741
1067
|
switch (command) {
|
|
742
1068
|
case 'init':
|
|
743
1069
|
cmdInit(flags);
|
|
@@ -760,6 +1086,9 @@ async function main() {
|
|
|
760
1086
|
case 'records':
|
|
761
1087
|
await cmdRecords(flags);
|
|
762
1088
|
break;
|
|
1089
|
+
case 'cases':
|
|
1090
|
+
await cmdCases(flags);
|
|
1091
|
+
break;
|
|
763
1092
|
case 'logs':
|
|
764
1093
|
await cmdLogs(flags);
|
|
765
1094
|
break;
|
package/package.json
CHANGED