@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.
- package/dist/cli-g8qs2tj9.js +9777 -0
- package/dist/cli-pwbvtes8.js +2312 -0
- package/dist/cli-qszcpda3.js +284 -0
- package/dist/cli.js +4775 -1159
- package/dist/index-6h0nc1r5.js +2765 -0
- package/dist/provision-j5gq8cvx.js +72 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -777
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- 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
|