@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.
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 +40 -1
  17. package/dist/config.js +70 -3
  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,314 @@
1
+ export class AnalystTui {
2
+ state;
3
+ active = false;
4
+ closed = false;
5
+ lastRenderAt = 0;
6
+ spinnerIndex = 0;
7
+ constructor(options) {
8
+ const startedAt = options.startedAt ?? Date.now();
9
+ this.state = {
10
+ adapterName: options.adapterName,
11
+ command: options.command,
12
+ root: options.root,
13
+ reportPath: options.reportPath,
14
+ startedAt,
15
+ phase: 'launching analyst',
16
+ sawOutput: false,
17
+ lastOutputAt: startedAt,
18
+ eventCount: 0,
19
+ rateLimitCount: 0,
20
+ validation: 'pending',
21
+ artifacts: [],
22
+ summary: {},
23
+ timeline: [],
24
+ toolCounts: new Map(),
25
+ completed: false,
26
+ };
27
+ }
28
+ start() {
29
+ if (this.active || this.closed)
30
+ return;
31
+ this.active = true;
32
+ process.stderr.write('\u001b[?1049h\u001b[?25l');
33
+ this.pushTimeline('Moda', 'Launching read-only analyst.');
34
+ this.render(true);
35
+ }
36
+ recordEvent(event) {
37
+ if (this.closed)
38
+ return;
39
+ this.state.sawOutput = true;
40
+ this.state.lastOutputAt = Date.now();
41
+ this.state.eventCount += 1;
42
+ if (event.kind === 'rate_limit') {
43
+ this.state.rateLimitCount += 1;
44
+ this.state.phase = 'rate limit wait';
45
+ }
46
+ else if (event.kind === 'tool') {
47
+ this.state.phase = `using ${event.toolName ?? 'tool'}`;
48
+ if (event.toolName) {
49
+ this.state.toolCounts.set(event.toolName, (this.state.toolCounts.get(event.toolName) ?? 0) + 1);
50
+ }
51
+ }
52
+ else if (event.kind === 'command') {
53
+ this.state.phase = 'running command';
54
+ }
55
+ else if (event.kind === 'agent_text') {
56
+ this.state.phase = inferPhase(event.message);
57
+ }
58
+ const label = event.kind === 'rate_limit'
59
+ ? 'Rate limit'
60
+ : event.kind === 'tool'
61
+ ? event.toolName ?? 'Tool'
62
+ : event.kind === 'command'
63
+ ? 'Command'
64
+ : this.state.adapterName;
65
+ this.pushTimeline(label, event.message);
66
+ this.render();
67
+ }
68
+ heartbeat(options) {
69
+ if (this.closed)
70
+ return;
71
+ this.state.sawOutput = options.sawOutput;
72
+ this.render();
73
+ }
74
+ setValidation(status) {
75
+ if (this.closed)
76
+ return;
77
+ this.state.validation = status;
78
+ this.state.phase = status === 'running'
79
+ ? 'validating cited report'
80
+ : status === 'pass'
81
+ ? 'validation passed'
82
+ : status === 'fail'
83
+ ? 'validation failed'
84
+ : this.state.phase;
85
+ this.pushTimeline('Moda', `Validation ${status}.`);
86
+ this.render(true);
87
+ }
88
+ setSummary(summary) {
89
+ if (this.closed)
90
+ return;
91
+ this.state.summary = { ...this.state.summary, ...summary };
92
+ this.render(true);
93
+ }
94
+ setArtifacts(artifacts) {
95
+ if (this.closed)
96
+ return;
97
+ this.state.artifacts = artifacts;
98
+ if (artifacts.length > 0)
99
+ this.pushTimeline('Moda', `Wrote ${artifacts.length} local artifact${artifacts.length === 1 ? '' : 's'}.`);
100
+ this.render(true);
101
+ }
102
+ complete(status) {
103
+ if (this.closed)
104
+ return;
105
+ this.state.completed = true;
106
+ this.setValidation(status);
107
+ }
108
+ stop() {
109
+ if (this.closed)
110
+ return;
111
+ this.closed = true;
112
+ if (this.active) {
113
+ this.render(true);
114
+ process.stderr.write('\u001b[?25h\u001b[?1049l');
115
+ }
116
+ this.active = false;
117
+ }
118
+ pushTimeline(label, message) {
119
+ const compact = compactWhitespace(message);
120
+ if (!compact)
121
+ return;
122
+ const last = this.state.timeline.at(-1);
123
+ if (last?.label === label && last.message === compact)
124
+ return;
125
+ this.state.timeline.push({
126
+ time: timeOfDay(new Date()),
127
+ label,
128
+ message: compact,
129
+ });
130
+ if (this.state.timeline.length > 80)
131
+ this.state.timeline.splice(0, this.state.timeline.length - 80);
132
+ }
133
+ render(force = false) {
134
+ if (!this.active)
135
+ return;
136
+ const now = Date.now();
137
+ if (!force && now - this.lastRenderAt < 120)
138
+ return;
139
+ this.lastRenderAt = now;
140
+ const frame = renderAnalystTuiFrame(this.state, {
141
+ color: process.env.NO_COLOR !== '1',
142
+ columns: process.stderr.columns,
143
+ rows: process.stderr.rows,
144
+ now,
145
+ spinnerFrame: spinner(this.spinnerIndex++),
146
+ });
147
+ process.stderr.write(`\u001b[H\u001b[2J${frame}`);
148
+ }
149
+ }
150
+ export function renderAnalystTuiFrame(state, options = {}) {
151
+ const color = options.color ?? false;
152
+ const width = Math.max(72, Math.min(options.columns ?? 96, 128));
153
+ const rows = Math.max(22, Math.min(options.rows ?? 32, 48));
154
+ const now = options.now ?? Date.now();
155
+ const paint = (code, text) => color ? `\u001b[${code}m${text}\u001b[0m` : text;
156
+ const dim = (text) => paint(90, text);
157
+ const cyan = (text) => paint(36, text);
158
+ const green = (text) => paint(32, text);
159
+ const yellow = (text) => paint(33, text);
160
+ const red = (text) => paint(31, text);
161
+ const bold = (text) => paint(1, text);
162
+ const elapsedSeconds = Math.max(0, Math.round((now - state.startedAt) / 1000));
163
+ const quietForSeconds = Math.max(0, Math.round((now - state.lastOutputAt) / 1000));
164
+ const validation = state.validation === 'pass'
165
+ ? green('pass')
166
+ : state.validation === 'fail'
167
+ ? red('fail')
168
+ : state.validation === 'running'
169
+ ? yellow('running')
170
+ : dim('pending');
171
+ const spinnerText = state.completed ? (state.validation === 'pass' ? green('ok') : red('fail')) : cyan(options.spinnerFrame ?? '|');
172
+ const timelineRows = Math.max(6, rows - 22);
173
+ const tools = [...state.toolCounts.entries()]
174
+ .sort((a, b) => b[1] - a[1])
175
+ .slice(0, 4)
176
+ .map(([name, count]) => `${name} ${count}`)
177
+ .join(', ') || 'none yet';
178
+ const lines = [];
179
+ lines.push(`${bold('Moda Analyst')} ${dim('Step 4/7 Harness Discovery')}`);
180
+ lines.push(rule(width));
181
+ lines.push(`${spinnerText} ${state.adapterName} ${dim('read-only repository access')} ${dim(`elapsed ${formatDuration(elapsedSeconds)}`)} ${dim(state.sawOutput ? `last output ${formatDuration(quietForSeconds)} ago` : 'waiting for first output')}`);
182
+ lines.push('');
183
+ lines.push(cyan('Session'));
184
+ lines.push(keyValue('Phase', state.phase, width));
185
+ lines.push(keyValue('Command', state.command, width));
186
+ lines.push(keyValue('Root', state.root, width));
187
+ lines.push(keyValue('Output', state.reportPath, width));
188
+ lines.push(keyValue('Boundary', 'Agent can inspect files and print a report. Moda validates, then writes artifacts.', width));
189
+ lines.push('');
190
+ lines.push(cyan('Status'));
191
+ lines.push(` validation ${validation}`);
192
+ lines.push(` stream events ${state.eventCount}`);
193
+ lines.push(` rate limits ${state.rateLimitCount}`);
194
+ lines.push(` tools ${tools}`);
195
+ lines.push('');
196
+ lines.push(cyan('Evidence'));
197
+ lines.push(` runtime agents ${metric(state.summary.runtimeAgentCount)}`);
198
+ lines.push(` agent families ${metric(state.summary.agentFamilyCount)}`);
199
+ lines.push(` artifacts ${metric(state.summary.artifactCount)}`);
200
+ lines.push(` providers ${metric(state.summary.modelProviderCount)}`);
201
+ lines.push(` claims/citations ${metricPair(state.summary.claimCount, state.summary.citationCount)}`);
202
+ lines.push('');
203
+ lines.push(cyan('Timeline'));
204
+ const timeline = state.timeline.slice(-timelineRows);
205
+ if (timeline.length === 0) {
206
+ lines.push(` ${dim('Waiting for analyst output...')}`);
207
+ }
208
+ else {
209
+ for (const item of timeline) {
210
+ const prefix = ` ${dim(item.time)} ${item.label.padEnd(11).slice(0, 11)} `;
211
+ lines.push(...wrapWithPrefix(prefix, item.message, width));
212
+ }
213
+ }
214
+ if (state.artifacts.length > 0) {
215
+ lines.push('');
216
+ lines.push(cyan('Artifacts'));
217
+ for (const artifact of state.artifacts.slice(0, 5))
218
+ lines.push(` ${artifact}`);
219
+ }
220
+ lines.push('');
221
+ lines.push(dim('Ctrl+C aborts | --no-tui shows raw stream | --agent --stream emits NDJSON for agents'));
222
+ return fitToScreen(lines, rows, width).join('\n');
223
+ }
224
+ export function shouldUseAnalystTui(flags, outputMode) {
225
+ if (outputMode !== 'human')
226
+ return false;
227
+ if (flags['no-tui'] === 'true' || process.env.MODA_NO_TUI === '1')
228
+ return false;
229
+ if (process.env.TERM === 'dumb')
230
+ return false;
231
+ if (flags.tui === 'true')
232
+ return process.stderr.isTTY === true;
233
+ return process.stderr.isTTY === true && process.env.CI !== 'true';
234
+ }
235
+ function inferPhase(message) {
236
+ const lower = message.toLowerCase();
237
+ if (lower.includes('report') && (lower.includes('json') || lower.includes('final')))
238
+ return 'producing cited report';
239
+ if (lower.includes('citation') || lower.includes('line number') || lower.includes('evidence'))
240
+ return 'verifying citations';
241
+ if (lower.includes('read') || lower.includes('file'))
242
+ return 'reading source evidence';
243
+ if (lower.includes('repository') || lower.includes('structure') || lower.includes('map'))
244
+ return 'mapping repository';
245
+ if (lower.includes('agent') || lower.includes('harness'))
246
+ return 'classifying harness';
247
+ return 'investigating repo';
248
+ }
249
+ function keyValue(label, value, width) {
250
+ const prefix = ` ${label.padEnd(10)} `;
251
+ return wrapWithPrefix(prefix, value, width).join('\n');
252
+ }
253
+ function wrapWithPrefix(prefix, text, width) {
254
+ const available = Math.max(16, width - visibleLength(prefix));
255
+ const words = compactWhitespace(text).split(' ');
256
+ const lines = [];
257
+ let current = '';
258
+ for (const word of words) {
259
+ const next = current ? `${current} ${word}` : word;
260
+ if (visibleLength(next) > available && current) {
261
+ lines.push(`${prefix}${current}`);
262
+ current = word;
263
+ }
264
+ else {
265
+ current = next;
266
+ }
267
+ }
268
+ if (current)
269
+ lines.push(`${prefix}${current}`);
270
+ return lines.length > 0 ? lines : [prefix.trimEnd()];
271
+ }
272
+ function fitToScreen(lines, rows, width) {
273
+ return lines
274
+ .slice(0, rows)
275
+ .map((line) => trimVisible(line, width));
276
+ }
277
+ function trimVisible(value, width) {
278
+ if (visibleLength(value) <= width)
279
+ return value;
280
+ const stripped = stripAnsi(value);
281
+ return stripped.length > width ? `${stripped.slice(0, Math.max(0, width - 3))}...` : stripped;
282
+ }
283
+ function rule(width) {
284
+ return '-'.repeat(width);
285
+ }
286
+ function metric(value) {
287
+ return typeof value === 'number' && Number.isFinite(value) ? String(value) : 'pending';
288
+ }
289
+ function metricPair(left, right) {
290
+ return `${metric(left)} / ${metric(right)}`;
291
+ }
292
+ function formatDuration(seconds) {
293
+ if (seconds < 60)
294
+ return `${seconds}s`;
295
+ const minutes = Math.floor(seconds / 60);
296
+ const remaining = seconds % 60;
297
+ return `${minutes}m ${remaining}s`;
298
+ }
299
+ function timeOfDay(date) {
300
+ return date.toTimeString().slice(0, 8);
301
+ }
302
+ function spinner(index) {
303
+ return ['|', '/', '-', '\\'][index % 4] ?? '|';
304
+ }
305
+ function compactWhitespace(value) {
306
+ return value.replace(/\s+/g, ' ').trim();
307
+ }
308
+ function visibleLength(value) {
309
+ return stripAnsi(value).length;
310
+ }
311
+ function stripAnsi(value) {
312
+ return value.replace(/\u001b\[[0-9;?]*[A-Za-z]/g, '');
313
+ }
314
+ //# sourceMappingURL=analyst-tui.js.map
@@ -1,7 +1,11 @@
1
+ import type { ProfileIOOptions } from './profiles.js';
1
2
  export declare class ApiError extends Error {
2
3
  readonly statusCode: number;
3
4
  readonly responseBody: string;
4
5
  constructor(message: string, statusCode: number, responseBody: string);
5
6
  }
6
7
  export declare function callDataAPI(endpoint: string): Promise<unknown>;
8
+ export declare function callControlAPI(endpoint: string, options?: RequestInit, profileOptions?: ProfileIOOptions): Promise<unknown>;
9
+ export declare function getApiRequestCount(): number;
10
+ export declare function resetApiRequestCount(): void;
7
11
  //# sourceMappingURL=api-client.d.ts.map
@@ -1,8 +1,9 @@
1
1
  // src/api-client.ts
2
- import { MODA_API_KEY, MODA_BASE_URL } from './config.js';
2
+ import { resolveApiKey, resolveModaBaseUrl } from './config.js';
3
3
  const REQUEST_TIMEOUT_MS = 30_000;
4
4
  const MAX_RETRIES = 3;
5
5
  const INITIAL_BACKOFF_MS = 500;
6
+ let apiRequestCount = 0;
6
7
  export class ApiError extends Error {
7
8
  statusCode;
8
9
  responseBody;
@@ -17,7 +18,15 @@ function isRetryable(status) {
17
18
  return status === 429 || status >= 500;
18
19
  }
19
20
  export async function callDataAPI(endpoint) {
20
- const url = `${MODA_BASE_URL}/api/v1/data${endpoint}`;
21
+ apiRequestCount++;
22
+ const url = `${resolveModaBaseUrl()}/api/v1/data${endpoint}`;
23
+ return callModaAPI(url, { method: 'GET' });
24
+ }
25
+ export async function callControlAPI(endpoint, options = {}, profileOptions = {}) {
26
+ const url = `${resolveModaBaseUrl(profileOptions)}/api${endpoint}`;
27
+ return callModaAPI(url, options, profileOptions);
28
+ }
29
+ async function callModaAPI(url, options, profileOptions = {}) {
21
30
  let lastError;
22
31
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
23
32
  if (attempt > 0) {
@@ -28,9 +37,11 @@ export async function callDataAPI(endpoint) {
28
37
  const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
29
38
  try {
30
39
  const response = await fetch(url, {
40
+ ...options,
31
41
  headers: {
32
- 'x-api-key': MODA_API_KEY,
42
+ 'x-api-key': resolveApiKey(profileOptions) ?? '',
33
43
  'Content-Type': 'application/json',
44
+ ...(options.headers ?? {}),
34
45
  },
35
46
  signal: controller.signal,
36
47
  });
@@ -64,4 +75,10 @@ export async function callDataAPI(endpoint) {
64
75
  }
65
76
  throw lastError ?? new Error('Request failed after retries');
66
77
  }
78
+ export function getApiRequestCount() {
79
+ return apiRequestCount;
80
+ }
81
+ export function resetApiRequestCount() {
82
+ apiRequestCount = 0;
83
+ }
67
84
  //# sourceMappingURL=api-client.js.map
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,23 @@
1
- export declare function loadToken(): string | null;
2
- export declare function login(screen?: 'login' | 'signup'): Promise<string>;
3
- export declare function authFetch(path: string, options?: RequestInit): Promise<Response>;
1
+ import { type ProfileIOOptions } from './profiles.js';
2
+ export interface StoredAuth {
3
+ access_token: string;
4
+ expires_at: number;
5
+ profile?: string;
6
+ token_type?: 'bearer';
7
+ }
8
+ export interface AuthSessionOptions extends ProfileIOOptions {
9
+ includeExpired?: boolean;
10
+ }
11
+ export declare function isAuthSessionValid(data: StoredAuth | null, now?: number): data is StoredAuth;
12
+ export declare function loadToken(options?: AuthSessionOptions): string | null;
13
+ export declare function loadAuthSession(options?: AuthSessionOptions): StoredAuth | null;
14
+ export declare function saveAuthSession(accessToken: string, expiresIn: number, options?: ProfileIOOptions): StoredAuth;
15
+ export declare function clearAuthSession(options?: ProfileIOOptions): void;
16
+ export interface LoginOptions {
17
+ callbackPort?: number;
18
+ timeoutMs?: number;
19
+ openUrl?: (url: string) => Promise<void>;
20
+ }
21
+ export declare function login(screen?: 'login' | 'signup', options?: LoginOptions): Promise<string>;
22
+ export declare function authFetch(path: string, options?: RequestInit, authOptions?: AuthSessionOptions): Promise<Response>;
4
23
  //# sourceMappingURL=auth.d.ts.map