@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.
Files changed (3) hide show
  1. package/README.md +29 -12
  2. package/dist/index.js +322 -86
  3. 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
- Newer, less tested prerelease builds:
14
-
15
- ```bash
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-beta.6",
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: "The message to send. Reference a skill by leading the prompt with /name or %%/name%% - see `msc skills`"
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: text,
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
- msc sessions rm <session-id> [-y]
18066
- Delete a session and its server-side history.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@302ai/media-studio-cli",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0",
4
4
  "description": "CLI for AI Media Studio — let external agents drive the core media-studio features",
5
5
  "author": "302.AI",
6
6
  "license": "Apache-2.0",