@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.
- package/dist/agent-schemas.js +60 -3
- package/dist/auth.js +5 -1
- package/dist/cli.js +124 -38
- package/dist/config.js +5 -3
- package/dist/errors.d.ts +32 -0
- package/dist/errors.js +40 -0
- package/dist/exit-funnel.d.ts +14 -0
- package/dist/exit-funnel.js +49 -0
- package/dist/harness.js +18 -6
- package/dist/init/index.js +57 -4
- package/dist/init/sdk-guide.d.ts +28 -1
- package/dist/init/sdk-guide.js +102 -98
- package/dist/init/sdk-integration.d.ts +23 -3
- package/dist/init/sdk-integration.js +106 -24
- package/dist/init/skill-resolver.d.ts +19 -0
- package/dist/init/skill-resolver.js +78 -0
- package/dist/init/tenant.d.ts +6 -1
- package/dist/init/tenant.js +35 -11
- package/dist/manifest.d.ts +5 -0
- package/dist/manifest.js +111 -0
- package/dist/output.d.ts +44 -4
- package/dist/output.js +337 -32
- package/dist/prompts.d.ts +2 -1
- package/dist/prompts.js +38 -13
- package/dist/redact.d.ts +2 -0
- package/dist/redact.js +81 -0
- package/dist/schemas.d.ts +2 -2
- package/dist/skills-bundled.d.ts +35 -0
- package/dist/skills-bundled.js +157 -0
- package/dist/skills-manifest.d.ts +33 -0
- package/dist/skills-manifest.js +244 -0
- package/dist/skills.d.ts +8 -2
- package/dist/skills.js +55 -161
- package/package.json +6 -4
- package/skills/integration/index.json +123 -0
- package/skills/integration/integration-node-anthropic/EXAMPLE.md +59 -0
- package/skills/integration/integration-node-anthropic/SKILL.md +150 -0
- package/skills/integration/integration-node-generic/EXAMPLE.md +77 -0
- package/skills/integration/integration-node-generic/SKILL.md +170 -0
- package/skills/integration/integration-node-langchain/EXAMPLE.md +59 -0
- package/skills/integration/integration-node-langchain/SKILL.md +144 -0
- package/skills/integration/integration-node-openai/EXAMPLE.md +55 -0
- package/skills/integration/integration-node-openai/SKILL.md +148 -0
- package/skills/integration/integration-node-vercel-ai-sdk/EXAMPLE.md +55 -0
- package/skills/integration/integration-node-vercel-ai-sdk/SKILL.md +179 -0
- package/skills/integration/integration-node-vercel-ai-sdk/references/prompt-attribution.md +31 -0
- package/skills/integration/integration-python-anthropic/EXAMPLE.md +66 -0
- package/skills/integration/integration-python-anthropic/SKILL.md +153 -0
- package/skills/integration/integration-python-claude-agent-sdk/EXAMPLE.md +61 -0
- package/skills/integration/integration-python-claude-agent-sdk/SKILL.md +129 -0
- package/skills/integration/integration-python-generic/EXAMPLE.md +46 -0
- package/skills/integration/integration-python-generic/SKILL.md +146 -0
- package/skills/integration/integration-python-langchain/EXAMPLE.md +63 -0
- package/skills/integration/integration-python-langchain/SKILL.md +141 -0
- package/skills/integration/integration-python-openai/EXAMPLE.md +65 -0
- package/skills/integration/integration-python-openai/SKILL.md +151 -0
- package/skills/moda-cli/SKILL.md +51 -7
- package/dist/init/detect.d.ts +0 -9
- 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,
|
|
21
|
-
import { basename, dirname,
|
|
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
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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(
|
|
438
|
-
if (
|
|
439
|
-
|
|
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(
|
|
462
|
-
if (
|
|
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] ||
|
|
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,
|
|
489
|
-
if (
|
|
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(
|
|
512
|
-
|
|
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 = (
|
|
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
|
|
621
|
-
const
|
|
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:
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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.
|
|
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
|
|
15
|
+
"skills/**",
|
|
16
16
|
"README.md"
|
|
17
17
|
],
|
|
18
18
|
"publishConfig": {
|
|
19
19
|
"access": "public"
|
|
20
20
|
},
|
|
21
21
|
"scripts": {
|
|
22
|
-
"
|
|
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.
|