@302ai/media-studio-cli 0.1.0-beta.6 → 0.1.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 +29 -12
- package/dist/index.js +322 -86
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,16 +10,9 @@ npm install -g @302ai/media-studio-cli
|
|
|
10
10
|
# requires Node.js >= 22
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
npm install -g @302ai/media-studio-cli@beta
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
> **No stable release exists yet.** `latest` points at a prerelease, so both
|
|
20
|
-
> commands above currently install the same build. Staging- and Local-server
|
|
21
|
-
> visibility is gated on the package *version* carrying a prerelease tag — not
|
|
22
|
-
> on the dist-tag — so both are on for either install: **Staging** and
|
|
13
|
+
> **No stable release exists yet.** `latest` currently points at a prerelease.
|
|
14
|
+
> Staging- and Local-server visibility is gated on the package *version*
|
|
15
|
+
> carrying a prerelease tag — not on the dist-tag — so **Staging** and
|
|
23
16
|
> **Local** appear in `msc login` and in `--region` whichever way you install.
|
|
24
17
|
> A future stable release will hide them from both channels.
|
|
25
18
|
|
|
@@ -53,7 +46,6 @@ msc chat "hi" --session <id> # 切换到指定会话并设为默认
|
|
|
53
46
|
msc sessions # 列出所有会话(标记当前默认 `(current)`)
|
|
54
47
|
msc sessions --json # 机器可读的原始 JSON 数组
|
|
55
48
|
msc sessions use <id> # 纯本地切换默认会话(零请求、不校验)
|
|
56
|
-
msc sessions rm <id> # 删除服务端会话(交互确认,或加 -y/--yes 跳过)
|
|
57
49
|
msc skills # 列出技能库(--plaza 浏览技能广场)
|
|
58
50
|
msc skills add <slug> # 从广场安装技能(如 brainstorming,见 --plaza --json 的 slug)
|
|
59
51
|
msc skills rm <command> # 从技能库移除(-y/--yes 跳过确认)
|
|
@@ -61,6 +53,9 @@ msc skills enable <command> # 启用 / disable 停用
|
|
|
61
53
|
msc --help # all flags
|
|
62
54
|
```
|
|
63
55
|
|
|
56
|
+
Session deletion is not available in the CLI. To delete a session, open the
|
|
57
|
+
corresponding session page in the Media Studio web app and delete it manually.
|
|
58
|
+
|
|
64
59
|
`msc --help` carries an agent-oriented quickstart — the auth prerequisite, the
|
|
65
60
|
three canonical calls, and the `--json` contract. `msc guide` prints the full
|
|
66
61
|
manual: every command, both JSON schemas, the `nextStep` values, exit codes,
|
|
@@ -180,6 +175,29 @@ alone triggers nothing.
|
|
|
180
175
|
flag on your account (a persistent change), is reported as
|
|
181
176
|
`Auto-enabled for this turn: …`, and a failed enable aborts the turn.
|
|
182
177
|
|
|
178
|
+
### Attachments
|
|
179
|
+
|
|
180
|
+
Attach local files to a chat turn using `@path` anywhere in the prompt:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
msc chat "看看 @cover.png 这张图" # bare path (ends at whitespace)
|
|
184
|
+
msc chat "总结 @/abs/path/notes.pdf" # absolute path
|
|
185
|
+
msc chat '对比 @"my docs/report.docx" 内容' # quoted path with spaces
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The CLI mirrors the web composer's upload flow:
|
|
189
|
+
|
|
190
|
+
1. Validates each referenced file locally (exists, readable, supported extension, under 50 MB) before any network call.
|
|
191
|
+
2. Uploads each file via the web app's `POST /api/chat/attachments` endpoint with a terminal spinner (`Uploading attachment 1/2: cover.png`).
|
|
192
|
+
3. Rewrites the prompt to inline the remote URL using the web's `&&<url>&&` marker, which the upstream model and web session history natively understand.
|
|
193
|
+
4. Up to 5 unique attachments per turn (same as the web composer); referencing the same file twice uploads it once.
|
|
194
|
+
5. An unattachable reference (missing file, unsupported format) fails the turn before any session is provisioned.
|
|
195
|
+
6. Prose mentions (`@team 辛苦了`, email addresses) stay literal text — only tokens with a file extension or path slash are treated as attachments.
|
|
196
|
+
7. `@path` is local-filesystem-only: `@https://example.com/a.png` fails with `no such file`. There is no `@url` form — remote URLs are not attachments.
|
|
197
|
+
|
|
198
|
+
In `--json` mode the uploaded attachments are surfaced under the `attachments` array:
|
|
199
|
+
`[{ name, type, size, remoteUrl }]`.
|
|
200
|
+
|
|
183
201
|
### Staging server (expired TLS certificate)
|
|
184
202
|
|
|
185
203
|
The staging server's certificate is expired (ops declines renewal), so the
|
|
@@ -301,7 +319,6 @@ msc chat "刚才的数字是几?" # answers 42
|
|
|
301
319
|
msc sessions # shows session list with (current) marker
|
|
302
320
|
msc chat "hi" --session <id> # switch to specific session
|
|
303
321
|
msc sessions use <id> # switch default pointer locally
|
|
304
|
-
msc sessions rm <id> -y # delete session upstream (-y skips prompt)
|
|
305
322
|
```
|
|
306
323
|
|
|
307
324
|
Without the local server, point at production with
|
package/dist/index.js
CHANGED
|
@@ -4284,7 +4284,7 @@ var consola = createConsola2();
|
|
|
4284
4284
|
// package.json
|
|
4285
4285
|
var package_default = {
|
|
4286
4286
|
name: "@302ai/media-studio-cli",
|
|
4287
|
-
version: "0.1.0
|
|
4287
|
+
version: "0.1.0",
|
|
4288
4288
|
description: "CLI for AI Media Studio — let external agents drive the core media-studio features",
|
|
4289
4289
|
author: "302.AI",
|
|
4290
4290
|
license: "Apache-2.0",
|
|
@@ -9092,6 +9092,9 @@ var ZodURL = /* @__PURE__ */ $constructor("ZodURL", (inst, def) => {
|
|
|
9092
9092
|
$ZodURL.init(inst, def);
|
|
9093
9093
|
ZodStringFormat.init(inst, def);
|
|
9094
9094
|
});
|
|
9095
|
+
function url(params) {
|
|
9096
|
+
return _url(ZodURL, params);
|
|
9097
|
+
}
|
|
9095
9098
|
var ZodEmoji = /* @__PURE__ */ $constructor("ZodEmoji", (inst, def) => {
|
|
9096
9099
|
$ZodEmoji.init(inst, def);
|
|
9097
9100
|
ZodStringFormat.init(inst, def);
|
|
@@ -9643,6 +9646,83 @@ var ArtifactItemSchema = object({
|
|
|
9643
9646
|
productIdx: number2().optional()
|
|
9644
9647
|
});
|
|
9645
9648
|
var ArtifactItemsSchema = array(ArtifactItemSchema);
|
|
9649
|
+
|
|
9650
|
+
// ../media-studio-contract/src/attachments/constants.ts
|
|
9651
|
+
var MAX_ATTACHMENT_COUNT = 5;
|
|
9652
|
+
var MAX_ATTACHMENT_BYTES = 50 * 1024 * 1024;
|
|
9653
|
+
var ATTACHMENT_MIME_BY_EXTENSION = {
|
|
9654
|
+
png: "image/png",
|
|
9655
|
+
jpg: "image/jpeg",
|
|
9656
|
+
jpeg: "image/jpeg",
|
|
9657
|
+
gif: "image/gif",
|
|
9658
|
+
webp: "image/webp",
|
|
9659
|
+
avif: "image/avif",
|
|
9660
|
+
bmp: "image/bmp",
|
|
9661
|
+
svg: "image/svg+xml",
|
|
9662
|
+
ico: "image/x-icon",
|
|
9663
|
+
tif: "image/tiff",
|
|
9664
|
+
tiff: "image/tiff",
|
|
9665
|
+
heic: "image/heic",
|
|
9666
|
+
mp4: "video/mp4",
|
|
9667
|
+
webm: "video/webm",
|
|
9668
|
+
mov: "video/quicktime",
|
|
9669
|
+
m4v: "video/mp4",
|
|
9670
|
+
avi: "video/x-msvideo",
|
|
9671
|
+
mkv: "video/x-matroska",
|
|
9672
|
+
mp3: "audio/mpeg",
|
|
9673
|
+
wav: "audio/wav",
|
|
9674
|
+
m4a: "audio/mp4",
|
|
9675
|
+
aac: "audio/aac",
|
|
9676
|
+
ogg: "audio/ogg",
|
|
9677
|
+
flac: "audio/flac",
|
|
9678
|
+
opus: "audio/opus",
|
|
9679
|
+
pdf: "application/pdf",
|
|
9680
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
9681
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
9682
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
9683
|
+
txt: "text/plain",
|
|
9684
|
+
log: "text/plain",
|
|
9685
|
+
srt: "text/plain",
|
|
9686
|
+
vtt: "text/plain",
|
|
9687
|
+
ini: "text/plain",
|
|
9688
|
+
toml: "text/plain",
|
|
9689
|
+
jsonl: "text/plain",
|
|
9690
|
+
md: "text/markdown",
|
|
9691
|
+
markdown: "text/markdown",
|
|
9692
|
+
html: "text/html",
|
|
9693
|
+
htm: "text/html",
|
|
9694
|
+
csv: "text/csv",
|
|
9695
|
+
tsv: "text/tab-separated-values",
|
|
9696
|
+
json: "application/json",
|
|
9697
|
+
yaml: "text/yaml",
|
|
9698
|
+
yml: "text/yaml",
|
|
9699
|
+
xml: "text/xml"
|
|
9700
|
+
};
|
|
9701
|
+
|
|
9702
|
+
// ../media-studio-contract/src/attachments/markers.ts
|
|
9703
|
+
var ATTACHMENT_MARKER = "&&";
|
|
9704
|
+
function formatAttachmentMarker(remoteUrl) {
|
|
9705
|
+
return `${ATTACHMENT_MARKER}${remoteUrl}${ATTACHMENT_MARKER}`;
|
|
9706
|
+
}
|
|
9707
|
+
|
|
9708
|
+
// ../media-studio-contract/src/attachments/wire.ts
|
|
9709
|
+
var AttachmentKindSchema = _enum([
|
|
9710
|
+
"image",
|
|
9711
|
+
"video",
|
|
9712
|
+
"audio",
|
|
9713
|
+
"text",
|
|
9714
|
+
"file"
|
|
9715
|
+
]);
|
|
9716
|
+
var UploadedAttachmentItemSchema = object({
|
|
9717
|
+
id: string2(),
|
|
9718
|
+
name: string2(),
|
|
9719
|
+
type: AttachmentKindSchema,
|
|
9720
|
+
size: number2(),
|
|
9721
|
+
remoteUrl: url()
|
|
9722
|
+
});
|
|
9723
|
+
var AttachmentUploadResponseSchema = object({
|
|
9724
|
+
files: array(UploadedAttachmentItemSchema)
|
|
9725
|
+
});
|
|
9646
9726
|
// ../../node_modules/es-toolkit/dist/compat/predicate/isObject.mjs
|
|
9647
9727
|
function isObject2(value) {
|
|
9648
9728
|
return value !== null && (typeof value === "object" || typeof value === "function");
|
|
@@ -9835,6 +9915,15 @@ var MEDIA_STUDIO_HOSTS = [
|
|
|
9835
9915
|
new URL(MEDIA_STUDIO_URLS.cn).hostname,
|
|
9836
9916
|
new URL(MEDIA_STUDIO_URLS.staging).hostname
|
|
9837
9917
|
];
|
|
9918
|
+
var APP_ENVS = ["development", "staging", "production"];
|
|
9919
|
+
var APP_ENV_SCHEMA = _enum(APP_ENVS);
|
|
9920
|
+
var APP_ENV_HOSTS = {
|
|
9921
|
+
staging: [new URL(MEDIA_STUDIO_URLS.staging).hostname],
|
|
9922
|
+
production: [
|
|
9923
|
+
new URL(MEDIA_STUDIO_URLS.default).hostname,
|
|
9924
|
+
new URL(MEDIA_STUDIO_URLS.cn).hostname
|
|
9925
|
+
]
|
|
9926
|
+
};
|
|
9838
9927
|
|
|
9839
9928
|
// ../media-studio-contract/src/skills/activation.ts
|
|
9840
9929
|
var SKILL_ACTIVATION_MARKER = "%%";
|
|
@@ -10279,6 +10368,41 @@ function extractUpstreamErrorMessage(body) {
|
|
|
10279
10368
|
}
|
|
10280
10369
|
return M(body).with({ error: { message: z2.select(z2.string) } }, (msg) => msg.trim() || null).with({ message: z2.select(z2.string) }, (msg) => msg !== ERR_SESSION_EXPIRED ? msg.trim() || null : null).with({ error: z2.select(z2.string) }, (msg) => msg !== ERR_SESSION_EXPIRED ? msg.trim() || null : null).with(z2._, () => null).exhaustive();
|
|
10281
10370
|
}
|
|
10371
|
+
|
|
10372
|
+
// src/client/http.ts
|
|
10373
|
+
async function requestJson(fetchFn, url, init, schema, fallbackMessage) {
|
|
10374
|
+
const response = await fetchFn(url, init);
|
|
10375
|
+
if (!response.ok) {
|
|
10376
|
+
const rawBody = await response.text();
|
|
10377
|
+
throw new ChatUpstreamError(extractUpstreamErrorMessage(rawBody) || fallbackMessage(response.status), response.status, rawBody);
|
|
10378
|
+
}
|
|
10379
|
+
return schema.parse(await response.json());
|
|
10380
|
+
}
|
|
10381
|
+
|
|
10382
|
+
// src/client/attachments.ts
|
|
10383
|
+
class ClientAttachments {
|
|
10384
|
+
baseUrl;
|
|
10385
|
+
apiKey;
|
|
10386
|
+
customFetch;
|
|
10387
|
+
constructor(baseUrl, apiKey, customFetch) {
|
|
10388
|
+
this.baseUrl = baseUrl;
|
|
10389
|
+
this.apiKey = apiKey;
|
|
10390
|
+
this.customFetch = customFetch ?? fetch;
|
|
10391
|
+
}
|
|
10392
|
+
async upload(items) {
|
|
10393
|
+
const formData = new FormData;
|
|
10394
|
+
for (const item of items) {
|
|
10395
|
+
formData.append("files", item.file, item.file.name);
|
|
10396
|
+
formData.append("attachmentIds", item.id);
|
|
10397
|
+
}
|
|
10398
|
+
const response = await requestJson(this.customFetch, `${this.baseUrl}/api/chat/attachments`, {
|
|
10399
|
+
method: "POST",
|
|
10400
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
10401
|
+
body: formData
|
|
10402
|
+
}, AttachmentUploadResponseSchema, (status) => `Attachment upload failed with status ${status}`);
|
|
10403
|
+
return response.files;
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10282
10406
|
// ../../node_modules/eventsource-parser/dist/errors.js
|
|
10283
10407
|
class ParseError extends Error {
|
|
10284
10408
|
constructor(message, options) {
|
|
@@ -16670,15 +16794,6 @@ function readUpdateCheck() {
|
|
|
16670
16794
|
function saveUpdateCheck(check) {
|
|
16671
16795
|
writeConfigFile({ updateCheck: check });
|
|
16672
16796
|
}
|
|
16673
|
-
function clearLastSessionId(ifMatchesId) {
|
|
16674
|
-
const current = readConfigFile();
|
|
16675
|
-
if (!current?.lastSessionId)
|
|
16676
|
-
return;
|
|
16677
|
-
if (ifMatchesId && current.lastSessionId !== ifMatchesId)
|
|
16678
|
-
return;
|
|
16679
|
-
const { lastSessionId: _removed, ...rest } = current;
|
|
16680
|
-
saveConfigFile(rest);
|
|
16681
|
-
}
|
|
16682
16797
|
function resolveApiKey() {
|
|
16683
16798
|
const token = readAuthFile()?.token;
|
|
16684
16799
|
if (!token?.trim()) {
|
|
@@ -16698,16 +16813,6 @@ function resolveApiBaseUrl(flag) {
|
|
|
16698
16813
|
return getDefaultApiBaseUrl();
|
|
16699
16814
|
}
|
|
16700
16815
|
|
|
16701
|
-
// src/client/http.ts
|
|
16702
|
-
async function requestJson(fetchFn, url, init, schema, fallbackMessage) {
|
|
16703
|
-
const response = await fetchFn(url, init);
|
|
16704
|
-
if (!response.ok) {
|
|
16705
|
-
const rawBody = await response.text();
|
|
16706
|
-
throw new ChatUpstreamError(extractUpstreamErrorMessage(rawBody) || fallbackMessage(response.status), response.status, rawBody);
|
|
16707
|
-
}
|
|
16708
|
-
return schema.parse(await response.json());
|
|
16709
|
-
}
|
|
16710
|
-
|
|
16711
16816
|
// src/client/session-types.ts
|
|
16712
16817
|
var SessionSourceSchema = _enum2(["web", "cli"]);
|
|
16713
16818
|
var ApiResponseBaseSchema = object2({
|
|
@@ -16796,7 +16901,6 @@ var ListArtifactsQuerySchema = object2({
|
|
|
16796
16901
|
var ListArtifactsResponseSchema = ApiResponseBaseSchema.extend({
|
|
16797
16902
|
data: ArtifactItemsSchema
|
|
16798
16903
|
});
|
|
16799
|
-
var DeleteSessionResponseSchema = ApiResponseBaseSchema;
|
|
16800
16904
|
var PrewarmSessionReadySchema = object2({
|
|
16801
16905
|
session_id: string4(),
|
|
16802
16906
|
note: string4().nullable(),
|
|
@@ -16872,10 +16976,6 @@ class ClientSessions {
|
|
|
16872
16976
|
const url = `${this.baseUrl}/api/sessions/tasks?${params.toString()}`;
|
|
16873
16977
|
return requestJson(this.customFetch, url, { method: "GET", headers: this.authHeaders() }, ListArtifactsResponseSchema, (status) => `Failed to fetch session artifacts with status ${status}`);
|
|
16874
16978
|
}
|
|
16875
|
-
async remove(sessionId) {
|
|
16876
|
-
const url = `${this.baseUrl}/api/sessions?sessionId=${encodeURIComponent(sessionId)}`;
|
|
16877
|
-
await requestJson(this.customFetch, url, { method: "DELETE", headers: this.authHeaders() }, DeleteSessionResponseSchema, (status) => `Failed to delete session with status ${status}`);
|
|
16878
|
-
}
|
|
16879
16979
|
async prewarm(options) {
|
|
16880
16980
|
const { onStageChange, onSessionReady } = options;
|
|
16881
16981
|
const timeoutController = new AbortController;
|
|
@@ -17010,8 +17110,6 @@ var SkillListItemSchema = object2({
|
|
|
17010
17110
|
link: string4(),
|
|
17011
17111
|
descriptionEn: string4(),
|
|
17012
17112
|
descriptionZh: string4(),
|
|
17013
|
-
tagsEn: array2(string4()),
|
|
17014
|
-
tagsZh: array2(string4()),
|
|
17015
17113
|
enabled: boolean4(),
|
|
17016
17114
|
added: boolean4(),
|
|
17017
17115
|
builtIn: boolean4(),
|
|
@@ -17041,8 +17139,6 @@ var PlazaSkillItemSchema = object2({
|
|
|
17041
17139
|
command: string4(),
|
|
17042
17140
|
descriptionEn: string4(),
|
|
17043
17141
|
descriptionZh: string4(),
|
|
17044
|
-
tagsEn: array2(string4()),
|
|
17045
|
-
tagsZh: array2(string4()),
|
|
17046
17142
|
link: string4(),
|
|
17047
17143
|
cover: string4().nullable(),
|
|
17048
17144
|
version: string4(),
|
|
@@ -17144,6 +17240,7 @@ class MediaStudioClient {
|
|
|
17144
17240
|
chat;
|
|
17145
17241
|
sessions;
|
|
17146
17242
|
skills;
|
|
17243
|
+
attachments;
|
|
17147
17244
|
customFetch;
|
|
17148
17245
|
get apiKey() {
|
|
17149
17246
|
return this._apiKey;
|
|
@@ -17182,6 +17279,7 @@ class MediaStudioClient {
|
|
|
17182
17279
|
this.chat = new ClientChat(this.baseUrl, this._apiKey, interceptingFetch);
|
|
17183
17280
|
this.sessions = new ClientSessions(this.baseUrl, this._apiKey, interceptingFetch);
|
|
17184
17281
|
this.skills = new ClientSkills(this.baseUrl, this._apiKey, interceptingFetch);
|
|
17282
|
+
this.attachments = new ClientAttachments(this.baseUrl, this._apiKey, interceptingFetch);
|
|
17185
17283
|
}
|
|
17186
17284
|
async getMe() {
|
|
17187
17285
|
return requestJson(this.customFetch, `${this.baseUrl}/api/auth/me`, {
|
|
@@ -17575,6 +17673,144 @@ class AgentSpinner {
|
|
|
17575
17673
|
}
|
|
17576
17674
|
}
|
|
17577
17675
|
|
|
17676
|
+
// src/services/attachments.ts
|
|
17677
|
+
import { readFile, stat } from "node:fs/promises";
|
|
17678
|
+
import { basename, extname, resolve } from "node:path";
|
|
17679
|
+
|
|
17680
|
+
// src/lib/attachment-refs.ts
|
|
17681
|
+
var PATH_LIKE_RE = /[/\\]|\.[A-Za-z0-9]{1,10}$/;
|
|
17682
|
+
var REF_RE = /(^|(?<=\s))@(?:"([^"]+)"|([^\s@]+))/g;
|
|
17683
|
+
function findAttachmentRefs(prompt) {
|
|
17684
|
+
const refs = [];
|
|
17685
|
+
for (const match of prompt.matchAll(REF_RE)) {
|
|
17686
|
+
const start = match.index;
|
|
17687
|
+
const quoted = match[2];
|
|
17688
|
+
const bare = match[3];
|
|
17689
|
+
const path = quoted ?? bare;
|
|
17690
|
+
if (path === undefined || path.length === 0)
|
|
17691
|
+
continue;
|
|
17692
|
+
if (!PATH_LIKE_RE.test(path))
|
|
17693
|
+
continue;
|
|
17694
|
+
refs.push({ start, end: start + match[0].length, path });
|
|
17695
|
+
}
|
|
17696
|
+
return refs;
|
|
17697
|
+
}
|
|
17698
|
+
function composeAttachmentPrompt(prompt, urlByPath) {
|
|
17699
|
+
const refs = findAttachmentRefs(prompt);
|
|
17700
|
+
if (refs.length === 0)
|
|
17701
|
+
return prompt;
|
|
17702
|
+
const segments = [];
|
|
17703
|
+
let cursor = 0;
|
|
17704
|
+
for (const ref of refs) {
|
|
17705
|
+
segments.push(prompt.slice(cursor, ref.start));
|
|
17706
|
+
const url = urlByPath[ref.path];
|
|
17707
|
+
segments.push(url ? formatAttachmentMarker(url) : prompt.slice(ref.start, ref.end));
|
|
17708
|
+
cursor = ref.end;
|
|
17709
|
+
}
|
|
17710
|
+
segments.push(prompt.slice(cursor));
|
|
17711
|
+
return segments.join("");
|
|
17712
|
+
}
|
|
17713
|
+
|
|
17714
|
+
// src/services/attachments.ts
|
|
17715
|
+
class AttachmentResolutionError extends Error {
|
|
17716
|
+
}
|
|
17717
|
+
function formatBytes(size) {
|
|
17718
|
+
if (size < 1024)
|
|
17719
|
+
return `${size} B`;
|
|
17720
|
+
if (size < 1024 * 1024)
|
|
17721
|
+
return `${(size / 1024).toFixed(1)} KB`;
|
|
17722
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
17723
|
+
}
|
|
17724
|
+
async function readAttachment(typedPath, absPath) {
|
|
17725
|
+
let stats;
|
|
17726
|
+
try {
|
|
17727
|
+
stats = await stat(absPath);
|
|
17728
|
+
} catch {
|
|
17729
|
+
throw new AttachmentResolutionError(`@${typedPath}: no such file (${absPath})`);
|
|
17730
|
+
}
|
|
17731
|
+
if (!stats.isFile()) {
|
|
17732
|
+
throw new AttachmentResolutionError(`@${typedPath}: not a regular file`);
|
|
17733
|
+
}
|
|
17734
|
+
const ext = extname(absPath).slice(1).toLowerCase();
|
|
17735
|
+
const mime = ext ? ATTACHMENT_MIME_BY_EXTENSION[ext] : undefined;
|
|
17736
|
+
if (!mime) {
|
|
17737
|
+
throw new AttachmentResolutionError(`@${typedPath}: unsupported attachment type${ext ? ` .${ext}` : ""}`);
|
|
17738
|
+
}
|
|
17739
|
+
if (stats.size === 0) {
|
|
17740
|
+
throw new AttachmentResolutionError(`@${typedPath}: file is empty`);
|
|
17741
|
+
}
|
|
17742
|
+
if (stats.size > MAX_ATTACHMENT_BYTES) {
|
|
17743
|
+
throw new AttachmentResolutionError(`@${typedPath}: ${formatBytes(stats.size)} exceeds the ` + `${formatBytes(MAX_ATTACHMENT_BYTES)} attachment limit`);
|
|
17744
|
+
}
|
|
17745
|
+
let file;
|
|
17746
|
+
try {
|
|
17747
|
+
const buffer = await readFile(absPath);
|
|
17748
|
+
const data = new Uint8Array(buffer.byteLength);
|
|
17749
|
+
data.set(buffer);
|
|
17750
|
+
file = new File([data], basename(absPath), { type: mime });
|
|
17751
|
+
} catch {
|
|
17752
|
+
throw new AttachmentResolutionError(`@${typedPath}: file is not readable (${absPath})`);
|
|
17753
|
+
}
|
|
17754
|
+
return file;
|
|
17755
|
+
}
|
|
17756
|
+
async function resolvePromptAttachments(options) {
|
|
17757
|
+
const refs = findAttachmentRefs(options.prompt);
|
|
17758
|
+
if (refs.length === 0) {
|
|
17759
|
+
return { text: options.prompt, attachments: [] };
|
|
17760
|
+
}
|
|
17761
|
+
const typedByResolved = {};
|
|
17762
|
+
for (const ref of refs) {
|
|
17763
|
+
const abs = resolve(ref.path);
|
|
17764
|
+
if (!(abs in typedByResolved))
|
|
17765
|
+
typedByResolved[abs] = ref.path;
|
|
17766
|
+
}
|
|
17767
|
+
const resolvedPaths = Object.keys(typedByResolved);
|
|
17768
|
+
if (resolvedPaths.length > MAX_ATTACHMENT_COUNT) {
|
|
17769
|
+
throw new AttachmentResolutionError(`Too many attachments: ${resolvedPaths.length} unique files referenced, ` + `the limit is ${MAX_ATTACHMENT_COUNT} (same as the web composer)`);
|
|
17770
|
+
}
|
|
17771
|
+
const prepared = await Promise.all(resolvedPaths.map(async (abs) => ({
|
|
17772
|
+
abs,
|
|
17773
|
+
typed: typedByResolved[abs] ?? abs,
|
|
17774
|
+
file: await readAttachment(typedByResolved[abs] ?? abs, abs)
|
|
17775
|
+
})));
|
|
17776
|
+
const client = new MediaStudioClient({
|
|
17777
|
+
baseUrl: options.apiBaseUrl,
|
|
17778
|
+
apiKey: options.apiKey
|
|
17779
|
+
});
|
|
17780
|
+
const urlByResolved = {};
|
|
17781
|
+
const attachments = [];
|
|
17782
|
+
const total = prepared.length;
|
|
17783
|
+
for (let i = 0;i < prepared.length; i++) {
|
|
17784
|
+
const item = prepared[i];
|
|
17785
|
+
if (!item)
|
|
17786
|
+
continue;
|
|
17787
|
+
options.onProgress?.(i + 1, total, item.file.name);
|
|
17788
|
+
const [uploaded] = await client.attachments.upload([
|
|
17789
|
+
{ id: nanoid3(), file: item.file }
|
|
17790
|
+
]);
|
|
17791
|
+
if (!uploaded) {
|
|
17792
|
+
throw new AttachmentResolutionError(`@${item.typed}: upload returned no file`);
|
|
17793
|
+
}
|
|
17794
|
+
urlByResolved[item.abs] = uploaded.remoteUrl;
|
|
17795
|
+
attachments.push({
|
|
17796
|
+
name: uploaded.name,
|
|
17797
|
+
type: uploaded.type,
|
|
17798
|
+
size: uploaded.size,
|
|
17799
|
+
remoteUrl: uploaded.remoteUrl
|
|
17800
|
+
});
|
|
17801
|
+
}
|
|
17802
|
+
const urlByTyped = {};
|
|
17803
|
+
for (const ref of refs) {
|
|
17804
|
+
const url = urlByResolved[resolve(ref.path)];
|
|
17805
|
+
if (url !== undefined)
|
|
17806
|
+
urlByTyped[ref.path] = url;
|
|
17807
|
+
}
|
|
17808
|
+
return {
|
|
17809
|
+
text: composeAttachmentPrompt(options.prompt, urlByTyped),
|
|
17810
|
+
attachments
|
|
17811
|
+
};
|
|
17812
|
+
}
|
|
17813
|
+
|
|
17578
17814
|
// src/services/session.ts
|
|
17579
17815
|
function isSessionGoneError(err) {
|
|
17580
17816
|
if (!(err instanceof ChatUpstreamError))
|
|
@@ -17757,7 +17993,7 @@ async function resolveChatSession(options) {
|
|
|
17757
17993
|
return { sessionId, recreateOnGone: false };
|
|
17758
17994
|
}
|
|
17759
17995
|
async function streamTurn(options) {
|
|
17760
|
-
const { args, sessionId, spinner, autoEnabled } = options;
|
|
17996
|
+
const { args, sessionId, spinner, autoEnabled, attachments } = options;
|
|
17761
17997
|
if (!args.json) {
|
|
17762
17998
|
spinner.start("Connecting to Media Studio...");
|
|
17763
17999
|
}
|
|
@@ -17838,6 +18074,7 @@ async function streamTurn(options) {
|
|
|
17838
18074
|
text: fullText,
|
|
17839
18075
|
steps,
|
|
17840
18076
|
media: mediaAssets,
|
|
18077
|
+
...attachments.length > 0 ? { attachments } : {},
|
|
17841
18078
|
...autoEnabled.length > 0 ? { autoEnabled } : {},
|
|
17842
18079
|
...args["show-reasoning"] && reasoning ? { reasoning } : {}
|
|
17843
18080
|
};
|
|
@@ -17859,6 +18096,7 @@ async function streamTurn(options) {
|
|
|
17859
18096
|
text: fullText,
|
|
17860
18097
|
steps,
|
|
17861
18098
|
media: mediaAssets,
|
|
18099
|
+
...attachments.length > 0 ? { attachments } : {},
|
|
17862
18100
|
...autoEnabled.length > 0 ? { autoEnabled } : {},
|
|
17863
18101
|
...args["show-reasoning"] && reasoning ? { reasoning } : {}
|
|
17864
18102
|
};
|
|
@@ -17900,7 +18138,7 @@ var chatCommand = defineCommand({
|
|
|
17900
18138
|
prompt: {
|
|
17901
18139
|
type: "positional",
|
|
17902
18140
|
required: true,
|
|
17903
|
-
description:
|
|
18141
|
+
description: 'The message to send. Reference a skill by leading the prompt with /name or %%/name%% (see `msc skills`); attach a local file with @path or @"path with spaces"'
|
|
17904
18142
|
},
|
|
17905
18143
|
json: {
|
|
17906
18144
|
type: "boolean",
|
|
@@ -17929,6 +18167,14 @@ var chatCommand = defineCommand({
|
|
|
17929
18167
|
if (autoEnabled.length > 0 && args.json !== true) {
|
|
17930
18168
|
consola.info(`Auto-enabled for this turn: ${autoEnabled.join(", ")}`);
|
|
17931
18169
|
}
|
|
18170
|
+
const { text: promptWithAttachments, attachments } = await resolvePromptAttachments({
|
|
18171
|
+
prompt: text,
|
|
18172
|
+
apiBaseUrl,
|
|
18173
|
+
apiKey: resolveApiKey(),
|
|
18174
|
+
...args.json ? {} : {
|
|
18175
|
+
onProgress: (index, total, name) => spinner.start(`Uploading attachment ${index}/${total}: ${name}`)
|
|
18176
|
+
}
|
|
18177
|
+
});
|
|
17932
18178
|
const resolved = await resolveChatSession({
|
|
17933
18179
|
sessionFlag: args.session,
|
|
17934
18180
|
apiBaseUrl,
|
|
@@ -17938,13 +18184,14 @@ var chatCommand = defineCommand({
|
|
|
17938
18184
|
});
|
|
17939
18185
|
await runTurn({
|
|
17940
18186
|
args,
|
|
17941
|
-
prompt:
|
|
18187
|
+
prompt: promptWithAttachments,
|
|
17942
18188
|
sessionId: resolved.sessionId,
|
|
17943
18189
|
recreateOnGone: resolved.recreateOnGone,
|
|
17944
18190
|
apiBaseUrl,
|
|
17945
18191
|
apiKey: resolveApiKey(),
|
|
17946
18192
|
spinner,
|
|
17947
18193
|
skillUrls,
|
|
18194
|
+
attachments,
|
|
17948
18195
|
autoEnabled
|
|
17949
18196
|
});
|
|
17950
18197
|
} catch (err) {
|
|
@@ -17974,6 +18221,12 @@ images, videos, and multimedia content through AI Media Studio.
|
|
|
17974
18221
|
3. Refine in the next turn (multi-turn is automatic):
|
|
17975
18222
|
msc chat "change the car color to emerald green and add rain"
|
|
17976
18223
|
|
|
18224
|
+
Agent Skill (recommended for AI agents):
|
|
18225
|
+
If the agent does not already know how to use msc, install the public
|
|
18226
|
+
Agent Skill before starting a new session:
|
|
18227
|
+
npx -y skills add 302ai/msc-skill --skill msc-skill -g -y
|
|
18228
|
+
This is optional for direct terminal use.
|
|
18229
|
+
|
|
17977
18230
|
|
|
17978
18231
|
2. AUTHENTICATION & SERVERS
|
|
17979
18232
|
|
|
@@ -18016,11 +18269,25 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
|
|
|
18016
18269
|
Pass \`-r\` or \`--show-reasoning\` to stream the model's planning and
|
|
18017
18270
|
thinking process before the final generation steps.
|
|
18018
18271
|
|
|
18272
|
+
Attaching Files (@path):
|
|
18273
|
+
Attach local images, videos, audio, or documents by typing \`@path\` or
|
|
18274
|
+
\`@"path with spaces"\` anywhere in the prompt:
|
|
18275
|
+
msc chat "analyze @cover.png"
|
|
18276
|
+
msc chat "summarize @/abs/path/notes.pdf"
|
|
18277
|
+
msc chat 'compare @"my docs/report.docx" with @"my docs/summary.docx"'
|
|
18278
|
+
|
|
18279
|
+
The CLI validates files locally (existence, supported format, under 50 MB),
|
|
18280
|
+
uploads them with progress indication, and inlines remote URLs into the prompt.
|
|
18281
|
+
Up to 5 attachments per turn. Referencing the same file multiple times
|
|
18282
|
+
uploads it only once. Plain prose mentions (@team) and emails are ignored.
|
|
18283
|
+
|
|
18284
|
+
@path is local-filesystem-only: @https://example.com/a.png fails with
|
|
18285
|
+
"no such file". Remote URLs are not attachments and there is no @url form.
|
|
18286
|
+
|
|
18019
18287
|
Session Switching:
|
|
18020
18288
|
Pass \`--session <id>\` to direct a prompt to a specific existing session
|
|
18021
18289
|
and make it your new active session.
|
|
18022
18290
|
|
|
18023
|
-
|
|
18024
18291
|
4. USING SKILLS
|
|
18025
18292
|
|
|
18026
18293
|
Skills allow prompts to trigger specialized models, pipelines, and workflows
|
|
@@ -18062,9 +18329,12 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
|
|
|
18062
18329
|
msc sessions use <session-id>
|
|
18063
18330
|
Switch your active session pointer without sending a prompt.
|
|
18064
18331
|
|
|
18065
|
-
|
|
18066
|
-
|
|
18332
|
+
Session deletion is not available in the CLI. Open the corresponding session
|
|
18333
|
+
in the Media Studio web app and delete it there manually.
|
|
18067
18334
|
|
|
18335
|
+
Web app (open the region matching your CLI login):
|
|
18336
|
+
${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url}`).join(`
|
|
18337
|
+
`)}
|
|
18068
18338
|
|
|
18069
18339
|
6. AUTOMATION & SCRIPTING (--json)
|
|
18070
18340
|
|
|
@@ -18087,6 +18357,14 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
|
|
|
18087
18357
|
"taskId": "string"
|
|
18088
18358
|
}
|
|
18089
18359
|
],
|
|
18360
|
+
"attachments": [ // present if local files were attached
|
|
18361
|
+
{
|
|
18362
|
+
"name": "string",
|
|
18363
|
+
"type": "string", // "image" | "video" | "audio" | "file"
|
|
18364
|
+
"size": 1024,
|
|
18365
|
+
"remoteUrl": "https://..."
|
|
18366
|
+
}
|
|
18367
|
+
],
|
|
18090
18368
|
"reasoning": "string?" // present if --show-reasoning was passed
|
|
18091
18369
|
}
|
|
18092
18370
|
|
|
@@ -19967,11 +20245,6 @@ var logoutCommand = defineCommand({
|
|
|
19967
20245
|
}
|
|
19968
20246
|
});
|
|
19969
20247
|
|
|
19970
|
-
// src/lib/confirm.ts
|
|
19971
|
-
function needsConfirmationRefusal(yes) {
|
|
19972
|
-
return !yes && !process.stdin.isTTY && true;
|
|
19973
|
-
}
|
|
19974
|
-
|
|
19975
20248
|
// src/commands/sessions.ts
|
|
19976
20249
|
function formatLastActive(iso) {
|
|
19977
20250
|
return iso.replace("T", " ").slice(0, 19);
|
|
@@ -20113,65 +20386,28 @@ var useSessionCommand = defineCommand({
|
|
|
20113
20386
|
consola.success(`Switched default session to ${args.sessionId}`);
|
|
20114
20387
|
}
|
|
20115
20388
|
});
|
|
20116
|
-
var rmSessionCommand = defineCommand({
|
|
20117
|
-
meta: {
|
|
20118
|
-
name: "rm",
|
|
20119
|
-
description: "Delete a session server-side"
|
|
20120
|
-
},
|
|
20121
|
-
args: {
|
|
20122
|
-
sessionId: {
|
|
20123
|
-
type: "positional",
|
|
20124
|
-
required: true,
|
|
20125
|
-
description: "Session id to delete"
|
|
20126
|
-
},
|
|
20127
|
-
yes: {
|
|
20128
|
-
type: "boolean",
|
|
20129
|
-
alias: "y",
|
|
20130
|
-
description: "Skip confirmation prompt"
|
|
20131
|
-
}
|
|
20132
|
-
},
|
|
20133
|
-
run: async ({ args }) => {
|
|
20134
|
-
try {
|
|
20135
|
-
if (needsConfirmationRefusal(args.yes === true)) {
|
|
20136
|
-
consola.error("Confirmation required. Pass --yes to confirm deletion non-interactively.");
|
|
20137
|
-
process.exit(1);
|
|
20138
|
-
return;
|
|
20139
|
-
}
|
|
20140
|
-
if (!args.yes) {
|
|
20141
|
-
const confirmed = await confirm({
|
|
20142
|
-
message: `Delete session "${args.sessionId}"? Upstream conversation memory will be destroyed.`,
|
|
20143
|
-
initialValue: false
|
|
20144
|
-
});
|
|
20145
|
-
if (isCancel(confirmed) || !confirmed) {
|
|
20146
|
-
consola.info("Session deletion cancelled.");
|
|
20147
|
-
return;
|
|
20148
|
-
}
|
|
20149
|
-
}
|
|
20150
|
-
const client = resolveClient();
|
|
20151
|
-
await client.sessions.remove(args.sessionId);
|
|
20152
|
-
clearLastSessionId(args.sessionId);
|
|
20153
|
-
consola.success(`Session "${args.sessionId}" deleted.`);
|
|
20154
|
-
} catch (err) {
|
|
20155
|
-
reportCommandError(err);
|
|
20156
|
-
}
|
|
20157
|
-
}
|
|
20158
|
-
});
|
|
20159
20389
|
var sessionsCommand = defineCommand({
|
|
20160
20390
|
meta: {
|
|
20161
20391
|
name: "sessions",
|
|
20162
|
-
description: "Manage Media Studio chat sessions"
|
|
20392
|
+
description: "Manage Media Studio chat sessions; deletion is available in the web app"
|
|
20163
20393
|
},
|
|
20164
20394
|
default: "list",
|
|
20165
20395
|
subCommands: {
|
|
20166
20396
|
list: listSessionsCommand,
|
|
20167
20397
|
artifacts: artifactsCommand,
|
|
20168
|
-
use: useSessionCommand
|
|
20169
|
-
rm: rmSessionCommand
|
|
20398
|
+
use: useSessionCommand
|
|
20170
20399
|
}
|
|
20171
20400
|
});
|
|
20172
20401
|
|
|
20173
20402
|
// src/commands/skills.ts
|
|
20174
20403
|
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
20404
|
+
|
|
20405
|
+
// src/lib/confirm.ts
|
|
20406
|
+
function needsConfirmationRefusal(yes) {
|
|
20407
|
+
return !yes && !process.stdin.isTTY && true;
|
|
20408
|
+
}
|
|
20409
|
+
|
|
20410
|
+
// src/commands/skills.ts
|
|
20175
20411
|
function resolveClient2() {
|
|
20176
20412
|
return new MediaStudioClient({
|
|
20177
20413
|
baseUrl: resolveApiBaseUrl(),
|
package/package.json
CHANGED