@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
package/dist/keys.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import { assertValidProfileName, getModaPaths, secretFilePathFromRef, writeSecretJsonFile, } from './profiles.js';
|
|
5
|
-
export const KEY_LIFECYCLE_ENDPOINT_REQUIREMENTS = {
|
|
6
|
-
list: 'GET /api/cli/keys?profile=<profile>',
|
|
7
|
-
create: 'POST /api/cli/keys',
|
|
8
|
-
rotate: 'POST /api/cli/keys/<key_id>/rotate',
|
|
9
|
-
revoke: 'POST /api/cli/keys/<key_id>/revoke',
|
|
10
|
-
};
|
|
11
|
-
export function keySecretRef(profile, keyId) {
|
|
12
|
-
assertValidProfileName(profile);
|
|
13
|
-
assertSafeKeyId(keyId);
|
|
14
|
-
return `file:secrets/api-keys/${profile}/${keyId}.json`;
|
|
15
|
-
}
|
|
16
|
-
export function createLocalKeyDescriptor(input) {
|
|
17
|
-
assertValidProfileName(input.profile);
|
|
18
|
-
const now = input.now ?? new Date();
|
|
19
|
-
const id = stableKeyId(input.profile, input.name, now);
|
|
20
|
-
return {
|
|
21
|
-
id,
|
|
22
|
-
profile: input.profile,
|
|
23
|
-
name: input.name,
|
|
24
|
-
purpose: input.purpose ?? 'data_api',
|
|
25
|
-
environment: input.environment,
|
|
26
|
-
tenant_id: input.tenant_id,
|
|
27
|
-
workspace_id: input.workspace_id,
|
|
28
|
-
secret_ref: input.secret_ref ?? keySecretRef(input.profile, id),
|
|
29
|
-
status: 'active',
|
|
30
|
-
created_at: now.toISOString(),
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
export function validateKeyDescriptor(descriptor) {
|
|
34
|
-
assertValidProfileName(descriptor.profile);
|
|
35
|
-
assertSafeKeyId(descriptor.id);
|
|
36
|
-
if (!descriptor.name.trim()) {
|
|
37
|
-
throw new Error('Key name is required.');
|
|
38
|
-
}
|
|
39
|
-
if (descriptor.status !== 'active' &&
|
|
40
|
-
descriptor.status !== 'rotating' &&
|
|
41
|
-
descriptor.status !== 'revoked') {
|
|
42
|
-
throw new Error(`Invalid key status '${descriptor.status}'.`);
|
|
43
|
-
}
|
|
44
|
-
if (descriptor.purpose !== 'data_api' &&
|
|
45
|
-
descriptor.purpose !== 'ingest' &&
|
|
46
|
-
descriptor.purpose !== 'automation') {
|
|
47
|
-
throw new Error(`Invalid key purpose '${descriptor.purpose}'.`);
|
|
48
|
-
}
|
|
49
|
-
if (descriptor.environment !== 'local' &&
|
|
50
|
-
descriptor.environment !== 'dev' &&
|
|
51
|
-
descriptor.environment !== 'staging' &&
|
|
52
|
-
descriptor.environment !== 'prod') {
|
|
53
|
-
throw new Error(`Invalid key environment '${descriptor.environment}'.`);
|
|
54
|
-
}
|
|
55
|
-
if (!descriptor.secret_ref.startsWith('file:')) {
|
|
56
|
-
throw new Error('Local key descriptors must point to a file: secret ref.');
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
export function writeLocalApiKeySecret(descriptor, apiKey, options = {}) {
|
|
60
|
-
validateKeyDescriptor(descriptor);
|
|
61
|
-
if (!apiKey.trim()) {
|
|
62
|
-
throw new Error('API key value is required.');
|
|
63
|
-
}
|
|
64
|
-
const path = secretFilePathFromRef(descriptor.secret_ref, options);
|
|
65
|
-
if (!path) {
|
|
66
|
-
throw new Error(`Unsupported key secret reference: ${descriptor.secret_ref}`);
|
|
67
|
-
}
|
|
68
|
-
writeSecretJsonFile(path, {
|
|
69
|
-
value: apiKey,
|
|
70
|
-
key_id: descriptor.id,
|
|
71
|
-
profile: descriptor.profile,
|
|
72
|
-
purpose: descriptor.purpose,
|
|
73
|
-
created_at: descriptor.created_at,
|
|
74
|
-
});
|
|
75
|
-
return path;
|
|
76
|
-
}
|
|
77
|
-
export function keyDescriptorStorePath(options = {}) {
|
|
78
|
-
return join(getModaPaths(options).dir, 'keys.json');
|
|
79
|
-
}
|
|
80
|
-
export function loadLocalKeyDescriptors(options = {}) {
|
|
81
|
-
const path = keyDescriptorStorePath(options);
|
|
82
|
-
try {
|
|
83
|
-
if (!existsSync(path))
|
|
84
|
-
return [];
|
|
85
|
-
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
86
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
87
|
-
return [];
|
|
88
|
-
}
|
|
89
|
-
const keys = parsed.keys;
|
|
90
|
-
if (!Array.isArray(keys))
|
|
91
|
-
return [];
|
|
92
|
-
return keys.filter((key) => {
|
|
93
|
-
try {
|
|
94
|
-
validateKeyDescriptor(key);
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
export function saveLocalKeyDescriptors(keys, options = {}) {
|
|
107
|
-
for (const key of keys)
|
|
108
|
-
validateKeyDescriptor(key);
|
|
109
|
-
writeSecretJsonFile(keyDescriptorStorePath(options), {
|
|
110
|
-
version: 1,
|
|
111
|
-
keys,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
export function upsertLocalKeyDescriptor(descriptor, options = {}) {
|
|
115
|
-
validateKeyDescriptor(descriptor);
|
|
116
|
-
const keys = loadLocalKeyDescriptors(options);
|
|
117
|
-
const next = [
|
|
118
|
-
...keys.filter((key) => key.id !== descriptor.id),
|
|
119
|
-
descriptor,
|
|
120
|
-
].sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
121
|
-
saveLocalKeyDescriptors(next, options);
|
|
122
|
-
return next;
|
|
123
|
-
}
|
|
124
|
-
function stableKeyId(profile, name, now) {
|
|
125
|
-
const hash = createHash('sha256')
|
|
126
|
-
.update(`${profile}\0${name}\0${now.toISOString()}`)
|
|
127
|
-
.digest('hex')
|
|
128
|
-
.slice(0, 12);
|
|
129
|
-
return `key_${hash}`;
|
|
130
|
-
}
|
|
131
|
-
function assertSafeKeyId(keyId) {
|
|
132
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(keyId)) {
|
|
133
|
-
throw new Error(`Invalid key id '${keyId}'. Use letters, numbers, dots, dashes, or underscores.`);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
//# sourceMappingURL=keys.js.map
|
package/dist/manifest.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import type { CommandMetadata } from './command-registry.js';
|
|
2
|
-
export declare const MANIFEST_SCHEMA_VERSION = "moda.manifest.v1";
|
|
3
|
-
export declare function buildManifest(commands: CommandMetadata[]): Record<string, unknown>;
|
|
4
|
-
export declare function renderManifestHuman(manifest: Record<string, unknown>): string;
|
|
5
|
-
//# sourceMappingURL=manifest.d.ts.map
|
package/dist/manifest.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { VERSION } from './config.js';
|
|
2
|
-
export const MANIFEST_SCHEMA_VERSION = 'moda.manifest.v1';
|
|
3
|
-
export function buildManifest(commands) {
|
|
4
|
-
return {
|
|
5
|
-
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
6
|
-
cli_version: VERSION,
|
|
7
|
-
invocations: [
|
|
8
|
-
'bunx @moda-ai/cli',
|
|
9
|
-
'npx -y @moda-ai/cli',
|
|
10
|
-
'moda',
|
|
11
|
-
],
|
|
12
|
-
global_flags: [
|
|
13
|
-
'--agent',
|
|
14
|
-
'--stream',
|
|
15
|
-
'--json',
|
|
16
|
-
'--human',
|
|
17
|
-
'--profile <name>',
|
|
18
|
-
'--quiet',
|
|
19
|
-
'--no-telemetry',
|
|
20
|
-
'--no-update-check',
|
|
21
|
-
'--json-schemas',
|
|
22
|
-
],
|
|
23
|
-
env: [
|
|
24
|
-
'MODA_FORMAT',
|
|
25
|
-
'MODA_AGENT',
|
|
26
|
-
'MODA_API_KEY',
|
|
27
|
-
'MODA_TENANT_ID',
|
|
28
|
-
'MODA_PROFILE',
|
|
29
|
-
'MODA_BASE_URL',
|
|
30
|
-
'MODA_AUTH_TIMEOUT_MS',
|
|
31
|
-
'MODA_HEARTBEAT_MS',
|
|
32
|
-
'MODA_AGENT_MAX_OUTPUT_BYTES',
|
|
33
|
-
'MODA_CLI_TELEMETRY',
|
|
34
|
-
'MODA_CLI_UPDATE_CHECK',
|
|
35
|
-
'MODA_CLI_TIPS',
|
|
36
|
-
],
|
|
37
|
-
exit_codes: [
|
|
38
|
-
{ code: 0, name: 'ok', meaning: 'Command completed successfully.' },
|
|
39
|
-
{ code: 1, name: 'error', meaning: 'Command, input, API, network, or unexpected failure.' },
|
|
40
|
-
{ code: 3, name: 'degraded', meaning: 'Reserved for `moda ask` local fallback.' },
|
|
41
|
-
{ code: 4, name: 'auth_required', meaning: 'Run login_command, then resume.' },
|
|
42
|
-
{ code: 5, name: 'input_required', meaning: 'Choose from input_request choices, then resume.' },
|
|
43
|
-
{ code: 130, name: 'cancelled', meaning: 'Interrupted or output consumer closed.' },
|
|
44
|
-
],
|
|
45
|
-
events: [
|
|
46
|
-
event('started', 'First event for every agent stream.', false),
|
|
47
|
-
event('progress', 'Progress update while work continues.', false),
|
|
48
|
-
event('warning', 'Non-fatal warning or skipped action.', false),
|
|
49
|
-
event('artifact', 'File or artifact produced by a command.', false),
|
|
50
|
-
event('heartbeat', 'Idle liveness event emitted after MODA_HEARTBEAT_MS without output.', false),
|
|
51
|
-
event('needs_input', 'Terminal event requesting exit-and-reinvoke input.', true),
|
|
52
|
-
event('auth_required', 'Terminal event requesting authentication before retry.', true),
|
|
53
|
-
event('completed', 'Terminal success event.', true),
|
|
54
|
-
event('error', 'Terminal failure event.', true),
|
|
55
|
-
event('cancelled', 'Terminal cancellation event.', true),
|
|
56
|
-
event('approval_required', 'Deprecated; use needs_input.', true, true),
|
|
57
|
-
],
|
|
58
|
-
schemas: [
|
|
59
|
-
'moda.agent.v1',
|
|
60
|
-
'moda.agent_event.v1',
|
|
61
|
-
'moda.intelligence.v1',
|
|
62
|
-
'production_ask.v0.1',
|
|
63
|
-
],
|
|
64
|
-
commands: commands.map((command) => ({
|
|
65
|
-
name: command.name,
|
|
66
|
-
aliases: command.aliases ?? [],
|
|
67
|
-
summary: command.description,
|
|
68
|
-
output_modes: command.outputModes,
|
|
69
|
-
streaming: command.outputModes.includes('agent-stream') || command.outputModes.includes('agent'),
|
|
70
|
-
examples: command.examples,
|
|
71
|
-
auth: command.auth,
|
|
72
|
-
mutability: command.mutability,
|
|
73
|
-
exit_codes: exitCodesForCommand(command.name),
|
|
74
|
-
})),
|
|
75
|
-
concepts: [
|
|
76
|
-
{ term: 'tenant', meaning: 'A Moda organization/workspace that owns data, prompts, skills, and API keys.' },
|
|
77
|
-
{ term: 'profile', meaning: 'A named local CLI configuration under ~/.moda that selects environment, tenant, and secret references.' },
|
|
78
|
-
{ term: 'harness', meaning: 'Moda’s local graph of runtime agents, prompts, tools, identities, and deployments in a codebase.' },
|
|
79
|
-
{ term: 'harness report', meaning: 'A cited analyst artifact under .moda/ that justifies the harness graph before sync.' },
|
|
80
|
-
{ term: 'remote analyze run', meaning: 'A Moda-hosted harness analysis tracked in .moda/harness-remote-run.json; fetch its status or result with `moda harness pull`.' },
|
|
81
|
-
{ term: 'conversation', meaning: 'A user-agent interaction thread stored in Moda analytics.' },
|
|
82
|
-
{ term: 'trace', meaning: 'Raw OTLP span evidence for one run or conversation, exposed by `moda audit`.' },
|
|
83
|
-
{ term: 'ingest key', meaning: 'A `moda_sk_` API key for SDKs, CI, and Data API calls. Treat it as a secret.' },
|
|
84
|
-
{ term: 'CLI session token', meaning: 'A local browser-auth session token used by CLI auth/bootstrap flows, not by application SDKs.' },
|
|
85
|
-
{ term: 'prompt', meaning: 'A code-first prompt file tracked by `.moda/prompts.yml` and synced to Moda.' },
|
|
86
|
-
{ term: 'skill', meaning: 'A SKILL.md agent instruction file generated by or synced to Moda.' },
|
|
87
|
-
],
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
export function renderManifestHuman(manifest) {
|
|
91
|
-
const commands = Array.isArray(manifest.commands) ? manifest.commands : [];
|
|
92
|
-
return [
|
|
93
|
-
`Moda CLI manifest ${manifest.cli_version ?? ''}`,
|
|
94
|
-
'',
|
|
95
|
-
'Commands:',
|
|
96
|
-
...commands.map((command) => {
|
|
97
|
-
const row = command;
|
|
98
|
-
return ` ${row.name ?? 'unknown'} - ${row.summary ?? ''}`;
|
|
99
|
-
}),
|
|
100
|
-
'',
|
|
101
|
-
'Run `moda manifest --json` for the full machine-readable contract.',
|
|
102
|
-
].join('\n');
|
|
103
|
-
}
|
|
104
|
-
function event(name, when, terminal, deprecated = false) {
|
|
105
|
-
return { name, event_version: 1, when, terminal, ...(deprecated ? { deprecated: true } : {}) };
|
|
106
|
-
}
|
|
107
|
-
function exitCodesForCommand(command) {
|
|
108
|
-
if (command === 'ask')
|
|
109
|
-
return [0, 1, 3, 4, 5, 130];
|
|
110
|
-
if (command === 'init' || command === 'harness')
|
|
111
|
-
return [0, 1, 4, 5, 130];
|
|
112
|
-
return [0, 1, 4, 130];
|
|
113
|
-
}
|
|
114
|
-
//# sourceMappingURL=manifest.js.map
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export interface MarkdownTerminalOptions {
|
|
2
|
-
/** When false, all structural fixes still apply but zero ANSI codes are emitted. */
|
|
3
|
-
color: boolean;
|
|
4
|
-
}
|
|
5
|
-
/** Minimal ANSI styling helpers shared by the renderer and ask chrome. */
|
|
6
|
-
export interface TerminalStyles {
|
|
7
|
-
bold(text: string): string;
|
|
8
|
-
italic(text: string): string;
|
|
9
|
-
dim(text: string): string;
|
|
10
|
-
code(text: string): string;
|
|
11
|
-
/** Section heading: bold + underline. */
|
|
12
|
-
heading(text: string): string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* The CLI's color convention (mirrors `src/tui.ts` / `src/analyst-tui.ts`):
|
|
16
|
-
* color only for a human at a TTY, honoring `NO_COLOR` (present with ANY value,
|
|
17
|
-
* including the empty string, disables — per the no-color.org spec) and
|
|
18
|
-
* `TERM=dumb`.
|
|
19
|
-
*/
|
|
20
|
-
export declare function shouldColorTerminalOutput(opts?: {
|
|
21
|
-
isTTY?: boolean;
|
|
22
|
-
env?: NodeJS.ProcessEnv;
|
|
23
|
-
}): boolean;
|
|
24
|
-
/**
|
|
25
|
-
* ANSI style constructors. Closers restore the specific attribute (22/23/39)
|
|
26
|
-
* rather than a full reset so nested styles (code inside bold, bold inside a
|
|
27
|
-
* heading) survive. When `color` is false every helper is the identity.
|
|
28
|
-
*/
|
|
29
|
-
export declare function terminalStyles(color: boolean): TerminalStyles;
|
|
30
|
-
/** Remove ANSI/OSC control sequences from untrusted content. */
|
|
31
|
-
export declare function stripAnsi(text: string): string;
|
|
32
|
-
/**
|
|
33
|
-
* Render an agent markdown answer for direct terminal display.
|
|
34
|
-
*
|
|
35
|
-
* - Headers lose their `#` sigils; h1/h2 render bold+underlined, h3+ bold,
|
|
36
|
-
* with a blank line before.
|
|
37
|
-
* - `**bold**`, `*italic*`/`_italic_`, and backtick `code` spans lose their
|
|
38
|
-
* sigils and gain ANSI styling (bold / italic / cyan) when `color` is on.
|
|
39
|
-
* - List items are re-indented to consistent 2-space-per-level steps based on
|
|
40
|
-
* their original RELATIVE nesting (odd indents round to the nearest level; a
|
|
41
|
-
* top-level-looking item after deeper items stays top-level). Bullets render
|
|
42
|
-
* `•` at level 0 and `-` deeper; ordered items keep their numbers.
|
|
43
|
-
* - `|` table rows and fenced code blocks pass through untouched (only ANSI
|
|
44
|
-
* control sequences are stripped, since that content is backend-controlled).
|
|
45
|
-
* - Runs of 3+ blank lines collapse to a single blank line.
|
|
46
|
-
* - Prose is never reflowed or wrapped.
|
|
47
|
-
*/
|
|
48
|
-
export declare function renderMarkdownForTerminal(markdown: string, opts: MarkdownTerminalOptions): string;
|
|
49
|
-
//# sourceMappingURL=markdown-terminal.d.ts.map
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
// src/markdown-terminal.ts
|
|
2
|
-
//
|
|
3
|
-
// Pure markdown -> terminal renderer for human-mode output (`moda ask`).
|
|
4
|
-
// The cloud agent answers in markdown; printing it raw leaves literal `##`
|
|
5
|
-
// header sigils, `**bold**` asterisks, and the model's erratic list
|
|
6
|
-
// indentation on screen. This module strips the sigils, applies ANSI styling
|
|
7
|
-
// when the terminal supports it, and normalizes list nesting to consistent
|
|
8
|
-
// 2-space steps — WITHOUT reflowing prose (terminals wrap themselves) and
|
|
9
|
-
// WITHOUT touching `|` tables or fenced code blocks.
|
|
10
|
-
//
|
|
11
|
-
// No dependencies: raw ANSI escape constants only (the CLI has no color lib).
|
|
12
|
-
/**
|
|
13
|
-
* The CLI's color convention (mirrors `src/tui.ts` / `src/analyst-tui.ts`):
|
|
14
|
-
* color only for a human at a TTY, honoring `NO_COLOR` (present with ANY value,
|
|
15
|
-
* including the empty string, disables — per the no-color.org spec) and
|
|
16
|
-
* `TERM=dumb`.
|
|
17
|
-
*/
|
|
18
|
-
export function shouldColorTerminalOutput(opts = {}) {
|
|
19
|
-
const env = opts.env ?? process.env;
|
|
20
|
-
const isTTY = opts.isTTY ?? process.stdout.isTTY === true;
|
|
21
|
-
if (!isTTY)
|
|
22
|
-
return false;
|
|
23
|
-
if (env.NO_COLOR !== undefined)
|
|
24
|
-
return false;
|
|
25
|
-
if (env.TERM === 'dumb')
|
|
26
|
-
return false;
|
|
27
|
-
return true;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* ANSI style constructors. Closers restore the specific attribute (22/23/39)
|
|
31
|
-
* rather than a full reset so nested styles (code inside bold, bold inside a
|
|
32
|
-
* heading) survive. When `color` is false every helper is the identity.
|
|
33
|
-
*/
|
|
34
|
-
export function terminalStyles(color) {
|
|
35
|
-
const paint = (open, close) => (text) => color ? `\u001b[${open}m${text}\u001b[${close}m` : text;
|
|
36
|
-
return {
|
|
37
|
-
bold: paint('1', '22'),
|
|
38
|
-
italic: paint('3', '23'),
|
|
39
|
-
dim: paint('2', '22'),
|
|
40
|
-
code: paint('36', '39'),
|
|
41
|
-
heading: paint('1;4', '22;24'),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
const FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
45
|
-
const FENCE_CLOSE_RE = /^\s*(`{3,}|~{3,})\s*$/;
|
|
46
|
-
const TABLE_ROW_RE = /^\s*\|.*\S/;
|
|
47
|
-
// Matches ANSI/OSC control sequences: CSI (ESC [ … final byte), OSC (ESC ] …
|
|
48
|
-
// BEL or ST), and other two-byte escapes. Backend-controlled content (table
|
|
49
|
-
// cells, code-fence bodies, streamed tool names/titles) is passed through
|
|
50
|
-
// verbatim, so any escape sequences an LLM or a compromised backend emits must
|
|
51
|
-
// be stripped before they reach the terminal — otherwise they could clear the
|
|
52
|
-
// screen, spoof UI, or trigger OSC side-effects in some emulators.
|
|
53
|
-
const ANSI_RE = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)|[@-Z\\-_])/g;
|
|
54
|
-
/** Remove ANSI/OSC control sequences from untrusted content. */
|
|
55
|
-
export function stripAnsi(text) {
|
|
56
|
-
return text.replace(ANSI_RE, '');
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* A fenced code block's opening marker: the fence character (`` ` `` or `~`)
|
|
60
|
-
* and its length. CommonMark closes a fence only with the same character and at
|
|
61
|
-
* least as many markers, so tracking these prevents a `~~~` line from closing a
|
|
62
|
-
* ` ``` ` block (and a 3-backtick line from closing a 4-backtick opener).
|
|
63
|
-
*/
|
|
64
|
-
function parseFenceOpen(line) {
|
|
65
|
-
const match = FENCE_RE.exec(line);
|
|
66
|
-
if (!match)
|
|
67
|
-
return null;
|
|
68
|
-
return { char: match[1][0], len: match[1].length };
|
|
69
|
-
}
|
|
70
|
-
/** Whether `line` closes an open fence per the CommonMark same-char/length rule. */
|
|
71
|
-
function isFenceClose(line, fence) {
|
|
72
|
-
const match = FENCE_CLOSE_RE.exec(line);
|
|
73
|
-
return match !== null && match[1][0] === fence.char && match[1].length >= fence.len;
|
|
74
|
-
}
|
|
75
|
-
const HEADER_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
76
|
-
const BULLET_RE = /^(\s*)([-*+])\s+(.*)$/;
|
|
77
|
-
const ORDERED_RE = /^(\s*)(\d+)[.)]\s+(.*)$/;
|
|
78
|
-
/**
|
|
79
|
-
* Render an agent markdown answer for direct terminal display.
|
|
80
|
-
*
|
|
81
|
-
* - Headers lose their `#` sigils; h1/h2 render bold+underlined, h3+ bold,
|
|
82
|
-
* with a blank line before.
|
|
83
|
-
* - `**bold**`, `*italic*`/`_italic_`, and backtick `code` spans lose their
|
|
84
|
-
* sigils and gain ANSI styling (bold / italic / cyan) when `color` is on.
|
|
85
|
-
* - List items are re-indented to consistent 2-space-per-level steps based on
|
|
86
|
-
* their original RELATIVE nesting (odd indents round to the nearest level; a
|
|
87
|
-
* top-level-looking item after deeper items stays top-level). Bullets render
|
|
88
|
-
* `•` at level 0 and `-` deeper; ordered items keep their numbers.
|
|
89
|
-
* - `|` table rows and fenced code blocks pass through untouched (only ANSI
|
|
90
|
-
* control sequences are stripped, since that content is backend-controlled).
|
|
91
|
-
* - Runs of 3+ blank lines collapse to a single blank line.
|
|
92
|
-
* - Prose is never reflowed or wrapped.
|
|
93
|
-
*/
|
|
94
|
-
export function renderMarkdownForTerminal(markdown, opts) {
|
|
95
|
-
const style = terminalStyles(opts.color);
|
|
96
|
-
const out = [];
|
|
97
|
-
// Original indent width backing each active list level; index == level.
|
|
98
|
-
const listStack = [];
|
|
99
|
-
// The open fence's char + length, or null outside a fenced code block.
|
|
100
|
-
let fence = null;
|
|
101
|
-
for (const raw of markdown.replace(/\r\n/g, '\n').split('\n')) {
|
|
102
|
-
if (fence) {
|
|
103
|
-
out.push(stripAnsi(raw));
|
|
104
|
-
if (isFenceClose(raw, fence))
|
|
105
|
-
fence = null;
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
const opened = parseFenceOpen(raw);
|
|
109
|
-
if (opened) {
|
|
110
|
-
fence = opened;
|
|
111
|
-
listStack.length = 0;
|
|
112
|
-
out.push(stripAnsi(raw));
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (TABLE_ROW_RE.test(raw)) {
|
|
116
|
-
// Tables depend on exact column spacing — pass through monospaced.
|
|
117
|
-
out.push(stripAnsi(raw));
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const line = raw.replace(/\s+$/, '');
|
|
121
|
-
if (line === '') {
|
|
122
|
-
out.push('');
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
const header = HEADER_RE.exec(line);
|
|
126
|
-
if (header) {
|
|
127
|
-
listStack.length = 0;
|
|
128
|
-
// Strip inline sigils inside the header text (plain), then style the
|
|
129
|
-
// whole line so nested `**bold**` can't fight the heading attributes.
|
|
130
|
-
const text = renderInline(header[2], terminalStyles(false));
|
|
131
|
-
if (out.length > 0 && out[out.length - 1] !== '')
|
|
132
|
-
out.push('');
|
|
133
|
-
out.push(header[1].length <= 2 ? style.heading(text) : style.bold(text));
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
const bullet = BULLET_RE.exec(line);
|
|
137
|
-
const ordered = bullet ? null : ORDERED_RE.exec(line);
|
|
138
|
-
if (bullet || ordered) {
|
|
139
|
-
const match = (bullet ?? ordered);
|
|
140
|
-
const level = resolveListLevel(match[1].length, listStack);
|
|
141
|
-
const indent = ' '.repeat(level);
|
|
142
|
-
const text = renderInline(match[3], style);
|
|
143
|
-
if (bullet) {
|
|
144
|
-
out.push(`${indent}${level === 0 ? '•' : '-'} ${text}`);
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
out.push(`${indent}${match[2]}. ${text}`);
|
|
148
|
-
}
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
// Unindented prose ends any open list; indented non-list lines are treated
|
|
152
|
-
// as continuations and keep the current nesting context.
|
|
153
|
-
if (!/^\s{2,}/.test(line))
|
|
154
|
-
listStack.length = 0;
|
|
155
|
-
out.push(renderInline(line, style));
|
|
156
|
-
}
|
|
157
|
-
return collapseBlankRuns(out).join('\n');
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Map an item's original indent width onto a consistent nesting level.
|
|
161
|
-
* `stack[level]` remembers the original indent that opened that level:
|
|
162
|
-
* meaningfully shallower indents pop back out, meaningfully deeper indents
|
|
163
|
-
* open a new level, and anything within +/-1 space sticks to the current
|
|
164
|
-
* level (rounding the model's odd 3/5/7-space indents to the nearest level).
|
|
165
|
-
*/
|
|
166
|
-
function resolveListLevel(indent, stack) {
|
|
167
|
-
while (stack.length > 0 && indent < stack[stack.length - 1] - 1)
|
|
168
|
-
stack.pop();
|
|
169
|
-
if (stack.length === 0 || indent > stack[stack.length - 1] + 1)
|
|
170
|
-
stack.push(indent);
|
|
171
|
-
return stack.length - 1;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Strip/style inline markdown sigils: `code` spans first (protected via
|
|
175
|
-
* placeholders so asterisks/underscores inside them are never re-parsed),
|
|
176
|
-
* then `**bold**`/`__bold__`, then word-boundary `*italic*`/`_italic_`
|
|
177
|
-
* (mid-word underscores as in `silent_empty_result` never match).
|
|
178
|
-
*/
|
|
179
|
-
function renderInline(text, style) {
|
|
180
|
-
const codeSpans = [];
|
|
181
|
-
// Strip any ANSI escapes already in the (backend-controlled) text BEFORE we
|
|
182
|
-
// add our own styling, so this only removes injected sequences — never the
|
|
183
|
-
// codes the style helpers emit below.
|
|
184
|
-
let result = stripAnsi(text).replace(/`([^`]+)`/g, (_, code) => {
|
|
185
|
-
codeSpans.push(code);
|
|
186
|
-
return `\u0000${codeSpans.length - 1}\u0000`;
|
|
187
|
-
});
|
|
188
|
-
result = result.replace(/\*\*([^\s*](?:[^*]*[^\s*])?)\*\*/g, (_, inner) => style.bold(inner));
|
|
189
|
-
result = result.replace(/__([^\s_](?:[^_]*[^\s_])?)__/g, (_, inner) => style.bold(inner));
|
|
190
|
-
result = result.replace(/(?<![\w*])\*([^\s*](?:[^*\n]*[^\s*])?)\*(?![\w*])/g, (_, inner) => style.italic(inner));
|
|
191
|
-
result = result.replace(/(?<![\w_])_([^\s_](?:[^_\n]*[^\s_])?)_(?![\w_])/g, (_, inner) => style.italic(inner));
|
|
192
|
-
return result.replace(/\u0000(\d+)\u0000/g, (_, index) => style.code(codeSpans[Number(index)]));
|
|
193
|
-
}
|
|
194
|
-
/** Collapse runs of 3+ blank lines down to a single blank line. */
|
|
195
|
-
function collapseBlankRuns(lines) {
|
|
196
|
-
const collapsed = [];
|
|
197
|
-
let blanks = 0;
|
|
198
|
-
for (const line of lines) {
|
|
199
|
-
if (line === '') {
|
|
200
|
-
blanks += 1;
|
|
201
|
-
continue;
|
|
202
|
-
}
|
|
203
|
-
if (blanks > 0)
|
|
204
|
-
collapsed.push('', ...(blanks === 2 ? [''] : []));
|
|
205
|
-
blanks = 0;
|
|
206
|
-
collapsed.push(line);
|
|
207
|
-
}
|
|
208
|
-
if (blanks > 0)
|
|
209
|
-
collapsed.push('', ...(blanks === 2 ? [''] : []));
|
|
210
|
-
return collapsed;
|
|
211
|
-
}
|
|
212
|
-
//# sourceMappingURL=markdown-terminal.js.map
|