@moda-ai/cli 1.27.1 → 1.28.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.
@@ -546,14 +546,25 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
546
546
  const profileOptions = legacyProfileOptions ?? { profile: context.profile, env: context.env };
547
547
  const subcommand = positionals[0] || "status";
548
548
  switch (subcommand) {
549
- case "init":
550
- await initPrompts({ silent: true });
549
+ case "init": {
550
+ const init = await initPrompts({ silent: true });
551
551
  context.output.writeData({
552
552
  manifest: MANIFEST_PATH,
553
+ manifestPath: init.manifestPath,
553
554
  lockfile: LOCK_PATH,
554
- promptPaths: readManifest().promptPaths
555
+ lockPath: init.lockPath,
556
+ cwd: init.cwd,
557
+ created: init.created,
558
+ promptPaths: init.promptPaths
559
+ }, {
560
+ artifacts: [{
561
+ kind: "prompt_manifest",
562
+ path: init.manifestPath,
563
+ description: init.created ? "Prompt discovery manifest created by this run." : "Prompt discovery manifest already existed; left unchanged."
564
+ }]
555
565
  });
556
566
  break;
567
+ }
557
568
  case "status":
558
569
  context.output.writeData(await getPromptStatus());
559
570
  break;
@@ -577,8 +588,12 @@ async function runPromptsCommand(input, legacyFlags, legacyProfileOptions) {
577
588
  }
578
589
  }
579
590
  async function initPrompts(options = {}) {
591
+ const cwd = process.cwd();
592
+ const manifestPath = resolve3(cwd, MANIFEST_PATH);
593
+ const lockPath = resolve3(cwd, LOCK_PATH);
580
594
  mkdirSync(MODA_DIR, { recursive: true });
581
- if (!existsSync2(MANIFEST_PATH)) {
595
+ const existed = existsSync2(MANIFEST_PATH);
596
+ if (!existed) {
582
597
  writeFileSync2(MANIFEST_PATH, [
583
598
  "version: 1",
584
599
  "prompt_paths:",
@@ -587,13 +602,28 @@ async function initPrompts(options = {}) {
587
602
  ].join(`
588
603
  `));
589
604
  }
605
+ if (!existsSync2(manifestPath)) {
606
+ throw new Error(`Could not write ${manifestPath}: the file is missing after init. Check directory permissions and available disk space.`);
607
+ }
608
+ const result = {
609
+ manifestPath,
610
+ lockPath,
611
+ cwd,
612
+ created: !existed,
613
+ promptPaths: readManifest().promptPaths
614
+ };
590
615
  if (!options.silent) {
591
616
  printJson({
592
617
  manifest: MANIFEST_PATH,
618
+ manifestPath,
593
619
  lockfile: LOCK_PATH,
594
- promptPaths: readManifest().promptPaths
620
+ lockPath,
621
+ cwd,
622
+ created: result.created,
623
+ promptPaths: result.promptPaths
595
624
  });
596
625
  }
626
+ return result;
597
627
  }
598
628
  async function syncPrompts(flags, profileOptions, context) {
599
629
  if (flags.watch === "true") {
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  runPromptsCommand,
9
9
  runSkillsCommand,
10
10
  runStatusCommand
11
- } from "./cli-cjqzrp9k.js";
11
+ } from "./cli-x1a56ws9.js";
12
12
  import {
13
13
  ApiError,
14
14
  HARNESS_REPORT_APPROVAL_PATH,
@@ -60,6 +60,7 @@ import {
60
60
  clearStoredApiKeyCredentials,
61
61
  createCliOutput,
62
62
  createCommandContext,
63
+ embeddedTenantIdFromApiKey,
63
64
  ensureSecretFilePermissions,
64
65
  getActiveProfile,
65
66
  getModaDir,
@@ -76,6 +77,7 @@ import {
76
77
  resolveTenantId,
77
78
  safeWriteStderr,
78
79
  safeWriteStdout,
80
+ saveCliConfig,
79
81
  secretFilePathFromRef,
80
82
  setActiveProfile,
81
83
  upsertProfile,
@@ -2028,6 +2030,47 @@ function renderJsonSchemas() {
2028
2030
  }, null, 2);
2029
2031
  }
2030
2032
 
2033
+ // src/api-key-auth.ts
2034
+ var PROBE_TIMEOUT_MS = 1e4;
2035
+ function resolveApiKeyCredential(options = {}) {
2036
+ const key = resolveApiKey(options);
2037
+ if (!key)
2038
+ return;
2039
+ const env = options.env ?? process.env;
2040
+ const source = env.MODA_API_KEY === key ? "env" : resolveApiKeyFromActiveProfile(options) === key ? "profile" : "config";
2041
+ return { key, source, tenantId: embeddedTenantIdFromApiKey(key) };
2042
+ }
2043
+ function redactApiKey(key) {
2044
+ if (key.length <= 12)
2045
+ return "<redacted>";
2046
+ return `${key.slice(0, 8)}...${key.slice(-4)}`;
2047
+ }
2048
+ async function probeApiKey(key, options = {}) {
2049
+ let response;
2050
+ try {
2051
+ response = await fetch(`${resolveModaBaseUrl(options)}/api/v1/data/overview`, {
2052
+ method: "GET",
2053
+ headers: { "x-api-key": key },
2054
+ signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
2055
+ });
2056
+ } catch (error) {
2057
+ const reason = error instanceof Error ? error.message : String(error);
2058
+ return { outcome: "unreachable", reason };
2059
+ }
2060
+ if (response.status === 401 || response.status === 403) {
2061
+ return { outcome: "rejected", status: response.status };
2062
+ }
2063
+ if (response.status !== 200) {
2064
+ return { outcome: "unreachable", reason: `HTTP ${response.status}` };
2065
+ }
2066
+ try {
2067
+ await response.json();
2068
+ } catch {
2069
+ return { outcome: "unreachable", reason: "non-JSON response from the Data API" };
2070
+ }
2071
+ return { outcome: "live" };
2072
+ }
2073
+
2031
2074
  // src/keys.ts
2032
2075
  import { createHash } from "node:crypto";
2033
2076
  import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
@@ -2212,13 +2255,75 @@ function serializeProfile(profile, activeProfile) {
2212
2255
  auth_token_ref: profile.auth_token_ref
2213
2256
  };
2214
2257
  }
2258
+ async function readStdin() {
2259
+ const chunks = [];
2260
+ for await (const chunk of process.stdin)
2261
+ chunks.push(Buffer.from(chunk));
2262
+ return Buffer.concat(chunks).toString("utf8").trim();
2263
+ }
2264
+ async function apiKeyLoginCandidate(context) {
2265
+ if (boolFlag2(context, "api-key-stdin")) {
2266
+ const piped = await readStdin();
2267
+ if (piped)
2268
+ return { key: piped, fromArgv: false };
2269
+ throw new CliInputError("No API key received on stdin.", 'Pipe the key in, e.g. `printf %s "$KEY" | moda auth login --api-key-stdin`.');
2270
+ }
2271
+ const inline = flag(context, "api-key");
2272
+ if (inline)
2273
+ return { key: inline, fromArgv: true };
2274
+ const env = context.env ?? process.env;
2275
+ if (env.MODA_API_KEY)
2276
+ return { key: env.MODA_API_KEY, fromArgv: false };
2277
+ return;
2278
+ }
2279
+ async function loginWithApiKey(context, candidate) {
2280
+ const { key } = candidate;
2281
+ if (candidate.fromArgv) {
2282
+ console.error('Warning: passing the key as --api-key=<key> exposes it in this process\'s command line and your shell history. Prefer MODA_API_KEY, or `printf %s "$KEY" | moda auth login --api-key-stdin`.');
2283
+ }
2284
+ const authOptions = { profile: context.profile, env: context.env };
2285
+ const probe = await probeApiKey(key, authOptions);
2286
+ if (probe.outcome === "rejected") {
2287
+ throw new CliInputError(`API key rejected by the Moda API (HTTP ${probe.status}).`, 'The key is revoked, malformed, or belongs to another environment. Mint a new one with "moda provision --tenant-id=<org-id>", or check MODA_BASE_URL points at the right environment.');
2288
+ }
2289
+ if (probe.outcome === "unreachable") {
2290
+ throw new CliInputError(`Could not validate the API key: ${probe.reason}.`, "The key was not saved. Check network access to the Moda API and retry; the key itself may still be valid.");
2291
+ }
2292
+ const tenantId = embeddedTenantIdFromApiKey(key);
2293
+ saveCliConfig({ api_key: key, ...tenantId ? { tenant_id: tenantId } : {} }, authOptions);
2294
+ const envKey = (context.env ?? process.env).MODA_API_KEY;
2295
+ const shadowedByEnv = Boolean(envKey && envKey !== key);
2296
+ if (shadowedByEnv) {
2297
+ console.error(`Warning: MODA_API_KEY is set to a different key (${redactApiKey(envKey)}) and takes precedence over the key just saved. Later commands will use the environment key. Unset MODA_API_KEY to use the saved credential.`);
2298
+ }
2299
+ context.output.writeData({
2300
+ status: "authenticated",
2301
+ method: "api_key",
2302
+ profile: context.profile ?? context.env?.MODA_PROFILE ?? process.env.MODA_PROFILE ?? null,
2303
+ apiKeyCredential: { masked: redactApiKey(key) },
2304
+ tenant_id: tenantId ?? null,
2305
+ persisted: true,
2306
+ shadowed_by_env: shadowedByEnv,
2307
+ effective_credential: shadowedByEnv ? "env" : "saved"
2308
+ }, shadowedByEnv ? { status: "degraded" } : undefined);
2309
+ return { exitCode: shadowedByEnv ? EXIT_DEGRADED : EXIT_OK };
2310
+ }
2215
2311
  async function runAuthCommand(context) {
2216
2312
  switch (subcommand(context)) {
2217
2313
  case "login": {
2314
+ const wantsApiKey = "api-key" in context.flags || boolFlag2(context, "api-key-stdin");
2315
+ const nonInteractive = boolFlag2(context, "non-interactive");
2316
+ const candidate = wantsApiKey || nonInteractive ? await apiKeyLoginCandidate(context) : undefined;
2317
+ if (candidate)
2318
+ return loginWithApiKey(context, candidate);
2319
+ if (wantsApiKey || nonInteractive) {
2320
+ throw new CliInputError("No API key available for non-interactive login.", 'Set MODA_API_KEY, pipe the key with "moda auth login --api-key-stdin", or pass "moda auth login --api-key=<key>" (exposes it in argv). Mint a key with "moda provision --tenant-id=<org-id>" (needs an existing browser session), or run "moda init".');
2321
+ }
2218
2322
  const screen = flag(context, "screen") === "signup" ? "signup" : "login";
2219
2323
  await login(screen, { profile: context.profile, env: context.env });
2220
2324
  context.output.writeData({
2221
2325
  status: "authenticated",
2326
+ method: "browser",
2222
2327
  profile: context.profile ?? process.env.MODA_PROFILE ?? null
2223
2328
  });
2224
2329
  return { exitCode: 0 };
@@ -2243,7 +2348,33 @@ async function runAuthCommand(context) {
2243
2348
  case "whoami": {
2244
2349
  const session = loadAuthSession({ profile: context.profile, env: context.env });
2245
2350
  if (!session) {
2246
- throw new CliInputError("Not authenticated.", 'Run "moda auth login" or "moda init".');
2351
+ const credential = resolveApiKeyCredential({
2352
+ profile: context.profile,
2353
+ env: context.env
2354
+ });
2355
+ if (credential) {
2356
+ const probe = await probeApiKey(credential.key, {
2357
+ profile: context.profile,
2358
+ env: context.env
2359
+ });
2360
+ if (probe.outcome === "rejected") {
2361
+ throw new CliInputError(`API key rejected by the Moda API (HTTP ${probe.status}).`, 'The key is revoked or belongs to another environment. Mint a new one with "moda provision --tenant-id=<org-id>", or run "moda auth login".');
2362
+ }
2363
+ context.output.writeData({
2364
+ profile: context.profile ?? process.env.MODA_PROFILE ?? null,
2365
+ credential: "api_key",
2366
+ apiKeyCredential: {
2367
+ source: credential.source,
2368
+ masked: redactApiKey(credential.key),
2369
+ verified: probe.outcome === "live"
2370
+ },
2371
+ tenant_id: credential.tenantId ?? null,
2372
+ session: null,
2373
+ context: null
2374
+ }, probe.outcome === "live" ? undefined : { status: "degraded" });
2375
+ return { exitCode: probe.outcome === "live" ? EXIT_OK : EXIT_DEGRADED };
2376
+ }
2377
+ throw new CliInputError("Not authenticated.", 'Non-interactive: set MODA_API_KEY, or run "moda auth login --api-key=<key>". Interactive: run "moda auth login" or "moda init".');
2247
2378
  }
2248
2379
  const response = await authFetch("/api/cli/context", {}, { profile: context.profile, env: context.env });
2249
2380
  if (!response.ok) {
@@ -2265,8 +2396,17 @@ async function runAuthCommand(context) {
2265
2396
  const authOptions = { profile: context.profile, env: context.env };
2266
2397
  const session = loadAuthSession({ ...authOptions, includeExpired: true });
2267
2398
  const loggedIn = isAuthSessionValid(session);
2399
+ const credential = resolveApiKeyCredential(authOptions);
2268
2400
  const data = {
2269
2401
  loggedIn,
2402
+ authenticated: loggedIn || Boolean(credential),
2403
+ apiKeyCredential: credential ? {
2404
+ present: true,
2405
+ source: credential.source,
2406
+ masked: redactApiKey(credential.key),
2407
+ tenantId: credential.tenantId ?? null,
2408
+ verified: null
2409
+ } : { present: false },
2270
2410
  profile: context.profile ?? context.env.MODA_PROFILE ?? session?.profile ?? null,
2271
2411
  expiresAt: session ? new Date(session.expires_at * 1000).toISOString() : null
2272
2412
  };
@@ -2286,9 +2426,16 @@ async function runAuthCommand(context) {
2286
2426
  }
2287
2427
  } catch {}
2288
2428
  data.online = online;
2429
+ if (credential && data.apiKeyCredential.present) {
2430
+ const probe = await probeApiKey(credential.key, authOptions);
2431
+ data.apiKeyCredential.verified = probe.outcome === "live";
2432
+ if (probe.outcome === "rejected") {
2433
+ data.authenticated = loggedIn;
2434
+ }
2435
+ }
2289
2436
  }
2290
2437
  context.output.writeData(data);
2291
- return { exitCode: loggedIn ? EXIT_OK : EXIT_AUTH_REQUIRED };
2438
+ return { exitCode: data.authenticated ? EXIT_OK : EXIT_AUTH_REQUIRED };
2292
2439
  }
2293
2440
  case "token": {
2294
2441
  const session = loadAuthSession({ profile: context.profile, env: context.env });
@@ -4794,7 +4941,7 @@ function buildResumeCommand(command, flags) {
4794
4941
  ];
4795
4942
  }
4796
4943
  function printHelp() {
4797
- console.error(`Usage: moda <command> [options]
4944
+ console.log(`Usage: moda <command> [options]
4798
4945
 
4799
4946
  Commands:
4800
4947
  init Set up Moda in your project
@@ -4802,11 +4949,12 @@ Commands:
4802
4949
  status Show concise local Moda setup status
4803
4950
  manifest Describe the CLI machine protocol and commands
4804
4951
  doctor Diagnose local Moda setup and data flow
4805
- auth login Authenticate with Moda
4806
- auth status Show local auth session state (exit 4 when logged out); --online probes the API
4952
+ auth login Authenticate with Moda (browser)
4953
+ auth login --api-key Non-interactive: validate MODA_API_KEY (or --api-key=<key>) and save it
4954
+ auth status Show auth state for both credentials (exit 4 when neither is usable); --online probes the API
4807
4955
  auth token Print the raw bearer token for $(moda auth token)
4808
4956
  auth logout Clear cached Moda auth
4809
- auth whoami Show the authenticated Moda user
4957
+ auth whoami Show the authenticated Moda identity (session or API key)
4810
4958
  profiles list List local Moda profiles
4811
4959
  profiles use <name> Switch active Moda profile
4812
4960
  profiles create <name> Create or update a local Moda profile
@@ -5025,6 +5173,8 @@ Provision flags (headless; requires an existing "moda auth login" session):
5025
5173
 
5026
5174
  Examples:
5027
5175
  moda init
5176
+ moda auth login --api-key=$MODA_API_KEY
5177
+ moda auth whoami
5028
5178
  moda provision --tenant-id=<org-id>
5029
5179
  moda status
5030
5180
  moda manifest --json
@@ -5089,7 +5239,11 @@ Examples:
5089
5239
  moda fixes
5090
5240
  moda fix start <problem_id> --wait
5091
5241
  moda fix verify <fix_id> --prompt-file=prompts/agent.prompt.md
5092
- moda fix mark-applied <fix_id> --note="tool description updated in our agent config"`);
5242
+ moda fix mark-applied <fix_id> --note="tool description updated in our agent config"
5243
+
5244
+ Run \`moda <command> --help\` for a command's subcommands, and
5245
+ \`moda <command> <subcommand> --help\` for that subcommand's flags
5246
+ (e.g. \`moda auth login --help\`).`);
5093
5247
  }
5094
5248
  function printCommandHelp(definition) {
5095
5249
  const lines = [];
@@ -5100,6 +5254,14 @@ function printCommandHelp(definition) {
5100
5254
  if (definition.mutability === "write") {
5101
5255
  lines.push("This command can mutate state.");
5102
5256
  }
5257
+ if (definition.subcommands && definition.subcommands.length > 0) {
5258
+ lines.push("", "Subcommands:");
5259
+ const width = Math.max(...definition.subcommands.map((entry) => entry.name.length));
5260
+ for (const entry of definition.subcommands) {
5261
+ lines.push(` ${entry.name.padEnd(width)} ${entry.description}`);
5262
+ }
5263
+ lines.push("", `Run 'moda ${definition.name} <subcommand> --help' for that subcommand's flags.`);
5264
+ }
5103
5265
  if (definition.examples.length > 0) {
5104
5266
  lines.push("", "Examples:");
5105
5267
  for (const example of definition.examples) {
@@ -5107,7 +5269,28 @@ function printCommandHelp(definition) {
5107
5269
  }
5108
5270
  }
5109
5271
  lines.push("", "Run 'moda --help' for the full command list and common flags", "(--json, --agent, --profile, --days-back, --limit, --time-range, ...).");
5110
- console.error(lines.join(`
5272
+ console.log(lines.join(`
5273
+ `));
5274
+ }
5275
+ function printSubcommandHelp(parent, entry) {
5276
+ const lines = [];
5277
+ lines.push(`moda ${parent.name} ${entry.name} — ${entry.description}`, "");
5278
+ lines.push(`Usage: ${entry.usage}`);
5279
+ if (entry.flags && entry.flags.length > 0) {
5280
+ lines.push("", "Options:");
5281
+ const width = Math.max(...entry.flags.map((flag2) => flag2.name.length));
5282
+ for (const flag2 of entry.flags) {
5283
+ lines.push(` ${flag2.name.padEnd(width)} ${flag2.description}`);
5284
+ }
5285
+ }
5286
+ if (entry.examples && entry.examples.length > 0) {
5287
+ lines.push("", "Examples:");
5288
+ for (const example of entry.examples) {
5289
+ lines.push(` ${example}`);
5290
+ }
5291
+ }
5292
+ lines.push("", `Run 'moda ${parent.name} --help' for the other ${parent.name} subcommands.`);
5293
+ console.log(lines.join(`
5111
5294
  `));
5112
5295
  }
5113
5296
  async function attachWindows(rows, windowSize) {
@@ -5859,6 +6042,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
5859
6042
  }
5860
6043
  return 0;
5861
6044
  }
6045
+ var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff"]);
5862
6046
  function legacyCommand(metadata) {
5863
6047
  return {
5864
6048
  ...metadata,
@@ -5952,7 +6136,7 @@ var commandRegistry = createCommandRegistry([
5952
6136
  resetApiRequestCountBeforeRun: true,
5953
6137
  telemetry: "result",
5954
6138
  handler: async (context) => {
5955
- const { runInit } = await import("./index-fwxpt51s.js");
6139
+ const { runInit } = await import("./index-g3gt62rm.js");
5956
6140
  if (context.outputMode === "agent-stream") {
5957
6141
  context.output.writeEvent({
5958
6142
  event: "started",
@@ -6025,15 +6209,81 @@ var commandRegistry = createCommandRegistry([
6025
6209
  },
6026
6210
  {
6027
6211
  name: "auth",
6028
- description: "Authenticate and inspect browser auth sessions",
6212
+ description: "Authenticate (browser or API key) and inspect auth state",
6029
6213
  examples: [
6030
6214
  "moda auth login",
6215
+ "moda auth login --api-key=$MODA_API_KEY",
6031
6216
  "moda auth status",
6032
6217
  "moda auth status --online",
6033
6218
  "moda auth token",
6034
6219
  "moda auth whoami",
6035
6220
  "moda auth logout"
6036
6221
  ],
6222
+ subcommands: [
6223
+ {
6224
+ name: "login",
6225
+ description: "Authenticate with Moda (browser by default, or an API key)",
6226
+ usage: "moda auth login [--api-key[=<key>] | --api-key-stdin] [--non-interactive] [--screen=login|signup]",
6227
+ flags: [
6228
+ {
6229
+ name: "--api-key",
6230
+ description: "Authenticate non-interactively with the API key in MODA_API_KEY. Validates it, saves it, and never opens a browser."
6231
+ },
6232
+ {
6233
+ name: "--api-key-stdin",
6234
+ description: "Read the API key from stdin. Safest form: the key never appears in argv or shell history."
6235
+ },
6236
+ {
6237
+ name: "--api-key=<key>",
6238
+ description: "Supply the key inline. Exposes it in this process's command line and shell history; prefer MODA_API_KEY or --api-key-stdin."
6239
+ },
6240
+ {
6241
+ name: "--non-interactive",
6242
+ description: "Never open a browser. Uses MODA_API_KEY if set, otherwise fails with the commands that would mint a key."
6243
+ },
6244
+ {
6245
+ name: "--screen=login|signup",
6246
+ description: "Which browser screen to open (interactive flow only)."
6247
+ }
6248
+ ],
6249
+ examples: [
6250
+ "MODA_API_KEY=<key> moda auth login --non-interactive",
6251
+ "moda auth login --api-key",
6252
+ 'printf %s "$KEY" | moda auth login --api-key-stdin',
6253
+ "moda auth login"
6254
+ ]
6255
+ },
6256
+ {
6257
+ name: "status",
6258
+ description: "Show local auth state for both credentials; exit 4 when neither is usable",
6259
+ usage: "moda auth status [--online]",
6260
+ flags: [
6261
+ {
6262
+ name: "--online",
6263
+ description: "Also probe the API to confirm the browser session is accepted."
6264
+ }
6265
+ ],
6266
+ examples: ["moda auth status", "moda auth status --online"]
6267
+ },
6268
+ {
6269
+ name: "whoami",
6270
+ description: "Show the authenticated identity (browser session, or API-key credential)",
6271
+ usage: "moda auth whoami",
6272
+ examples: ["moda auth whoami"]
6273
+ },
6274
+ {
6275
+ name: "token",
6276
+ description: "Print the raw browser session bearer token for $(moda auth token)",
6277
+ usage: "moda auth token",
6278
+ examples: ["moda auth token", 'curl -H "Authorization: Bearer $(moda auth token)" ...']
6279
+ },
6280
+ {
6281
+ name: "logout",
6282
+ description: "Revoke and clear the browser session and any stored API key",
6283
+ usage: "moda auth logout",
6284
+ examples: ["moda auth logout"]
6285
+ }
6286
+ ],
6037
6287
  auth: "none",
6038
6288
  outputModes: ["human", "json", "agent"],
6039
6289
  mutability: "write",
@@ -6336,11 +6586,75 @@ var commandRegistry = createCommandRegistry([
6336
6586
  legacyCommand({
6337
6587
  name: "prompts",
6338
6588
  description: "Manage prompt versions and labels",
6339
- examples: ["moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
6589
+ examples: ["moda prompts init", "moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
6590
+ subcommands: [
6591
+ {
6592
+ name: "init",
6593
+ description: "Create .moda/prompts.yml in the current directory (never overwrites)",
6594
+ usage: "moda prompts init",
6595
+ flags: [],
6596
+ examples: ["moda prompts init", "cd /path/to/project && moda prompts init"]
6597
+ },
6598
+ {
6599
+ name: "status",
6600
+ description: "Show prompt files that changed since the last sync",
6601
+ usage: "moda prompts status",
6602
+ examples: ["moda prompts status"]
6603
+ },
6604
+ {
6605
+ name: "diff",
6606
+ description: "Alias of status: show local prompt changes",
6607
+ usage: "moda prompts diff",
6608
+ examples: ["moda prompts diff"]
6609
+ },
6610
+ {
6611
+ name: "sync",
6612
+ description: "Upload changed prompt versions to Moda Cloud",
6613
+ usage: "moda prompts sync [--watch] [--dry-run]",
6614
+ flags: [
6615
+ { name: "--watch", description: "Re-upload whenever a prompt file changes." },
6616
+ { name: "--dry-run", description: "Show what would be uploaded without writing." }
6617
+ ],
6618
+ examples: ["moda prompts sync", "moda prompts sync --dry-run"]
6619
+ },
6620
+ {
6621
+ name: "promote",
6622
+ description: "Move a dev/staging/prod label to a prompt version",
6623
+ usage: "moda prompts promote <key> --label=<dev|staging|prod> --version=<version_id>",
6624
+ flags: [
6625
+ { name: "--label=<label>", description: "Release label to move (dev, staging, prod)." },
6626
+ { name: "--version=<id>", description: "Prompt version id to point the label at." }
6627
+ ],
6628
+ examples: ["moda prompts promote support.triage --label=prod --version=pver_abc123"]
6629
+ },
6630
+ {
6631
+ name: "ab",
6632
+ description: "Run a judged prompt A/B replay experiment",
6633
+ usage: "moda prompts ab --baseline=<path> --candidate=<path> --conversations=<ids>",
6634
+ flags: [
6635
+ { name: "--baseline=<path>", description: "Prompt file to treat as the control." },
6636
+ { name: "--candidate=<path>", description: "Prompt file to treat as the variant." },
6637
+ { name: "--conversations=<ids>", description: "Comma-separated conversation ids to replay." }
6638
+ ],
6639
+ examples: [
6640
+ "moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --conversations=conv_1,conv_2"
6641
+ ]
6642
+ },
6643
+ {
6644
+ name: "propose",
6645
+ description: "Revise a prompt from a run's failures",
6646
+ usage: "moda prompts propose [--gate] [--promote-on-win]",
6647
+ flags: [
6648
+ { name: "--gate", description: "Automatically A/B the proposed revision." },
6649
+ { name: "--promote-on-win", description: "Promote the candidate if it wins the gate." }
6650
+ ],
6651
+ examples: ["moda prompts propose --gate --promote-on-win"]
6652
+ }
6653
+ ],
6340
6654
  auth: "api-key",
6341
6655
  outputModes: ["human", "json", "agent"],
6342
6656
  mutability: "write",
6343
- validateConfigBeforeRun: true,
6657
+ validateConfigBeforeRun: (parsed) => !OFFLINE_PROMPTS_SUBCOMMANDS.has(parsed.positional ?? "status"),
6344
6658
  resetApiRequestCountBeforeRun: true,
6345
6659
  telemetry: "result-and-error"
6346
6660
  }),
@@ -6474,7 +6788,13 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
6474
6788
  if (command === "--help" || command === "-h" || flags.help === "true") {
6475
6789
  const helpTarget = command && command !== "--help" && command !== "-h" ? commandRegistry.find(command) : undefined;
6476
6790
  if (helpTarget) {
6477
- printCommandHelp(helpTarget);
6791
+ const subcommandName = parsed.positional;
6792
+ const subcommandEntry = subcommandName ? helpTarget.subcommands?.find((entry) => entry.name === subcommandName) : undefined;
6793
+ if (subcommandEntry) {
6794
+ printSubcommandHelp(helpTarget, subcommandEntry);
6795
+ } else {
6796
+ printCommandHelp(helpTarget);
6797
+ }
6478
6798
  } else {
6479
6799
  printHelp();
6480
6800
  }
@@ -6519,7 +6839,8 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
6519
6839
  })
6520
6840
  });
6521
6841
  setActiveContext(context);
6522
- if (definition.validateConfigBeforeRun) {
6842
+ const needsConfig = typeof definition.validateConfigBeforeRun === "function" ? definition.validateConfigBeforeRun(parsed) : definition.validateConfigBeforeRun;
6843
+ if (needsConfig) {
6523
6844
  try {
6524
6845
  validateConfig();
6525
6846
  } catch (error) {
@@ -9,7 +9,7 @@ import {
9
9
  runPromptSync,
10
10
  runSkillSync,
11
11
  upsertSkillManifestRecord
12
- } from "./cli-cjqzrp9k.js";
12
+ } from "./cli-x1a56ws9.js";
13
13
  import {
14
14
  codingAgentDisplayName,
15
15
  describeCodingAgentEvent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.27.1",
3
+ "version": "1.28.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schema_version": "moda.skill_index.v1",
3
- "bundled_at": "2026-08-19T08:07:44.284Z",
4
- "cli_version": "1.27.1",
3
+ "bundled_at": "2026-08-19T21:29:46.133Z",
4
+ "cli_version": "1.28.0",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",
@@ -55,9 +55,9 @@ Optional:
55
55
 
56
56
  - `MODA_BASE_URL` defaults to `https://moda.dev`; override only for
57
57
  self-hosted or staging.
58
- - `MODA_SKILL_VERSION` — export to `2.3.0` so search-adoption telemetry can
58
+ - `MODA_SKILL_VERSION` — export to `2.4.0` so search-adoption telemetry can
59
59
  attribute usage to this skill version. Set it once per session:
60
- `export MODA_SKILL_VERSION=2.3.0`.
60
+ `export MODA_SKILL_VERSION=2.4.0`.
61
61
 
62
62
  ## Setup
63
63
 
@@ -106,10 +106,12 @@ Output shape:
106
106
 
107
107
  Rules and limitations:
108
108
 
109
- - **Requires an existing CLI session.** There is no browserless signup or
110
- login: with no stored session it exits `4` (`auth_required`) telling you to
109
+ - **Requires an existing CLI session.** Minting a *new* key is control-plane
110
+ work: with no stored session it exits `4` (`auth_required`) telling you to
111
111
  run `moda auth login` on a machine with a browser first, or to skip
112
- provisioning and export `MODA_API_KEY` directly.
112
+ provisioning and export `MODA_API_KEY` directly. If you already hold a key,
113
+ you do not need this command at all — `moda auth login --api-key` gets you
114
+ to an authenticated state with no browser.
113
115
  - **Never prompts, even on a TTY.** A single-tenant account auto-selects;
114
116
  with multiple tenants you must pass `--tenant-id=<id>` (or set
115
117
  `MODA_TENANT_ID`), otherwise it exits `5` (`needs_input`) with per-tenant
@@ -134,13 +136,34 @@ just need credentials.
134
136
 
135
137
  ```bash
136
138
  moda auth login # browser PKCE sign-in; stores a moda_ct_ session token
139
+ moda auth login --api-key # NON-INTERACTIVE: validate MODA_API_KEY and save it
140
+ moda auth login --non-interactive # never open a browser; fail if no key is available
141
+ printf %s "$KEY" | moda auth login --api-key-stdin # key never touches argv
142
+ moda auth login --api-key=<key> # inline; WARNS, key is visible in argv + shell history
137
143
  moda auth logout # revoke the session server-side, then delete it locally
138
- moda auth status # local session state; exit 0 logged in, exit 4 logged out
144
+ moda auth status # both credentials; exit 0 if either is usable, else 4
139
145
  moda auth status --online # additionally probe GET /api/cli/context
146
+ moda auth whoami # identity from the session, or from the API key
140
147
  moda auth token # print the raw bearer token + newline, nothing else
141
148
  curl -H "Authorization: Bearer $(moda auth token)" https://moda.dev/api/cli/context
142
149
  ```
143
150
 
151
+ **If you are an agent with `MODA_API_KEY` and no browser, this is the whole
152
+ flow:**
153
+
154
+ ```bash
155
+ moda auth whoami # exit 0 + "verified": true means you are authenticated
156
+ ```
157
+
158
+ The API key alone is sufficient for every data command; `moda auth login` is
159
+ only needed for control-plane work (`moda provision`, `moda init`). There are
160
+ two independent credentials and `auth` reports both:
161
+
162
+ | Credential | Obtained by | Used for |
163
+ | --- | --- | --- |
164
+ | `moda_ct_` session token | `moda auth login` (browser PKCE) | control plane: `provision`, `init`, `auth token` |
165
+ | `moda_sk_` API key | `MODA_API_KEY`, `moda provision`, `moda init` | every data command |
166
+
144
167
  - `moda auth login` opens the browser consent flow at `{MODA_BASE_URL}/auth/cli`
145
168
  with a localhost callback (ports 8432-8435, first free). Approving in the
146
169
  browser hands the CLI a one-time code that it exchanges for a `moda_ct_...`
@@ -151,13 +174,45 @@ curl -H "Authorization: Bearer $(moda auth token)" https://moda.dev/api/cli/cont
151
174
  - `moda auth logout` revokes the stored `moda_ct_` session server-side
152
175
  (best-effort) and deletes the local credential; legacy sessions are just
153
176
  deleted locally.
177
+ - `moda auth login --api-key` authenticates **without a browser**. Prefer
178
+ `MODA_API_KEY` (bare `--api-key`) or `--api-key-stdin`; `--api-key=<key>`
179
+ works but puts the secret in argv and shell history, so it warns. The key is
180
+ validated against the Data API before anything is written, then saved to
181
+ `~/.moda/config.json`. A rejected key (401/403) or an unreachable API both
182
+ fail without persisting. `--non-interactive` is the same path plus a
183
+ guarantee that no browser opens even when no key is found — it exits with
184
+ the commands that would mint one.
185
+ - If `MODA_API_KEY` holds a **different** key than the one being saved, the
186
+ environment still wins afterwards. Login says so instead of reporting a
187
+ clean success: `"shadowed_by_env": true`, `"effective_credential": "env"`,
188
+ exit `3`. Unset `MODA_API_KEY` to use the saved credential.
189
+ - `moda auth whoami` reports the browser session when there is one. With no
190
+ session but a usable API key it reports
191
+ `{ "credential": "api_key", "apiKeyCredential": { "source": <"env"|"profile"|"config">,
192
+ "masked": <string>, "verified": <bool> }, "tenant_id": <string|null> }`.
193
+ `verified: true` means the key was accepted by the API just now. If the API
194
+ is unreachable the command exits `3` (degraded) with `verified: false`
195
+ rather than claiming an identity it could not confirm; a rejected key is an
196
+ error, not a "logged out" state.
154
197
  - `moda auth status` reports
155
- `{ "loggedIn": <bool>, "profile": <string|null>, "expiresAt": <ISO|null> }`
198
+ `{ "loggedIn": <bool>, "authenticated": <bool>, "apiKeyCredential": {...},
199
+ "profile": <string|null>, "expiresAt": <ISO|null> }`
156
200
  plus `"user": { "email": <string> }` when the stored session carries an
157
- email (PKCE `moda_ct_` sessions do; legacy sessions omit it), and exits `4`
158
- when logged out. That exit is an **expected state signal, not
159
- an error**: the payload is clean data (no error block, no stderr noise), and
160
- no browser is ever opened. With `--online` the payload adds
201
+ email (PKCE `moda_ct_` sessions do; legacy sessions omit it). `loggedIn` is
202
+ **browser-session only**; `authenticated` is true when *either* credential is
203
+ usable, and the exit code follows `authenticated` (`0` usable, `4` neither).
204
+ `apiKeyCredential` is `{ "present": false }` or
205
+ `{ "present": true, "source": <"env"|"profile"|"config">, "masked": <string>,
206
+ "tenantId": <string|null> }` — `source: "env"` tells you a `MODA_API_KEY` is
207
+ shadowing anything on disk. The raw key is never emitted. Exit `4` is an
208
+ **expected state signal, not an error**: the payload is clean data (no error
209
+ block, no stderr noise), and no browser is ever opened.
210
+ Without `--online` there is no network I/O, so `authenticated` means the
211
+ credential is **present**, not that it still works, and
212
+ `apiKeyCredential.verified` is `null`. With `--online` the key is probed too:
213
+ a rejected key gets `verified: false`, stops counting toward `authenticated`,
214
+ and exits `4`; an unreachable API leaves it counting, so a blip never kills a
215
+ good credential. With `--online` the payload adds
161
216
  `{ "online": { "reachable": <bool>, "ok": <bool>, "tenantCount": <n> } }`:
162
217
  `reachable: true` means any HTTP response arrived (even a 401/500), `ok`
163
218
  reflects HTTP success, and `tenantCount` is present only when the response
@@ -834,8 +889,10 @@ npx: `npx -p @moda-ai/cli moda <command>`.
834
889
  | `moda init` | Browser auth + provision API key + install agent rules |
835
890
  | `moda provision` | Headless tenant + API key provisioning; single JSON credentials doc on stdout (exit `4` without a session, `5` when `--tenant-id` is needed) |
836
891
  | `moda auth login` | Browser PKCE sign-in; stores a revocable `moda_ct_` session token |
892
+ | `moda auth login --api-key` | Non-interactive: validate `MODA_API_KEY` (or `--api-key=<key>`) and save it; never opens a browser |
893
+ | `moda auth whoami` | Authenticated identity from the session, or from the API key when there is no session |
837
894
  | `moda auth logout` | Revoke the CLI session server-side, then delete it locally |
838
- | `moda auth status` | Local auth session state (exit `4` = logged out — expected, not an error) |
895
+ | `moda auth status` | Both credentials; exit `0` if either is usable, `4` if neither (expected, not an error) |
839
896
  | `moda auth token` | Print the raw bearer token for `$(moda auth token)` composition |
840
897
  | `moda search "<query>"` | **Message-grain semantic/keyword/hybrid search (incl. tool calls); primary content-discovery path** |
841
898
  | `moda overview` | Dashboard KPIs, top clusters, recent activity |