@moda-ai/cli 1.22.1 → 1.23.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/dist/cli-c4se7a5d.js +762 -0
- package/dist/cli-mnqa4rp6.js +1563 -0
- package/dist/cli-stmjdgvy.js +2842 -0
- package/dist/cli-y55bgg14.js +6980 -0
- package/dist/cli-ysegdrm5.js +286 -0
- package/dist/cli.js +4779 -1159
- package/dist/harness-github-actions-7t3cncnt.js +95 -0
- package/dist/index-4ser01zr.js +2769 -0
- package/dist/provision-hm1ppwj6.js +74 -0
- package/package.json +3 -2
- package/skills/integration/index.json +2 -2
- package/dist/agent-schemas.d.ts +0 -3
- package/dist/agent-schemas.js +0 -358
- package/dist/agent.d.ts +0 -5
- package/dist/agent.js +0 -477
- package/dist/analyst-tui.d.ts +0 -81
- package/dist/analyst-tui.js +0 -314
- package/dist/api-client.d.ts +0 -46
- package/dist/api-client.js +0 -120
- package/dist/ask-stream.d.ts +0 -146
- package/dist/ask-stream.js +0 -473
- package/dist/auth-storage.d.ts +0 -35
- package/dist/auth-storage.js +0 -159
- package/dist/auth.d.ts +0 -50
- package/dist/auth.js +0 -694
- package/dist/cli-session.d.ts +0 -26
- package/dist/cli-session.js +0 -169
- package/dist/cli.d.ts +0 -44
- package/dist/command-context.d.ts +0 -62
- package/dist/command-context.js +0 -57
- package/dist/command-registry.d.ts +0 -22
- package/dist/command-registry.js +0 -15
- package/dist/config.d.ts +0 -60
- package/dist/config.js +0 -110
- package/dist/doctor.d.ts +0 -79
- package/dist/doctor.js +0 -1182
- package/dist/errors.d.ts +0 -50
- package/dist/errors.js +0 -64
- package/dist/exit-funnel.d.ts +0 -14
- package/dist/exit-funnel.js +0 -53
- package/dist/harness-artifact-bodies.d.ts +0 -28
- package/dist/harness-artifact-bodies.js +0 -276
- package/dist/harness-candidates.d.ts +0 -77
- package/dist/harness-candidates.js +0 -482
- package/dist/harness-remote.d.ts +0 -135
- package/dist/harness-remote.js +0 -540
- package/dist/harness-report.d.ts +0 -145
- package/dist/harness-report.js +0 -1579
- package/dist/harness-sync-limits.d.ts +0 -8
- package/dist/harness-sync-limits.js +0 -21
- package/dist/harness.d.ts +0 -463
- package/dist/harness.js +0 -4009
- package/dist/init/index.d.ts +0 -78
- package/dist/init/index.js +0 -1442
- package/dist/init/mcp.d.ts +0 -2
- package/dist/init/mcp.js +0 -27
- package/dist/init/progress-board.d.ts +0 -80
- package/dist/init/progress-board.js +0 -225
- package/dist/init/rules.d.ts +0 -6
- package/dist/init/rules.js +0 -171
- package/dist/init/sdk-catalog.d.ts +0 -10
- package/dist/init/sdk-catalog.js +0 -80
- package/dist/init/sdk-guide.d.ts +0 -19
- package/dist/init/sdk-guide.js +0 -185
- package/dist/init/sdk-integration.d.ts +0 -53
- package/dist/init/sdk-integration.js +0 -835
- package/dist/init/sdk-protocol.d.ts +0 -135
- package/dist/init/sdk-protocol.js +0 -152
- package/dist/init/sdk-verification.d.ts +0 -27
- package/dist/init/sdk-verification.js +0 -123
- package/dist/init/tenant.d.ts +0 -67
- package/dist/init/tenant.js +0 -421
- package/dist/intelligence-schema.d.ts +0 -349
- package/dist/intelligence-schema.js +0 -174
- package/dist/keys.d.ts +0 -47
- package/dist/keys.js +0 -136
- package/dist/manifest.d.ts +0 -5
- package/dist/manifest.js +0 -114
- package/dist/markdown-terminal.d.ts +0 -49
- package/dist/markdown-terminal.js +0 -212
- package/dist/output.d.ts +0 -191
- package/dist/output.js +0 -624
- package/dist/production-intelligence.d.ts +0 -175
- package/dist/production-intelligence.js +0 -1430
- package/dist/profile-commands.d.ts +0 -10
- package/dist/profile-commands.js +0 -342
- package/dist/profiles.d.ts +0 -112
- package/dist/profiles.js +0 -573
- package/dist/prompts-ab.d.ts +0 -71
- package/dist/prompts-ab.js +0 -313
- package/dist/prompts-propose.d.ts +0 -15
- package/dist/prompts-propose.js +0 -212
- package/dist/prompts.d.ts +0 -58
- package/dist/prompts.js +0 -418
- package/dist/provision.d.ts +0 -28
- package/dist/provision.js +0 -104
- package/dist/redact.d.ts +0 -2
- package/dist/redact.js +0 -90
- package/dist/schemas.d.ts +0 -225
- package/dist/schemas.js +0 -126
- package/dist/skills-bundled.d.ts +0 -35
- package/dist/skills-bundled.js +0 -157
- package/dist/skills-manifest.d.ts +0 -33
- package/dist/skills-manifest.js +0 -244
- package/dist/skills.d.ts +0 -57
- package/dist/skills.js +0 -783
- package/dist/sync.d.ts +0 -5
- package/dist/sync.js +0 -36
- package/dist/telemetry.d.ts +0 -56
- package/dist/telemetry.js +0 -167
- package/dist/transforms.d.ts +0 -25
- package/dist/transforms.js +0 -102
- package/dist/tui.d.ts +0 -48
- package/dist/tui.js +0 -393
- package/dist/update-check.d.ts +0 -31
- package/dist/update-check.js +0 -141
- package/dist/workspace.d.ts +0 -12
- package/dist/workspace.js +0 -1294
|
@@ -0,0 +1,1563 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
class CliInputError extends Error {
|
|
6
|
+
usage;
|
|
7
|
+
constructor(message, usage) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.usage = usage;
|
|
10
|
+
this.name = "CliInputError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class CliAccessError extends Error {
|
|
15
|
+
guidance;
|
|
16
|
+
constructor(message, options = {}) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "CliAccessError";
|
|
19
|
+
this.guidance = options.guidance;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class CliInputRequiredError extends Error {
|
|
24
|
+
input_request;
|
|
25
|
+
constructor(message, input_request, usage) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.input_request = input_request;
|
|
28
|
+
this.name = "CliInputRequiredError";
|
|
29
|
+
if (usage)
|
|
30
|
+
this.usage = usage;
|
|
31
|
+
}
|
|
32
|
+
usage;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
class CliAuthError extends Error {
|
|
36
|
+
auth;
|
|
37
|
+
constructor(message, auth, usage) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.auth = auth;
|
|
40
|
+
this.name = "CliAuthError";
|
|
41
|
+
if (usage)
|
|
42
|
+
this.usage = usage;
|
|
43
|
+
}
|
|
44
|
+
usage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class CliCancellationError extends Error {
|
|
48
|
+
constructor(message) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.name = "CliCancellationError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function isNetworkLikeError(error) {
|
|
54
|
+
return error instanceof Error && (error.message.startsWith("Network error:") || error.message.startsWith("Request timed out") || error.name === "AbortError" || /(?:ECONNRESET|ECONNREFUSED|ETIMEDOUT|ENOTFOUND|socket hang up)/i.test(error.message));
|
|
55
|
+
}
|
|
56
|
+
function isApiLikeError(error) {
|
|
57
|
+
return error instanceof Error && (error.name === "ApiError" || ("statusCode" in error) && typeof error.statusCode === "number");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/config.ts
|
|
61
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
62
|
+
|
|
63
|
+
// src/profiles.ts
|
|
64
|
+
import {
|
|
65
|
+
chmodSync,
|
|
66
|
+
existsSync,
|
|
67
|
+
mkdirSync,
|
|
68
|
+
readFileSync,
|
|
69
|
+
rmSync,
|
|
70
|
+
statSync,
|
|
71
|
+
writeFileSync
|
|
72
|
+
} from "node:fs";
|
|
73
|
+
import { homedir } from "node:os";
|
|
74
|
+
import { dirname, isAbsolute, join, normalize, relative } from "node:path";
|
|
75
|
+
var PROFILE_CONFIG_VERSION = 1;
|
|
76
|
+
var PROD_BASE_URL = "https://moda.dev";
|
|
77
|
+
var DEFAULT_INGEST_URL = "https://moda-ingest.modas.workers.dev";
|
|
78
|
+
var DEFAULT_PROFILE_NAME = "prod";
|
|
79
|
+
function getEnv(options = {}) {
|
|
80
|
+
return options.env ?? process.env;
|
|
81
|
+
}
|
|
82
|
+
function resolveModaHome(options = {}) {
|
|
83
|
+
const env = getEnv(options);
|
|
84
|
+
return options.home || env.HOME || env.USERPROFILE || homedir();
|
|
85
|
+
}
|
|
86
|
+
function getModaPaths(options = {}) {
|
|
87
|
+
const home = resolveModaHome(options);
|
|
88
|
+
const dir = join(home, ".moda");
|
|
89
|
+
return {
|
|
90
|
+
home,
|
|
91
|
+
dir,
|
|
92
|
+
legacyConfigPath: join(dir, "config.json"),
|
|
93
|
+
legacyAuthPath: join(dir, "auth.json"),
|
|
94
|
+
profileConfigPath: join(dir, "profiles.json"),
|
|
95
|
+
secretsDir: join(dir, "secrets")
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function getModaDir(options = {}) {
|
|
99
|
+
return getModaPaths(options).dir;
|
|
100
|
+
}
|
|
101
|
+
function isRecord(value) {
|
|
102
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
function optionalString(value) {
|
|
105
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
106
|
+
}
|
|
107
|
+
function readJsonFile(path) {
|
|
108
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
109
|
+
}
|
|
110
|
+
function safeReadRecord(path) {
|
|
111
|
+
try {
|
|
112
|
+
if (!existsSync(path))
|
|
113
|
+
return;
|
|
114
|
+
const parsed = readJsonFile(path);
|
|
115
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function loadLegacyCliConfig(options = {}) {
|
|
121
|
+
const parsed = safeReadRecord(getModaPaths(options).legacyConfigPath);
|
|
122
|
+
if (!parsed)
|
|
123
|
+
return {};
|
|
124
|
+
return {
|
|
125
|
+
api_key: optionalString(parsed.api_key),
|
|
126
|
+
ingest_url: optionalString(parsed.ingest_url),
|
|
127
|
+
tenant_id: optionalString(parsed.tenant_id)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function writeLegacyCliConfig(update, options = {}) {
|
|
131
|
+
const paths = getModaPaths(options);
|
|
132
|
+
mkdirSync(paths.dir, { recursive: true });
|
|
133
|
+
const merged = { ...loadLegacyCliConfig(options), ...update };
|
|
134
|
+
writeSecretJsonFile(paths.legacyConfigPath, merged);
|
|
135
|
+
return merged;
|
|
136
|
+
}
|
|
137
|
+
function profileDefaults(name, environment, now) {
|
|
138
|
+
const local = environment === "local";
|
|
139
|
+
const staging = environment === "staging";
|
|
140
|
+
return {
|
|
141
|
+
name,
|
|
142
|
+
environment,
|
|
143
|
+
base_url: local ? "http://localhost:3000" : staging ? "https://staging.moda.dev" : PROD_BASE_URL,
|
|
144
|
+
ingest_url: local ? "http://localhost:8787" : DEFAULT_INGEST_URL,
|
|
145
|
+
metadata: {
|
|
146
|
+
created_at: now.toISOString(),
|
|
147
|
+
source: "default"
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function createDefaultProfiles(options = {}) {
|
|
152
|
+
const now = options.now ?? new Date;
|
|
153
|
+
return {
|
|
154
|
+
local: profileDefaults("local", "local", now),
|
|
155
|
+
staging: profileDefaults("staging", "staging", now),
|
|
156
|
+
prod: profileDefaults("prod", "prod", now)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
function isLoopbackHostname(hostname) {
|
|
160
|
+
const host = hostname.toLowerCase();
|
|
161
|
+
return host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
162
|
+
}
|
|
163
|
+
function isLoopbackBaseUrl(url) {
|
|
164
|
+
try {
|
|
165
|
+
return isLoopbackHostname(new URL(url).hostname);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function defaultIngestUrlForEnvironment(environment) {
|
|
171
|
+
if (environment === "local")
|
|
172
|
+
return "http://localhost:8787";
|
|
173
|
+
return DEFAULT_INGEST_URL;
|
|
174
|
+
}
|
|
175
|
+
function environmentAllowsLoopback(environment) {
|
|
176
|
+
return environment !== "prod";
|
|
177
|
+
}
|
|
178
|
+
function sanitizeProfileBaseUrl(url, environment) {
|
|
179
|
+
const normalized = normalizeModaBaseUrl(url);
|
|
180
|
+
if (environmentAllowsLoopback(environment) || !isLoopbackBaseUrl(normalized)) {
|
|
181
|
+
return normalized;
|
|
182
|
+
}
|
|
183
|
+
return PROD_BASE_URL;
|
|
184
|
+
}
|
|
185
|
+
function sanitizeProfileIngestUrl(url, environment) {
|
|
186
|
+
if (!url)
|
|
187
|
+
return url;
|
|
188
|
+
if (environmentAllowsLoopback(environment) || !isLoopbackBaseUrl(url)) {
|
|
189
|
+
return url;
|
|
190
|
+
}
|
|
191
|
+
return DEFAULT_INGEST_URL;
|
|
192
|
+
}
|
|
193
|
+
function enforceNonLoopbackBaseUrl(url, environment) {
|
|
194
|
+
return sanitizeProfileBaseUrl(url, environment);
|
|
195
|
+
}
|
|
196
|
+
function enforceNonLoopbackIngestUrl(url, environment) {
|
|
197
|
+
return sanitizeProfileIngestUrl(url, environment) ?? defaultIngestUrlForEnvironment(environment);
|
|
198
|
+
}
|
|
199
|
+
function activeProfileEnvironment(options) {
|
|
200
|
+
try {
|
|
201
|
+
return getActiveProfile(options).environment;
|
|
202
|
+
} catch {
|
|
203
|
+
return "prod";
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function normalizeEnvironment(value) {
|
|
207
|
+
if (value === "local" || value === "dev" || value === "staging" || value === "prod") {
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
return "prod";
|
|
211
|
+
}
|
|
212
|
+
function isSecretReference(value) {
|
|
213
|
+
return value.startsWith("env:") || value.startsWith("file:") || value === "legacy:config.api_key" || value === "legacy:auth.access_token";
|
|
214
|
+
}
|
|
215
|
+
function normalizeSecretReference(value) {
|
|
216
|
+
if (typeof value !== "string")
|
|
217
|
+
return;
|
|
218
|
+
return isSecretReference(value) ? value : undefined;
|
|
219
|
+
}
|
|
220
|
+
function assertValidProfileName(name) {
|
|
221
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) {
|
|
222
|
+
throw new Error(`Invalid profile name '${name}'. Use letters, numbers, dots, dashes, or underscores.`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function normalizeProfile(fallbackName, value, options) {
|
|
226
|
+
if (!isRecord(value))
|
|
227
|
+
return;
|
|
228
|
+
const name = optionalString(value.name) ?? fallbackName;
|
|
229
|
+
try {
|
|
230
|
+
assertValidProfileName(name);
|
|
231
|
+
} catch {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const defaults = profileDefaults(name, normalizeEnvironment(value.environment), options.now ?? new Date);
|
|
235
|
+
return {
|
|
236
|
+
...defaults,
|
|
237
|
+
base_url: sanitizeProfileBaseUrl(optionalString(value.base_url) ?? defaults.base_url, normalizeEnvironment(value.environment)),
|
|
238
|
+
ingest_url: sanitizeProfileIngestUrl(optionalString(value.ingest_url) ?? defaults.ingest_url, normalizeEnvironment(value.environment)),
|
|
239
|
+
tenant_id: optionalString(value.tenant_id),
|
|
240
|
+
workspace_id: optionalString(value.workspace_id),
|
|
241
|
+
api_key_ref: normalizeSecretReference(value.api_key_ref),
|
|
242
|
+
auth_token_ref: normalizeSecretReference(value.auth_token_ref),
|
|
243
|
+
metadata: isRecord(value.metadata) ? value.metadata : defaults.metadata
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function inspectLegacyAuth(path) {
|
|
247
|
+
const parsed = safeReadRecord(path);
|
|
248
|
+
const expiresAt = typeof parsed?.expires_at === "number" ? parsed.expires_at : undefined;
|
|
249
|
+
return {
|
|
250
|
+
path,
|
|
251
|
+
present: Boolean(parsed),
|
|
252
|
+
has_access_token: typeof parsed?.access_token === "string",
|
|
253
|
+
expires_at: expiresAt,
|
|
254
|
+
expired: expiresAt === undefined ? undefined : Date.now() / 1000 > expiresAt - 60
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function inspectProfileMigrationState(options = {}) {
|
|
258
|
+
const paths = getModaPaths(options);
|
|
259
|
+
const legacyConfig = loadLegacyCliConfig(options);
|
|
260
|
+
return {
|
|
261
|
+
legacy_config: {
|
|
262
|
+
path: paths.legacyConfigPath,
|
|
263
|
+
present: existsSync(paths.legacyConfigPath),
|
|
264
|
+
has_api_key: Boolean(legacyConfig.api_key),
|
|
265
|
+
has_ingest_url: Boolean(legacyConfig.ingest_url),
|
|
266
|
+
has_tenant_id: Boolean(legacyConfig.tenant_id)
|
|
267
|
+
},
|
|
268
|
+
legacy_auth: inspectLegacyAuth(paths.legacyAuthPath)
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
function applyLegacyAwareness(config, options) {
|
|
272
|
+
const legacy = loadLegacyCliConfig(options);
|
|
273
|
+
const migration = inspectProfileMigrationState(options);
|
|
274
|
+
const prod = config.profiles.prod;
|
|
275
|
+
if (prod && (legacy.api_key || legacy.ingest_url || legacy.tenant_id)) {
|
|
276
|
+
const migratedIngestUrl = legacy.ingest_url && !isLoopbackBaseUrl(legacy.ingest_url) ? legacy.ingest_url : prod.ingest_url;
|
|
277
|
+
config.profiles.prod = {
|
|
278
|
+
...prod,
|
|
279
|
+
ingest_url: migratedIngestUrl,
|
|
280
|
+
tenant_id: legacy.tenant_id ?? prod.tenant_id,
|
|
281
|
+
api_key_ref: legacy.api_key ? "legacy:config.api_key" : prod.api_key_ref,
|
|
282
|
+
auth_token_ref: migration.legacy_auth.has_access_token ? "legacy:auth.access_token" : prod.auth_token_ref,
|
|
283
|
+
metadata: {
|
|
284
|
+
...prod.metadata,
|
|
285
|
+
source: "migration",
|
|
286
|
+
legacy_config_path: migration.legacy_config.path,
|
|
287
|
+
legacy_auth_path: migration.legacy_auth.path
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
...config,
|
|
293
|
+
migrations: migration
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function normalizeProfileConfig(raw, options = {}) {
|
|
297
|
+
const profiles = createDefaultProfiles(options);
|
|
298
|
+
let activeProfile = DEFAULT_PROFILE_NAME;
|
|
299
|
+
if (isRecord(raw)) {
|
|
300
|
+
if (isRecord(raw.profiles)) {
|
|
301
|
+
for (const [name, profile] of Object.entries(raw.profiles)) {
|
|
302
|
+
const normalized = normalizeProfile(name, profile, options);
|
|
303
|
+
if (normalized)
|
|
304
|
+
profiles[normalized.name] = normalized;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
const configuredActive = optionalString(raw.active_profile);
|
|
308
|
+
if (configuredActive && profiles[configuredActive]) {
|
|
309
|
+
activeProfile = configuredActive;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return applyLegacyAwareness({
|
|
313
|
+
version: PROFILE_CONFIG_VERSION,
|
|
314
|
+
active_profile: activeProfile,
|
|
315
|
+
profiles,
|
|
316
|
+
migrations: inspectProfileMigrationState(options)
|
|
317
|
+
}, options);
|
|
318
|
+
}
|
|
319
|
+
function loadProfileConfig(options = {}) {
|
|
320
|
+
const path = getModaPaths(options).profileConfigPath;
|
|
321
|
+
const parsed = safeReadRecord(path);
|
|
322
|
+
return normalizeProfileConfig(parsed, options);
|
|
323
|
+
}
|
|
324
|
+
function saveProfileConfig(config, options = {}) {
|
|
325
|
+
const active = config.active_profile || DEFAULT_PROFILE_NAME;
|
|
326
|
+
if (!config.profiles[active]) {
|
|
327
|
+
throw new Error(`Cannot activate missing profile '${active}'.`);
|
|
328
|
+
}
|
|
329
|
+
const path = getModaPaths(options).profileConfigPath;
|
|
330
|
+
writeSecretJsonFile(path, {
|
|
331
|
+
version: PROFILE_CONFIG_VERSION,
|
|
332
|
+
active_profile: active,
|
|
333
|
+
profiles: config.profiles,
|
|
334
|
+
migrations: inspectProfileMigrationState(options)
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
function getActiveProfile(options = {}) {
|
|
338
|
+
const config = loadProfileConfig(options);
|
|
339
|
+
const envProfile = optionalString(getEnv(options).MODA_PROFILE);
|
|
340
|
+
const requested = options.profile || envProfile || config.active_profile;
|
|
341
|
+
const profile = config.profiles[requested];
|
|
342
|
+
if (!profile) {
|
|
343
|
+
throw new Error(`Moda profile '${requested}' does not exist.`);
|
|
344
|
+
}
|
|
345
|
+
return profile;
|
|
346
|
+
}
|
|
347
|
+
function setActiveProfile(name, options = {}) {
|
|
348
|
+
assertValidProfileName(name);
|
|
349
|
+
const config = loadProfileConfig(options);
|
|
350
|
+
if (!config.profiles[name]) {
|
|
351
|
+
throw new Error(`Moda profile '${name}' does not exist.`);
|
|
352
|
+
}
|
|
353
|
+
const next = { ...config, active_profile: name };
|
|
354
|
+
saveProfileConfig(next, options);
|
|
355
|
+
return next;
|
|
356
|
+
}
|
|
357
|
+
function upsertProfile(profile, options = {}) {
|
|
358
|
+
assertValidProfileName(profile.name);
|
|
359
|
+
const config = loadProfileConfig(options);
|
|
360
|
+
const next = {
|
|
361
|
+
...config,
|
|
362
|
+
profiles: {
|
|
363
|
+
...config.profiles,
|
|
364
|
+
[profile.name]: {
|
|
365
|
+
...profile,
|
|
366
|
+
metadata: {
|
|
367
|
+
...profile.metadata,
|
|
368
|
+
updated_at: (options.now ?? new Date).toISOString()
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
saveProfileConfig(next, options);
|
|
374
|
+
return next;
|
|
375
|
+
}
|
|
376
|
+
function syncLegacyConfigToProfile(legacy, options = {}) {
|
|
377
|
+
const active = getActiveProfile(options);
|
|
378
|
+
return upsertProfile({
|
|
379
|
+
...active,
|
|
380
|
+
ingest_url: legacy.ingest_url ?? active.ingest_url,
|
|
381
|
+
tenant_id: legacy.tenant_id ?? active.tenant_id,
|
|
382
|
+
api_key_ref: legacy.api_key ? "legacy:config.api_key" : active.api_key_ref,
|
|
383
|
+
metadata: {
|
|
384
|
+
...active.metadata,
|
|
385
|
+
source: "migration",
|
|
386
|
+
legacy_config_path: getModaPaths(options).legacyConfigPath
|
|
387
|
+
}
|
|
388
|
+
}, options);
|
|
389
|
+
}
|
|
390
|
+
function clearStoredApiKeyCredentials(options = {}) {
|
|
391
|
+
let cleared = false;
|
|
392
|
+
const paths = getModaPaths(options);
|
|
393
|
+
const legacy = loadLegacyCliConfig(options);
|
|
394
|
+
if (legacy.api_key || legacy.tenant_id) {
|
|
395
|
+
writeSecretJsonFile(paths.legacyConfigPath, legacy.ingest_url ? { ingest_url: legacy.ingest_url } : {});
|
|
396
|
+
cleared = true;
|
|
397
|
+
}
|
|
398
|
+
const config = loadProfileConfig(options);
|
|
399
|
+
const profileName = options.profile || optionalString(getEnv(options).MODA_PROFILE) || config.active_profile;
|
|
400
|
+
const profile = config.profiles[profileName];
|
|
401
|
+
if (profile && (profile.api_key_ref || profile.tenant_id)) {
|
|
402
|
+
if (profile.api_key_ref?.startsWith("file:")) {
|
|
403
|
+
const secretPath = resolveFileSecretPath(profile.api_key_ref, options);
|
|
404
|
+
if (secretPath)
|
|
405
|
+
rmSync(secretPath, { force: true });
|
|
406
|
+
}
|
|
407
|
+
const { api_key_ref: _key, tenant_id: _tenant, ...rest } = profile;
|
|
408
|
+
saveProfileConfig({ ...config, profiles: { ...config.profiles, [profileName]: rest } }, options);
|
|
409
|
+
cleared = true;
|
|
410
|
+
}
|
|
411
|
+
return cleared;
|
|
412
|
+
}
|
|
413
|
+
function resolveFileSecretPath(reference, options) {
|
|
414
|
+
if (!reference.startsWith("file:"))
|
|
415
|
+
return;
|
|
416
|
+
const rawPath = reference.slice("file:".length);
|
|
417
|
+
if (!rawPath)
|
|
418
|
+
return;
|
|
419
|
+
const paths = getModaPaths(options);
|
|
420
|
+
const secretPath = isAbsolute(rawPath) ? normalize(rawPath) : normalize(join(paths.dir, rawPath));
|
|
421
|
+
const rel = relative(paths.dir, secretPath);
|
|
422
|
+
if (rel.startsWith("..") || isAbsolute(rel)) {
|
|
423
|
+
throw new Error(`Secret reference escapes the Moda directory: ${reference}`);
|
|
424
|
+
}
|
|
425
|
+
return secretPath;
|
|
426
|
+
}
|
|
427
|
+
function secretFilePathFromRef(reference, options = {}) {
|
|
428
|
+
return resolveFileSecretPath(reference, options);
|
|
429
|
+
}
|
|
430
|
+
function resolveSecretReference(reference, options = {}) {
|
|
431
|
+
if (!reference)
|
|
432
|
+
return;
|
|
433
|
+
const env = getEnv(options);
|
|
434
|
+
if (reference.startsWith("env:")) {
|
|
435
|
+
return env[reference.slice("env:".length)] || undefined;
|
|
436
|
+
}
|
|
437
|
+
if (reference === "legacy:config.api_key") {
|
|
438
|
+
return loadLegacyCliConfig(options).api_key;
|
|
439
|
+
}
|
|
440
|
+
if (reference === "legacy:auth.access_token") {
|
|
441
|
+
const parsed2 = safeReadRecord(getModaPaths(options).legacyAuthPath);
|
|
442
|
+
return optionalString(parsed2?.access_token);
|
|
443
|
+
}
|
|
444
|
+
const filePath = resolveFileSecretPath(reference, options);
|
|
445
|
+
if (!filePath)
|
|
446
|
+
return;
|
|
447
|
+
const parsed = safeReadRecord(filePath);
|
|
448
|
+
if (parsed) {
|
|
449
|
+
return optionalString(parsed.value) ?? optionalString(parsed.api_key) ?? optionalString(parsed.access_token);
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const raw = readFileSync(filePath, "utf8").trim();
|
|
453
|
+
return raw || undefined;
|
|
454
|
+
} catch {
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
function resolveApiKeyFromActiveProfile(options = {}) {
|
|
459
|
+
try {
|
|
460
|
+
return resolveSecretReference(getActiveProfile(options).api_key_ref, options);
|
|
461
|
+
} catch {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
function normalizeModaBaseUrl(url) {
|
|
466
|
+
try {
|
|
467
|
+
const parsed = new URL(url);
|
|
468
|
+
if (parsed.hostname === "www.moda.dev") {
|
|
469
|
+
parsed.hostname = "moda.dev";
|
|
470
|
+
return parsed.toString().replace(/\/$/, "");
|
|
471
|
+
}
|
|
472
|
+
} catch {}
|
|
473
|
+
return url.replace(/\/$/, "");
|
|
474
|
+
}
|
|
475
|
+
function resolveBaseUrl(options = {}) {
|
|
476
|
+
const environment = activeProfileEnvironment(options);
|
|
477
|
+
const envBase = optionalString(getEnv(options).MODA_BASE_URL);
|
|
478
|
+
if (envBase) {
|
|
479
|
+
return enforceNonLoopbackBaseUrl(normalizeModaBaseUrl(envBase), environment);
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
return enforceNonLoopbackBaseUrl(normalizeModaBaseUrl(getActiveProfile(options).base_url || PROD_BASE_URL), environment);
|
|
483
|
+
} catch {
|
|
484
|
+
return PROD_BASE_URL;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function resolveIngestUrl(options = {}) {
|
|
488
|
+
const environment = activeProfileEnvironment(options);
|
|
489
|
+
const envIngest = optionalString(getEnv(options).MODA_INGEST_URL);
|
|
490
|
+
if (envIngest) {
|
|
491
|
+
return enforceNonLoopbackIngestUrl(envIngest, environment);
|
|
492
|
+
}
|
|
493
|
+
try {
|
|
494
|
+
const profileIngest = getActiveProfile(options).ingest_url;
|
|
495
|
+
if (profileIngest) {
|
|
496
|
+
return enforceNonLoopbackIngestUrl(profileIngest, environment);
|
|
497
|
+
}
|
|
498
|
+
} catch {}
|
|
499
|
+
const legacyIngest = optionalString(loadLegacyCliConfig(options).ingest_url);
|
|
500
|
+
if (legacyIngest) {
|
|
501
|
+
return enforceNonLoopbackIngestUrl(legacyIngest, environment);
|
|
502
|
+
}
|
|
503
|
+
return defaultIngestUrlForEnvironment(environment);
|
|
504
|
+
}
|
|
505
|
+
function resolveTenantIdFromActiveProfile(options = {}) {
|
|
506
|
+
try {
|
|
507
|
+
return getActiveProfile(options).tenant_id;
|
|
508
|
+
} catch {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function modeString(mode) {
|
|
513
|
+
return `0${(mode & 511).toString(8).padStart(3, "0")}`;
|
|
514
|
+
}
|
|
515
|
+
function ensureSecretFilePermissions(path, repair = true) {
|
|
516
|
+
if (!existsSync(path)) {
|
|
517
|
+
return { path, exists: false, secure: false, repaired: false };
|
|
518
|
+
}
|
|
519
|
+
const before = statSync(path);
|
|
520
|
+
if (!before.isFile()) {
|
|
521
|
+
return {
|
|
522
|
+
path,
|
|
523
|
+
exists: true,
|
|
524
|
+
secure: false,
|
|
525
|
+
repaired: false,
|
|
526
|
+
mode: modeString(before.mode),
|
|
527
|
+
reason: "not_file"
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
if ((before.mode & 63) !== 0 && repair) {
|
|
531
|
+
try {
|
|
532
|
+
chmodSync(path, 384);
|
|
533
|
+
const after = statSync(path);
|
|
534
|
+
return {
|
|
535
|
+
path,
|
|
536
|
+
exists: true,
|
|
537
|
+
secure: (after.mode & 63) === 0,
|
|
538
|
+
repaired: true,
|
|
539
|
+
mode: modeString(after.mode)
|
|
540
|
+
};
|
|
541
|
+
} catch {
|
|
542
|
+
return {
|
|
543
|
+
path,
|
|
544
|
+
exists: true,
|
|
545
|
+
secure: false,
|
|
546
|
+
repaired: false,
|
|
547
|
+
mode: modeString(before.mode),
|
|
548
|
+
reason: "chmod_failed"
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return {
|
|
553
|
+
path,
|
|
554
|
+
exists: true,
|
|
555
|
+
secure: (before.mode & 63) === 0,
|
|
556
|
+
repaired: false,
|
|
557
|
+
mode: modeString(before.mode)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function writeSecretJsonFile(path, value) {
|
|
561
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
562
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}
|
|
563
|
+
`, { mode: 384 });
|
|
564
|
+
try {
|
|
565
|
+
chmodSync(path, 384);
|
|
566
|
+
} catch {}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// src/config.ts
|
|
570
|
+
var pkg = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf8"));
|
|
571
|
+
var VERSION = pkg.version;
|
|
572
|
+
var MODA_BASE_URL = resolveBaseUrl();
|
|
573
|
+
function loadCliConfig(options = {}) {
|
|
574
|
+
return loadLegacyCliConfig(options);
|
|
575
|
+
}
|
|
576
|
+
function saveCliConfig(update, options = {}) {
|
|
577
|
+
const merged = writeLegacyCliConfig(update, options);
|
|
578
|
+
try {
|
|
579
|
+
syncLegacyConfigToProfile(merged, options);
|
|
580
|
+
} catch {}
|
|
581
|
+
}
|
|
582
|
+
function resolveApiKey(options = {}) {
|
|
583
|
+
const env = options.env ?? process.env;
|
|
584
|
+
return env.MODA_API_KEY || resolveApiKeyFromActiveProfile(options) || loadCliConfig(options).api_key || undefined;
|
|
585
|
+
}
|
|
586
|
+
var SIGNED_API_KEY_PREFIX = "moda_sk_";
|
|
587
|
+
var SIGNED_API_KEY_VERSION = "v1";
|
|
588
|
+
function embeddedTenantIdFromApiKey(key) {
|
|
589
|
+
if (!key.startsWith(SIGNED_API_KEY_PREFIX))
|
|
590
|
+
return;
|
|
591
|
+
try {
|
|
592
|
+
const decoded = Buffer.from(key.slice(SIGNED_API_KEY_PREFIX.length), "base64url").toString("utf8");
|
|
593
|
+
const parts = decoded.split(":");
|
|
594
|
+
if (parts.length !== 5 || parts[0] !== SIGNED_API_KEY_VERSION)
|
|
595
|
+
return;
|
|
596
|
+
return parts[2] || undefined;
|
|
597
|
+
} catch {
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function resolveIngestUrl2(options = {}) {
|
|
602
|
+
return resolveIngestUrl(options);
|
|
603
|
+
}
|
|
604
|
+
function resolveTenantId(options = {}) {
|
|
605
|
+
const env = options.env ?? process.env;
|
|
606
|
+
return env.MODA_TENANT_ID || resolveTenantIdFromActiveProfile(options) || loadCliConfig(options).tenant_id || undefined;
|
|
607
|
+
}
|
|
608
|
+
var MODA_API_KEY = process.env.MODA_API_KEY;
|
|
609
|
+
var MODA_INGEST_URL = process.env.MODA_INGEST_URL || DEFAULT_INGEST_URL;
|
|
610
|
+
function resolveModaBaseUrl(options = {}) {
|
|
611
|
+
return resolveBaseUrl(options) || PROD_BASE_URL;
|
|
612
|
+
}
|
|
613
|
+
function validateConfig() {
|
|
614
|
+
if (!resolveApiKey()) {
|
|
615
|
+
throw new CliAuthError('No Moda API key found. Set MODA_API_KEY or run "moda init".', {
|
|
616
|
+
reason: "no_stored_credentials",
|
|
617
|
+
login_command: ["moda", "init"]
|
|
618
|
+
}, "Get your API key from https://moda.dev/settings");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/output.ts
|
|
623
|
+
import { randomUUID } from "node:crypto";
|
|
624
|
+
|
|
625
|
+
// ../../packages/harness-snapshot/src/policy.ts
|
|
626
|
+
var MAX_SCANNED_FILE_BYTES = 512 * 1024;
|
|
627
|
+
var DEFAULT_MAX_INVENTORY_FILES = 12000;
|
|
628
|
+
var DEFAULT_MAX_INVENTORY_TEXT_BYTES = 48 * 1024 * 1024;
|
|
629
|
+
var MAX_SNAPSHOT_BODY_BYTES = 4500000;
|
|
630
|
+
var MAX_RECORDED_SKIPS = 1000;
|
|
631
|
+
var SKIPPED_DIRS = new Set([
|
|
632
|
+
".angular",
|
|
633
|
+
".cache",
|
|
634
|
+
".expo",
|
|
635
|
+
".git",
|
|
636
|
+
".hg",
|
|
637
|
+
".idea",
|
|
638
|
+
".svn",
|
|
639
|
+
".next",
|
|
640
|
+
".nuxt",
|
|
641
|
+
".output",
|
|
642
|
+
".parcel-cache",
|
|
643
|
+
".pytest_cache",
|
|
644
|
+
".ruff_cache",
|
|
645
|
+
".serverless",
|
|
646
|
+
".sst",
|
|
647
|
+
".turbo",
|
|
648
|
+
".wrangler",
|
|
649
|
+
".wrangler-shared",
|
|
650
|
+
".moda",
|
|
651
|
+
".moda-test",
|
|
652
|
+
".moda-test-home",
|
|
653
|
+
".moda-cli-workspaces",
|
|
654
|
+
".moda-cli-benchmarks",
|
|
655
|
+
"node_modules",
|
|
656
|
+
"dist",
|
|
657
|
+
"build",
|
|
658
|
+
"coverage",
|
|
659
|
+
"out",
|
|
660
|
+
"target",
|
|
661
|
+
"logs",
|
|
662
|
+
"tmp",
|
|
663
|
+
"temp",
|
|
664
|
+
"__pycache__",
|
|
665
|
+
".venv",
|
|
666
|
+
"venv"
|
|
667
|
+
]);
|
|
668
|
+
var SKIPPED_PATH_PREFIXES = [
|
|
669
|
+
".claude/worktrees",
|
|
670
|
+
".codex/environments"
|
|
671
|
+
];
|
|
672
|
+
function posixNormalize(path) {
|
|
673
|
+
const absolute = path.startsWith("/");
|
|
674
|
+
const parts = path.split("/").filter((part) => part !== "" && part !== ".");
|
|
675
|
+
const normalized = parts.join("/");
|
|
676
|
+
return absolute ? `/${normalized}` : normalized;
|
|
677
|
+
}
|
|
678
|
+
function basename(path) {
|
|
679
|
+
const normalized = posixNormalize(path);
|
|
680
|
+
const index = normalized.lastIndexOf("/");
|
|
681
|
+
return index === -1 ? normalized : normalized.slice(index + 1);
|
|
682
|
+
}
|
|
683
|
+
function extname(path) {
|
|
684
|
+
const name = basename(path);
|
|
685
|
+
const index = name.lastIndexOf(".");
|
|
686
|
+
return index <= 0 ? "" : name.slice(index);
|
|
687
|
+
}
|
|
688
|
+
function isSkippedDir(path) {
|
|
689
|
+
return SKIPPED_DIRS.has(basename(path));
|
|
690
|
+
}
|
|
691
|
+
function isSkippedPathPrefix(path) {
|
|
692
|
+
const normalized = posixNormalize(path);
|
|
693
|
+
return SKIPPED_PATH_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
|
|
694
|
+
}
|
|
695
|
+
function isSourceFile(path) {
|
|
696
|
+
return /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i.test(path);
|
|
697
|
+
}
|
|
698
|
+
function isPackageOrLockfile(path) {
|
|
699
|
+
return path === "package.json" || path === "pyproject.toml" || path === "bun.lock" || path === "bun.lockb" || path === "pnpm-lock.yaml" || path === "yarn.lock";
|
|
700
|
+
}
|
|
701
|
+
function isPromptLikePath(path) {
|
|
702
|
+
return /(^|\/)prompts?\//i.test(path) && /\.(prompt\.)?(md|json|ya?ml)$/i.test(path);
|
|
703
|
+
}
|
|
704
|
+
function isSkillLikePath(path) {
|
|
705
|
+
return /(^|\/)(\.claude\/skills|\.cursor\/rules|skills?)\//i.test(path) && /\.(md|mdc)$/i.test(path);
|
|
706
|
+
}
|
|
707
|
+
function isEvalLikePath(path) {
|
|
708
|
+
return /(^|\/)(evals?|tests?|__tests__)\//i.test(path) && /\.(ts|tsx|js|mjs|py|json|ya?ml)$/i.test(path);
|
|
709
|
+
}
|
|
710
|
+
function isDeploymentLikePath(path) {
|
|
711
|
+
return /(^|\/)(wrangler\.toml|vercel\.json|render\.ya?ml|railway\.json|Dockerfile|docker-compose\.ya?ml)$/i.test(path) || /^\.github\/workflows\/[^/]+\.ya?ml$/i.test(path);
|
|
712
|
+
}
|
|
713
|
+
function isDeveloperAgentConfigPath(path) {
|
|
714
|
+
return path === ".claude/settings.json" || path === ".codex/hooks.json" || path === ".cursor/hooks.json";
|
|
715
|
+
}
|
|
716
|
+
function isReadmeOrAgents(path) {
|
|
717
|
+
const name = basename(path);
|
|
718
|
+
return name === "README.md" || name === "AGENTS.md";
|
|
719
|
+
}
|
|
720
|
+
function shouldInventoryFile(path) {
|
|
721
|
+
const normalized = posixNormalize(path);
|
|
722
|
+
const name = basename(normalized);
|
|
723
|
+
const extension = extname(normalized).toLowerCase();
|
|
724
|
+
if (isSourceFile(normalized))
|
|
725
|
+
return true;
|
|
726
|
+
if (isPackageOrLockfile(normalized))
|
|
727
|
+
return true;
|
|
728
|
+
if (isPromptLikePath(normalized))
|
|
729
|
+
return true;
|
|
730
|
+
if (isSkillLikePath(normalized))
|
|
731
|
+
return true;
|
|
732
|
+
if (isEvalLikePath(normalized))
|
|
733
|
+
return true;
|
|
734
|
+
if (isDeploymentLikePath(normalized))
|
|
735
|
+
return true;
|
|
736
|
+
if (isDeveloperAgentConfigPath(normalized))
|
|
737
|
+
return true;
|
|
738
|
+
if (isReadmeOrAgents(normalized))
|
|
739
|
+
return true;
|
|
740
|
+
return extension === ".toml" && (name === "pyproject.toml" || name === "wrangler.toml");
|
|
741
|
+
}
|
|
742
|
+
function isTextFile(path) {
|
|
743
|
+
const extension = extname(path).toLowerCase();
|
|
744
|
+
return [
|
|
745
|
+
"",
|
|
746
|
+
".cjs",
|
|
747
|
+
".js",
|
|
748
|
+
".json",
|
|
749
|
+
".jsx",
|
|
750
|
+
".md",
|
|
751
|
+
".mdc",
|
|
752
|
+
".mjs",
|
|
753
|
+
".py",
|
|
754
|
+
".toml",
|
|
755
|
+
".ts",
|
|
756
|
+
".tsx",
|
|
757
|
+
".txt",
|
|
758
|
+
".yaml",
|
|
759
|
+
".yml"
|
|
760
|
+
].includes(extension);
|
|
761
|
+
}
|
|
762
|
+
function isSecretishPath(path) {
|
|
763
|
+
const name = basename(path).toLowerCase();
|
|
764
|
+
return name === ".env" || name.startsWith(".env.") || name.includes("secret") || name.includes("private-key") || name.endsWith(".pem");
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// ../../packages/harness-snapshot/src/redact.ts
|
|
768
|
+
var REDACTED = "[REDACTED]";
|
|
769
|
+
var MAX_DEPTH = 256;
|
|
770
|
+
var SECRET_KEYS = new Set([
|
|
771
|
+
"api_key",
|
|
772
|
+
"apikey",
|
|
773
|
+
"token",
|
|
774
|
+
"access_token",
|
|
775
|
+
"authorization",
|
|
776
|
+
"secret",
|
|
777
|
+
"password",
|
|
778
|
+
"cli_token"
|
|
779
|
+
]);
|
|
780
|
+
var SECRET_PATTERNS = [
|
|
781
|
+
{ pattern: /\bmoda_sk_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
|
|
782
|
+
{ pattern: /\bmoda_ct_[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
|
|
783
|
+
{ pattern: /\bBearer\s+([A-Za-z0-9._~+/-]+=*)\b/gi, prefixLength: 8 },
|
|
784
|
+
{ pattern: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, prefixLength: 8 },
|
|
785
|
+
{ pattern: /\bsk-ant-api\d{2}-[A-Za-z0-9_-]{20,}\b/g, prefixLength: 12 },
|
|
786
|
+
{ pattern: /\bsk-(?:proj-|svcacct-)?[A-Za-z0-9_-]{20,}\b/g, prefixLength: 8 },
|
|
787
|
+
{ pattern: /\bAKIA[A-Z0-9]{16}\b/g, prefixLength: 8 },
|
|
788
|
+
{ pattern: /\bAIza[A-Za-z0-9_-]{35}\b/g, prefixLength: 8 },
|
|
789
|
+
{ pattern: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/g, prefixLength: 8 },
|
|
790
|
+
{ pattern: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/g, prefixLength: 8 }
|
|
791
|
+
];
|
|
792
|
+
function redactDeep(value) {
|
|
793
|
+
return redactValue(value, 0, new WeakSet);
|
|
794
|
+
}
|
|
795
|
+
function redactValue(value, depth, seen) {
|
|
796
|
+
if (typeof value === "string")
|
|
797
|
+
return redactString(value);
|
|
798
|
+
if (value === null || typeof value !== "object")
|
|
799
|
+
return value;
|
|
800
|
+
if (depth >= MAX_DEPTH)
|
|
801
|
+
return "[MaxDepth]";
|
|
802
|
+
if (seen.has(value))
|
|
803
|
+
return "[Circular]";
|
|
804
|
+
seen.add(value);
|
|
805
|
+
if (Array.isArray(value)) {
|
|
806
|
+
try {
|
|
807
|
+
return value.map((item) => redactValue(item, depth + 1, seen));
|
|
808
|
+
} finally {
|
|
809
|
+
seen.delete(value);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
const out = {};
|
|
813
|
+
try {
|
|
814
|
+
for (const [key, item] of Object.entries(value)) {
|
|
815
|
+
out[key] = shouldRedactKey(key) ? redactSecretValue(item) : redactValue(item, depth + 1, seen);
|
|
816
|
+
}
|
|
817
|
+
} finally {
|
|
818
|
+
seen.delete(value);
|
|
819
|
+
}
|
|
820
|
+
return out;
|
|
821
|
+
}
|
|
822
|
+
function shouldRedactKey(key) {
|
|
823
|
+
return SECRET_KEYS.has(key.replace(/[-\s]/g, "_").toLowerCase());
|
|
824
|
+
}
|
|
825
|
+
function redactSecretValue(value) {
|
|
826
|
+
if (typeof value !== "string")
|
|
827
|
+
return REDACTED;
|
|
828
|
+
const redacted = redactString(value);
|
|
829
|
+
if (redacted !== value)
|
|
830
|
+
return redacted;
|
|
831
|
+
if (/^(configured|missing|not_configured|none|present|absent)$/i.test(value))
|
|
832
|
+
return value;
|
|
833
|
+
return REDACTED;
|
|
834
|
+
}
|
|
835
|
+
function redactStringWithCount(input) {
|
|
836
|
+
let text = input;
|
|
837
|
+
let redactionCount = 0;
|
|
838
|
+
for (const { pattern, prefixLength } of SECRET_PATTERNS) {
|
|
839
|
+
text = text.replace(pattern, (match) => {
|
|
840
|
+
redactionCount += 1;
|
|
841
|
+
if (/^Bearer\s+/i.test(match)) {
|
|
842
|
+
const token = match.replace(/^Bearer\s+/i, "");
|
|
843
|
+
return `Bearer ${token.slice(0, prefixLength)}...[REDACTED]`;
|
|
844
|
+
}
|
|
845
|
+
return `${match.slice(0, prefixLength)}...[REDACTED]`;
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
return { text, redactionCount };
|
|
849
|
+
}
|
|
850
|
+
function redactString(input) {
|
|
851
|
+
return redactStringWithCount(input).text;
|
|
852
|
+
}
|
|
853
|
+
function redactSnapshotText(text) {
|
|
854
|
+
const parts = text.split(/(\r\n|\r|\n)/);
|
|
855
|
+
let redactionCount = 0;
|
|
856
|
+
for (let index = 0;index < parts.length; index += 2) {
|
|
857
|
+
const redacted = redactStringWithCount(parts[index] ?? "");
|
|
858
|
+
parts[index] = redacted.text;
|
|
859
|
+
redactionCount += redacted.redactionCount;
|
|
860
|
+
}
|
|
861
|
+
return { text: parts.join(""), redactionCount };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// ../../packages/harness-snapshot/src/pack.ts
|
|
865
|
+
function utf8ByteLength(value) {
|
|
866
|
+
let bytes = 0;
|
|
867
|
+
for (let index = 0;index < value.length; index += 1) {
|
|
868
|
+
const code = value.charCodeAt(index);
|
|
869
|
+
if (code <= 127) {
|
|
870
|
+
bytes += 1;
|
|
871
|
+
} else if (code <= 2047) {
|
|
872
|
+
bytes += 2;
|
|
873
|
+
} else if (code >= 55296 && code <= 56319) {
|
|
874
|
+
const next = value.charCodeAt(index + 1);
|
|
875
|
+
if (next >= 56320 && next <= 57343) {
|
|
876
|
+
bytes += 4;
|
|
877
|
+
index += 1;
|
|
878
|
+
} else {
|
|
879
|
+
bytes += 3;
|
|
880
|
+
}
|
|
881
|
+
} else {
|
|
882
|
+
bytes += 3;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return bytes;
|
|
886
|
+
}
|
|
887
|
+
function isValidRelativePath(path) {
|
|
888
|
+
if (path.length === 0 || path.startsWith("/") || path.includes("\\"))
|
|
889
|
+
return false;
|
|
890
|
+
return !path.split("/").some((part) => part === "..");
|
|
891
|
+
}
|
|
892
|
+
function packSnapshot(inputs, opts) {
|
|
893
|
+
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_INVENTORY_FILES;
|
|
894
|
+
const maxTextBytes = opts.maxTextBytes ?? DEFAULT_MAX_INVENTORY_TEXT_BYTES;
|
|
895
|
+
const maxSnapshotBytes = opts.maxSnapshotBytes ?? MAX_SNAPSHOT_BODY_BYTES;
|
|
896
|
+
const files = [];
|
|
897
|
+
const skips = [];
|
|
898
|
+
let inventoryTextBytes = 0;
|
|
899
|
+
let serializedBytes = 0;
|
|
900
|
+
let byteCount = 0;
|
|
901
|
+
let truncated = false;
|
|
902
|
+
const recordSkip = (path, reason) => {
|
|
903
|
+
if (skips.length < MAX_RECORDED_SKIPS)
|
|
904
|
+
skips.push({ path, reason });
|
|
905
|
+
};
|
|
906
|
+
const sorted = [...inputs].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
907
|
+
for (const input of sorted) {
|
|
908
|
+
if (!isValidRelativePath(input.path)) {
|
|
909
|
+
recordSkip(input.path, "invalid relative path");
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
const path = posixNormalize(input.path);
|
|
913
|
+
if (input.isSymlink) {
|
|
914
|
+
recordSkip(path, "symlink skipped for safety");
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
if (isSecretishPath(path)) {
|
|
918
|
+
recordSkip(path, "secret-bearing path");
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
if (!shouldInventoryFile(path)) {
|
|
922
|
+
recordSkip(path, "not an inventory file");
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (!isTextFile(path)) {
|
|
926
|
+
recordSkip(path, "non-text file");
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (input.byteSize > MAX_SCANNED_FILE_BYTES) {
|
|
930
|
+
recordSkip(path, "file too large");
|
|
931
|
+
truncated = true;
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (files.length >= maxFiles || inventoryTextBytes + input.byteSize > maxTextBytes) {
|
|
935
|
+
recordSkip(path, "inventory budget");
|
|
936
|
+
truncated = true;
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const text = opts.redact === false ? input.text : redactSnapshotText(input.text).text;
|
|
940
|
+
const file = { path, text };
|
|
941
|
+
const serializedCost = utf8ByteLength(JSON.stringify(file)) + 1;
|
|
942
|
+
if (serializedBytes + serializedCost > maxSnapshotBytes) {
|
|
943
|
+
recordSkip(path, "snapshot budget");
|
|
944
|
+
truncated = true;
|
|
945
|
+
break;
|
|
946
|
+
}
|
|
947
|
+
files.push(file);
|
|
948
|
+
inventoryTextBytes += input.byteSize;
|
|
949
|
+
serializedBytes += serializedCost;
|
|
950
|
+
byteCount += utf8ByteLength(text);
|
|
951
|
+
}
|
|
952
|
+
return {
|
|
953
|
+
snapshot: {
|
|
954
|
+
repo: { name: opts.repoName, root: "." },
|
|
955
|
+
files,
|
|
956
|
+
truncated,
|
|
957
|
+
fileCount: files.length,
|
|
958
|
+
byteCount,
|
|
959
|
+
...opts.gitCommit ? { gitCommit: opts.gitCommit } : {}
|
|
960
|
+
},
|
|
961
|
+
skips
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
// src/output.ts
|
|
965
|
+
var AGENT_SCHEMA_VERSION = "moda.agent.v1";
|
|
966
|
+
var AGENT_EVENT_SCHEMA_VERSION = "moda.agent_event.v1";
|
|
967
|
+
var FEEDBACK_TIP = 'Help us improve! If a result in this response looks wrong or is missing data, flag it: moda feedback "<describe what looked wrong>" [--category=...] [--conversation-id=... | --cluster-id=... | --tool-name=...]. Only submit genuine observations.';
|
|
968
|
+
var EXIT_OK = 0;
|
|
969
|
+
var EXIT_ERROR = 1;
|
|
970
|
+
var EXIT_DEGRADED = 3;
|
|
971
|
+
var EXIT_AUTH_REQUIRED = 4;
|
|
972
|
+
var EXIT_INPUT_REQUIRED = 5;
|
|
973
|
+
var EXIT_CANCELLED = 130;
|
|
974
|
+
var AGENT_ERROR_CODES = [
|
|
975
|
+
"input_error",
|
|
976
|
+
"api_error",
|
|
977
|
+
"network_error",
|
|
978
|
+
"auth_required",
|
|
979
|
+
"input_required",
|
|
980
|
+
"cancelled",
|
|
981
|
+
"command_error",
|
|
982
|
+
"access_denied",
|
|
983
|
+
"unknown_error",
|
|
984
|
+
"cloud_unavailable"
|
|
985
|
+
];
|
|
986
|
+
var FALLBACK_RUN_ID = randomUUID();
|
|
987
|
+
var fallbackSeq = 0;
|
|
988
|
+
var stdoutClosed = false;
|
|
989
|
+
var stderrClosed = false;
|
|
990
|
+
process.stdout.on("error", (error) => {
|
|
991
|
+
if (error.code === "EPIPE") {
|
|
992
|
+
stdoutClosed = true;
|
|
993
|
+
process.exitCode = EXIT_CANCELLED;
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
throw error;
|
|
997
|
+
});
|
|
998
|
+
process.stderr.on("error", (error) => {
|
|
999
|
+
if (error.code === "EPIPE") {
|
|
1000
|
+
stderrClosed = true;
|
|
1001
|
+
process.exitCode = EXIT_CANCELLED;
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
throw error;
|
|
1005
|
+
});
|
|
1006
|
+
function wasOutputCancelled() {
|
|
1007
|
+
return stdoutClosed || stderrClosed;
|
|
1008
|
+
}
|
|
1009
|
+
function safeWriteStdout(text) {
|
|
1010
|
+
if (stdoutClosed)
|
|
1011
|
+
return false;
|
|
1012
|
+
try {
|
|
1013
|
+
if (false) {} else {
|
|
1014
|
+
process.stdout.write(text);
|
|
1015
|
+
}
|
|
1016
|
+
return true;
|
|
1017
|
+
} catch (error) {
|
|
1018
|
+
if (error.code === "EPIPE") {
|
|
1019
|
+
stdoutClosed = true;
|
|
1020
|
+
process.exitCode = EXIT_CANCELLED;
|
|
1021
|
+
return false;
|
|
1022
|
+
}
|
|
1023
|
+
throw error;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
function safeWriteStderr(text) {
|
|
1027
|
+
if (stderrClosed)
|
|
1028
|
+
return false;
|
|
1029
|
+
try {
|
|
1030
|
+
if (false) {} else {
|
|
1031
|
+
process.stderr.write(text);
|
|
1032
|
+
}
|
|
1033
|
+
return true;
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (error.code === "EPIPE") {
|
|
1036
|
+
stderrClosed = true;
|
|
1037
|
+
process.exitCode = EXIT_CANCELLED;
|
|
1038
|
+
return false;
|
|
1039
|
+
}
|
|
1040
|
+
throw error;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
function explicitModeFromFlags(flags) {
|
|
1044
|
+
if (flags.agent === "true" && (flags.stream === "true" || flags.format === "ndjson"))
|
|
1045
|
+
return "agent-stream";
|
|
1046
|
+
if (flags.agent === "true")
|
|
1047
|
+
return "agent";
|
|
1048
|
+
if (flags.json === "true")
|
|
1049
|
+
return "json";
|
|
1050
|
+
if (flags.human === "true" || flags.prose === "true")
|
|
1051
|
+
return "human";
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
function outputModeFromFlags(flags) {
|
|
1055
|
+
return explicitModeFromFlags(flags) ?? "human";
|
|
1056
|
+
}
|
|
1057
|
+
function outputModeFromEnv(env) {
|
|
1058
|
+
const fmt = (env.MODA_FORMAT ?? "").trim().toLowerCase();
|
|
1059
|
+
if (fmt === "human" || fmt === "json" || fmt === "agent")
|
|
1060
|
+
return fmt;
|
|
1061
|
+
if (fmt === "agent-stream" || fmt === "ndjson")
|
|
1062
|
+
return "agent-stream";
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
function truthyEnv(value) {
|
|
1066
|
+
return value != null && value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
1067
|
+
}
|
|
1068
|
+
function isAgentEnvironment(env) {
|
|
1069
|
+
return truthyEnv(env.CLAUDECODE) || truthyEnv(env.CURSOR) || truthyEnv(env.CI) || truthyEnv(env.MODA_AGENT);
|
|
1070
|
+
}
|
|
1071
|
+
function resolveOutputMode(flags, opts = {}) {
|
|
1072
|
+
const explicit = explicitModeFromFlags(flags);
|
|
1073
|
+
if (explicit)
|
|
1074
|
+
return explicit;
|
|
1075
|
+
const env = opts.env ?? process.env;
|
|
1076
|
+
const fromEnv = outputModeFromEnv(env);
|
|
1077
|
+
if (fromEnv)
|
|
1078
|
+
return fromEnv;
|
|
1079
|
+
if (isAgentEnvironment(env))
|
|
1080
|
+
return "agent";
|
|
1081
|
+
const isTTY = opts.isTTY ?? Boolean(process.stdout.isTTY);
|
|
1082
|
+
if (!isTTY)
|
|
1083
|
+
return "agent";
|
|
1084
|
+
return "human";
|
|
1085
|
+
}
|
|
1086
|
+
function createCliOutput(options) {
|
|
1087
|
+
const state = {
|
|
1088
|
+
runId: options.runId ?? randomUUID(),
|
|
1089
|
+
seq: 0,
|
|
1090
|
+
started: false,
|
|
1091
|
+
terminal: false,
|
|
1092
|
+
startedAt: Date.now(),
|
|
1093
|
+
lastEventAt: Date.now()
|
|
1094
|
+
};
|
|
1095
|
+
const emitEvent = (event) => {
|
|
1096
|
+
if (options.quiet || options.mode !== "agent-stream")
|
|
1097
|
+
return;
|
|
1098
|
+
if (state.terminal)
|
|
1099
|
+
return;
|
|
1100
|
+
if (event.event === "started" && state.started)
|
|
1101
|
+
return;
|
|
1102
|
+
if (event.event === "started")
|
|
1103
|
+
state.started = true;
|
|
1104
|
+
if (isTerminalEvent(event.event)) {
|
|
1105
|
+
state.terminal = true;
|
|
1106
|
+
clearHeartbeat(state);
|
|
1107
|
+
}
|
|
1108
|
+
state.lastEventAt = Date.now();
|
|
1109
|
+
safeWriteStdout(`${JSON.stringify(createAgentEvent(options.command, event, state))}
|
|
1110
|
+
`);
|
|
1111
|
+
};
|
|
1112
|
+
const ensureStarted = () => {
|
|
1113
|
+
if (options.mode !== "agent-stream" || state.started || options.quiet)
|
|
1114
|
+
return;
|
|
1115
|
+
startHeartbeat(options.command, state);
|
|
1116
|
+
emitEvent({
|
|
1117
|
+
event: "started",
|
|
1118
|
+
phase: options.command,
|
|
1119
|
+
message: `Starting ${options.command}.`,
|
|
1120
|
+
elapsed_ms: 0
|
|
1121
|
+
});
|
|
1122
|
+
};
|
|
1123
|
+
const writeEnvelope = (data, writeOptions = {}) => {
|
|
1124
|
+
return createAgentEnvelope({
|
|
1125
|
+
command: options.command,
|
|
1126
|
+
status: writeOptions.status === "error" ? "error" : writeOptions.status === "degraded" ? "degraded" : "ok",
|
|
1127
|
+
data,
|
|
1128
|
+
meta: {
|
|
1129
|
+
...writeOptions.meta ?? {},
|
|
1130
|
+
run_id: state.runId,
|
|
1131
|
+
...options.command === "feedback" ? {} : { tip: FEEDBACK_TIP }
|
|
1132
|
+
},
|
|
1133
|
+
summary: writeOptions.summary,
|
|
1134
|
+
findings: writeOptions.findings,
|
|
1135
|
+
evidenceRefs: writeOptions.evidenceRefs,
|
|
1136
|
+
recommendedActions: writeOptions.recommendedActions,
|
|
1137
|
+
nextCommands: writeOptions.nextCommands,
|
|
1138
|
+
artifacts: writeOptions.artifacts,
|
|
1139
|
+
warnings: writeOptions.warnings,
|
|
1140
|
+
errors: writeOptions.errors
|
|
1141
|
+
});
|
|
1142
|
+
};
|
|
1143
|
+
const output = {
|
|
1144
|
+
mode: options.mode,
|
|
1145
|
+
quiet: options.quiet === true,
|
|
1146
|
+
writeData(data, writeOptions = {}) {
|
|
1147
|
+
if (options.quiet)
|
|
1148
|
+
return;
|
|
1149
|
+
if (options.mode === "agent" || options.mode === "agent-stream") {
|
|
1150
|
+
if (state.terminal)
|
|
1151
|
+
return;
|
|
1152
|
+
const envelope = writeEnvelope(data, writeOptions);
|
|
1153
|
+
if (options.mode === "agent-stream") {
|
|
1154
|
+
ensureStarted();
|
|
1155
|
+
emitEvent({
|
|
1156
|
+
event: envelope.status === "error" ? "error" : "completed",
|
|
1157
|
+
status: envelope.status,
|
|
1158
|
+
result: envelope,
|
|
1159
|
+
...envelope.errors[0] ? { error: envelope.errors[0] } : {}
|
|
1160
|
+
});
|
|
1161
|
+
} else {
|
|
1162
|
+
state.terminal = true;
|
|
1163
|
+
safeWriteStdout(`${JSON.stringify(envelope)}
|
|
1164
|
+
`);
|
|
1165
|
+
}
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
safeWriteStdout(`${JSON.stringify(redactDeep(data), null, 2)}
|
|
1169
|
+
`);
|
|
1170
|
+
},
|
|
1171
|
+
writeEvent(event) {
|
|
1172
|
+
if (options.quiet)
|
|
1173
|
+
return;
|
|
1174
|
+
if (options.mode !== "agent-stream")
|
|
1175
|
+
return;
|
|
1176
|
+
if (event.event === "started") {
|
|
1177
|
+
startHeartbeat(options.command, state);
|
|
1178
|
+
emitEvent(event);
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
ensureStarted();
|
|
1182
|
+
emitEvent(event);
|
|
1183
|
+
},
|
|
1184
|
+
writeError(error) {
|
|
1185
|
+
if (options.quiet || state.terminal)
|
|
1186
|
+
return;
|
|
1187
|
+
const agentError = errorToAgentError(error);
|
|
1188
|
+
const envelope = createAgentErrorEnvelope(options.command || "unknown", agentError, state.runId);
|
|
1189
|
+
if (options.mode === "agent-stream") {
|
|
1190
|
+
ensureStarted();
|
|
1191
|
+
if (error instanceof CliInputRequiredError) {
|
|
1192
|
+
emitEvent({
|
|
1193
|
+
event: "needs_input",
|
|
1194
|
+
status: "error",
|
|
1195
|
+
message: error.message,
|
|
1196
|
+
error: agentError,
|
|
1197
|
+
input_request: error.input_request,
|
|
1198
|
+
result: envelope
|
|
1199
|
+
});
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (error instanceof CliAuthError) {
|
|
1203
|
+
emitEvent({
|
|
1204
|
+
event: "auth_required",
|
|
1205
|
+
status: "error",
|
|
1206
|
+
message: error.message,
|
|
1207
|
+
error: agentError,
|
|
1208
|
+
auth: error.auth,
|
|
1209
|
+
result: envelope
|
|
1210
|
+
});
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
if (error instanceof CliCancellationError) {
|
|
1214
|
+
emitEvent({
|
|
1215
|
+
event: "cancelled",
|
|
1216
|
+
status: "error",
|
|
1217
|
+
message: error.message,
|
|
1218
|
+
error: agentError,
|
|
1219
|
+
result: envelope
|
|
1220
|
+
});
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
emitEvent({
|
|
1224
|
+
event: "error",
|
|
1225
|
+
status: "error",
|
|
1226
|
+
message: agentError.message,
|
|
1227
|
+
error: agentError,
|
|
1228
|
+
result: envelope
|
|
1229
|
+
});
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (options.mode === "agent" || options.mode === "json") {
|
|
1233
|
+
state.terminal = true;
|
|
1234
|
+
safeWriteStdout(`${JSON.stringify(envelope)}
|
|
1235
|
+
`);
|
|
1236
|
+
}
|
|
1237
|
+
},
|
|
1238
|
+
writeCancelled(reason = "Command cancelled.") {
|
|
1239
|
+
output.writeError(new CliCancellationError(reason));
|
|
1240
|
+
},
|
|
1241
|
+
hasTerminalOutput() {
|
|
1242
|
+
return state.terminal;
|
|
1243
|
+
},
|
|
1244
|
+
close() {
|
|
1245
|
+
clearHeartbeat(state);
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
return output;
|
|
1249
|
+
}
|
|
1250
|
+
function createAgentEnvelope(options) {
|
|
1251
|
+
const safeData = redactDeep(options.data);
|
|
1252
|
+
const bounded = boundForAgent(safeData);
|
|
1253
|
+
const envelope = {
|
|
1254
|
+
schema_version: AGENT_SCHEMA_VERSION,
|
|
1255
|
+
command: options.command,
|
|
1256
|
+
status: options.status,
|
|
1257
|
+
generated_at: new Date().toISOString(),
|
|
1258
|
+
...options.profile ? { profile: options.profile } : {},
|
|
1259
|
+
summary: normalizeSummary(options.summary, options.command, options.status, safeData),
|
|
1260
|
+
findings: options.findings ?? [],
|
|
1261
|
+
evidence_refs: options.evidenceRefs ?? [],
|
|
1262
|
+
recommended_actions: options.recommendedActions ?? [],
|
|
1263
|
+
next_commands: normalizeNextCommands(options.nextCommands ?? []),
|
|
1264
|
+
artifacts: options.artifacts ?? [],
|
|
1265
|
+
warnings: options.warnings ?? inferWarnings(safeData),
|
|
1266
|
+
errors: options.errors ?? [],
|
|
1267
|
+
data: bounded.data,
|
|
1268
|
+
...options.meta ? { meta: options.meta } : {},
|
|
1269
|
+
...bounded.truncation ? { truncation: bounded.truncation } : {}
|
|
1270
|
+
};
|
|
1271
|
+
return redactDeep(envelope);
|
|
1272
|
+
}
|
|
1273
|
+
function createAgentErrorEnvelope(command, error, runId) {
|
|
1274
|
+
return createAgentEnvelope({
|
|
1275
|
+
command,
|
|
1276
|
+
status: "error",
|
|
1277
|
+
data: null,
|
|
1278
|
+
meta: runId ? { run_id: runId } : undefined,
|
|
1279
|
+
summary: {
|
|
1280
|
+
text: error.message,
|
|
1281
|
+
confidence: "high"
|
|
1282
|
+
},
|
|
1283
|
+
errors: [error],
|
|
1284
|
+
nextCommands: (error.suggested_commands ?? []).map((commandText) => ({
|
|
1285
|
+
command: commandText,
|
|
1286
|
+
purpose: "Suggested recovery command.",
|
|
1287
|
+
mutability: commandText.includes(" sync") || commandText.startsWith("moda sync") ? "sync" : "read",
|
|
1288
|
+
requires_approval: commandText.includes(" --yes") || commandText.includes(" sync")
|
|
1289
|
+
}))
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function createAgentEvent(command, event, state) {
|
|
1293
|
+
const stamped = {
|
|
1294
|
+
schema_version: AGENT_EVENT_SCHEMA_VERSION,
|
|
1295
|
+
command,
|
|
1296
|
+
generated_at: new Date().toISOString(),
|
|
1297
|
+
run_id: state?.runId ?? FALLBACK_RUN_ID,
|
|
1298
|
+
seq: state ? ++state.seq : ++fallbackSeq,
|
|
1299
|
+
event_version: event.event_version ?? 1,
|
|
1300
|
+
...event
|
|
1301
|
+
};
|
|
1302
|
+
return redactDeep(stamped);
|
|
1303
|
+
}
|
|
1304
|
+
function errorToAgentError(error) {
|
|
1305
|
+
if (error instanceof CliInputRequiredError) {
|
|
1306
|
+
return {
|
|
1307
|
+
code: "input_required",
|
|
1308
|
+
message: error.message,
|
|
1309
|
+
cause: error.usage,
|
|
1310
|
+
suggested_commands: error.input_request.resume_command ? [error.input_request.resume_command.join(" ")] : undefined,
|
|
1311
|
+
input_request: error.input_request,
|
|
1312
|
+
blocking: true
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
if (error instanceof CliAuthError) {
|
|
1316
|
+
const suggested = [error.auth.login_command.join(" ")];
|
|
1317
|
+
if (error.auth.resume_command)
|
|
1318
|
+
suggested.push(error.auth.resume_command.join(" "));
|
|
1319
|
+
return {
|
|
1320
|
+
code: "auth_required",
|
|
1321
|
+
message: error.message,
|
|
1322
|
+
cause: error.usage,
|
|
1323
|
+
suggested_commands: suggested,
|
|
1324
|
+
auth: error.auth,
|
|
1325
|
+
blocking: true
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
if (error instanceof CliCancellationError) {
|
|
1329
|
+
return {
|
|
1330
|
+
code: "cancelled",
|
|
1331
|
+
message: error.message,
|
|
1332
|
+
blocking: true
|
|
1333
|
+
};
|
|
1334
|
+
}
|
|
1335
|
+
if (error instanceof CliAccessError) {
|
|
1336
|
+
return {
|
|
1337
|
+
code: "access_denied",
|
|
1338
|
+
message: error.message,
|
|
1339
|
+
cause: error.guidance,
|
|
1340
|
+
blocking: true
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
if (error instanceof Error) {
|
|
1344
|
+
if (error instanceof CliInputError || error.name === "ZodError") {
|
|
1345
|
+
return {
|
|
1346
|
+
code: "input_error",
|
|
1347
|
+
message: error.message,
|
|
1348
|
+
cause: "usage" in error && typeof error.usage === "string" ? error.usage : undefined,
|
|
1349
|
+
suggested_commands: "usage" in error && typeof error.usage === "string" ? extractSuggestedCommands(error.usage) : undefined,
|
|
1350
|
+
blocking: true
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
if (isApiLikeError(error)) {
|
|
1354
|
+
const statusCode = "statusCode" in error ? error.statusCode : undefined;
|
|
1355
|
+
return {
|
|
1356
|
+
code: "api_error",
|
|
1357
|
+
message: error.message,
|
|
1358
|
+
cause: typeof statusCode === "number" ? `HTTP ${statusCode}` : undefined,
|
|
1359
|
+
blocking: true
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
if (isNetworkLikeError(error)) {
|
|
1363
|
+
return {
|
|
1364
|
+
code: "network_error",
|
|
1365
|
+
message: error.message,
|
|
1366
|
+
blocking: true
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
code: "command_error",
|
|
1371
|
+
message: error.message,
|
|
1372
|
+
cause: "usage" in error && typeof error.usage === "string" ? error.usage : undefined,
|
|
1373
|
+
suggested_commands: "usage" in error && typeof error.usage === "string" ? extractSuggestedCommands(error.usage) : undefined,
|
|
1374
|
+
blocking: true
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
return {
|
|
1378
|
+
code: "unknown_error",
|
|
1379
|
+
message: String(error),
|
|
1380
|
+
blocking: true
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
function isTerminalEvent(event) {
|
|
1384
|
+
return event === "completed" || event === "error" || event === "cancelled" || event === "needs_input" || event === "auth_required";
|
|
1385
|
+
}
|
|
1386
|
+
function startHeartbeat(command, state) {
|
|
1387
|
+
if (state.heartbeat)
|
|
1388
|
+
return;
|
|
1389
|
+
const intervalMs = Number(process.env.MODA_HEARTBEAT_MS ?? 1e4);
|
|
1390
|
+
if (!Number.isFinite(intervalMs) || intervalMs <= 0)
|
|
1391
|
+
return;
|
|
1392
|
+
state.heartbeat = setInterval(() => {
|
|
1393
|
+
if (state.terminal || stdoutClosed)
|
|
1394
|
+
return;
|
|
1395
|
+
const now = Date.now();
|
|
1396
|
+
if (now - state.lastEventAt < intervalMs)
|
|
1397
|
+
return;
|
|
1398
|
+
state.lastEventAt = now;
|
|
1399
|
+
safeWriteStdout(`${JSON.stringify(createAgentEvent(command, {
|
|
1400
|
+
event: "heartbeat",
|
|
1401
|
+
elapsed_ms: now - state.startedAt
|
|
1402
|
+
}, state))}
|
|
1403
|
+
`);
|
|
1404
|
+
}, intervalMs);
|
|
1405
|
+
state.heartbeat.unref();
|
|
1406
|
+
}
|
|
1407
|
+
function clearHeartbeat(state) {
|
|
1408
|
+
if (!state.heartbeat)
|
|
1409
|
+
return;
|
|
1410
|
+
clearInterval(state.heartbeat);
|
|
1411
|
+
state.heartbeat = undefined;
|
|
1412
|
+
}
|
|
1413
|
+
function normalizeSummary(summary, command, status, data) {
|
|
1414
|
+
if (typeof summary === "string") {
|
|
1415
|
+
return { text: summary, confidence: "high" };
|
|
1416
|
+
}
|
|
1417
|
+
if (summary?.text) {
|
|
1418
|
+
return {
|
|
1419
|
+
text: summary.text,
|
|
1420
|
+
confidence: summary.confidence ?? "high"
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
return inferSummary(command, status, data);
|
|
1424
|
+
}
|
|
1425
|
+
function inferSummary(command, status, data) {
|
|
1426
|
+
const record = asRecord(data);
|
|
1427
|
+
if (status === "error") {
|
|
1428
|
+
return { text: `${command} failed.`, confidence: "high" };
|
|
1429
|
+
}
|
|
1430
|
+
const summary = asRecord(record?.summary);
|
|
1431
|
+
if (summary) {
|
|
1432
|
+
const statusValue = typeof summary.status === "string" ? summary.status : undefined;
|
|
1433
|
+
const fail = typeof summary.fail === "number" ? summary.fail : undefined;
|
|
1434
|
+
const warning = typeof summary.warning === "number" ? summary.warning : undefined;
|
|
1435
|
+
if (statusValue && fail !== undefined && warning !== undefined) {
|
|
1436
|
+
return {
|
|
1437
|
+
text: `${command} completed with status ${statusValue}: ${fail} failing checks and ${warning} warnings.`,
|
|
1438
|
+
confidence: "high"
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
const runtimeAgentCount = typeof summary.runtimeAgentCount === "number" ? summary.runtimeAgentCount : undefined;
|
|
1442
|
+
if (runtimeAgentCount !== undefined) {
|
|
1443
|
+
return {
|
|
1444
|
+
text: `${command} completed. Harness summary includes ${runtimeAgentCount} runtime agent(s).`,
|
|
1445
|
+
confidence: "high"
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof record?.status === "string") {
|
|
1450
|
+
return { text: `${command} completed with status ${record.status}.`, confidence: "high" };
|
|
1451
|
+
}
|
|
1452
|
+
return { text: `${command} completed.`, confidence: "medium" };
|
|
1453
|
+
}
|
|
1454
|
+
function normalizeNextCommands(commands) {
|
|
1455
|
+
return commands.map((command) => {
|
|
1456
|
+
if (typeof command !== "string")
|
|
1457
|
+
return command;
|
|
1458
|
+
return {
|
|
1459
|
+
command,
|
|
1460
|
+
purpose: "Suggested next Moda command.",
|
|
1461
|
+
mutability: inferMutability(command),
|
|
1462
|
+
requires_approval: command.includes(" --yes") || command.includes(" sync")
|
|
1463
|
+
};
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
function inferMutability(command) {
|
|
1467
|
+
if (/\b(sync|approve|init|install|pull|gen|remove|promote)\b/.test(command)) {
|
|
1468
|
+
if (/\bsync\b/.test(command))
|
|
1469
|
+
return "sync";
|
|
1470
|
+
return "write";
|
|
1471
|
+
}
|
|
1472
|
+
return "read";
|
|
1473
|
+
}
|
|
1474
|
+
function inferWarnings(data) {
|
|
1475
|
+
const record = asRecord(data);
|
|
1476
|
+
const direct = record?.warnings;
|
|
1477
|
+
if (Array.isArray(direct))
|
|
1478
|
+
return direct.filter((item) => typeof item === "string");
|
|
1479
|
+
const validation = asRecord(record?.validation);
|
|
1480
|
+
if (Array.isArray(validation?.warnings)) {
|
|
1481
|
+
return validation.warnings.filter((item) => typeof item === "string");
|
|
1482
|
+
}
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
function boundForAgent(data) {
|
|
1486
|
+
const maxBytes = Number(process.env.MODA_AGENT_MAX_OUTPUT_BYTES ?? 200000);
|
|
1487
|
+
const serialized = JSON.stringify(data);
|
|
1488
|
+
const bytes = Buffer.byteLength(serialized, "utf8");
|
|
1489
|
+
if (bytes <= maxBytes)
|
|
1490
|
+
return { data };
|
|
1491
|
+
const preview = serialized.slice(0, Math.max(0, maxBytes - 256));
|
|
1492
|
+
return {
|
|
1493
|
+
data: {
|
|
1494
|
+
truncated: true,
|
|
1495
|
+
original_bytes: bytes,
|
|
1496
|
+
max_bytes: maxBytes,
|
|
1497
|
+
preview
|
|
1498
|
+
},
|
|
1499
|
+
truncation: {
|
|
1500
|
+
data: {
|
|
1501
|
+
original_bytes: bytes,
|
|
1502
|
+
max_bytes: maxBytes,
|
|
1503
|
+
reason: "agent_output_byte_limit"
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
}
|
|
1508
|
+
function asRecord(value) {
|
|
1509
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
1510
|
+
}
|
|
1511
|
+
function extractSuggestedCommands(text) {
|
|
1512
|
+
const commands = [...text.matchAll(/`(moda [^`]+)`/g)].map((match) => match[1]);
|
|
1513
|
+
return commands.length > 0 ? commands : undefined;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
// src/command-context.ts
|
|
1517
|
+
function flagEnabled(flags, key) {
|
|
1518
|
+
return flags[key] === "true";
|
|
1519
|
+
}
|
|
1520
|
+
function stringOption(value) {
|
|
1521
|
+
if (value === undefined || value === null)
|
|
1522
|
+
return;
|
|
1523
|
+
const text = String(value).trim();
|
|
1524
|
+
return text.length > 0 ? text : undefined;
|
|
1525
|
+
}
|
|
1526
|
+
function createCommandContext(input, options = {}) {
|
|
1527
|
+
const flags = options.commandFlags ?? input.flags;
|
|
1528
|
+
const rawFlags = options.rawFlags ?? input.flags;
|
|
1529
|
+
const outputMode = options.outputMode ?? outputModeFromFlags(flags);
|
|
1530
|
+
const quiet = flagEnabled(flags, "quiet");
|
|
1531
|
+
const interactive = outputMode === "human" && !quiet && !flagEnabled(flags, "yes") && process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
1532
|
+
const telemetryMeta = {};
|
|
1533
|
+
return {
|
|
1534
|
+
command: options.commandName ?? input.command,
|
|
1535
|
+
positional: input.positional,
|
|
1536
|
+
positionals: input.positionals,
|
|
1537
|
+
flags,
|
|
1538
|
+
rawFlags,
|
|
1539
|
+
startedAt: options.startedAt ?? Date.now(),
|
|
1540
|
+
cwd: options.cwd ?? process.cwd(),
|
|
1541
|
+
env: options.env ?? process.env,
|
|
1542
|
+
outputMode,
|
|
1543
|
+
output: createCliOutput({
|
|
1544
|
+
command: options.commandName ?? input.command,
|
|
1545
|
+
mode: outputMode,
|
|
1546
|
+
quiet
|
|
1547
|
+
}),
|
|
1548
|
+
profile: flags.profile,
|
|
1549
|
+
dryRun: flagEnabled(flags, "dry-run"),
|
|
1550
|
+
yes: flagEnabled(flags, "yes"),
|
|
1551
|
+
quiet,
|
|
1552
|
+
verbose: flagEnabled(flags, "verbose"),
|
|
1553
|
+
interactive,
|
|
1554
|
+
recordTelemetry(meta) {
|
|
1555
|
+
Object.assign(telemetryMeta, meta);
|
|
1556
|
+
},
|
|
1557
|
+
getTelemetryMeta() {
|
|
1558
|
+
return telemetryMeta;
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
export { __require, PROD_BASE_URL, DEFAULT_INGEST_URL, getModaPaths, getModaDir, isLoopbackBaseUrl, assertValidProfileName, loadProfileConfig, saveProfileConfig, getActiveProfile, setActiveProfile, upsertProfile, clearStoredApiKeyCredentials, secretFilePathFromRef, resolveSecretReference, resolveApiKeyFromActiveProfile, resolveTenantIdFromActiveProfile, ensureSecretFilePermissions, writeSecretJsonFile, CliInputError, CliAccessError, CliInputRequiredError, CliAuthError, CliCancellationError, isNetworkLikeError, isApiLikeError, VERSION, loadCliConfig, saveCliConfig, resolveApiKey, embeddedTenantIdFromApiKey, resolveIngestUrl2 as resolveIngestUrl, resolveTenantId, resolveModaBaseUrl, validateConfig, MAX_SCANNED_FILE_BYTES, DEFAULT_MAX_INVENTORY_FILES, DEFAULT_MAX_INVENTORY_TEXT_BYTES, MAX_RECORDED_SKIPS, SKIPPED_DIRS, isSkippedDir, isSkippedPathPrefix, isEvalLikePath, shouldInventoryFile, isTextFile, isSecretishPath, redactDeep, packSnapshot, EXIT_OK, EXIT_ERROR, EXIT_DEGRADED, EXIT_AUTH_REQUIRED, EXIT_INPUT_REQUIRED, EXIT_CANCELLED, AGENT_ERROR_CODES, wasOutputCancelled, safeWriteStdout, safeWriteStderr, resolveOutputMode, createCliOutput, stringOption, createCommandContext };
|