octwin-cli 0.1.12 → 0.1.13
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/CHANGELOG.md +22 -0
- package/dist/index.js +213 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ Format: [Keep a Changelog](https://keepachangelog.com/) — newest first, bucket
|
|
|
5
5
|
**Added · Changed · Deprecated · Removed · Fixed · Security**. The platform-wide view lives in the
|
|
6
6
|
repo root [`CHANGELOG.md`](../../CHANGELOG.md); this file is the CLI-only cut that ships with the package.
|
|
7
7
|
|
|
8
|
+
## [0.1.13] - 2026-07-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **A headless media loop — produce + send.** Two paired additions close the coverage hole where any
|
|
12
|
+
media-collect flow (e.g. `activate-app`'s registration / ID uploads) stalled at the upload prompt
|
|
13
|
+
because the CLI could neither *make* nor *send* an image:
|
|
14
|
+
- `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]` — AI-generates an
|
|
15
|
+
image on the platform (needs a `media:generate`-scoped token), stores it as a public asset, and prints
|
|
16
|
+
its `MEDIA-` handle + serve URL. `--out` downloads the bytes to a file (WhatsApp renders only
|
|
17
|
+
`.png`/`.jpg`); `--json` emits `{ media_id, url, mime, width, height, bytes }`.
|
|
18
|
+
- `octwin chat --media <file|media-id>` — uploads a local file (or a media id from
|
|
19
|
+
`media generate --json`) as an image/document/audio inbound; any accompanying `"message"` rides as
|
|
20
|
+
its caption. The platform's media pipeline folds the upload into a running collect, so media flows
|
|
21
|
+
are now fully drivable headlessly.
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
- **The KB-drift nudge now names what changed.** With the platform serving per-entry content hashes, the
|
|
25
|
+
post-command nudge appends a `(N changed · M added · K removed)` summary instead of a bare hash pair.
|
|
26
|
+
- **`octwin platform-kb` prints a changelog on pull.** Instead of only a doc/catalog count, the pull now
|
|
27
|
+
diffs the fresh index against your last pull and lists exactly which docs/catalogs were added (`+`),
|
|
28
|
+
changed (`~`), or removed (`-`) — so a replaced schema shape is visible, not silent.
|
|
29
|
+
|
|
8
30
|
## [0.1.12] - 2026-07-22
|
|
9
31
|
|
|
10
32
|
### Fixed
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* octwin records [entity] [id] # inspect the pack's XRM data (records:read token)
|
|
18
18
|
* octwin cases [caseId] [--queues] # inspect casework (support tickets) — list / one case + timeline
|
|
19
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
|
+
* octwin chat "msg" [--as h] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn via the web channel (+ send media)
|
|
21
|
+
* octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (media:generate scope)
|
|
21
22
|
* octwin platform-kb [pull] [--dir .] # pull the platform capability reference for the authoring skill
|
|
22
23
|
* octwin test [--dir .] # = validate --remote (the full platform check)
|
|
23
24
|
*
|
|
@@ -35,7 +36,7 @@
|
|
|
35
36
|
* (pure-YAML enforcement + manifest/flow Zod) and installs it onto the project.
|
|
36
37
|
*/
|
|
37
38
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync, cpSync } from 'node:fs';
|
|
38
|
-
import { join, resolve, dirname } from 'node:path';
|
|
39
|
+
import { join, resolve, dirname, basename } from 'node:path';
|
|
39
40
|
import { homedir } from 'node:os';
|
|
40
41
|
import { fileURLToPath } from 'node:url';
|
|
41
42
|
import { parse as parseYaml } from 'yaml';
|
|
@@ -112,6 +113,50 @@ function errDetail(json) {
|
|
|
112
113
|
const msg = typeof json === 'string' ? json : (json.error ?? json.message ?? JSON.stringify(json));
|
|
113
114
|
return msg ? ` — ${msg}` : '';
|
|
114
115
|
}
|
|
116
|
+
// ── media helpers (produce/send loop) ───────────────────────────────────────
|
|
117
|
+
/** Extension → MIME for local files the `chat --media` sender uploads (mirrors
|
|
118
|
+
* the platform's accepted image/document/audio MIME sets). */
|
|
119
|
+
const MEDIA_MIME_BY_EXT = {
|
|
120
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp',
|
|
121
|
+
'.pdf': 'application/pdf', '.txt': 'text/plain',
|
|
122
|
+
'.doc': 'application/msword',
|
|
123
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
124
|
+
'.ogg': 'audio/ogg', '.mp3': 'audio/mpeg', '.m4a': 'audio/mp4', '.wav': 'audio/wav', '.webm': 'audio/webm', '.aac': 'audio/aac',
|
|
125
|
+
};
|
|
126
|
+
/** MIME → file extension (for naming a fetched-by-id media part). */
|
|
127
|
+
const EXT_BY_MEDIA_MIME = {
|
|
128
|
+
'image/jpeg': 'jpg', 'image/png': 'png', 'image/gif': 'gif', 'image/webp': 'webp', 'application/pdf': 'pdf',
|
|
129
|
+
};
|
|
130
|
+
/** The inbound `type` the platform expects for a MIME (image/document/audio). */
|
|
131
|
+
function mediaKindOf(mime) {
|
|
132
|
+
if (mime.startsWith('image/'))
|
|
133
|
+
return 'image';
|
|
134
|
+
if (mime.startsWith('audio/'))
|
|
135
|
+
return 'audio';
|
|
136
|
+
return 'document';
|
|
137
|
+
}
|
|
138
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
139
|
+
/** Resolve a `--media` argument to an uploadable part: a local file path, OR a
|
|
140
|
+
* media UUID (as returned by `octwin media generate --json`) fetched from the
|
|
141
|
+
* public serve route. The cosmetic `MEDIA-…` handle is NOT fetchable (the serve
|
|
142
|
+
* route is UUID-keyed) — use the file written by `--out`, or the `media_id`. */
|
|
143
|
+
async function resolveMediaPart(url, arg) {
|
|
144
|
+
if (existsSync(arg)) {
|
|
145
|
+
const buffer = readFileSync(arg);
|
|
146
|
+
const ext = arg.slice(arg.lastIndexOf('.')).toLowerCase();
|
|
147
|
+
const mime = MEDIA_MIME_BY_EXT[ext] ?? 'application/octet-stream';
|
|
148
|
+
return { blob: new Blob([buffer], { type: mime }), kind: mediaKindOf(mime), filename: basename(arg) };
|
|
149
|
+
}
|
|
150
|
+
if (UUID_RE.test(arg)) {
|
|
151
|
+
const res = await fetchOrDie(`${url}/api/media/${arg}`, undefined, 'fetch media');
|
|
152
|
+
if (!res.ok)
|
|
153
|
+
die(`--media '${arg}' not found (HTTP ${res.status}) — pass a local file path, or a media id from 'octwin media generate --json'`);
|
|
154
|
+
const mime = res.headers.get('content-type') ?? 'application/octet-stream';
|
|
155
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
156
|
+
return { blob: new Blob([buffer], { type: mime }), kind: mediaKindOf(mime), filename: `${arg}.${EXT_BY_MEDIA_MIME[mime] ?? 'bin'}` };
|
|
157
|
+
}
|
|
158
|
+
die(`--media '${arg}' is neither an existing file nor a media id (UUID). The cosmetic MEDIA-… handle isn't fetchable — send the file written by 'octwin media generate --out', or pass the media_id from '--json'.`);
|
|
159
|
+
}
|
|
115
160
|
// ── bundle collection ───────────────────────────────────────────────────────
|
|
116
161
|
const SKIP_DIRS = new Set(['.git', 'node_modules', '.pack-bundles', 'dist', '.mastra']);
|
|
117
162
|
/** Collect every text file under `packDir` into a `{ relPath: content }` map. */
|
|
@@ -243,18 +288,34 @@ async function notifyIfOutdated() {
|
|
|
243
288
|
}
|
|
244
289
|
catch { /* a version check must never break the CLI */ }
|
|
245
290
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
function readLocalKbHash(packDir) {
|
|
291
|
+
/** A previously-pulled KB's identity in `<packDir>/.octwin/platform-kb/index.json`
|
|
292
|
+
* (content hash + per-entry index), or null if nothing has been pulled yet. */
|
|
293
|
+
function readLocalKb(packDir) {
|
|
250
294
|
try {
|
|
251
295
|
const idx = JSON.parse(readFileSync(join(packDir, '.octwin', 'platform-kb', 'index.json'), 'utf8'));
|
|
252
|
-
return
|
|
296
|
+
return {
|
|
297
|
+
content_hash: typeof idx.content_hash === 'string' ? idx.content_hash : null,
|
|
298
|
+
index: Array.isArray(idx.index) ? idx.index : [],
|
|
299
|
+
};
|
|
253
300
|
}
|
|
254
301
|
catch {
|
|
255
302
|
return null;
|
|
256
303
|
}
|
|
257
304
|
}
|
|
305
|
+
/** Diff two KB indexes by key + per-entry `hash` → added / removed / changed keys. */
|
|
306
|
+
function diffKbIndex(prev, next) {
|
|
307
|
+
const prevMap = new Map(prev.map(e => [e.key, e.hash]));
|
|
308
|
+
const nextKeys = new Set(next.map(e => e.key));
|
|
309
|
+
const added = [], changed = [];
|
|
310
|
+
for (const e of next) {
|
|
311
|
+
if (!prevMap.has(e.key))
|
|
312
|
+
added.push(e.key);
|
|
313
|
+
else if (prevMap.get(e.key) !== e.hash)
|
|
314
|
+
changed.push(e.key);
|
|
315
|
+
}
|
|
316
|
+
const removed = prev.filter(e => !nextKeys.has(e.key)).map(e => e.key);
|
|
317
|
+
return { added, removed, changed };
|
|
318
|
+
}
|
|
258
319
|
/** Nudge (to stderr) when the platform's capability KB has changed since the last
|
|
259
320
|
* `octwin platform-kb pull`. The sibling of `notifyIfOutdated`, for the KB instead
|
|
260
321
|
* of the CLI: run only after commands that already hit the platform, so this adds
|
|
@@ -266,8 +327,8 @@ async function notifyIfKbStale(flags) {
|
|
|
266
327
|
return;
|
|
267
328
|
try {
|
|
268
329
|
const packDir = resolve(flags.dir ?? '.');
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
330
|
+
const local = readLocalKb(packDir);
|
|
331
|
+
if (!local?.content_hash)
|
|
271
332
|
return; // never pulled → the skill already says to pull
|
|
272
333
|
const t = resolveTargetOrNull(flags, packDir);
|
|
273
334
|
if (!t)
|
|
@@ -281,8 +342,21 @@ async function notifyIfKbStale(flags) {
|
|
|
281
342
|
if (!res.ok)
|
|
282
343
|
return;
|
|
283
344
|
const meta = await res.json();
|
|
284
|
-
if (meta.content_hash && meta.content_hash !==
|
|
285
|
-
|
|
345
|
+
if (meta.content_hash && meta.content_hash !== local.content_hash) {
|
|
346
|
+
// Per-entry summary (now that the index carries per-entry hashes) — the
|
|
347
|
+
// exact list of what changed is one `octwin platform-kb` away.
|
|
348
|
+
let summary = '';
|
|
349
|
+
if (Array.isArray(meta.index) && local.index.length) {
|
|
350
|
+
const { added, removed, changed } = diffKbIndex(local.index, meta.index);
|
|
351
|
+
const parts = [
|
|
352
|
+
changed.length && `${changed.length} changed`,
|
|
353
|
+
added.length && `${added.length} added`,
|
|
354
|
+
removed.length && `${removed.length} removed`,
|
|
355
|
+
].filter(Boolean);
|
|
356
|
+
if (parts.length)
|
|
357
|
+
summary = ` (${parts.join(' · ')})`;
|
|
358
|
+
}
|
|
359
|
+
console.error(`\n⬆ the platform capability reference changed since you last pulled it${summary}.`);
|
|
286
360
|
console.error(' Refresh it: octwin platform-kb');
|
|
287
361
|
}
|
|
288
362
|
}
|
|
@@ -298,6 +372,7 @@ function commandTouchesPlatform(command, flags) {
|
|
|
298
372
|
case 'status':
|
|
299
373
|
case 'test':
|
|
300
374
|
case 'chat':
|
|
375
|
+
case 'media':
|
|
301
376
|
case 'records':
|
|
302
377
|
case 'cases':
|
|
303
378
|
case 'logs':
|
|
@@ -631,6 +706,9 @@ async function cmdPlatformKb(flags) {
|
|
|
631
706
|
process.exit(1);
|
|
632
707
|
}
|
|
633
708
|
const bundle = JSON.parse(text);
|
|
709
|
+
// Snapshot the prior pull's index BEFORE overwriting it, so we can show the
|
|
710
|
+
// author EXACTLY what changed (not just a count).
|
|
711
|
+
const prior = readLocalKb(packDir);
|
|
634
712
|
// Write the reference into <packDir>/.octwin/platform-kb/ — markdown docs (the
|
|
635
713
|
// skill reads these first) + JSON catalogs (precise field schemas). Gitignored.
|
|
636
714
|
const outDir = join(packDir, '.octwin', 'platform-kb');
|
|
@@ -654,6 +732,23 @@ async function cmdPlatformKb(flags) {
|
|
|
654
732
|
writeFileSync(join(outDir, 'index.json'), JSON.stringify({ version: bundle.version, content_hash: bundle.content_hash, generated_at: bundle.generated_at, index: bundle.index }, null, 2) + '\n', 'utf8');
|
|
655
733
|
console.log(`✓ Pulled the Octwin platform KB → ${outDir}`);
|
|
656
734
|
console.log(` ${mdCount} markdown docs + ${jsonCount} JSON catalogs (reference version ${bundle.version ?? '?'})`);
|
|
735
|
+
// Changelog since the last pull — per-entry hashes tell us WHICH docs/catalogs
|
|
736
|
+
// moved (a schema shape being replaced shows as a `~ changed`), not just a count.
|
|
737
|
+
if (prior?.content_hash) {
|
|
738
|
+
if (prior.content_hash === bundle.content_hash) {
|
|
739
|
+
console.log(' (no content changes since your last pull)');
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
const { added, removed, changed } = diffKbIndex(prior.index, bundle.index ?? []);
|
|
743
|
+
console.log(` Δ changes since your last pull (${prior.content_hash} → ${bundle.content_hash ?? '?'}):`);
|
|
744
|
+
for (const k of added)
|
|
745
|
+
console.log(` + ${k} (new)`);
|
|
746
|
+
for (const k of changed)
|
|
747
|
+
console.log(` ~ ${k} (changed)`);
|
|
748
|
+
for (const k of removed)
|
|
749
|
+
console.log(` - ${k} (removed)`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
657
752
|
console.log(' The octwin-pack authoring skill reads these as the source of truth for what the platform supports.');
|
|
658
753
|
}
|
|
659
754
|
// ── records / cases / logs / chat — headless inspect + test with the deploy token ────
|
|
@@ -931,9 +1026,10 @@ async function cmdChat(flags) {
|
|
|
931
1026
|
const from = flags.as ?? 'cli-tester';
|
|
932
1027
|
const asJson = flags.json === true;
|
|
933
1028
|
const tapId = typeof flags.tap === 'string' ? flags.tap : undefined;
|
|
1029
|
+
const mediaArg = typeof flags.media === 'string' ? flags.media : undefined;
|
|
934
1030
|
const message = flags._[0];
|
|
935
|
-
if (!message && !tapId)
|
|
936
|
-
die('usage: octwin chat "your message" [--as <handle>] [--tap <tap-id>] [--json]');
|
|
1031
|
+
if (!message && !tapId && !mediaArg)
|
|
1032
|
+
die('usage: octwin chat "your message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]');
|
|
937
1033
|
// Fresh idempotency key per call (see the command doc above).
|
|
938
1034
|
const localId = `cli-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
939
1035
|
if (!asJson)
|
|
@@ -957,16 +1053,35 @@ async function cmdChat(flags) {
|
|
|
957
1053
|
if (f.id != null && f.id > boundary)
|
|
958
1054
|
boundary = f.id;
|
|
959
1055
|
}
|
|
960
|
-
// Phase 2 — send the inbound (
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1056
|
+
// Phase 2 — send the inbound (media upload, an interactive tap, or text).
|
|
1057
|
+
const inbound = `${url}/api/web/inbound/${tenant}/${project}`;
|
|
1058
|
+
let postRes;
|
|
1059
|
+
if (mediaArg && !tapId) {
|
|
1060
|
+
// Multipart upload → the platform's media pipeline sets $inbound_media, which
|
|
1061
|
+
// a running collect folds into $state.<field> (docs/19 §8a). Any accompanying
|
|
1062
|
+
// message rides as the media caption.
|
|
1063
|
+
const { blob, kind, filename } = await resolveMediaPart(url, mediaArg);
|
|
1064
|
+
console.log(`→ [${from}] (media:${kind}) ${filename}${message ? ` — "${message}"` : ''}`);
|
|
1065
|
+
const form = new FormData();
|
|
1066
|
+
form.append('type', kind);
|
|
1067
|
+
form.append('from', from);
|
|
1068
|
+
form.append('local_id', localId);
|
|
1069
|
+
if (message)
|
|
1070
|
+
form.append('caption', message);
|
|
1071
|
+
form.append('file', blob, filename);
|
|
1072
|
+
postRes = await fetchOrDie(inbound, { method: 'POST', body: form }, 'send media'); // fetch sets the multipart boundary
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
console.log(`→ [${from}] ${tapId ? `(tap) ${tapId}` : message}`);
|
|
1076
|
+
const body = tapId
|
|
1077
|
+
? { type: 'interactive', from, tap_id: tapId, ...(message ? { raw_title: message } : {}), local_id: localId }
|
|
1078
|
+
: { type: 'text', from, text: message, local_id: localId };
|
|
1079
|
+
postRes = await fetchOrDie(inbound, {
|
|
1080
|
+
method: 'POST',
|
|
1081
|
+
headers: { 'content-type': 'application/json' },
|
|
1082
|
+
body: JSON.stringify(body),
|
|
1083
|
+
}, 'send message');
|
|
1084
|
+
}
|
|
970
1085
|
if (!postRes.ok) {
|
|
971
1086
|
await cancel();
|
|
972
1087
|
die(`send rejected (HTTP ${postRes.status}): ${await postRes.text()}`);
|
|
@@ -1011,6 +1126,67 @@ async function cmdChat(flags) {
|
|
|
1011
1126
|
}
|
|
1012
1127
|
console.log(`\n(same --as '${from}' continues this conversation — timeline: octwin logs --as ${from})`);
|
|
1013
1128
|
}
|
|
1129
|
+
/** `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]`
|
|
1130
|
+
* — AI-generate an image on the platform (needs a `media:generate`-scoped token),
|
|
1131
|
+
* store it as a public asset, and return its `MEDIA-` handle + serve URL. `--out`
|
|
1132
|
+
* downloads the bytes to a file (WhatsApp renders only `.png`/`.jpg`); the paired
|
|
1133
|
+
* `octwin chat --media <file|id>` feeds it into a running media-collect flow. */
|
|
1134
|
+
async function cmdMedia(flags) {
|
|
1135
|
+
const sub = flags._[0];
|
|
1136
|
+
if (sub !== 'generate')
|
|
1137
|
+
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]');
|
|
1138
|
+
const packDir = resolve(flags.dir ?? '.');
|
|
1139
|
+
const { url, tenant, project, token } = resolveTarget(flags, packDir);
|
|
1140
|
+
const prompt = flags._[1];
|
|
1141
|
+
if (!prompt)
|
|
1142
|
+
die('usage: octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]');
|
|
1143
|
+
const asJson = flags.json === true;
|
|
1144
|
+
const size = typeof flags.size === 'string' ? flags.size : undefined;
|
|
1145
|
+
const out = typeof flags.out === 'string' ? flags.out : undefined;
|
|
1146
|
+
if (!asJson)
|
|
1147
|
+
console.log(`→ Generating an image on '${tenant}/${project}' @ ${url} …`);
|
|
1148
|
+
const res = await fetchOrDie(`${url}/api/admin/tenants/${tenant}/projects/${project}/media/generate`, {
|
|
1149
|
+
method: 'POST',
|
|
1150
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${token}` },
|
|
1151
|
+
body: JSON.stringify({ prompt, ...(size ? { size } : {}) }),
|
|
1152
|
+
}, 'media generate');
|
|
1153
|
+
const text = await res.text();
|
|
1154
|
+
if (!res.ok) {
|
|
1155
|
+
let j;
|
|
1156
|
+
try {
|
|
1157
|
+
j = JSON.parse(text);
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
j = text;
|
|
1161
|
+
}
|
|
1162
|
+
console.error(`✗ media generate failed (HTTP ${res.status})${errDetail(j)}`);
|
|
1163
|
+
printAuthHint(res.status, url);
|
|
1164
|
+
process.exit(1);
|
|
1165
|
+
}
|
|
1166
|
+
const r = JSON.parse(text);
|
|
1167
|
+
const absUrl = /^https?:/i.test(r.url) ? r.url : `${url}${r.url}`;
|
|
1168
|
+
// --out → download the bytes so the paired `chat --media <file>` can send them.
|
|
1169
|
+
if (out) {
|
|
1170
|
+
const dl = await fetchOrDie(absUrl, undefined, 'download generated image');
|
|
1171
|
+
if (!dl.ok)
|
|
1172
|
+
die(`could not download the generated image (HTTP ${dl.status})`);
|
|
1173
|
+
writeFileSync(out, Buffer.from(await dl.arrayBuffer()));
|
|
1174
|
+
const ext = out.slice(out.lastIndexOf('.')).toLowerCase();
|
|
1175
|
+
if (ext !== '.png' && ext !== '.jpg' && ext !== '.jpeg') {
|
|
1176
|
+
console.error(` ⚠ '${out}' isn't a .png/.jpg — WhatsApp only renders those extensions.`);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
if (asJson) {
|
|
1180
|
+
console.log(JSON.stringify({ media_id: r.media_id, url: absUrl, mime: r.mime, width: r.width, height: r.height, bytes: r.bytes }));
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
console.log(`✓ Generated ${r.media_ref} (${r.width}×${r.height}, ${r.mime}, ${r.bytes} bytes)`);
|
|
1184
|
+
console.log(` id: ${r.media_id}`);
|
|
1185
|
+
console.log(` url: ${absUrl}`);
|
|
1186
|
+
if (out)
|
|
1187
|
+
console.log(` saved → ${out}`);
|
|
1188
|
+
console.log(` Send it into a chat: octwin chat "here you go" --media ${out ?? r.media_id} --as <handle>`);
|
|
1189
|
+
}
|
|
1014
1190
|
/** `octwin cases [caseId] [--queues]` — inspect casework (support tickets):
|
|
1015
1191
|
* the aggregate inbox, one case + its timeline, or the queue list. */
|
|
1016
1192
|
async function cmdCases(flags) {
|
|
@@ -1106,7 +1282,8 @@ function help() {
|
|
|
1106
1282
|
octwin records [entity] [id] # inspect the pack's XRM data (needs a records:read token)
|
|
1107
1283
|
octwin cases [caseId] [--queues] [--json] # inspect casework (support tickets) + timelines
|
|
1108
1284
|
octwin logs [conversationId] [--as <handle>] [--json] # list conversations / show one's event timeline
|
|
1109
|
-
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--json] # drive a turn + print every render
|
|
1285
|
+
octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json] # drive a turn (+ send media) + print every render
|
|
1286
|
+
octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json] # AI-generate an image → MEDIA- handle (needs media:generate scope)
|
|
1110
1287
|
octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
|
|
1111
1288
|
octwin test [--dir .] # = validate --remote (the full platform check)
|
|
1112
1289
|
|
|
@@ -1145,11 +1322,19 @@ const COMMAND_HELP = {
|
|
|
1145
1322
|
No id = recent conversations (handle, status, last activity; --as filters).
|
|
1146
1323
|
With id = the full event timeline including what each turn rendered.
|
|
1147
1324
|
--json = raw events (verbatim payloads).`,
|
|
1148
|
-
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--json]
|
|
1325
|
+
chat: `octwin chat "message" [--as <handle>] [--tap <tap-id>] [--media <file|id>] [--json]
|
|
1149
1326
|
Drive one turn through the dev web channel and print every render with its
|
|
1150
1327
|
tap ids. Same --as handle = same conversation (multi-turn works).
|
|
1151
1328
|
--tap presses a rendered button/list row instead of sending text.
|
|
1329
|
+
--media uploads a local file (or a media id from 'media generate --json') as
|
|
1330
|
+
an image/document/audio inbound — any "message" rides as its caption; feeds a
|
|
1331
|
+
running media-collect flow (e.g. activate-app).
|
|
1152
1332
|
--json dumps the raw SSE envelopes for the turn.`,
|
|
1333
|
+
media: `octwin media generate "<prompt>" [--out <file.png>] [--size 1024x1024] [--json]
|
|
1334
|
+
AI-generate an image (needs a media:generate-scoped token), store it as a
|
|
1335
|
+
public asset, and print its MEDIA- handle + serve URL. --out downloads the
|
|
1336
|
+
bytes (WhatsApp renders only .png/.jpg); --json emits { media_id, url, mime,
|
|
1337
|
+
width, height, bytes }. Pair with 'octwin chat --media' to drive media flows.`,
|
|
1153
1338
|
'platform-kb': `octwin platform-kb [pull] [--dir .] [--url <url>] [--tenant <slug>] [--token <t>]
|
|
1154
1339
|
Pull the platform capability reference (markdown + JSON catalogs) into
|
|
1155
1340
|
.octwin/platform-kb/ for the octwin-pack authoring skill.`,
|
|
@@ -1196,6 +1381,9 @@ async function main() {
|
|
|
1196
1381
|
case 'chat':
|
|
1197
1382
|
await cmdChat(flags);
|
|
1198
1383
|
break;
|
|
1384
|
+
case 'media':
|
|
1385
|
+
await cmdMedia(flags);
|
|
1386
|
+
break;
|
|
1199
1387
|
case 'platform-kb':
|
|
1200
1388
|
await cmdPlatformKb(flags);
|
|
1201
1389
|
break;
|
package/package.json
CHANGED