@moda-ai/cli 1.13.0 → 1.13.2

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.
@@ -1,8 +1,12 @@
1
1
  // src/init/tenant.ts — Tenant selection and API key provisioning
2
+ import { randomUUID } from 'node:crypto';
3
+ import { existsSync, readFileSync, rmSync } from 'node:fs';
4
+ import { hostname } from 'node:os';
5
+ import { join } from 'node:path';
2
6
  import { select } from '@inquirer/prompts';
3
- import { authFetch } from '../auth.js';
7
+ import { authFetch, getCachedLoginContext } from '../auth.js';
4
8
  import { loadCliConfig, resolveIngestUrl, resolveModaBaseUrl, saveCliConfig, } from '../config.js';
5
- import { resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, } from '../profiles.js';
9
+ import { getModaDir, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, writeSecretJsonFile, } from '../profiles.js';
6
10
  import { CliAccessError, CliInputError, CliInputRequiredError } from '../errors.js';
7
11
  // Mirrors the backend's checkAdminAccess: only the viewer role is denied.
8
12
  const ADMIN_ACCESS_MESSAGE = 'You need admin or owner access to create API keys.';
@@ -12,6 +16,118 @@ const SIGNED_KEY_VERSION = 'v1';
12
16
  // Bound the pre-bootstrap liveness probe tightly: a slow or unreachable
13
17
  // endpoint must never block init — we just mint a fresh key instead.
14
18
  const VALIDATE_TIMEOUT_MS = 3_000;
19
+ /**
20
+ * Post-bootstrap validate-retry schedule (WP-2.3): key provisioning spans
21
+ * Postgres + Cloudflare KV, so a freshly minted key can lag a moment before
22
+ * it validates. Retry the liveness probe at these delays (3 attempts after
23
+ * the initial call, ~3.5s of retry sleeps plus up to VALIDATE_TIMEOUT_MS per
24
+ * probe against a hanging endpoint) before warning and moving on.
25
+ */
26
+ const POST_BOOTSTRAP_VALIDATE_DELAYS_MS = [
27
+ 500, 1_000, 2_000,
28
+ ];
29
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
30
+ // The backend truncates the bootstrap label to 60 printable chars; clip the
31
+ // hostname client-side too so what we send is what lands on the key name.
32
+ const MAX_LABEL_LENGTH = 60;
33
+ // Backend contract for the Idempotency-Key header (UUIDs match this too).
34
+ const IDEMPOTENCY_KEY_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
35
+ /**
36
+ * State file backing bootstrap idempotency across process restarts:
37
+ * ~/.moda/pending-bootstrap.json maps tenant id -> the Idempotency-Key of an
38
+ * in-flight (or failed) bootstrap POST. Written BEFORE the POST and cleared
39
+ * on success, so a retry after a network failure reuses the same key and the
40
+ * backend replays the original response instead of minting another ApiKey.
41
+ *
42
+ * Deliberately global (~/.moda, like config.json) and shared across
43
+ * profiles: the server scopes keys to the authenticated user, so the worst
44
+ * case of cross-profile sharing is one extra key minted — not worth
45
+ * profile-scoping the file.
46
+ */
47
+ function pendingBootstrapPath(options = {}) {
48
+ return join(getModaDir(options), 'pending-bootstrap.json');
49
+ }
50
+ function loadPendingBootstrapState(options = {}) {
51
+ try {
52
+ const path = pendingBootstrapPath(options);
53
+ if (!existsSync(path))
54
+ return {};
55
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
56
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
57
+ return {};
58
+ }
59
+ const state = {};
60
+ for (const [tenantId, entry] of Object.entries(parsed)) {
61
+ if (entry &&
62
+ typeof entry === 'object' &&
63
+ typeof entry.idempotency_key === 'string') {
64
+ state[tenantId] = entry;
65
+ }
66
+ }
67
+ return state;
68
+ }
69
+ catch {
70
+ // A corrupt state file must never block init; fresh keys are always safe.
71
+ return {};
72
+ }
73
+ }
74
+ /**
75
+ * Return the Idempotency-Key to use for this tenant's bootstrap POST,
76
+ * persisting it before the network call. A key left over from a previous
77
+ * failed attempt is reused so the retry replays instead of double-minting.
78
+ * File-system failures degrade to an in-memory key (init must never block).
79
+ */
80
+ function reserveBootstrapIdempotencyKey(tenantId, options = {}) {
81
+ const state = loadPendingBootstrapState(options);
82
+ const existing = state[tenantId]?.idempotency_key;
83
+ if (existing && IDEMPOTENCY_KEY_PATTERN.test(existing)) {
84
+ return existing;
85
+ }
86
+ const key = randomUUID();
87
+ state[tenantId] = {
88
+ idempotency_key: key,
89
+ created_at: new Date().toISOString(),
90
+ };
91
+ try {
92
+ writeSecretJsonFile(pendingBootstrapPath(options), state);
93
+ }
94
+ catch {
95
+ // Best-effort persistence: without it a retry mints a fresh key, which
96
+ // is the pre-idempotency behavior, not a failure.
97
+ }
98
+ return key;
99
+ }
100
+ /** Clear the tenant's pending key after a successful bootstrap. */
101
+ function clearBootstrapIdempotencyKey(tenantId, options = {}) {
102
+ try {
103
+ const state = loadPendingBootstrapState(options);
104
+ if (!(tenantId in state))
105
+ return;
106
+ delete state[tenantId];
107
+ const path = pendingBootstrapPath(options);
108
+ if (Object.keys(state).length === 0) {
109
+ rmSync(path, { force: true });
110
+ }
111
+ else {
112
+ writeSecretJsonFile(path, state);
113
+ }
114
+ }
115
+ catch {
116
+ // Leftover state is harmless: the key replays the stored response.
117
+ }
118
+ }
119
+ /** Label sent in the bootstrap body; the backend sanitizes it further. */
120
+ function bootstrapLabel(custom) {
121
+ const clipped = custom?.trim().slice(0, MAX_LABEL_LENGTH);
122
+ if (clipped)
123
+ return clipped;
124
+ try {
125
+ return hostname().slice(0, MAX_LABEL_LENGTH) || 'moda init';
126
+ }
127
+ catch {
128
+ return 'moda init';
129
+ }
130
+ }
15
131
  /**
16
132
  * Signed CLI keys embed their tenant: the format is
17
133
  * moda_sk_<base64url("v1:keyId:tenantId:timestamp:signature")> — see
@@ -87,6 +203,27 @@ function findStoredKeyForTenant(tenantId, options = {}) {
87
203
  }
88
204
  return undefined;
89
205
  }
206
+ /**
207
+ * Post-bootstrap verification (WP-2.3): confirm a freshly minted key is live
208
+ * before declaring success, reusing the same Data API probe as dedup.
209
+ * Provisioning spans Postgres + Cloudflare KV (eventual consistency), so
210
+ * failures are retried at the POST_BOOTSTRAP_VALIDATE_DELAYS_MS schedule.
211
+ * Exhaustion is a stderr warning, never an init failure: init's final doctor
212
+ * step surfaces persistent problems, and blocking here past the retry
213
+ * schedule would punish every user for a transient propagation delay.
214
+ */
215
+ async function verifyFreshKeyLive(key, probeOptions, overrides) {
216
+ const delays = overrides?.delaysMs ?? POST_BOOTSTRAP_VALIDATE_DELAYS_MS;
217
+ const sleep = overrides?.sleep ?? defaultSleep;
218
+ if (await isStoredKeyLive(key, probeOptions))
219
+ return;
220
+ for (const delayMs of delays) {
221
+ await sleep(delayMs);
222
+ if (await isStoredKeyLive(key, probeOptions))
223
+ return;
224
+ }
225
+ console.error(' Warning: API key created but not yet active; propagation can take a few seconds. moda doctor --online will confirm.');
226
+ }
90
227
  export async function selectTenantAndCreateKey(options = {}) {
91
228
  const log = (message) => {
92
229
  if (!options.silent)
@@ -99,13 +236,24 @@ export async function selectTenantAndCreateKey(options = {}) {
99
236
  profile: options.profile,
100
237
  env: options.env,
101
238
  };
102
- // 1. Fetch CLI context
103
- const contextRes = await authFetch('/api/cli/context', {}, authOptions);
104
- if (!contextRes.ok) {
105
- const body = await contextRes.text();
106
- throw new Error(`Failed to fetch context (${contextRes.status}): ${body}`);
239
+ // 1. Fetch CLI context. A PKCE login that just ran in this process already
240
+ // returned the user + tenants in the exchange response; reuse it and skip
241
+ // one /api/cli/context round-trip. The cache is profile-keyed (a login for
242
+ // a different profile returns null here), and the fetch stays as the
243
+ // fallback for cached-token runs (no login happened) and legacy logins.
244
+ let ctx;
245
+ const cachedContext = getCachedLoginContext(authOptions);
246
+ if (cachedContext) {
247
+ ctx = cachedContext;
248
+ }
249
+ else {
250
+ const contextRes = await authFetch('/api/cli/context', {}, authOptions);
251
+ if (!contextRes.ok) {
252
+ const body = await contextRes.text();
253
+ throw new Error(`Failed to fetch context (${contextRes.status}): ${body}`);
254
+ }
255
+ ctx = (await contextRes.json());
107
256
  }
108
- const ctx = (await contextRes.json());
109
257
  log(` Authenticated as ${ctx.user.email}`);
110
258
  // 2. Select tenant
111
259
  if (ctx.tenants.length === 0) {
@@ -161,11 +309,16 @@ export async function selectTenantAndCreateKey(options = {}) {
161
309
  // so re-running init does not accumulate one CLI key per run. A failed
162
310
  // decode, tenant mismatch, or probe failure falls through to minting.
163
311
  let result;
312
+ let reused = false;
164
313
  const storedKey = findStoredKeyForTenant(selected.id, authOptions);
165
314
  if (storedKey &&
166
315
  embeddedTenantId(storedKey) === selected.id &&
167
316
  (await isStoredKeyLive(storedKey, authOptions))) {
168
317
  log(` Using existing CLI key for ${selected.name} (created earlier)`);
318
+ // A pending idempotency key left by an earlier failed bootstrap is moot
319
+ // once the stored key is reused; clear it so it does not linger.
320
+ clearBootstrapIdempotencyKey(selected.id, authOptions);
321
+ reused = true;
169
322
  result = {
170
323
  apiKey: storedKey,
171
324
  tenantId: selected.id,
@@ -173,40 +326,79 @@ export async function selectTenantAndCreateKey(options = {}) {
173
326
  tenantSlug: selected.slug,
174
327
  };
175
328
  }
176
- // 4. Create API key via bootstrap endpoint
329
+ // 4. Create API key via bootstrap endpoint. The Idempotency-Key is
330
+ // persisted to ~/.moda/pending-bootstrap.json BEFORE the POST, so a retry
331
+ // after a network failure reuses the same key and the backend replays the
332
+ // original response instead of minting a second ApiKey.
177
333
  if (!result) {
334
+ const idempotencyKey = reserveBootstrapIdempotencyKey(selected.id, authOptions);
178
335
  const bootstrapRes = await authFetch('/api/cli/bootstrap', {
179
336
  method: 'POST',
180
- headers: { 'Content-Type': 'application/json' },
181
- body: JSON.stringify({ tenantId: selected.id }),
337
+ headers: {
338
+ 'Content-Type': 'application/json',
339
+ 'Idempotency-Key': idempotencyKey,
340
+ },
341
+ body: JSON.stringify({
342
+ tenantId: selected.id,
343
+ label: bootstrapLabel(options.label),
344
+ }),
182
345
  }, authOptions);
183
346
  if (bootstrapRes.status === 403) {
184
347
  throw new CliAccessError(ADMIN_ACCESS_MESSAGE, { guidance: ADMIN_ACCESS_GUIDANCE });
185
348
  }
349
+ if (bootstrapRes.status === 409) {
350
+ const body = await bootstrapRes.text();
351
+ let parsed;
352
+ try {
353
+ parsed = JSON.parse(body);
354
+ }
355
+ catch {
356
+ // Fall through to the generic error below.
357
+ }
358
+ if (parsed?.error === 'cli_key_cap_reached') {
359
+ throw new CliAccessError(parsed.message ||
360
+ 'This organization has reached its CLI API key limit.', {
361
+ // Command-agnostic: this path is shared by `moda init` and
362
+ // `moda provision`.
363
+ guidance: 'Revoke old CLI keys in the dashboard (Settings -> API Keys), then retry.',
364
+ });
365
+ }
366
+ throw new Error(`Failed to create API key (409): ${body}`);
367
+ }
186
368
  if (!bootstrapRes.ok) {
187
369
  const body = await bootstrapRes.text();
188
370
  throw new Error(`Failed to create API key (${bootstrapRes.status}): ${body}`);
189
371
  }
190
372
  result = (await bootstrapRes.json());
373
+ clearBootstrapIdempotencyKey(selected.id, authOptions);
374
+ // Fresh mints only: the reused-key path above already proved liveness
375
+ // via the dedup probe, so re-validating there would be a wasted call.
376
+ await verifyFreshKeyLive(result.apiKey, authOptions, options.validateRetry);
191
377
  }
192
378
  // Persist to ~/.moda/config.json (mode 0600) so the snapshot/session-end
193
379
  // hook can authenticate (api_key) and the local dev orchestrator can scope
194
380
  // ClickHouse (tenant_id) without the user exporting env vars into the shell
195
381
  // that launches Claude. Env still wins over the file at read time.
196
- saveCliConfig({
197
- api_key: result.apiKey,
198
- tenant_id: result.tenantId,
199
- ingest_url: resolveIngestUrl(authOptions),
200
- }, authOptions);
201
- // resolveTenantId() prefers MODA_TENANT_ID over the config file, so a stale
202
- // env var silently shadows the selection we just saved. Warn unless the env
203
- // var was itself the source of the selection.
204
- const envTenantId = (options.env ?? process.env).MODA_TENANT_ID;
205
- if (envTenantId && options.tenantIdSource !== 'env' && envTenantId !== result.tenantId) {
206
- console.error(` Warning: MODA_TENANT_ID is set to a different organization id and will shadow the saved selection (${result.tenantId}) for later commands. Unset it or set it to the selected organization id.`);
207
- }
208
- log(' API key + tenant saved to ~/.moda/config.json\n');
209
- log(' (Set MODA_API_KEY / MODA_TENANT_ID to override for scripts/CI.)\n');
210
- return result;
382
+ //
383
+ // Headless callers (persistConfig: false) skip this block entirely: they
384
+ // own their credential storage, and the env-shadow warning below only
385
+ // makes sense when a selection was actually saved.
386
+ if (options.persistConfig !== false) {
387
+ saveCliConfig({
388
+ api_key: result.apiKey,
389
+ tenant_id: result.tenantId,
390
+ ingest_url: resolveIngestUrl(authOptions),
391
+ }, authOptions);
392
+ // resolveTenantId() prefers MODA_TENANT_ID over the config file, so a stale
393
+ // env var silently shadows the selection we just saved. Warn unless the env
394
+ // var was itself the source of the selection.
395
+ const envTenantId = (options.env ?? process.env).MODA_TENANT_ID;
396
+ if (envTenantId && options.tenantIdSource !== 'env' && envTenantId !== result.tenantId) {
397
+ console.error(` Warning: MODA_TENANT_ID is set to a different organization id and will shadow the saved selection (${result.tenantId}) for later commands. Unset it or set it to the selected organization id.`);
398
+ }
399
+ log(' API key + tenant saved to ~/.moda/config.json\n');
400
+ log(' (Set MODA_API_KEY / MODA_TENANT_ID to override for scripts/CI.)\n');
401
+ }
402
+ return { ...result, reused };
211
403
  }
212
404
  //# sourceMappingURL=tenant.js.map
@@ -1,4 +1,4 @@
1
- import { authFetch, clearAuthSession, isAuthSessionValid, loadAuthSession, login } from './auth.js';
1
+ import { authFetch, clearAuthSession, isAuthSessionValid, loadAuthSession, login, revokeCliSessionToken, } from './auth.js';
2
2
  import { EXIT_AUTH_REQUIRED, EXIT_OK } from './output.js';
3
3
  import { KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS, createLocalKeyDescriptor, loadLocalKeyDescriptors, upsertLocalKeyDescriptor, writeLocalApiKeySecret, } from './keys.js';
4
4
  import { DEFAULT_INGEST_URL, PROD_BASE_URL, ensureSecretFilePermissions, getActiveProfile, getModaPaths, loadProfileConfig, resolveApiKeyFromActiveProfile, resolveSecretReference, secretFilePathFromRef, setActiveProfile, upsertProfile, } from './profiles.js';
@@ -72,6 +72,10 @@ export async function runAuthCommand(context) {
72
72
  return { exitCode: 0 };
73
73
  }
74
74
  case 'logout': {
75
+ // moda_ct session tokens are server-side revocable: best-effort revoke
76
+ // (bounded, catch-all inside) before the local deletion. Legacy
77
+ // auth0-kind sessions have nothing to revoke.
78
+ await revokeCliSessionToken({ profile: context.profile, env: context.env });
75
79
  clearAuthSession({ profile: context.profile, env: context.env });
76
80
  context.output.writeData({
77
81
  status: 'logged_out',
@@ -119,6 +123,11 @@ export async function runAuthCommand(context) {
119
123
  ? new Date(session.expires_at * 1000).toISOString()
120
124
  : null,
121
125
  };
126
+ // `user` is emitted only when the stored session carries an email —
127
+ // the PKCE exchange path persists user_email; legacy sessions lack it.
128
+ if (session?.user_email) {
129
+ data.user = { email: session.user_email };
130
+ }
122
131
  if (boolFlag(context, 'online')) {
123
132
  let online = { reachable: false };
124
133
  try {
@@ -0,0 +1,28 @@
1
+ import { type CommandContext } from './command-context.js';
2
+ export interface ProvisionRunOptions {
3
+ /**
4
+ * Original invocation as an argv array (e.g. ['moda', 'provision',
5
+ * '--save']), threaded into recoverable failures so the caller knows
6
+ * exactly how to resume after logging in.
7
+ */
8
+ resumeCommand?: string[];
9
+ }
10
+ export interface ProvisionResult {
11
+ exitCode: number;
12
+ apiKey?: string;
13
+ }
14
+ /**
15
+ * The credentials document printed to stdout. Field names are a public
16
+ * contract (`moda provision | jq -r .apiKey`); do not rename.
17
+ */
18
+ export interface ProvisionPayload {
19
+ apiKey: string;
20
+ tenantId: string;
21
+ tenantName: string;
22
+ tenantSlug: string;
23
+ ingestUrl: string;
24
+ baseUrl: string;
25
+ reused: boolean;
26
+ }
27
+ export declare function runProvision(context: CommandContext, options?: ProvisionRunOptions): Promise<ProvisionResult>;
28
+ //# sourceMappingURL=provision.d.ts.map
@@ -0,0 +1,104 @@
1
+ // src/provision.ts — moda provision: headless tenant + API key provisioning
2
+ // for CI jobs and agents. Requires an existing CLI session (moda auth login
3
+ // run previously); browserless signup is explicitly out of scope (decision
4
+ // D3), so a missing session fails fast with CliAuthError instead of opening
5
+ // a browser.
6
+ import { loadAuthSession } from './auth.js';
7
+ import { stringOption } from './command-context.js';
8
+ import { resolveIngestUrl, resolveModaBaseUrl } from './config.js';
9
+ import { CliAuthError } from './errors.js';
10
+ import { selectTenantAndCreateKey } from './init/tenant.js';
11
+ export async function runProvision(context, options = {}) {
12
+ const resumeCommand = options.resumeCommand ?? ['moda', 'provision'];
13
+ // Pre-coercion raw values (like init's tenantIdFlag/profileFlag): the
14
+ // dispatcher's flagsToArgs number-coerces numeric-looking values, which
15
+ // must never happen to tenant ids, profile names, or labels.
16
+ const profileFlag = stringOption(context.rawFlags.profile);
17
+ const authOptions = { profile: profileFlag, env: context.env };
18
+ // Provision never opens a browser (headless by contract), so the session
19
+ // check happens up front: no stored session is a recoverable exit-4
20
+ // failure with explicit instructions, not a login prompt.
21
+ const session = loadAuthSession(authOptions);
22
+ if (!session) {
23
+ const expiredSession = loadAuthSession({ ...authOptions, includeExpired: true });
24
+ throw new CliAuthError('moda provision requires an existing CLI session and never opens a browser. ' +
25
+ 'Run "moda auth login" on a machine with a browser first, then re-run provision here ' +
26
+ '(or skip provisioning and set MODA_API_KEY directly).', {
27
+ reason: expiredSession ? 'token_expired' : 'no_stored_credentials',
28
+ login_command: ['moda', 'auth', 'login'],
29
+ resume_command: resumeCommand,
30
+ });
31
+ }
32
+ const flagTenantId = stringOption(context.rawFlags['tenant-id'] ?? context.rawFlags.tenant_id);
33
+ const envTenantId = stringOption(context.env.MODA_TENANT_ID);
34
+ const label = stringOption(context.rawFlags.label);
35
+ const save = context.flags.save === 'true';
36
+ // Reuses init's dedup + bootstrap machinery (stored-key liveness probe,
37
+ // Idempotency-Key persistence, label, cap/viewer errors). Multi-tenant
38
+ // accounts without --tenant-id/MODA_TENANT_ID throw CliInputRequiredError
39
+ // (exit 5) with per-tenant resume flags — even on a TTY, provision is
40
+ // headless by contract and never prompts.
41
+ let result;
42
+ try {
43
+ result = await selectTenantAndCreateKey({
44
+ tenantId: flagTenantId ?? envTenantId,
45
+ tenantIdSource: flagTenantId !== undefined ? 'flag' : envTenantId !== undefined ? 'env' : undefined,
46
+ // Unconditional (init derives this from flags/TTY): provision is
47
+ // headless by contract even on an interactive terminal.
48
+ nonInteractive: true,
49
+ // stdout must stay exactly one JSON credentials document; all provision
50
+ // prose goes to stderr, so suppress the tenant-selection prose entirely.
51
+ silent: true,
52
+ // Multi-tenant runs surface CliInputRequiredError (exit 5); echo this
53
+ // invocation's resume command so the caller knows how to continue.
54
+ resumeCommand,
55
+ label,
56
+ // Headless callers own their credential storage; --save opts into the
57
+ // init-style ~/.moda/config.json persistence.
58
+ persistConfig: save,
59
+ profile: profileFlag,
60
+ env: context.env,
61
+ });
62
+ }
63
+ catch (error) {
64
+ // A mid-flight 401 classified by authFetch (token_expired /
65
+ // token_revoked / renew_failed during the context or bootstrap call)
66
+ // throws a CliAuthError that carries only the login command. Re-throw
67
+ // with this invocation's resume command attached so the caller knows how
68
+ // to continue after recovering, matching the upfront session gate above.
69
+ if (error instanceof CliAuthError && !error.auth.resume_command) {
70
+ throw new CliAuthError(error.message, {
71
+ reason: error.auth.reason,
72
+ login_command: error.auth.login_command,
73
+ resume_command: resumeCommand,
74
+ });
75
+ }
76
+ throw error;
77
+ }
78
+ const payload = {
79
+ apiKey: result.apiKey,
80
+ tenantId: result.tenantId,
81
+ tenantName: result.tenantName,
82
+ tenantSlug: result.tenantSlug,
83
+ ingestUrl: resolveIngestUrl(authOptions),
84
+ baseUrl: resolveModaBaseUrl(authOptions),
85
+ reused: result.reused,
86
+ };
87
+ // Security note on stderr, never stdout: stdout carries a live credential
88
+ // and must stay exactly one parseable JSON document.
89
+ console.error('Output contains a live API key. Avoid piping to logs.');
90
+ if (save) {
91
+ console.error('Saved API key + tenant to ~/.moda/config.json (--save).');
92
+ }
93
+ // `moda provision` exists to be composed: `KEY=$(moda provision | jq -r
94
+ // .apiKey)`. Command substitution IS a pipe, so stdout is a non-TTY and
95
+ // the CLI would auto-select agent mode and wrap the payload in a
96
+ // moda.agent.v1 envelope, burying apiKey under `.data`. Like `moda auth
97
+ // token`, this command therefore deliberately bypasses the CliOutput
98
+ // envelope machinery in EVERY output mode (human/json/agent) and writes
99
+ // exactly one JSON document to stdout — nothing else. Human TTY runs get
100
+ // the same machine-parseable JSON: credentials output is the whole point.
101
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
102
+ return { exitCode: 0, apiKey: result.apiKey };
103
+ }
104
+ //# sourceMappingURL=provision.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.13.0",
3
+ "version": "1.13.2",
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-07-06T09:53:55.227Z",
4
- "cli_version": "1.13.0",
3
+ "bundled_at": "2026-07-06T10:32:52.250Z",
4
+ "cli_version": "1.13.2",
5
5
  "skills": [
6
6
  {
7
7
  "id": "integration-node-anthropic",