@moda-ai/cli 1.17.0 → 1.17.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.
- package/dist/profile-commands.js +13 -1
- package/dist/profiles.d.ts +14 -0
- package/dist/profiles.js +39 -1
- package/dist/prompts-propose.d.ts +10 -0
- package/dist/prompts-propose.js +76 -0
- package/dist/prompts.js +5 -1
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
package/dist/profile-commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
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
|
-
import { DEFAULT_INGEST_URL, PROD_BASE_URL, ensureSecretFilePermissions, getActiveProfile, getModaPaths, loadProfileConfig, resolveApiKeyFromActiveProfile, resolveSecretReference, secretFilePathFromRef, setActiveProfile, upsertProfile, } from './profiles.js';
|
|
4
|
+
import { DEFAULT_INGEST_URL, PROD_BASE_URL, clearStoredApiKeyCredentials, ensureSecretFilePermissions, getActiveProfile, getModaPaths, loadProfileConfig, resolveApiKeyFromActiveProfile, resolveSecretReference, secretFilePathFromRef, setActiveProfile, upsertProfile, } from './profiles.js';
|
|
5
5
|
import { CliInputError } from './errors.js';
|
|
6
6
|
// Bound the `auth status --online` probe: without a signal, undici's defaults
|
|
7
7
|
// can stall a status check for minutes on a black-holed connection.
|
|
@@ -77,9 +77,21 @@ export async function runAuthCommand(context) {
|
|
|
77
77
|
// auth0-kind sessions have nothing to revoke.
|
|
78
78
|
await revokeCliSessionToken({ profile: context.profile, env: context.env });
|
|
79
79
|
clearAuthSession({ profile: context.profile, env: context.env });
|
|
80
|
+
// The session token is only the control-plane credential. Data commands
|
|
81
|
+
// authenticate with the moda_sk_ API key written by `moda init`
|
|
82
|
+
// (~/.moda/config.json + profile api_key_ref), so logout must clear it
|
|
83
|
+
// too or the next login keeps acting as the old tenant.
|
|
84
|
+
const apiKeyCleared = clearStoredApiKeyCredentials({
|
|
85
|
+
profile: context.profile,
|
|
86
|
+
env: context.env,
|
|
87
|
+
});
|
|
88
|
+
if (context.env.MODA_API_KEY) {
|
|
89
|
+
console.error('Note: MODA_API_KEY is set in the environment and still takes precedence. Unset it to fully sign out.');
|
|
90
|
+
}
|
|
80
91
|
context.output.writeData({
|
|
81
92
|
status: 'logged_out',
|
|
82
93
|
profile: context.profile ?? process.env.MODA_PROFILE ?? null,
|
|
94
|
+
api_key_cleared: apiKeyCleared,
|
|
83
95
|
});
|
|
84
96
|
return { exitCode: 0 };
|
|
85
97
|
}
|
package/dist/profiles.d.ts
CHANGED
|
@@ -80,6 +80,20 @@ export declare function getActiveProfile(options?: ProfileIOOptions): ModaProfil
|
|
|
80
80
|
export declare function setActiveProfile(name: string, options?: ProfileIOOptions): ProfileConfig;
|
|
81
81
|
export declare function upsertProfile(profile: ModaProfile, options?: ProfileIOOptions): ProfileConfig;
|
|
82
82
|
export declare function syncLegacyConfigToProfile(legacy: LegacyCliConfig, options?: ProfileIOOptions): ProfileConfig;
|
|
83
|
+
/**
|
|
84
|
+
* Forget the stored data-plane credentials (`moda_sk_` API key + tenant
|
|
85
|
+
* binding written by `moda init`): the legacy config's api_key/tenant_id and
|
|
86
|
+
* the target profile's api_key_ref/tenant_id, deleting `file:` key secrets
|
|
87
|
+
* under ~/.moda. Called by `moda auth logout` — without this, analytics
|
|
88
|
+
* commands keep authenticating as the old tenant after a re-login, because
|
|
89
|
+
* they use the x-api-key plane, not the session token. MODA_API_KEY in the
|
|
90
|
+
* environment still wins at resolve time.
|
|
91
|
+
*
|
|
92
|
+
* The legacy config must be cleared BEFORE reading the profile config:
|
|
93
|
+
* applyLegacyAwareness re-derives api_key_ref onto the prod profile on every
|
|
94
|
+
* load while config.json still holds an api_key.
|
|
95
|
+
*/
|
|
96
|
+
export declare function clearStoredApiKeyCredentials(options?: ProfileIOOptions): boolean;
|
|
83
97
|
export declare function secretFilePathFromRef(reference: SecretReference, options?: ProfileIOOptions): string | undefined;
|
|
84
98
|
export declare function resolveSecretReference(reference: SecretReference | undefined, options?: ProfileIOOptions): string | undefined;
|
|
85
99
|
export declare function resolveApiKeyFromActiveProfile(options?: ProfileIOOptions): string | undefined;
|
package/dist/profiles.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from 'node:fs';
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, isAbsolute, join, normalize, relative } from 'node:path';
|
|
4
4
|
export const PROFILE_CONFIG_VERSION = 1;
|
|
@@ -347,6 +347,44 @@ export function syncLegacyConfigToProfile(legacy, options = {}) {
|
|
|
347
347
|
},
|
|
348
348
|
}, options);
|
|
349
349
|
}
|
|
350
|
+
/**
|
|
351
|
+
* Forget the stored data-plane credentials (`moda_sk_` API key + tenant
|
|
352
|
+
* binding written by `moda init`): the legacy config's api_key/tenant_id and
|
|
353
|
+
* the target profile's api_key_ref/tenant_id, deleting `file:` key secrets
|
|
354
|
+
* under ~/.moda. Called by `moda auth logout` — without this, analytics
|
|
355
|
+
* commands keep authenticating as the old tenant after a re-login, because
|
|
356
|
+
* they use the x-api-key plane, not the session token. MODA_API_KEY in the
|
|
357
|
+
* environment still wins at resolve time.
|
|
358
|
+
*
|
|
359
|
+
* The legacy config must be cleared BEFORE reading the profile config:
|
|
360
|
+
* applyLegacyAwareness re-derives api_key_ref onto the prod profile on every
|
|
361
|
+
* load while config.json still holds an api_key.
|
|
362
|
+
*/
|
|
363
|
+
export function clearStoredApiKeyCredentials(options = {}) {
|
|
364
|
+
let cleared = false;
|
|
365
|
+
const paths = getModaPaths(options);
|
|
366
|
+
const legacy = loadLegacyCliConfig(options);
|
|
367
|
+
if (legacy.api_key || legacy.tenant_id) {
|
|
368
|
+
writeSecretJsonFile(paths.legacyConfigPath, legacy.ingest_url ? { ingest_url: legacy.ingest_url } : {});
|
|
369
|
+
cleared = true;
|
|
370
|
+
}
|
|
371
|
+
const config = loadProfileConfig(options);
|
|
372
|
+
const profileName = options.profile ||
|
|
373
|
+
optionalString(getEnv(options).MODA_PROFILE) ||
|
|
374
|
+
config.active_profile;
|
|
375
|
+
const profile = config.profiles[profileName];
|
|
376
|
+
if (profile && (profile.api_key_ref || profile.tenant_id)) {
|
|
377
|
+
if (profile.api_key_ref?.startsWith('file:')) {
|
|
378
|
+
const secretPath = resolveFileSecretPath(profile.api_key_ref, options);
|
|
379
|
+
if (secretPath)
|
|
380
|
+
rmSync(secretPath, { force: true });
|
|
381
|
+
}
|
|
382
|
+
const { api_key_ref: _key, tenant_id: _tenant, ...rest } = profile;
|
|
383
|
+
saveProfileConfig({ ...config, profiles: { ...config.profiles, [profileName]: rest } }, options);
|
|
384
|
+
cleared = true;
|
|
385
|
+
}
|
|
386
|
+
return cleared;
|
|
387
|
+
}
|
|
350
388
|
function resolveFileSecretPath(reference, options) {
|
|
351
389
|
if (!reference.startsWith('file:'))
|
|
352
390
|
return undefined;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandContext } from './command-context.js';
|
|
2
|
+
import type { ProfileIOOptions } from './profiles.js';
|
|
3
|
+
/**
|
|
4
|
+
* `moda prompts propose` — turn a `prompts ab` run's failures into a revised
|
|
5
|
+
* candidate registered as a new (unlabeled) prompt version. Does NOT promote:
|
|
6
|
+
* the operator gates the candidate with a fresh `prompts ab` (ideally on the
|
|
7
|
+
* printed holdout cases the revision never saw) and promotes only if it wins.
|
|
8
|
+
*/
|
|
9
|
+
export declare function runPromptPropose(positionals: string[], flags: Record<string, string>, profileOptions: ProfileIOOptions, context: CommandContext): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=prompts-propose.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { callControlAPI } from './api-client.js';
|
|
4
|
+
import { validateConfig } from './config.js';
|
|
5
|
+
// Propose is a single OpenRouter revision behind the backend; give it room.
|
|
6
|
+
const PROPOSE_TIMEOUT_MS = 300_000;
|
|
7
|
+
/**
|
|
8
|
+
* `moda prompts propose` — turn a `prompts ab` run's failures into a revised
|
|
9
|
+
* candidate registered as a new (unlabeled) prompt version. Does NOT promote:
|
|
10
|
+
* the operator gates the candidate with a fresh `prompts ab` (ideally on the
|
|
11
|
+
* printed holdout cases the revision never saw) and promotes only if it wins.
|
|
12
|
+
*/
|
|
13
|
+
export async function runPromptPropose(positionals, flags, profileOptions, context) {
|
|
14
|
+
validateConfig();
|
|
15
|
+
const promptKey = flags['prompt-key'] || positionals[1];
|
|
16
|
+
const fromRunId = flags['from-run'] || flags['from-run-id'] || flags['run-id'];
|
|
17
|
+
const replaySetId = flags['set-id'] || flags['replay-set-id'];
|
|
18
|
+
if (!promptKey || !fromRunId || !replaySetId) {
|
|
19
|
+
throw new Error('Usage: moda prompts propose --prompt-key=<key> --from-run=<run_id> --set-id=<replay_set_id> [--max-dossiers=8] [--out=path.prompt.md]');
|
|
20
|
+
}
|
|
21
|
+
const maxDossiers = parsePositiveInt(flags['max-dossiers'], 8, 16);
|
|
22
|
+
const result = (await callControlAPI(`/prompts/${encodeURIComponent(promptKey)}/propose`, { method: 'POST', body: JSON.stringify({ fromRunId, replaySetId, maxDossiers }) }, profileOptions, { timeoutMs: PROPOSE_TIMEOUT_MS, retries: 0 }));
|
|
23
|
+
let outPath;
|
|
24
|
+
if (result.status === 'proposed' && result.content && flags.out) {
|
|
25
|
+
outPath = isAbsolute(flags.out) ? flags.out : resolve(process.cwd(), flags.out);
|
|
26
|
+
writeFileSync(outPath, result.content, 'utf8');
|
|
27
|
+
}
|
|
28
|
+
if (context.outputMode === 'human') {
|
|
29
|
+
printHuman(result, outPath);
|
|
30
|
+
}
|
|
31
|
+
context.output.writeData(outPath ? { ...result, outPath } : result);
|
|
32
|
+
}
|
|
33
|
+
function printHuman(result, outPath) {
|
|
34
|
+
const w = (s) => process.stderr.write(s + '\n');
|
|
35
|
+
if (result.status !== 'proposed') {
|
|
36
|
+
w(`No candidate produced (${result.status}): ${result.note}`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
w(`Proposed candidate for '${result.promptKey}'`);
|
|
40
|
+
w(` new version: ${result.versionId} (unlabeled — not promoted)`);
|
|
41
|
+
w(` base version: ${result.baseVersionId ?? '(none)'}`);
|
|
42
|
+
w(` from run: built from ${result.dossierCount} failure dossier(s)`);
|
|
43
|
+
if (result.changelog)
|
|
44
|
+
w(` changelog: ${result.changelog}`);
|
|
45
|
+
if (result.risks.length)
|
|
46
|
+
w(` risks: ${result.risks.map((r) => `- ${r}`).join('\n ')}`);
|
|
47
|
+
if (outPath)
|
|
48
|
+
w(` written to: ${outPath}`);
|
|
49
|
+
w('');
|
|
50
|
+
w('Next — gate on holdout cases the revision never saw, then promote if it wins:');
|
|
51
|
+
if (result.holdoutCaseIds.length) {
|
|
52
|
+
w(` holdout cases (${result.holdoutCaseIds.length}): ${result.holdoutCaseIds.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
w(' (too few failures to reserve a holdout — gate on a fresh --auto-generate set)');
|
|
56
|
+
}
|
|
57
|
+
if (outPath) {
|
|
58
|
+
// The candidate lives only in the registry as an unlabeled version; `prompts ab`
|
|
59
|
+
// resolves --candidate from local file content, so point it at the written --out
|
|
60
|
+
// file. Using --candidate-key=<key> would resolve to the baseline and compare the
|
|
61
|
+
// prompt against itself.
|
|
62
|
+
w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=${outPath} --auto-generate`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
w(` # re-run with --out=<path.prompt.md> to materialize the candidate, then:`);
|
|
66
|
+
w(` moda prompts ab --baseline-key=${result.promptKey} --candidate=<path.prompt.md> --auto-generate`);
|
|
67
|
+
}
|
|
68
|
+
w(` moda prompts promote ${result.promptKey} --label=prod --version=${result.versionId}`);
|
|
69
|
+
}
|
|
70
|
+
function parsePositiveInt(raw, fallback, max) {
|
|
71
|
+
const n = Number.parseInt(raw ?? '', 10);
|
|
72
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
73
|
+
return fallback;
|
|
74
|
+
return Math.min(n, max);
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=prompts-propose.js.map
|
package/dist/prompts.js
CHANGED
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
|
5
5
|
import { callControlAPI } from './api-client.js';
|
|
6
6
|
import { createCommandContext } from './command-context.js';
|
|
7
7
|
import { runPromptAb } from './prompts-ab.js';
|
|
8
|
+
import { runPromptPropose } from './prompts-propose.js';
|
|
8
9
|
const MODA_DIR = '.moda';
|
|
9
10
|
const MANIFEST_PATH = join(MODA_DIR, 'prompts.yml');
|
|
10
11
|
const LOCK_PATH = join(MODA_DIR, 'prompts.lock.json');
|
|
@@ -55,8 +56,11 @@ export async function runPromptsCommand(input, legacyFlags, legacyProfileOptions
|
|
|
55
56
|
case 'ab':
|
|
56
57
|
await runPromptAb(flags, profileOptions, context);
|
|
57
58
|
break;
|
|
59
|
+
case 'propose':
|
|
60
|
+
await runPromptPropose(positionals, flags, profileOptions, context);
|
|
61
|
+
break;
|
|
58
62
|
default:
|
|
59
|
-
throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, or
|
|
63
|
+
throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, promote, ab, or propose.`);
|
|
60
64
|
}
|
|
61
65
|
}
|
|
62
66
|
export async function initPrompts(options = {}) {
|
package/package.json
CHANGED