@moda-ai/cli 1.20.1 → 1.22.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/README.md +3 -0
- package/dist/api-client.d.ts +1 -1
- package/dist/api-client.js +2 -2
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +109 -1
- package/dist/init/index.d.ts +2 -0
- package/dist/init/index.js +101 -26
- package/dist/init/sdk-catalog.d.ts +10 -0
- package/dist/init/sdk-catalog.js +80 -0
- package/dist/init/sdk-guide.d.ts +5 -34
- package/dist/init/sdk-guide.js +74 -290
- package/dist/init/sdk-integration.d.ts +43 -44
- package/dist/init/sdk-integration.js +796 -133
- package/dist/init/sdk-protocol.d.ts +135 -0
- package/dist/init/sdk-protocol.js +152 -0
- package/dist/init/sdk-verification.d.ts +27 -0
- package/dist/init/sdk-verification.js +123 -0
- package/dist/manifest.js +1 -0
- package/dist/output.d.ts +8 -0
- package/dist/output.js +11 -0
- package/dist/schemas.d.ts +34 -0
- package/dist/schemas.js +30 -0
- package/package.json +1 -1
- package/skills/integration/index.json +2 -2
- package/skills/moda-cli/SKILL.md +29 -1
- package/dist/init/skill-resolver.d.ts +0 -19
- package/dist/init/skill-resolver.js +0 -78
package/README.md
CHANGED
|
@@ -58,6 +58,9 @@ moda tool-failures # Tool failure overview
|
|
|
58
58
|
moda tool-failure-detail <tool_name> # Per-tool failure detail
|
|
59
59
|
moda tool-failure-detail <tool_name> --include-window --window=1
|
|
60
60
|
|
|
61
|
+
moda feedback "cluster label looks wrong" # Help us improve: flag wrong/missing data
|
|
62
|
+
moda feedback "..." --category=missing_data --conversation-id=<id>
|
|
63
|
+
|
|
61
64
|
# Prompt management
|
|
62
65
|
moda prompts init # Create .moda/prompts.yml
|
|
63
66
|
moda prompts status # Read-only local prompt status
|
package/dist/api-client.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare class ApiError extends Error {
|
|
|
14
14
|
export declare class NetworkError extends Error {
|
|
15
15
|
constructor(message: string);
|
|
16
16
|
}
|
|
17
|
-
export declare function callDataAPI(endpoint: string): Promise<unknown>;
|
|
17
|
+
export declare function callDataAPI(endpoint: string, init?: RequestInit, requestOptions?: ModaRequestOptions): Promise<unknown>;
|
|
18
18
|
/**
|
|
19
19
|
* Per-call transport overrides. Used by expensive, long-running, non-idempotent
|
|
20
20
|
* endpoints (e.g. the agent `ask` POST) that need a longer timeout than the
|
package/dist/api-client.js
CHANGED
|
@@ -31,10 +31,10 @@ export class NetworkError extends Error {
|
|
|
31
31
|
function isRetryable(status) {
|
|
32
32
|
return status === 429 || status >= 500;
|
|
33
33
|
}
|
|
34
|
-
export async function callDataAPI(endpoint) {
|
|
34
|
+
export async function callDataAPI(endpoint, init = { method: 'GET' }, requestOptions = {}) {
|
|
35
35
|
apiRequestCount++;
|
|
36
36
|
const url = `${resolveModaBaseUrl()}/api/v1/data${endpoint}`;
|
|
37
|
-
return callModaAPI(url, {
|
|
37
|
+
return callModaAPI(url, init, {}, requestOptions);
|
|
38
38
|
}
|
|
39
39
|
export async function callControlAPI(endpoint, options = {}, profileOptions = {}, requestOptions = {}) {
|
|
40
40
|
const url = `${resolveModaBaseUrl(profileOptions)}/api${endpoint}`;
|
package/dist/cli.d.ts
CHANGED
|
@@ -21,6 +21,12 @@ export declare function flagsToArgs(flags: Record<string, string>, positionalKey
|
|
|
21
21
|
* reusing this for a command with positionals.
|
|
22
22
|
*/
|
|
23
23
|
export declare function buildResumeCommand(command: string, flags: Record<string, string>): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Coarse caller identity for the feedback body's `agent` field. Same
|
|
26
|
+
* truthy-env semantics as the agent-environment detection in output.ts;
|
|
27
|
+
* never sourced from flags, because `--agent` is the output-mode flag.
|
|
28
|
+
*/
|
|
29
|
+
export declare function detectCallerAgent(env: NodeJS.ProcessEnv): string;
|
|
24
30
|
export declare function runCommand(command: string, positional: string | undefined, flags: Record<string, string>, positionals?: string[], context?: CommandContext): Promise<number>;
|
|
25
31
|
interface CommandDispatchResult {
|
|
26
32
|
exitCode: number;
|
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { validateConfig, VERSION, resolveApiKey } from './config.js';
|
|
4
4
|
import { callDataAPI, ApiError, getApiRequestCount, resetApiRequestCount, } from './api-client.js';
|
|
5
|
-
import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, } from './schemas.js';
|
|
5
|
+
import { OverviewSchema, ClustersSchema, ClusterConversationsSchema, ConversationsSchema, SearchSchema, WorldStateSchema, ContextSchema, AuditSchema, FrustrationsSchema, ToolFailuresSchema, ToolFailureDetailSchema, FeedbackSchema, FEEDBACK_CATEGORIES, FEEDBACK_SEVERITIES, } from './schemas.js';
|
|
6
6
|
import { withFrustrationAnchors, withToolFailureAnchors, embeddedWindowSize, asRecord, asString, asNumber } from './transforms.js';
|
|
7
7
|
import { runPromptsCommand } from './prompts.js';
|
|
8
8
|
import { runSkillsCommand } from './skills.js';
|
|
@@ -170,6 +170,7 @@ Commands:
|
|
|
170
170
|
frustrations Get user frustration detections
|
|
171
171
|
tool-failures Get tool failure overview
|
|
172
172
|
tool-failure-detail <tool_name> Get per-tool failure detail
|
|
173
|
+
feedback "<note>" Flag wrong/missing data or CLI quirks to the Moda team
|
|
173
174
|
|
|
174
175
|
Prompt management:
|
|
175
176
|
prompts init Create .moda/prompts.yml
|
|
@@ -235,6 +236,16 @@ World-state flags:
|
|
|
235
236
|
--summary-only Return segment summaries only (omit the event stream)
|
|
236
237
|
--event-limit=N Max world-state events to return (1-5000, default 2000)
|
|
237
238
|
|
|
239
|
+
Feedback flags:
|
|
240
|
+
--category=CAT bad_cluster_label|mismatched_frustration|missing_data|noisy_data|
|
|
241
|
+
wrong_tool_failure|incorrect_loop|api_quirk|other (default other)
|
|
242
|
+
--severity=info|low|medium|high How bad it is (default low)
|
|
243
|
+
--conversation-id=ID Attach the conversation you were looking at
|
|
244
|
+
--cluster-id=ID Attach a cluster node id
|
|
245
|
+
--tool-name=NAME Attach a tool name
|
|
246
|
+
--run-id=ID Attach a run id
|
|
247
|
+
(set MODA_CLI_TIPS=0 to hide the feedback tip line)
|
|
248
|
+
|
|
238
249
|
Init flags:
|
|
239
250
|
--yes Accept all defaults (non-interactive)
|
|
240
251
|
--analyst=auto|claude|codex|cursor|local-scan
|
|
@@ -310,6 +321,7 @@ Examples:
|
|
|
310
321
|
moda search "refund flow" --mode=semantic --time-range=7d --limit=10
|
|
311
322
|
moda world-state <conversation_id> --summary-only
|
|
312
323
|
moda frustrations --days-back=14 --limit=20
|
|
324
|
+
moda feedback "cluster label looks wrong" --category=bad_cluster_label --cluster-id=<node_id>
|
|
313
325
|
moda context <conversation_id> --window=3
|
|
314
326
|
moda audit <conversation_id|trace_id>
|
|
315
327
|
moda audit <trace_id> --kind=trace --json
|
|
@@ -421,6 +433,21 @@ function recordConversationsTelemetry(context, data) {
|
|
|
421
433
|
has_more: hasMore === true ? 'yes' : hasMore === false ? 'no' : 'na',
|
|
422
434
|
});
|
|
423
435
|
}
|
|
436
|
+
/**
|
|
437
|
+
* Coarse caller identity for the feedback body's `agent` field. Same
|
|
438
|
+
* truthy-env semantics as the agent-environment detection in output.ts;
|
|
439
|
+
* never sourced from flags, because `--agent` is the output-mode flag.
|
|
440
|
+
*/
|
|
441
|
+
export function detectCallerAgent(env) {
|
|
442
|
+
const truthy = (value) => value != null && value !== '' && value !== '0' && value.toLowerCase() !== 'false';
|
|
443
|
+
if (truthy(env.CLAUDECODE))
|
|
444
|
+
return 'claude-code';
|
|
445
|
+
if (truthy(env.CURSOR))
|
|
446
|
+
return 'cursor';
|
|
447
|
+
if (truthy(env.MODA_AGENT) || truthy(env.CI))
|
|
448
|
+
return 'agent';
|
|
449
|
+
return 'human';
|
|
450
|
+
}
|
|
424
451
|
export async function runCommand(command, positional, flags, positionals = positional ? [positional] : [], context = createCommandContext({ command, positional, positionals, flags })) {
|
|
425
452
|
switch (command) {
|
|
426
453
|
case 'prompts': {
|
|
@@ -624,6 +651,54 @@ export async function runCommand(command, positional, flags, positionals = posit
|
|
|
624
651
|
await printWithOptionalWindows(withToolFailureAnchors(data), 'examples', params.include_window === true, params.window ?? 1, context.output);
|
|
625
652
|
break;
|
|
626
653
|
}
|
|
654
|
+
case 'feedback': {
|
|
655
|
+
// Join every positional so an unquoted multi-word note is kept whole
|
|
656
|
+
// instead of silently truncating to the first word.
|
|
657
|
+
const note = positionals.join(' ').trim();
|
|
658
|
+
if (note === '') {
|
|
659
|
+
throw new CliInputError('<note> is required', `Usage: moda feedback "<note>" [--category=${FEEDBACK_CATEGORIES.join('|')}] [--severity=${FEEDBACK_SEVERITIES.join('|')}] [--conversation-id=ID] [--cluster-id=ID] [--tool-name=NAME] [--run-id=ID]`);
|
|
660
|
+
}
|
|
661
|
+
// Raw flag strings, not flagsToArgs: its numeric coercion would turn a
|
|
662
|
+
// purely numeric id ("--conversation-id=873459") into a number that
|
|
663
|
+
// FeedbackSchema's z.string() rejects.
|
|
664
|
+
const params = FeedbackSchema.parse({
|
|
665
|
+
note,
|
|
666
|
+
...(flags.category !== undefined ? { category: flags.category } : {}),
|
|
667
|
+
...(flags.severity !== undefined ? { severity: flags.severity } : {}),
|
|
668
|
+
...(flags['conversation-id'] !== undefined ? { conversation_id: flags['conversation-id'] } : {}),
|
|
669
|
+
...(flags['cluster-id'] !== undefined ? { cluster_id: flags['cluster-id'] } : {}),
|
|
670
|
+
...(flags['tool-name'] !== undefined ? { tool_name: flags['tool-name'] } : {}),
|
|
671
|
+
...(flags['run-id'] !== undefined ? { run_id: flags['run-id'] } : {}),
|
|
672
|
+
});
|
|
673
|
+
const refs = {};
|
|
674
|
+
if (params.conversation_id)
|
|
675
|
+
refs.conversation_id = params.conversation_id;
|
|
676
|
+
if (params.cluster_id)
|
|
677
|
+
refs.cluster_id = params.cluster_id;
|
|
678
|
+
if (params.tool_name)
|
|
679
|
+
refs.tool_name = params.tool_name;
|
|
680
|
+
if (params.run_id)
|
|
681
|
+
refs.run_id = params.run_id;
|
|
682
|
+
const category = params.category ?? 'other';
|
|
683
|
+
const severity = params.severity ?? 'low';
|
|
684
|
+
// retries: 0. The backend mints a fresh feedback_id per request and
|
|
685
|
+
// moda.agent_feedback has no dedup layer, so a retried POST after a
|
|
686
|
+
// lost response would insert duplicate rows.
|
|
687
|
+
const data = await callDataAPI('/feedback', {
|
|
688
|
+
method: 'POST',
|
|
689
|
+
body: JSON.stringify({
|
|
690
|
+
category,
|
|
691
|
+
severity,
|
|
692
|
+
source: 'cli',
|
|
693
|
+
agent: detectCallerAgent(process.env),
|
|
694
|
+
cli_version: VERSION,
|
|
695
|
+
note: params.note,
|
|
696
|
+
...(Object.keys(refs).length > 0 ? { refs } : {}),
|
|
697
|
+
}),
|
|
698
|
+
}, { retries: 0 });
|
|
699
|
+
context.output.writeData({ submitted: true, category, severity, ...(asRecord(data) ?? {}) }, { summary: 'Feedback submitted. Thank you for helping us improve!' });
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
627
702
|
default:
|
|
628
703
|
throw new CliInputError(`Unknown command '${command}'`, 'Run "moda --help" for usage information.');
|
|
629
704
|
}
|
|
@@ -1036,6 +1111,16 @@ export const commandRegistry = createCommandRegistry([
|
|
|
1036
1111
|
examples: ['moda tool-failure-detail search --include-window'],
|
|
1037
1112
|
...dataApiDefaults,
|
|
1038
1113
|
}),
|
|
1114
|
+
legacyCommand({
|
|
1115
|
+
name: 'feedback',
|
|
1116
|
+
description: 'Flag wrong/missing data or CLI quirks to the Moda team',
|
|
1117
|
+
examples: [
|
|
1118
|
+
'moda feedback "cluster label looks wrong" --category=bad_cluster_label --cluster-id=<node_id>',
|
|
1119
|
+
'moda feedback "search finds nothing for a conversation I can open" --category=missing_data --conversation-id=<id>',
|
|
1120
|
+
],
|
|
1121
|
+
...dataApiDefaults,
|
|
1122
|
+
mutability: 'write',
|
|
1123
|
+
}),
|
|
1039
1124
|
legacyCommand({
|
|
1040
1125
|
name: 'prompts',
|
|
1041
1126
|
description: 'Manage prompt versions and labels',
|
|
@@ -1111,6 +1196,28 @@ export function printCliError(error) {
|
|
|
1111
1196
|
safeWriteStderr(`Error: ${String(error)}\n`);
|
|
1112
1197
|
}
|
|
1113
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Human-TTY twin of the envelope `meta.tip`: one dim stderr line after a
|
|
1201
|
+
* successful command. stderr only (stdout stays machine-readable), TTY only
|
|
1202
|
+
* (pipes/CI stay noise-free), and MODA_CLI_TIPS=0 opts out.
|
|
1203
|
+
*/
|
|
1204
|
+
function printFeedbackTip(commandName, context) {
|
|
1205
|
+
if (context.outputMode !== 'human' || context.output.quiet)
|
|
1206
|
+
return;
|
|
1207
|
+
if (commandName === 'feedback')
|
|
1208
|
+
return;
|
|
1209
|
+
if (!process.stderr.isTTY)
|
|
1210
|
+
return;
|
|
1211
|
+
// Intentional strict `=== '0'` (NOT the permissive truthy() used by
|
|
1212
|
+
// detectCallerAgent): `MODA_CLI_TIPS=0` is the one documented opt-out form
|
|
1213
|
+
// (manifest env list + docs), so keep this exact-match — don't "fix" it to
|
|
1214
|
+
// match the other guards or it silently changes the published interface.
|
|
1215
|
+
if (process.env.MODA_CLI_TIPS === '0')
|
|
1216
|
+
return;
|
|
1217
|
+
const text = 'Tip: Use `moda feedback "<note>"` to help us improve!';
|
|
1218
|
+
const colorOk = !('NO_COLOR' in process.env) && process.env.TERM !== 'dumb';
|
|
1219
|
+
safeWriteStderr(`\n${colorOk ? `\x1b[2m${text}\x1b[0m` : text}\n`);
|
|
1220
|
+
}
|
|
1114
1221
|
function printAgentCliError(command, error, mode = 'agent') {
|
|
1115
1222
|
const output = createCliOutput({
|
|
1116
1223
|
command: command || 'unknown',
|
|
@@ -1236,6 +1343,7 @@ export async function dispatchParsedCommand(parsed, startedAt = Date.now()) {
|
|
|
1236
1343
|
if (!updateCheckDisabled) {
|
|
1237
1344
|
printUpdateNotice();
|
|
1238
1345
|
}
|
|
1346
|
+
printFeedbackTip(definition.name, context);
|
|
1239
1347
|
return finishRun(context, { kind: 'completed', exitCode: result.exitCode });
|
|
1240
1348
|
}
|
|
1241
1349
|
catch (error) {
|
package/dist/init/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { type SdkIntegrationOutcome } from './sdk-integration.js';
|
|
1
2
|
export interface InitResult {
|
|
2
3
|
apiKey: string;
|
|
3
4
|
tenantName: string;
|
|
4
5
|
phases: InitPhaseResult[];
|
|
5
6
|
setupPlan: InitSetupAction[];
|
|
7
|
+
sdk_integration?: SdkIntegrationOutcome;
|
|
6
8
|
}
|
|
7
9
|
export interface InitPhaseResult {
|
|
8
10
|
id: 'welcome' | 'auth' | 'workspace' | 'harness' | 'plan' | 'apply' | 'validate';
|
package/dist/init/index.js
CHANGED
|
@@ -16,9 +16,10 @@ import { initPrompts, runPromptSync } from '../prompts.js';
|
|
|
16
16
|
import { runSkillSync } from '../skills.js';
|
|
17
17
|
import { SDK_GUIDE_PATH, writeSdkIntegrationGuide } from './sdk-guide.js';
|
|
18
18
|
import { runSdkIntegration } from './sdk-integration.js';
|
|
19
|
+
import { fetchAuditWithApiKey } from './sdk-verification.js';
|
|
19
20
|
import { InitProgressBoard } from './progress-board.js';
|
|
20
21
|
import { createCommandContext } from '../command-context.js';
|
|
21
|
-
import { resolveModaBaseUrl, VERSION } from '../config.js';
|
|
22
|
+
import { resolveModaBaseUrl, resolveIngestUrl, VERSION } from '../config.js';
|
|
22
23
|
import { CliAuthError, CliInputError } from '../errors.js';
|
|
23
24
|
import { describeSyncIdentity, describeSyncTenantLabel, detectAvailableCodingAgents, isDeterministicLocalAnalyst, runHarnessCommand, } from '../harness.js';
|
|
24
25
|
import { pollRemoteAnalyzeUntilDone, startRemoteAnalyze, } from '../harness-remote.js';
|
|
@@ -516,38 +517,35 @@ export async function runInit(flags, options = {}) {
|
|
|
516
517
|
markApplied(setupPlan, 'harness-rescan');
|
|
517
518
|
}
|
|
518
519
|
const sdkAction = setupPlan.find((action) => action.category === 'sdk');
|
|
520
|
+
let sdkIntegration;
|
|
519
521
|
if (sdkAction?.approved === true) {
|
|
520
522
|
laneBoard?.startLane('sdk', 'SDK integration', {
|
|
521
523
|
detail: 'launching your coding agent',
|
|
522
524
|
expectation: 'usually 3-10 min',
|
|
523
525
|
});
|
|
524
526
|
emit('apply', 'Running your coding agent to integrate the Moda SDK.');
|
|
525
|
-
const
|
|
527
|
+
const verificationBaseUrl = resolveModaBaseUrl({ profile });
|
|
528
|
+
sdkIntegration = await runSdkIntegration({
|
|
529
|
+
apiKey,
|
|
530
|
+
ingestUrl: resolveIngestUrl({ profile }),
|
|
531
|
+
fetchAudit: (id) => fetchAuditWithApiKey(id, apiKey, verificationBaseUrl),
|
|
526
532
|
onProgress: (message) => {
|
|
527
533
|
laneBoard?.update('sdk', message);
|
|
528
534
|
emitLaneProgress('apply', 'sdk', message);
|
|
529
535
|
},
|
|
536
|
+
onPhase: (phase, message) => {
|
|
537
|
+
laneBoard?.update('sdk', message);
|
|
538
|
+
emitLaneProgress('apply', 'sdk', message);
|
|
539
|
+
},
|
|
530
540
|
// Presence of onEvent selects the streaming agent invocation so tool
|
|
531
541
|
// cards arrive incrementally; onProgress above receives the rendered
|
|
532
542
|
// cards (describeCodingAgentEvent), which is all the lane needs.
|
|
533
543
|
onEvent: () => { },
|
|
534
544
|
});
|
|
535
|
-
|
|
536
|
-
laneBoard
|
|
537
|
-
|
|
538
|
-
}
|
|
539
|
-
else if (result.status === 'no-agent') {
|
|
540
|
-
const guidePath = writeSdkIntegrationGuide();
|
|
541
|
-
sdkAction.files = [guidePath];
|
|
542
|
-
laneBoard?.warn('sdk', `no coding agent CLI found; wrote ${guidePath} to integrate by hand`);
|
|
543
|
-
markApplied(setupPlan, 'sdk');
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
const guidePath = writeSdkIntegrationGuide();
|
|
547
|
-
sdkAction.files = [guidePath];
|
|
548
|
-
laneBoard?.warn('sdk', `${result.adapter} did not complete; wrote ${guidePath} to finish manually`);
|
|
549
|
-
markApplied(setupPlan, 'sdk');
|
|
550
|
-
}
|
|
545
|
+
applySdkIntegrationOutcome(setupPlan, sdkIntegration, {
|
|
546
|
+
laneBoard,
|
|
547
|
+
writeGuide: () => writeSdkIntegrationGuide(rootDir),
|
|
548
|
+
});
|
|
551
549
|
}
|
|
552
550
|
else {
|
|
553
551
|
markApplied(setupPlan, 'sdk', 'User skipped SDK integration.');
|
|
@@ -627,7 +625,7 @@ export async function runInit(flags, options = {}) {
|
|
|
627
625
|
const offlineValidation = isTruthyFlag(flagValue(flags, 'offline'));
|
|
628
626
|
log(renderInitStep(5, 5, 'Verify', offlineValidation
|
|
629
627
|
? 'Running local doctor checks so the setup leaves a clear state.'
|
|
630
|
-
: 'Running Moda doctor online
|
|
628
|
+
: 'Running Moda doctor online as a tenant health check (not proof of SDK integration).'));
|
|
631
629
|
emit('validate', `Running Moda doctor diagnostics (${offlineValidation ? 'offline' : 'online'}).`);
|
|
632
630
|
const doctorReport = await buildDoctorReport(createCommandContext({
|
|
633
631
|
command: 'doctor',
|
|
@@ -665,7 +663,7 @@ export async function runInit(flags, options = {}) {
|
|
|
665
663
|
skipped: setupPlan.filter((item) => item.status === 'skipped' || item.status === 'manual'),
|
|
666
664
|
telemetrySeen,
|
|
667
665
|
}));
|
|
668
|
-
return { apiKey, tenantName, phases, setupPlan };
|
|
666
|
+
return { apiKey, tenantName, phases, setupPlan, ...(sdkIntegration ? { sdk_integration: sdkIntegration } : {}) };
|
|
669
667
|
}
|
|
670
668
|
function buildSetupPlan(options) {
|
|
671
669
|
return [
|
|
@@ -716,15 +714,13 @@ function buildSetupPlan(options) {
|
|
|
716
714
|
},
|
|
717
715
|
{
|
|
718
716
|
category: 'sdk',
|
|
719
|
-
title: '
|
|
717
|
+
title: 'Launch your coding agent to integrate the Moda SDK',
|
|
720
718
|
status: options.integrateSdk ? 'planned' : 'manual',
|
|
721
719
|
approved: options.integrateSdk,
|
|
722
|
-
// Files are repo-specific and unknown ahead of the agent run; the fallback
|
|
723
|
-
// path rewrites this to the guide path when no agent CLI is available.
|
|
724
720
|
files: [],
|
|
725
|
-
command: 'moda init',
|
|
721
|
+
command: 'moda init --sdk-integrate',
|
|
726
722
|
reason: options.integrateSdk
|
|
727
|
-
? '
|
|
723
|
+
? 'Your agent investigates the repo, applies matching integration skill(s), installs packages, runs repo checks, sends one verification request, and Moda verifies the exact trace. Your Moda API key is provided to the agent\'s environment for the verification run.'
|
|
728
724
|
: `SDK integration stays manual; see ${SDK_GUIDE_PATH} if you generate the guide.`,
|
|
729
725
|
},
|
|
730
726
|
{
|
|
@@ -758,7 +754,7 @@ function buildSetupPlan(options) {
|
|
|
758
754
|
},
|
|
759
755
|
{
|
|
760
756
|
category: 'validation',
|
|
761
|
-
title: 'Run Moda doctor online
|
|
757
|
+
title: 'Run Moda doctor online as a tenant health check',
|
|
762
758
|
status: 'planned',
|
|
763
759
|
approved: true,
|
|
764
760
|
files: [],
|
|
@@ -778,6 +774,85 @@ function markApplied(setupPlan, category, skipReason) {
|
|
|
778
774
|
if (action.approved)
|
|
779
775
|
action.status = 'applied';
|
|
780
776
|
}
|
|
777
|
+
function markManual(setupPlan, category, reason) {
|
|
778
|
+
const action = setupPlan.find((item) => item.category === category);
|
|
779
|
+
if (!action)
|
|
780
|
+
return;
|
|
781
|
+
action.status = 'manual';
|
|
782
|
+
action.reason = reason;
|
|
783
|
+
}
|
|
784
|
+
function applySdkIntegrationOutcome(setupPlan, outcome, deps) {
|
|
785
|
+
const sdkAction = setupPlan.find((action) => action.category === 'sdk');
|
|
786
|
+
if (!sdkAction)
|
|
787
|
+
return;
|
|
788
|
+
const { laneBoard, writeGuide } = deps;
|
|
789
|
+
const verificationId = outcome.verification_id ?? 'unknown';
|
|
790
|
+
const firstRemaining = outcome.agent_result?.remaining_actions?.[0];
|
|
791
|
+
switch (outcome.status) {
|
|
792
|
+
case 'integrated_verified': {
|
|
793
|
+
const llmSpans = outcome.verification?.llm_spans ?? 0;
|
|
794
|
+
laneBoard?.complete('sdk', `verified — trace reached Moda (${llmSpans} llm span(s), conversation ${verificationId})`);
|
|
795
|
+
markApplied(setupPlan, 'sdk');
|
|
796
|
+
if (outcome.agent_result?.files_changed?.length) {
|
|
797
|
+
sdkAction.files = [...new Set(outcome.agent_result.files_changed)].slice(0, 20);
|
|
798
|
+
}
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case 'telemetry_not_observed':
|
|
802
|
+
laneBoard?.warn('sdk', `integration applied but the verification trace was not observed; check MODA_API_KEY at runtime, then moda audit ${verificationId}`);
|
|
803
|
+
markManual(setupPlan, 'sdk', outcome.detail ?? 'Verification trace was not observed.');
|
|
804
|
+
break;
|
|
805
|
+
case 'needs_human_input': {
|
|
806
|
+
const securityLead = outcome.security_findings?.length
|
|
807
|
+
? `Security: possible API key leak in ${outcome.security_findings.join(', ')} — remove the key, rotate it, and keep secrets in the environment. `
|
|
808
|
+
: '';
|
|
809
|
+
const note = firstRemaining ?? outcome.detail ?? 'Additional human input required.';
|
|
810
|
+
laneBoard?.warn('sdk', `${securityLead}${note}`);
|
|
811
|
+
markManual(setupPlan, 'sdk', `${securityLead}${note}`.trim());
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case 'nothing_to_integrate': {
|
|
815
|
+
if (outcome.verification?.observed === true) {
|
|
816
|
+
laneBoard?.complete('sdk', outcome.detail ?? 'existing integration verified — trace reached Moda');
|
|
817
|
+
markApplied(setupPlan, 'sdk');
|
|
818
|
+
sdkAction.reason = outcome.detail ?? 'Existing Moda integration verified.';
|
|
819
|
+
break;
|
|
820
|
+
}
|
|
821
|
+
// With a smoke attempt the verification id is real and auditable (the
|
|
822
|
+
// trace may land late); without one the user must pick their own id.
|
|
823
|
+
const reason = outcome.verification
|
|
824
|
+
? 'agent reports Moda is already integrated but the verification trace was not observed; ' +
|
|
825
|
+
`check MODA_API_KEY at runtime, then \`moda audit ${verificationId}\``
|
|
826
|
+
: 'agent reports Moda is already integrated — not independently verified; ' +
|
|
827
|
+
'send a real request through your app with a conversation id you choose, then run `moda audit` with that id';
|
|
828
|
+
laneBoard?.warn('sdk', reason);
|
|
829
|
+
markManual(setupPlan, 'sdk', reason);
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
case 'agent_unavailable': {
|
|
833
|
+
const guidePath = writeGuide();
|
|
834
|
+
sdkAction.files = [guidePath];
|
|
835
|
+
laneBoard?.warn('sdk', `no coding agent CLI found; wrote ${guidePath} to integrate by hand`);
|
|
836
|
+
markManual(setupPlan, 'sdk', `No coding agent CLI found; wrote ${guidePath}.`);
|
|
837
|
+
break;
|
|
838
|
+
}
|
|
839
|
+
case 'cancelled':
|
|
840
|
+
laneBoard?.fail('sdk', outcome.detail ?? 'SDK integration cancelled');
|
|
841
|
+
markManual(setupPlan, 'sdk', outcome.detail ?? 'SDK integration cancelled.');
|
|
842
|
+
break;
|
|
843
|
+
case 'repository_checks_failed':
|
|
844
|
+
case 'smoke_execution_failed':
|
|
845
|
+
case 'agent_failed':
|
|
846
|
+
default: {
|
|
847
|
+
const guidePath = writeGuide();
|
|
848
|
+
sdkAction.files = [guidePath];
|
|
849
|
+
const detail = outcome.detail ?? 'SDK integration did not complete.';
|
|
850
|
+
laneBoard?.warn('sdk', `${detail}; wrote ${guidePath} to finish manually`);
|
|
851
|
+
markManual(setupPlan, 'sdk', `${detail}; wrote ${guidePath}.`);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
}
|
|
781
856
|
function changedFiles(setupPlan) {
|
|
782
857
|
return [...new Set(setupPlan
|
|
783
858
|
.filter((action) => action.status === 'applied')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type BundledSkillIndex } from '../skills-bundled.js';
|
|
2
|
+
export interface MaterializedSdkCatalog {
|
|
3
|
+
dir: string;
|
|
4
|
+
relativeDir: string;
|
|
5
|
+
index: BundledSkillIndex;
|
|
6
|
+
}
|
|
7
|
+
export declare function materializeSdkCatalog(cwd: string, runId: string): MaterializedSdkCatalog;
|
|
8
|
+
export declare function cleanupSdkCatalog(dir: string): void;
|
|
9
|
+
export declare function renderCatalogListing(index: BundledSkillIndex): string;
|
|
10
|
+
//# sourceMappingURL=sdk-catalog.d.ts.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// src/init/sdk-catalog.ts — Ephemeral, run-scoped materialization of the
|
|
2
|
+
// complete bundled SDK integration-skill catalog for a coding agent to browse.
|
|
3
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, } from 'node:fs';
|
|
4
|
+
import { join, relative, resolve } from 'node:path';
|
|
5
|
+
import { bundledSkillsDir, readBundledIndex, } from '../skills-bundled.js';
|
|
6
|
+
export function materializeSdkCatalog(cwd, runId) {
|
|
7
|
+
if (!/^[a-z0-9-]+$/.test(runId)) {
|
|
8
|
+
throw new Error(`Invalid SDK catalog run id: ${JSON.stringify(runId).slice(0, 80)}`);
|
|
9
|
+
}
|
|
10
|
+
const sourceDir = bundledSkillsDir();
|
|
11
|
+
const indexPath = join(sourceDir, 'index.json');
|
|
12
|
+
if (!existsSync(sourceDir) || !existsSync(indexPath)) {
|
|
13
|
+
throw missingBundleError(sourceDir);
|
|
14
|
+
}
|
|
15
|
+
const index = readBundledIndex();
|
|
16
|
+
if (!index) {
|
|
17
|
+
throw missingBundleError(sourceDir);
|
|
18
|
+
}
|
|
19
|
+
const dir = join(cwd, '.moda', 'tmp', `sdk-catalog-${runId}`);
|
|
20
|
+
cleanupSdkCatalog(dir);
|
|
21
|
+
copyDirectoryRecursive(sourceDir, dir);
|
|
22
|
+
return {
|
|
23
|
+
dir,
|
|
24
|
+
relativeDir: relative(cwd, dir).replace(/\\/g, '/'),
|
|
25
|
+
index,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function cleanupSdkCatalog(dir) {
|
|
29
|
+
const normalized = resolve(dir).replace(/\\/g, '/');
|
|
30
|
+
if (!normalized.includes('.moda/tmp/sdk-catalog-'))
|
|
31
|
+
return;
|
|
32
|
+
try {
|
|
33
|
+
rmSync(dir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Catalog cleanup is best-effort and must never mask the integration result.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function renderCatalogListing(index) {
|
|
40
|
+
return index.skills.map((skill) => {
|
|
41
|
+
const targets = [`language: ${skill.targets.language}`];
|
|
42
|
+
if (skill.targets.frameworks.length > 0) {
|
|
43
|
+
targets.push(`frameworks: ${skill.targets.frameworks.join(',')}`);
|
|
44
|
+
}
|
|
45
|
+
if (skill.targets.providers.length > 0) {
|
|
46
|
+
targets.push(`providers: ${skill.targets.providers.join(',')}`);
|
|
47
|
+
}
|
|
48
|
+
return `- ${skill.id} — ${skill.summary} [${targets.join('; ')}]`;
|
|
49
|
+
}).join('\n');
|
|
50
|
+
}
|
|
51
|
+
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
52
|
+
mkdirSync(targetDir, { recursive: true });
|
|
53
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
54
|
+
const source = join(sourceDir, entry.name);
|
|
55
|
+
const target = join(targetDir, entry.name);
|
|
56
|
+
if (entry.isDirectory()) {
|
|
57
|
+
copyDirectoryRecursive(source, target);
|
|
58
|
+
}
|
|
59
|
+
else if (entry.isFile()) {
|
|
60
|
+
copyFileSync(source, target);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function missingBundleError(sourceDir) {
|
|
65
|
+
let detail = 'is absent';
|
|
66
|
+
try {
|
|
67
|
+
if (existsSync(sourceDir) && !statSync(sourceDir).isDirectory()) {
|
|
68
|
+
detail = 'is not a directory';
|
|
69
|
+
}
|
|
70
|
+
else if (existsSync(sourceDir)) {
|
|
71
|
+
detail = 'has no readable index.json';
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
detail = 'is not readable';
|
|
76
|
+
}
|
|
77
|
+
return new Error(`Bundled SDK skill catalog ${detail} at ${sourceDir}. ` +
|
|
78
|
+
'Set MODA_BUNDLED_SKILLS_DIR to a built bundle, or run the integration-skill bundle build in a dev tree.');
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=sdk-catalog.js.map
|
package/dist/init/sdk-guide.d.ts
CHANGED
|
@@ -6,43 +6,14 @@ type RuntimeSummary = {
|
|
|
6
6
|
providers: string[];
|
|
7
7
|
runtimeAgentNames: string[];
|
|
8
8
|
};
|
|
9
|
-
export type SdkGuideContext = {
|
|
10
|
-
languages: string[];
|
|
11
|
-
installCommands: string[];
|
|
12
|
-
providerHints: string[];
|
|
13
|
-
frameworkHints: string[];
|
|
14
|
-
packageManagers: string[];
|
|
15
|
-
runtimeSummary: RuntimeSummary;
|
|
16
|
-
};
|
|
17
9
|
/**
|
|
18
10
|
* Write the human/agent-readable fallback guide (.moda/SDK_INTEGRATION.md).
|
|
19
|
-
*
|
|
20
|
-
* When a bundled integration skill matches the detected stack, the guide is
|
|
21
|
-
* rendered from that skill's content plus the detection block (one shared
|
|
22
|
-
* renderer with the agent-prompt path). When no skill resolves (no language
|
|
23
|
-
* detected) the slim generic guide is used; when a skill resolved but its
|
|
24
|
-
* content could not be loaded (missing/unreadable bundle) the generic guide is
|
|
25
|
-
* flagged as a degraded fallback and pinned to the detected stack.
|
|
11
|
+
* Used when automatic SDK integration did not complete successfully.
|
|
26
12
|
*/
|
|
27
13
|
export declare function writeSdkIntegrationGuide(cwd?: string): string;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
export declare function
|
|
32
|
-
/** Drop a SKILL.md's YAML frontmatter so only the prose body is rendered. */
|
|
33
|
-
export declare function stripSkillFrontmatter(markdown: string): string;
|
|
34
|
-
export declare function detectSdkGuideContext(cwd: string): SdkGuideContext;
|
|
35
|
-
/**
|
|
36
|
-
* Slim last-resort guide, used when no bundled skill resolves (no language
|
|
37
|
-
* detected) or when a skill resolved but its content could not be loaded (the
|
|
38
|
-
* bundle is missing/unreadable). Detected stacks with readable content get the
|
|
39
|
-
* far richer skill-based guide via renderSkillBasedGuide().
|
|
40
|
-
*
|
|
41
|
-
* When `resolvedSkillId` is set the intro flags the degradation and pins the
|
|
42
|
-
* reader to the detection block instead of claiming no stack was detected —
|
|
43
|
-
* mirrors the degraded fallback in the agent-prompt path so a successful
|
|
44
|
-
* detection never reads as stack-less.
|
|
45
|
-
*/
|
|
46
|
-
export declare function renderSdkIntegrationGuide(context: SdkGuideContext, resolvedSkillId?: string): string;
|
|
14
|
+
export declare function renderSdkIntegrationGuideContent(): string;
|
|
15
|
+
/** Read optional harness discovery context from `.moda/harness.json`. */
|
|
16
|
+
export declare function readRuntimeSummary(cwd: string): RuntimeSummary;
|
|
17
|
+
export declare function renderRuntimeSummaryBlock(summary: RuntimeSummary): string;
|
|
47
18
|
export {};
|
|
48
19
|
//# sourceMappingURL=sdk-guide.d.ts.map
|