@moda-ai/cli 1.4.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +11 -5
- package/dist/harness.d.ts +31 -0
- package/dist/harness.js +53 -9
- package/dist/init/index.d.ts +1 -1
- package/dist/init/index.js +217 -60
- package/dist/init/sdk-guide.d.ts +18 -0
- package/dist/init/sdk-guide.js +2 -2
- package/dist/init/sdk-integration.d.ts +26 -0
- package/dist/init/sdk-integration.js +80 -0
- package/dist/prompts.d.ts +16 -0
- package/dist/prompts.js +12 -3
- package/dist/skills.d.ts +26 -0
- package/dist/skills.js +160 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -161,6 +161,8 @@ Commands:
|
|
|
161
161
|
skills gen Kick off tenant-wide skill generation
|
|
162
162
|
skills pull Pull tenant-generated skills into .claude/skills (and .cursor/rules)
|
|
163
163
|
skills pull --status=proposed Include not-yet-promoted skills (default: approved)
|
|
164
|
+
skills sync Push local .claude/skills/**/SKILL.md up to Moda
|
|
165
|
+
skills sync --dry-run Show what would be pushed without writing
|
|
164
166
|
|
|
165
167
|
Options:
|
|
166
168
|
--help Show this help message
|
|
@@ -192,10 +194,13 @@ Init flags:
|
|
|
192
194
|
Choose harness analyst adapter
|
|
193
195
|
--tui Show full-screen analyst dashboard in human TTYs
|
|
194
196
|
--no-tui Show raw external analyst output
|
|
195
|
-
--sdk-
|
|
196
|
-
--no-sdk-
|
|
197
|
+
--sdk-integrate Run your coding agent to install the Moda SDK and instrument LLM calls
|
|
198
|
+
--no-sdk-integrate Skip SDK integration (alias: --no-sdk-guide)
|
|
197
199
|
--ci-cd Create GitHub Actions Moda validation workflow
|
|
198
200
|
--no-ci-cd Skip CI/CD workflow creation
|
|
201
|
+
--autosync Create GitHub Actions workflow to sync prompts + skills to Moda on push to main
|
|
202
|
+
--no-autosync Skip autosync workflow creation
|
|
203
|
+
--offline Run final validation offline (skip the online telemetry check)
|
|
199
204
|
|
|
200
205
|
Examples:
|
|
201
206
|
moda init
|
|
@@ -225,6 +230,7 @@ Examples:
|
|
|
225
230
|
moda harness scan --json
|
|
226
231
|
moda harness explain --agent=agent_support
|
|
227
232
|
moda init --ci-cd
|
|
233
|
+
moda init --autosync
|
|
228
234
|
moda overview --days-back=30
|
|
229
235
|
moda clusters --time-range=7d
|
|
230
236
|
moda conversations --search="error" --limit=5
|
|
@@ -578,7 +584,7 @@ export const commandRegistry = createCommandRegistry([
|
|
|
578
584
|
{
|
|
579
585
|
name: 'init',
|
|
580
586
|
description: 'Set up Moda in your project',
|
|
581
|
-
examples: ['moda init', 'moda init --
|
|
587
|
+
examples: ['moda init', 'moda init --sdk-integrate', 'moda init --ci-cd', 'moda init --autosync'],
|
|
582
588
|
auth: 'none',
|
|
583
589
|
outputModes: ['human', 'json', 'agent'],
|
|
584
590
|
mutability: 'write',
|
|
@@ -902,8 +908,8 @@ export const commandRegistry = createCommandRegistry([
|
|
|
902
908
|
// },
|
|
903
909
|
{
|
|
904
910
|
name: 'skills',
|
|
905
|
-
description: 'Generate and
|
|
906
|
-
examples: ['moda skills gen', 'moda skills pull'],
|
|
911
|
+
description: 'Generate, pull, and sync Moda skills',
|
|
912
|
+
examples: ['moda skills gen', 'moda skills pull', 'moda skills sync'],
|
|
907
913
|
auth: 'none',
|
|
908
914
|
outputModes: ['human', 'json', 'agent'],
|
|
909
915
|
mutability: 'write',
|
package/dist/harness.d.ts
CHANGED
|
@@ -222,9 +222,40 @@ export interface HarnessSyncState {
|
|
|
222
222
|
privacy: string[];
|
|
223
223
|
written: string[];
|
|
224
224
|
}
|
|
225
|
+
type HarnessAnalystAdapter = 'claude' | 'codex' | 'cursor' | 'local-scan';
|
|
226
|
+
type ExternalHarnessAnalystAdapter = Exclude<HarnessAnalystAdapter, 'local-scan'>;
|
|
227
|
+
export type CodingAgentMode = 'read-only' | 'write';
|
|
225
228
|
export declare function runHarnessCommand(context: CommandContext): Promise<{
|
|
226
229
|
exitCode: number;
|
|
227
230
|
}>;
|
|
231
|
+
export type CodingAgentAdapter = ExternalHarnessAnalystAdapter;
|
|
232
|
+
export interface CodingAgentRun {
|
|
233
|
+
adapter: CodingAgentAdapter;
|
|
234
|
+
command: string;
|
|
235
|
+
status: number | null;
|
|
236
|
+
stdout: string;
|
|
237
|
+
stderr: string;
|
|
238
|
+
timedOut: boolean;
|
|
239
|
+
failedToStart: boolean;
|
|
240
|
+
}
|
|
241
|
+
/** Coding-agent CLIs found on PATH, ranked best-first (claude > codex > cursor). */
|
|
242
|
+
export declare function detectAvailableCodingAgents(): Array<{
|
|
243
|
+
adapter: CodingAgentAdapter;
|
|
244
|
+
command: string;
|
|
245
|
+
}>;
|
|
246
|
+
export declare function codingAgentDisplayName(adapter: CodingAgentAdapter): string;
|
|
247
|
+
/** Spawn an installed coding-agent CLI with a prompt; capture its output. */
|
|
248
|
+
export declare function runCodingAgent(options: {
|
|
249
|
+
adapter: CodingAgentAdapter;
|
|
250
|
+
prompt: string;
|
|
251
|
+
rootDir: string;
|
|
252
|
+
mode: CodingAgentMode;
|
|
253
|
+
env?: NodeJS.ProcessEnv;
|
|
254
|
+
stream?: boolean;
|
|
255
|
+
timeoutMs?: number;
|
|
256
|
+
progressToStderr?: boolean;
|
|
257
|
+
onProgress?: (message: string, phase?: string) => void;
|
|
258
|
+
}): Promise<CodingAgentRun>;
|
|
228
259
|
export declare function scanHarness(options?: ScanOptions): HarnessGraph;
|
|
229
260
|
export declare function summarizeHarness(graph: HarnessGraph): HarnessSummary;
|
|
230
261
|
export declare function readHarnessSyncState(rootDir: string): HarnessSyncState | undefined;
|
package/dist/harness.js
CHANGED
|
@@ -1215,6 +1215,7 @@ function normalizeAnalystAdapter(raw) {
|
|
|
1215
1215
|
throw new CliInputError(`Unknown harness analyst '${raw}'.`, 'Use `--analyst=claude|codex|cursor|local-scan`.');
|
|
1216
1216
|
}
|
|
1217
1217
|
function resolveExternalHarnessAnalystInvocation(options) {
|
|
1218
|
+
const mode = options.mode ?? 'read-only';
|
|
1218
1219
|
const override = options.flags['analyst-command'] || process.env.MODA_HARNESS_ANALYST_COMMAND;
|
|
1219
1220
|
if (override) {
|
|
1220
1221
|
const [command, ...args] = splitCommandLine(override);
|
|
@@ -1229,16 +1230,19 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1229
1230
|
}
|
|
1230
1231
|
if (options.adapter === 'claude') {
|
|
1231
1232
|
const extraArgs = externalAnalystExtraArgs(options.adapter);
|
|
1233
|
+
// Write mode auto-accepts edits and unblocks the tools an integration needs
|
|
1234
|
+
// (Edit/Write/Bash for package installs); read-only stays in plan mode with a
|
|
1235
|
+
// safe-Bash allowlist so the harness analyst can never mutate the tree.
|
|
1236
|
+
const permission = mode === 'write'
|
|
1237
|
+
? ['--permission-mode', 'acceptEdits', '--allowedTools', 'Read,Grep,Glob,Edit,Write,Bash']
|
|
1238
|
+
: ['--permission-mode', 'plan', '--allowedTools', 'Read,Grep,Glob,Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)'];
|
|
1232
1239
|
return {
|
|
1233
1240
|
adapter: options.adapter,
|
|
1234
1241
|
command,
|
|
1235
1242
|
args: [
|
|
1236
1243
|
'--print',
|
|
1237
1244
|
...extraArgs,
|
|
1238
|
-
|
|
1239
|
-
'plan',
|
|
1240
|
-
'--allowedTools',
|
|
1241
|
-
'Read,Grep,Glob,Bash(pwd),Bash(ls *),Bash(find *),Bash(rg *),Bash(grep *),Bash(cat *),Bash(git status *),Bash(git ls-files *),Bash(git grep *)',
|
|
1245
|
+
...permission,
|
|
1242
1246
|
...(options.stream ? ['--verbose'] : []),
|
|
1243
1247
|
'--output-format',
|
|
1244
1248
|
options.stream ? 'stream-json' : 'text',
|
|
@@ -1256,7 +1260,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1256
1260
|
'exec',
|
|
1257
1261
|
'--skip-git-repo-check',
|
|
1258
1262
|
'--sandbox',
|
|
1259
|
-
'read-only',
|
|
1263
|
+
mode === 'write' ? 'workspace-write' : 'read-only',
|
|
1260
1264
|
...(options.stream ? ['--json'] : []),
|
|
1261
1265
|
'-C',
|
|
1262
1266
|
options.rootDir,
|
|
@@ -1271,10 +1275,7 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1271
1275
|
args: cursorSubcommandArgs(command, [
|
|
1272
1276
|
'--print',
|
|
1273
1277
|
'--trust',
|
|
1274
|
-
'--mode',
|
|
1275
|
-
'plan',
|
|
1276
|
-
'--sandbox',
|
|
1277
|
-
'enabled',
|
|
1278
|
+
...(mode === 'write' ? ['--force'] : ['--mode', 'plan', '--sandbox', 'enabled']),
|
|
1278
1279
|
'--workspace',
|
|
1279
1280
|
options.rootDir,
|
|
1280
1281
|
'--output-format',
|
|
@@ -1284,6 +1285,49 @@ function resolveExternalHarnessAnalystInvocation(options) {
|
|
|
1284
1285
|
streamEvents: options.stream,
|
|
1285
1286
|
};
|
|
1286
1287
|
}
|
|
1288
|
+
/** Coding-agent CLIs found on PATH, ranked best-first (claude > codex > cursor). */
|
|
1289
|
+
export function detectAvailableCodingAgents() {
|
|
1290
|
+
return rankHarnessAnalysts(detectAvailableHarnessAnalysts());
|
|
1291
|
+
}
|
|
1292
|
+
export function codingAgentDisplayName(adapter) {
|
|
1293
|
+
return analystDisplayName(adapter);
|
|
1294
|
+
}
|
|
1295
|
+
/** Spawn an installed coding-agent CLI with a prompt; capture its output. */
|
|
1296
|
+
export async function runCodingAgent(options) {
|
|
1297
|
+
const stream = options.stream ?? false;
|
|
1298
|
+
const invocation = resolveExternalHarnessAnalystInvocation({
|
|
1299
|
+
adapter: options.adapter,
|
|
1300
|
+
flags: {},
|
|
1301
|
+
prompt: options.prompt,
|
|
1302
|
+
rootDir: options.rootDir,
|
|
1303
|
+
stream,
|
|
1304
|
+
mode: options.mode,
|
|
1305
|
+
});
|
|
1306
|
+
const timeoutMs = options.timeoutMs ?? numericEnv('MODA_HARNESS_ANALYST_TIMEOUT_MS', 20 * 60 * 1000);
|
|
1307
|
+
const result = await runExternalAnalystWithRetries(invocation, {
|
|
1308
|
+
cwd: options.rootDir,
|
|
1309
|
+
env: buildExternalAnalystEnv(options.env ?? process.env),
|
|
1310
|
+
inheritedOutput: false,
|
|
1311
|
+
timeoutMs,
|
|
1312
|
+
maxBuffer: 20 * 1024 * 1024,
|
|
1313
|
+
progress: Boolean(options.onProgress) || Boolean(options.progressToStderr),
|
|
1314
|
+
progressToStderr: options.progressToStderr ?? false,
|
|
1315
|
+
streamEvents: stream,
|
|
1316
|
+
adapter: invocation.adapter,
|
|
1317
|
+
maxAttempts: externalAnalystMaxAttempts(options.adapter),
|
|
1318
|
+
backoffMs: numericEnvAllowZero('MODA_HARNESS_ANALYST_RETRY_BACKOFF_MS', 6_000),
|
|
1319
|
+
onProgress: options.onProgress,
|
|
1320
|
+
});
|
|
1321
|
+
return {
|
|
1322
|
+
adapter: invocation.adapter,
|
|
1323
|
+
command: invocation.command,
|
|
1324
|
+
status: result.status,
|
|
1325
|
+
stdout: result.stdout,
|
|
1326
|
+
stderr: result.stderr,
|
|
1327
|
+
timedOut: result.timedOut,
|
|
1328
|
+
failedToStart: Boolean(result.error),
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1287
1331
|
function externalAnalystExtraArgs(adapter) {
|
|
1288
1332
|
return splitCommandLine(process.env[`MODA_HARNESS_ANALYST_${adapter.toUpperCase()}_ARGS`] ?? '');
|
|
1289
1333
|
}
|
package/dist/init/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface InitPhaseResult {
|
|
|
11
11
|
details?: string;
|
|
12
12
|
}
|
|
13
13
|
export interface InitSetupAction {
|
|
14
|
-
category: 'sdk' | 'harness' | 'hooks' | 'prompts' | 'skills' | 'ci-cd' | 'validation';
|
|
14
|
+
category: 'sdk' | 'harness' | 'hooks' | 'prompts' | 'skills' | 'ci-cd' | 'autosync' | 'validation';
|
|
15
15
|
title: string;
|
|
16
16
|
status: 'planned' | 'applied' | 'skipped' | 'manual';
|
|
17
17
|
approved: boolean;
|
package/dist/init/index.js
CHANGED
|
@@ -6,15 +6,17 @@ import { login, loadToken } from '../auth.js';
|
|
|
6
6
|
import { selectTenantAndCreateKey } from './tenant.js';
|
|
7
7
|
import { detectAgents } from './mcp.js';
|
|
8
8
|
import { installRules } from './rules.js';
|
|
9
|
-
import { initPrompts } from '../prompts.js';
|
|
10
|
-
import {
|
|
9
|
+
import { initPrompts, runPromptSync } from '../prompts.js';
|
|
10
|
+
import { runSkillSync } from '../skills.js';
|
|
11
11
|
// Coding-agent capture is temporarily disabled — see the hooks phase below.
|
|
12
12
|
// import { runHooksInstall } from '../workspace.js';
|
|
13
13
|
import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
|
|
14
|
+
import { runSdkIntegration } from './sdk-integration.js';
|
|
14
15
|
import { createCommandContext } from '../command-context.js';
|
|
16
|
+
import { VERSION } from '../config.js';
|
|
15
17
|
import { CliInputError } from '../errors.js';
|
|
16
18
|
import { runHarnessCommand } from '../harness.js';
|
|
17
|
-
import {
|
|
19
|
+
import { buildDoctorReport } from '../doctor.js';
|
|
18
20
|
import { renderInitBanner, renderInitComplete, renderInitNote, renderInitSetupPlan, renderInitStep, } from '../tui.js';
|
|
19
21
|
export async function runInit(flags, options = {}) {
|
|
20
22
|
const explicitYes = flags.yes === true ||
|
|
@@ -30,9 +32,7 @@ export async function runInit(flags, options = {}) {
|
|
|
30
32
|
if (human)
|
|
31
33
|
console.log(message);
|
|
32
34
|
};
|
|
33
|
-
|
|
34
|
-
// underscore-prefixed binding so re-enabling it is a rename away.
|
|
35
|
-
const _warn = (message) => {
|
|
35
|
+
const warn = (message) => {
|
|
36
36
|
if (human)
|
|
37
37
|
console.error(message);
|
|
38
38
|
};
|
|
@@ -102,6 +102,32 @@ export async function runInit(flags, options = {}) {
|
|
|
102
102
|
if (human)
|
|
103
103
|
printHarnessDetectionSummary();
|
|
104
104
|
emit('harness', 'Harness discovery complete.', 'completed');
|
|
105
|
+
// Upload the cited harness graph so Moda's backend has the understanding, not
|
|
106
|
+
// just local artifacts. `harness sync --from-report` refuses an unapproved
|
|
107
|
+
// report, so approve it first. Best-effort: a network blip on either call must
|
|
108
|
+
// not abort onboarding — auth + key provisioning already succeeded.
|
|
109
|
+
try {
|
|
110
|
+
await runHarnessCommand(createCommandContext({
|
|
111
|
+
command: 'harness',
|
|
112
|
+
positional: 'approve',
|
|
113
|
+
positionals: ['approve'],
|
|
114
|
+
flags: commandFlags({ yes: true, json: json || agent, agent, quiet: true, 'no-telemetry': true }),
|
|
115
|
+
}));
|
|
116
|
+
await runHarnessCommand(createCommandContext({
|
|
117
|
+
command: 'harness',
|
|
118
|
+
positional: 'sync',
|
|
119
|
+
positionals: ['sync'],
|
|
120
|
+
flags: commandFlags({ 'from-report': true, yes: true, json: json || agent, agent, quiet: true, 'no-telemetry': true }),
|
|
121
|
+
}));
|
|
122
|
+
log(renderInitNote('Synced harness graph to Moda'));
|
|
123
|
+
emit('harness', 'Harness graph synced to Moda.', 'completed');
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
127
|
+
warn(renderInitNote(`Harness sync skipped: ${message}`, 'warning'));
|
|
128
|
+
warn(' Run `moda harness approve --yes && moda harness sync --from-report` later to upload.');
|
|
129
|
+
emit('harness', 'Harness sync skipped.', 'warning');
|
|
130
|
+
}
|
|
105
131
|
phases.push({ id: 'harness', title: 'Harness Discovery', status: 'completed' });
|
|
106
132
|
// Step 5: Setup plan
|
|
107
133
|
log(renderInitStep(5, 7, 'Setup Plan', 'Every write/sync action is explicit before it is applied.'));
|
|
@@ -109,8 +135,9 @@ export async function runInit(flags, options = {}) {
|
|
|
109
135
|
// Coding-agent capture is temporarily disabled; never prompt for or install trace hooks.
|
|
110
136
|
// installHooks: await shouldIngestCodingAgentTraces(flags, yes),
|
|
111
137
|
installHooks: false,
|
|
112
|
-
|
|
138
|
+
integrateSdk: await shouldIntegrateSdk(flags, yes),
|
|
113
139
|
configureCiCd: await shouldConfigureCiCd(flags, yes),
|
|
140
|
+
configureAutosync: await shouldConfigureAutosync(flags, yes),
|
|
114
141
|
});
|
|
115
142
|
log(renderInitSetupPlan(setupPlan));
|
|
116
143
|
emit('plan', 'Setup plan ready.', 'completed');
|
|
@@ -140,34 +167,69 @@ export async function runInit(flags, options = {}) {
|
|
|
140
167
|
const agents = detectAgents();
|
|
141
168
|
if (agents.length > 0) {
|
|
142
169
|
log(renderInitNote(`Detected coding agents: ${agents.join(', ')}`));
|
|
170
|
+
// Install only the moda-cli skill so those agents can query Moda analytics
|
|
171
|
+
// from the terminal. SDK integration is a one-shot agent run below, not a
|
|
172
|
+
// persistent skill.
|
|
143
173
|
await installRules(agents, { yes, silent: !human });
|
|
144
|
-
// Pull any skills the pipeline has already generated for this tenant into
|
|
145
|
-
// the local agent skill dirs. Best-effort: a brand-new tenant has none yet,
|
|
146
|
-
// and a transient ingest/network error must not fail setup.
|
|
147
|
-
try {
|
|
148
|
-
const pulled = await pullSkills({ silent: true });
|
|
149
|
-
if (pulled.wrote + pulled.updated > 0) {
|
|
150
|
-
log(renderInitNote(`Pulled ${pulled.wrote + pulled.updated} learned skill(s) into agent skills`));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// Silent: skills are additive; never block init on them.
|
|
155
|
-
}
|
|
156
174
|
}
|
|
157
175
|
else {
|
|
158
176
|
log(renderInitNote('No coding agents detected, skipping', 'skipped'));
|
|
159
177
|
}
|
|
160
178
|
await initPrompts({ silent: true });
|
|
161
179
|
log(renderInitNote('Created or verified .moda/prompts.yml'));
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
180
|
+
// Push the manifest's prompt files to Moda so production traces can attribute
|
|
181
|
+
// managed prompt versions. A fresh repo has none yet — that's a harmless no-op.
|
|
182
|
+
try {
|
|
183
|
+
const promptSync = await runPromptSync({}, { profile: stringFlag(flags.profile) });
|
|
184
|
+
log(renderInitNote(promptSync.synced.length > 0
|
|
185
|
+
? `Synced ${promptSync.synced.length} prompt(s) to Moda`
|
|
186
|
+
: 'No prompt files to sync yet (add paths to .moda/prompts.yml)'));
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
190
|
+
warn(renderInitNote(`Prompt sync skipped: ${message}`, 'warning'));
|
|
191
|
+
}
|
|
192
|
+
// Push locally-authored skills (`.claude/skills/**/SKILL.md`) to Moda, in
|
|
193
|
+
// parallel intent with prompt sync. A fresh repo has none yet — harmless
|
|
194
|
+
// no-op. Best-effort: a network blip must not abort onboarding.
|
|
195
|
+
try {
|
|
196
|
+
const skillSync = await runSkillSync({}, { profile: stringFlag(flags.profile) });
|
|
197
|
+
if (skillSync.synced.length > 0) {
|
|
198
|
+
log(renderInitNote(`Synced ${skillSync.synced.length} skill(s) to Moda`));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
log(renderInitNote('No local skills to sync yet (add .claude/skills/<name>/SKILL.md)'));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
206
|
+
warn(renderInitNote(`Skill sync skipped: ${message}`, 'warning'));
|
|
207
|
+
}
|
|
208
|
+
const sdkAction = setupPlan.find((action) => action.category === 'sdk');
|
|
209
|
+
if (sdkAction?.approved === true) {
|
|
210
|
+
log(renderInitNote('Running your coding agent to integrate the Moda SDK (this can take a few minutes)...', 'warning'));
|
|
211
|
+
const result = await runSdkIntegration({ human });
|
|
212
|
+
if (result.status === 'integrated') {
|
|
213
|
+
log(renderInitNote(`Integrated the Moda SDK with ${result.adapter}. Review the diff before committing.`));
|
|
214
|
+
markApplied(setupPlan, 'sdk');
|
|
215
|
+
}
|
|
216
|
+
else if (result.status === 'no-agent') {
|
|
217
|
+
const guidePath = writeSdkIntegrationGuide();
|
|
218
|
+
sdkAction.files = [guidePath];
|
|
219
|
+
log(renderInitNote(`No coding agent CLI found; wrote ${guidePath} to integrate by hand`, 'warning'));
|
|
220
|
+
markApplied(setupPlan, 'sdk');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const guidePath = writeSdkIntegrationGuide();
|
|
224
|
+
sdkAction.files = [guidePath];
|
|
225
|
+
warn(renderInitNote(`SDK integration via ${result.adapter} did not complete: ${result.detail}`, 'warning'));
|
|
226
|
+
log(renderInitNote(`Wrote ${guidePath} so you can finish the integration manually`));
|
|
227
|
+
markApplied(setupPlan, 'sdk');
|
|
228
|
+
}
|
|
167
229
|
}
|
|
168
230
|
else {
|
|
169
|
-
log(renderInitNote('Skipped SDK integration
|
|
170
|
-
markApplied(setupPlan, 'sdk', 'User skipped SDK integration
|
|
231
|
+
log(renderInitNote('Skipped SDK integration', 'skipped'));
|
|
232
|
+
markApplied(setupPlan, 'sdk', 'User skipped SDK integration.');
|
|
171
233
|
}
|
|
172
234
|
const configureCiCd = setupPlan.find((action) => action.category === 'ci-cd')?.approved === true;
|
|
173
235
|
if (configureCiCd) {
|
|
@@ -175,6 +237,12 @@ export async function runInit(flags, options = {}) {
|
|
|
175
237
|
log(renderInitNote(`Wrote ${workflowPath}`));
|
|
176
238
|
markApplied(setupPlan, 'ci-cd');
|
|
177
239
|
}
|
|
240
|
+
const configureAutosync = setupPlan.find((action) => action.category === 'autosync')?.approved === true;
|
|
241
|
+
if (configureAutosync) {
|
|
242
|
+
const workflowPath = writeAutosyncWorkflow();
|
|
243
|
+
log(renderInitNote(`Wrote ${workflowPath}`));
|
|
244
|
+
markApplied(setupPlan, 'autosync');
|
|
245
|
+
}
|
|
178
246
|
log(renderInitNote('Reusing approved Step 4 harness report; no second analyst run needed'));
|
|
179
247
|
markApplied(setupPlan, 'harness');
|
|
180
248
|
markApplied(setupPlan, 'hooks', ingestCodingAgentTraces ? undefined : 'Coding-agent trace capture is temporarily unavailable.');
|
|
@@ -182,22 +250,26 @@ export async function runInit(flags, options = {}) {
|
|
|
182
250
|
markApplied(setupPlan, 'skills');
|
|
183
251
|
emit('apply', 'Approved changes applied.', 'completed');
|
|
184
252
|
phases.push({ id: 'apply', title: 'Apply Approved Changes', status: 'completed' });
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
253
|
+
const offlineValidation = isTruthyFlag(flagValue(flags, 'offline'));
|
|
254
|
+
log(renderInitStep(7, 7, 'Validate', offlineValidation
|
|
255
|
+
? 'Running local doctor checks so the setup leaves a clear state.'
|
|
256
|
+
: 'Running Moda doctor online to confirm setup and that telemetry is reaching Moda.'));
|
|
257
|
+
emit('validate', `Running Moda doctor diagnostics (${offlineValidation ? 'offline' : 'online'}).`);
|
|
258
|
+
const doctorReport = await buildDoctorReport(createCommandContext({
|
|
188
259
|
command: 'doctor',
|
|
189
260
|
positional: undefined,
|
|
190
261
|
positionals: [],
|
|
191
|
-
flags: commandFlags({
|
|
192
|
-
|
|
193
|
-
quiet: true,
|
|
194
|
-
offline: true,
|
|
195
|
-
'no-telemetry': true,
|
|
196
|
-
}),
|
|
197
|
-
}));
|
|
262
|
+
flags: commandFlags({ json: true, quiet: true, 'no-telemetry': true }),
|
|
263
|
+
}), { offline: offlineValidation });
|
|
198
264
|
markApplied(setupPlan, 'validation');
|
|
199
|
-
log(renderInitNote(
|
|
200
|
-
|
|
265
|
+
log(renderInitNote(`Validation checks completed: ${doctorReport.summary.pass} passing, ${doctorReport.summary.warning} warning, ${doctorReport.summary.fail} failing`));
|
|
266
|
+
if (!offlineValidation) {
|
|
267
|
+
const cloud = doctorReport.checks.find((check) => check.id === 'cloud_data_flow');
|
|
268
|
+
if (cloud) {
|
|
269
|
+
log(renderInitNote(`Moda Cloud telemetry: ${cloud.status}`, cloud.status === 'pass' ? undefined : 'warning'));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
emit('validate', 'Validation checks complete.', doctorReport.summary.fail > 0 ? 'warning' : 'completed');
|
|
201
273
|
phases.push({ id: 'validate', title: 'Validate', status: 'completed' });
|
|
202
274
|
// Done
|
|
203
275
|
log(renderInitComplete({
|
|
@@ -212,7 +284,7 @@ function buildSetupPlan(options) {
|
|
|
212
284
|
return [
|
|
213
285
|
{
|
|
214
286
|
category: 'harness',
|
|
215
|
-
title: 'Analyze harness
|
|
287
|
+
title: 'Analyze harness, write cited report, and sync the graph to Moda',
|
|
216
288
|
status: 'planned',
|
|
217
289
|
approved: true,
|
|
218
290
|
files: [
|
|
@@ -222,7 +294,7 @@ function buildSetupPlan(options) {
|
|
|
222
294
|
'.moda/harness.json',
|
|
223
295
|
'.moda/HARNESS.md',
|
|
224
296
|
],
|
|
225
|
-
command: 'moda harness analyze --analyst=auto --summary',
|
|
297
|
+
command: 'moda harness analyze --analyst=auto --summary && moda harness approve --yes && moda harness sync --from-report',
|
|
226
298
|
},
|
|
227
299
|
{
|
|
228
300
|
category: 'hooks',
|
|
@@ -235,30 +307,34 @@ function buildSetupPlan(options) {
|
|
|
235
307
|
},
|
|
236
308
|
{
|
|
237
309
|
category: 'prompts',
|
|
238
|
-
title: 'Initialize prompt registry manifest',
|
|
310
|
+
title: 'Initialize the prompt registry manifest and sync prompts to Moda',
|
|
239
311
|
status: 'planned',
|
|
240
312
|
approved: true,
|
|
241
313
|
files: ['.moda/prompts.yml'],
|
|
242
|
-
command: 'moda prompts init',
|
|
314
|
+
command: 'moda prompts init && moda prompts sync',
|
|
243
315
|
},
|
|
244
316
|
{
|
|
245
317
|
category: 'skills',
|
|
246
|
-
title: '
|
|
318
|
+
title: 'Install the moda-cli skill into detected coding agents and sync local skills to Moda',
|
|
247
319
|
status: 'planned',
|
|
248
320
|
approved: true,
|
|
249
321
|
files: ['.claude/skills', '.cursor/rules'],
|
|
250
|
-
|
|
322
|
+
// Skill install into detected agents happens inside `moda init`; there is no
|
|
323
|
+
// standalone install command, so surface only the re-runnable sync here.
|
|
324
|
+
command: 'moda skills sync',
|
|
251
325
|
},
|
|
252
326
|
{
|
|
253
327
|
category: 'sdk',
|
|
254
|
-
title: '
|
|
255
|
-
status: options.
|
|
256
|
-
approved: options.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
328
|
+
title: 'Run a coding agent to integrate the Moda SDK',
|
|
329
|
+
status: options.integrateSdk ? 'planned' : 'manual',
|
|
330
|
+
approved: options.integrateSdk,
|
|
331
|
+
// Files are repo-specific and unknown ahead of the agent run; the fallback
|
|
332
|
+
// path rewrites this to the guide path when no agent CLI is available.
|
|
333
|
+
files: [],
|
|
334
|
+
command: 'moda init',
|
|
335
|
+
reason: options.integrateSdk
|
|
336
|
+
? 'Installs the SDK package and edits source via your coding agent; review the diff before committing.'
|
|
337
|
+
: `SDK integration stays manual; see ${SDK_GUIDE_PATH} if you generate the guide.`,
|
|
262
338
|
},
|
|
263
339
|
{
|
|
264
340
|
category: 'ci-cd',
|
|
@@ -269,13 +345,22 @@ function buildSetupPlan(options) {
|
|
|
269
345
|
command: 'moda doctor --online --json',
|
|
270
346
|
reason: options.configureCiCd ? undefined : 'No CI/CD files are changed until a project-specific plan is approved.',
|
|
271
347
|
},
|
|
348
|
+
{
|
|
349
|
+
category: 'autosync',
|
|
350
|
+
title: 'Autosync prompts and skills to Moda on push to main',
|
|
351
|
+
status: options.configureAutosync ? 'planned' : 'manual',
|
|
352
|
+
approved: options.configureAutosync,
|
|
353
|
+
files: ['.github/workflows/moda-autosync.yml'],
|
|
354
|
+
command: 'moda prompts sync && moda skills sync',
|
|
355
|
+
reason: options.configureAutosync ? undefined : 'No autosync workflow is written until it is explicitly approved (--autosync).',
|
|
356
|
+
},
|
|
272
357
|
{
|
|
273
358
|
category: 'validation',
|
|
274
|
-
title: 'Run
|
|
359
|
+
title: 'Run Moda doctor online to confirm telemetry is reaching Moda',
|
|
275
360
|
status: 'planned',
|
|
276
361
|
approved: true,
|
|
277
362
|
files: [],
|
|
278
|
-
command: 'moda doctor',
|
|
363
|
+
command: 'moda doctor --online',
|
|
279
364
|
},
|
|
280
365
|
];
|
|
281
366
|
}
|
|
@@ -296,6 +381,13 @@ function changedFiles(setupPlan) {
|
|
|
296
381
|
.filter((action) => action.status === 'applied')
|
|
297
382
|
.flatMap((action) => action.files))].filter((file) => !file.includes('.env'));
|
|
298
383
|
}
|
|
384
|
+
// The CLI version generated workflows pin to. Pinning (rather than `@latest`,
|
|
385
|
+
// which resolves at CI run time) keeps a future compromised `@moda-ai/cli`
|
|
386
|
+
// publish from silently executing in adopters' CI with their MODA_API_KEY /
|
|
387
|
+
// MODA_TENANT_ID present — especially important for autosync, which *writes* to
|
|
388
|
+
// the Moda registry. The pin tracks the CLI that generated the file, so each
|
|
389
|
+
// `moda init` upgrade is an explicit, reviewable bump of the workflow.
|
|
390
|
+
const GENERATED_WORKFLOW_CLI = `@moda-ai/cli@${VERSION}`;
|
|
299
391
|
function writeCiCdWorkflow(cwd = process.cwd()) {
|
|
300
392
|
const relativePath = '.github/workflows/moda-validation.yml';
|
|
301
393
|
const path = join(cwd, relativePath);
|
|
@@ -308,6 +400,11 @@ function writeCiCdWorkflow(cwd = process.cwd()) {
|
|
|
308
400
|
' push:',
|
|
309
401
|
' branches: [main]',
|
|
310
402
|
'',
|
|
403
|
+
'# Least privilege: these jobs only read the checked-out repo and talk to',
|
|
404
|
+
'# Moda over the API key; they never write back to the repository.',
|
|
405
|
+
'permissions:',
|
|
406
|
+
' contents: read',
|
|
407
|
+
'',
|
|
311
408
|
'jobs:',
|
|
312
409
|
' moda-doctor:',
|
|
313
410
|
' runs-on: ubuntu-latest',
|
|
@@ -320,7 +417,51 @@ function writeCiCdWorkflow(cwd = process.cwd()) {
|
|
|
320
417
|
' with:',
|
|
321
418
|
' node-version: 22',
|
|
322
419
|
' - name: Validate Moda setup',
|
|
323
|
-
|
|
420
|
+
` run: npx -y ${GENERATED_WORKFLOW_CLI} moda doctor --online --json`,
|
|
421
|
+
'',
|
|
422
|
+
].join('\n')}`);
|
|
423
|
+
return relativePath;
|
|
424
|
+
}
|
|
425
|
+
// Generates a push-to-main autosync workflow that keeps the Moda registry in
|
|
426
|
+
// sync with the repo: it pushes prompt and skill changes after they merge. It
|
|
427
|
+
// only runs on `main` (never on pull_request) so unmerged versions are never
|
|
428
|
+
// published. Keep its env in step with writeCiCdWorkflow().
|
|
429
|
+
function writeAutosyncWorkflow(cwd = process.cwd()) {
|
|
430
|
+
const relativePath = '.github/workflows/moda-autosync.yml';
|
|
431
|
+
const path = join(cwd, relativePath);
|
|
432
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
433
|
+
writeFileSync(path, `${[
|
|
434
|
+
'name: Moda Autosync',
|
|
435
|
+
'',
|
|
436
|
+
'on:',
|
|
437
|
+
' push:',
|
|
438
|
+
' branches: [main]',
|
|
439
|
+
'',
|
|
440
|
+
'# Least privilege: this job only reads the checked-out repo and writes to',
|
|
441
|
+
'# Moda over the API key; it never writes back to the repository.',
|
|
442
|
+
'permissions:',
|
|
443
|
+
' contents: read',
|
|
444
|
+
'',
|
|
445
|
+
'# A single in-flight sync at a time; newer pushes supersede older runs.',
|
|
446
|
+
'concurrency:',
|
|
447
|
+
' group: moda-autosync',
|
|
448
|
+
' cancel-in-progress: true',
|
|
449
|
+
'',
|
|
450
|
+
'jobs:',
|
|
451
|
+
' moda-sync:',
|
|
452
|
+
' runs-on: ubuntu-latest',
|
|
453
|
+
' env:',
|
|
454
|
+
' MODA_API_KEY: ${{ secrets.MODA_API_KEY }}',
|
|
455
|
+
' MODA_TENANT_ID: ${{ secrets.MODA_TENANT_ID }}',
|
|
456
|
+
' steps:',
|
|
457
|
+
' - uses: actions/checkout@v4',
|
|
458
|
+
' - uses: actions/setup-node@v4',
|
|
459
|
+
' with:',
|
|
460
|
+
' node-version: 22',
|
|
461
|
+
` - name: Sync prompts to Moda`,
|
|
462
|
+
` run: npx -y ${GENERATED_WORKFLOW_CLI} moda prompts sync`,
|
|
463
|
+
` - name: Sync skills to Moda`,
|
|
464
|
+
` run: npx -y ${GENERATED_WORKFLOW_CLI} moda skills sync`,
|
|
324
465
|
'',
|
|
325
466
|
].join('\n')}`);
|
|
326
467
|
return relativePath;
|
|
@@ -383,16 +524,32 @@ async function shouldConfigureCiCd(flags, yes) {
|
|
|
383
524
|
default: false,
|
|
384
525
|
});
|
|
385
526
|
}
|
|
386
|
-
async function
|
|
387
|
-
const explicit = parseOptionalBoolean(flagValue(flags, '
|
|
527
|
+
async function shouldConfigureAutosync(flags, yes) {
|
|
528
|
+
const explicit = parseOptionalBoolean(flagValue(flags, 'autosync', 'auto-sync', 'auto_sync'));
|
|
388
529
|
if (explicit !== undefined)
|
|
389
530
|
return explicit;
|
|
390
|
-
if (isTruthyFlag(flagValue(flags, 'no-
|
|
531
|
+
if (isTruthyFlag(flagValue(flags, 'no-autosync', 'no_autosync', 'no-auto-sync', 'no_auto_sync')))
|
|
391
532
|
return false;
|
|
392
533
|
if (yes)
|
|
393
|
-
return
|
|
534
|
+
return false;
|
|
535
|
+
return confirm({
|
|
536
|
+
message: 'Create a GitHub Actions workflow to autosync prompts and skills to Moda on push to main?',
|
|
537
|
+
default: false,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
async function shouldIntegrateSdk(flags, yes) {
|
|
541
|
+
const explicit = parseOptionalBoolean(flagValue(flags, 'sdk-integrate', 'sdk_integrate', 'sdk-guide', 'sdk_guide', 'sdk'));
|
|
542
|
+
if (explicit !== undefined)
|
|
543
|
+
return explicit;
|
|
544
|
+
if (isTruthyFlag(flagValue(flags, 'no-sdk-integrate', 'no_sdk_integrate', 'no-sdk-guide', 'no_sdk_guide', 'no-sdk', 'no_sdk'))) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
// SDK integration now installs packages and edits source via a coding agent.
|
|
548
|
+
// Never do that unattended (--yes/--json/--agent); require an explicit opt-in.
|
|
549
|
+
if (yes)
|
|
550
|
+
return false;
|
|
394
551
|
return confirm({
|
|
395
|
-
message: '
|
|
552
|
+
message: 'Run your coding agent now to install the Moda SDK and instrument your LLM calls? (Edits source, installs packages.)',
|
|
396
553
|
default: true,
|
|
397
554
|
});
|
|
398
555
|
}
|
package/dist/init/sdk-guide.d.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
1
|
export declare const SDK_GUIDE_PATH = ".moda/SDK_INTEGRATION.md";
|
|
2
|
+
type RuntimeSummary = {
|
|
3
|
+
runtimeAgents: number;
|
|
4
|
+
agentFamilies: number;
|
|
5
|
+
frameworks: string[];
|
|
6
|
+
providers: string[];
|
|
7
|
+
runtimeAgentNames: string[];
|
|
8
|
+
};
|
|
9
|
+
export type SdkGuideContext = {
|
|
10
|
+
languages: string[];
|
|
11
|
+
installCommands: string[];
|
|
12
|
+
providerHints: string[];
|
|
13
|
+
frameworkHints: string[];
|
|
14
|
+
packageManagers: string[];
|
|
15
|
+
runtimeSummary: RuntimeSummary;
|
|
16
|
+
};
|
|
2
17
|
export declare function writeSdkIntegrationGuide(cwd?: string): string;
|
|
18
|
+
export declare function detectSdkGuideContext(cwd: string): SdkGuideContext;
|
|
19
|
+
export declare function renderSdkIntegrationGuide(context: SdkGuideContext): string;
|
|
20
|
+
export {};
|
|
3
21
|
//# sourceMappingURL=sdk-guide.d.ts.map
|
package/dist/init/sdk-guide.js
CHANGED
|
@@ -8,7 +8,7 @@ export function writeSdkIntegrationGuide(cwd = process.cwd()) {
|
|
|
8
8
|
writeFileSync(absolutePath, renderSdkIntegrationGuide(detectSdkGuideContext(cwd)));
|
|
9
9
|
return relativePath;
|
|
10
10
|
}
|
|
11
|
-
function detectSdkGuideContext(cwd) {
|
|
11
|
+
export function detectSdkGuideContext(cwd) {
|
|
12
12
|
const languages = [];
|
|
13
13
|
const installCommands = [];
|
|
14
14
|
const packageManagers = [];
|
|
@@ -64,7 +64,7 @@ function detectSdkGuideContext(cwd) {
|
|
|
64
64
|
runtimeSummary,
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
|
-
function renderSdkIntegrationGuide(context) {
|
|
67
|
+
export function renderSdkIntegrationGuide(context) {
|
|
68
68
|
const languages = context.languages.length > 0 ? context.languages.join(', ') : 'Not detected yet';
|
|
69
69
|
const packageManagers = context.packageManagers.length > 0 ? context.packageManagers.join(', ') : 'Not detected yet';
|
|
70
70
|
const installCommands = context.installCommands.length > 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type CodingAgentAdapter } from '../harness.js';
|
|
2
|
+
export type SdkIntegrationResult = {
|
|
3
|
+
status: 'integrated';
|
|
4
|
+
adapter: CodingAgentAdapter;
|
|
5
|
+
} | {
|
|
6
|
+
status: 'no-agent';
|
|
7
|
+
} | {
|
|
8
|
+
status: 'failed';
|
|
9
|
+
adapter: CodingAgentAdapter;
|
|
10
|
+
detail: string;
|
|
11
|
+
};
|
|
12
|
+
export interface SdkIntegrationOptions {
|
|
13
|
+
cwd?: string;
|
|
14
|
+
/** Explicit adapter override (e.g. from --analyst); otherwise best-ranked installed CLI. */
|
|
15
|
+
adapter?: CodingAgentAdapter;
|
|
16
|
+
/** Stream the agent's progress to the user (human terminals only). */
|
|
17
|
+
human?: boolean;
|
|
18
|
+
onProgress?: (message: string) => void;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Pick an installed coding-agent CLI and run it, in write mode, to integrate the
|
|
22
|
+
* Moda SDK into the repo. Returns `no-agent` (without throwing) when no CLI is
|
|
23
|
+
* installed so the caller can fall back to writing the static guide.
|
|
24
|
+
*/
|
|
25
|
+
export declare function runSdkIntegration(options?: SdkIntegrationOptions): Promise<SdkIntegrationResult>;
|
|
26
|
+
//# sourceMappingURL=sdk-integration.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/init/sdk-integration.ts — Run an installed coding agent to integrate the
|
|
2
|
+
// Moda SDK directly, instead of leaving a guide for the user to action by hand.
|
|
3
|
+
//
|
|
4
|
+
// This reuses the same coding-agent spawn machinery as `moda harness analyze`
|
|
5
|
+
// (see src/harness.ts), but in write mode: the agent is expected to install the
|
|
6
|
+
// SDK package and edit source to instrument LLM calls.
|
|
7
|
+
import { codingAgentDisplayName, detectAvailableCodingAgents, runCodingAgent, } from '../harness.js';
|
|
8
|
+
import { detectSdkGuideContext, renderSdkIntegrationGuide } from './sdk-guide.js';
|
|
9
|
+
/**
|
|
10
|
+
* Pick an installed coding-agent CLI and run it, in write mode, to integrate the
|
|
11
|
+
* Moda SDK into the repo. Returns `no-agent` (without throwing) when no CLI is
|
|
12
|
+
* installed so the caller can fall back to writing the static guide.
|
|
13
|
+
*/
|
|
14
|
+
export async function runSdkIntegration(options = {}) {
|
|
15
|
+
const cwd = options.cwd ?? process.cwd();
|
|
16
|
+
const available = detectAvailableCodingAgents();
|
|
17
|
+
if (available.length === 0)
|
|
18
|
+
return { status: 'no-agent' };
|
|
19
|
+
const adapter = options.adapter && available.some((item) => item.adapter === options.adapter)
|
|
20
|
+
? options.adapter
|
|
21
|
+
: available[0].adapter;
|
|
22
|
+
const prompt = renderSdkIntegrationPrompt(cwd);
|
|
23
|
+
const human = options.human ?? false;
|
|
24
|
+
const emit = (message) => {
|
|
25
|
+
options.onProgress?.(message);
|
|
26
|
+
if (human)
|
|
27
|
+
process.stderr.write(` ${codingAgentDisplayName(adapter)}: ${message}\n`);
|
|
28
|
+
};
|
|
29
|
+
const run = await runCodingAgent({
|
|
30
|
+
adapter,
|
|
31
|
+
prompt,
|
|
32
|
+
rootDir: cwd,
|
|
33
|
+
mode: 'write',
|
|
34
|
+
stream: human,
|
|
35
|
+
progressToStderr: false,
|
|
36
|
+
onProgress: (message) => emit(message),
|
|
37
|
+
});
|
|
38
|
+
if (run.failedToStart) {
|
|
39
|
+
return { status: 'failed', adapter, detail: `${codingAgentDisplayName(adapter)} CLI failed to start.` };
|
|
40
|
+
}
|
|
41
|
+
if (run.timedOut) {
|
|
42
|
+
return { status: 'failed', adapter, detail: `${codingAgentDisplayName(adapter)} timed out during SDK integration.` };
|
|
43
|
+
}
|
|
44
|
+
if (run.status !== 0) {
|
|
45
|
+
const tail = `${run.stdout}\n${run.stderr}`.trim().slice(-800);
|
|
46
|
+
return {
|
|
47
|
+
status: 'failed',
|
|
48
|
+
adapter,
|
|
49
|
+
detail: `${codingAgentDisplayName(adapter)} exited with status ${run.status ?? 'unknown'}.${tail ? `\n${tail}` : ''}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
return { status: 'integrated', adapter };
|
|
53
|
+
}
|
|
54
|
+
function renderSdkIntegrationPrompt(cwd) {
|
|
55
|
+
const guide = renderSdkIntegrationGuide(detectSdkGuideContext(cwd));
|
|
56
|
+
return [
|
|
57
|
+
'You are running inside `moda init` as a one-shot SDK integration task. The user has',
|
|
58
|
+
'approved letting you install packages and edit source files in THIS repository to add',
|
|
59
|
+
'Moda LLM observability. Do the integration now — do not just describe it.',
|
|
60
|
+
'',
|
|
61
|
+
'Scope and safety rules:',
|
|
62
|
+
'- Only touch this repository (the working directory you were launched in).',
|
|
63
|
+
'- Install the Moda SDK with the package manager this repo already uses.',
|
|
64
|
+
'- Instrument the real LLM call sites; do not invent files or frameworks that are not here.',
|
|
65
|
+
'- Never write MODA_API_KEY (or any secret) into source files, and never create or edit',
|
|
66
|
+
' .env files. The key belongs in the environment / a secret manager at runtime.',
|
|
67
|
+
'- Make the smallest correct change. Match the existing code style and imports.',
|
|
68
|
+
'- If something is ambiguous or the repo has no LLM calls to instrument, stop and explain',
|
|
69
|
+
' rather than guessing.',
|
|
70
|
+
'',
|
|
71
|
+
'Follow this Moda-authored integration spec exactly (it reflects the detected languages,',
|
|
72
|
+
'package managers, providers, and frameworks for this repo):',
|
|
73
|
+
'',
|
|
74
|
+
guide,
|
|
75
|
+
'',
|
|
76
|
+
'When finished, print a short summary of: the package(s) installed, the files you edited,',
|
|
77
|
+
'and any step a human still needs to do (e.g. setting MODA_API_KEY in the deploy env).',
|
|
78
|
+
].join('\n');
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sdk-integration.js.map
|
package/dist/prompts.d.ts
CHANGED
|
@@ -3,4 +3,20 @@ export declare function runPromptsCommand(positionals: string[], flags: Record<s
|
|
|
3
3
|
export declare function initPrompts(options?: {
|
|
4
4
|
silent?: boolean;
|
|
5
5
|
}): Promise<void>;
|
|
6
|
+
export interface PromptSyncResult {
|
|
7
|
+
synced: Array<{
|
|
8
|
+
key: string;
|
|
9
|
+
promptId?: string;
|
|
10
|
+
versionId?: string;
|
|
11
|
+
contentHash: string;
|
|
12
|
+
sourcePath?: string;
|
|
13
|
+
}>;
|
|
14
|
+
dryRun: boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sync the manifest's prompt files to the backend and update the lockfile.
|
|
18
|
+
* Returns the synced list without printing, so callers (e.g. `moda init`)
|
|
19
|
+
* control their own output. `moda prompts sync` wraps this with JSON output.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runPromptSync(flags: Record<string, string>, profileOptions: ProfileIOOptions): Promise<PromptSyncResult>;
|
|
6
22
|
//# sourceMappingURL=prompts.d.ts.map
|
package/dist/prompts.js
CHANGED
|
@@ -59,7 +59,12 @@ async function syncPrompts(flags, profileOptions) {
|
|
|
59
59
|
}
|
|
60
60
|
await syncPromptsOnce(flags, profileOptions);
|
|
61
61
|
}
|
|
62
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Sync the manifest's prompt files to the backend and update the lockfile.
|
|
64
|
+
* Returns the synced list without printing, so callers (e.g. `moda init`)
|
|
65
|
+
* control their own output. `moda prompts sync` wraps this with JSON output.
|
|
66
|
+
*/
|
|
67
|
+
export async function runPromptSync(flags, profileOptions) {
|
|
63
68
|
await initPrompts({ silent: true });
|
|
64
69
|
const prompts = discoverPrompts();
|
|
65
70
|
const sourceCommit = getGitCommit();
|
|
@@ -81,9 +86,13 @@ async function syncPromptsOnce(flags, profileOptions) {
|
|
|
81
86
|
if (!dryRun) {
|
|
82
87
|
writeLock(prompts, response.synced ?? []);
|
|
83
88
|
}
|
|
89
|
+
return { synced: response.synced ?? [], dryRun };
|
|
90
|
+
}
|
|
91
|
+
async function syncPromptsOnce(flags, profileOptions) {
|
|
92
|
+
const result = await runPromptSync(flags, profileOptions);
|
|
84
93
|
printJson({
|
|
85
|
-
synced:
|
|
86
|
-
lockfile: dryRun ? undefined : LOCK_PATH,
|
|
94
|
+
synced: result.synced,
|
|
95
|
+
lockfile: result.dryRun ? undefined : LOCK_PATH,
|
|
87
96
|
});
|
|
88
97
|
}
|
|
89
98
|
async function watchPromptSync(flags, profileOptions) {
|
package/dist/skills.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ParsedArgs } from './cli.js';
|
|
2
|
+
import type { ProfileIOOptions } from './profiles.js';
|
|
2
3
|
interface PullSummary {
|
|
3
4
|
total: number;
|
|
4
5
|
pruned: number;
|
|
@@ -17,6 +18,31 @@ export declare function pullSkills(options?: {
|
|
|
17
18
|
cwd?: string;
|
|
18
19
|
silent?: boolean;
|
|
19
20
|
}): Promise<PullSummary>;
|
|
21
|
+
export interface SkillSyncResult {
|
|
22
|
+
synced: Array<{
|
|
23
|
+
key: string;
|
|
24
|
+
skillId?: string;
|
|
25
|
+
versionId?: string;
|
|
26
|
+
version?: number;
|
|
27
|
+
contentHash: string;
|
|
28
|
+
changed?: boolean;
|
|
29
|
+
sourcePath?: string;
|
|
30
|
+
}>;
|
|
31
|
+
dryRun: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Push locally-authored `.claude/skills/**\/SKILL.md` files up to Moda via
|
|
35
|
+
* `POST /api/skills/sync`. Modeled on `runPromptSync`: returns the synced list
|
|
36
|
+
* without printing so callers (e.g. `moda init`) control their own output.
|
|
37
|
+
* `moda skills sync` wraps this with JSON output.
|
|
38
|
+
*
|
|
39
|
+
* Skills that Moda itself manages — those previously pulled/generated (tracked
|
|
40
|
+
* in `.moda/skills.yml`) and the shipped `moda-cli` skill — are excluded so we
|
|
41
|
+
* only push skills the user authored.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runSkillSync(flags?: Record<string, string>, profileOptions?: ProfileIOOptions, options?: {
|
|
44
|
+
cwd?: string;
|
|
45
|
+
}): Promise<SkillSyncResult>;
|
|
20
46
|
/**
|
|
21
47
|
* `moda skills <subcommand>` dispatcher.
|
|
22
48
|
*/
|
package/dist/skills.js
CHANGED
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
// Skills are managed artifacts. A pull installs the latest server-published
|
|
16
16
|
// generation snapshot, tracked in .moda/skills.yml, and prunes only files that a
|
|
17
17
|
// previous Moda pull wrote.
|
|
18
|
+
import { execFileSync } from 'node:child_process';
|
|
18
19
|
import { createHash } from 'node:crypto';
|
|
19
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
20
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
20
21
|
import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from 'node:path';
|
|
22
|
+
import { callControlAPI } from './api-client.js';
|
|
21
23
|
import { resolveApiKey, resolveIngestUrl, resolveTenantId } from './config.js';
|
|
22
24
|
import { claudeSkillToCursorRule } from './init/rules.js';
|
|
23
25
|
import { detectAgents } from './init/mcp.js';
|
|
@@ -189,6 +191,157 @@ function recordOutcome(summary, status, log, relativePath) {
|
|
|
189
191
|
break;
|
|
190
192
|
}
|
|
191
193
|
}
|
|
194
|
+
const SKILLS_DIR = join('.claude', 'skills');
|
|
195
|
+
const SKILL_FILE = 'SKILL.md';
|
|
196
|
+
const MAX_SKILL_WALK_DEPTH = 4;
|
|
197
|
+
/**
|
|
198
|
+
* Push locally-authored `.claude/skills/**\/SKILL.md` files up to Moda via
|
|
199
|
+
* `POST /api/skills/sync`. Modeled on `runPromptSync`: returns the synced list
|
|
200
|
+
* without printing so callers (e.g. `moda init`) control their own output.
|
|
201
|
+
* `moda skills sync` wraps this with JSON output.
|
|
202
|
+
*
|
|
203
|
+
* Skills that Moda itself manages — those previously pulled/generated (tracked
|
|
204
|
+
* in `.moda/skills.yml`) and the shipped `moda-cli` skill — are excluded so we
|
|
205
|
+
* only push skills the user authored.
|
|
206
|
+
*/
|
|
207
|
+
export async function runSkillSync(flags = {}, profileOptions = {}, options = {}) {
|
|
208
|
+
const cwd = options.cwd ?? process.cwd();
|
|
209
|
+
const dryRun = flags['dry-run'] === 'true';
|
|
210
|
+
const skills = discoverLocalSkills(cwd);
|
|
211
|
+
if (skills.length === 0) {
|
|
212
|
+
return { synced: [], dryRun };
|
|
213
|
+
}
|
|
214
|
+
const sourceCommit = getGitCommit(cwd);
|
|
215
|
+
const response = (await callControlAPI('/skills/sync', {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
body: JSON.stringify({
|
|
218
|
+
skills: skills.map((skill) => ({
|
|
219
|
+
key: skill.key,
|
|
220
|
+
name: skill.name,
|
|
221
|
+
description: skill.description,
|
|
222
|
+
skillMd: skill.skillMd,
|
|
223
|
+
sourcePath: skill.sourcePath,
|
|
224
|
+
contentHash: skill.contentHash,
|
|
225
|
+
sourceCommit,
|
|
226
|
+
})),
|
|
227
|
+
sourceCommit,
|
|
228
|
+
dryRun,
|
|
229
|
+
}),
|
|
230
|
+
}, profileOptions));
|
|
231
|
+
return { synced: response.synced ?? [], dryRun };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Discover user-authored skills under `.claude/skills/`. Excludes Moda-managed
|
|
235
|
+
* skills (the `.moda/skills.yml` managed paths and the shipped `moda-cli`
|
|
236
|
+
* skill) and validates each directory key.
|
|
237
|
+
*/
|
|
238
|
+
function discoverLocalSkills(cwd) {
|
|
239
|
+
const skillsRoot = join(cwd, SKILLS_DIR);
|
|
240
|
+
if (!existsSync(skillsRoot))
|
|
241
|
+
return [];
|
|
242
|
+
const managed = new Set(managedSkillPaths(cwd));
|
|
243
|
+
const skills = [];
|
|
244
|
+
for (const absPath of walkSkillFiles(skillsRoot, 0)) {
|
|
245
|
+
const sourcePath = relative(cwd, absPath).replace(/\\/g, '/');
|
|
246
|
+
if (managed.has(sourcePath))
|
|
247
|
+
continue;
|
|
248
|
+
const key = basename(dirname(absPath));
|
|
249
|
+
// Never re-upload Moda's own shipped skill; only accept plain identifiers.
|
|
250
|
+
if (key === 'moda-cli' || !/^[A-Za-z0-9_-]+$/.test(key))
|
|
251
|
+
continue;
|
|
252
|
+
let content;
|
|
253
|
+
try {
|
|
254
|
+
content = readFileSync(absPath, 'utf8');
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (!content.trim())
|
|
260
|
+
continue;
|
|
261
|
+
const { name, description } = parseSkillFrontmatter(content, key);
|
|
262
|
+
skills.push({
|
|
263
|
+
key,
|
|
264
|
+
name,
|
|
265
|
+
description,
|
|
266
|
+
skillMd: content,
|
|
267
|
+
sourcePath,
|
|
268
|
+
contentHash: sha256(content),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return skills.sort((a, b) => a.key.localeCompare(b.key));
|
|
272
|
+
}
|
|
273
|
+
/** Managed SKILL.md paths recorded by a previous `moda skills pull`. */
|
|
274
|
+
function managedSkillPaths(cwd) {
|
|
275
|
+
const manifest = readSkillsManifest(cwd);
|
|
276
|
+
if (!manifest)
|
|
277
|
+
return [];
|
|
278
|
+
return manifest.managedPaths.filter((path) => path.startsWith('.claude/skills/') && path.endsWith('/SKILL.md'));
|
|
279
|
+
}
|
|
280
|
+
/** Recursively find `SKILL.md` files, never following symlinks. */
|
|
281
|
+
function walkSkillFiles(dir, depth) {
|
|
282
|
+
if (depth > MAX_SKILL_WALK_DEPTH)
|
|
283
|
+
return [];
|
|
284
|
+
let entries;
|
|
285
|
+
try {
|
|
286
|
+
entries = readdirSync(dir);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
const files = [];
|
|
292
|
+
for (const entry of entries) {
|
|
293
|
+
const fullPath = join(dir, entry);
|
|
294
|
+
let stat;
|
|
295
|
+
try {
|
|
296
|
+
stat = lstatSync(fullPath);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (stat.isSymbolicLink())
|
|
302
|
+
continue;
|
|
303
|
+
if (stat.isDirectory()) {
|
|
304
|
+
files.push(...walkSkillFiles(fullPath, depth + 1));
|
|
305
|
+
}
|
|
306
|
+
else if (stat.isFile() && entry === SKILL_FILE) {
|
|
307
|
+
files.push(fullPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return files;
|
|
311
|
+
}
|
|
312
|
+
/** Pull `name`/`description` from a SKILL.md YAML frontmatter block. */
|
|
313
|
+
function parseSkillFrontmatter(raw, fallbackKey) {
|
|
314
|
+
let name = fallbackKey;
|
|
315
|
+
let description;
|
|
316
|
+
if (raw.startsWith('---')) {
|
|
317
|
+
const end = raw.indexOf('\n---', 3);
|
|
318
|
+
if (end !== -1) {
|
|
319
|
+
const frontmatter = raw.slice(3, end);
|
|
320
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
321
|
+
const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
322
|
+
if (nameMatch)
|
|
323
|
+
name = stripQuotes(nameMatch[1].trim()) || fallbackKey;
|
|
324
|
+
if (descMatch)
|
|
325
|
+
description = stripQuotes(descMatch[1].trim()) || undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return { name, description };
|
|
329
|
+
}
|
|
330
|
+
function stripQuotes(value) {
|
|
331
|
+
return value.replace(/^["']|["']$/g, '');
|
|
332
|
+
}
|
|
333
|
+
function getGitCommit(cwd) {
|
|
334
|
+
try {
|
|
335
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
336
|
+
cwd,
|
|
337
|
+
encoding: 'utf8',
|
|
338
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
339
|
+
}).trim();
|
|
340
|
+
}
|
|
341
|
+
catch {
|
|
342
|
+
return undefined;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
192
345
|
/**
|
|
193
346
|
* `moda skills <subcommand>` dispatcher.
|
|
194
347
|
*/
|
|
@@ -218,8 +371,13 @@ export async function runSkillsCommand(args) {
|
|
|
218
371
|
}
|
|
219
372
|
return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
|
|
220
373
|
}
|
|
374
|
+
if (sub === 'sync') {
|
|
375
|
+
const result = await runSkillSync(args.flags, { profile: args.flags.profile });
|
|
376
|
+
console.log(JSON.stringify({ synced: result.synced, dryRun: result.dryRun }));
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
221
379
|
if (sub !== 'pull') {
|
|
222
|
-
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]');
|
|
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]');
|
|
223
381
|
}
|
|
224
382
|
const status = (args.flags.status ?? 'approved').toLowerCase();
|
|
225
383
|
if (!VALID_STATUSES.has(status)) {
|