@moda-ai/cli 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +477 -0
- package/dist/analyst-tui.d.ts +81 -0
- package/dist/analyst-tui.js +314 -0
- package/dist/api-client.d.ts +4 -0
- package/dist/api-client.js +20 -3
- package/dist/auth.d.ts +22 -3
- package/dist/auth.js +230 -66
- package/dist/cli.d.ts +20 -3
- package/dist/cli.js +768 -93
- package/dist/command-context.d.ts +34 -0
- package/dist/command-context.js +38 -0
- package/dist/command-registry.d.ts +22 -0
- package/dist/command-registry.js +15 -0
- package/dist/config.d.ts +40 -1
- package/dist/config.js +70 -3
- package/dist/doctor.d.ts +79 -0
- package/dist/doctor.js +1102 -0
- package/dist/errors.d.ts +5 -0
- package/dist/errors.js +9 -0
- package/dist/harness-report.d.ts +138 -0
- package/dist/harness-report.js +1464 -0
- package/dist/harness.d.ts +233 -0
- package/dist/harness.js +3264 -0
- package/dist/init/index.d.ts +30 -1
- package/dist/init/index.js +383 -23
- package/dist/init/rules.d.ts +5 -1
- package/dist/init/rules.js +22 -16
- package/dist/init/sdk-guide.d.ts +3 -0
- package/dist/init/sdk-guide.js +397 -0
- package/dist/init/tenant.js +12 -3
- package/dist/keys.d.ts +47 -0
- package/dist/keys.js +136 -0
- package/dist/output.d.ts +118 -0
- package/dist/output.js +228 -0
- package/dist/production-intelligence.d.ts +134 -0
- package/dist/production-intelligence.js +1113 -0
- package/dist/profile-commands.d.ts +10 -0
- package/dist/profile-commands.js +252 -0
- package/dist/profiles.d.ts +90 -0
- package/dist/profiles.js +432 -0
- package/dist/prompts.d.ts +6 -0
- package/dist/prompts.js +368 -0
- package/dist/schemas.d.ts +4 -4
- package/dist/skills.d.ts +25 -0
- package/dist/skills.js +468 -0
- package/dist/sync.d.ts +5 -0
- package/dist/sync.js +27 -0
- package/dist/telemetry.d.ts +18 -0
- package/dist/telemetry.js +63 -0
- package/dist/tui.d.ts +40 -0
- package/dist/tui.js +387 -0
- package/dist/workspace.d.ts +12 -0
- package/dist/workspace.js +1294 -0
- package/hooks/moda-snapshot.sh +41 -0
- package/package.json +10 -7
- package/skills/moda-cli/SKILL.md +48 -4
- package/skills/integrate-moda/SKILL.md +0 -63
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
export const SDK_GUIDE_PATH = '.moda/SDK_INTEGRATION.md';
|
|
4
|
+
export function writeSdkIntegrationGuide(cwd = process.cwd()) {
|
|
5
|
+
const relativePath = SDK_GUIDE_PATH;
|
|
6
|
+
const absolutePath = join(cwd, relativePath);
|
|
7
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
8
|
+
writeFileSync(absolutePath, renderSdkIntegrationGuide(detectSdkGuideContext(cwd)));
|
|
9
|
+
return relativePath;
|
|
10
|
+
}
|
|
11
|
+
function detectSdkGuideContext(cwd) {
|
|
12
|
+
const languages = [];
|
|
13
|
+
const installCommands = [];
|
|
14
|
+
const packageManagers = [];
|
|
15
|
+
const providerHints = new Set();
|
|
16
|
+
const frameworkHints = new Set();
|
|
17
|
+
const packageJsonPath = join(cwd, 'package.json');
|
|
18
|
+
if (existsSync(packageJsonPath)) {
|
|
19
|
+
languages.push('Node.js / TypeScript');
|
|
20
|
+
const packageManager = detectNodePackageManager(cwd);
|
|
21
|
+
packageManagers.push(packageManager);
|
|
22
|
+
installCommands.push(nodeInstallCommand(packageManager));
|
|
23
|
+
const packageJson = readPackageJson(packageJsonPath);
|
|
24
|
+
for (const dependency of packageDependencyNames(packageJson)) {
|
|
25
|
+
addDependencyHint(dependency, providerHints, frameworkHints);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const pythonFiles = ['pyproject.toml', 'requirements.txt', 'requirements-dev.txt', 'Pipfile', 'setup.py']
|
|
29
|
+
.filter((file) => existsSync(join(cwd, file)));
|
|
30
|
+
if (pythonFiles.length > 0) {
|
|
31
|
+
languages.push('Python');
|
|
32
|
+
const packageManager = detectPythonPackageManager(cwd);
|
|
33
|
+
packageManagers.push(packageManager);
|
|
34
|
+
installCommands.push(pythonInstallCommand(packageManager));
|
|
35
|
+
for (const file of pythonFiles) {
|
|
36
|
+
const text = safeRead(join(cwd, file));
|
|
37
|
+
addPythonHints(text, providerHints, frameworkHints);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const sourceHints = addSourceHints(cwd, providerHints, frameworkHints);
|
|
41
|
+
if (sourceHints.hasNodeSource && !languages.includes('Node.js / TypeScript')) {
|
|
42
|
+
const packageManager = detectNodePackageManager(cwd);
|
|
43
|
+
languages.push('Node.js / TypeScript');
|
|
44
|
+
packageManagers.push(packageManager);
|
|
45
|
+
installCommands.push(nodeInstallCommand(packageManager));
|
|
46
|
+
}
|
|
47
|
+
if (sourceHints.hasPythonSource && !languages.includes('Python')) {
|
|
48
|
+
const packageManager = detectPythonPackageManager(cwd);
|
|
49
|
+
languages.push('Python');
|
|
50
|
+
packageManagers.push(packageManager);
|
|
51
|
+
installCommands.push(pythonInstallCommand(packageManager));
|
|
52
|
+
}
|
|
53
|
+
const runtimeSummary = readRuntimeSummary(cwd);
|
|
54
|
+
for (const provider of runtimeSummary.providers)
|
|
55
|
+
providerHints.add(provider);
|
|
56
|
+
for (const framework of runtimeSummary.frameworks)
|
|
57
|
+
frameworkHints.add(framework);
|
|
58
|
+
return {
|
|
59
|
+
languages: unique(languages),
|
|
60
|
+
installCommands: unique(installCommands),
|
|
61
|
+
providerHints: unique([...providerHints]),
|
|
62
|
+
frameworkHints: unique([...frameworkHints]),
|
|
63
|
+
packageManagers: unique(packageManagers),
|
|
64
|
+
runtimeSummary,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function renderSdkIntegrationGuide(context) {
|
|
68
|
+
const languages = context.languages.length > 0 ? context.languages.join(', ') : 'Not detected yet';
|
|
69
|
+
const packageManagers = context.packageManagers.length > 0 ? context.packageManagers.join(', ') : 'Not detected yet';
|
|
70
|
+
const installCommands = context.installCommands.length > 0
|
|
71
|
+
? context.installCommands
|
|
72
|
+
: ['npm install moda-ai', 'pip install moda-ai'];
|
|
73
|
+
const providers = context.providerHints.length > 0 ? context.providerHints.join(', ') : 'No provider imports detected yet';
|
|
74
|
+
const frameworks = context.frameworkHints.length > 0 ? context.frameworkHints.join(', ') : 'No agent framework detected yet';
|
|
75
|
+
const runtimeNames = context.runtimeSummary.runtimeAgentNames.slice(0, 8);
|
|
76
|
+
return `${[
|
|
77
|
+
'# Moda SDK Integration Guide',
|
|
78
|
+
'',
|
|
79
|
+
'Generated by `moda init`.',
|
|
80
|
+
'',
|
|
81
|
+
'V1 does not install packages or edit source code automatically. Use this guide as the human-approved checklist for SDK package installation and code instrumentation.',
|
|
82
|
+
'',
|
|
83
|
+
'## What Moda Detected',
|
|
84
|
+
'',
|
|
85
|
+
`- Languages: ${languages}`,
|
|
86
|
+
`- Package managers: ${packageManagers}`,
|
|
87
|
+
`- Model providers: ${providers}`,
|
|
88
|
+
`- Agent frameworks: ${frameworks}`,
|
|
89
|
+
`- Harness graph: ${context.runtimeSummary.runtimeAgents} runtime agent(s), ${context.runtimeSummary.agentFamilies} agent family/families`,
|
|
90
|
+
...(runtimeNames.length > 0 ? [`- Runtime agents: ${runtimeNames.join(', ')}`] : []),
|
|
91
|
+
'',
|
|
92
|
+
'## 1. Approve Package Installation',
|
|
93
|
+
'',
|
|
94
|
+
'Run the command that matches the harness you are instrumenting:',
|
|
95
|
+
'',
|
|
96
|
+
...installCommands.flatMap((command) => ['```bash', command, '```', '']),
|
|
97
|
+
'Do not paste `MODA_API_KEY` into source. Keep it in your deployment environment, secret manager, or local shell.',
|
|
98
|
+
'',
|
|
99
|
+
'## 2. Initialize Moda Once At Startup',
|
|
100
|
+
'',
|
|
101
|
+
'Initialize before any OpenAI, Anthropic, LangChain, AutoGen, CrewAI, or app-specific LLM client is created.',
|
|
102
|
+
'',
|
|
103
|
+
'Node.js / TypeScript:',
|
|
104
|
+
'',
|
|
105
|
+
'```ts',
|
|
106
|
+
"import { Moda } from 'moda-ai';",
|
|
107
|
+
'',
|
|
108
|
+
'await Moda.init(process.env.MODA_API_KEY!);',
|
|
109
|
+
'```',
|
|
110
|
+
'',
|
|
111
|
+
'Python:',
|
|
112
|
+
'',
|
|
113
|
+
'```python',
|
|
114
|
+
'import moda',
|
|
115
|
+
'',
|
|
116
|
+
'moda.init() # reads MODA_API_KEY',
|
|
117
|
+
'```',
|
|
118
|
+
'',
|
|
119
|
+
'## 3. Set Conversation And User Context',
|
|
120
|
+
'',
|
|
121
|
+
'Set a stable conversation id before each LLM call. If one user can reach the same agent through SMS, WhatsApp, voice, or Slack, use the same user id across channels.',
|
|
122
|
+
'',
|
|
123
|
+
'Node.js / TypeScript:',
|
|
124
|
+
'',
|
|
125
|
+
'```ts',
|
|
126
|
+
'Moda.conversationId = sessionId;',
|
|
127
|
+
'Moda.userId = userId;',
|
|
128
|
+
'```',
|
|
129
|
+
'',
|
|
130
|
+
'Python:',
|
|
131
|
+
'',
|
|
132
|
+
'```python',
|
|
133
|
+
'moda.conversation_id = session_id',
|
|
134
|
+
'moda.user_id = user_id',
|
|
135
|
+
'```',
|
|
136
|
+
'',
|
|
137
|
+
'For concurrent request handlers, use the scoped context helpers from the SDK instead of setting global context across overlapping requests.',
|
|
138
|
+
'',
|
|
139
|
+
'## 4. Attribute Managed Prompts',
|
|
140
|
+
'',
|
|
141
|
+
'Keep prompts in files, sync them with Moda, and render them through the SDK so production traces point at exact prompt versions.',
|
|
142
|
+
'',
|
|
143
|
+
'```bash',
|
|
144
|
+
'moda prompts status',
|
|
145
|
+
'moda prompts sync',
|
|
146
|
+
'```',
|
|
147
|
+
'',
|
|
148
|
+
'Node.js / TypeScript:',
|
|
149
|
+
'',
|
|
150
|
+
'```ts',
|
|
151
|
+
'const rendered = Moda.prompt("support.triage").render({',
|
|
152
|
+
' ticket: { text: userMessage },',
|
|
153
|
+
'});',
|
|
154
|
+
'',
|
|
155
|
+
'await client.chat.completions.create({',
|
|
156
|
+
' model: "gpt-4o",',
|
|
157
|
+
' messages: rendered.messages,',
|
|
158
|
+
'});',
|
|
159
|
+
'```',
|
|
160
|
+
'',
|
|
161
|
+
'Python:',
|
|
162
|
+
'',
|
|
163
|
+
'```python',
|
|
164
|
+
'rendered = moda.prompt("support.triage").render({',
|
|
165
|
+
' "ticket": {"text": user_message},',
|
|
166
|
+
'})',
|
|
167
|
+
'',
|
|
168
|
+
'client.chat.completions.create(',
|
|
169
|
+
' model="gpt-4o",',
|
|
170
|
+
' messages=rendered["messages"],',
|
|
171
|
+
')',
|
|
172
|
+
'```',
|
|
173
|
+
'',
|
|
174
|
+
'## 5. Vercel AI SDK',
|
|
175
|
+
'',
|
|
176
|
+
'If this project uses the `ai` package or `@ai-sdk/*`, pass Moda telemetry explicitly:',
|
|
177
|
+
'',
|
|
178
|
+
'```ts',
|
|
179
|
+
'const result = await generateText({',
|
|
180
|
+
" model: openai('gpt-4o'),",
|
|
181
|
+
' messages: rendered.messages,',
|
|
182
|
+
' experimental_telemetry: Moda.getVercelAITelemetry(),',
|
|
183
|
+
'});',
|
|
184
|
+
'```',
|
|
185
|
+
'',
|
|
186
|
+
'Render the prompt before calling `Moda.getVercelAITelemetry()` so prompt metadata is attached.',
|
|
187
|
+
'',
|
|
188
|
+
'## 6. Flush Before Exit',
|
|
189
|
+
'',
|
|
190
|
+
'Node.js / TypeScript:',
|
|
191
|
+
'',
|
|
192
|
+
'```ts',
|
|
193
|
+
'await Moda.flush();',
|
|
194
|
+
'```',
|
|
195
|
+
'',
|
|
196
|
+
'Python:',
|
|
197
|
+
'',
|
|
198
|
+
'```python',
|
|
199
|
+
'moda.flush()',
|
|
200
|
+
'```',
|
|
201
|
+
'',
|
|
202
|
+
'## 7. Validate',
|
|
203
|
+
'',
|
|
204
|
+
'After the code change is reviewed and merged, run:',
|
|
205
|
+
'',
|
|
206
|
+
'```bash',
|
|
207
|
+
'moda doctor --online --json',
|
|
208
|
+
'```',
|
|
209
|
+
'',
|
|
210
|
+
'The check should show recent traces/logs/runs. If it does not, confirm `MODA_API_KEY` is available at runtime and that `Moda.init()` runs before the first LLM client is created.',
|
|
211
|
+
'',
|
|
212
|
+
'## Human Approval Boundary',
|
|
213
|
+
'',
|
|
214
|
+
'- This guide is safe for `moda init` to write automatically.',
|
|
215
|
+
'- Package installation is a human-approved step.',
|
|
216
|
+
'- Source edits are a human-approved step.',
|
|
217
|
+
'- Environment variable files are not created or updated by `moda init`.',
|
|
218
|
+
'',
|
|
219
|
+
].join('\n')}`;
|
|
220
|
+
}
|
|
221
|
+
function detectNodePackageManager(cwd) {
|
|
222
|
+
if (existsSync(join(cwd, 'bun.lock')) || existsSync(join(cwd, 'bun.lockb')))
|
|
223
|
+
return 'bun';
|
|
224
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml')))
|
|
225
|
+
return 'pnpm';
|
|
226
|
+
if (existsSync(join(cwd, 'yarn.lock')))
|
|
227
|
+
return 'yarn';
|
|
228
|
+
return 'npm';
|
|
229
|
+
}
|
|
230
|
+
function detectPythonPackageManager(cwd) {
|
|
231
|
+
if (existsSync(join(cwd, 'uv.lock')))
|
|
232
|
+
return 'uv';
|
|
233
|
+
if (existsSync(join(cwd, 'poetry.lock')))
|
|
234
|
+
return 'poetry';
|
|
235
|
+
return 'pip';
|
|
236
|
+
}
|
|
237
|
+
function nodeInstallCommand(packageManager) {
|
|
238
|
+
if (packageManager === 'bun')
|
|
239
|
+
return 'bun add moda-ai';
|
|
240
|
+
if (packageManager === 'pnpm')
|
|
241
|
+
return 'pnpm add moda-ai';
|
|
242
|
+
if (packageManager === 'yarn')
|
|
243
|
+
return 'yarn add moda-ai';
|
|
244
|
+
return 'npm install moda-ai';
|
|
245
|
+
}
|
|
246
|
+
function pythonInstallCommand(packageManager) {
|
|
247
|
+
if (packageManager === 'uv')
|
|
248
|
+
return 'uv add moda-ai';
|
|
249
|
+
if (packageManager === 'poetry')
|
|
250
|
+
return 'poetry add moda-ai';
|
|
251
|
+
return 'pip install moda-ai';
|
|
252
|
+
}
|
|
253
|
+
function readPackageJson(path) {
|
|
254
|
+
try {
|
|
255
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
256
|
+
return isRecord(parsed) ? parsed : {};
|
|
257
|
+
}
|
|
258
|
+
catch {
|
|
259
|
+
return {};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function packageDependencyNames(packageJson) {
|
|
263
|
+
const names = new Set();
|
|
264
|
+
for (const key of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
265
|
+
const deps = packageJson[key];
|
|
266
|
+
if (!isRecord(deps))
|
|
267
|
+
continue;
|
|
268
|
+
for (const name of Object.keys(deps))
|
|
269
|
+
names.add(name);
|
|
270
|
+
}
|
|
271
|
+
return [...names];
|
|
272
|
+
}
|
|
273
|
+
function addDependencyHint(dependency, providers, frameworks) {
|
|
274
|
+
const name = dependency.toLowerCase();
|
|
275
|
+
if (name === 'openai' || name.includes('@ai-sdk/openai'))
|
|
276
|
+
providers.add('OpenAI');
|
|
277
|
+
if (name === '@anthropic-ai/sdk' || name.includes('@ai-sdk/anthropic'))
|
|
278
|
+
providers.add('Anthropic');
|
|
279
|
+
if (name === 'ai' || name.startsWith('@ai-sdk/'))
|
|
280
|
+
frameworks.add('Vercel AI SDK');
|
|
281
|
+
if (name.includes('langchain') || name.includes('langgraph'))
|
|
282
|
+
frameworks.add('LangChain / LangGraph');
|
|
283
|
+
if (name.includes('crewai'))
|
|
284
|
+
frameworks.add('CrewAI');
|
|
285
|
+
if (name.includes('autogen'))
|
|
286
|
+
frameworks.add('AutoGen');
|
|
287
|
+
}
|
|
288
|
+
function addPythonHints(text, providers, frameworks) {
|
|
289
|
+
const lower = text.toLowerCase();
|
|
290
|
+
if (/\bopenai\b/.test(lower))
|
|
291
|
+
providers.add('OpenAI');
|
|
292
|
+
if (/\banthropic\b/.test(lower))
|
|
293
|
+
providers.add('Anthropic');
|
|
294
|
+
if (/\blangchain\b|\blanggraph\b/.test(lower))
|
|
295
|
+
frameworks.add('LangChain / LangGraph');
|
|
296
|
+
if (/\bcrewai\b/.test(lower))
|
|
297
|
+
frameworks.add('CrewAI');
|
|
298
|
+
if (/\bautogen\b/.test(lower))
|
|
299
|
+
frameworks.add('AutoGen');
|
|
300
|
+
}
|
|
301
|
+
function addSourceHints(cwd, providers, frameworks) {
|
|
302
|
+
let hasNodeSource = false;
|
|
303
|
+
let hasPythonSource = false;
|
|
304
|
+
for (const dir of ['src', 'app', 'lib', 'agents', 'python']) {
|
|
305
|
+
const root = join(cwd, dir);
|
|
306
|
+
if (!existsSync(root))
|
|
307
|
+
continue;
|
|
308
|
+
for (const file of listShallowSourceFiles(root).slice(0, 80)) {
|
|
309
|
+
if (/\.(ts|tsx|js|jsx|mjs)$/i.test(file))
|
|
310
|
+
hasNodeSource = true;
|
|
311
|
+
if (/\.py$/i.test(file))
|
|
312
|
+
hasPythonSource = true;
|
|
313
|
+
addPythonHints(safeRead(file), providers, frameworks);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { hasNodeSource, hasPythonSource };
|
|
317
|
+
}
|
|
318
|
+
function listShallowSourceFiles(root) {
|
|
319
|
+
const files = [];
|
|
320
|
+
const stack = [{ path: root, depth: 0 }];
|
|
321
|
+
while (stack.length > 0 && files.length < 120) {
|
|
322
|
+
const current = stack.pop();
|
|
323
|
+
if (!current)
|
|
324
|
+
continue;
|
|
325
|
+
let entries;
|
|
326
|
+
try {
|
|
327
|
+
entries = readdirSync(current.path, { withFileTypes: true });
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
const path = join(current.path, entry.name);
|
|
334
|
+
if (entry.isDirectory()) {
|
|
335
|
+
if (current.depth < 2 && !['node_modules', '.git', 'dist', 'build', '.venv'].includes(entry.name)) {
|
|
336
|
+
stack.push({ path, depth: current.depth + 1 });
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (/\.(ts|tsx|js|jsx|mjs|py|md|toml|json|ya?ml)$/i.test(entry.name))
|
|
341
|
+
files.push(path);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return files;
|
|
345
|
+
}
|
|
346
|
+
function readRuntimeSummary(cwd) {
|
|
347
|
+
const empty = {
|
|
348
|
+
runtimeAgents: 0,
|
|
349
|
+
agentFamilies: 0,
|
|
350
|
+
frameworks: [],
|
|
351
|
+
providers: [],
|
|
352
|
+
runtimeAgentNames: [],
|
|
353
|
+
};
|
|
354
|
+
const path = join(cwd, '.moda', 'harness.json');
|
|
355
|
+
if (!existsSync(path))
|
|
356
|
+
return empty;
|
|
357
|
+
try {
|
|
358
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
359
|
+
if (!isRecord(parsed))
|
|
360
|
+
return empty;
|
|
361
|
+
const runtimeAgents = Array.isArray(parsed.runtimeAgents) ? parsed.runtimeAgents : [];
|
|
362
|
+
const agentFamilies = Array.isArray(parsed.agentFamilies) ? parsed.agentFamilies : [];
|
|
363
|
+
const frameworks = Array.isArray(parsed.agentFrameworks) ? parsed.agentFrameworks : [];
|
|
364
|
+
const providers = Array.isArray(parsed.modelProviders) ? parsed.modelProviders : [];
|
|
365
|
+
return {
|
|
366
|
+
runtimeAgents: runtimeAgents.length,
|
|
367
|
+
agentFamilies: agentFamilies.length,
|
|
368
|
+
runtimeAgentNames: runtimeAgents
|
|
369
|
+
.map((agent) => isRecord(agent) && typeof agent.name === 'string' ? agent.name : undefined)
|
|
370
|
+
.filter((name) => Boolean(name)),
|
|
371
|
+
frameworks: frameworks
|
|
372
|
+
.map((framework) => isRecord(framework) && typeof framework.name === 'string' ? framework.name : undefined)
|
|
373
|
+
.filter((name) => Boolean(name)),
|
|
374
|
+
providers: providers
|
|
375
|
+
.map((provider) => isRecord(provider) && typeof provider.name === 'string' ? provider.name : undefined)
|
|
376
|
+
.filter((name) => Boolean(name)),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch {
|
|
380
|
+
return empty;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
function safeRead(path) {
|
|
384
|
+
try {
|
|
385
|
+
return readFileSync(path, 'utf8');
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
return '';
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function unique(values) {
|
|
392
|
+
return [...new Set(values.filter(Boolean))];
|
|
393
|
+
}
|
|
394
|
+
function isRecord(value) {
|
|
395
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
396
|
+
}
|
|
397
|
+
//# sourceMappingURL=sdk-guide.js.map
|
package/dist/init/tenant.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/init/tenant.ts — Tenant selection and API key provisioning
|
|
2
2
|
import { select } from '@inquirer/prompts';
|
|
3
3
|
import { authFetch } from '../auth.js';
|
|
4
|
+
import { resolveIngestUrl, saveCliConfig } from '../config.js';
|
|
4
5
|
export async function selectTenantAndCreateKey() {
|
|
5
6
|
// 1. Fetch CLI context
|
|
6
7
|
const contextRes = await authFetch('/api/cli/context');
|
|
@@ -45,9 +46,17 @@ export async function selectTenantAndCreateKey() {
|
|
|
45
46
|
throw new Error(`Failed to create API key (${bootstrapRes.status}): ${body}`);
|
|
46
47
|
}
|
|
47
48
|
const result = (await bootstrapRes.json());
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
// Persist to ~/.moda/config.json (mode 0600) so the snapshot/session-end
|
|
50
|
+
// hook can authenticate (api_key) and the local dev orchestrator can scope
|
|
51
|
+
// ClickHouse (tenant_id) without the user exporting env vars into the shell
|
|
52
|
+
// that launches Claude. Env still wins over the file at read time.
|
|
53
|
+
saveCliConfig({
|
|
54
|
+
api_key: result.apiKey,
|
|
55
|
+
tenant_id: result.tenantId,
|
|
56
|
+
ingest_url: resolveIngestUrl(),
|
|
57
|
+
});
|
|
58
|
+
console.log(' API key + tenant saved to ~/.moda/config.json\n');
|
|
59
|
+
console.log(' (Set MODA_API_KEY / MODA_TENANT_ID to override for scripts/CI.)\n');
|
|
51
60
|
return result;
|
|
52
61
|
}
|
|
53
62
|
//# sourceMappingURL=tenant.js.map
|
package/dist/keys.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type ModaEnvironment, type ProfileIOOptions, type SecretReference } from './profiles.js';
|
|
2
|
+
export type ModaKeyStatus = 'active' | 'rotating' | 'revoked';
|
|
3
|
+
export type ModaKeyPurpose = 'data_api' | 'ingest' | 'automation';
|
|
4
|
+
export interface ModaKeyDescriptor {
|
|
5
|
+
id: string;
|
|
6
|
+
profile: string;
|
|
7
|
+
name: string;
|
|
8
|
+
purpose: ModaKeyPurpose;
|
|
9
|
+
environment: ModaEnvironment;
|
|
10
|
+
tenant_id?: string;
|
|
11
|
+
workspace_id?: string;
|
|
12
|
+
secret_ref: SecretReference;
|
|
13
|
+
status: ModaKeyStatus;
|
|
14
|
+
created_at: string;
|
|
15
|
+
expires_at?: string;
|
|
16
|
+
last_used_at?: string;
|
|
17
|
+
metadata?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
export interface CreateLocalKeyDescriptorInput {
|
|
20
|
+
profile: string;
|
|
21
|
+
environment: ModaEnvironment;
|
|
22
|
+
name: string;
|
|
23
|
+
purpose?: ModaKeyPurpose;
|
|
24
|
+
tenant_id?: string;
|
|
25
|
+
workspace_id?: string;
|
|
26
|
+
secret_ref?: SecretReference;
|
|
27
|
+
now?: Date;
|
|
28
|
+
}
|
|
29
|
+
export interface KeyDescriptorStore {
|
|
30
|
+
version: 1;
|
|
31
|
+
keys: ModaKeyDescriptor[];
|
|
32
|
+
}
|
|
33
|
+
export declare const KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS: {
|
|
34
|
+
readonly list: "GET /api/cli/keys?profile=<profile>";
|
|
35
|
+
readonly create: "POST /api/cli/keys";
|
|
36
|
+
readonly rotate: "POST /api/cli/keys/<key_id>/rotate";
|
|
37
|
+
readonly revoke: "POST /api/cli/keys/<key_id>/revoke";
|
|
38
|
+
};
|
|
39
|
+
export declare function keySecretRef(profile: string, keyId: string): SecretReference;
|
|
40
|
+
export declare function createLocalKeyDescriptor(input: CreateLocalKeyDescriptorInput): ModaKeyDescriptor;
|
|
41
|
+
export declare function validateKeyDescriptor(descriptor: ModaKeyDescriptor): void;
|
|
42
|
+
export declare function writeLocalApiKeySecret(descriptor: ModaKeyDescriptor, apiKey: string, options?: ProfileIOOptions): string;
|
|
43
|
+
export declare function keyDescriptorStorePath(options?: ProfileIOOptions): string;
|
|
44
|
+
export declare function loadLocalKeyDescriptors(options?: ProfileIOOptions): ModaKeyDescriptor[];
|
|
45
|
+
export declare function saveLocalKeyDescriptors(keys: ModaKeyDescriptor[], options?: ProfileIOOptions): void;
|
|
46
|
+
export declare function upsertLocalKeyDescriptor(descriptor: ModaKeyDescriptor, options?: ProfileIOOptions): ModaKeyDescriptor[];
|
|
47
|
+
//# sourceMappingURL=keys.d.ts.map
|
package/dist/keys.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { assertValidProfileName, getModaPaths, secretFilePathFromRef, writeSecretJsonFile, } from './profiles.js';
|
|
5
|
+
export const KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
|
|
6
|
+
list: 'GET /api/cli/keys?profile=<profile>',
|
|
7
|
+
create: 'POST /api/cli/keys',
|
|
8
|
+
rotate: 'POST /api/cli/keys/<key_id>/rotate',
|
|
9
|
+
revoke: 'POST /api/cli/keys/<key_id>/revoke',
|
|
10
|
+
};
|
|
11
|
+
export function keySecretRef(profile, keyId) {
|
|
12
|
+
assertValidProfileName(profile);
|
|
13
|
+
assertSafeKeyId(keyId);
|
|
14
|
+
return `file:secrets/api-keys/${profile}/${keyId}.json`;
|
|
15
|
+
}
|
|
16
|
+
export function createLocalKeyDescriptor(input) {
|
|
17
|
+
assertValidProfileName(input.profile);
|
|
18
|
+
const now = input.now ?? new Date();
|
|
19
|
+
const id = stableKeyId(input.profile, input.name, now);
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
profile: input.profile,
|
|
23
|
+
name: input.name,
|
|
24
|
+
purpose: input.purpose ?? 'data_api',
|
|
25
|
+
environment: input.environment,
|
|
26
|
+
tenant_id: input.tenant_id,
|
|
27
|
+
workspace_id: input.workspace_id,
|
|
28
|
+
secret_ref: input.secret_ref ?? keySecretRef(input.profile, id),
|
|
29
|
+
status: 'active',
|
|
30
|
+
created_at: now.toISOString(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function validateKeyDescriptor(descriptor) {
|
|
34
|
+
assertValidProfileName(descriptor.profile);
|
|
35
|
+
assertSafeKeyId(descriptor.id);
|
|
36
|
+
if (!descriptor.name.trim()) {
|
|
37
|
+
throw new Error('Key name is required.');
|
|
38
|
+
}
|
|
39
|
+
if (descriptor.status !== 'active' &&
|
|
40
|
+
descriptor.status !== 'rotating' &&
|
|
41
|
+
descriptor.status !== 'revoked') {
|
|
42
|
+
throw new Error(`Invalid key status '${descriptor.status}'.`);
|
|
43
|
+
}
|
|
44
|
+
if (descriptor.purpose !== 'data_api' &&
|
|
45
|
+
descriptor.purpose !== 'ingest' &&
|
|
46
|
+
descriptor.purpose !== 'automation') {
|
|
47
|
+
throw new Error(`Invalid key purpose '${descriptor.purpose}'.`);
|
|
48
|
+
}
|
|
49
|
+
if (descriptor.environment !== 'local' &&
|
|
50
|
+
descriptor.environment !== 'dev' &&
|
|
51
|
+
descriptor.environment !== 'staging' &&
|
|
52
|
+
descriptor.environment !== 'prod') {
|
|
53
|
+
throw new Error(`Invalid key environment '${descriptor.environment}'.`);
|
|
54
|
+
}
|
|
55
|
+
if (!descriptor.secret_ref.startsWith('file:')) {
|
|
56
|
+
throw new Error('Local key descriptors must point to a file: secret ref.');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function writeLocalApiKeySecret(descriptor, apiKey, options = {}) {
|
|
60
|
+
validateKeyDescriptor(descriptor);
|
|
61
|
+
if (!apiKey.trim()) {
|
|
62
|
+
throw new Error('API key value is required.');
|
|
63
|
+
}
|
|
64
|
+
const path = secretFilePathFromRef(descriptor.secret_ref, options);
|
|
65
|
+
if (!path) {
|
|
66
|
+
throw new Error(`Unsupported key secret reference: ${descriptor.secret_ref}`);
|
|
67
|
+
}
|
|
68
|
+
writeSecretJsonFile(path, {
|
|
69
|
+
value: apiKey,
|
|
70
|
+
key_id: descriptor.id,
|
|
71
|
+
profile: descriptor.profile,
|
|
72
|
+
purpose: descriptor.purpose,
|
|
73
|
+
created_at: descriptor.created_at,
|
|
74
|
+
});
|
|
75
|
+
return path;
|
|
76
|
+
}
|
|
77
|
+
export function keyDescriptorStorePath(options = {}) {
|
|
78
|
+
return join(getModaPaths(options).dir, 'keys.json');
|
|
79
|
+
}
|
|
80
|
+
export function loadLocalKeyDescriptors(options = {}) {
|
|
81
|
+
const path = keyDescriptorStorePath(options);
|
|
82
|
+
try {
|
|
83
|
+
if (!existsSync(path))
|
|
84
|
+
return [];
|
|
85
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
86
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
const keys = parsed.keys;
|
|
90
|
+
if (!Array.isArray(keys))
|
|
91
|
+
return [];
|
|
92
|
+
return keys.filter((key) => {
|
|
93
|
+
try {
|
|
94
|
+
validateKeyDescriptor(key);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export function saveLocalKeyDescriptors(keys, options = {}) {
|
|
107
|
+
for (const key of keys)
|
|
108
|
+
validateKeyDescriptor(key);
|
|
109
|
+
writeSecretJsonFile(keyDescriptorStorePath(options), {
|
|
110
|
+
version: 1,
|
|
111
|
+
keys,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export function upsertLocalKeyDescriptor(descriptor, options = {}) {
|
|
115
|
+
validateKeyDescriptor(descriptor);
|
|
116
|
+
const keys = loadLocalKeyDescriptors(options);
|
|
117
|
+
const next = [
|
|
118
|
+
...keys.filter((key) => key.id !== descriptor.id),
|
|
119
|
+
descriptor,
|
|
120
|
+
].sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
121
|
+
saveLocalKeyDescriptors(next, options);
|
|
122
|
+
return next;
|
|
123
|
+
}
|
|
124
|
+
function stableKeyId(profile, name, now) {
|
|
125
|
+
const hash = createHash('sha256')
|
|
126
|
+
.update(`${profile}\0${name}\0${now.toISOString()}`)
|
|
127
|
+
.digest('hex')
|
|
128
|
+
.slice(0, 12);
|
|
129
|
+
return `key_${hash}`;
|
|
130
|
+
}
|
|
131
|
+
function assertSafeKeyId(keyId) {
|
|
132
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(keyId)) {
|
|
133
|
+
throw new Error(`Invalid key id '${keyId}'. Use letters, numbers, dots, dashes, or underscores.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=keys.js.map
|