@moda-ai/cli 1.27.1 → 1.28.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.
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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,
|
|
@@ -931,9 +933,11 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
931
933
|
const summaryParts = [];
|
|
932
934
|
const tool = packet.candidate_tool;
|
|
933
935
|
const skill = packet.candidate_skill;
|
|
936
|
+
const skillEdit = packet.candidate_skill_edit;
|
|
934
937
|
const hasTool = typeof tool?.proposed_description === "string" && tool.proposed_description !== "";
|
|
935
938
|
const hasSkill = typeof skill?.skill_md === "string" && skill.skill_md !== "";
|
|
936
|
-
|
|
939
|
+
const hasSkillEdit = typeof skillEdit?.proposed_skill_md === "string" && skillEdit.proposed_skill_md !== "";
|
|
940
|
+
if (!hasTool && !hasSkill && !hasSkillEdit) {
|
|
937
941
|
return { findings, artifacts, warnings, nextCommands, summarySuffix: "" };
|
|
938
942
|
}
|
|
939
943
|
const rawRef = typeof packet.short_ref === "string" && packet.short_ref.trim() ? packet.short_ref : fixId;
|
|
@@ -1012,6 +1016,34 @@ function materializePacketCandidates(packet, fixId, human) {
|
|
|
1012
1016
|
w(` moda fix mark-applied ${fixId} --note="installed skill ${skillName}"`);
|
|
1013
1017
|
}
|
|
1014
1018
|
}
|
|
1019
|
+
if (hasSkillEdit && skillEdit) {
|
|
1020
|
+
const skillKey = typeof skillEdit.skill_key === "string" && skillEdit.skill_key ? skillEdit.skill_key : typeof skillEdit.skill_name === "string" && skillEdit.skill_name ? skillEdit.skill_name : "moda-skill";
|
|
1021
|
+
const changeSummary = typeof skillEdit.change_summary === "string" ? skillEdit.change_summary : "";
|
|
1022
|
+
const currentPath = writeCandidateFile("SKILL.current.md", typeof skillEdit.current_skill_md === "string" ? skillEdit.current_skill_md : "", "fix_skill_edit_current", `Current SKILL.md of your synced skill '${skillKey}' — the diff base.`);
|
|
1023
|
+
const proposedPath = writeCandidateFile("SKILL.proposed.md", skillEdit.proposed_skill_md ?? "", "fix_skill_edit_proposed", `Proposed SKILL.md for '${skillKey}' (minimal edit) — apply it to the existing skill in your repo, then \`moda fix mark-applied ${fixId}\`.`);
|
|
1024
|
+
findings.push({
|
|
1025
|
+
id: "fix:candidate_skill_edit",
|
|
1026
|
+
title: `skill '${skillKey}' edit proposed${changeSummary ? `: ${changeSummary}` : " (minimal delta to the existing skill)"}`,
|
|
1027
|
+
status: "proposed",
|
|
1028
|
+
severity: "info",
|
|
1029
|
+
description: `This amends the EXISTING synced skill '${skillKey}' — a minimal delta, not a new skill. Apply the proposed SKILL.md in your repo, then confirm with \`moda fix mark-applied ${fixId}\`.`
|
|
1030
|
+
});
|
|
1031
|
+
summaryParts.push(`Includes a skill-edit candidate amending existing skill '${skillKey}'.`);
|
|
1032
|
+
if (human) {
|
|
1033
|
+
w("");
|
|
1034
|
+
w(`Skill-edit candidate — amends existing skill '${skillKey}'`);
|
|
1035
|
+
if (changeSummary)
|
|
1036
|
+
block("change summary", changeSummary);
|
|
1037
|
+
if (currentPath)
|
|
1038
|
+
w(` current SKILL.md written to ${currentPath}`);
|
|
1039
|
+
if (proposedPath)
|
|
1040
|
+
w(` proposed SKILL.md written to ${proposedPath}`);
|
|
1041
|
+
if (currentPath && proposedPath)
|
|
1042
|
+
w(` diff: diff ${currentPath} ${proposedPath}`);
|
|
1043
|
+
w(` apply the edit to the existing skill in your repo, then confirm:`);
|
|
1044
|
+
w(` moda fix mark-applied ${fixId} --note="amended skill ${skillKey}"`);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1015
1047
|
nextCommands.push({
|
|
1016
1048
|
command: `moda fix mark-applied ${fixId} --note="where/how you applied it"`,
|
|
1017
1049
|
purpose: "Confirm you applied the candidate in your own stack (no PR) — flips the fix to SHIPPED and starts recurrence monitoring.",
|
|
@@ -1085,7 +1117,7 @@ function nextCommandsFor(fix) {
|
|
|
1085
1117
|
mutability: "write",
|
|
1086
1118
|
requires_approval: true
|
|
1087
1119
|
};
|
|
1088
|
-
const skillGatePending = fix.fixType === "SKILL" && fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId;
|
|
1120
|
+
const skillGatePending = fix.fixType === "SKILL" && (fix.candidateRef?.kind === "skill_edit" || fix.candidateRef?.kind === "skill" && !fix.candidateRef?.skillId);
|
|
1089
1121
|
switch (fix.status) {
|
|
1090
1122
|
case "VERIFIED":
|
|
1091
1123
|
if (isArtifactFix(fix)) {
|
|
@@ -2028,6 +2060,47 @@ function renderJsonSchemas() {
|
|
|
2028
2060
|
}, null, 2);
|
|
2029
2061
|
}
|
|
2030
2062
|
|
|
2063
|
+
// src/api-key-auth.ts
|
|
2064
|
+
var PROBE_TIMEOUT_MS = 1e4;
|
|
2065
|
+
function resolveApiKeyCredential(options = {}) {
|
|
2066
|
+
const key = resolveApiKey(options);
|
|
2067
|
+
if (!key)
|
|
2068
|
+
return;
|
|
2069
|
+
const env = options.env ?? process.env;
|
|
2070
|
+
const source = env.MODA_API_KEY === key ? "env" : resolveApiKeyFromActiveProfile(options) === key ? "profile" : "config";
|
|
2071
|
+
return { key, source, tenantId: embeddedTenantIdFromApiKey(key) };
|
|
2072
|
+
}
|
|
2073
|
+
function redactApiKey(key) {
|
|
2074
|
+
if (key.length <= 12)
|
|
2075
|
+
return "<redacted>";
|
|
2076
|
+
return `${key.slice(0, 8)}...${key.slice(-4)}`;
|
|
2077
|
+
}
|
|
2078
|
+
async function probeApiKey(key, options = {}) {
|
|
2079
|
+
let response;
|
|
2080
|
+
try {
|
|
2081
|
+
response = await fetch(`${resolveModaBaseUrl(options)}/api/v1/data/overview`, {
|
|
2082
|
+
method: "GET",
|
|
2083
|
+
headers: { "x-api-key": key },
|
|
2084
|
+
signal: AbortSignal.timeout(PROBE_TIMEOUT_MS)
|
|
2085
|
+
});
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2088
|
+
return { outcome: "unreachable", reason };
|
|
2089
|
+
}
|
|
2090
|
+
if (response.status === 401 || response.status === 403) {
|
|
2091
|
+
return { outcome: "rejected", status: response.status };
|
|
2092
|
+
}
|
|
2093
|
+
if (response.status !== 200) {
|
|
2094
|
+
return { outcome: "unreachable", reason: `HTTP ${response.status}` };
|
|
2095
|
+
}
|
|
2096
|
+
try {
|
|
2097
|
+
await response.json();
|
|
2098
|
+
} catch {
|
|
2099
|
+
return { outcome: "unreachable", reason: "non-JSON response from the Data API" };
|
|
2100
|
+
}
|
|
2101
|
+
return { outcome: "live" };
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2031
2104
|
// src/keys.ts
|
|
2032
2105
|
import { createHash } from "node:crypto";
|
|
2033
2106
|
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -2212,13 +2285,75 @@ function serializeProfile(profile, activeProfile) {
|
|
|
2212
2285
|
auth_token_ref: profile.auth_token_ref
|
|
2213
2286
|
};
|
|
2214
2287
|
}
|
|
2288
|
+
async function readStdin() {
|
|
2289
|
+
const chunks = [];
|
|
2290
|
+
for await (const chunk of process.stdin)
|
|
2291
|
+
chunks.push(Buffer.from(chunk));
|
|
2292
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
2293
|
+
}
|
|
2294
|
+
async function apiKeyLoginCandidate(context) {
|
|
2295
|
+
if (boolFlag2(context, "api-key-stdin")) {
|
|
2296
|
+
const piped = await readStdin();
|
|
2297
|
+
if (piped)
|
|
2298
|
+
return { key: piped, fromArgv: false };
|
|
2299
|
+
throw new CliInputError("No API key received on stdin.", 'Pipe the key in, e.g. `printf %s "$KEY" | moda auth login --api-key-stdin`.');
|
|
2300
|
+
}
|
|
2301
|
+
const inline = flag(context, "api-key");
|
|
2302
|
+
if (inline)
|
|
2303
|
+
return { key: inline, fromArgv: true };
|
|
2304
|
+
const env = context.env ?? process.env;
|
|
2305
|
+
if (env.MODA_API_KEY)
|
|
2306
|
+
return { key: env.MODA_API_KEY, fromArgv: false };
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
async function loginWithApiKey(context, candidate) {
|
|
2310
|
+
const { key } = candidate;
|
|
2311
|
+
if (candidate.fromArgv) {
|
|
2312
|
+
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`.');
|
|
2313
|
+
}
|
|
2314
|
+
const authOptions = { profile: context.profile, env: context.env };
|
|
2315
|
+
const probe = await probeApiKey(key, authOptions);
|
|
2316
|
+
if (probe.outcome === "rejected") {
|
|
2317
|
+
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.');
|
|
2318
|
+
}
|
|
2319
|
+
if (probe.outcome === "unreachable") {
|
|
2320
|
+
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.");
|
|
2321
|
+
}
|
|
2322
|
+
const tenantId = embeddedTenantIdFromApiKey(key);
|
|
2323
|
+
saveCliConfig({ api_key: key, ...tenantId ? { tenant_id: tenantId } : {} }, authOptions);
|
|
2324
|
+
const envKey = (context.env ?? process.env).MODA_API_KEY;
|
|
2325
|
+
const shadowedByEnv = Boolean(envKey && envKey !== key);
|
|
2326
|
+
if (shadowedByEnv) {
|
|
2327
|
+
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.`);
|
|
2328
|
+
}
|
|
2329
|
+
context.output.writeData({
|
|
2330
|
+
status: "authenticated",
|
|
2331
|
+
method: "api_key",
|
|
2332
|
+
profile: context.profile ?? context.env?.MODA_PROFILE ?? process.env.MODA_PROFILE ?? null,
|
|
2333
|
+
apiKeyCredential: { masked: redactApiKey(key) },
|
|
2334
|
+
tenant_id: tenantId ?? null,
|
|
2335
|
+
persisted: true,
|
|
2336
|
+
shadowed_by_env: shadowedByEnv,
|
|
2337
|
+
effective_credential: shadowedByEnv ? "env" : "saved"
|
|
2338
|
+
}, shadowedByEnv ? { status: "degraded" } : undefined);
|
|
2339
|
+
return { exitCode: shadowedByEnv ? EXIT_DEGRADED : EXIT_OK };
|
|
2340
|
+
}
|
|
2215
2341
|
async function runAuthCommand(context) {
|
|
2216
2342
|
switch (subcommand(context)) {
|
|
2217
2343
|
case "login": {
|
|
2344
|
+
const wantsApiKey = "api-key" in context.flags || boolFlag2(context, "api-key-stdin");
|
|
2345
|
+
const nonInteractive = boolFlag2(context, "non-interactive");
|
|
2346
|
+
const candidate = wantsApiKey || nonInteractive ? await apiKeyLoginCandidate(context) : undefined;
|
|
2347
|
+
if (candidate)
|
|
2348
|
+
return loginWithApiKey(context, candidate);
|
|
2349
|
+
if (wantsApiKey || nonInteractive) {
|
|
2350
|
+
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".');
|
|
2351
|
+
}
|
|
2218
2352
|
const screen = flag(context, "screen") === "signup" ? "signup" : "login";
|
|
2219
2353
|
await login(screen, { profile: context.profile, env: context.env });
|
|
2220
2354
|
context.output.writeData({
|
|
2221
2355
|
status: "authenticated",
|
|
2356
|
+
method: "browser",
|
|
2222
2357
|
profile: context.profile ?? process.env.MODA_PROFILE ?? null
|
|
2223
2358
|
});
|
|
2224
2359
|
return { exitCode: 0 };
|
|
@@ -2243,7 +2378,33 @@ async function runAuthCommand(context) {
|
|
|
2243
2378
|
case "whoami": {
|
|
2244
2379
|
const session = loadAuthSession({ profile: context.profile, env: context.env });
|
|
2245
2380
|
if (!session) {
|
|
2246
|
-
|
|
2381
|
+
const credential = resolveApiKeyCredential({
|
|
2382
|
+
profile: context.profile,
|
|
2383
|
+
env: context.env
|
|
2384
|
+
});
|
|
2385
|
+
if (credential) {
|
|
2386
|
+
const probe = await probeApiKey(credential.key, {
|
|
2387
|
+
profile: context.profile,
|
|
2388
|
+
env: context.env
|
|
2389
|
+
});
|
|
2390
|
+
if (probe.outcome === "rejected") {
|
|
2391
|
+
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".');
|
|
2392
|
+
}
|
|
2393
|
+
context.output.writeData({
|
|
2394
|
+
profile: context.profile ?? process.env.MODA_PROFILE ?? null,
|
|
2395
|
+
credential: "api_key",
|
|
2396
|
+
apiKeyCredential: {
|
|
2397
|
+
source: credential.source,
|
|
2398
|
+
masked: redactApiKey(credential.key),
|
|
2399
|
+
verified: probe.outcome === "live"
|
|
2400
|
+
},
|
|
2401
|
+
tenant_id: credential.tenantId ?? null,
|
|
2402
|
+
session: null,
|
|
2403
|
+
context: null
|
|
2404
|
+
}, probe.outcome === "live" ? undefined : { status: "degraded" });
|
|
2405
|
+
return { exitCode: probe.outcome === "live" ? EXIT_OK : EXIT_DEGRADED };
|
|
2406
|
+
}
|
|
2407
|
+
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
2408
|
}
|
|
2248
2409
|
const response = await authFetch("/api/cli/context", {}, { profile: context.profile, env: context.env });
|
|
2249
2410
|
if (!response.ok) {
|
|
@@ -2265,8 +2426,17 @@ async function runAuthCommand(context) {
|
|
|
2265
2426
|
const authOptions = { profile: context.profile, env: context.env };
|
|
2266
2427
|
const session = loadAuthSession({ ...authOptions, includeExpired: true });
|
|
2267
2428
|
const loggedIn = isAuthSessionValid(session);
|
|
2429
|
+
const credential = resolveApiKeyCredential(authOptions);
|
|
2268
2430
|
const data = {
|
|
2269
2431
|
loggedIn,
|
|
2432
|
+
authenticated: loggedIn || Boolean(credential),
|
|
2433
|
+
apiKeyCredential: credential ? {
|
|
2434
|
+
present: true,
|
|
2435
|
+
source: credential.source,
|
|
2436
|
+
masked: redactApiKey(credential.key),
|
|
2437
|
+
tenantId: credential.tenantId ?? null,
|
|
2438
|
+
verified: null
|
|
2439
|
+
} : { present: false },
|
|
2270
2440
|
profile: context.profile ?? context.env.MODA_PROFILE ?? session?.profile ?? null,
|
|
2271
2441
|
expiresAt: session ? new Date(session.expires_at * 1000).toISOString() : null
|
|
2272
2442
|
};
|
|
@@ -2286,9 +2456,16 @@ async function runAuthCommand(context) {
|
|
|
2286
2456
|
}
|
|
2287
2457
|
} catch {}
|
|
2288
2458
|
data.online = online;
|
|
2459
|
+
if (credential && data.apiKeyCredential.present) {
|
|
2460
|
+
const probe = await probeApiKey(credential.key, authOptions);
|
|
2461
|
+
data.apiKeyCredential.verified = probe.outcome === "live";
|
|
2462
|
+
if (probe.outcome === "rejected") {
|
|
2463
|
+
data.authenticated = loggedIn;
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2289
2466
|
}
|
|
2290
2467
|
context.output.writeData(data);
|
|
2291
|
-
return { exitCode:
|
|
2468
|
+
return { exitCode: data.authenticated ? EXIT_OK : EXIT_AUTH_REQUIRED };
|
|
2292
2469
|
}
|
|
2293
2470
|
case "token": {
|
|
2294
2471
|
const session = loadAuthSession({ profile: context.profile, env: context.env });
|
|
@@ -4794,7 +4971,7 @@ function buildResumeCommand(command, flags) {
|
|
|
4794
4971
|
];
|
|
4795
4972
|
}
|
|
4796
4973
|
function printHelp() {
|
|
4797
|
-
console.
|
|
4974
|
+
console.log(`Usage: moda <command> [options]
|
|
4798
4975
|
|
|
4799
4976
|
Commands:
|
|
4800
4977
|
init Set up Moda in your project
|
|
@@ -4802,11 +4979,12 @@ Commands:
|
|
|
4802
4979
|
status Show concise local Moda setup status
|
|
4803
4980
|
manifest Describe the CLI machine protocol and commands
|
|
4804
4981
|
doctor Diagnose local Moda setup and data flow
|
|
4805
|
-
auth login Authenticate with Moda
|
|
4806
|
-
auth
|
|
4982
|
+
auth login Authenticate with Moda (browser)
|
|
4983
|
+
auth login --api-key Non-interactive: validate MODA_API_KEY (or --api-key=<key>) and save it
|
|
4984
|
+
auth status Show auth state for both credentials (exit 4 when neither is usable); --online probes the API
|
|
4807
4985
|
auth token Print the raw bearer token for $(moda auth token)
|
|
4808
4986
|
auth logout Clear cached Moda auth
|
|
4809
|
-
auth whoami Show the authenticated Moda
|
|
4987
|
+
auth whoami Show the authenticated Moda identity (session or API key)
|
|
4810
4988
|
profiles list List local Moda profiles
|
|
4811
4989
|
profiles use <name> Switch active Moda profile
|
|
4812
4990
|
profiles create <name> Create or update a local Moda profile
|
|
@@ -5025,6 +5203,8 @@ Provision flags (headless; requires an existing "moda auth login" session):
|
|
|
5025
5203
|
|
|
5026
5204
|
Examples:
|
|
5027
5205
|
moda init
|
|
5206
|
+
moda auth login --api-key=$MODA_API_KEY
|
|
5207
|
+
moda auth whoami
|
|
5028
5208
|
moda provision --tenant-id=<org-id>
|
|
5029
5209
|
moda status
|
|
5030
5210
|
moda manifest --json
|
|
@@ -5089,7 +5269,11 @@ Examples:
|
|
|
5089
5269
|
moda fixes
|
|
5090
5270
|
moda fix start <problem_id> --wait
|
|
5091
5271
|
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"
|
|
5272
|
+
moda fix mark-applied <fix_id> --note="tool description updated in our agent config"
|
|
5273
|
+
|
|
5274
|
+
Run \`moda <command> --help\` for a command's subcommands, and
|
|
5275
|
+
\`moda <command> <subcommand> --help\` for that subcommand's flags
|
|
5276
|
+
(e.g. \`moda auth login --help\`).`);
|
|
5093
5277
|
}
|
|
5094
5278
|
function printCommandHelp(definition) {
|
|
5095
5279
|
const lines = [];
|
|
@@ -5100,6 +5284,14 @@ function printCommandHelp(definition) {
|
|
|
5100
5284
|
if (definition.mutability === "write") {
|
|
5101
5285
|
lines.push("This command can mutate state.");
|
|
5102
5286
|
}
|
|
5287
|
+
if (definition.subcommands && definition.subcommands.length > 0) {
|
|
5288
|
+
lines.push("", "Subcommands:");
|
|
5289
|
+
const width = Math.max(...definition.subcommands.map((entry) => entry.name.length));
|
|
5290
|
+
for (const entry of definition.subcommands) {
|
|
5291
|
+
lines.push(` ${entry.name.padEnd(width)} ${entry.description}`);
|
|
5292
|
+
}
|
|
5293
|
+
lines.push("", `Run 'moda ${definition.name} <subcommand> --help' for that subcommand's flags.`);
|
|
5294
|
+
}
|
|
5103
5295
|
if (definition.examples.length > 0) {
|
|
5104
5296
|
lines.push("", "Examples:");
|
|
5105
5297
|
for (const example of definition.examples) {
|
|
@@ -5107,7 +5299,28 @@ function printCommandHelp(definition) {
|
|
|
5107
5299
|
}
|
|
5108
5300
|
}
|
|
5109
5301
|
lines.push("", "Run 'moda --help' for the full command list and common flags", "(--json, --agent, --profile, --days-back, --limit, --time-range, ...).");
|
|
5110
|
-
console.
|
|
5302
|
+
console.log(lines.join(`
|
|
5303
|
+
`));
|
|
5304
|
+
}
|
|
5305
|
+
function printSubcommandHelp(parent, entry) {
|
|
5306
|
+
const lines = [];
|
|
5307
|
+
lines.push(`moda ${parent.name} ${entry.name} — ${entry.description}`, "");
|
|
5308
|
+
lines.push(`Usage: ${entry.usage}`);
|
|
5309
|
+
if (entry.flags && entry.flags.length > 0) {
|
|
5310
|
+
lines.push("", "Options:");
|
|
5311
|
+
const width = Math.max(...entry.flags.map((flag2) => flag2.name.length));
|
|
5312
|
+
for (const flag2 of entry.flags) {
|
|
5313
|
+
lines.push(` ${flag2.name.padEnd(width)} ${flag2.description}`);
|
|
5314
|
+
}
|
|
5315
|
+
}
|
|
5316
|
+
if (entry.examples && entry.examples.length > 0) {
|
|
5317
|
+
lines.push("", "Examples:");
|
|
5318
|
+
for (const example of entry.examples) {
|
|
5319
|
+
lines.push(` ${example}`);
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
lines.push("", `Run 'moda ${parent.name} --help' for the other ${parent.name} subcommands.`);
|
|
5323
|
+
console.log(lines.join(`
|
|
5111
5324
|
`));
|
|
5112
5325
|
}
|
|
5113
5326
|
async function attachWindows(rows, windowSize) {
|
|
@@ -5859,6 +6072,7 @@ async function runCommand(command, positional, flags, positionals = positional ?
|
|
|
5859
6072
|
}
|
|
5860
6073
|
return 0;
|
|
5861
6074
|
}
|
|
6075
|
+
var OFFLINE_PROMPTS_SUBCOMMANDS = new Set(["init", "status", "diff"]);
|
|
5862
6076
|
function legacyCommand(metadata) {
|
|
5863
6077
|
return {
|
|
5864
6078
|
...metadata,
|
|
@@ -5952,7 +6166,7 @@ var commandRegistry = createCommandRegistry([
|
|
|
5952
6166
|
resetApiRequestCountBeforeRun: true,
|
|
5953
6167
|
telemetry: "result",
|
|
5954
6168
|
handler: async (context) => {
|
|
5955
|
-
const { runInit } = await import("./index-
|
|
6169
|
+
const { runInit } = await import("./index-g3gt62rm.js");
|
|
5956
6170
|
if (context.outputMode === "agent-stream") {
|
|
5957
6171
|
context.output.writeEvent({
|
|
5958
6172
|
event: "started",
|
|
@@ -6025,15 +6239,81 @@ var commandRegistry = createCommandRegistry([
|
|
|
6025
6239
|
},
|
|
6026
6240
|
{
|
|
6027
6241
|
name: "auth",
|
|
6028
|
-
description: "Authenticate and inspect
|
|
6242
|
+
description: "Authenticate (browser or API key) and inspect auth state",
|
|
6029
6243
|
examples: [
|
|
6030
6244
|
"moda auth login",
|
|
6245
|
+
"moda auth login --api-key=$MODA_API_KEY",
|
|
6031
6246
|
"moda auth status",
|
|
6032
6247
|
"moda auth status --online",
|
|
6033
6248
|
"moda auth token",
|
|
6034
6249
|
"moda auth whoami",
|
|
6035
6250
|
"moda auth logout"
|
|
6036
6251
|
],
|
|
6252
|
+
subcommands: [
|
|
6253
|
+
{
|
|
6254
|
+
name: "login",
|
|
6255
|
+
description: "Authenticate with Moda (browser by default, or an API key)",
|
|
6256
|
+
usage: "moda auth login [--api-key[=<key>] | --api-key-stdin] [--non-interactive] [--screen=login|signup]",
|
|
6257
|
+
flags: [
|
|
6258
|
+
{
|
|
6259
|
+
name: "--api-key",
|
|
6260
|
+
description: "Authenticate non-interactively with the API key in MODA_API_KEY. Validates it, saves it, and never opens a browser."
|
|
6261
|
+
},
|
|
6262
|
+
{
|
|
6263
|
+
name: "--api-key-stdin",
|
|
6264
|
+
description: "Read the API key from stdin. Safest form: the key never appears in argv or shell history."
|
|
6265
|
+
},
|
|
6266
|
+
{
|
|
6267
|
+
name: "--api-key=<key>",
|
|
6268
|
+
description: "Supply the key inline. Exposes it in this process's command line and shell history; prefer MODA_API_KEY or --api-key-stdin."
|
|
6269
|
+
},
|
|
6270
|
+
{
|
|
6271
|
+
name: "--non-interactive",
|
|
6272
|
+
description: "Never open a browser. Uses MODA_API_KEY if set, otherwise fails with the commands that would mint a key."
|
|
6273
|
+
},
|
|
6274
|
+
{
|
|
6275
|
+
name: "--screen=login|signup",
|
|
6276
|
+
description: "Which browser screen to open (interactive flow only)."
|
|
6277
|
+
}
|
|
6278
|
+
],
|
|
6279
|
+
examples: [
|
|
6280
|
+
"MODA_API_KEY=<key> moda auth login --non-interactive",
|
|
6281
|
+
"moda auth login --api-key",
|
|
6282
|
+
'printf %s "$KEY" | moda auth login --api-key-stdin',
|
|
6283
|
+
"moda auth login"
|
|
6284
|
+
]
|
|
6285
|
+
},
|
|
6286
|
+
{
|
|
6287
|
+
name: "status",
|
|
6288
|
+
description: "Show local auth state for both credentials; exit 4 when neither is usable",
|
|
6289
|
+
usage: "moda auth status [--online]",
|
|
6290
|
+
flags: [
|
|
6291
|
+
{
|
|
6292
|
+
name: "--online",
|
|
6293
|
+
description: "Also probe the API to confirm the browser session is accepted."
|
|
6294
|
+
}
|
|
6295
|
+
],
|
|
6296
|
+
examples: ["moda auth status", "moda auth status --online"]
|
|
6297
|
+
},
|
|
6298
|
+
{
|
|
6299
|
+
name: "whoami",
|
|
6300
|
+
description: "Show the authenticated identity (browser session, or API-key credential)",
|
|
6301
|
+
usage: "moda auth whoami",
|
|
6302
|
+
examples: ["moda auth whoami"]
|
|
6303
|
+
},
|
|
6304
|
+
{
|
|
6305
|
+
name: "token",
|
|
6306
|
+
description: "Print the raw browser session bearer token for $(moda auth token)",
|
|
6307
|
+
usage: "moda auth token",
|
|
6308
|
+
examples: ["moda auth token", 'curl -H "Authorization: Bearer $(moda auth token)" ...']
|
|
6309
|
+
},
|
|
6310
|
+
{
|
|
6311
|
+
name: "logout",
|
|
6312
|
+
description: "Revoke and clear the browser session and any stored API key",
|
|
6313
|
+
usage: "moda auth logout",
|
|
6314
|
+
examples: ["moda auth logout"]
|
|
6315
|
+
}
|
|
6316
|
+
],
|
|
6037
6317
|
auth: "none",
|
|
6038
6318
|
outputModes: ["human", "json", "agent"],
|
|
6039
6319
|
mutability: "write",
|
|
@@ -6336,11 +6616,75 @@ var commandRegistry = createCommandRegistry([
|
|
|
6336
6616
|
legacyCommand({
|
|
6337
6617
|
name: "prompts",
|
|
6338
6618
|
description: "Manage prompt versions and labels",
|
|
6339
|
-
examples: ["moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
|
|
6619
|
+
examples: ["moda prompts init", "moda prompts status", "moda prompts sync", "moda prompts ab --baseline=old.md --candidate=new.md"],
|
|
6620
|
+
subcommands: [
|
|
6621
|
+
{
|
|
6622
|
+
name: "init",
|
|
6623
|
+
description: "Create .moda/prompts.yml in the current directory (never overwrites)",
|
|
6624
|
+
usage: "moda prompts init",
|
|
6625
|
+
flags: [],
|
|
6626
|
+
examples: ["moda prompts init", "cd /path/to/project && moda prompts init"]
|
|
6627
|
+
},
|
|
6628
|
+
{
|
|
6629
|
+
name: "status",
|
|
6630
|
+
description: "Show prompt files that changed since the last sync",
|
|
6631
|
+
usage: "moda prompts status",
|
|
6632
|
+
examples: ["moda prompts status"]
|
|
6633
|
+
},
|
|
6634
|
+
{
|
|
6635
|
+
name: "diff",
|
|
6636
|
+
description: "Alias of status: show local prompt changes",
|
|
6637
|
+
usage: "moda prompts diff",
|
|
6638
|
+
examples: ["moda prompts diff"]
|
|
6639
|
+
},
|
|
6640
|
+
{
|
|
6641
|
+
name: "sync",
|
|
6642
|
+
description: "Upload changed prompt versions to Moda Cloud",
|
|
6643
|
+
usage: "moda prompts sync [--watch] [--dry-run]",
|
|
6644
|
+
flags: [
|
|
6645
|
+
{ name: "--watch", description: "Re-upload whenever a prompt file changes." },
|
|
6646
|
+
{ name: "--dry-run", description: "Show what would be uploaded without writing." }
|
|
6647
|
+
],
|
|
6648
|
+
examples: ["moda prompts sync", "moda prompts sync --dry-run"]
|
|
6649
|
+
},
|
|
6650
|
+
{
|
|
6651
|
+
name: "promote",
|
|
6652
|
+
description: "Move a dev/staging/prod label to a prompt version",
|
|
6653
|
+
usage: "moda prompts promote <key> --label=<dev|staging|prod> --version=<version_id>",
|
|
6654
|
+
flags: [
|
|
6655
|
+
{ name: "--label=<label>", description: "Release label to move (dev, staging, prod)." },
|
|
6656
|
+
{ name: "--version=<id>", description: "Prompt version id to point the label at." }
|
|
6657
|
+
],
|
|
6658
|
+
examples: ["moda prompts promote support.triage --label=prod --version=pver_abc123"]
|
|
6659
|
+
},
|
|
6660
|
+
{
|
|
6661
|
+
name: "ab",
|
|
6662
|
+
description: "Run a judged prompt A/B replay experiment",
|
|
6663
|
+
usage: "moda prompts ab --baseline=<path> --candidate=<path> --conversations=<ids>",
|
|
6664
|
+
flags: [
|
|
6665
|
+
{ name: "--baseline=<path>", description: "Prompt file to treat as the control." },
|
|
6666
|
+
{ name: "--candidate=<path>", description: "Prompt file to treat as the variant." },
|
|
6667
|
+
{ name: "--conversations=<ids>", description: "Comma-separated conversation ids to replay." }
|
|
6668
|
+
],
|
|
6669
|
+
examples: [
|
|
6670
|
+
"moda prompts ab --baseline=prompts/agent.prompt.md --candidate=prompts/agent-v2.prompt.md --conversations=conv_1,conv_2"
|
|
6671
|
+
]
|
|
6672
|
+
},
|
|
6673
|
+
{
|
|
6674
|
+
name: "propose",
|
|
6675
|
+
description: "Revise a prompt from a run's failures",
|
|
6676
|
+
usage: "moda prompts propose [--gate] [--promote-on-win]",
|
|
6677
|
+
flags: [
|
|
6678
|
+
{ name: "--gate", description: "Automatically A/B the proposed revision." },
|
|
6679
|
+
{ name: "--promote-on-win", description: "Promote the candidate if it wins the gate." }
|
|
6680
|
+
],
|
|
6681
|
+
examples: ["moda prompts propose --gate --promote-on-win"]
|
|
6682
|
+
}
|
|
6683
|
+
],
|
|
6340
6684
|
auth: "api-key",
|
|
6341
6685
|
outputModes: ["human", "json", "agent"],
|
|
6342
6686
|
mutability: "write",
|
|
6343
|
-
validateConfigBeforeRun:
|
|
6687
|
+
validateConfigBeforeRun: (parsed) => !OFFLINE_PROMPTS_SUBCOMMANDS.has(parsed.positional ?? "status"),
|
|
6344
6688
|
resetApiRequestCountBeforeRun: true,
|
|
6345
6689
|
telemetry: "result-and-error"
|
|
6346
6690
|
}),
|
|
@@ -6474,7 +6818,13 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
6474
6818
|
if (command === "--help" || command === "-h" || flags.help === "true") {
|
|
6475
6819
|
const helpTarget = command && command !== "--help" && command !== "-h" ? commandRegistry.find(command) : undefined;
|
|
6476
6820
|
if (helpTarget) {
|
|
6477
|
-
|
|
6821
|
+
const subcommandName = parsed.positional;
|
|
6822
|
+
const subcommandEntry = subcommandName ? helpTarget.subcommands?.find((entry) => entry.name === subcommandName) : undefined;
|
|
6823
|
+
if (subcommandEntry) {
|
|
6824
|
+
printSubcommandHelp(helpTarget, subcommandEntry);
|
|
6825
|
+
} else {
|
|
6826
|
+
printCommandHelp(helpTarget);
|
|
6827
|
+
}
|
|
6478
6828
|
} else {
|
|
6479
6829
|
printHelp();
|
|
6480
6830
|
}
|
|
@@ -6519,7 +6869,8 @@ async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
6519
6869
|
})
|
|
6520
6870
|
});
|
|
6521
6871
|
setActiveContext(context);
|
|
6522
|
-
|
|
6872
|
+
const needsConfig = typeof definition.validateConfigBeforeRun === "function" ? definition.validateConfigBeforeRun(parsed) : definition.validateConfigBeforeRun;
|
|
6873
|
+
if (needsConfig) {
|
|
6523
6874
|
try {
|
|
6524
6875
|
validateConfig();
|
|
6525
6876
|
} catch (error) {
|
package/package.json
CHANGED
package/skills/moda-cli/SKILL.md
CHANGED
|
@@ -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.
|
|
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.
|
|
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.**
|
|
110
|
-
|
|
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 #
|
|
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>, "
|
|
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)
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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` |
|
|
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 |
|