@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.
- package/README.md +24 -54
- package/dist/auth.js +39 -67
- package/dist/cli.js +93 -12
- package/dist/config.d.ts +1 -2
- package/dist/config.js +1 -2
- package/dist/init/detect.d.ts +1 -1
- package/dist/init/detect.js +28 -5
- package/dist/init/index.js +26 -68
- package/dist/init/mcp.d.ts +1 -7
- package/dist/init/mcp.js +5 -88
- package/dist/init/rules.d.ts +1 -1
- package/dist/init/rules.js +151 -58
- package/dist/init/tenant.js +3 -41
- package/dist/schemas.d.ts +59 -0
- package/dist/schemas.js +35 -0
- package/package.json +6 -17
- package/skills/integrate-moda/SKILL.md +63 -0
- package/skills/moda-cli/SKILL.md +291 -0
- package/dist/__tests__/api-client.test.d.ts +0 -2
- package/dist/__tests__/api-client.test.js +0 -73
- package/dist/__tests__/cli.test.d.ts +0 -2
- package/dist/__tests__/cli.test.js +0 -109
- package/dist/__tests__/e2e.test.d.ts +0 -2
- package/dist/__tests__/e2e.test.js +0 -239
- package/dist/__tests__/handlers.test.d.ts +0 -2
- package/dist/__tests__/handlers.test.js +0 -71
- package/dist/__tests__/integration.test.d.ts +0 -2
- package/dist/__tests__/integration.test.js +0 -114
- package/dist/__tests__/schemas.test.d.ts +0 -2
- package/dist/__tests__/schemas.test.js +0 -81
- package/dist/handlers.d.ts +0 -7
- package/dist/handlers.js +0 -117
- package/dist/http-server.d.ts +0 -3
- package/dist/http-server.js +0 -74
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -26
- package/dist/tools.d.ts +0 -260
- package/dist/tools.js +0 -133
- package/dist/worker.d.ts +0 -9
- package/dist/worker.js +0 -24
- package/mcp-config.example.json +0 -12
package/dist/init/mcp.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
// src/init/mcp.ts —
|
|
2
|
-
import { existsSync
|
|
3
|
-
import { join
|
|
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
|
package/dist/init/rules.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare function installRules(detectedAgents: string[]
|
|
1
|
+
export declare function installRules(detectedAgents: string[]): Promise<void>;
|
|
2
2
|
//# sourceMappingURL=rules.d.ts.map
|
package/dist/init/rules.js
CHANGED
|
@@ -1,72 +1,165 @@
|
|
|
1
|
-
// src/init/rules.ts —
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
message: 'Install agent rules for coding assistants?',
|
|
43
|
-
default: true,
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
if (!shouldInstall)
|
|
119
|
+
console.log(' Skipped');
|
|
47
120
|
return;
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
package/dist/init/tenant.js
CHANGED
|
@@ -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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
"description": "CLI
|
|
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
|