@moda-ai/cli 1.22.0 → 1.22.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.
Files changed (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -777
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. package/dist/workspace.js +0 -1294
package/dist/skills.js DELETED
@@ -1,777 +0,0 @@
1
- // src/skills.ts — `moda skills` command: pull tenant-generated skills locally.
2
- //
3
- // The Moda skill-creation pipeline generates per-tenant SKILL.md files
4
- // server-side (moda-skill-harness) and stores their full text in ClickHouse.
5
- // `moda skills gen` kicks off server-side skill generation through the ingest
6
- // worker, which validates the user's Bearer API key before forwarding to the
7
- // internal harness. `moda skills pull` fetches generated skills via
8
- // `GET /v1/skills` (authenticated with the same Bearer API key used for
9
- // snapshots) and writes each into the user's coding-agent skill directory so
10
- // the agent can use them:
11
- //
12
- // .claude/skills/<skill_id>/SKILL.md (Claude Code, canonical format)
13
- // .cursor/rules/<skill_id>.mdc (Cursor, when a .cursor dir exists)
14
- //
15
- // Skills are managed artifacts. A pull installs the latest server-published
16
- // generation snapshot, tracked in .moda/skills.yml, and prunes only files that a
17
- // previous Moda pull wrote.
18
- import { execFileSync } from 'node:child_process';
19
- import { createHash } from 'node:crypto';
20
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
21
- import { basename, dirname, join, relative } from 'node:path';
22
- import { callControlAPI } from './api-client.js';
23
- import { createCommandContext } from './command-context.js';
24
- import { resolveApiKey, resolveIngestUrl, resolveTenantId } from './config.js';
25
- import { claudeSkillToCursorRule } from './init/rules.js';
26
- import { detectAgents } from './init/mcp.js';
27
- import { SKILL_SOURCE_HARNESS, SKILL_SOURCE_INTEGRATION_BUNDLED, pruneManagedSkillPaths, readSkillsManifest, writeSkillsManifest, } from './skills-manifest.js';
28
- import { CliInputError } from './errors.js';
29
- const SKILLS_REQUEST_TIMEOUT_MS = 15_000;
30
- const VALID_STATUSES = new Set(['approved', 'proposed', 'all']);
31
- const SKILL_PROPOSALS_MANIFEST_PATH = join('.moda', 'skill-proposals.yml');
32
- /**
33
- * Fetch a tenant's generated skills from the ingest worker. Uses the persisted
34
- * API key + ingest URL (`~/.moda/config.json`), mirroring snapshot auth.
35
- */
36
- async function fetchSkills(status) {
37
- const apiKey = resolveApiKey();
38
- if (!apiKey) {
39
- throw new CliInputError('No Moda API key found. Run `moda init` first (or set MODA_API_KEY).');
40
- }
41
- const ingestUrl = resolveIngestUrl().replace(/\/$/, '');
42
- const url = `${ingestUrl}/v1/skills?status=${encodeURIComponent(status)}`;
43
- const controller = new AbortController();
44
- const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
45
- let response;
46
- try {
47
- response = await fetch(url, {
48
- headers: { Authorization: `Bearer ${apiKey}` },
49
- signal: controller.signal,
50
- });
51
- }
52
- catch (err) {
53
- const message = err instanceof Error ? err.message : String(err);
54
- throw new Error(`Network error: failed to reach ${ingestUrl} (${message})`);
55
- }
56
- finally {
57
- clearTimeout(timeout);
58
- }
59
- if (!response.ok) {
60
- const body = await response.text().catch(() => '');
61
- throw new Error(`Failed to fetch skills (HTTP ${response.status}): ${body.slice(0, 200)}`);
62
- }
63
- const data = (await response.json());
64
- if (!data.success) {
65
- throw new Error(data.message || 'Failed to fetch skills');
66
- }
67
- const skills = Array.isArray(data.skills) ? data.skills : [];
68
- const release = data.release && typeof data.release === 'object' ? data.release : null;
69
- return {
70
- skills,
71
- tenantId: data.tenant_id || resolveTenantId() || '',
72
- release,
73
- releaseKey: releaseKeyFor(release, skills, status),
74
- status,
75
- };
76
- }
77
- /**
78
- * Authenticated GET against the ingest worker's `/v1/skills/runs` surface.
79
- * Returns `{ status, body }` so callers can treat 404 (no such run) as data;
80
- * every other non-2xx throws.
81
- */
82
- async function skillRunsRequest(url) {
83
- const apiKey = resolveApiKey();
84
- if (!apiKey) {
85
- throw new CliInputError('No Moda API key found. Run `moda init` first (or set MODA_API_KEY).');
86
- }
87
- const controller = new AbortController();
88
- const timeout = setTimeout(() => controller.abort(), SKILLS_REQUEST_TIMEOUT_MS);
89
- let response;
90
- try {
91
- response = await fetch(url, {
92
- headers: { Authorization: `Bearer ${apiKey}` },
93
- signal: controller.signal,
94
- });
95
- }
96
- catch (err) {
97
- const message = err instanceof Error ? err.message : String(err);
98
- throw new Error(`Network error: failed to reach ${url} (${message})`);
99
- }
100
- finally {
101
- clearTimeout(timeout);
102
- }
103
- const text = await response.text().catch(() => '');
104
- if (!response.ok && response.status !== 404) {
105
- throw new Error(`Failed to fetch run status (HTTP ${response.status}): ${text.slice(0, 200)}`);
106
- }
107
- let body = {};
108
- try {
109
- body = text ? JSON.parse(text) : {};
110
- }
111
- catch {
112
- body = {};
113
- }
114
- return { status: response.status, body };
115
- }
116
- /**
117
- * Resolve a run's status + recent events. Without a run id, the tenant's
118
- * latest run (`GET /v1/skills/runs?limit=1`) is used; the detail form
119
- * (`GET /v1/skills/runs/:id`) always supplies the event breadcrumbs.
120
- */
121
- async function fetchSkillRunStatus(runId) {
122
- const ingestUrl = resolveIngestUrl().replace(/\/$/, '');
123
- let resolvedRunId = runId;
124
- if (!resolvedRunId) {
125
- const list = await skillRunsRequest(`${ingestUrl}/v1/skills/runs?limit=1`);
126
- const listBody = list.body;
127
- const latest = Array.isArray(listBody.runs) ? listBody.runs[0] : undefined;
128
- if (!latest)
129
- return { run: null, events: [] };
130
- resolvedRunId = latest.generation_run_id;
131
- // Defensive: a row without an id can still be shown, just without events.
132
- if (!resolvedRunId)
133
- return { run: latest, events: [] };
134
- }
135
- const detail = await skillRunsRequest(`${ingestUrl}/v1/skills/runs/${encodeURIComponent(resolvedRunId)}`);
136
- if (detail.status === 404) {
137
- if (runId) {
138
- throw new CliInputError(`No skill generation run found with id '${runId}'`);
139
- }
140
- return { run: null, events: [] };
141
- }
142
- const detailBody = detail.body;
143
- return {
144
- run: detailBody.run ?? null,
145
- events: Array.isArray(detailBody.events) ? detailBody.events : [],
146
- };
147
- }
148
- const STATUS_HUMAN_EVENT_LIMIT = 10;
149
- /** Human rendering for `moda skills status` (key: value lines, gen-style). */
150
- function formatRunStatusHuman(run, events) {
151
- const counters = run.counters ?? {};
152
- const lines = [
153
- run.generation_run_id && `run: ${run.generation_run_id}`,
154
- `status: ${run.status || 'unknown'}`,
155
- run.current_stage && `stage: ${run.current_stage}`,
156
- run.outcome && `outcome: ${run.outcome}`,
157
- `processed: ${Number(counters.sessions_processed ?? 0)}`,
158
- `failed: ${Number(counters.sessions_failed ?? 0)}`,
159
- Number(counters.distilled_skills_generated ?? 0) > 0
160
- && `distilled: ${Number(counters.distilled_skills_generated ?? 0)}`,
161
- Number(counters.skills_indexed ?? 0) > 0
162
- && `indexed: ${Number(counters.skills_indexed ?? 0)}`,
163
- (run.generated_skill_ids?.length ?? 0) > 0
164
- && `skills: ${(run.generated_skill_ids ?? []).join(', ')}`,
165
- run.error && `error: ${run.error}`,
166
- run.started_at && `started: ${run.started_at}`,
167
- run.updated_at && `updated: ${run.updated_at}`,
168
- ].filter(Boolean);
169
- if (events.length > 0) {
170
- lines.push('recent events (newest first):');
171
- for (const event of events.slice(0, STATUS_HUMAN_EVENT_LIMIT)) {
172
- lines.push(` ${[event.created_at, event.level, event.stage, event.message]
173
- .filter(Boolean)
174
- .join(' ')}`);
175
- }
176
- }
177
- return lines.join('\n');
178
- }
179
- /**
180
- * Write `content` to `filePath`, creating parent dirs. Returns whether the
181
- * file was created, updated (content differed), or left unchanged.
182
- */
183
- function writeIfChanged(filePath, content) {
184
- try {
185
- mkdirSync(dirname(filePath), { recursive: true });
186
- if (existsSync(filePath)) {
187
- const current = readFileSync(filePath, 'utf8');
188
- if (current === content)
189
- return 'unchanged';
190
- writeFileSync(filePath, content);
191
- return 'updated';
192
- }
193
- writeFileSync(filePath, content);
194
- return 'wrote';
195
- }
196
- catch {
197
- return 'failed';
198
- }
199
- }
200
- /**
201
- * Pull generated skills into the detected coding agents' skill directories.
202
- * Exported so `moda init` can run it as a setup step. Best-effort and quiet on
203
- * an empty store; never throws for "no skills yet".
204
- */
205
- export async function pullSkills(options = {}) {
206
- const status = options.status ?? 'approved';
207
- const cwd = options.cwd ?? process.cwd();
208
- const log = (msg) => {
209
- if (!options.silent)
210
- console.log(msg);
211
- };
212
- const fetched = await fetchSkills(status);
213
- const { skills } = fetched;
214
- const summary = {
215
- total: skills.length,
216
- pruned: 0,
217
- wrote: 0,
218
- updated: 0,
219
- unchanged: 0,
220
- failed: 0,
221
- };
222
- if (skills.length === 0) {
223
- log(' No skills to pull yet.');
224
- return summary;
225
- }
226
- const previousManifest = readSkillsManifest(cwd);
227
- // A release change only invalidates harness-generated skills. Paths owned
228
- // by integration-bundled records are CLI-managed and must never be pruned
229
- // by a harness release change (cross-source regression guard).
230
- const preservedIntegrationRecords = (previousManifest?.records ?? []).filter((record) => record.source === SKILL_SOURCE_INTEGRATION_BUNDLED);
231
- if (previousManifest &&
232
- previousManifest.managedPaths.length > 0 &&
233
- previousManifest.releaseKey !== fetched.releaseKey) {
234
- const integrationPaths = new Set(preservedIntegrationRecords.flatMap((record) => record.paths));
235
- const harnessPaths = previousManifest.managedPaths.filter((path) => !integrationPaths.has(path));
236
- if (harnessPaths.length > 0) {
237
- summary.pruned = pruneManagedSkillPaths(cwd, harnessPaths, log);
238
- }
239
- }
240
- // Always target Claude Code (canonical SKILL.md). Also write Cursor rules
241
- // when the project has a .cursor directory.
242
- const agents = detectAgents();
243
- const writeCursor = agents.includes('Cursor') || existsSync(join(cwd, '.cursor'));
244
- const records = [];
245
- for (const skill of skills) {
246
- if (!skill.skill_id || !skill.skill_md) {
247
- summary.failed++;
248
- continue;
249
- }
250
- // Path-traversal guard: the API response is trusted but defense-in-depth
251
- // for a compromised / MITM'd ingest endpoint. `basename` strips any
252
- // leading directory components, and the allowlist rejects `..`, `/`,
253
- // `\`, null bytes, and anything else outside a plain identifier so the
254
- // resolved path cannot escape the per-skill directory.
255
- const safeId = basename(skill.skill_id);
256
- if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
257
- summary.failed++;
258
- log(` skipped invalid skill_id: ${JSON.stringify(skill.skill_id).slice(0, 80)}`);
259
- continue;
260
- }
261
- const claudePath = join(cwd, '.claude', 'skills', safeId, 'SKILL.md');
262
- const claudeStatus = writeIfChanged(claudePath, skill.skill_md);
263
- const claudeRelativePath = `.claude/skills/${safeId}/SKILL.md`;
264
- recordOutcome(summary, claudeStatus, log, claudeRelativePath);
265
- const paths = [];
266
- if (claudeStatus !== 'failed')
267
- paths.push(claudeRelativePath);
268
- if (writeCursor) {
269
- const cursorPath = join(cwd, '.cursor', 'rules', `${safeId}.mdc`);
270
- const cursorStatus = writeIfChanged(cursorPath, claudeSkillToCursorRule(skill.skill_md));
271
- const cursorRelativePath = `.cursor/rules/${safeId}.mdc`;
272
- recordOutcome(summary, cursorStatus, log, cursorRelativePath);
273
- if (cursorStatus !== 'failed')
274
- paths.push(cursorRelativePath);
275
- }
276
- if (paths.length > 0) {
277
- records.push({
278
- skillId: safeId,
279
- contentHash: sha256(skill.skill_md),
280
- source: SKILL_SOURCE_HARNESS,
281
- paths,
282
- });
283
- }
284
- }
285
- writeSkillsManifest(cwd, manifestHeaderFor(fetched), [...records, ...preservedIntegrationRecords]);
286
- return summary;
287
- }
288
- function recordOutcome(summary, status, log, relativePath) {
289
- switch (status) {
290
- case 'wrote':
291
- summary.wrote++;
292
- log(` wrote ${relativePath}`);
293
- break;
294
- case 'updated':
295
- summary.updated++;
296
- log(` updated ${relativePath}`);
297
- break;
298
- case 'unchanged':
299
- summary.unchanged++;
300
- break;
301
- case 'failed':
302
- summary.failed++;
303
- log(` failed ${relativePath}`);
304
- break;
305
- }
306
- }
307
- const SKILLS_DIR = join('.claude', 'skills');
308
- const SKILL_FILE = 'SKILL.md';
309
- const MAX_SKILL_WALK_DEPTH = 4;
310
- /**
311
- * Push locally-authored `.claude/skills/**\/SKILL.md` files up to Moda via
312
- * `POST /api/skills/sync`. Modeled on `runPromptSync`: returns the synced list
313
- * without printing so callers (e.g. `moda init`) control their own output.
314
- * `moda skills sync` wraps this with JSON output.
315
- *
316
- * Skills that Moda itself manages — those previously pulled/generated (tracked
317
- * in `.moda/skills.yml`) and the shipped `moda-cli` skill — are excluded so we
318
- * only push skills the user authored.
319
- */
320
- export async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
321
- const cwd = options.cwd ?? process.cwd();
322
- const dryRun = flags['dry-run'] === 'true';
323
- const skills = discoverLocalSkills(cwd);
324
- if (skills.length === 0) {
325
- return { synced: [], dryRun };
326
- }
327
- const sourceCommit = getGitCommit(cwd);
328
- const response = (await callControlAPI('/skills/sync', {
329
- method: 'POST',
330
- body: JSON.stringify({
331
- skills: skills.map((skill) => ({
332
- key: skill.key,
333
- name: skill.name,
334
- description: skill.description,
335
- skillMd: skill.skillMd,
336
- sourcePath: skill.sourcePath,
337
- contentHash: skill.contentHash,
338
- sourceCommit,
339
- })),
340
- sourceCommit,
341
- dryRun,
342
- }),
343
- }, profileOptions));
344
- return { synced: response.synced ?? [], dryRun };
345
- }
346
- /**
347
- * Discover user-authored skills under `.claude/skills/`. Excludes Moda-managed
348
- * skills (the `.moda/skills.yml` managed paths and the shipped `moda-cli`
349
- * skill) and validates each directory key.
350
- */
351
- function discoverLocalSkills(cwd) {
352
- const skillsRoot = join(cwd, SKILLS_DIR);
353
- if (!existsSync(skillsRoot))
354
- return [];
355
- const managed = new Set(managedSkillPaths(cwd));
356
- const skills = [];
357
- for (const absPath of walkSkillFiles(skillsRoot, 0)) {
358
- const sourcePath = relative(cwd, absPath).replace(/\\/g, '/');
359
- if (managed.has(sourcePath))
360
- continue;
361
- const key = basename(dirname(absPath));
362
- // Never re-upload Moda's own shipped skill; only accept plain identifiers.
363
- if (key === 'moda-cli' || !/^[A-Za-z0-9_-]+$/.test(key))
364
- continue;
365
- let content;
366
- try {
367
- content = readFileSync(absPath, 'utf8');
368
- }
369
- catch {
370
- continue;
371
- }
372
- if (!content.trim())
373
- continue;
374
- const { name, description } = parseSkillFrontmatter(content, key);
375
- skills.push({
376
- key,
377
- name,
378
- description,
379
- skillMd: content,
380
- sourcePath,
381
- contentHash: sha256(content),
382
- });
383
- }
384
- return skills.sort((a, b) => a.key.localeCompare(b.key));
385
- }
386
- /** Managed SKILL.md paths recorded by a previous `moda skills pull`. */
387
- function managedSkillPaths(cwd) {
388
- const manifest = readSkillsManifest(cwd);
389
- if (!manifest)
390
- return [];
391
- return manifest.managedPaths.filter((path) => path.startsWith('.claude/skills/') && path.endsWith('/SKILL.md'));
392
- }
393
- /** Recursively find `SKILL.md` files, never following symlinks. */
394
- function walkSkillFiles(dir, depth) {
395
- if (depth > MAX_SKILL_WALK_DEPTH)
396
- return [];
397
- let entries;
398
- try {
399
- entries = readdirSync(dir);
400
- }
401
- catch {
402
- return [];
403
- }
404
- const files = [];
405
- for (const entry of entries) {
406
- const fullPath = join(dir, entry);
407
- let stat;
408
- try {
409
- stat = lstatSync(fullPath);
410
- }
411
- catch {
412
- continue;
413
- }
414
- if (stat.isSymbolicLink())
415
- continue;
416
- if (stat.isDirectory()) {
417
- files.push(...walkSkillFiles(fullPath, depth + 1));
418
- }
419
- else if (stat.isFile() && entry === SKILL_FILE) {
420
- files.push(fullPath);
421
- }
422
- }
423
- return files;
424
- }
425
- /** Pull `name`/`description` from a SKILL.md YAML frontmatter block. */
426
- function parseSkillFrontmatter(raw, fallbackKey) {
427
- let name = fallbackKey;
428
- let description;
429
- if (raw.startsWith('---')) {
430
- const end = raw.indexOf('\n---', 3);
431
- if (end !== -1) {
432
- const frontmatter = raw.slice(3, end);
433
- const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
434
- const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
435
- if (nameMatch)
436
- name = stripQuotes(nameMatch[1].trim()) || fallbackKey;
437
- if (descMatch)
438
- description = stripQuotes(descMatch[1].trim()) || undefined;
439
- }
440
- }
441
- return { name, description };
442
- }
443
- function stripQuotes(value) {
444
- return value.replace(/^["']|["']$/g, '');
445
- }
446
- function getGitCommit(cwd) {
447
- try {
448
- return execFileSync('git', ['rev-parse', 'HEAD'], {
449
- cwd,
450
- encoding: 'utf8',
451
- stdio: ['ignore', 'pipe', 'ignore'],
452
- }).trim();
453
- }
454
- catch {
455
- return undefined;
456
- }
457
- }
458
- async function fetchSkillProposals(flags, profileOptions = {}) {
459
- const params = new URLSearchParams();
460
- if (flags['generation-run-id'])
461
- params.set('generationRunId', flags['generation-run-id']);
462
- const status = flags.status ?? 'ready_for_pr';
463
- if (status !== 'all')
464
- params.set('status', status);
465
- if (flags['tenant-id'])
466
- params.set('tenantId', flags['tenant-id']);
467
- const suffix = params.toString() ? `?${params.toString()}` : '';
468
- const response = (await callControlAPI(`/skills/proposals${suffix}`, { method: 'GET' }, profileOptions));
469
- return Array.isArray(response.proposals) ? response.proposals : [];
470
- }
471
- async function applySkillProposal(proposalId, flags, profileOptions = {}, options = {}) {
472
- const cwd = options.cwd ?? process.cwd();
473
- const params = new URLSearchParams();
474
- if (flags['tenant-id'])
475
- params.set('tenantId', flags['tenant-id']);
476
- const suffix = params.toString() ? `?${params.toString()}` : '';
477
- const proposal = (await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}${suffix}`, { method: 'GET' }, profileOptions));
478
- if (!proposal.skillMd?.trim()) {
479
- throw new CliInputError(`Proposal ${proposalId} does not include SKILL.md content`);
480
- }
481
- const safeId = safeSkillId(proposal.skillId);
482
- const relativePath = `.claude/skills/${safeId}/SKILL.md`;
483
- const writeStatus = writeIfChanged(join(cwd, relativePath), proposal.skillMd);
484
- if (writeStatus === 'failed') {
485
- throw new Error(`Failed to write ${relativePath}`);
486
- }
487
- writeSkillProposalManifest(cwd, proposal, relativePath);
488
- let acknowledged = true;
489
- let ackError;
490
- try {
491
- await callControlAPI(`/skills/proposals/${encodeURIComponent(proposalId)}/mark-applied${suffix}`, {
492
- method: 'POST',
493
- body: JSON.stringify({ appliedBy: 'moda-cli' }),
494
- }, profileOptions);
495
- }
496
- catch (err) {
497
- acknowledged = false;
498
- ackError = err instanceof Error ? err.message : String(err);
499
- }
500
- return {
501
- proposalId,
502
- skillId: proposal.skillId,
503
- path: relativePath,
504
- status: writeStatus,
505
- acknowledged,
506
- ackError,
507
- };
508
- }
509
- function safeSkillId(skillId) {
510
- const safeId = basename(skillId || '');
511
- if (!safeId || !/^[A-Za-z0-9_-]+$/.test(safeId)) {
512
- throw new CliInputError(`Invalid skill_id in proposal: ${JSON.stringify(skillId).slice(0, 80)}`);
513
- }
514
- return safeId;
515
- }
516
- function writeSkillProposalManifest(cwd, proposal, relativePath) {
517
- const manifestPath = join(cwd, SKILL_PROPOSALS_MANIFEST_PATH);
518
- mkdirSync(dirname(manifestPath), { recursive: true });
519
- const existing = existsSync(manifestPath) ? readFileSync(manifestPath, 'utf8') : 'applied_proposals:\n';
520
- const entry = [
521
- ` - proposal_id: "${escapeYamlString(proposal.proposalId)}"`,
522
- ` skill_id: "${escapeYamlString(proposal.skillId)}"`,
523
- ` skill_family_id: "${escapeYamlString(proposal.skillFamilyId)}"`,
524
- ` skill_version: ${Number(proposal.skillVersion || 0)}`,
525
- ` generation_run_id: "${escapeYamlString(proposal.generationRunId)}"`,
526
- ` clustering_run_id: "${escapeYamlString(proposal.clusteringRunId)}"`,
527
- ` topic_id: "${escapeYamlString(proposal.topicId)}"`,
528
- ` replay_set_id: "${escapeYamlString(proposal.replaySetId)}"`,
529
- ` delta_pass_rate: ${Number(proposal.deltaPassRate || 0)}`,
530
- ` path: "${escapeYamlString(relativePath)}"`,
531
- ` applied_at: "${new Date().toISOString()}"`,
532
- ].join('\n');
533
- const prefix = existing.endsWith('\n') ? existing : `${existing}\n`;
534
- writeFileSync(manifestPath, `${prefix}${entry}\n`);
535
- }
536
- function escapeYamlString(value) {
537
- return String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
538
- }
539
- function formatProposalRate(value) {
540
- return `${(Number(value || 0) * 100).toFixed(1)}%`;
541
- }
542
- export async function runSkillsCommand(input) {
543
- const context = 'output' in input
544
- ? input
545
- : createCommandContext({
546
- command: input.command,
547
- positional: input.positional,
548
- positionals: input.positionals,
549
- flags: input.flags,
550
- }, {
551
- outputMode: input.flags.agent === 'true' ? 'agent' : input.flags.json === 'true' ? 'json' : 'human',
552
- });
553
- const sub = context.positional;
554
- const positionals = context.positionals ?? (sub ? [sub] : []);
555
- const flags = context.flags;
556
- if (sub === 'gen') {
557
- const result = await generateSkills(flags);
558
- if (context.outputMode !== 'human') {
559
- context.output.writeData(result, {
560
- status: Number(result.sessions_failed ?? 0) > 0 ? 'error' : 'success',
561
- });
562
- }
563
- else if (result.accepted) {
564
- console.log([
565
- 'accepted: true',
566
- result.generation_run_id && `generation_run_id: ${result.generation_run_id}`,
567
- result.message && `message: ${result.message}`,
568
- result.generation_run_id
569
- && `follow progress: moda skills status ${result.generation_run_id}`,
570
- ].filter(Boolean).join('\n'));
571
- }
572
- else {
573
- console.log([
574
- `processed: ${result.sessions_processed ?? 0}`,
575
- `failed: ${result.sessions_failed ?? 0}`,
576
- `pending: ${result.sessions_pending ?? 0}`,
577
- `pending_found: ${result.pending_sessions_found ?? 0}`,
578
- `distilled: ${Number(result.cluster_result?.distilled_skills_generated ?? 0)}`,
579
- `indexed: ${Number(result.cluster_result?.skills_indexed ?? 0)}`,
580
- result.distillation_skipped
581
- && `distillation skipped: ${result.distillation_skip_reason || 'unknown'}`,
582
- ].filter(Boolean).join('\n'));
583
- }
584
- return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
585
- }
586
- if (sub === 'status') {
587
- const runId = positionals[1] || flags['run-id'] || undefined;
588
- const { run, events } = await fetchSkillRunStatus(runId);
589
- if (context.outputMode !== 'human') {
590
- context.output.writeData({ run, events }, { status: run?.status === 'failed' ? 'error' : 'success' });
591
- }
592
- else if (!run) {
593
- console.log('No skill generation runs found yet. Start one with `moda skills gen`.');
594
- }
595
- else {
596
- console.log(formatRunStatusHuman(run, events));
597
- }
598
- return run?.status === 'failed' ? 1 : 0;
599
- }
600
- if (sub === 'proposals' && positionals[1] === 'list') {
601
- const proposals = await fetchSkillProposals(flags, { profile: context.profile });
602
- if (flags.json === 'true') {
603
- console.log(JSON.stringify({ proposals }));
604
- }
605
- else if (proposals.length === 0) {
606
- console.log('No skill proposals found.');
607
- }
608
- else {
609
- for (const proposal of proposals) {
610
- console.log([
611
- proposal.proposalId,
612
- proposal.proposalStatus,
613
- `${proposal.skillId}@v${proposal.skillVersion}`,
614
- `delta=${formatProposalRate(proposal.deltaPassRate)}`,
615
- `baseline=${formatProposalRate(proposal.baselinePassRate)}`,
616
- `candidate=${formatProposalRate(proposal.candidatePassRate)}`,
617
- proposal.name,
618
- ].filter(Boolean).join('\t'));
619
- }
620
- }
621
- return 0;
622
- }
623
- if (sub === 'proposal' && positionals[1] === 'apply') {
624
- const proposalId = positionals[2] || flags['proposal-id'];
625
- if (!proposalId) {
626
- throw new CliInputError('A proposal id is required', 'Usage: moda skills proposal apply <proposal-id>');
627
- }
628
- const result = await applySkillProposal(proposalId, flags, { profile: context.profile });
629
- if (flags.json === 'true') {
630
- console.log(JSON.stringify(result));
631
- }
632
- else {
633
- console.log([
634
- `${result.status} ${result.path}`,
635
- result.acknowledged ? 'acknowledged: true' : 'acknowledged: false',
636
- ].join('\n'));
637
- }
638
- if (!result.acknowledged) {
639
- // The local SKILL.md was written, but the server still lists this
640
- // proposal as ready_for_pr. Warn (to stderr so JSON stdout stays clean)
641
- // and exit non-zero so the failed acknowledgement isn't silently treated
642
- // as success — re-running `apply` re-attempts the acknowledgement.
643
- console.error(`Warning: wrote ${result.path} but failed to acknowledge proposal ${proposalId} on the server` +
644
- (result.ackError ? ` (${result.ackError})` : '') +
645
- '. The proposal is still listed as ready — re-run `moda skills proposal apply` to acknowledge it.');
646
- return 1;
647
- }
648
- return 0;
649
- }
650
- if (sub === 'sync') {
651
- const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
652
- context.output.writeData({ synced: result.synced, dryRun: result.dryRun });
653
- return 0;
654
- }
655
- if (sub !== 'pull') {
656
- 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 status [run-id] | 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]');
657
- }
658
- const status = (flags.status ?? 'approved').toLowerCase();
659
- if (!VALID_STATUSES.has(status)) {
660
- throw new CliInputError(`Invalid --status '${status}'`, `Must be one of: ${[...VALID_STATUSES].join(', ')}`);
661
- }
662
- const summary = await pullSkills({ status, silent: context.outputMode !== 'human' });
663
- const parts = [
664
- summary.pruned && `${summary.pruned} pruned`,
665
- summary.wrote && `${summary.wrote} written`,
666
- summary.updated && `${summary.updated} updated`,
667
- summary.unchanged && `${summary.unchanged} unchanged`,
668
- summary.failed && `${summary.failed} failed`,
669
- ].filter(Boolean);
670
- if (context.outputMode === 'human' && summary.total > 0) {
671
- console.log(` Done — ${parts.join(', ') || 'no changes'}`);
672
- }
673
- if (context.outputMode !== 'human') {
674
- context.output.writeData({ status, summary });
675
- }
676
- return summary.failed > 0 ? 1 : 0;
677
- }
678
- async function generateSkills(flags) {
679
- const apiKey = resolveApiKey();
680
- if (!apiKey) {
681
- throw new CliInputError('No Moda API key found. Run `moda init` first (or set MODA_API_KEY).');
682
- }
683
- const tenantId = flags['tenant-id'] || resolveTenantId();
684
- const directHarnessUrl = flags['harness-url'] || process.env.MODA_SKILL_HARNESS_URL;
685
- if (directHarnessUrl && !tenantId) {
686
- throw new CliInputError('No Moda tenant id found. Run `moda init` first (or set MODA_TENANT_ID / --tenant-id).');
687
- }
688
- const endpoint = directHarnessUrl
689
- ? `${directHarnessUrl.replace(/\/+$/, '')}/skills/generate`
690
- : `${resolveIngestUrl().replace(/\/+$/, '')}/v1/skills/generate`;
691
- const waitForCompletion = flags.wait === 'true' || flags['wait-for-completion'] === 'true';
692
- const forceReprocess = flags['force-reprocess'] === 'true';
693
- const body = compactObject({
694
- tenant_id: tenantId || undefined,
695
- max_sessions: parsePositiveInt(flags['max-sessions'], 0),
696
- lookback_hours: parsePositiveInt(flags['lookback-hours'], 0),
697
- start_at: flags['start-at'] || undefined,
698
- end_at: flags['end-at'] || undefined,
699
- clustering_run_id: flags['clustering-run-id'] || undefined,
700
- replay_set_id: flags['replay-set-id'] || flags['eval-set-id'] || undefined,
701
- run_clustering: flags['no-clustering'] !== 'true',
702
- source: normalizeSkillSource(flags.source),
703
- clustering_timeout_seconds: parsePositiveInt(flags['clustering-timeout-seconds'], 7200),
704
- clustering_poll_seconds: parsePositiveInt(flags['clustering-poll-seconds'], 15),
705
- max_cluster_pct: parseOptionalFloat(flags['max-cluster-pct']),
706
- reprocess_segments: flags['reprocess-segments'] === 'true' || forceReprocess,
707
- run_replay: flags.replay === 'true' && flags['no-replay'] !== 'true',
708
- max_traces: parsePositiveInt(flags['max-traces'], 0),
709
- improve_candidates: flags['improve-candidates'] === 'true',
710
- improvement_rounds: parsePositiveInt(flags['improvement-rounds'], 2),
711
- force_reprocess: forceReprocess,
712
- wait_for_completion: waitForCompletion,
713
- });
714
- const headers = {
715
- Authorization: `Bearer ${apiKey}`,
716
- 'Content-Type': 'application/json',
717
- };
718
- if (tenantId)
719
- headers['X-Moda-Tenant-Id'] = tenantId;
720
- const response = await fetch(endpoint, {
721
- method: 'POST',
722
- headers,
723
- body: JSON.stringify(body),
724
- });
725
- if (!response.ok) {
726
- const text = await response.text().catch(() => '');
727
- throw new Error(`Failed to generate skills (HTTP ${response.status}): ${text.slice(0, 200)}`);
728
- }
729
- return (await response.json());
730
- }
731
- function parsePositiveInt(value, fallback) {
732
- if (!value)
733
- return fallback;
734
- const parsed = Number.parseInt(value, 10);
735
- return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
736
- }
737
- function parseOptionalFloat(value) {
738
- if (!value)
739
- return undefined;
740
- const parsed = Number.parseFloat(value);
741
- return Number.isFinite(parsed) ? parsed : undefined;
742
- }
743
- function compactObject(input) {
744
- return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
745
- }
746
- function normalizeSkillSource(value) {
747
- const normalized = (value || 'all').toLowerCase().replace(/-/g, '_');
748
- if (['all', 'sdk', 'coding_agent'].includes(normalized))
749
- return normalized;
750
- return 'all';
751
- }
752
- function releaseKeyFor(release, skills, status) {
753
- if (release?.generation_run_id)
754
- return `${status}:run:${release.generation_run_id}`;
755
- if (Number(release?.generation_version || 0) > 0) {
756
- return `${status}:version:${Number(release?.generation_version)}`;
757
- }
758
- return `${status}:legacy:${sha256(JSON.stringify(skills.map((skill) => ({
759
- skill_id: skill.skill_id,
760
- hash: sha256(skill.skill_md || ''),
761
- })).sort((a, b) => a.skill_id.localeCompare(b.skill_id))))}`;
762
- }
763
- function manifestHeaderFor(fetched) {
764
- const release = fetched.release;
765
- return {
766
- tenantId: fetched.tenantId,
767
- releaseKey: fetched.releaseKey,
768
- statusFilter: fetched.status,
769
- generationRunId: release?.generation_run_id || '',
770
- generationVersion: Number(release?.generation_version || 0),
771
- releasedAt: release?.released_at || '',
772
- };
773
- }
774
- function sha256(input) {
775
- return createHash('sha256').update(input).digest('hex');
776
- }
777
- //# sourceMappingURL=skills.js.map