@moda-ai/cli 1.10.1 → 1.10.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/skills.js +138 -1
- package/package.json +1 -1
package/dist/skills.js
CHANGED
|
@@ -27,6 +27,7 @@ import { CliInputError } from './errors.js';
|
|
|
27
27
|
const SKILLS_REQUEST_TIMEOUT_MS = 15_000;
|
|
28
28
|
const VALID_STATUSES = new Set(['approved', 'proposed', 'all']);
|
|
29
29
|
const SKILLS_MANIFEST_PATH = join('.moda', 'skills.yml');
|
|
30
|
+
const SKILL_PROPOSALS_MANIFEST_PATH = join('.moda', 'skill-proposals.yml');
|
|
30
31
|
/**
|
|
31
32
|
* Fetch a tenant's generated skills from the ingest worker. Uses the persisted
|
|
32
33
|
* API key + ingest URL (`~/.moda/config.json`), mirroring snapshot auth.
|
|
@@ -342,11 +343,96 @@ function getGitCommit(cwd) {
|
|
|
342
343
|
return undefined;
|
|
343
344
|
}
|
|
344
345
|
}
|
|
346
|
+
async function fetchSkillProposals(flags, profileOptions = {}) {
|
|
347
|
+
const params = new URLSearchParams();
|
|
348
|
+
if (flags['generation-run-id'])
|
|
349
|
+
params.set('generationRunId', flags['generation-run-id']);
|
|
350
|
+
const status = flags.status ?? 'ready_for_pr';
|
|
351
|
+
if (status !== 'all')
|
|
352
|
+
params.set('status', status);
|
|
353
|
+
if (flags['tenant-id'])
|
|
354
|
+
params.set('tenantId', flags['tenant-id']);
|
|
355
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
356
|
+
const response = (await callControlAPI(`/skills/proposals${suffix}`, { method: 'GET' }, profileOptions));
|
|
357
|
+
return Array.isArray(response.proposals) ? response.proposals : [];
|
|
358
|
+
}
|
|
359
|
+
async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
|
|
360
|
+
const cwd = options.cwd ?? process.cwd();
|
|
361
|
+
const params = new URLSearchParams();
|
|
362
|
+
if (flags['tenant-id'])
|
|
363
|
+
params.set('tenantId', flags['tenant-id']);
|
|
364
|
+
const suffix = params.toString() ? `?${params.toString()}` : '';
|
|
365
|
+
const proposal = (await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: 'GET' }, profileOptions));
|
|
366
|
+
if (!proposal.skillMd?.trim()) {
|
|
367
|
+
throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
|
|
368
|
+
}
|
|
369
|
+
const safeId = safeSkillId(proposal.skillId);
|
|
370
|
+
const relativePath = `.claude/skills/${safeId}/SKILL.md`;
|
|
371
|
+
const writeStatus = writeIfChanged(join(cwd, relativePath), proposal.skillMd);
|
|
372
|
+
if (writeStatus === 'failed') {
|
|
373
|
+
throw new Error(`Failed to write ${relativePath}`);
|
|
374
|
+
}
|
|
375
|
+
writeSkillProposalManifest(cwd, proposal, relativePath);
|
|
376
|
+
let acknowledged = true;
|
|
377
|
+
let ackError;
|
|
378
|
+
try {
|
|
379
|
+
await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
|
|
380
|
+
method: 'POST',
|
|
381
|
+
body: JSON.stringify({ appliedBy: 'moda-cli' }),
|
|
382
|
+
}, profileOptions);
|
|
383
|
+
}
|
|
384
|
+
catch (err) {
|
|
385
|
+
acknowledged = false;
|
|
386
|
+
ackError = err instanceof Error ? err.message : String(err);
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
proposalId,
|
|
390
|
+
skillId: proposal.skillId,
|
|
391
|
+
path: relativePath,
|
|
392
|
+
status: writeStatus,
|
|
393
|
+
acknowledged,
|
|
394
|
+
ackError,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function safeSkillId(skillId) {
|
|
398
|
+
const safeId = basename(skillId || '');
|
|
399
|
+
if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
|
|
400
|
+
throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
|
|
401
|
+
}
|
|
402
|
+
return safeId;
|
|
403
|
+
}
|
|
404
|
+
function writeSkillProposalManifest(cwd, proposal, relativePath) {
|
|
405
|
+
const manifestPath = join(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
|
|
406
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
407
|
+
const existing = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : 'applied_proposals:\n';
|
|
408
|
+
const entry = [
|
|
409
|
+
` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
|
|
410
|
+
` skill_id: "${escapeYamlString(proposal.skillId)}"`,
|
|
411
|
+
` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
|
|
412
|
+
` skill_version: ${Number(proposal.skillVersion || 0)}`,
|
|
413
|
+
` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
|
|
414
|
+
` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
|
|
415
|
+
` topic_id: "${escapeYamlString(proposal.topicId)}"`,
|
|
416
|
+
` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
|
|
417
|
+
` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
|
|
418
|
+
` path: "${escapeYamlString(relativePath)}"`,
|
|
419
|
+
` applied_at: "${new Date().toISOString()}"`,
|
|
420
|
+
].join('\n');
|
|
421
|
+
const prefix = existing.endsWith('\n') ? existing : `${existing}\n`;
|
|
422
|
+
writeFileSync(manifestPath, `${prefix}${entry}\n`);
|
|
423
|
+
}
|
|
424
|
+
function escapeYamlString(value) {
|
|
425
|
+
return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
426
|
+
}
|
|
427
|
+
function formatProposalRate(value) {
|
|
428
|
+
return `${(Number(value || 0) * 100).toFixed(1)}%`;
|
|
429
|
+
}
|
|
345
430
|
/**
|
|
346
431
|
* `moda skills <subcommand>` dispatcher.
|
|
347
432
|
*/
|
|
348
433
|
export async function runSkillsCommand(args) {
|
|
349
434
|
const sub = args.positional;
|
|
435
|
+
const positionals = args.positionals ?? (sub ? [sub] : []);
|
|
350
436
|
if (sub === 'gen') {
|
|
351
437
|
const result = await generateSkills(args.flags);
|
|
352
438
|
if (args.flags.json === 'true') {
|
|
@@ -371,13 +457,63 @@ export async function runSkillsCommand(args) {
|
|
|
371
457
|
}
|
|
372
458
|
return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
|
|
373
459
|
}
|
|
460
|
+
if (sub === 'proposals' && positionals[1] === 'list') {
|
|
461
|
+
const proposals = await fetchSkillProposals(args.flags, { profile: args.flags.profile });
|
|
462
|
+
if (args.flags.json === 'true') {
|
|
463
|
+
console.log(JSON.stringify({ proposals }));
|
|
464
|
+
}
|
|
465
|
+
else if (proposals.length === 0) {
|
|
466
|
+
console.log('No skill proposals found.');
|
|
467
|
+
}
|
|
468
|
+
else {
|
|
469
|
+
for (const proposal of proposals) {
|
|
470
|
+
console.log([
|
|
471
|
+
proposal.proposalId,
|
|
472
|
+
proposal.proposalStatus,
|
|
473
|
+
`${proposal.skillId}@v${proposal.skillVersion}`,
|
|
474
|
+
`delta=${formatProposalRate(proposal.deltaPassRate)}`,
|
|
475
|
+
`baseline=${formatProposalRate(proposal.baselinePassRate)}`,
|
|
476
|
+
`candidate=${formatProposalRate(proposal.candidatePassRate)}`,
|
|
477
|
+
proposal.name,
|
|
478
|
+
].filter(Boolean).join('\t'));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return 0;
|
|
482
|
+
}
|
|
483
|
+
if (sub === 'proposal' && positionals[1] === 'apply') {
|
|
484
|
+
const proposalId = positionals[2] || args.flags['proposal-id'];
|
|
485
|
+
if (!proposalId) {
|
|
486
|
+
throw new CliInputError('A proposal id is required', 'Usage: moda skills proposal apply <proposal-id>');
|
|
487
|
+
}
|
|
488
|
+
const result = await applySkillProposal(proposalId, args.flags, { profile: args.flags.profile });
|
|
489
|
+
if (args.flags.json === 'true') {
|
|
490
|
+
console.log(JSON.stringify(result));
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
console.log([
|
|
494
|
+
`${result.status} ${result.path}`,
|
|
495
|
+
result.acknowledged ? 'acknowledged: true' : 'acknowledged: false',
|
|
496
|
+
].join('\n'));
|
|
497
|
+
}
|
|
498
|
+
if (!result.acknowledged) {
|
|
499
|
+
// The local SKILL.md was written, but the server still lists this
|
|
500
|
+
// proposal as ready_for_pr. Warn (to stderr so JSON stdout stays clean)
|
|
501
|
+
// and exit non-zero so the failed acknowledgement isn't silently treated
|
|
502
|
+
// as success — re-running `apply` re-attempts the acknowledgement.
|
|
503
|
+
console.error(`Warning: wrote ${result.path} but failed to acknowledge proposal ${proposalId} on the server` +
|
|
504
|
+
(result.ackError ? ` (${result.ackError})` : '') +
|
|
505
|
+
'. The proposal is still listed as ready — re-run `moda skills proposal apply` to acknowledge it.');
|
|
506
|
+
return 1;
|
|
507
|
+
}
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
374
510
|
if (sub === 'sync') {
|
|
375
511
|
const result = await runSkillSync(args.flags, { profile: args.flags.profile });
|
|
376
512
|
console.log(JSON.stringify({ synced: result.synced, dryRun: result.dryRun }));
|
|
377
513
|
return 0;
|
|
378
514
|
}
|
|
379
515
|
if (sub !== 'pull') {
|
|
380
|
-
throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
|
|
516
|
+
throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
|
|
381
517
|
}
|
|
382
518
|
const status = (args.flags.status ?? 'approved').toLowerCase();
|
|
383
519
|
if (!VALID_STATUSES.has(status)) {
|
|
@@ -418,6 +554,7 @@ async function generateSkills(flags) {
|
|
|
418
554
|
start_at: flags['start-at'] || undefined,
|
|
419
555
|
end_at: flags['end-at'] || undefined,
|
|
420
556
|
clustering_run_id: flags['clustering-run-id'] || undefined,
|
|
557
|
+
replay_set_id: flags['replay-set-id'] || flags['eval-set-id'] || undefined,
|
|
421
558
|
run_clustering: flags['no-clustering'] !== 'true',
|
|
422
559
|
source: normalizeSkillSource(flags.source),
|
|
423
560
|
clustering_timeout_seconds: parsePositiveInt(flags['clustering-timeout-seconds'], 0),
|