@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 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
@@ -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
@@ -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, { method: 'GET' });
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) {
@@ -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';
@@ -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 result = await runSdkIntegration({
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
- if (result.status === 'integrated') {
536
- laneBoard?.complete('sdk', `integrated with ${result.adapter}; review the diff before committing`);
537
- markApplied(setupPlan, 'sdk');
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 to confirm setup and that telemetry is reaching Moda.'));
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: 'Run a coding agent to integrate the Moda SDK',
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
- ? 'Installs the SDK package and edits source via your coding agent; review the diff before committing.'
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 to confirm telemetry is reaching Moda',
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
@@ -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
- /** Shared renderer: detection block + the resolved skill's full content. */
29
- export declare function renderSkillBasedGuide(skillId: string, skillMd: string, context: SdkGuideContext): string;
30
- /** The "What Moda Detected" section, shared by prompts and guides. */
31
- export declare function renderDetectionBlock(context: SdkGuideContext): string;
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