@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
|
-
|
|
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,
|
|
@@ -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
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
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.
|
|
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-
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
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 |
|