@moda-ai/cli 1.22.1 → 1.22.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/dist/cli-g8qs2tj9.js +9777 -0
  2. package/dist/cli-pwbvtes8.js +2312 -0
  3. package/dist/cli-qszcpda3.js +284 -0
  4. package/dist/cli.js +4775 -1159
  5. package/dist/index-6h0nc1r5.js +2765 -0
  6. package/dist/provision-j5gq8cvx.js +72 -0
  7. package/package.json +3 -2
  8. package/skills/integration/index.json +2 -2
  9. package/dist/agent-schemas.d.ts +0 -3
  10. package/dist/agent-schemas.js +0 -358
  11. package/dist/agent.d.ts +0 -5
  12. package/dist/agent.js +0 -477
  13. package/dist/analyst-tui.d.ts +0 -81
  14. package/dist/analyst-tui.js +0 -314
  15. package/dist/api-client.d.ts +0 -46
  16. package/dist/api-client.js +0 -120
  17. package/dist/ask-stream.d.ts +0 -146
  18. package/dist/ask-stream.js +0 -473
  19. package/dist/auth-storage.d.ts +0 -35
  20. package/dist/auth-storage.js +0 -159
  21. package/dist/auth.d.ts +0 -50
  22. package/dist/auth.js +0 -694
  23. package/dist/cli-session.d.ts +0 -26
  24. package/dist/cli-session.js +0 -169
  25. package/dist/cli.d.ts +0 -44
  26. package/dist/command-context.d.ts +0 -62
  27. package/dist/command-context.js +0 -57
  28. package/dist/command-registry.d.ts +0 -22
  29. package/dist/command-registry.js +0 -15
  30. package/dist/config.d.ts +0 -60
  31. package/dist/config.js +0 -110
  32. package/dist/doctor.d.ts +0 -79
  33. package/dist/doctor.js +0 -1182
  34. package/dist/errors.d.ts +0 -50
  35. package/dist/errors.js +0 -64
  36. package/dist/exit-funnel.d.ts +0 -14
  37. package/dist/exit-funnel.js +0 -53
  38. package/dist/harness-artifact-bodies.d.ts +0 -28
  39. package/dist/harness-artifact-bodies.js +0 -276
  40. package/dist/harness-candidates.d.ts +0 -77
  41. package/dist/harness-candidates.js +0 -482
  42. package/dist/harness-remote.d.ts +0 -135
  43. package/dist/harness-remote.js +0 -540
  44. package/dist/harness-report.d.ts +0 -145
  45. package/dist/harness-report.js +0 -1579
  46. package/dist/harness-sync-limits.d.ts +0 -8
  47. package/dist/harness-sync-limits.js +0 -21
  48. package/dist/harness.d.ts +0 -463
  49. package/dist/harness.js +0 -4009
  50. package/dist/init/index.d.ts +0 -78
  51. package/dist/init/index.js +0 -1442
  52. package/dist/init/mcp.d.ts +0 -2
  53. package/dist/init/mcp.js +0 -27
  54. package/dist/init/progress-board.d.ts +0 -80
  55. package/dist/init/progress-board.js +0 -225
  56. package/dist/init/rules.d.ts +0 -6
  57. package/dist/init/rules.js +0 -171
  58. package/dist/init/sdk-catalog.d.ts +0 -10
  59. package/dist/init/sdk-catalog.js +0 -80
  60. package/dist/init/sdk-guide.d.ts +0 -19
  61. package/dist/init/sdk-guide.js +0 -185
  62. package/dist/init/sdk-integration.d.ts +0 -53
  63. package/dist/init/sdk-integration.js +0 -835
  64. package/dist/init/sdk-protocol.d.ts +0 -135
  65. package/dist/init/sdk-protocol.js +0 -152
  66. package/dist/init/sdk-verification.d.ts +0 -27
  67. package/dist/init/sdk-verification.js +0 -123
  68. package/dist/init/tenant.d.ts +0 -67
  69. package/dist/init/tenant.js +0 -421
  70. package/dist/intelligence-schema.d.ts +0 -349
  71. package/dist/intelligence-schema.js +0 -174
  72. package/dist/keys.d.ts +0 -47
  73. package/dist/keys.js +0 -136
  74. package/dist/manifest.d.ts +0 -5
  75. package/dist/manifest.js +0 -114
  76. package/dist/markdown-terminal.d.ts +0 -49
  77. package/dist/markdown-terminal.js +0 -212
  78. package/dist/output.d.ts +0 -191
  79. package/dist/output.js +0 -624
  80. package/dist/production-intelligence.d.ts +0 -175
  81. package/dist/production-intelligence.js +0 -1430
  82. package/dist/profile-commands.d.ts +0 -10
  83. package/dist/profile-commands.js +0 -342
  84. package/dist/profiles.d.ts +0 -112
  85. package/dist/profiles.js +0 -573
  86. package/dist/prompts-ab.d.ts +0 -71
  87. package/dist/prompts-ab.js +0 -313
  88. package/dist/prompts-propose.d.ts +0 -15
  89. package/dist/prompts-propose.js +0 -212
  90. package/dist/prompts.d.ts +0 -58
  91. package/dist/prompts.js +0 -418
  92. package/dist/provision.d.ts +0 -28
  93. package/dist/provision.js +0 -104
  94. package/dist/redact.d.ts +0 -2
  95. package/dist/redact.js +0 -90
  96. package/dist/schemas.d.ts +0 -225
  97. package/dist/schemas.js +0 -126
  98. package/dist/skills-bundled.d.ts +0 -35
  99. package/dist/skills-bundled.js +0 -157
  100. package/dist/skills-manifest.d.ts +0 -33
  101. package/dist/skills-manifest.js +0 -244
  102. package/dist/skills.d.ts +0 -57
  103. package/dist/skills.js +0 -783
  104. package/dist/sync.d.ts +0 -5
  105. package/dist/sync.js +0 -36
  106. package/dist/telemetry.d.ts +0 -56
  107. package/dist/telemetry.js +0 -167
  108. package/dist/transforms.d.ts +0 -25
  109. package/dist/transforms.js +0 -102
  110. package/dist/tui.d.ts +0 -48
  111. package/dist/tui.js +0 -393
  112. package/dist/update-check.d.ts +0 -31
  113. package/dist/update-check.js +0 -141
  114. package/dist/workspace.d.ts +0 -12
  115. 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
@@ -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