@moda-ai/cli 1.1.0 → 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.
Files changed (59) hide show
  1. package/README.md +36 -0
  2. package/dist/agent.d.ts +5 -0
  3. package/dist/agent.js +477 -0
  4. package/dist/analyst-tui.d.ts +81 -0
  5. package/dist/analyst-tui.js +314 -0
  6. package/dist/api-client.d.ts +4 -0
  7. package/dist/api-client.js +20 -3
  8. package/dist/auth.d.ts +22 -3
  9. package/dist/auth.js +230 -66
  10. package/dist/cli.d.ts +20 -3
  11. package/dist/cli.js +768 -93
  12. package/dist/command-context.d.ts +34 -0
  13. package/dist/command-context.js +38 -0
  14. package/dist/command-registry.d.ts +22 -0
  15. package/dist/command-registry.js +15 -0
  16. package/dist/config.d.ts +41 -2
  17. package/dist/config.js +73 -5
  18. package/dist/doctor.d.ts +79 -0
  19. package/dist/doctor.js +1102 -0
  20. package/dist/errors.d.ts +5 -0
  21. package/dist/errors.js +9 -0
  22. package/dist/harness-report.d.ts +138 -0
  23. package/dist/harness-report.js +1464 -0
  24. package/dist/harness.d.ts +233 -0
  25. package/dist/harness.js +3264 -0
  26. package/dist/init/index.d.ts +30 -1
  27. package/dist/init/index.js +383 -23
  28. package/dist/init/rules.d.ts +5 -1
  29. package/dist/init/rules.js +22 -16
  30. package/dist/init/sdk-guide.d.ts +3 -0
  31. package/dist/init/sdk-guide.js +397 -0
  32. package/dist/init/tenant.js +12 -3
  33. package/dist/keys.d.ts +47 -0
  34. package/dist/keys.js +136 -0
  35. package/dist/output.d.ts +118 -0
  36. package/dist/output.js +228 -0
  37. package/dist/production-intelligence.d.ts +134 -0
  38. package/dist/production-intelligence.js +1113 -0
  39. package/dist/profile-commands.d.ts +10 -0
  40. package/dist/profile-commands.js +252 -0
  41. package/dist/profiles.d.ts +90 -0
  42. package/dist/profiles.js +432 -0
  43. package/dist/prompts.d.ts +6 -0
  44. package/dist/prompts.js +368 -0
  45. package/dist/schemas.d.ts +4 -4
  46. package/dist/skills.d.ts +25 -0
  47. package/dist/skills.js +468 -0
  48. package/dist/sync.d.ts +5 -0
  49. package/dist/sync.js +27 -0
  50. package/dist/telemetry.d.ts +18 -0
  51. package/dist/telemetry.js +63 -0
  52. package/dist/tui.d.ts +40 -0
  53. package/dist/tui.js +387 -0
  54. package/dist/workspace.d.ts +12 -0
  55. package/dist/workspace.js +1294 -0
  56. package/hooks/moda-snapshot.sh +41 -0
  57. package/package.json +10 -7
  58. package/skills/moda-cli/SKILL.md +48 -4
  59. package/skills/integrate-moda/SKILL.md +0 -63
@@ -0,0 +1,368 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { createHash } from 'node:crypto';
3
+ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, writeFileSync, } from 'node:fs';
4
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
5
+ import { callControlAPI } from './api-client.js';
6
+ const MODA_DIR = '.moda';
7
+ const MANIFEST_PATH = join(MODA_DIR, 'prompts.yml');
8
+ const LOCK_PATH = join(MODA_DIR, 'prompts.lock.json');
9
+ const DEFAULT_PROMPT_PATHS = [
10
+ 'prompts/**/*.prompt.md',
11
+ 'prompts/**/*.prompt.json',
12
+ 'prompts/**/*.prompt.yaml',
13
+ 'prompts/**/*.prompt.yml',
14
+ ];
15
+ export async function runPromptsCommand(positionals, flags, profileOptions = { profile: flags.profile }) {
16
+ const subcommand = positionals[0] || 'status';
17
+ switch (subcommand) {
18
+ case 'init':
19
+ await initPrompts();
20
+ break;
21
+ case 'status':
22
+ printJson(await getPromptStatus());
23
+ break;
24
+ case 'diff':
25
+ printJson(await getPromptStatus());
26
+ break;
27
+ case 'sync':
28
+ await syncPrompts(flags, profileOptions);
29
+ break;
30
+ case 'promote':
31
+ await promotePrompt(positionals, flags, profileOptions);
32
+ break;
33
+ default:
34
+ throw new Error(`Unknown prompts command '${subcommand}'. Use init, status, diff, sync, or promote.`);
35
+ }
36
+ }
37
+ export async function initPrompts(options = {}) {
38
+ mkdirSync(MODA_DIR, { recursive: true });
39
+ if (!existsSync(MANIFEST_PATH)) {
40
+ writeFileSync(MANIFEST_PATH, [
41
+ 'version: 1',
42
+ 'prompt_paths:',
43
+ ...DEFAULT_PROMPT_PATHS.map((path) => ` - "${path}"`),
44
+ '',
45
+ ].join('\n'));
46
+ }
47
+ if (!options.silent) {
48
+ printJson({
49
+ manifest: MANIFEST_PATH,
50
+ lockfile: LOCK_PATH,
51
+ promptPaths: readManifest().promptPaths,
52
+ });
53
+ }
54
+ }
55
+ async function syncPrompts(flags, profileOptions) {
56
+ if (flags.watch === 'true') {
57
+ await watchPromptSync(flags, profileOptions);
58
+ return;
59
+ }
60
+ await syncPromptsOnce(flags, profileOptions);
61
+ }
62
+ async function syncPromptsOnce(flags, profileOptions) {
63
+ await initPrompts({ silent: true });
64
+ const prompts = discoverPrompts();
65
+ const sourceCommit = getGitCommit();
66
+ const manifestHash = hashFile(MANIFEST_PATH);
67
+ const dryRun = flags['dry-run'] === 'true';
68
+ const response = await callControlAPI('/prompts/sync', {
69
+ method: 'POST',
70
+ body: JSON.stringify({
71
+ prompts: prompts.map((prompt) => ({
72
+ ...prompt,
73
+ sourceCommit,
74
+ manifestHash,
75
+ })),
76
+ sourceCommit,
77
+ manifestHash,
78
+ dryRun,
79
+ }),
80
+ }, profileOptions);
81
+ if (!dryRun) {
82
+ writeLock(prompts, response.synced ?? []);
83
+ }
84
+ printJson({
85
+ synced: response.synced ?? [],
86
+ lockfile: dryRun ? undefined : LOCK_PATH,
87
+ });
88
+ }
89
+ async function watchPromptSync(flags, profileOptions) {
90
+ console.error('Watching prompt files. Press Ctrl+C to stop.');
91
+ let lastSignature = '';
92
+ const intervalMs = Math.max(1000, Number(flags.interval ?? 2500));
93
+ while (true) {
94
+ const status = await getPromptStatus();
95
+ const signature = stableJson(status.prompts);
96
+ if (signature !== lastSignature) {
97
+ lastSignature = signature;
98
+ if (status.changed > 0 || status.new > 0 || status.deleted > 0) {
99
+ await syncPromptsOnce({ ...flags, watch: 'false' }, profileOptions);
100
+ }
101
+ }
102
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
103
+ }
104
+ }
105
+ async function promotePrompt(positionals, flags, profileOptions) {
106
+ const key = positionals[1];
107
+ const label = flags.label;
108
+ const versionId = flags.version || flags['version-id'];
109
+ if (!key)
110
+ throw new Error('Usage: moda prompts promote <prompt_key> --label=prod --version=<version_id>');
111
+ if (!label)
112
+ throw new Error('Missing required flag: --label=dev|staging|prod');
113
+ if (!versionId)
114
+ throw new Error('Missing required flag: --version=<version_id>');
115
+ const response = await callControlAPI(`/prompts/${encodeURIComponent(key)}/labels`, {
116
+ method: 'POST',
117
+ body: JSON.stringify({ label, versionId }),
118
+ }, profileOptions);
119
+ printJson(response);
120
+ }
121
+ async function getPromptStatus() {
122
+ await initPrompts({ silent: true });
123
+ const prompts = discoverPrompts();
124
+ const lock = readLock();
125
+ const rows = prompts.map((prompt) => {
126
+ const locked = lock?.prompts[prompt.key];
127
+ const state = !locked
128
+ ? 'new'
129
+ : locked.contentHash === prompt.contentHash
130
+ ? 'unchanged'
131
+ : 'changed';
132
+ return {
133
+ key: prompt.key,
134
+ sourcePath: prompt.sourcePath,
135
+ contentHash: prompt.contentHash,
136
+ lockedHash: locked?.contentHash ?? null,
137
+ versionId: locked?.versionId ?? null,
138
+ state,
139
+ };
140
+ });
141
+ const discoveredKeys = new Set(prompts.map((prompt) => prompt.key));
142
+ for (const [key, locked] of Object.entries(lock?.prompts ?? {})) {
143
+ if (!discoveredKeys.has(key)) {
144
+ rows.push({
145
+ key,
146
+ sourcePath: locked.sourcePath,
147
+ contentHash: null,
148
+ lockedHash: locked.contentHash,
149
+ versionId: locked.versionId ?? null,
150
+ state: 'deleted',
151
+ });
152
+ }
153
+ }
154
+ return {
155
+ manifest: MANIFEST_PATH,
156
+ lockfile: LOCK_PATH,
157
+ total: rows.length,
158
+ changed: rows.filter((row) => row.state === 'changed').length,
159
+ new: rows.filter((row) => row.state === 'new').length,
160
+ deleted: rows.filter((row) => row.state === 'deleted').length,
161
+ prompts: rows,
162
+ };
163
+ }
164
+ function discoverPrompts() {
165
+ const manifest = readManifest();
166
+ // A pattern like `src/prompts/**/*.prompt.md` should *only* match files under
167
+ // src/prompts. Extract both the base directory (everything up to the first
168
+ // glob segment) and the file suffix so we can scope `walk()` accordingly.
169
+ const rules = manifest.promptPaths
170
+ .map((pattern) => {
171
+ const suffixMatch = pattern.match(/\.prompt\.(md|json|ya?ml)$/i);
172
+ if (!suffixMatch)
173
+ return null;
174
+ const suffix = `.prompt.${suffixMatch[1]}`;
175
+ const globIndex = pattern.search(/[*?[]/);
176
+ const baseRaw = globIndex === -1 ? dirname(pattern) : pattern.slice(0, globIndex);
177
+ const base = baseRaw.replace(/[\\/]+$/, '') || '.';
178
+ const baseAbs = isAbsolute(base) ? base : resolve(process.cwd(), base);
179
+ return { baseAbs, suffix };
180
+ })
181
+ .filter((rule) => Boolean(rule));
182
+ const effectiveRules = rules.length
183
+ ? rules
184
+ : [
185
+ { baseAbs: process.cwd(), suffix: '.prompt.md' },
186
+ { baseAbs: process.cwd(), suffix: '.prompt.json' },
187
+ ];
188
+ const discovered = new Set();
189
+ for (const { baseAbs, suffix } of effectiveRules) {
190
+ if (!existsSync(baseAbs))
191
+ continue;
192
+ for (const file of walk(baseAbs)) {
193
+ if (file.endsWith(suffix))
194
+ discovered.add(file);
195
+ }
196
+ }
197
+ return [...discovered].sort().map(parsePromptFile);
198
+ }
199
+ function parsePromptFile(absPath) {
200
+ const sourcePath = relative(process.cwd(), absPath);
201
+ const raw = readFileSync(absPath, 'utf8');
202
+ if (absPath.endsWith('.json')) {
203
+ const parsed = JSON.parse(raw);
204
+ const key = parsed.key || keyFromPath(sourcePath);
205
+ const content = parsed.content || JSON.stringify(parsed.messages ?? [], null, 2);
206
+ return normalizePrompt({ ...parsed, key, name: parsed.name || key, sourcePath, content });
207
+ }
208
+ const { frontmatter, body } = parseFrontmatter(raw);
209
+ const key = frontmatter.key || keyFromPath(sourcePath);
210
+ return normalizePrompt({
211
+ key,
212
+ name: frontmatter.name || key,
213
+ description: frontmatter.description,
214
+ category: frontmatter.category,
215
+ systemPrompt: frontmatter.system_prompt || frontmatter.systemPrompt,
216
+ modelConfig: frontmatter.model ? { model: frontmatter.model } : undefined,
217
+ sourcePath,
218
+ content: body.trim(),
219
+ });
220
+ }
221
+ function normalizePrompt(prompt) {
222
+ const normalized = stableJson({
223
+ key: prompt.key,
224
+ content: prompt.content,
225
+ systemPrompt: prompt.systemPrompt ?? null,
226
+ messages: prompt.messages ?? [],
227
+ variables: prompt.variables ?? [],
228
+ variablesSchema: prompt.variablesSchema ?? {},
229
+ modelConfig: prompt.modelConfig ?? {},
230
+ toolRefs: prompt.toolRefs ?? [],
231
+ responseSchema: prompt.responseSchema ?? null,
232
+ });
233
+ return {
234
+ key: prompt.key,
235
+ name: prompt.name || prompt.key,
236
+ description: prompt.description,
237
+ sourcePath: prompt.sourcePath,
238
+ content: prompt.content,
239
+ systemPrompt: prompt.systemPrompt,
240
+ messages: prompt.messages,
241
+ variables: prompt.variables,
242
+ variablesSchema: prompt.variablesSchema,
243
+ modelConfig: prompt.modelConfig,
244
+ toolRefs: prompt.toolRefs,
245
+ responseSchema: prompt.responseSchema,
246
+ contentHash: sha256(normalized),
247
+ };
248
+ }
249
+ function readManifest() {
250
+ if (!existsSync(MANIFEST_PATH)) {
251
+ return { promptPaths: DEFAULT_PROMPT_PATHS };
252
+ }
253
+ const text = readFileSync(MANIFEST_PATH, 'utf8');
254
+ const promptPaths = [...text.matchAll(/^\s*-\s*["']?([^"'\n]+)["']?\s*$/gm)]
255
+ .map((match) => match[1])
256
+ .filter((path) => path.includes('.prompt.'));
257
+ return { promptPaths: promptPaths.length ? promptPaths : DEFAULT_PROMPT_PATHS };
258
+ }
259
+ function readLock() {
260
+ if (!existsSync(LOCK_PATH))
261
+ return null;
262
+ return JSON.parse(readFileSync(LOCK_PATH, 'utf8'));
263
+ }
264
+ function writeLock(prompts, synced) {
265
+ const syncedByKey = new Map(synced.map((item) => [item.key, item]));
266
+ const lock = {
267
+ version: 1,
268
+ generatedAt: new Date().toISOString(),
269
+ prompts: Object.fromEntries(prompts.map((prompt) => {
270
+ const result = syncedByKey.get(prompt.key);
271
+ return [prompt.key, {
272
+ key: prompt.key,
273
+ sourcePath: prompt.sourcePath,
274
+ contentHash: prompt.contentHash,
275
+ promptId: result?.promptId,
276
+ versionId: result?.versionId,
277
+ }];
278
+ })),
279
+ };
280
+ mkdirSync(dirname(LOCK_PATH), { recursive: true });
281
+ writeFileSync(LOCK_PATH, `${JSON.stringify(lock, null, 2)}\n`);
282
+ }
283
+ function walk(dir) {
284
+ let entries;
285
+ try {
286
+ entries = readdirSync(dir);
287
+ }
288
+ catch {
289
+ return [];
290
+ }
291
+ const files = [];
292
+ for (const entry of entries) {
293
+ if (entry === 'node_modules' || entry === '.git' || entry === 'dist' || entry === 'build')
294
+ continue;
295
+ const fullPath = join(dir, entry);
296
+ // lstatSync — never follow symlinks. A symlink in the tree could otherwise
297
+ // (a) loop into an ancestor and recurse forever, or (b) silently point at a
298
+ // sensitive file like .env that would then get uploaded as prompt content.
299
+ let stat;
300
+ try {
301
+ stat = lstatSync(fullPath);
302
+ }
303
+ catch {
304
+ continue;
305
+ }
306
+ if (stat.isSymbolicLink())
307
+ continue;
308
+ if (stat.isDirectory())
309
+ files.push(...walk(fullPath));
310
+ else if (stat.isFile())
311
+ files.push(fullPath);
312
+ }
313
+ return files;
314
+ }
315
+ function parseFrontmatter(raw) {
316
+ if (!raw.startsWith('---'))
317
+ return { frontmatter: {}, body: raw };
318
+ const end = raw.indexOf('\n---', 3);
319
+ if (end === -1)
320
+ return { frontmatter: {}, body: raw };
321
+ const frontmatterText = raw.slice(3, end).trim();
322
+ const frontmatter = Object.fromEntries(frontmatterText
323
+ .split('\n')
324
+ .map((line) => line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/))
325
+ .filter((match) => Boolean(match))
326
+ .map((match) => [match[1], match[2].replace(/^["']|["']$/g, '')]));
327
+ return { frontmatter, body: raw.slice(end + 4) };
328
+ }
329
+ function keyFromPath(path) {
330
+ return path
331
+ .replace(/^prompts\//, '')
332
+ .replace(/\.prompt\.(md|json|ya?ml)$/i, '')
333
+ .replace(/[\\/]/g, '.');
334
+ }
335
+ function hashFile(path) {
336
+ return existsSync(path) ? sha256(readFileSync(path, 'utf8')) : '';
337
+ }
338
+ function getGitCommit() {
339
+ try {
340
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
341
+ encoding: 'utf8',
342
+ stdio: ['ignore', 'pipe', 'ignore'],
343
+ }).trim();
344
+ }
345
+ catch {
346
+ return undefined;
347
+ }
348
+ }
349
+ function stableJson(value) {
350
+ return JSON.stringify(sortJson(value));
351
+ }
352
+ function sortJson(value) {
353
+ if (Array.isArray(value))
354
+ return value.map(sortJson);
355
+ if (value && typeof value === 'object') {
356
+ return Object.fromEntries(Object.entries(value)
357
+ .sort(([a], [b]) => a.localeCompare(b))
358
+ .map(([key, item]) => [key, sortJson(item)]));
359
+ }
360
+ return value;
361
+ }
362
+ function sha256(input) {
363
+ return createHash('sha256').update(input).digest('hex');
364
+ }
365
+ function printJson(value) {
366
+ console.log(JSON.stringify(value, null, 2));
367
+ }
368
+ //# sourceMappingURL=prompts.js.map
package/dist/schemas.d.ts CHANGED
@@ -39,20 +39,20 @@ export declare const ConversationsSchema: z.ZodObject<{
39
39
  offset: z.ZodOptional<z.ZodDefault<z.ZodNumber>>;
40
40
  }, "strip", z.ZodTypeAny, {
41
41
  search?: string | undefined;
42
+ environment?: "staging" | "all" | "development" | "production" | undefined;
42
43
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
43
44
  limit?: number | undefined;
44
45
  offset?: number | undefined;
45
46
  cluster_id?: string | undefined;
46
47
  user_id?: string | undefined;
47
- environment?: "all" | "development" | "staging" | "production" | undefined;
48
48
  }, {
49
49
  search?: string | undefined;
50
+ environment?: "staging" | "all" | "development" | "production" | undefined;
50
51
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
51
52
  limit?: number | undefined;
52
53
  offset?: number | undefined;
53
54
  cluster_id?: string | undefined;
54
55
  user_id?: string | undefined;
55
- environment?: "all" | "development" | "staging" | "production" | undefined;
56
56
  }>;
57
57
  export declare const ContextSchema: z.ZodObject<{
58
58
  conversation_id: z.ZodString;
@@ -143,14 +143,14 @@ export declare const RunsSchema: z.ZodObject<{
143
143
  limit?: number | undefined;
144
144
  offset?: number | undefined;
145
145
  user_id?: string | undefined;
146
- outcome?: "running" | "succeeded" | "partial" | "failed" | "loop" | "abandoned" | "unknown" | undefined;
146
+ outcome?: "unknown" | "running" | "failed" | "succeeded" | "partial" | "loop" | "abandoned" | undefined;
147
147
  class_id?: string | undefined;
148
148
  }, {
149
149
  time_range?: "all" | "1h" | "3d" | "7d" | "24h" | "30d" | "90d" | undefined;
150
150
  limit?: number | undefined;
151
151
  offset?: number | undefined;
152
152
  user_id?: string | undefined;
153
- outcome?: "running" | "succeeded" | "partial" | "failed" | "loop" | "abandoned" | "unknown" | undefined;
153
+ outcome?: "unknown" | "running" | "failed" | "succeeded" | "partial" | "loop" | "abandoned" | undefined;
154
154
  class_id?: string | undefined;
155
155
  }>;
156
156
  /** moda run <runId> — single execution detail with side-effect receipts. */
@@ -0,0 +1,25 @@
1
+ import type { ParsedArgs } from './cli.js';
2
+ interface PullSummary {
3
+ total: number;
4
+ pruned: number;
5
+ wrote: number;
6
+ updated: number;
7
+ unchanged: number;
8
+ failed: number;
9
+ }
10
+ /**
11
+ * Pull generated skills into the detected coding agents' skill directories.
12
+ * Exported so `moda init` can run it as a setup step. Best-effort and quiet on
13
+ * an empty store; never throws for "no skills yet".
14
+ */
15
+ export declare function pullSkills(options?: {
16
+ status?: string;
17
+ cwd?: string;
18
+ silent?: boolean;
19
+ }): Promise<PullSummary>;
20
+ /**
21
+ * `moda skills <subcommand>` dispatcher.
22
+ */
23
+ export declare function runSkillsCommand(args: ParsedArgs): Promise<number>;
24
+ export {};
25
+ //# sourceMappingURL=skills.d.ts.map