@moda-ai/cli 1.10.2 → 1.12.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.
Files changed (59) hide show
  1. package/dist/agent-schemas.js +60 -3
  2. package/dist/auth.js +5 -1
  3. package/dist/cli.js +124 -38
  4. package/dist/config.js +5 -3
  5. package/dist/errors.d.ts +32 -0
  6. package/dist/errors.js +40 -0
  7. package/dist/exit-funnel.d.ts +14 -0
  8. package/dist/exit-funnel.js +49 -0
  9. package/dist/harness.js +18 -6
  10. package/dist/init/index.js +57 -4
  11. package/dist/init/sdk-guide.d.ts +28 -1
  12. package/dist/init/sdk-guide.js +102 -98
  13. package/dist/init/sdk-integration.d.ts +23 -3
  14. package/dist/init/sdk-integration.js +106 -24
  15. package/dist/init/skill-resolver.d.ts +19 -0
  16. package/dist/init/skill-resolver.js +78 -0
  17. package/dist/init/tenant.d.ts +6 -1
  18. package/dist/init/tenant.js +35 -11
  19. package/dist/manifest.d.ts +5 -0
  20. package/dist/manifest.js +111 -0
  21. package/dist/output.d.ts +44 -4
  22. package/dist/output.js +337 -32
  23. package/dist/prompts.d.ts +2 -1
  24. package/dist/prompts.js +38 -13
  25. package/dist/redact.d.ts +2 -0
  26. package/dist/redact.js +81 -0
  27. package/dist/schemas.d.ts +2 -2
  28. package/dist/skills-bundled.d.ts +35 -0
  29. package/dist/skills-bundled.js +157 -0
  30. package/dist/skills-manifest.d.ts +33 -0
  31. package/dist/skills-manifest.js +244 -0
  32. package/dist/skills.d.ts +8 -2
  33. package/dist/skills.js +55 -161
  34. package/package.json +6 -4
  35. package/skills/integration/index.json +123 -0
  36. package/skills/integration/integration-node-anthropic/EXAMPLE.md +59 -0
  37. package/skills/integration/integration-node-anthropic/SKILL.md +150 -0
  38. package/skills/integration/integration-node-generic/EXAMPLE.md +77 -0
  39. package/skills/integration/integration-node-generic/SKILL.md +170 -0
  40. package/skills/integration/integration-node-langchain/EXAMPLE.md +59 -0
  41. package/skills/integration/integration-node-langchain/SKILL.md +144 -0
  42. package/skills/integration/integration-node-openai/EXAMPLE.md +55 -0
  43. package/skills/integration/integration-node-openai/SKILL.md +148 -0
  44. package/skills/integration/integration-node-vercel-ai-sdk/EXAMPLE.md +55 -0
  45. package/skills/integration/integration-node-vercel-ai-sdk/SKILL.md +179 -0
  46. package/skills/integration/integration-node-vercel-ai-sdk/references/prompt-attribution.md +31 -0
  47. package/skills/integration/integration-python-anthropic/EXAMPLE.md +66 -0
  48. package/skills/integration/integration-python-anthropic/SKILL.md +153 -0
  49. package/skills/integration/integration-python-claude-agent-sdk/EXAMPLE.md +61 -0
  50. package/skills/integration/integration-python-claude-agent-sdk/SKILL.md +129 -0
  51. package/skills/integration/integration-python-generic/EXAMPLE.md +46 -0
  52. package/skills/integration/integration-python-generic/SKILL.md +146 -0
  53. package/skills/integration/integration-python-langchain/EXAMPLE.md +63 -0
  54. package/skills/integration/integration-python-langchain/SKILL.md +141 -0
  55. package/skills/integration/integration-python-openai/EXAMPLE.md +65 -0
  56. package/skills/integration/integration-python-openai/SKILL.md +151 -0
  57. package/skills/moda-cli/SKILL.md +51 -7
  58. package/dist/init/detect.d.ts +0 -9
  59. package/dist/init/detect.js +0 -179
package/dist/skills.js CHANGED
@@ -17,16 +17,17 @@
17
17
  // previous Moda pull wrote.
18
18
  import { execFileSync } from 'node:child_process';
19
19
  import { createHash } from 'node:crypto';
20
- import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmdirSync, unlinkSync, writeFileSync, } from 'node:fs';
21
- import { basename, dirname, isAbsolute, join, normalize, relative, resolve } from 'node:path';
20
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
21
+ import { basename, dirname, join, relative } from 'node:path';
22
22
  import { callControlAPI } from './api-client.js';
23
+ import { createCommandContext } from './command-context.js';
23
24
  import { resolveApiKey, resolveIngestUrl, resolveTenantId } from './config.js';
24
25
  import { claudeSkillToCursorRule } from './init/rules.js';
25
26
  import { detectAgents } from './init/mcp.js';
27
+ import { SKILL_SOURCE_HARNESS, SKILL_SOURCE_INTEGRATION_BUNDLED, pruneManagedSkillPaths, readSkillsManifest, writeSkillsManifest, } from './skills-manifest.js';
26
28
  import { CliInputError } from './errors.js';
27
29
  const SKILLS_REQUEST_TIMEOUT_MS = 15_000;
28
30
  const VALID_STATUSES = new Set(['approved', 'proposed', 'all']);
29
- const SKILLS_MANIFEST_PATH = join('.moda', 'skills.yml');
30
31
  const SKILL_PROPOSALS_MANIFEST_PATH = join('.moda', 'skill-proposals.yml');
31
32
  /**
32
33
  * Fetch a tenant's generated skills from the ingest worker. Uses the persisted
@@ -121,10 +122,18 @@ export async function pullSkills(options = {}) {
121
122
  return summary;
122
123
  }
123
124
  const previousManifest = readSkillsManifest(cwd);
125
+ // A release change only invalidates harness-generated skills. Paths owned
126
+ // by integration-bundled records are CLI-managed and must never be pruned
127
+ // by a harness release change (cross-source regression guard).
128
+ const preservedIntegrationRecords = (previousManifest?.records ?? []).filter((record) => record.source === SKILL_SOURCE_INTEGRATION_BUNDLED);
124
129
  if (previousManifest &&
125
130
  previousManifest.managedPaths.length > 0 &&
126
131
  previousManifest.releaseKey !== fetched.releaseKey) {
127
- summary.pruned = pruneManagedSkillPaths(cwd, previousManifest.managedPaths, log);
132
+ const integrationPaths = new Set(preservedIntegrationRecords.flatMap((record) => record.paths));
133
+ const harnessPaths = previousManifest.managedPaths.filter((path) => !integrationPaths.has(path));
134
+ if (harnessPaths.length > 0) {
135
+ summary.pruned = pruneManagedSkillPaths(cwd, harnessPaths, log);
136
+ }
128
137
  }
129
138
  // Always target Claude Code (canonical SKILL.md). Also write Cursor rules
130
139
  // when the project has a .cursor directory.
@@ -166,11 +175,12 @@ export async function pullSkills(options = {}) {
166
175
  records.push({
167
176
  skillId: safeId,
168
177
  contentHash: sha256(skill.skill_md),
178
+ source: SKILL_SOURCE_HARNESS,
169
179
  paths,
170
180
  });
171
181
  }
172
182
  }
173
- writeSkillsManifest(cwd, fetched, records);
183
+ writeSkillsManifest(cwd, manifestHeaderFor(fetched), [...records, ...preservedIntegrationRecords]);
174
184
  return summary;
175
185
  }
176
186
  function recordOutcome(summary, status, log, relativePath) {
@@ -427,16 +437,26 @@ function escapeYamlString(value) {
427
437
  function formatProposalRate(value) {
428
438
  return `${(Number(value || 0) * 100).toFixed(1)}%`;
429
439
  }
430
- /**
431
- * `moda skills <subcommand>` dispatcher.
432
- */
433
- export async function runSkillsCommand(args) {
434
- const sub = args.positional;
435
- const positionals = args.positionals ?? (sub ? [sub] : []);
440
+ export async function runSkillsCommand(input) {
441
+ const context = 'output' in input
442
+ ? input
443
+ : createCommandContext({
444
+ command: input.command,
445
+ positional: input.positional,
446
+ positionals: input.positionals,
447
+ flags: input.flags,
448
+ }, {
449
+ outputMode: input.flags.agent === 'true' ? 'agent' : input.flags.json === 'true' ? 'json' : 'human',
450
+ });
451
+ const sub = context.positional;
452
+ const positionals = context.positionals ?? (sub ? [sub] : []);
453
+ const flags = context.flags;
436
454
  if (sub === 'gen') {
437
- const result = await generateSkills(args.flags);
438
- if (args.flags.json === 'true') {
439
- console.log(JSON.stringify(result));
455
+ const result = await generateSkills(flags);
456
+ if (context.outputMode !== 'human') {
457
+ context.output.writeData(result, {
458
+ status: Number(result.sessions_failed ?? 0) > 0 ? 'error' : 'success',
459
+ });
440
460
  }
441
461
  else if (result.accepted) {
442
462
  console.log([
@@ -458,8 +478,8 @@ export async function runSkillsCommand(args) {
458
478
  return Number(result.sessions_failed ?? 0) > 0 ? 1 : 0;
459
479
  }
460
480
  if (sub === 'proposals' && positionals[1] === 'list') {
461
- const proposals = await fetchSkillProposals(args.flags, { profile: args.flags.profile });
462
- if (args.flags.json === 'true') {
481
+ const proposals = await fetchSkillProposals(flags, { profile: context.profile });
482
+ if (flags.json === 'true') {
463
483
  console.log(JSON.stringify({ proposals }));
464
484
  }
465
485
  else if (proposals.length === 0) {
@@ -481,12 +501,12 @@ export async function runSkillsCommand(args) {
481
501
  return 0;
482
502
  }
483
503
  if (sub === 'proposal' && positionals[1] === 'apply') {
484
- const proposalId = positionals[2] || args.flags['proposal-id'];
504
+ const proposalId = positionals[2] || flags['proposal-id'];
485
505
  if (!proposalId) {
486
506
  throw new CliInputError('A proposal id is required', 'Usage: moda skills proposal apply <proposal-id>');
487
507
  }
488
- const result = await applySkillProposal(proposalId, args.flags, { profile: args.flags.profile });
489
- if (args.flags.json === 'true') {
508
+ const result = await applySkillProposal(proposalId, flags, { profile: context.profile });
509
+ if (flags.json === 'true') {
490
510
  console.log(JSON.stringify(result));
491
511
  }
492
512
  else {
@@ -508,18 +528,18 @@ export async function runSkillsCommand(args) {
508
528
  return 0;
509
529
  }
510
530
  if (sub === 'sync') {
511
- const result = await runSkillSync(args.flags, { profile: args.flags.profile });
512
- console.log(JSON.stringify({ synced: result.synced, dryRun: result.dryRun }));
531
+ const result = await runSkillSync(flags, { profile: context.profile, env: context.env });
532
+ context.output.writeData({ synced: result.synced, dryRun: result.dryRun });
513
533
  return 0;
514
534
  }
515
535
  if (sub !== 'pull') {
516
536
  throw new CliInputError(sub ? `Unknown skills subcommand '${sub}'` : 'A skills subcommand is required', 'Usage: moda skills gen [--source=all|sdk|coding-agent] [--max-sessions=N] [--start-at=ISO] [--end-at=ISO] [--replay] [--replay-set-id=ID] [--reprocess-segments] [--force-reprocess] [--wait] | moda skills proposals list [--status=ready_for_pr] | moda skills proposal apply <proposal-id> | moda skills pull [--status=approved|proposed|all] | moda skills sync [--dry-run]');
517
537
  }
518
- const status = (args.flags.status ?? 'approved').toLowerCase();
538
+ const status = (flags.status ?? 'approved').toLowerCase();
519
539
  if (!VALID_STATUSES.has(status)) {
520
540
  throw new CliInputError(`Invalid --status '${status}'`, `Must be one of: ${[...VALID_STATUSES].join(', ')}`);
521
541
  }
522
- const summary = await pullSkills({ status });
542
+ const summary = await pullSkills({ status, silent: context.outputMode !== 'human' });
523
543
  const parts = [
524
544
  summary.pruned && `${summary.pruned} pruned`,
525
545
  summary.wrote && `${summary.wrote} written`,
@@ -527,9 +547,12 @@ export async function runSkillsCommand(args) {
527
547
  summary.unchanged && `${summary.unchanged} unchanged`,
528
548
  summary.failed && `${summary.failed} failed`,
529
549
  ].filter(Boolean);
530
- if (summary.total > 0) {
550
+ if (context.outputMode === 'human' && summary.total > 0) {
531
551
  console.log(` Done — ${parts.join(', ') || 'no changes'}`);
532
552
  }
553
+ if (context.outputMode !== 'human') {
554
+ context.output.writeData({ status, summary });
555
+ }
533
556
  return summary.failed > 0 ? 1 : 0;
534
557
  }
535
558
  async function generateSkills(flags) {
@@ -617,146 +640,17 @@ function releaseKeyFor(release, skills, status) {
617
640
  hash: sha256(skill.skill_md || ''),
618
641
  })).sort((a, b) => a.skill_id.localeCompare(b.skill_id))))}`;
619
642
  }
620
- function readSkillsManifest(cwd) {
621
- const manifestPath = join(cwd, SKILLS_MANIFEST_PATH);
622
- if (!existsSync(manifestPath))
623
- return null;
624
- let text = '';
625
- try {
626
- text = readFileSync(manifestPath, 'utf8');
627
- }
628
- catch {
629
- return null;
630
- }
643
+ function manifestHeaderFor(fetched) {
644
+ const release = fetched.release;
631
645
  return {
632
- tenantId: readYamlScalar(text, 'tenant_id'),
633
- generationRunId: readYamlScalar(text, 'generation_run_id'),
634
- generationVersion: Number(readYamlScalar(text, 'generation_version') || 0),
635
- releaseKey: readYamlScalar(text, 'release_key'),
636
- releasedAt: readYamlScalar(text, 'released_at'),
637
- managedPaths: readManagedPaths(text),
646
+ tenantId: fetched.tenantId,
647
+ releaseKey: fetched.releaseKey,
648
+ statusFilter: fetched.status,
649
+ generationRunId: release?.generation_run_id || '',
650
+ generationVersion: Number(release?.generation_version || 0),
651
+ releasedAt: release?.released_at || '',
638
652
  };
639
653
  }
640
- function writeSkillsManifest(cwd, fetched, records) {
641
- const manifestPath = join(cwd, SKILLS_MANIFEST_PATH);
642
- const managedPaths = [...new Set(records.flatMap((record) => record.paths))].sort();
643
- const release = fetched.release;
644
- const lines = [
645
- 'version: 1',
646
- 'source: moda',
647
- `tenant_id: ${yamlQuote(fetched.tenantId)}`,
648
- `release_key: ${yamlQuote(fetched.releaseKey)}`,
649
- `status_filter: ${yamlQuote(fetched.status)}`,
650
- `generation_run_id: ${yamlQuote(release?.generation_run_id || '')}`,
651
- `generation_version: ${Number(release?.generation_version || 0)}`,
652
- `released_at: ${yamlQuote(release?.released_at || '')}`,
653
- `installed_at: ${yamlQuote(new Date().toISOString())}`,
654
- 'managed_paths:',
655
- ...managedPaths.map((path) => ` - ${yamlQuote(path)}`),
656
- 'skills:',
657
- ...records.flatMap((record) => [
658
- ` - skill_id: ${yamlQuote(record.skillId)}`,
659
- ` content_hash: ${yamlQuote(record.contentHash)}`,
660
- ' paths:',
661
- ...record.paths.map((path) => ` - ${yamlQuote(path)}`),
662
- ]),
663
- '',
664
- ];
665
- mkdirSync(dirname(manifestPath), { recursive: true });
666
- writeFileSync(manifestPath, lines.join('\n'));
667
- }
668
- function pruneManagedSkillPaths(cwd, managedPaths, log) {
669
- let pruned = 0;
670
- for (const rawPath of [...new Set(managedPaths)]) {
671
- const safePath = safeManagedRelativePath(rawPath);
672
- if (!safePath)
673
- continue;
674
- const absolutePath = absoluteManagedPath(cwd, safePath);
675
- if (!absolutePath || !existsSync(absolutePath))
676
- continue;
677
- try {
678
- unlinkSync(absolutePath);
679
- pruned++;
680
- log(` pruned ${safePath}`);
681
- pruneEmptyClaudeSkillDir(cwd, safePath);
682
- }
683
- catch {
684
- // Best-effort cleanup. The following write pass can still repair files
685
- // that remain writable.
686
- }
687
- }
688
- return pruned;
689
- }
690
- function pruneEmptyClaudeSkillDir(cwd, relativePath) {
691
- if (!relativePath.startsWith('.claude/skills/') || !relativePath.endsWith('/SKILL.md'))
692
- return;
693
- const dir = dirname(join(cwd, relativePath));
694
- try {
695
- if (existsSync(dir) && readdirSync(dir).length === 0) {
696
- rmdirSync(dir);
697
- }
698
- }
699
- catch {
700
- // Empty-dir pruning is only cosmetic.
701
- }
702
- }
703
- function absoluteManagedPath(cwd, relativePath) {
704
- const abs = resolve(cwd, relativePath);
705
- const root = resolve(cwd);
706
- const rel = relative(root, abs);
707
- if (rel.startsWith('..') || isAbsolute(rel))
708
- return null;
709
- return abs;
710
- }
711
- function safeManagedRelativePath(rawPath) {
712
- if (!rawPath || rawPath.includes('\0') || isAbsolute(rawPath))
713
- return null;
714
- const normalized = normalize(rawPath).replace(/\\/g, '/');
715
- if (normalized.startsWith('../') || normalized === '..' || normalized.includes('/../'))
716
- return null;
717
- const isClaudeSkill = normalized.startsWith('.claude/skills/') && normalized.endsWith('/SKILL.md');
718
- const isCursorRule = normalized.startsWith('.cursor/rules/') && normalized.endsWith('.mdc');
719
- return isClaudeSkill || isCursorRule ? normalized : null;
720
- }
721
- function readYamlScalar(text, key) {
722
- const match = text.match(new RegExp(`^${key}:\\s*(.*?)\\s*$`, 'm'));
723
- return match ? parseYamlScalar(match[1]) : '';
724
- }
725
- function readManagedPaths(text) {
726
- const paths = [];
727
- let inManagedPaths = false;
728
- for (const line of text.split(/\r?\n/)) {
729
- if (line.trim() === 'managed_paths:') {
730
- inManagedPaths = true;
731
- continue;
732
- }
733
- if (inManagedPaths && line.length > 0 && !line.startsWith(' '))
734
- break;
735
- if (!inManagedPaths)
736
- continue;
737
- const match = line.match(/^\s*-\s*(.*?)\s*$/);
738
- if (match)
739
- paths.push(parseYamlScalar(match[1]));
740
- }
741
- return paths.filter(Boolean);
742
- }
743
- function parseYamlScalar(raw) {
744
- const value = raw.trim();
745
- if (!value)
746
- return '';
747
- if (value.startsWith('"')) {
748
- try {
749
- return String(JSON.parse(value));
750
- }
751
- catch {
752
- return value.replace(/^"|"$/g, '');
753
- }
754
- }
755
- return value.replace(/^'|'$/g, '');
756
- }
757
- function yamlQuote(value) {
758
- return JSON.stringify(value);
759
- }
760
654
  function sha256(input) {
761
655
  return createHash('sha256').update(input).digest('hex');
762
656
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.10.2",
3
+ "version": "1.12.0",
4
4
  "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,17 +12,18 @@
12
12
  "dist/**/*.d.ts",
13
13
  "!dist/__tests__",
14
14
  "hooks/**",
15
- "skills/**/SKILL.md",
15
+ "skills/**",
16
16
  "README.md"
17
17
  ],
18
18
  "publishConfig": {
19
19
  "access": "public"
20
20
  },
21
21
  "scripts": {
22
- "build": "tsc",
22
+ "bundle:skills": "bun scripts/bundle-skills.ts",
23
+ "build": "bun run bundle:skills && tsc",
23
24
  "dev": "tsc --watch",
24
25
  "typecheck": "tsc --noEmit",
25
- "test": "vitest run",
26
+ "test": "bun run bundle:skills && vitest run",
26
27
  "test:watch": "vitest",
27
28
  "lint": "eslint ."
28
29
  },
@@ -52,6 +53,7 @@
52
53
  },
53
54
  "devDependencies": {
54
55
  "@moda/eslint-config": "*",
56
+ "@moda/integration-skills": "*",
55
57
  "@types/node": "^22.10.5",
56
58
  "eslint": "^9.17.0",
57
59
  "typescript": "^5.7.2",
@@ -0,0 +1,123 @@
1
+ {
2
+ "schema_version": "moda.skill_index.v1",
3
+ "bundled_at": "2026-07-06T02:25:00.333Z",
4
+ "cli_version": "1.12.0",
5
+ "skills": [
6
+ {
7
+ "id": "integration-node-anthropic",
8
+ "version": "1.0.0",
9
+ "summary": "Auto-instrument @anthropic-ai/sdk client calls with Moda in a Node.js or TypeScript app; no per-call changes required.",
10
+ "targets": {
11
+ "language": "node",
12
+ "frameworks": [],
13
+ "providers": [
14
+ "anthropic"
15
+ ]
16
+ }
17
+ },
18
+ {
19
+ "id": "integration-node-generic",
20
+ "version": "1.0.0",
21
+ "summary": "General Moda SDK integration for a Node.js or TypeScript app when no specific framework or provider skill matches.",
22
+ "targets": {
23
+ "language": "node",
24
+ "frameworks": [],
25
+ "providers": []
26
+ }
27
+ },
28
+ {
29
+ "id": "integration-node-langchain",
30
+ "version": "1.0.0",
31
+ "summary": "Instrument a Node.js LangChain or LangGraph app with Moda: init before the framework, explicit conversation ids around every run.",
32
+ "targets": {
33
+ "language": "node",
34
+ "frameworks": [
35
+ "langchain"
36
+ ],
37
+ "providers": []
38
+ }
39
+ },
40
+ {
41
+ "id": "integration-node-openai",
42
+ "version": "1.0.0",
43
+ "summary": "Auto-instrument direct openai client calls with Moda in a Node.js or TypeScript app; no per-call changes required.",
44
+ "targets": {
45
+ "language": "node",
46
+ "frameworks": [],
47
+ "providers": [
48
+ "openai"
49
+ ]
50
+ }
51
+ },
52
+ {
53
+ "id": "integration-node-vercel-ai-sdk",
54
+ "version": "1.0.0",
55
+ "summary": "Instrument Vercel AI SDK calls (generateText, streamText, generateObject) with Moda telemetry in a Node.js or TypeScript app.",
56
+ "targets": {
57
+ "language": "node",
58
+ "frameworks": [
59
+ "vercel-ai-sdk"
60
+ ],
61
+ "providers": []
62
+ }
63
+ },
64
+ {
65
+ "id": "integration-python-anthropic",
66
+ "version": "1.0.0",
67
+ "summary": "Auto-instrument anthropic client calls with Moda in a Python app; no per-call changes required.",
68
+ "targets": {
69
+ "language": "python",
70
+ "frameworks": [],
71
+ "providers": [
72
+ "anthropic"
73
+ ]
74
+ }
75
+ },
76
+ {
77
+ "id": "integration-python-claude-agent-sdk",
78
+ "version": "1.0.0",
79
+ "summary": "Instrument Claude Agent SDK sessions (ClaudeSDKClient) with Moda via the dedicated moda-claude-agent-sdk instrumentor.",
80
+ "targets": {
81
+ "language": "python",
82
+ "frameworks": [
83
+ "claude-agent-sdk"
84
+ ],
85
+ "providers": []
86
+ }
87
+ },
88
+ {
89
+ "id": "integration-python-generic",
90
+ "version": "1.0.0",
91
+ "summary": "General Moda SDK integration for a Python app when no specific framework or provider skill matches.",
92
+ "targets": {
93
+ "language": "python",
94
+ "frameworks": [],
95
+ "providers": []
96
+ }
97
+ },
98
+ {
99
+ "id": "integration-python-langchain",
100
+ "version": "1.0.0",
101
+ "summary": "Instrument a Python LangChain or LangGraph app with Moda: init before the framework, explicit conversation ids around every run.",
102
+ "targets": {
103
+ "language": "python",
104
+ "frameworks": [
105
+ "langchain"
106
+ ],
107
+ "providers": []
108
+ }
109
+ },
110
+ {
111
+ "id": "integration-python-openai",
112
+ "version": "1.0.0",
113
+ "summary": "Auto-instrument openai client calls with Moda in a Python app; no per-call changes required.",
114
+ "targets": {
115
+ "language": "python",
116
+ "frameworks": [],
117
+ "providers": [
118
+ "openai"
119
+ ]
120
+ }
121
+ }
122
+ ]
123
+ }
@@ -0,0 +1,59 @@
1
+ # Golden reference: Express + Anthropic SDK + Moda
2
+
3
+ The complete finished integration for a minimal Express chat server that
4
+ calls the Anthropic SDK directly. Match this structure; adapt names to the
5
+ target repository.
6
+
7
+ ## src/server.ts
8
+
9
+ ```typescript
10
+ import express from 'express';
11
+ import { Moda, withContext } from 'moda-ai';
12
+ import Anthropic from '@anthropic-ai/sdk';
13
+
14
+ // Initialize Moda once, before the Anthropic client is created. After this,
15
+ // every messages.create / messages.stream call is captured automatically.
16
+ await Moda.init(process.env.MODA_API_KEY!);
17
+
18
+ const client = new Anthropic();
19
+ const app = express();
20
+ app.use(express.json());
21
+
22
+ app.post('/chat', async (req, res) => {
23
+ const { sessionId, userId, message } = req.body;
24
+
25
+ // Scoped context: safe under concurrent requests.
26
+ const response = await withContext(`session_${sessionId}`, userId, async () =>
27
+ client.messages.create({
28
+ model: 'claude-sonnet-4-20250514',
29
+ max_tokens: 1024,
30
+ messages: [{ role: 'user', content: message }],
31
+ }),
32
+ );
33
+
34
+ const reply = response.content
35
+ .filter((block) => block.type === 'text')
36
+ .map((block) => block.text)
37
+ .join('');
38
+ res.json({ reply });
39
+ });
40
+
41
+ // Flush pending telemetry on shutdown.
42
+ process.on('SIGTERM', async () => {
43
+ await Moda.flush();
44
+ await Moda.shutdown();
45
+ process.exit(0);
46
+ });
47
+
48
+ app.listen(3000);
49
+ ```
50
+
51
+ ## What changed, and what did not
52
+
53
+ - `moda-ai` added to `package.json` dependencies.
54
+ - One `Moda.init` call added at startup, before `new Anthropic()`.
55
+ - The handler wraps its existing Anthropic call in `withContext`; the
56
+ `messages.create` call itself is unchanged — instrumentation is automatic.
57
+ - A `SIGTERM` handler flushes telemetry on shutdown.
58
+ - No `.env` file was created or edited; `MODA_API_KEY` is read from the
59
+ runtime environment.
@@ -0,0 +1,150 @@
1
+ ---
2
+ id: integration-node-anthropic
3
+ name: "Integrate Moda with the Anthropic SDK (Node.js / TypeScript)"
4
+ version: 1.0.0
5
+ category: integration
6
+ targets:
7
+ language: node
8
+ frameworks: []
9
+ providers:
10
+ - anthropic
11
+ sdk_package: moda-ai
12
+ summary: "Auto-instrument @anthropic-ai/sdk client calls with Moda in a Node.js or TypeScript app; no per-call changes required."
13
+ ---
14
+
15
+ # Integrate Moda with the Anthropic SDK (Node.js / TypeScript)
16
+
17
+ Follow the steps below in order. Each step has a stop condition; when you hit
18
+ one, stop and report instead of guessing. `EXAMPLE.md` in this directory shows
19
+ the complete finished integration; match its structure. For API details beyond
20
+ this skill, read https://docs.moda.dev/ingestion/providers/anthropic and
21
+ https://docs.moda.dev/ingestion/moda-sdk-node (if the installed `moda-ai`
22
+ package ships docs under `node_modules/moda-ai/docs/`, prefer those; they
23
+ match the installed SDK version).
24
+
25
+ ## Safety rules (apply to every step)
26
+
27
+ - Only touch the repository you were launched in.
28
+ - Never write the `MODA_API_KEY` value (or any secret) into source files, and
29
+ never create or edit `.env` files. The key belongs in the runtime
30
+ environment or a secret manager.
31
+ - Make the smallest correct change. Match the existing code style and imports.
32
+ - Do not commit unless the user asks.
33
+ - If a step is ambiguous in this repository, stop and report rather than
34
+ guessing.
35
+
36
+ ## Step 1: Confirm the raw Anthropic client is the integration surface
37
+
38
+ Find the real LLM call sites: `client.messages.create(...)` or
39
+ `client.messages.stream(...)` on a client constructed with `new Anthropic()`
40
+ from the `@anthropic-ai/sdk` package.
41
+
42
+ - If the Anthropic calls go through a wrapper framework instead — the `ai`
43
+ package (Vercel AI SDK) or LangChain — stop: instrument the wrapper layer
44
+ with its dedicated skill, never the underlying provider client.
45
+ - If the repo wraps its Anthropic client in a helper module (for example
46
+ `lib/anthropic.ts` exporting a shared client), plan to instrument at that
47
+ one place; do not edit every call site.
48
+ - If there are no Anthropic call sites at all, stop and report.
49
+
50
+ ## Step 2: Install the SDK
51
+
52
+ Install `moda-ai` with the package manager this repository already uses
53
+ (check the lockfile: `bun.lock`/`bun.lockb` means bun, `pnpm-lock.yaml` means
54
+ pnpm, `yarn.lock` means yarn, otherwise npm):
55
+
56
+ ```bash
57
+ npm install moda-ai
58
+ ```
59
+
60
+ If `moda-ai` is already in `package.json`, skip the install.
61
+
62
+ ## Step 3: Initialize Moda once at startup, before the client is created
63
+
64
+ Instrumentation is automatic: once `Moda.init` has run, every
65
+ `messages.create` / `messages.stream` call (including extended thinking and
66
+ tool use) is captured with no per-call changes.
67
+
68
+ `Moda.init` must run before the Anthropic client is constructed. If a
69
+ `Moda.init` call already exists, verify it runs at startup before client
70
+ creation and skip this step; never add a second call.
71
+
72
+ ```typescript
73
+ import { Moda } from 'moda-ai';
74
+ import Anthropic from '@anthropic-ai/sdk';
75
+
76
+ await Moda.init(process.env.MODA_API_KEY!);
77
+
78
+ const client = new Anthropic();
79
+ ```
80
+
81
+ If the client lives in a shared helper module, initialize Moda at the top of
82
+ the application entry point (before that module is imported), or at the top
83
+ of the helper module itself — whichever the existing structure makes
84
+ smallest.
85
+
86
+ ## Step 4: Set conversation and user context
87
+
88
+ Set a stable conversation id before each LLM call so multi-turn
89
+ conversations group together, and a user id when one user can be identified.
90
+
91
+ For sequential code, use the global properties:
92
+
93
+ ```typescript
94
+ Moda.conversationId = `session_${sessionId}`;
95
+ Moda.userId = userId;
96
+ ```
97
+
98
+ For concurrent request handlers (any web server), use the scoped helper so
99
+ overlapping requests do not leak context into each other:
100
+
101
+ ```typescript
102
+ import { withContext } from 'moda-ai';
103
+
104
+ const response = await withContext(`session_${sessionId}`, userId, async () =>
105
+ client.messages.create({
106
+ model: 'claude-sonnet-4-20250514',
107
+ max_tokens: 1024,
108
+ messages: [{ role: 'user', content: userMessage }],
109
+ }),
110
+ );
111
+ ```
112
+
113
+ Reuse the session/thread identifier the app already has. If no session
114
+ identifier exists anywhere, the SDK falls back to hashing the first user
115
+ message plus the system prompt — acceptable only for simple chatbots that
116
+ pass full message history on every call. If this repo is an agent framework
117
+ or does not pass full history, an explicit conversation id is required; stop
118
+ and report if you cannot find a session identifier to use.
119
+
120
+ ## Step 5: Flush before exit
121
+
122
+ For short-lived scripts and CLIs, flush before the process exits:
123
+
124
+ ```typescript
125
+ await Moda.flush();
126
+ ```
127
+
128
+ For long-running servers, flush on shutdown:
129
+
130
+ ```typescript
131
+ process.on('SIGTERM', async () => {
132
+ await Moda.flush();
133
+ await Moda.shutdown();
134
+ process.exit(0);
135
+ });
136
+ ```
137
+
138
+ ## Step 6: Validate
139
+
140
+ Report the packages installed, files edited, and any step a human still needs
141
+ to do (for example setting `MODA_API_KEY` in the deploy environment). Then
142
+ verify telemetry reaches Moda:
143
+
144
+ ```bash
145
+ moda doctor --online --json
146
+ ```
147
+
148
+ The report should show recent traces. If it does not, confirm `MODA_API_KEY`
149
+ is available in the runtime environment and that `Moda.init()` runs before
150
+ the Anthropic client is created.