@moda-ai/cli 1.0.4 → 1.0.5

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 (41) hide show
  1. package/README.md +24 -54
  2. package/dist/auth.js +39 -67
  3. package/dist/cli.js +93 -12
  4. package/dist/config.d.ts +1 -2
  5. package/dist/config.js +1 -2
  6. package/dist/init/detect.d.ts +1 -1
  7. package/dist/init/detect.js +28 -5
  8. package/dist/init/index.js +26 -68
  9. package/dist/init/mcp.d.ts +1 -7
  10. package/dist/init/mcp.js +5 -88
  11. package/dist/init/rules.d.ts +1 -1
  12. package/dist/init/rules.js +151 -58
  13. package/dist/init/tenant.js +3 -41
  14. package/dist/schemas.d.ts +59 -0
  15. package/dist/schemas.js +35 -0
  16. package/package.json +6 -17
  17. package/skills/integrate-moda/SKILL.md +63 -0
  18. package/skills/moda-cli/SKILL.md +291 -0
  19. package/dist/__tests__/api-client.test.d.ts +0 -2
  20. package/dist/__tests__/api-client.test.js +0 -73
  21. package/dist/__tests__/cli.test.d.ts +0 -2
  22. package/dist/__tests__/cli.test.js +0 -109
  23. package/dist/__tests__/e2e.test.d.ts +0 -2
  24. package/dist/__tests__/e2e.test.js +0 -239
  25. package/dist/__tests__/handlers.test.d.ts +0 -2
  26. package/dist/__tests__/handlers.test.js +0 -71
  27. package/dist/__tests__/integration.test.d.ts +0 -2
  28. package/dist/__tests__/integration.test.js +0 -114
  29. package/dist/__tests__/schemas.test.d.ts +0 -2
  30. package/dist/__tests__/schemas.test.js +0 -81
  31. package/dist/handlers.d.ts +0 -7
  32. package/dist/handlers.js +0 -117
  33. package/dist/http-server.d.ts +0 -3
  34. package/dist/http-server.js +0 -74
  35. package/dist/index.d.ts +0 -3
  36. package/dist/index.js +0 -26
  37. package/dist/tools.d.ts +0 -260
  38. package/dist/tools.js +0 -133
  39. package/dist/worker.d.ts +0 -9
  40. package/dist/worker.js +0 -24
  41. package/mcp-config.example.json +0 -12
package/dist/init/mcp.js CHANGED
@@ -1,8 +1,7 @@
1
- // src/init/mcp.ts — MCP server auto-configuration for coding agents
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
1
+ // src/init/mcp.ts — Coding agent detection
2
+ import { existsSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
4
  import { execSync } from 'node:child_process';
5
- import { checkbox } from '@inquirer/prompts';
6
5
  function isOnPath(cmd) {
7
6
  try {
8
7
  execSync(`which ${cmd}`, { stdio: 'pipe' });
@@ -17,94 +16,12 @@ export function detectAgents() {
17
16
  const agents = [];
18
17
  // Claude Code
19
18
  if (existsSync(join(cwd, '.claude')) || isOnPath('claude')) {
20
- agents.push({
21
- name: 'Claude Code',
22
- configPath: join(cwd, '.claude', 'settings.local.json'),
23
- configKey: 'mcpServers',
24
- });
19
+ agents.push('Claude Code');
25
20
  }
26
21
  // Cursor
27
22
  if (existsSync(join(cwd, '.cursor'))) {
28
- agents.push({
29
- name: 'Cursor',
30
- configPath: join(cwd, '.cursor', 'mcp.json'),
31
- configKey: 'mcpServers',
32
- });
33
- }
34
- // VS Code
35
- if (existsSync(join(cwd, '.vscode'))) {
36
- agents.push({
37
- name: 'VS Code',
38
- configPath: join(cwd, '.vscode', 'mcp.json'),
39
- configKey: 'servers',
40
- });
23
+ agents.push('Cursor');
41
24
  }
42
25
  return agents;
43
26
  }
44
- export async function configureMCP(agents, apiKey, autoYes = false) {
45
- if (agents.length === 0)
46
- return [];
47
- let selected;
48
- if (autoYes) {
49
- selected = agents;
50
- }
51
- else {
52
- const choices = agents.map((a) => ({
53
- name: a.name,
54
- value: a,
55
- checked: true,
56
- }));
57
- selected = await checkbox({
58
- message: 'Configure MCP server for:',
59
- choices,
60
- });
61
- }
62
- const configured = [];
63
- for (const agent of selected) {
64
- try {
65
- writeAgentConfig(agent, apiKey);
66
- configured.push(agent.name);
67
- }
68
- catch (err) {
69
- const message = err instanceof Error ? err.message : String(err);
70
- console.error(` Warning: Failed to configure ${agent.name}: ${message}`);
71
- }
72
- }
73
- // Show manual instructions for skipped agents
74
- const skipped = agents.filter((a) => !selected.includes(a));
75
- if (skipped.length > 0) {
76
- console.log('\n Manual setup for skipped agents:');
77
- for (const agent of skipped) {
78
- console.log(` ${agent.name}: Add moda server to ${agent.configPath}`);
79
- }
80
- }
81
- return configured;
82
- }
83
- function writeAgentConfig(agent, apiKey) {
84
- // Ensure directory exists
85
- const dir = dirname(agent.configPath);
86
- mkdirSync(dir, { recursive: true });
87
- // Read existing config
88
- let config = {};
89
- if (existsSync(agent.configPath)) {
90
- try {
91
- config = JSON.parse(readFileSync(agent.configPath, 'utf8'));
92
- }
93
- catch {
94
- config = {};
95
- }
96
- }
97
- // Merge moda server entry
98
- const servers = config[agent.configKey] || {};
99
- servers['moda'] = {
100
- command: 'npx',
101
- args: ['-y', '@moda-ai/cli'],
102
- env: {
103
- MODA_API_KEY: apiKey,
104
- },
105
- };
106
- config[agent.configKey] = servers;
107
- // Write back
108
- writeFileSync(agent.configPath, JSON.stringify(config, null, 2) + '\n');
109
- }
110
27
  //# sourceMappingURL=mcp.js.map
@@ -1,2 +1,2 @@
1
- export declare function installRules(detectedAgents: string[], autoYes?: boolean): Promise<void>;
1
+ export declare function installRules(detectedAgents: string[]): Promise<void>;
2
2
  //# sourceMappingURL=rules.d.ts.map
@@ -1,72 +1,165 @@
1
- // src/init/rules.ts — Agent rules file installation
2
- import { existsSync, readFileSync, writeFileSync, appendFileSync, mkdirSync } from 'node:fs';
3
- import { join, dirname } from 'node:path';
1
+ // src/init/rules.ts — Install Moda agent skills into the user's project.
2
+ //
3
+ // Each entry in SKILLS is a directory under <package>/skills/ that contains a
4
+ // SKILL.md (Claude Code format). At install time we copy each skill into every
5
+ // detected coding agent, translating frontmatter where the agent expects a
6
+ // different shape (e.g. Cursor's .mdc rules).
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
8
+ import { dirname, join } from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
4
10
  import { confirm } from '@inquirer/prompts';
5
- const MODA_RULES_CONTENT = `
6
-
7
- ## Moda SDK
8
-
9
- Moda is integrated in this project for LLM observability. The \`moda-ai\` SDK
10
- auto-instruments OpenAI and Anthropic calls via OpenTelemetry.
11
-
12
- ### Initialization
13
-
14
- - TypeScript: \`import { Moda } from 'moda-ai'; await Moda.init(process.env.MODA_API_KEY!);\`
15
- - Python: \`import moda; moda.init()\` (reads MODA_API_KEY from env)
16
-
17
- ### Conversation Tracking
18
-
19
- Set conversation ID before LLM calls to group multi-turn conversations:
20
- - TypeScript: \`Moda.conversationId = 'session-id';\`
21
- - Python: \`moda.conversation_id = 'session-id'\`
22
-
23
- ### Before Exit
24
-
25
- Flush pending traces before the process exits:
26
- - TypeScript: \`await Moda.flush();\`
27
- - Python: \`moda.flush()\`
28
- `;
29
- const AGENT_RULES_FILES = {
30
- 'Claude Code': 'CLAUDE.md',
31
- 'Cursor': '.cursor/rules/moda.mdc',
11
+ // --- Package path resolution ---
12
+ //
13
+ // Resolved relative to this module's URL so it works whether we're running
14
+ // from src/ in dev or from the published dist/ inside node_modules.
15
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
16
+ const SKILLS_SOURCE_DIR = join(PACKAGE_ROOT, 'skills');
17
+ const SKILLS = [
18
+ {
19
+ id: 'integrate-moda',
20
+ label: 'integrate-moda — instrument this project with the Moda SDK',
21
+ },
22
+ {
23
+ id: 'moda-cli',
24
+ label: 'moda-cli — let coding agents query Moda analytics via the CLI',
25
+ },
26
+ ];
27
+ const ADAPTERS = {
28
+ 'Claude Code': {
29
+ name: 'Claude Code',
30
+ targetFor: (skillId) => ({
31
+ dir: join('.claude', 'skills', skillId),
32
+ filename: 'SKILL.md',
33
+ }),
34
+ transform: (content) => content,
35
+ },
36
+ 'Cursor': {
37
+ name: 'Cursor',
38
+ targetFor: (skillId) => ({
39
+ dir: join('.cursor', 'rules'),
40
+ filename: `${skillId}.mdc`,
41
+ }),
42
+ transform: claudeSkillToCursorRule,
43
+ },
32
44
  };
33
- export async function installRules(detectedAgents, autoYes = false) {
34
- const applicable = detectedAgents.filter((a) => a in AGENT_RULES_FILES);
35
- if (applicable.length === 0) {
36
- console.log(' No supported agents detected for rules files');
45
+ // --- Frontmatter translation ---
46
+ //
47
+ // Cursor's .mdc rules use a different frontmatter shape:
48
+ // ---
49
+ // description: "..."
50
+ // alwaysApply: false
51
+ // ---
52
+ // The body markdown is identical, so we strip the Claude frontmatter, lift
53
+ // the description out of it, and emit Cursor's variant.
54
+ const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/;
55
+ const DESCRIPTION_RE = /^description:\s*(.+?)\s*$/m;
56
+ function claudeSkillToCursorRule(skillContent) {
57
+ const match = FRONTMATTER_RE.exec(skillContent);
58
+ if (!match) {
59
+ // Body without frontmatter — pass through untouched rather than guess.
60
+ return skillContent;
61
+ }
62
+ const [, frontmatter, body] = match;
63
+ const descMatch = DESCRIPTION_RE.exec(frontmatter);
64
+ const description = descMatch?.[1].trim() ?? '';
65
+ // JSON.stringify gives us a YAML-safe double-quoted string with proper
66
+ // escaping for any quotes or backslashes inside the description.
67
+ const cursorFrontmatter = `---\n` +
68
+ `description: ${JSON.stringify(description)}\n` +
69
+ `alwaysApply: false\n` +
70
+ `---\n`;
71
+ return `${cursorFrontmatter}\n${body.replace(/^\s+/, '')}`;
72
+ }
73
+ // --- I/O helpers ---
74
+ function loadSkillSource(skillId) {
75
+ const path = join(SKILLS_SOURCE_DIR, skillId, 'SKILL.md');
76
+ if (!existsSync(path)) {
77
+ throw new Error(`Skill source missing: ${path}`);
78
+ }
79
+ return readFileSync(path, 'utf8');
80
+ }
81
+ function installSkillForAgent(skill, adapter, content) {
82
+ const { dir, filename } = adapter.targetFor(skill.id);
83
+ const dirPath = join(process.cwd(), dir);
84
+ const filePath = join(dirPath, filename);
85
+ const relativePath = join(dir, filename);
86
+ try {
87
+ mkdirSync(dirPath, { recursive: true });
88
+ if (existsSync(filePath)) {
89
+ return { agent: adapter.name, relativePath, status: 'exists' };
90
+ }
91
+ writeFileSync(filePath, adapter.transform(content));
92
+ return { agent: adapter.name, relativePath, status: 'wrote' };
93
+ }
94
+ catch (err) {
95
+ return {
96
+ agent: adapter.name,
97
+ relativePath,
98
+ status: 'failed',
99
+ error: err instanceof Error ? err.message : String(err),
100
+ };
101
+ }
102
+ }
103
+ // --- Public entry point ---
104
+ export async function installRules(detectedAgents) {
105
+ const adapters = detectedAgents
106
+ .map((a) => ADAPTERS[a])
107
+ .filter((a) => a !== undefined);
108
+ if (adapters.length === 0) {
109
+ console.log(' No supported coding agents detected');
37
110
  return;
38
111
  }
39
- let shouldInstall = autoYes;
112
+ const agentList = adapters.map((a) => a.name).join(', ');
113
+ const skillList = SKILLS.map((s) => `\n • ${s.label}`).join('');
114
+ const shouldInstall = await confirm({
115
+ message: `Install Moda skills for ${agentList}?${skillList}\n Continue?`,
116
+ default: true,
117
+ });
40
118
  if (!shouldInstall) {
41
- shouldInstall = await confirm({
42
- message: 'Install agent rules for coding assistants?',
43
- default: true,
44
- });
45
- }
46
- if (!shouldInstall)
119
+ console.log(' Skipped');
47
120
  return;
48
- for (const agent of applicable) {
49
- const filePath = join(process.cwd(), AGENT_RULES_FILES[agent]);
121
+ }
122
+ let wrote = 0;
123
+ let skipped = 0;
124
+ let failed = 0;
125
+ for (const skill of SKILLS) {
126
+ let content;
50
127
  try {
51
- const dir = dirname(filePath);
52
- mkdirSync(dir, { recursive: true });
53
- if (existsSync(filePath)) {
54
- const content = readFileSync(filePath, 'utf8');
55
- if (content.includes('## Moda SDK')) {
56
- console.log(` ${agent}: Moda SDK section already exists in ${AGENT_RULES_FILES[agent]}`);
57
- continue;
58
- }
59
- appendFileSync(filePath, MODA_RULES_CONTENT);
60
- }
61
- else {
62
- writeFileSync(filePath, MODA_RULES_CONTENT.trimStart());
63
- }
64
- console.log(` Added Moda SDK reference to ${AGENT_RULES_FILES[agent]}`);
128
+ content = loadSkillSource(skill.id);
65
129
  }
66
130
  catch (err) {
67
131
  const message = err instanceof Error ? err.message : String(err);
68
- console.error(` Warning: Failed to write ${AGENT_RULES_FILES[agent]}: ${message}`);
132
+ console.error(` Warning: ${message}`);
133
+ failed += adapters.length;
134
+ continue;
69
135
  }
136
+ for (const adapter of adapters) {
137
+ const outcome = installSkillForAgent(skill, adapter, content);
138
+ switch (outcome.status) {
139
+ case 'wrote':
140
+ console.log(` ${outcome.agent}: wrote ${outcome.relativePath}`);
141
+ wrote++;
142
+ break;
143
+ case 'exists':
144
+ console.log(` ${outcome.agent}: ${outcome.relativePath} already exists — skipping`);
145
+ skipped++;
146
+ break;
147
+ case 'failed':
148
+ console.error(` ${outcome.agent}: failed to write ${outcome.relativePath} — ${outcome.error}`);
149
+ failed++;
150
+ break;
151
+ }
152
+ }
153
+ }
154
+ const summary = [
155
+ wrote && `${wrote} written`,
156
+ skipped && `${skipped} already present`,
157
+ failed && `${failed} failed`,
158
+ ]
159
+ .filter(Boolean)
160
+ .join(', ');
161
+ if (summary) {
162
+ console.log(` Done — ${summary}`);
70
163
  }
71
164
  }
72
165
  //# sourceMappingURL=rules.js.map
@@ -1,7 +1,5 @@
1
1
  // src/init/tenant.ts — Tenant selection and API key provisioning
2
2
  import { select } from '@inquirer/prompts';
3
- import { readFileSync, writeFileSync, existsSync, appendFileSync } from 'node:fs';
4
- import { join } from 'node:path';
5
3
  import { authFetch } from '../auth.js';
6
4
  export async function selectTenantAndCreateKey() {
7
5
  // 1. Fetch CLI context
@@ -47,45 +45,9 @@ export async function selectTenantAndCreateKey() {
47
45
  throw new Error(`Failed to create API key (${bootstrapRes.status}): ${body}`);
48
46
  }
49
47
  const result = (await bootstrapRes.json());
50
- // 4. Write .env
51
- writeEnvFile(result.apiKey);
52
- // 5. Update .gitignore
53
- ensureGitignore();
54
- console.log(' API key created and saved to .env');
48
+ console.log(' API key created.\n');
49
+ console.log(` Add this to your environment variables:\n`);
50
+ console.log(` MODA_API_KEY=${result.apiKey}\n`);
55
51
  return result;
56
52
  }
57
- function writeEnvFile(apiKey) {
58
- const envPath = join(process.cwd(), '.env');
59
- const line = `MODA_API_KEY=${apiKey}`;
60
- if (existsSync(envPath)) {
61
- const content = readFileSync(envPath, 'utf8');
62
- if (content.includes('MODA_API_KEY=')) {
63
- // Replace existing line
64
- const updated = content.replace(/^MODA_API_KEY=.*$/m, line);
65
- writeFileSync(envPath, updated);
66
- }
67
- else {
68
- // Append
69
- const separator = content.endsWith('\n') ? '' : '\n';
70
- appendFileSync(envPath, `${separator}${line}\n`);
71
- }
72
- }
73
- else {
74
- writeFileSync(envPath, `${line}\n`);
75
- }
76
- }
77
- function ensureGitignore() {
78
- const gitignorePath = join(process.cwd(), '.gitignore');
79
- if (existsSync(gitignorePath)) {
80
- const content = readFileSync(gitignorePath, 'utf8');
81
- const lines = content.split('\n').map((l) => l.trim());
82
- if (!lines.includes('.env')) {
83
- const separator = content.endsWith('\n') ? '' : '\n';
84
- appendFileSync(gitignorePath, `${separator}.env\n`);
85
- }
86
- }
87
- else {
88
- writeFileSync(gitignorePath, '.env\n');
89
- }
90
- }
91
53
  //# sourceMappingURL=tenant.js.map
package/dist/schemas.d.ts CHANGED
@@ -106,4 +106,63 @@ export declare const ToolFailureDetailSchema: z.ZodObject<{
106
106
  offset?: number | undefined;
107
107
  subtype?: string | undefined;
108
108
  }>;
109
+ /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
110
+ export declare const WorkflowsSchema: z.ZodObject<{
111
+ time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
112
+ user_id: z.ZodOptional<z.ZodString>;
113
+ }, "strip", z.ZodTypeAny, {
114
+ time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
115
+ user_id?: string | undefined;
116
+ }, {
117
+ time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
118
+ user_id?: string | undefined;
119
+ }>;
120
+ /** moda runs — list workflow executions with classified outcomes. */
121
+ export declare const RunsSchema: z.ZodObject<{
122
+ time_range: z.ZodOptional<z.ZodDefault<z.ZodEnum<["all", "1h", "3d", "7d", "24h", "30d", "90d"]>>>;
123
+ outcome: z.ZodOptional<z.ZodEnum<["running", "succeeded", "partial", "failed", "loop", "abandoned", "unknown"]>>;
124
+ /** Filter to one workflow class id (e.g. cron:helpscout-triage-hourly). */
125
+ class_id: z.ZodOptional<z.ZodString>;
126
+ user_id: z.ZodOptional<z.ZodString>;
127
+ limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
128
+ offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
129
+ }, "strip", z.ZodTypeAny, {
130
+ time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
131
+ limit?: number | undefined;
132
+ offset?: number | undefined;
133
+ user_id?: string | undefined;
134
+ outcome?: "running" | "succeeded" | "partial" | "failed" | "loop" | "abandoned" | "unknown" | undefined;
135
+ class_id?: string | undefined;
136
+ }, {
137
+ time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
138
+ limit?: number | undefined;
139
+ offset?: number | undefined;
140
+ user_id?: string | undefined;
141
+ outcome?: "running" | "succeeded" | "partial" | "failed" | "loop" | "abandoned" | "unknown" | undefined;
142
+ class_id?: string | undefined;
143
+ }>;
144
+ /** moda run <runId> — single execution detail with side-effect receipts. */
145
+ export declare const RunDetailSchema: z.ZodObject<{
146
+ run_id: z.ZodString;
147
+ }, "strip", z.ZodTypeAny, {
148
+ run_id: string;
149
+ }, {
150
+ run_id: string;
151
+ }>;
152
+ /** moda loops — currently active loop detections. */
153
+ export declare const LoopsSchema: z.ZodObject<{
154
+ limit: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
155
+ }, "strip", z.ZodTypeAny, {
156
+ limit?: number | undefined;
157
+ }, {
158
+ limit?: number | undefined;
159
+ }>;
160
+ /** moda loop <detectionId> — single loop detection detail. */
161
+ export declare const LoopDetailSchema: z.ZodObject<{
162
+ detection_id: z.ZodString;
163
+ }, "strip", z.ZodTypeAny, {
164
+ detection_id: string;
165
+ }, {
166
+ detection_id: string;
167
+ }>;
109
168
  //# sourceMappingURL=schemas.d.ts.map
package/dist/schemas.js CHANGED
@@ -45,4 +45,39 @@ export const ToolFailureDetailSchema = z.object({
45
45
  limit: z.number().min(1).max(20).default(5).optional(),
46
46
  offset: z.number().min(0).default(0).optional(),
47
47
  });
48
+ // ─── Workflows + Runs (agent runs runtime) ──────────────────
49
+ //
50
+ // Naming model:
51
+ // workflow = the recurring class definition (e.g. cron:helpscout-triage-hourly)
52
+ // run = one execution of a workflow
53
+ // `moda workflows` lists the catalog; `moda runs` lists executions.
54
+ const RUN_OUTCOMES = ['running', 'succeeded', 'partial', 'failed', 'loop', 'abandoned', 'unknown'];
55
+ const TIME_RANGES = ['all', '1h', '3d', '7d', '24h', '30d', '90d'];
56
+ /** moda workflows — recurring workflow catalog (read from agent_job_classes). */
57
+ export const WorkflowsSchema = z.object({
58
+ time_range: z.enum(TIME_RANGES).default('all').optional(),
59
+ user_id: z.string().optional(),
60
+ });
61
+ /** moda runs — list workflow executions with classified outcomes. */
62
+ export const RunsSchema = z.object({
63
+ time_range: z.enum(TIME_RANGES).default('7d').optional(),
64
+ outcome: z.enum(RUN_OUTCOMES).optional(),
65
+ /** Filter to one workflow class id (e.g. cron:helpscout-triage-hourly). */
66
+ class_id: z.string().optional(),
67
+ user_id: z.string().optional(),
68
+ limit: z.number().min(1).max(100).default(50).optional(),
69
+ offset: z.number().min(0).default(0).optional(),
70
+ });
71
+ /** moda run <runId> — single execution detail with side-effect receipts. */
72
+ export const RunDetailSchema = z.object({
73
+ run_id: z.string().uuid({ message: 'run_id must be a UUID' }),
74
+ });
75
+ /** moda loops — currently active loop detections. */
76
+ export const LoopsSchema = z.object({
77
+ limit: z.number().min(1).max(200).default(25).optional(),
78
+ });
79
+ /** moda loop <detectionId> — single loop detection detail. */
80
+ export const LoopDetailSchema = z.object({
81
+ detection_id: z.string().uuid({ message: 'detection_id must be a UUID' }),
82
+ });
48
83
  //# sourceMappingURL=schemas.js.map
package/package.json CHANGED
@@ -1,21 +1,19 @@
1
1
  {
2
2
  "name": "@moda-ai/cli",
3
- "version": "1.0.4",
4
- "description": "CLI and MCP server for Moda - AI agent analytics and observability",
3
+ "version": "1.0.5",
4
+ "description": "CLI for Moda - AI agent analytics and observability",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
6
  "bin": {
8
7
  "cli": "./dist/cli.js",
9
- "moda": "./dist/cli.js",
10
- "moda-mcp": "./dist/index.js"
8
+ "moda": "./dist/cli.js"
11
9
  },
12
10
  "files": [
13
11
  "dist/**/*.js",
14
12
  "dist/**/*.d.ts",
15
13
  "!dist/__tests__",
14
+ "skills/**/SKILL.md",
16
15
  "README.md",
17
- ".env.example",
18
- "mcp-config.example.json"
16
+ ".env.example"
19
17
  ],
20
18
  "publishConfig": {
21
19
  "access": "public"
@@ -24,13 +22,10 @@
24
22
  "build": "tsc",
25
23
  "prepublishOnly": "tsc",
26
24
  "dev": "tsc --watch",
27
- "start": "node dist/index.js",
28
- "start:http": "node dist/http-server.js",
29
25
  "typecheck": "tsc --noEmit",
30
26
  "test": "vitest run",
31
27
  "test:ci": "vitest run",
32
- "test:watch": "vitest",
33
- "test:e2e": "npm run build && vitest run e2e"
28
+ "test:watch": "vitest"
34
29
  },
35
30
  "repository": {
36
31
  "type": "git",
@@ -40,7 +35,6 @@
40
35
  "moda",
41
36
  "llm",
42
37
  "observability",
43
- "mcp",
44
38
  "cli",
45
39
  "analytics",
46
40
  "ai-agents"
@@ -53,15 +47,10 @@
53
47
  },
54
48
  "dependencies": {
55
49
  "@inquirer/prompts": "^8.4.1",
56
- "@modelcontextprotocol/sdk": "^1.0.4",
57
- "cors": "^2.8.5",
58
- "express": "^4.21.2",
59
50
  "open": "^11.0.0",
60
51
  "zod": "^3.24.1"
61
52
  },
62
53
  "devDependencies": {
63
- "@types/cors": "^2.8.19",
64
- "@types/express": "^5.0.6",
65
54
  "@types/node": "^22.10.5",
66
55
  "typescript": "^5.7.2",
67
56
  "vitest": "^3.0.0"
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: integrate-moda
3
+ description: Integrate the Moda SDK into this project for LLM observability. Use when the user asks to add Moda, set up observability, or instrument LLM calls.
4
+ ---
5
+
6
+ # Integrate Moda SDK
7
+
8
+ Moda auto-instruments LLM calls (OpenAI, Anthropic) via OpenTelemetry.
9
+ After `Moda.init()`, all LLM calls are captured automatically — no wrappers needed.
10
+
11
+ ## Steps
12
+
13
+ 1. Install the SDK: `npm install moda-ai` (or bun/yarn/pnpm equivalent)
14
+ 2. Ensure `MODA_API_KEY` is set in the environment
15
+ 3. Initialize once at app startup, before any LLM client is created:
16
+
17
+ ```typescript
18
+ import { Moda } from 'moda-ai';
19
+ await Moda.init(process.env.MODA_API_KEY!);
20
+ ```
21
+
22
+ Python: `import moda; moda.init()`
23
+
24
+ 4. Set conversation ID before LLM calls to group multi-turn conversations.
25
+ Without this, each call appears as a separate conversation in the dashboard.
26
+
27
+ ```typescript
28
+ Moda.conversationId = sessionId;
29
+ Moda.userId = userId; // optional but recommended
30
+ ```
31
+
32
+ For concurrent requests, use scoped context:
33
+ ```typescript
34
+ import { withConversationId } from 'moda-ai';
35
+ await withConversationId(sessionId, async () => {
36
+ await openai.chat.completions.create({ ... });
37
+ });
38
+ ```
39
+
40
+ Python: `moda.conversation_id = session_id` or `with moda.set_conversation_id(session_id):`
41
+
42
+ 5. Flush pending traces before process exit: `await Moda.flush();`
43
+
44
+ ## Vercel AI SDK
45
+
46
+ When using the `ai` package, auto-instrumentation does not cover it. Pass telemetry config:
47
+ ```typescript
48
+ const result = await generateText({
49
+ model: openai('gpt-4o'),
50
+ prompt: 'Hello',
51
+ experimental_telemetry: Moda.getVercelAITelemetry(),
52
+ });
53
+ ```
54
+
55
+ ## Rules
56
+
57
+ - Initialize Moda before creating any LLM client instances
58
+ - Place `Moda.init()` in the app entry point, not in individual route handlers
59
+ - Set `conversationId` per request/session, not globally
60
+ - Use `withConversationId` in server environments handling concurrent requests
61
+ - Call `Moda.flush()` in graceful shutdown handlers and before `process.exit()`
62
+ - Do not wrap LLM calls — auto-instrumentation handles OpenAI and Anthropic automatically
63
+ - Do not import or configure OpenTelemetry manually — Moda manages its own provider