@302ai/media-studio-cli 0.1.0 → 0.1.1-beta.1

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 +36 -9
  2. package/dist/index.js +757 -211
  3. package/package.json +1 -1
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.1-beta.1",
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",
@@ -9701,6 +9701,7 @@ var ATTACHMENT_MIME_BY_EXTENSION = {
9701
9701
 
9702
9702
  // ../media-studio-contract/src/attachments/markers.ts
9703
9703
  var ATTACHMENT_MARKER = "&&";
9704
+ var ATTACHMENT_MARKER_REGEX = /&&([^&\s]+)&&/g;
9704
9705
  function formatAttachmentMarker(remoteUrl) {
9705
9706
  return `${ATTACHMENT_MARKER}${remoteUrl}${ATTACHMENT_MARKER}`;
9706
9707
  }
@@ -9749,6 +9750,151 @@ function isString(value) {
9749
9750
  return typeof value === "string";
9750
9751
  }
9751
9752
  // ../media-studio-contract/src/err-codes.ts
9753
+ var ErrCode = {
9754
+ MISSING_REQUIRED_PARAM: 10001,
9755
+ INVALID_PARAM_FORMAT: 10002,
9756
+ RESOURCE_NOT_FOUND: 10003,
9757
+ METHOD_NOT_ALLOWED: 10004,
9758
+ RATE_LIMITED: 10005,
9759
+ PAYLOAD_TOO_LARGE: 10006,
9760
+ UNSUPPORTED_MEDIA_TYPE: 10007,
9761
+ INTERNAL_ERROR: 10500,
9762
+ DEPENDENCY_UNAVAILABLE: 10501,
9763
+ DATABASE_ERROR: 10502,
9764
+ SERVICE_UNAVAILABLE: 10503,
9765
+ CACHE_UNAVAILABLE: 10504,
9766
+ UNAUTHORIZED: 20001,
9767
+ TOKEN_MISSING: 20002,
9768
+ TOKEN_INVALID: 20003,
9769
+ TOKEN_EXPIRED: 20004,
9770
+ TOKEN_REVOKED: 20005,
9771
+ REFRESH_TOKEN_INVALID: 20006,
9772
+ REFRESH_TOKEN_ROTATED: 20007,
9773
+ VERIFICATION_CODE_INVALID: 20101,
9774
+ VERIFICATION_CODE_EXPIRED: 20102,
9775
+ CODE_SEND_RATE_LIMITED: 20103,
9776
+ CODE_SEND_DAILY_LIMIT: 20104,
9777
+ EMAIL_SEND_FAILED: 20105,
9778
+ EMAIL_ALREADY_REGISTERED: 20106,
9779
+ EMAIL_NOT_REGISTERED: 20107,
9780
+ OAUTH_STATE_INVALID: 20201,
9781
+ OAUTH_CALLBACK_FAILED: 20202,
9782
+ OAUTH_ACCOUNT_ALREADY_BOUND: 20203,
9783
+ SSO_INFO_CONFLICT: 20204,
9784
+ FORBIDDEN: 20301,
9785
+ ADMIN_REQUIRED: 20302,
9786
+ ACCOUNT_DEACTIVATED: 20303,
9787
+ INSUFFICIENT_BALANCE: 30001,
9788
+ CREDIT_FREEZE_FAILED: 30002,
9789
+ FREEZE_RECORD_NOT_FOUND: 30003,
9790
+ FREEZE_ALREADY_SETTLED: 30004,
9791
+ CREDIT_SETTLE_FAILED: 30005,
9792
+ SETTLE_EXCEEDS_FREEZE: 30006,
9793
+ UNFREEZE_FAILED: 30007,
9794
+ CREDIT_BATCH_EXPIRED: 30008,
9795
+ CREDIT_BATCH_NOT_FOUND: 30009,
9796
+ CONCURRENT_MODIFICATION: 30010,
9797
+ PRICING_RULE_NOT_FOUND: 30101,
9798
+ MODEL_PRICE_MISSING: 30102,
9799
+ USAGE_RECORD_DUPLICATED: 30103,
9800
+ BALANCE_CACHE_REBUILD_FAILED: 30104,
9801
+ LEDGER_WRITE_FAILED: 30105,
9802
+ UPSTREAM_ERROR: 40001,
9803
+ UPSTREAM_TIMEOUT: 40002,
9804
+ UPSTREAM_RATE_LIMITED: 40003,
9805
+ UPSTREAM_UNAVAILABLE: 40004,
9806
+ UPSTREAM_AUTH_FAILED: 40005,
9807
+ UPSTREAM_DATA_INVALID: 40006,
9808
+ MODEL_UNSUPPORTED: 40101,
9809
+ MODEL_LABEL_INVALID: 40102,
9810
+ MODEL_PRICE_NOT_CONFIGURED: 40103,
9811
+ MODEL_LABEL_DEPRECATED: 40104,
9812
+ SANDBOX_TOKEN_INVALID: 40201,
9813
+ SANDBOX_TOKEN_EXPIRED: 40202,
9814
+ OSS_TRANSFER_FAILED: 40301,
9815
+ OSS_URL_PRESIGN_FAILED: 40302,
9816
+ PLAN_NOT_FOUND: 50001,
9817
+ PLAN_UNAVAILABLE: 50002,
9818
+ PLAN_PRICE_MISSING: 50003,
9819
+ ORDER_NOT_FOUND: 50101,
9820
+ ORDER_ALREADY_PAID: 50102,
9821
+ ORDER_EXPIRED: 50103,
9822
+ ORDER_AMOUNT_MISMATCH: 50104,
9823
+ ORDER_STATUS_NOT_ALLOWED: 50105,
9824
+ ORDER_CANCEL_FAILED: 50106,
9825
+ ORDER_PENDING_EXISTS: 50107,
9826
+ PAYMENT_FAILED: 50201,
9827
+ PAYMENT_REJECTED: 50202,
9828
+ PAYMENT_METHOD_NOT_FOUND: 50203,
9829
+ WEBHOOK_SIGNATURE_INVALID: 50204,
9830
+ WEBHOOK_EVENT_UNKNOWN: 50205,
9831
+ STRIPE_API_ERROR: 50206,
9832
+ SUBSCRIPTION_NOT_FOUND: 50301,
9833
+ SUBSCRIPTION_STATUS_NOT_ALLOWED: 50302,
9834
+ SUBSCRIPTION_CANCELED: 50303,
9835
+ UPGRADE_PRORATION_FAILED: 50304,
9836
+ DOWNGRADE_NOT_ALLOWED: 50305,
9837
+ AUTO_RENEW_FAILED: 50306,
9838
+ SAVED_CARD_INVALID: 50307,
9839
+ REFUND_NOT_ELIGIBLE: 50401,
9840
+ REFUND_WINDOW_EXPIRED: 50402,
9841
+ REFUND_THRESHOLD_EXCEEDED: 50403,
9842
+ REFUND_FAILED: 50404,
9843
+ TOPUP_SUBSCRIPTION_ONLY: 50501,
9844
+ TOPUP_FROZEN: 50502,
9845
+ TOPUP_NOT_FOUND: 50503,
9846
+ SESSION_NOT_FOUND: 60001,
9847
+ SESSION_ALREADY_EXISTS: 60002,
9848
+ SESSION_STATUS_NOT_ALLOWED: 60003,
9849
+ CHAT_SESSION_EXPIRED: 60004,
9850
+ CHAT_CONTEXT_MISSING: 60005,
9851
+ PROMPT_SUGGESTION_FAILED: 60006,
9852
+ TITLE_GENERATION_FAILED: 60007,
9853
+ CHAT_TASK_NOT_FOUND: 60101,
9854
+ CHAT_TASK_IN_PROGRESS: 60102,
9855
+ CHAT_TASK_CANCELED: 60103,
9856
+ CHAT_TASK_STATUS_NOT_ALLOWED: 60104,
9857
+ SANDBOX_CREATE_FAILED: 60201,
9858
+ SANDBOX_WARMUP_FAILED: 60202,
9859
+ SANDBOX_TIMEOUT: 60203,
9860
+ SANDBOX_UNAVAILABLE: 60204,
9861
+ SANDBOX_COMMAND_FAILED: 60205,
9862
+ SKILL_NOT_FOUND: 60301,
9863
+ SKILL_FILE_INVALID: 60302,
9864
+ SKILL_BUILTIN_IMMUTABLE: 60303,
9865
+ SKILL_UPLOAD_INVALID: 60304,
9866
+ ASSET_NOT_FOUND: 70001,
9867
+ ASSET_GROUP_NOT_FOUND: 70002,
9868
+ ASSET_GROUP_NAME_EXISTS: 70003,
9869
+ ASSET_UPLOAD_FAILED: 70004,
9870
+ ASSET_TOO_LARGE: 70005,
9871
+ ASSET_TYPE_NOT_ALLOWED: 70006,
9872
+ ASSET_GROUP_INVALID: 70007,
9873
+ OSS_UPLOAD_FAILED: 70101,
9874
+ OSS_DOWNLOAD_FAILED: 70102,
9875
+ COVER_UPLOAD_FAILED: 70103,
9876
+ OSS_DELETE_FAILED: 70104,
9877
+ FILE_NOT_FOUND: 70201,
9878
+ FILE_READ_FAILED: 70202,
9879
+ FILE_WRITE_FAILED: 70203,
9880
+ FILE_FORMAT_INVALID: 70204,
9881
+ FILE_FORMAT_TEXT_FILE_ONLY: 70205,
9882
+ FILE_OPERATION_INVALID: 70206,
9883
+ FILE_TARGET_PATH_REQUIRED: 70207,
9884
+ FILE_PATH_COUNT_MISMATCH: 70208,
9885
+ FILE_RENAME_BATCH_UNSUPPORTED: 70209,
9886
+ ADMIN_NOT_LOGGED_IN: 90001,
9887
+ ADMIN_2FA_REQUIRED: 90002,
9888
+ ADMIN_2FA_CODE_INVALID: 90003,
9889
+ ADMIN_2FA_RATE_LIMITED: 90004,
9890
+ ADMIN_FORBIDDEN: 90005,
9891
+ ADMIN_NOT_FOUND: 90006,
9892
+ RISK_CONTROL_TRIGGERED: 90101,
9893
+ RISK_CONTROL_RATE_LIMITED: 90102,
9894
+ RECONCILIATION_MISMATCH: 90103,
9895
+ RECONCILIATION_TASK_FAILED: 90104,
9896
+ AUDIT_LOG_WRITE_FAILED: 90105
9897
+ };
9752
9898
  var ERR_NEXT_STEP = {
9753
9899
  10001: "fix_input",
9754
9900
  10002: "fix_input",
@@ -9928,11 +10074,14 @@ var APP_ENV_HOSTS = {
9928
10074
  // ../media-studio-contract/src/skills/activation.ts
9929
10075
  var SKILL_ACTIVATION_MARKER = "%%";
9930
10076
  var SKILL_ACTIVATION_RE = /%%(\/[^%\s]+)%%/g;
10077
+ function createSkillActivationRegex() {
10078
+ return new RegExp(SKILL_ACTIVATION_RE.source, SKILL_ACTIVATION_RE.flags);
10079
+ }
9931
10080
  function formatSkillActivation(command) {
9932
10081
  return `${SKILL_ACTIVATION_MARKER}${command}${SKILL_ACTIVATION_MARKER}`;
9933
10082
  }
9934
10083
  function findSkillActivationMatches(text) {
9935
- return Array.from(text.matchAll(SKILL_ACTIVATION_RE)).flatMap((match, index) => {
10084
+ return Array.from(text.matchAll(createSkillActivationRegex())).flatMap((match, index) => {
9936
10085
  const start = match.index;
9937
10086
  const command = match[1];
9938
10087
  if (start === undefined || !command)
@@ -9949,6 +10098,25 @@ function findSkillActivationMatches(text) {
9949
10098
  });
9950
10099
  }
9951
10100
 
10101
+ // ../media-studio-contract/src/sessions/derive-first-turn-note.ts
10102
+ var DEFAULT_FALLBACK_TITLE = "Agent Task";
10103
+ var MAX_NOTE_LENGTH = 64;
10104
+ function deriveFirstTurnNote(rawText, options) {
10105
+ const fallback = options?.fallbackTitle ?? DEFAULT_FALLBACK_TITLE;
10106
+ if (!rawText) {
10107
+ return fallback;
10108
+ }
10109
+ const unwrapped = rawText.replace(/\$--model\s+"([^"$]+)"[^$]*\$/gi, "$1 ").replace(/\$--model\s+([^$\s]+)[^$]*\$/gi, "$1 ").replace(SKILL_ACTIVATION_RE, "$1").replace(ATTACHMENT_MARKER_REGEX, "$1").replace(/\s+/g, " ").trim();
10110
+ if (!unwrapped) {
10111
+ return fallback;
10112
+ }
10113
+ const codePoints = Array.from(unwrapped);
10114
+ if (codePoints.length > MAX_NOTE_LENGTH) {
10115
+ return codePoints.slice(0, MAX_NOTE_LENGTH).join("");
10116
+ }
10117
+ return unwrapped;
10118
+ }
10119
+
9952
10120
  // ../media-studio-contract/src/skills/compose-turn.ts
9953
10121
  function applySkillActivations(text, spans) {
9954
10122
  const ordered = spans.toSorted((a, b) => b.start - a.start);
@@ -10027,6 +10195,15 @@ function attempt2(func) {
10027
10195
  return [error, null];
10028
10196
  }
10029
10197
  }
10198
+
10199
+ // ../../node_modules/es-toolkit/dist/util/attemptAsync.mjs
10200
+ async function attemptAsync(func) {
10201
+ try {
10202
+ return [null, await func()];
10203
+ } catch (error) {
10204
+ return [error, null];
10205
+ }
10206
+ }
10030
10207
  // ../../node_modules/ts-pattern/dist/index.js
10031
10208
  var t2 = Symbol.for("@ts-pattern/matcher");
10032
10209
  var e2 = Symbol.for("@ts-pattern/isVariadic");
@@ -16721,9 +16898,7 @@ function validateApiBaseUrl(urlString) {
16721
16898
  }
16722
16899
  var AuthDataSchema = object2({
16723
16900
  token: string4().min(1),
16724
- apiUrl: string4().refine((val) => isAllowedCliApiOrigin(val), {
16725
- message: "Untrusted API URL in auth file"
16726
- }).optional(),
16901
+ apiUrl: string4().optional(),
16727
16902
  uid: string4().optional(),
16728
16903
  username: string4().optional(),
16729
16904
  email: string4().optional(),
@@ -16737,11 +16912,35 @@ var ConfigFileSchema = object2({
16737
16912
  latest: string4().optional()
16738
16913
  }).optional()
16739
16914
  });
16915
+
16916
+ class UntrustedCredentialError extends Error {
16917
+ rejectedUrl;
16918
+ constructor(rejectedUrl) {
16919
+ super(`Untrusted apiUrl in the saved credential: "${rejectedUrl}". ` + `Allowed hosts: ${MEDIA_STUDIO_HOSTS.join(", ")}. ` + `Run \`msc login\` to sign in - it opens a browser, so a human must complete it.`);
16920
+ this.name = "UntrustedCredentialError";
16921
+ this.rejectedUrl = rejectedUrl;
16922
+ }
16923
+ }
16740
16924
  function readAuthFile() {
16925
+ let raw;
16741
16926
  try {
16742
- const parsed = JSON.parse(readFileSync(getAuthFilePath(), "utf8"));
16743
- const res = AuthDataSchema.safeParse(parsed);
16744
- return res.success ? res.data : null;
16927
+ raw = JSON.parse(readFileSync(getAuthFilePath(), "utf8"));
16928
+ } catch {
16929
+ return null;
16930
+ }
16931
+ const shape = AuthDataSchema.safeParse(raw);
16932
+ if (!shape.success || !shape.data.token.trim()) {
16933
+ return null;
16934
+ }
16935
+ const savedUrl = shape.data.apiUrl?.trim();
16936
+ if (savedUrl && !isAllowedCliApiOrigin(savedUrl)) {
16937
+ throw new UntrustedCredentialError(savedUrl);
16938
+ }
16939
+ return shape.data;
16940
+ }
16941
+ function readAuthFileIfUsable() {
16942
+ try {
16943
+ return readAuthFile();
16745
16944
  } catch {
16746
16945
  return null;
16747
16946
  }
@@ -16795,7 +16994,8 @@ function saveUpdateCheck(check) {
16795
16994
  writeConfigFile({ updateCheck: check });
16796
16995
  }
16797
16996
  function resolveApiKey() {
16798
- const token = readAuthFile()?.token;
16997
+ const auth = readAuthFile();
16998
+ const token = auth?.token;
16799
16999
  if (!token?.trim()) {
16800
17000
  throw new Error("Missing authentication. Run `msc login` to sign in - it opens a browser, so a human " + "must complete it. Instant if that browser is already signed in to the web app.");
16801
17001
  }
@@ -16804,7 +17004,7 @@ function resolveApiKey() {
16804
17004
  function resolveApiBaseUrl(flag) {
16805
17005
  if (flag)
16806
17006
  return validateApiBaseUrl(flag);
16807
- const fromAuth = readAuthFile()?.apiUrl;
17007
+ const fromAuth = readAuthFileIfUsable()?.apiUrl;
16808
17008
  if (typeof fromAuth === "string" && fromAuth.trim()) {
16809
17009
  if (isAllowedCliApiOrigin(fromAuth)) {
16810
17010
  return fromAuth;
@@ -16815,6 +17015,7 @@ function resolveApiBaseUrl(flag) {
16815
17015
 
16816
17016
  // src/client/session-types.ts
16817
17017
  var SessionSourceSchema = _enum2(["web", "cli"]);
17018
+ var CLI_DEFAULT_SESSION_NOTE = "Agent Task";
16818
17019
  var ApiResponseBaseSchema = object2({
16819
17020
  success: boolean4(),
16820
17021
  error: object2({
@@ -16969,6 +17170,15 @@ class ClientSessions {
16969
17170
  const url = `${this.baseUrl}/api/sessions${search ? `?${search}` : ""}`;
16970
17171
  return requestJson(this.customFetch, url, { method: "GET", headers: this.authHeaders() }, ListSessionsResponseSchema, (status) => `Failed to fetch sessions with status ${status}`);
16971
17172
  }
17173
+ async exists(sessionId) {
17174
+ const params = new URLSearchParams({ sessionId, limit: "1" });
17175
+ const [err] = await attemptAsync(() => requestJson(this.customFetch, `${this.baseUrl}/api/sessions/detail?${params.toString()}`, { method: "GET", headers: this.authHeaders() }, unknown2(), (status) => `Failed to verify session ${sessionId} with status ${status}`));
17176
+ if (!err)
17177
+ return true;
17178
+ if (err instanceof ChatUpstreamError && err.status === 404)
17179
+ return false;
17180
+ throw err;
17181
+ }
16972
17182
  async artifacts(query) {
16973
17183
  const params = new URLSearchParams({ sessionId: query.sessionId });
16974
17184
  if (query.taskId !== undefined)
@@ -17006,12 +17216,17 @@ class ClientSessions {
17006
17216
  signal
17007
17217
  });
17008
17218
  if (!response.ok) {
17009
- await response.body?.cancel().catch(() => {
17010
- return;
17011
- });
17219
+ const body = await response.text().catch(() => "");
17012
17220
  const retryable = response.status >= 500 || response.status === 429;
17013
- if (!retryable)
17221
+ if (!retryable) {
17222
+ if (!state.sessionReady) {
17223
+ state.outcome = {
17224
+ status: "failed",
17225
+ error: new ChatUpstreamError(`Session prewarm was rejected with status ${response.status}`, response.status, body)
17226
+ };
17227
+ }
17014
17228
  break;
17229
+ }
17015
17230
  continue;
17016
17231
  }
17017
17232
  await this.consumePrewarmStream(response, state, {
@@ -17030,7 +17245,10 @@ class ClientSessions {
17030
17245
  }
17031
17246
  if (state.outcome)
17032
17247
  return state.outcome;
17033
- return state.sessionReady ? { status: "fallback", session: state.sessionReady } : { status: "failed" };
17248
+ return state.sessionReady ? { status: "fallback", session: state.sessionReady } : {
17249
+ status: "failed",
17250
+ error: new ChatUpstreamError("Session prewarm never completed: no session_ready event (request retries exhausted or timed out).", 0, "")
17251
+ };
17034
17252
  }
17035
17253
  async consumePrewarmStream(response, state, callbacks) {
17036
17254
  if (!response.body)
@@ -17061,12 +17279,18 @@ class ClientSessions {
17061
17279
  }
17062
17280
  case "done": {
17063
17281
  PrewarmDoneSchema.parse(data);
17064
- state.outcome = state.sessionReady ? { status: "ready", session: state.sessionReady } : { status: "failed" };
17282
+ state.outcome = state.sessionReady ? { status: "ready", session: state.sessionReady } : {
17283
+ status: "failed",
17284
+ error: new ChatUpstreamError("Session prewarm ended before the session was created upstream.", 0, "")
17285
+ };
17065
17286
  return;
17066
17287
  }
17067
17288
  case "error": {
17068
- PrewarmErrorSchema.parse(data);
17069
- state.outcome = state.sessionReady ? { status: "fallback", session: state.sessionReady } : { status: "failed" };
17289
+ const payload = PrewarmErrorSchema.parse(data);
17290
+ state.outcome = state.sessionReady ? { status: "fallback", session: state.sessionReady } : {
17291
+ status: "failed",
17292
+ error: new ChatUpstreamError(payload.message, 0, JSON.stringify({ error: payload }))
17293
+ };
17070
17294
  return;
17071
17295
  }
17072
17296
  default:
@@ -17261,7 +17485,7 @@ class MediaStudioClient {
17261
17485
  const nextKey = rotatedToken.trim();
17262
17486
  this._apiKey = nextKey;
17263
17487
  try {
17264
- const authData = readAuthFile();
17488
+ const authData = readAuthFileIfUsable();
17265
17489
  if (authData) {
17266
17490
  saveAuthFile({
17267
17491
  ...authData,
@@ -17584,10 +17808,50 @@ var NEXT_STEP_GUIDANCE = {
17584
17808
  retry: "The failure looks transient. Retrying the same request may succeed.",
17585
17809
  none: ""
17586
17810
  };
17811
+ var TRANSPORT_CAUSE_DEPTH = 5;
17812
+ var TRANSPORT_MESSAGE = /fetch failed|Unable to connect/i;
17813
+ var ERRNO_SHAPE = /^[A-Z][A-Z0-9_]*$/;
17814
+ function isTerminalControl(codePoint) {
17815
+ return codePoint <= 8 && codePoint !== 9 || codePoint >= 14 && codePoint <= 31 || codePoint >= 127 && codePoint <= 159 || codePoint === 8206 || codePoint === 8207 || codePoint >= 8234 && codePoint <= 8238 || codePoint >= 8294 && codePoint <= 8297;
17816
+ }
17817
+ function stripTerminalControls(text) {
17818
+ let out = "";
17819
+ for (const char of text) {
17820
+ if (!isTerminalControl(char.codePointAt(0) ?? -1))
17821
+ out += char;
17822
+ }
17823
+ return out;
17824
+ }
17825
+ function detailOf(node, code) {
17826
+ if (!("message" in node))
17827
+ return "";
17828
+ const { message } = node;
17829
+ if (typeof message !== "string")
17830
+ return "";
17831
+ return stripTerminalControls(message.split(code).join(" ")).replace(/\s+/g, " ").trim().replace(/^[:-]\s*/, "");
17832
+ }
17833
+ function findTransportErrno(err) {
17834
+ let current = err;
17835
+ for (let depth = 0;depth < TRANSPORT_CAUSE_DEPTH; depth++) {
17836
+ if (!(current instanceof Error))
17837
+ return null;
17838
+ const code = "code" in current ? current.code : undefined;
17839
+ const errno = typeof code === "string" && ERRNO_SHAPE.test(code) ? code : null;
17840
+ if (errno)
17841
+ return { code: errno, detail: detailOf(current, errno) };
17842
+ current = current.cause;
17843
+ }
17844
+ return null;
17845
+ }
17846
+ function isTransportFailure(err) {
17847
+ return findTransportErrno(err) !== null || err instanceof Error && TRANSPORT_MESSAGE.test(err.message);
17848
+ }
17587
17849
  function reportCommandError(err, options) {
17588
- const message = err instanceof Error ? err.message : String(err);
17850
+ const plain = err instanceof Error ? err.message : String(err);
17851
+ const transport = findTransportErrno(err);
17852
+ const message = transport && !plain.includes(transport.code) ? `${plain} (${transport.code}${transport.detail ? `: ${transport.detail}` : ""})` : plain;
17589
17853
  const upstream = err instanceof ChatUpstreamError ? err : null;
17590
- const nextStep = upstream?.nextStep ?? (upstream?.status === 401 ? "relogin" : null);
17854
+ const nextStep = upstream?.nextStep ?? (upstream?.status === 401 || err instanceof UntrustedCredentialError ? "relogin" : null);
17591
17855
  const status = upstream?.status !== undefined && upstream.status !== 0 ? upstream.status : null;
17592
17856
  const guidance = nextStep ? NEXT_STEP_GUIDANCE[nextStep] : "";
17593
17857
  if (options?.json) {
@@ -17597,7 +17861,8 @@ function reportCommandError(err, options) {
17597
17861
  ...upstream?.errCode != null ? { errCode: upstream.errCode } : {},
17598
17862
  ...status != null ? { httpStatus: status } : {},
17599
17863
  ...nextStep != null ? { nextStep } : {},
17600
- ...guidance ? { guidance } : {}
17864
+ ...guidance ? { guidance } : {},
17865
+ ...transport ? { transportCode: transport.code } : {}
17601
17866
  };
17602
17867
  process.stdout.write(`${JSON.stringify(payload, null, 2)}
17603
17868
  `);
@@ -17812,29 +18077,66 @@ async function resolvePromptAttachments(options) {
17812
18077
  }
17813
18078
 
17814
18079
  // src/services/session.ts
18080
+ var SESSION_GONE_ERR_CODES = [
18081
+ ErrCode.SESSION_NOT_FOUND,
18082
+ ErrCode.CHAT_SESSION_EXPIRED
18083
+ ];
17815
18084
  function isSessionGoneError(err) {
17816
18085
  if (!(err instanceof ChatUpstreamError))
17817
18086
  return false;
18087
+ if (err.errCode !== null) {
18088
+ return SESSION_GONE_ERR_CODES.includes(err.errCode);
18089
+ }
17818
18090
  if (err.status === 404)
17819
18091
  return true;
17820
18092
  return err.body.includes(ERR_SESSION_EXPIRED);
17821
18093
  }
18094
+
18095
+ class SessionNotFoundError extends Error {
18096
+ sessionId;
18097
+ constructor(sessionId) {
18098
+ super(`Session ${sessionId} does not exist.
18099
+ ` + ' → Create one: `msc sessions use --new`, or `msc chat --new "<prompt>"` to also send a prompt.');
18100
+ this.name = "SessionNotFoundError";
18101
+ this.sessionId = sessionId;
18102
+ }
18103
+ }
18104
+ async function ensureSessionExists(options) {
18105
+ const client = new MediaStudioClient({
18106
+ baseUrl: options.apiBaseUrl,
18107
+ apiKey: options.apiKey
18108
+ });
18109
+ if (await client.sessions.exists(options.sessionId))
18110
+ return;
18111
+ throw new SessionNotFoundError(options.sessionId);
18112
+ }
18113
+ function finalizeProvision(result, spinner) {
18114
+ spinner?.stop();
18115
+ if (result.status === "failed") {
18116
+ throw result.error;
18117
+ }
18118
+ if (result.status === "fallback") {
18119
+ consola.warn("Session created, but sandbox/skill setup did not fully complete. Plain-text chat still works.");
18120
+ }
18121
+ return result.sessionId;
18122
+ }
17822
18123
  async function provisionSession(options) {
17823
- const sessionId = nanoid3();
17824
18124
  const client = new MediaStudioClient({
17825
18125
  baseUrl: options.apiBaseUrl,
17826
18126
  apiKey: options.apiKey
17827
18127
  });
18128
+ const sessionId = nanoid3();
17828
18129
  const outcome = await client.sessions.prewarm({
17829
18130
  sessionId,
17830
18131
  source: "cli",
18132
+ note: options.note ?? CLI_DEFAULT_SESSION_NOTE,
17831
18133
  onSessionReady: () => saveLastSessionId(sessionId),
17832
- ...options.onStageChange ? { onStageChange: options.onStageChange } : {}
18134
+ onStageChange: options.onStageChange
17833
18135
  });
17834
- return M(outcome).with({ status: "ready" }, { status: "fallback" }, ({ status }) => ({
17835
- status,
17836
- sessionId
17837
- })).with({ status: "failed" }, () => ({ status: "failed" })).exhaustive();
18136
+ if (outcome.status === "failed") {
18137
+ return { status: "failed", error: outcome.error };
18138
+ }
18139
+ return { status: outcome.status, sessionId };
17838
18140
  }
17839
18141
 
17840
18142
  // src/lib/skill-refs.ts
@@ -17890,7 +18192,7 @@ async function ensureUsable(client, skill) {
17890
18192
  });
17891
18193
  if (ensured)
17892
18194
  return;
17893
- throw new SkillResolutionError(describeEnableFailure(skill, cause));
18195
+ throw new SkillResolutionError(describeEnableFailure(skill, cause), cause ? { cause } : undefined);
17894
18196
  }
17895
18197
  async function resolvePromptSkills(options) {
17896
18198
  const spans = findSkillRefSpans(options.prompt);
@@ -17963,34 +18265,59 @@ function printTerminalMediaAssets(mediaAssets, fullText) {
17963
18265
  process.stdout.write(`====================
17964
18266
  `);
17965
18267
  }
17966
- function finalizeProvision(result, spinner) {
17967
- spinner.stop();
17968
- if (result.status === "failed") {
17969
- throw new Error("Session provisioning failed before any session was created upstream. Try again.");
17970
- }
17971
- if (result.status === "fallback") {
17972
- consola.warn("Session created, but sandbox/skill setup did not fully complete. Continuing with chat.");
17973
- }
17974
- return result.sessionId;
18268
+ function buildNotePrompt(prompt, attachments) {
18269
+ return prompt.replace(ATTACHMENT_MARKER_REGEX, (marker, url) => {
18270
+ const attachment = attachments.find((a) => a.remoteUrl === url);
18271
+ return attachment ? attachment.name || attachment.remoteUrl : marker;
18272
+ });
18273
+ }
18274
+ async function provisionNewSession(options) {
18275
+ const onStageChange = options.quiet ? undefined : (stage) => options.spinner.start(PROVISION_STAGE_LABELS[stage]);
18276
+ const note = deriveFirstTurnNote(options.prompt, {
18277
+ fallbackTitle: CLI_DEFAULT_SESSION_NOTE
18278
+ });
18279
+ const result = await provisionSession({
18280
+ apiBaseUrl: options.apiBaseUrl,
18281
+ apiKey: options.apiKey,
18282
+ note,
18283
+ onStageChange
18284
+ });
18285
+ return finalizeProvision(result, options.spinner);
17975
18286
  }
17976
18287
  async function resolveChatSession(options) {
18288
+ if (options.newSession) {
18289
+ return {
18290
+ sessionId: await provisionNewSession({
18291
+ ...options,
18292
+ prompt: options.prompt
18293
+ }),
18294
+ recreateOnGone: false
18295
+ };
18296
+ }
17977
18297
  if (options.sessionFlag) {
17978
18298
  saveLastSessionId(options.sessionFlag);
17979
- return { sessionId: options.sessionFlag, recreateOnGone: true };
18299
+ return { sessionId: options.sessionFlag, recreateOnGone: false };
17980
18300
  }
17981
18301
  const pointer = readLastSessionId();
17982
18302
  if (pointer) {
17983
18303
  return { sessionId: pointer, recreateOnGone: true };
17984
18304
  }
17985
- const result = await provisionSession({
17986
- apiBaseUrl: options.apiBaseUrl,
17987
- apiKey: options.apiKey,
17988
- ...options.quiet ? {} : {
17989
- onStageChange: (stage) => options.spinner.start(PROVISION_STAGE_LABELS[stage])
17990
- }
17991
- });
17992
- const sessionId = finalizeProvision(result, options.spinner);
17993
- return { sessionId, recreateOnGone: false };
18305
+ return {
18306
+ sessionId: await provisionNewSession({
18307
+ ...options,
18308
+ prompt: options.prompt
18309
+ }),
18310
+ recreateOnGone: false
18311
+ };
18312
+ }
18313
+ function describeStreamFailure(err) {
18314
+ if (err instanceof Error && err.name === "AbortError") {
18315
+ return "aborted";
18316
+ }
18317
+ if (err instanceof Error && err.message) {
18318
+ return err.message;
18319
+ }
18320
+ return String(err);
17994
18321
  }
17995
18322
  async function streamTurn(options) {
17996
18323
  const { args, sessionId, spinner, autoEnabled, attachments } = options;
@@ -18011,84 +18338,103 @@ async function streamTurn(options) {
18011
18338
  const steps = [];
18012
18339
  const mediaAssets = [];
18013
18340
  let hasFinished = false;
18014
- for await (const ev of stream) {
18015
- M(ev).with({ type: "text-delta", delta: z2.string }, ({ delta }) => {
18016
- isThinking = false;
18017
- spinner.stop();
18018
- fullText += delta;
18019
- if (!args.json) {
18020
- process.stdout.write(delta);
18341
+ let emitting = false;
18342
+ let streamFailure;
18343
+ try {
18344
+ for await (const ev of stream) {
18345
+ if (ev.type !== "finish" && ev.type !== "error") {
18346
+ options.progress.emitted = true;
18021
18347
  }
18022
- }).with({ type: "reasoning-delta", delta: z2.string }, ({ delta }) => {
18023
- reasoning += delta;
18024
- if (args["show-reasoning"] && !args.json) {
18348
+ M(ev).with({ type: "text-delta", delta: z2.string }, ({ delta }) => {
18349
+ isThinking = false;
18025
18350
  spinner.stop();
18026
- process.stdout.write(delta);
18027
- } else if (!args.json) {
18028
- if (!isThinking) {
18029
- isThinking = true;
18351
+ fullText += delta;
18352
+ if (!args.json) {
18353
+ process.stdout.write(delta);
18354
+ }
18355
+ }).with({ type: "reasoning-delta", delta: z2.string }, ({ delta }) => {
18356
+ reasoning += delta;
18357
+ if (args["show-reasoning"] && !args.json) {
18030
18358
  spinner.stop();
18031
- if (fullText && !fullText.endsWith(`
18359
+ process.stdout.write(delta);
18360
+ } else if (!args.json) {
18361
+ if (!isThinking) {
18362
+ isThinking = true;
18363
+ spinner.stop();
18364
+ if (fullText && !fullText.endsWith(`
18032
18365
  `)) {
18033
- process.stdout.write(`
18366
+ process.stdout.write(`
18034
18367
  `);
18035
- fullText += `
18368
+ fullText += `
18036
18369
  `;
18037
- }
18038
- process.stdout.write(`[reasoning] Thinking...
18370
+ }
18371
+ process.stdout.write(`[reasoning] Thinking...
18039
18372
  `);
18040
- spinner.start("Thinking...");
18373
+ spinner.start("Thinking...");
18374
+ }
18041
18375
  }
18042
- }
18043
- }).with({ type: "tool-call", toolName: z2.string }, ({ toolName, args: toolArgs }) => {
18044
- isThinking = false;
18045
- spinner.stop();
18046
- if (fullText && !fullText.endsWith(`
18376
+ }).with({ type: "tool-call", toolName: z2.string }, ({ toolName, args: toolArgs }) => {
18377
+ isThinking = false;
18378
+ spinner.stop();
18379
+ if (fullText && !fullText.endsWith(`
18047
18380
  `) && !args.json) {
18048
- process.stdout.write(`
18381
+ process.stdout.write(`
18049
18382
  `);
18050
- fullText += `
18383
+ fullText += `
18051
18384
  `;
18052
- }
18053
- const formatted = formatToolMilestone(toolName, toolArgs);
18054
- steps.push(formatted.milestoneLine);
18055
- if (!args.json) {
18056
- process.stdout.write(`${formatted.milestoneLine}
18385
+ }
18386
+ const formatted = formatToolMilestone(toolName, toolArgs);
18387
+ steps.push(formatted.milestoneLine);
18388
+ if (!args.json) {
18389
+ process.stdout.write(`${formatted.milestoneLine}
18057
18390
  `);
18058
- spinner.start(formatted.actionLabel);
18059
- }
18060
- }).with({ type: "media-result" }, ({ result }) => {
18061
- spinner.stop();
18062
- addOrMergeMediaResult(mediaAssets, result);
18063
- }).with({ type: "error" }, (ev) => {
18064
- spinner.stop();
18065
- const raw = ("error" in ev && typeof ev.error === "string" ? ev.error : null) ?? ("message" in ev && typeof ev.message === "string" ? ev.message : null) ?? "Upstream stream error";
18066
- throw new ChatUpstreamError(extractUpstreamErrorMessage(raw) || raw, 0, raw);
18067
- }).with({ type: "finish" }, () => {
18068
- hasFinished = true;
18069
- spinner.stop();
18070
- if (args.json) {
18071
- const output = {
18072
- status: "success",
18073
- sessionId,
18074
- text: fullText,
18075
- steps,
18076
- media: mediaAssets,
18077
- ...attachments.length > 0 ? { attachments } : {},
18078
- ...autoEnabled.length > 0 ? { autoEnabled } : {},
18079
- ...args["show-reasoning"] && reasoning ? { reasoning } : {}
18080
- };
18081
- process.stdout.write(`${JSON.stringify(output, null, 2)}
18391
+ spinner.start(formatted.actionLabel);
18392
+ }
18393
+ }).with({ type: "media-result" }, ({ result }) => {
18394
+ spinner.stop();
18395
+ addOrMergeMediaResult(mediaAssets, result);
18396
+ }).with({ type: "error" }, (ev) => {
18397
+ spinner.stop();
18398
+ const raw = ("error" in ev && typeof ev.error === "string" ? ev.error : null) ?? ("message" in ev && typeof ev.message === "string" ? ev.message : null) ?? "Upstream stream error";
18399
+ throw new ChatUpstreamError(extractUpstreamErrorMessage(raw) || raw, 0, raw);
18400
+ }).with({ type: "finish" }, () => {
18401
+ emitting = true;
18402
+ hasFinished = true;
18403
+ spinner.stop();
18404
+ if (args.json) {
18405
+ const output = {
18406
+ status: "success",
18407
+ sessionId,
18408
+ text: fullText,
18409
+ steps,
18410
+ media: mediaAssets,
18411
+ ...attachments.length > 0 ? { attachments } : {},
18412
+ ...autoEnabled.length > 0 ? { autoEnabled } : {},
18413
+ ...args["show-reasoning"] && reasoning ? { reasoning } : {}
18414
+ };
18415
+ process.stdout.write(`${JSON.stringify(output, null, 2)}
18082
18416
  `);
18083
- return;
18084
- }
18085
- printTerminalMediaAssets(mediaAssets, fullText);
18086
- process.stdout.write(`session: ${sessionId}
18417
+ emitting = false;
18418
+ return;
18419
+ }
18420
+ printTerminalMediaAssets(mediaAssets, fullText);
18421
+ process.stdout.write(`session: ${sessionId}
18087
18422
  `);
18088
- }).with(z2._, () => {}).exhaustive();
18423
+ emitting = false;
18424
+ }).with(z2._, () => {}).exhaustive();
18425
+ }
18426
+ } catch (err) {
18427
+ if (err instanceof ChatUpstreamError)
18428
+ throw err;
18429
+ if (emitting)
18430
+ throw err;
18431
+ if (!hasFinished && !options.progress.emitted)
18432
+ throw err;
18433
+ streamFailure = err;
18089
18434
  }
18090
18435
  if (!hasFinished) {
18091
18436
  spinner.stop();
18437
+ consola.warn(streamFailure === undefined ? "Stream ended early: no finish event" : `Stream ended early: ${describeStreamFailure(streamFailure)}`);
18092
18438
  if (args.json) {
18093
18439
  const output = {
18094
18440
  status: "incomplete",
@@ -18113,20 +18459,49 @@ async function runTurn(options) {
18113
18459
  try {
18114
18460
  await streamTurn(options);
18115
18461
  } catch (err) {
18116
- if (options.recreateOnGone && isSessionGoneError(err)) {
18117
- consola.warn(`Session ${options.sessionId} is no longer available. Provisioning a new one...`);
18118
- const result = await provisionSession({
18119
- apiBaseUrl: options.apiBaseUrl,
18120
- apiKey: resolveApiKey(),
18121
- ...options.args.json ? {} : {
18122
- onStageChange: (stage) => options.spinner.start(PROVISION_STAGE_LABELS[stage])
18123
- }
18124
- });
18125
- const sessionId = finalizeProvision(result, options.spinner);
18126
- await streamTurn({ ...options, sessionId, apiKey: resolveApiKey() });
18127
- return;
18462
+ const sessionGone = isSessionGoneError(err);
18463
+ const ambiguousGone = err instanceof ChatUpstreamError && err.errCode === ErrCode.INVALID_PARAM_FORMAT;
18464
+ if (!sessionGone && !ambiguousGone) {
18465
+ throw err;
18128
18466
  }
18129
- throw err;
18467
+ if (options.progress.emitted) {
18468
+ throw new Error(`Session ${options.sessionId} failed after this turn had already started.
18469
+ ` + " → The partial result above stands; re-run the same prompt to continue in a new session.");
18470
+ }
18471
+ if (!options.recreateOnGone) {
18472
+ if (sessionGone || options.args.session) {
18473
+ throw new SessionNotFoundError(options.sessionId);
18474
+ }
18475
+ throw err;
18476
+ }
18477
+ if (ambiguousGone) {
18478
+ let confirmedGone = false;
18479
+ try {
18480
+ await ensureSessionExists({
18481
+ apiBaseUrl: options.apiBaseUrl,
18482
+ apiKey: resolveApiKey(),
18483
+ sessionId: options.sessionId
18484
+ });
18485
+ } catch (checkErr) {
18486
+ confirmedGone = checkErr instanceof SessionNotFoundError;
18487
+ }
18488
+ if (!confirmedGone) {
18489
+ throw err;
18490
+ }
18491
+ }
18492
+ consola.warn(`Session ${options.sessionId} is no longer available. Provisioning a new one...`);
18493
+ const sessionId = await provisionNewSession({
18494
+ apiBaseUrl: options.apiBaseUrl,
18495
+ apiKey: resolveApiKey(),
18496
+ spinner: options.spinner,
18497
+ quiet: options.args.json === true,
18498
+ prompt: buildNotePrompt(options.prompt, options.attachments)
18499
+ });
18500
+ await streamTurn({
18501
+ ...options,
18502
+ sessionId,
18503
+ apiKey: resolveApiKey()
18504
+ });
18130
18505
  }
18131
18506
  }
18132
18507
  var chatCommand = defineCommand({
@@ -18147,7 +18522,11 @@ var chatCommand = defineCommand({
18147
18522
  },
18148
18523
  session: {
18149
18524
  type: "string",
18150
- description: "Session id to chat with; becomes the default session for future runs"
18525
+ description: "Chat in an existing session id (verified upstream); becomes the default session. A missing id fails — create one with --new"
18526
+ },
18527
+ new: {
18528
+ type: "boolean",
18529
+ description: "Create a new session and chat in it; becomes the default session"
18151
18530
  },
18152
18531
  "show-reasoning": {
18153
18532
  type: "boolean",
@@ -18159,6 +18538,16 @@ var chatCommand = defineCommand({
18159
18538
  const spinner = new AgentSpinner;
18160
18539
  try {
18161
18540
  const apiBaseUrl = resolveApiBaseUrl();
18541
+ if (args.new && args.session) {
18542
+ throw new Error("--new and --session cannot be combined: --session selects an existing session, --new creates one.");
18543
+ }
18544
+ if (args.session) {
18545
+ await ensureSessionExists({
18546
+ apiBaseUrl,
18547
+ apiKey: resolveApiKey(),
18548
+ sessionId: args.session
18549
+ });
18550
+ }
18162
18551
  const { text, skillUrls, autoEnabled } = await resolvePromptSkills({
18163
18552
  prompt: args.prompt,
18164
18553
  apiBaseUrl,
@@ -18177,6 +18566,8 @@ var chatCommand = defineCommand({
18177
18566
  });
18178
18567
  const resolved = await resolveChatSession({
18179
18568
  sessionFlag: args.session,
18569
+ newSession: args.new,
18570
+ prompt: buildNotePrompt(promptWithAttachments, attachments),
18180
18571
  apiBaseUrl,
18181
18572
  apiKey: resolveApiKey(),
18182
18573
  spinner,
@@ -18187,6 +18578,7 @@ var chatCommand = defineCommand({
18187
18578
  prompt: promptWithAttachments,
18188
18579
  sessionId: resolved.sessionId,
18189
18580
  recreateOnGone: resolved.recreateOnGone,
18581
+ progress: { emitted: false },
18190
18582
  apiBaseUrl,
18191
18583
  apiKey: resolveApiKey(),
18192
18584
  spinner,
@@ -18243,7 +18635,11 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
18243
18635
 
18244
18636
  msc whoami
18245
18637
  Displays your current account information (user, email, UID, server URL).
18246
- Exits 0 if authenticated, 1 if unauthenticated or session expired.
18638
+ Exits 0 only when the platform confirmed the session, and the fields it
18639
+ prints are the platform's answer. Exits 1 otherwise - unauthenticated,
18640
+ expired, or the request never reached the platform (DNS, refused, TLS):
18641
+ a saved credential describes a session that was never checked, so it is
18642
+ never printed in place of an answer.
18247
18643
 
18248
18644
  msc logout
18249
18645
  Removes saved credentials on this machine.
@@ -18285,8 +18681,16 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
18285
18681
  "no such file". Remote URLs are not attachments and there is no @url form.
18286
18682
 
18287
18683
  Session Switching:
18288
- Pass \`--session <id>\` to direct a prompt to a specific existing session
18289
- and make it your new active session.
18684
+ Pass \`--session <id>\` to direct a prompt to an existing session and make it
18685
+ your new active session. The id is verified against the server first: the
18686
+ CLI only ever selects sessions that exist, so an unknown id fails with a
18687
+ hint instead of silently starting a different conversation.
18688
+
18689
+ Pass \`--new\` to start a fresh session (new id, never reused) and chat in
18690
+ it, ignoring the current default. \`--new\` and \`--session\` are mutually
18691
+ exclusive.
18692
+ msc chat "start over" --new # new session, new context
18693
+ msc sessions # pick an existing id for --session
18290
18694
 
18291
18695
  4. USING SKILLS
18292
18696
 
@@ -18301,7 +18705,7 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
18301
18705
  msc skills add <slug> # add a skill from the plaza to your library
18302
18706
 
18303
18707
  Referencing Skills in Prompts:
18304
- Lead the prompt with your skill reference — the message must start with it
18708
+ Lead the prompt with your skill reference - the message must start with it
18305
18709
  for the skill to activate:
18306
18710
  Slash form: msc chat "/<name> your prompt here"
18307
18711
  Marker form: msc chat "%%/<name>%% your prompt here"
@@ -18327,7 +18731,13 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url} (${r.des
18327
18731
  including download URLs and model metadata.
18328
18732
 
18329
18733
  msc sessions use <session-id>
18330
- Switch your active session pointer without sending a prompt.
18734
+ Switch your active session pointer to an existing session. The id is
18735
+ verified against the server; an unknown id fails here and leaves the
18736
+ pointer untouched.
18737
+
18738
+ msc sessions use --new
18739
+ Create a new session upstream and switch the pointer to it, without sending
18740
+ a prompt.
18331
18741
 
18332
18742
  Session deletion is not available in the CLI. Open the corresponding session
18333
18743
  in the Media Studio web app and delete it there manually.
@@ -18371,17 +18781,92 @@ ${getAvailableRegions().map((r) => ` ${r.key.padEnd(8)} - ${r.url}`).join(`
18371
18781
  Extracting the result URL (e.g. with jq):
18372
18782
  url=$(msc chat "a red balloon" --json | jq -r '.media[0].resultUrl')
18373
18783
 
18784
+ Exit codes: 0 = the turn ran to completion OR its stream ended early; 1 =
18785
+ \`status: "error"\`, a usage error, or any other failure. Only what the parser
18786
+ itself rejects (unknown flag, missing \`<prompt>\`) exits 1 before the command
18787
+ runs with an empty stdout; argument conflicts checked inside the command --
18788
+ \`--new\` with \`--session\`, \`sessions use --new <id>\` -- do print the error
18789
+ payload below. Either way, check the exit code first, then branch on
18790
+ \`.status\`.
18791
+ Exit code 1 does not imply a payload. A bare transport failure -- the host
18792
+ does not resolve, the port is refused, TLS fails -- is printed by the runtime
18793
+ with its \`cause\` chain instead, so \`--json\` stdout stays empty there: to
18794
+ tell "could not reach the platform" from "the platform answered with a
18795
+ failure", read the exit code, then check whether stdout has a payload at all.
18796
+ \`"success" | "incomplete" | "error"\` are \`chat\`-envelope values: \`chat\` is
18797
+ the only command whose *success* payload carries a \`status\`, and every
18798
+ command that supports \`--json\` puts \`"status": "error"\` on its failure
18799
+ payload. \`login\`, \`logout\` and \`whoami\` define no \`--json\` and emit no
18800
+ payload at all -- their failures are prose on stderr with exit 1. Do not
18801
+ reuse that vocabulary elsewhere -- \`sessions --json\` items carry
18802
+ \`status: { chat, task }\` (a boolean pair) and \`sessions artifacts <id> --json\`
18803
+ items carry the task lifecycle
18804
+ (\`pending | running | processing | completed | failed | cancelled\`).
18805
+ Human stdout carries no status marker -- an early end only warns on
18806
+ stderr -- so never parse it in scripts.
18807
+
18374
18808
  Error schema on failure (exit code 1):
18375
18809
  {
18376
18810
  "status": "error",
18377
18811
  "message": "string", // human-readable error description
18378
18812
  "errCode": 12345, // optional platform error code
18379
18813
  "httpStatus": 401, // optional HTTP status code
18380
- "nextStep": "relogin" // recommended action keyword
18814
+ "nextStep": "relogin", // recommended action keyword
18815
+ "guidance": "string?", // present when nextStep names an action;
18816
+ // absent for nextStep "none"
18817
+ "transportCode": "ECONNREFUSED" // optional: errno of a transport failure
18818
+ // the error wraps, when it had one
18819
+ }
18820
+
18821
+ Classifying a failure (branch on these fields, not on message prose):
18822
+ A consumer reads \`errCode\`, \`httpStatus\`, \`nextStep\` and
18823
+ \`transportCode\` - never the prose in \`message\`, which is written for a
18824
+ human and may change between versions.
18825
+ \`errCode\`, \`httpStatus\`, \`nextStep\` and \`transportCode\` are optional
18826
+ on *every* failure. All four absent means unclassified: nothing structured
18827
+ to act on, so report the payload as-is.
18828
+ \`transportCode\` is diagnostic, not a branch: it names the errno a
18829
+ transport failure left on the error's cause chain when a wrapper copied
18830
+ only the constant message. A bare transport failure does not reach this
18831
+ reporter at all - the runtime prints it with its cause chain and the
18832
+ command exits 1.
18833
+
18834
+ A rejected saved credential (auth.json's apiUrl is off the CLI allowlist)
18835
+ reports the same error shape with no errCode/httpStatus - nothing came from
18836
+ the platform - and nextStep "relogin", because \`msc login\` rewrites the file:
18837
+ {
18838
+ "status": "error",
18839
+ "message": "Untrusted apiUrl in the saved credential: ...",
18840
+ "nextStep": "relogin",
18841
+ "guidance": "Run \`msc login\` to re-authenticate; ..."
18381
18842
  }
18843
+ With no credential at all the payload stays minimal (status + message only),
18844
+ so an agent can tell "credential file rejected" from "not logged in" without
18845
+ reading the message.
18846
+
18847
+ A chat turn whose stream is open and then ends before the finish event --
18848
+ the connection dropped, or the response was truncated -- is not a failure: it
18849
+ exits 0 with \`status: "incomplete"\` and whatever
18850
+ \`text\`/\`steps\`/\`media\` had arrived. An early end may therefore carry an
18851
+ empty \`text\` and an empty \`media\` array, so do not treat \`incomplete\` as
18852
+ proof that a partial result exists. This applies once the turn has observed
18853
+ any frame at all; a stream request that fails before that -- the host does not
18854
+ resolve, the port is refused, TLS fails, and no event ever arrived -- never
18855
+ reaches \`incomplete\`: it is \`status: "error"\` with exit 1, carrying the
18856
+ errno in \`transportCode\` (and naming it in \`message\`). A
18857
+ platform error (an upstream \`error\` frame, or a non-2xx response) is likewise
18858
+ \`status: "error"\` with exit 1.
18859
+
18860
+ Session commands:
18861
+ msc sessions --json # list: [{ sessionId, note, ... }]
18862
+ msc sessions use <id> --json # { "sessionId": "string" }
18863
+ msc sessions use --new --json # { "sessionId": "string" } (newly created)
18864
+ \`--session\`/\`use <id>\` verify the id upstream; an unknown id exits 1 with
18865
+ the \`--new\` hint and leaves the default session untouched.
18382
18866
 
18383
18867
  Recommended next steps:
18384
- relogin Session expired or missing; run \`msc login\`
18868
+ relogin Session expired or missing, or a saved credential was
18869
+ rejected (its apiUrl is off the allowlist); run \`msc login\`
18385
18870
  retry_later Rate limit reached; wait before re-sending
18386
18871
  top_up_balance Insufficient balance; top up your account
18387
18872
  fix_input Invalid parameters or prompt; adjust input
@@ -19294,7 +19779,7 @@ function applyForHost(urlString) {
19294
19779
  return true;
19295
19780
  }
19296
19781
  function applyInsecureTlsIfRequested() {
19297
- const target = readAuthFile()?.apiUrl || getDefaultApiBaseUrl();
19782
+ const target = readAuthFileIfUsable()?.apiUrl || getDefaultApiBaseUrl();
19298
19783
  return applyForHost(target);
19299
19784
  }
19300
19785
  function applyInsecureTlsForUrl(urlString) {
@@ -20167,37 +20652,36 @@ var loginCommand = defineCommand({
20167
20652
  }
20168
20653
  },
20169
20654
  run: async ({ args }) => {
20170
- try {
20171
- if (process.env.CI || process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
20172
- throw new Error(`Browser sign-in needs a graphical session on this machine, so it cannot complete here.
20655
+ if (process.env.CI || process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
20656
+ throw new Error(`Browser sign-in needs a graphical session on this machine, so it cannot complete here.
20173
20657
  ` + "Run `msc login` from a terminal on a machine whose browser is signed in to Media Studio.\n" + "See `msc guide` for the full authentication flow.");
20658
+ }
20659
+ let targetApiUrl;
20660
+ if (args.region) {
20661
+ targetApiUrl = matchRegionUrl(args.region);
20662
+ } else if (process.env.CI || false || !process.stdin.isTTY) {
20663
+ targetApiUrl = SERVERS.default.url;
20664
+ } else {
20665
+ const answer = await select({
20666
+ message: "Select server:",
20667
+ options: getAvailableRegions().map((opt) => ({
20668
+ label: opt.label,
20669
+ value: opt.url
20670
+ })),
20671
+ initialValue: SERVERS.default.url
20672
+ });
20673
+ if (isCancel(answer)) {
20674
+ process.exit(130);
20174
20675
  }
20175
- let targetApiUrl;
20176
- if (args.region) {
20177
- targetApiUrl = matchRegionUrl(args.region);
20178
- } else if (process.env.CI || false || !process.stdin.isTTY) {
20179
- targetApiUrl = SERVERS.default.url;
20180
- } else {
20181
- const answer = await select({
20182
- message: "Select server:",
20183
- options: getAvailableRegions().map((opt) => ({
20184
- label: opt.label,
20185
- value: opt.url
20186
- })),
20187
- initialValue: SERVERS.default.url
20188
- });
20189
- if (isCancel(answer)) {
20190
- process.exit(130);
20191
- }
20192
- targetApiUrl = String(answer);
20193
- }
20194
- applyInsecureTlsForUrl(targetApiUrl);
20195
- const apiUrl = resolveApiBaseUrl(targetApiUrl);
20196
- consola.info("Opening browser for sign-in...");
20197
- if (!process.stdin.isTTY) {
20198
- const regionList = getAvailableRegions().map((r) => ` - ${r.key.padEnd(8)}: ${r.url} (${r.description})`).join(`
20676
+ targetApiUrl = String(answer);
20677
+ }
20678
+ applyInsecureTlsForUrl(targetApiUrl);
20679
+ const apiUrl = resolveApiBaseUrl(targetApiUrl);
20680
+ consola.info("Opening browser for sign-in...");
20681
+ if (!process.stdin.isTTY) {
20682
+ const regionList = getAvailableRegions().map((r) => ` - ${r.key.padEnd(8)}: ${r.url} (${r.description})`).join(`
20199
20683
  `);
20200
- process.stdout.write(`Sign-in needs a human at a browser on this machine:
20684
+ process.stdout.write(`Sign-in needs a human at a browser on this machine:
20201
20685
  ` + ` 1. already signed in to the Media Studio web app there -> completes at once
20202
20686
  ` + ` 2. not signed in -> a browser opens; a human finishes it within 120s
20203
20687
  ` + `
@@ -20207,29 +20691,24 @@ ${regionList}
20207
20691
  ` + `
20208
20692
  ` + `If you are an agent, ask the user to sign in to the web app in their browser first
20209
20693
  ` + "or ask the user to run `msc login` in their own terminal,\n" + "then poll `msc whoami` until it succeeds.\n");
20210
- }
20211
- const result = await runLoopbackLogin({
20212
- apiUrl,
20213
- onUrl: (url) => {
20214
- consola.info(`If your browser does not open automatically, visit:
20694
+ }
20695
+ const result = await runLoopbackLogin({
20696
+ apiUrl,
20697
+ onUrl: (url) => {
20698
+ consola.info(`If your browser does not open automatically, visit:
20215
20699
  ${url}
20216
20700
  ` + "Signed in to the Media Studio web app in that browser already? This completes without a prompt.");
20217
- }
20218
- });
20219
- saveAuthFile({
20220
- token: result.token,
20221
- apiUrl,
20222
- uid: result.uid,
20223
- username: result.username,
20224
- email: result.email,
20225
- updated_at: new Date().toISOString()
20226
- });
20227
- consola.success("Successfully logged in!");
20228
- } catch (err) {
20229
- const message = err instanceof Error ? err.message : String(err);
20230
- consola.error(message);
20231
- process.exit(1);
20232
- }
20701
+ }
20702
+ });
20703
+ saveAuthFile({
20704
+ token: result.token,
20705
+ apiUrl,
20706
+ uid: result.uid,
20707
+ username: result.username,
20708
+ email: result.email,
20709
+ updated_at: new Date().toISOString()
20710
+ });
20711
+ consola.success("Successfully logged in!");
20233
20712
  }
20234
20713
  });
20235
20714
 
@@ -20254,7 +20733,7 @@ function renderSessionsTable(sessions, currentSessionId) {
20254
20733
  id: s.id === currentSessionId ? `${s.id} (current)` : s.id,
20255
20734
  source: s.source,
20256
20735
  lastActive: formatLastActive(s.lastActiveAt),
20257
- note: s.note ? s.note.replace(/\r?\n/g, " ").trim() : "-"
20736
+ note: s.note ? s.note.replace(/\r?\n/g, " ").trim() || CLI_DEFAULT_SESSION_NOTE : CLI_DEFAULT_SESSION_NOTE
20258
20737
  }));
20259
20738
  const headers = {
20260
20739
  id: "ID",
@@ -20327,6 +20806,8 @@ var listSessionsCommand = defineCommand({
20327
20806
  const currentId = readLastSessionId();
20328
20807
  consola.log(renderSessionsTable(response.data, currentId));
20329
20808
  } catch (err) {
20809
+ if (isTransportFailure(err))
20810
+ throw err;
20330
20811
  reportCommandError(err, { json: args.json });
20331
20812
  }
20332
20813
  }
@@ -20365,6 +20846,8 @@ var artifactsCommand = defineCommand({
20365
20846
  }
20366
20847
  consola.log(renderArtifactsTable(response.data));
20367
20848
  } catch (err) {
20849
+ if (isTransportFailure(err))
20850
+ throw err;
20368
20851
  reportCommandError(err, { json: args.json });
20369
20852
  }
20370
20853
  }
@@ -20372,18 +20855,66 @@ var artifactsCommand = defineCommand({
20372
20855
  var useSessionCommand = defineCommand({
20373
20856
  meta: {
20374
20857
  name: "use",
20375
- description: "Switch default session (local pointer only, no network request)"
20858
+ description: "Switch the default session to an existing session, or create a fresh one with --new"
20376
20859
  },
20377
20860
  args: {
20378
20861
  sessionId: {
20379
20862
  type: "positional",
20380
- required: true,
20381
- description: "Session id to set as default"
20863
+ required: false,
20864
+ description: "Existing session id to set as default"
20865
+ },
20866
+ new: {
20867
+ type: "boolean",
20868
+ description: "Create a new session and make it the default"
20869
+ },
20870
+ json: {
20871
+ type: "boolean",
20872
+ alias: "j",
20873
+ description: "Output machine-readable JSON: { sessionId }"
20382
20874
  }
20383
20875
  },
20384
- run: ({ args }) => {
20385
- saveLastSessionId(args.sessionId);
20386
- consola.success(`Switched default session to ${args.sessionId}`);
20876
+ run: async ({ args }) => {
20877
+ try {
20878
+ const sessionId = args.sessionId;
20879
+ const apiBaseUrl = resolveApiBaseUrl();
20880
+ if (args.new) {
20881
+ if (sessionId) {
20882
+ throw new Error("--new and <session-id> cannot be combined: pass one, not both.");
20883
+ }
20884
+ const result = await provisionSession({
20885
+ apiBaseUrl,
20886
+ apiKey: resolveApiKey(),
20887
+ note: CLI_DEFAULT_SESSION_NOTE
20888
+ });
20889
+ const created = finalizeProvision(result);
20890
+ if (args.json) {
20891
+ process.stdout.write(`${JSON.stringify({ sessionId: created })}
20892
+ `);
20893
+ return;
20894
+ }
20895
+ consola.success(`Created and switched to session ${created}`);
20896
+ return;
20897
+ }
20898
+ if (!sessionId) {
20899
+ throw new Error("Pass the session id to switch to, or --new to create a new session.");
20900
+ }
20901
+ await ensureSessionExists({
20902
+ apiBaseUrl,
20903
+ apiKey: resolveApiKey(),
20904
+ sessionId
20905
+ });
20906
+ saveLastSessionId(sessionId);
20907
+ if (args.json) {
20908
+ process.stdout.write(`${JSON.stringify({ sessionId })}
20909
+ `);
20910
+ return;
20911
+ }
20912
+ consola.success(`Switched default session to ${sessionId}`);
20913
+ } catch (err) {
20914
+ if (isTransportFailure(err))
20915
+ throw err;
20916
+ reportCommandError(err, { json: args.json });
20917
+ }
20387
20918
  }
20388
20919
  });
20389
20920
  var sessionsCommand = defineCommand({
@@ -20458,6 +20989,8 @@ var addSkillCommand = defineCommand({
20458
20989
  }
20459
20990
  consola.success(`Installed ${stripControlChars(skill.command)} (${stripControlChars(skill.name)}).`);
20460
20991
  } catch (err) {
20992
+ if (isTransportFailure(err))
20993
+ throw err;
20461
20994
  reportCommandError(err, { json: args.json });
20462
20995
  }
20463
20996
  }
@@ -20509,6 +21042,8 @@ var rmSkillCommand = defineCommand({
20509
21042
  }
20510
21043
  consola.success(`Skill "${args["skill-command"]}" removed from your library.`);
20511
21044
  } catch (err) {
21045
+ if (isTransportFailure(err))
21046
+ throw err;
20512
21047
  reportCommandError(err, { json: args.json });
20513
21048
  }
20514
21049
  }
@@ -20540,6 +21075,8 @@ function makeSetEnabledCommand(name, enabled, description) {
20540
21075
  }
20541
21076
  consola.success(`${enabled ? "Enabled" : "Disabled"} ${stripControlChars(skill.command)}.`);
20542
21077
  } catch (err) {
21078
+ if (isTransportFailure(err))
21079
+ throw err;
20543
21080
  reportCommandError(err, { json: args.json });
20544
21081
  }
20545
21082
  }
@@ -20632,13 +21169,14 @@ var skillsCommand = defineCommand({
20632
21169
  ].join(`
20633
21170
  `));
20634
21171
  } catch (err) {
21172
+ if (isTransportFailure(err))
21173
+ throw err;
20635
21174
  reportCommandError(err, { json: args.json });
20636
21175
  }
20637
21176
  }
20638
21177
  });
20639
21178
 
20640
21179
  // src/commands/whoami.ts
20641
- var import_picocolors3 = __toESM(require_picocolors(), 1);
20642
21180
  function maskToken(token) {
20643
21181
  return `${token.slice(0, 10)}...${token.slice(-6)}`;
20644
21182
  }
@@ -20648,7 +21186,16 @@ var whoamiCommand = defineCommand({
20648
21186
  description: "Display current authentication status"
20649
21187
  },
20650
21188
  run: async () => {
20651
- const auth = readAuthFile();
21189
+ let auth;
21190
+ try {
21191
+ auth = readAuthFile();
21192
+ } catch (err) {
21193
+ consola.error(err instanceof Error ? err.message : String(err));
21194
+ consola.error(`
21195
+ → ${NEXT_STEP_GUIDANCE.relogin}`);
21196
+ process.exit(1);
21197
+ return;
21198
+ }
20652
21199
  if (!auth) {
20653
21200
  consola.error("Not logged in. Run `msc login` to authenticate - it opens a browser, so a human must complete it.");
20654
21201
  process.exit(1);
@@ -20670,13 +21217,17 @@ var whoamiCommand = defineCommand({
20670
21217
  const updatedUid = data.uid ?? auth.uid;
20671
21218
  const updatedEmail = data.email ?? auth.email;
20672
21219
  if (updatedName !== auth.username || updatedUid !== auth.uid || updatedEmail !== auth.email) {
20673
- saveAuthFile({
20674
- ...auth,
20675
- token: client.apiKey,
20676
- username: updatedName,
20677
- uid: updatedUid,
20678
- email: updatedEmail
20679
- });
21220
+ try {
21221
+ saveAuthFile({
21222
+ ...auth,
21223
+ token: client.apiKey,
21224
+ username: updatedName,
21225
+ uid: updatedUid,
21226
+ email: updatedEmail
21227
+ });
21228
+ } catch (err) {
21229
+ consola.warn(err instanceof Error ? err.message : String(err));
21230
+ }
20680
21231
  }
20681
21232
  consola.success("Authenticated");
20682
21233
  const lines = [
@@ -20684,27 +21235,16 @@ var whoamiCommand = defineCommand({
20684
21235
  updatedUid ? ` UID: ${updatedUid}` : null,
20685
21236
  updatedEmail ? ` Email: ${updatedEmail}` : null,
20686
21237
  data.apiKeyMasked ? ` API Key: ${data.apiKeyMasked}` : null,
20687
- ` Token: ${maskToken(auth.token)}`,
20688
- auth.updated_at ? import_picocolors3.gray(` Saved: ${auth.updated_at}`) : null
21238
+ ` Token: ${maskToken(client.apiKey)}`
20689
21239
  ].filter(Boolean);
20690
21240
  consola.log(`${lines.join(`
20691
21241
  `)}
20692
21242
  `);
20693
- return;
20694
21243
  } catch (err) {
20695
- const isNetworkError = err instanceof TypeError && /fetch failed|failed to fetch/i.test(err.message) || isObject2(err) && "code" in err && isString(err.code) && (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND" || err.code === "ETIMEDOUT" || err.code === "EHOSTUNREACH");
20696
- if (!isNetworkError) {
20697
- reportCommandError(err);
20698
- return;
20699
- }
21244
+ if (isTransportFailure(err))
21245
+ throw err;
21246
+ reportCommandError(err);
20700
21247
  }
20701
- consola.info("Authentication Status (Cached)");
20702
- consola.log(` Token: ${maskToken(auth.token)}
20703
- ` + (auth.username ? ` User: ${auth.username}
20704
- ` : "") + (auth.uid ? ` UID: ${auth.uid}
20705
- ` : "") + (auth.email ? ` Email: ${auth.email}
20706
- ` : "") + (auth.updated_at ? import_picocolors3.gray(` Saved: ${auth.updated_at}
20707
- `) : ""));
20708
21248
  }
20709
21249
  });
20710
21250
 
@@ -20763,6 +21303,8 @@ AGENT QUICKSTART
20763
21303
  1. msc login # a human must finish this in a browser; instant if
20764
21304
  # that browser is already signed in to the web app
20765
21305
  2. msc chat "a red car" --json # one turn; the conversation continues across runs
21306
+ # (--new starts a fresh session, --session <id> picks
21307
+ # an existing one; both are verified)
20766
21308
  3. msc skills --plaza --json # find a skill, then reference it by leading the
20767
21309
  # prompt with /name or %%/name%%
20768
21310
 
@@ -20774,6 +21316,10 @@ AGENT QUICKSTART
20774
21316
  Failure exits 1 with a JSON error carrying message, errCode, httpStatus and
20775
21317
  nextStep, which is the action to take:
20776
21318
  relogin | retry_later | top_up_balance | fix_input | retry | none.
21319
+ A rejected saved credential (auth.json's apiUrl is off the allowlist) reports
21320
+ no errCode/httpStatus - nothing came from the platform - and nextStep
21321
+ "relogin", since \`msc login\` rewrites the file. No credential at all reports
21322
+ no nextStep, so the two states differ without parsing the message.
20777
21323
  A turn reports status "success", or "incomplete" if the stream ended early.
20778
21324
  `;
20779
21325
  await runMain({