@monoes/monodesign 1.2.20 → 1.2.23

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 (95) hide show
  1. package/README.md +2 -2
  2. package/cli/engine/cli/main.mjs +21 -1
  3. package/cli/engine/design-system-checks.mjs +381 -0
  4. package/cli/engine/design-system-parse.mjs +433 -0
  5. package/cli/engine/design-system.mjs +20 -736
  6. package/cli/engine/detect-antipatterns-browser.js +28 -5
  7. package/cli/engine/detect-antipatterns.mjs +1 -0
  8. package/cli/engine/engines/regex/detect-text-matchers.mjs +371 -0
  9. package/cli/engine/engines/regex/detect-text-run.mjs +186 -0
  10. package/cli/engine/engines/regex/detect-text.mjs +11 -523
  11. package/cli/engine/engines/static-html/css-cascade-color.mjs +219 -0
  12. package/cli/engine/engines/static-html/css-cascade-static-data.mjs +138 -0
  13. package/cli/engine/engines/static-html/css-cascade-static-dom.mjs +132 -0
  14. package/cli/engine/engines/static-html/css-cascade-static-parse.mjs +393 -0
  15. package/cli/engine/engines/static-html/css-cascade.mjs +24 -837
  16. package/cli/engine/engines/static-html/detect-html.mjs +6 -1
  17. package/cli/engine/findings-group.mjs +94 -0
  18. package/cli/engine/fix/index.mjs +1 -26
  19. package/cli/engine/fix/unified-diff.mjs +107 -0
  20. package/cli/engine/rules/checks-background.mjs +17 -3
  21. package/cli/engine/rules/checks-element.mjs +2 -0
  22. package/cli/engine/rules/checks-pure.mjs +9 -2
  23. package/cli/lib/monodesign-config-consent.mjs +93 -0
  24. package/cli/lib/monodesign-config-core.mjs +37 -0
  25. package/cli/lib/monodesign-config-detection.mjs +133 -0
  26. package/cli/lib/monodesign-config-filter.mjs +198 -0
  27. package/cli/lib/monodesign-config-ignore-value.mjs +212 -0
  28. package/cli/lib/monodesign-config.mjs +21 -623
  29. package/package.json +2 -2
  30. package/skill/scripts/context-fields.mjs +65 -0
  31. package/skill/scripts/context-resolve.mjs +425 -0
  32. package/skill/scripts/context-update.mjs +151 -0
  33. package/skill/scripts/context-utils.mjs +68 -0
  34. package/skill/scripts/context-workspace.mjs +245 -0
  35. package/skill/scripts/context.mjs +17 -891
  36. package/skill/scripts/hook-admin-config.mjs +152 -0
  37. package/skill/scripts/hook-admin-ignore.mjs +131 -0
  38. package/skill/scripts/hook-admin-manifest.mjs +292 -0
  39. package/skill/scripts/hook-admin.mjs +8 -541
  40. package/skill/scripts/hook-lib-cache.mjs +145 -0
  41. package/skill/scripts/hook-lib-config.mjs +292 -0
  42. package/skill/scripts/hook-lib-detector.mjs +72 -0
  43. package/skill/scripts/hook-lib-events.mjs +182 -0
  44. package/skill/scripts/hook-lib-findings.mjs +201 -0
  45. package/skill/scripts/hook-lib-ignore.mjs +205 -0
  46. package/skill/scripts/hook-lib-render.mjs +273 -0
  47. package/skill/scripts/hook-lib-run.mjs +281 -0
  48. package/skill/scripts/hook-lib-scan-targets.mjs +128 -0
  49. package/skill/scripts/hook-lib.mjs +71 -1747
  50. package/skill/scripts/lib/design-parser-colors.mjs +182 -0
  51. package/skill/scripts/lib/design-parser-extract.mjs +333 -0
  52. package/skill/scripts/lib/design-parser-sections.mjs +214 -0
  53. package/skill/scripts/lib/design-parser-yaml.mjs +135 -0
  54. package/skill/scripts/lib/design-parser.mjs +10 -783
  55. package/skill/scripts/live/manual-apply-chunk.mjs +151 -0
  56. package/skill/scripts/live/manual-apply-compact.mjs +122 -0
  57. package/skill/scripts/live/manual-apply-evidence.mjs +115 -0
  58. package/skill/scripts/live/manual-apply-log.mjs +179 -0
  59. package/skill/scripts/live/manual-apply-transaction.mjs +122 -0
  60. package/skill/scripts/live/manual-apply.mjs +65 -640
  61. package/skill/scripts/live/svelte-component-accept.mjs +248 -0
  62. package/skill/scripts/live/svelte-component-css.mjs +230 -0
  63. package/skill/scripts/live/svelte-component-markup.mjs +104 -0
  64. package/skill/scripts/live/svelte-component-scaffold.mjs +118 -0
  65. package/skill/scripts/live/svelte-component-session.mjs +158 -0
  66. package/skill/scripts/live/svelte-component.mjs +32 -792
  67. package/skill/scripts/live-accept-mutate.mjs +152 -0
  68. package/skill/scripts/live-accept-parse.mjs +370 -0
  69. package/skill/scripts/live-accept-scrub.mjs +59 -0
  70. package/skill/scripts/live-accept-search.mjs +58 -0
  71. package/skill/scripts/live-accept.mjs +15 -631
  72. package/skill/scripts/live-commit-manual-edits-paths.mjs +27 -0
  73. package/skill/scripts/live-commit-manual-edits-repair.mjs +166 -0
  74. package/skill/scripts/live-commit-manual-edits-rollback.mjs +231 -0
  75. package/skill/scripts/live-commit-manual-edits-utils.mjs +167 -0
  76. package/skill/scripts/live-commit-manual-edits-verify.mjs +356 -0
  77. package/skill/scripts/live-commit-manual-edits.mjs +24 -881
  78. package/skill/scripts/live-copy-edit-agent.mjs +16 -551
  79. package/skill/scripts/live-copy-edit-compact.mjs +137 -0
  80. package/skill/scripts/live-copy-edit-post-apply.mjs +175 -0
  81. package/skill/scripts/live-copy-edit-runners.mjs +297 -0
  82. package/skill/scripts/live-inject-csp.mjs +132 -0
  83. package/skill/scripts/live-inject-files.mjs +229 -0
  84. package/skill/scripts/live-inject-tag.mjs +114 -0
  85. package/skill/scripts/live-inject.mjs +13 -440
  86. package/skill/scripts/live-server-poll.mjs +232 -0
  87. package/skill/scripts/live-server-routes.mjs +449 -0
  88. package/skill/scripts/live-server-state.mjs +361 -0
  89. package/skill/scripts/live-server.mjs +10 -946
  90. package/skill/scripts/live-wrap-css.mjs +132 -0
  91. package/skill/scripts/live-wrap-manual-edit.mjs +153 -0
  92. package/skill/scripts/live-wrap-search.mjs +247 -0
  93. package/skill/scripts/live-wrap.mjs +21 -484
  94. package/skill/scripts/palette-seeds.mjs +401 -0
  95. package/skill/scripts/palette.mjs +1 -395
@@ -0,0 +1,137 @@
1
+ // Compacting a staged copy-edit batch down to what's worth sending an AI
2
+ // runner, normalizing its reply shape, and the mock provider used in tests.
3
+ // Split out of live-copy-edit-agent.mjs (file-size sweep — pure move, no
4
+ // behaviour change).
5
+
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { truncate, isPathInsideOrEqual, tryParseJson } from './live-copy-edit-runners.mjs';
9
+ import { parseCopyEditBatchResult } from './live-copy-edit-agent.mjs';
10
+
11
+ function compactBatchForPrompt(batch) {
12
+ return {
13
+ pageUrl: batch?.pageUrl || null,
14
+ repair: batch?.repair || undefined,
15
+ entries: (batch?.entries || []).map((entry) => ({
16
+ id: entry.id,
17
+ pageUrl: entry.pageUrl,
18
+ stagedAt: entry.stagedAt || null,
19
+ element: compactContextForBatch(entry.element),
20
+ ops: (entry.ops || []).map(compactBatchOp),
21
+ })),
22
+ candidates: batch?.candidates || [],
23
+ };
24
+ }
25
+
26
+ function compactBatchOp(op) {
27
+ return {
28
+ entryId: op.entryId,
29
+ ref: op.ref,
30
+ contextRef: op.contextRef,
31
+ tag: op.tag,
32
+ elementId: op.elementId,
33
+ classes: op.classes,
34
+ originalText: op.originalText,
35
+ newText: op.newText,
36
+ deleted: op.deleted === true || undefined,
37
+ sourceHint: op.sourceHint,
38
+ leaf: compactContextForBatch(op.leaf),
39
+ nearbyEditableTexts: Array.isArray(op.nearbyEditableTexts) ? op.nearbyEditableTexts.slice(0, 8) : [],
40
+ container: compactContextForBatch(op.container),
41
+ contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 12) : [],
42
+ };
43
+ }
44
+
45
+ function compactContextForBatch(value) {
46
+ if (!value || typeof value !== 'object') return value || null;
47
+ return {
48
+ ref: value.ref,
49
+ tagName: value.tagName,
50
+ id: value.id,
51
+ classes: value.classes,
52
+ textContent: truncate(value.textContent, 900),
53
+ outerHTML: truncate(stripLiveRuntimeHtml(value.outerHTML), 1800),
54
+ };
55
+ }
56
+
57
+ function stripLiveRuntimeHtml(html) {
58
+ if (typeof html !== 'string') return html || null;
59
+ return html
60
+ .replace(/\sdata-monodesign-(?:original-text|editable|text-wrap)(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/g, '')
61
+ .replace(/\scontenteditable(?:=(?:"[^"]*"|'[^']*'|[^\s>]+))?/g, '')
62
+ .replace(/\sstyle=(["'])(?:(?!\1)[\s\S])*(?:-webkit-user-modify|user-select:\s*text|cursor:\s*text)(?:(?!\1)[\s\S])*\1/g, '');
63
+ }
64
+
65
+ function normalizeBatchResult(result) {
66
+ const status = result.status === 'partial' ? 'partial' : result.status === 'error' ? 'error' : 'done';
67
+ const appliedEntryIds = Array.isArray(result.appliedEntryIds)
68
+ ? result.appliedEntryIds.filter((id) => typeof id === 'string')
69
+ : [];
70
+ const failed = Array.isArray(result.failed)
71
+ ? result.failed.filter(Boolean).map((item) => ({
72
+ entryId: item.entryId || item.id || null,
73
+ reason: item.reason || item.message || 'failed',
74
+ candidates: Array.isArray(item.candidates) ? item.candidates : [],
75
+ }))
76
+ : [];
77
+ const files = Array.isArray(result.files) ? result.files.filter((file) => typeof file === 'string') : [];
78
+ const notes = Array.isArray(result.notes) ? result.notes.filter((note) => typeof note === 'string') : [];
79
+ const warnings = Array.isArray(result.warnings)
80
+ ? result.warnings
81
+ .filter(Boolean)
82
+ .map((warning) => typeof warning === 'string' ? { message: warning } : warning)
83
+ .filter((warning) => warning && typeof warning === 'object')
84
+ : [];
85
+ return {
86
+ status,
87
+ message: result.message || null,
88
+ appliedEntryIds,
89
+ failed,
90
+ files,
91
+ notes,
92
+ warnings,
93
+ };
94
+ }
95
+
96
+ function mockBatchResult(batch, env, cwd = process.cwd()) {
97
+ applyMockWrites(env, cwd);
98
+ const raw = env.MONODESIGN_LIVE_COPY_AGENT_MOCK_RESULT;
99
+ if (raw) {
100
+ const parsed = parseCopyEditBatchResult(raw);
101
+ if (parsed) return parsed;
102
+ throw new Error('Invalid MONODESIGN_LIVE_COPY_AGENT_MOCK_RESULT JSON');
103
+ }
104
+ return {
105
+ status: 'done',
106
+ appliedEntryIds: (batch?.entries || []).map((entry) => entry.id).filter(Boolean),
107
+ failed: [],
108
+ files: [],
109
+ notes: ['mock copy-edit batch result'],
110
+ };
111
+ }
112
+
113
+ function applyMockWrites(env, cwd) {
114
+ const raw = env.MONODESIGN_LIVE_COPY_AGENT_MOCK_WRITES;
115
+ if (!raw) return;
116
+ const writes = tryParseJson(raw);
117
+ if (!writes || typeof writes !== 'object' || Array.isArray(writes)) {
118
+ throw new Error('Invalid MONODESIGN_LIVE_COPY_AGENT_MOCK_WRITES JSON');
119
+ }
120
+ for (const [relativeFile, content] of Object.entries(writes)) {
121
+ if (typeof relativeFile !== 'string' || typeof content !== 'string') continue;
122
+ const absolute = path.resolve(cwd, relativeFile);
123
+ if (!isPathInsideOrEqual(cwd, absolute)) continue;
124
+ fs.mkdirSync(path.dirname(absolute), { recursive: true });
125
+ fs.writeFileSync(absolute, content, 'utf-8');
126
+ }
127
+ }
128
+
129
+ export {
130
+ compactBatchForPrompt,
131
+ compactBatchOp,
132
+ compactContextForBatch,
133
+ stripLiveRuntimeHtml,
134
+ normalizeBatchResult,
135
+ mockBatchResult,
136
+ applyMockWrites,
137
+ };
@@ -0,0 +1,175 @@
1
+ // Post-apply verification: JSON/JS syntax checks, JSX/TSX parse checks,
2
+ // leftover monodesign marker detection, and the project's own
3
+ // manual-edit-validate script. Split out of live-copy-edit-agent.mjs
4
+ // (file-size sweep — pure move, no behaviour change).
5
+
6
+ import { spawnSync } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { createRequire } from 'node:module';
10
+ import { isPathInsideOrEqual } from './live-copy-edit-runners.mjs';
11
+
12
+ const require = createRequire(import.meta.url);
13
+
14
+ export function runCopyEditPostApplyChecks({ cwd = process.cwd(), files = [] } = {}) {
15
+ const failures = [];
16
+ const warnings = [];
17
+ const uniqueFiles = [...new Set((files || []).filter((file) => typeof file === 'string' && file.trim()))];
18
+ for (const relativeFile of uniqueFiles) {
19
+ const file = path.resolve(cwd, relativeFile);
20
+ if (!isPathInsideOrEqual(cwd, file) || !fs.existsSync(file)) {
21
+ warnings.push({ file: relativeFile, reason: 'file_missing_or_outside_cwd' });
22
+ continue;
23
+ }
24
+ let content = '';
25
+ try { content = fs.readFileSync(file, 'utf-8'); } catch (err) {
26
+ failures.push({ file: relativeFile, reason: 'read_failed', message: err.message });
27
+ continue;
28
+ }
29
+ const markerMatch = findLeftoverMonodesignMarker(content);
30
+ if (markerMatch) failures.push({ file: relativeFile, reason: 'leftover_monodesign_marker', marker: markerMatch });
31
+ if (/\.json$/.test(relativeFile)) {
32
+ try {
33
+ JSON.parse(content);
34
+ } catch (err) {
35
+ failures.push({
36
+ file: relativeFile,
37
+ reason: 'invalid_json',
38
+ message: err.message || String(err),
39
+ });
40
+ }
41
+ }
42
+ const syntaxCheck = checkFrameworkSourceSyntax(relativeFile, content);
43
+ if (syntaxCheck?.failure) failures.push(syntaxCheck.failure);
44
+ if (syntaxCheck?.warning) warnings.push(syntaxCheck.warning);
45
+ if (/\.(mjs|cjs|js)$/.test(relativeFile)) {
46
+ const check = spawnSync(process.execPath, ['--check', file], { cwd, encoding: 'utf-8' });
47
+ if (check.status !== 0) {
48
+ failures.push({
49
+ file: relativeFile,
50
+ reason: 'invalid_js',
51
+ message: (check.stderr || check.stdout || '').trim(),
52
+ });
53
+ }
54
+ }
55
+ }
56
+ const validation = runManualEditValidationScript(cwd);
57
+ if (validation?.failure) failures.push(validation.failure);
58
+ if (validation?.warning) warnings.push(validation.warning);
59
+ return { ok: failures.length === 0, failures, warnings };
60
+ }
61
+
62
+ function checkFrameworkSourceSyntax(relativeFile, content) {
63
+ if (!/\.(jsx|tsx|ts)$/.test(relativeFile)) return null;
64
+ let parser;
65
+ try {
66
+ parser = require('@babel/parser');
67
+ } catch {
68
+ return { warning: { file: relativeFile, reason: 'syntax_parser_unavailable' } };
69
+ }
70
+ const plugins = ['jsx'];
71
+ if (/\.(ts|tsx)$/.test(relativeFile)) plugins.push('typescript');
72
+ try {
73
+ parser.parse(content, {
74
+ sourceType: 'module',
75
+ plugins,
76
+ errorRecovery: false,
77
+ });
78
+ return null;
79
+ } catch (err) {
80
+ return {
81
+ failure: {
82
+ file: relativeFile,
83
+ reason: 'invalid_source_syntax',
84
+ message: err.message || String(err),
85
+ },
86
+ };
87
+ }
88
+ }
89
+
90
+ function findLeftoverMonodesignMarker(content) {
91
+ const commentMarker = content.match(/^\s*(?:<!--|\{\/\*)\s*monodesign-carbonize-(?:start|end)\b|^\s*(?:<!--|\{\/\*)\s*monodesign-variants-(?:start|end)\b/m);
92
+ if (commentMarker) return commentMarker[0];
93
+
94
+ const attrPattern = /\bdata-monodesign-(?:variants?|original-text|editable|text-wrap)\s*=/g;
95
+ for (const line of content.split(/\r?\n/)) {
96
+ attrPattern.lastIndex = 0;
97
+ let match;
98
+ while ((match = attrPattern.exec(line))) {
99
+ if (!isInsideQuotedLiteral(line, match.index)) return match[0];
100
+ }
101
+ }
102
+ return null;
103
+ }
104
+
105
+ function isInsideQuotedLiteral(line, index) {
106
+ let quote = null;
107
+ let escaped = false;
108
+ for (let i = 0; i < index; i++) {
109
+ const ch = line[i];
110
+ if (escaped) {
111
+ escaped = false;
112
+ continue;
113
+ }
114
+ if (ch === '\\') {
115
+ escaped = true;
116
+ continue;
117
+ }
118
+ if (quote) {
119
+ if (ch === quote) quote = null;
120
+ continue;
121
+ }
122
+ if (ch === '"' || ch === "'" || ch === '`') quote = ch;
123
+ }
124
+ return quote !== null;
125
+ }
126
+
127
+ function runManualEditValidationScript(cwd) {
128
+ const script = readManualEditValidationScript(cwd);
129
+ if (!script) return null;
130
+ const validation = spawnSync(script, {
131
+ cwd,
132
+ encoding: 'utf-8',
133
+ shell: true,
134
+ timeout: 30_000,
135
+ });
136
+ if (validation.error) {
137
+ return {
138
+ failure: {
139
+ file: 'package.json',
140
+ reason: 'manual_edit_validation_failed',
141
+ message: validation.error.message || String(validation.error),
142
+ },
143
+ };
144
+ }
145
+ if (validation.status !== 0) {
146
+ return {
147
+ failure: {
148
+ file: 'package.json',
149
+ reason: 'manual_edit_validation_failed',
150
+ message: [validation.stderr, validation.stdout].filter(Boolean).join('\n').trim(),
151
+ },
152
+ };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ function readManualEditValidationScript(cwd) {
158
+ const pkgPath = path.join(cwd, 'package.json');
159
+ if (!fs.existsSync(pkgPath)) return null;
160
+ try {
161
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
162
+ const script = pkg?.scripts?.['monodesign:manual-edit-validate'];
163
+ return typeof script === 'string' && script.trim() ? script : null;
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ export {
170
+ checkFrameworkSourceSyntax,
171
+ findLeftoverMonodesignMarker,
172
+ isInsideQuotedLiteral,
173
+ runManualEditValidationScript,
174
+ readManualEditValidationScript,
175
+ };
@@ -0,0 +1,297 @@
1
+ // Running the Codex/Claude CLI as a child process to apply a copy-edit
2
+ // batch, plus the provider-selection and error-message helpers around it.
3
+ // Split out of live-copy-edit-agent.mjs (file-size sweep — pure move, no
4
+ // behaviour change).
5
+
6
+ import { spawn, spawnSync } from 'node:child_process';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+
10
+ const DEFAULT_TIMEOUT_MS = 60_000;
11
+
12
+ export function parseCopyEditAgentResult(text) {
13
+ const trimmed = String(text || '').trim();
14
+ if (!trimmed) return null;
15
+
16
+ const parsedOuter = tryParseJson(trimmed);
17
+ if (parsedOuter) {
18
+ if (typeof parsedOuter.result === 'string') {
19
+ const nested = parseCopyEditAgentResult(parsedOuter.result);
20
+ if (nested) return nested;
21
+ }
22
+ if (parsedOuter.status === 'done' || parsedOuter.status === 'partial' || parsedOuter.status === 'error') return parsedOuter;
23
+ }
24
+
25
+ const jsonMatch = trimmed.match(/\{[\s\S]*\}/);
26
+ if (!jsonMatch) return null;
27
+ const parsed = tryParseJson(jsonMatch[0]);
28
+ if (parsed?.status === 'done' || parsed?.status === 'partial' || parsed?.status === 'error') return parsed;
29
+ return null;
30
+ }
31
+
32
+ export function chooseCopyEditAgent({
33
+ env = process.env,
34
+ authCheck = commandAuthed,
35
+ chatAvailable = () => false,
36
+ } = {}) {
37
+ const mode = (env.MONODESIGN_LIVE_COPY_AGENT || 'auto').trim().toLowerCase();
38
+ if (mode === '0' || mode === 'false' || mode === 'off' || mode === 'none') return null;
39
+ if (mode === 'mock') return 'mock';
40
+ if (mode === 'chat') return chatAvailable() ? 'chat' : null;
41
+ if (mode === 'codex') return commandExists('codex') ? 'codex' : null;
42
+ if (mode === 'claude') return commandExists('claude') ? 'claude' : null;
43
+ if (mode !== 'auto') return null;
44
+ if (authCheck('codex')) return 'codex';
45
+ if (authCheck('claude')) return 'claude';
46
+ if (chatAvailable()) return 'chat';
47
+ return null;
48
+ }
49
+
50
+ function runCodex(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_TIMEOUT_MS }) {
51
+ const args = [
52
+ 'exec',
53
+ '--cd', cwd,
54
+ '--dangerously-bypass-approvals-and-sandbox',
55
+ '--ephemeral',
56
+ '--output-last-message', resultPath,
57
+ '-c', `model_reasoning_effort="${env.MONODESIGN_LIVE_COPY_AGENT_EFFORT || 'low'}"`,
58
+ ];
59
+ if (env.MONODESIGN_LIVE_COPY_AGENT_MODEL) {
60
+ args.push('--model', env.MONODESIGN_LIVE_COPY_AGENT_MODEL);
61
+ }
62
+ args.push('-');
63
+ return runAgentProcess('codex', args, prompt, { cwd, env, logPath, timeoutMs });
64
+ }
65
+
66
+ function runClaude(prompt, { cwd, env, resultPath, logPath, timeoutMs = DEFAULT_TIMEOUT_MS }) {
67
+ const args = [
68
+ '--print',
69
+ '--permission-mode', 'bypassPermissions',
70
+ '--output-format', 'json',
71
+ ];
72
+ if (env.MONODESIGN_LIVE_COPY_AGENT_MODEL) {
73
+ args.push('--model', env.MONODESIGN_LIVE_COPY_AGENT_MODEL);
74
+ }
75
+ args.push(prompt);
76
+ // Forward env as-is so CLAUDE_CODE_OAUTH_TOKEN and ANTHROPIC_API_KEY flow
77
+ // through. On macOS, `claude /login` stores creds in the Keychain, which a
78
+ // non-TTY subprocess cannot read; setting CLAUDE_CODE_OAUTH_TOKEN (via
79
+ // `claude setup-token`) is the supported headless auth path.
80
+ return runAgentProcess('claude', args, '', { cwd, env, logPath, timeoutMs, mirrorOutputPath: resultPath });
81
+ }
82
+
83
+ function runAgentProcess(command, args, stdin, { cwd, env, logPath, timeoutMs, mirrorOutputPath }) {
84
+ return new Promise((resolve, reject) => {
85
+ const log = fs.createWriteStream(logPath, { flags: 'a' });
86
+ const child = spawn(command, args, {
87
+ cwd,
88
+ env,
89
+ stdio: ['pipe', 'pipe', 'pipe'],
90
+ });
91
+ let output = '';
92
+ let settled = false;
93
+ const timer = setTimeout(() => {
94
+ child.kill('SIGTERM');
95
+ rejectOnce(new Error(`AI copy-edit worker timed out after ${timeoutMs}ms`));
96
+ }, timeoutMs);
97
+
98
+ const rejectOnce = (err) => {
99
+ if (settled) return;
100
+ settled = true;
101
+ clearTimeout(timer);
102
+ log.end();
103
+ reject(err);
104
+ };
105
+ const resolveOnce = () => {
106
+ if (settled) return;
107
+ settled = true;
108
+ clearTimeout(timer);
109
+ if (mirrorOutputPath) fs.writeFileSync(mirrorOutputPath, output);
110
+ log.end();
111
+ resolve();
112
+ };
113
+
114
+ process.once('SIGTERM', () => {
115
+ try { child.kill('SIGTERM'); } catch {}
116
+ });
117
+ child.stdout.on('data', (chunk) => {
118
+ output += chunk.toString();
119
+ log.write(chunk);
120
+ });
121
+ child.stderr.on('data', (chunk) => {
122
+ log.write(chunk);
123
+ });
124
+ child.on('error', rejectOnce);
125
+ child.on('exit', (code, signal) => {
126
+ if (code === 0) {
127
+ resolveOnce();
128
+ } else {
129
+ const hint = extractRunnerErrorMessage(output, command);
130
+ rejectOnce(new Error(hint || `${command} exited with ${signal || code}`));
131
+ }
132
+ });
133
+ if (stdin) child.stdin.end(stdin);
134
+ else child.stdin.end();
135
+ });
136
+ }
137
+
138
+ function isPathInsideOrEqual(cwd, file) {
139
+ const relative = path.relative(path.resolve(cwd), path.resolve(file)).split(path.sep).join('/');
140
+ return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
141
+ }
142
+
143
+ function tryParseJson(text) {
144
+ try { return JSON.parse(text); } catch { return null; }
145
+ }
146
+
147
+ function truncate(value, max) {
148
+ if (typeof value !== 'string') return value;
149
+ if (value.length <= max) return value;
150
+ return `${value.slice(0, max)}... [truncated ${value.length - max} chars]`;
151
+ }
152
+
153
+ function commandExists(command) {
154
+ const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
155
+ return !result.error && result.status === 0;
156
+ }
157
+
158
+ /**
159
+ * Build a diagnostic error message explaining why no AI runner is usable.
160
+ * Splits the previous "Install/authenticate Codex or Claude" lump into a
161
+ * per-provider summary so the user knows exactly which step unblocks them.
162
+ */
163
+ export function describeNoProviderError({
164
+ exists = commandExists,
165
+ chatAvailable = () => false,
166
+ env = process.env,
167
+ } = {}) {
168
+ const lines = ['No live copy-edit AI runner is available.'];
169
+ if (exists('claude')) {
170
+ if (env.CLAUDE_CODE_OAUTH_TOKEN) {
171
+ lines.push(' • Claude CLI: installed; CLAUDE_CODE_OAUTH_TOKEN is set but the CLI still rejected it. The token may be expired or invalid.');
172
+ } else {
173
+ lines.push(' • Claude CLI: installed but not selected. If Apply still fails, the subprocess may be unable to read your `claude /login` credentials (on macOS, the Keychain can be unreachable from a no-TTY child).');
174
+ lines.push(' Headless fix: run `claude setup-token` once, then `export CLAUDE_CODE_OAUTH_TOKEN=<the printed sk-ant-oat01-… token>` before starting `live-server.mjs`.');
175
+ lines.push(' Alternative: `export ANTHROPIC_API_KEY=<key>` if you have console.anthropic.com credits.');
176
+ }
177
+ } else {
178
+ lines.push(' • Claude CLI: not installed.');
179
+ }
180
+ if (exists('codex')) {
181
+ lines.push(' • Codex CLI: installed. If Apply still fails, run `codex login` to authenticate.');
182
+ } else {
183
+ lines.push(' • Codex CLI: not installed.');
184
+ }
185
+ if (chatAvailable()) {
186
+ lines.push(' • Chat: an Monodesign live session is polling but selection chose another provider — unexpected; please report.');
187
+ } else {
188
+ lines.push(' • Chat: no Monodesign live session is currently polling on this server. Start Monodesign live in your chat to route Apply through the chat agent.');
189
+ }
190
+ lines.push('Fix one of the above, or set MONODESIGN_LIVE_COPY_AGENT=mock for tests.');
191
+ return lines.join('\n');
192
+ }
193
+
194
+ /**
195
+ * Pull a human-readable failure reason out of a subprocess's stdout when the
196
+ * process exited non-zero. Recognizes:
197
+ * - Claude CLI `--output-format json` errors:
198
+ * {"is_error": true, "result": "Not logged in · Please run /login", ...}
199
+ * - Generic JSON payloads with `message` or `error` strings.
200
+ * - The last non-empty line of unstructured output.
201
+ * Returns null when nothing meaningful surfaces, so the caller can fall back
202
+ * to its existing "X exited with N" message.
203
+ */
204
+ export function extractRunnerErrorMessage(output, command) {
205
+ const text = String(output || '').trim();
206
+ if (!text) return null;
207
+ const candidates = [];
208
+ const direct = tryParseJson(text);
209
+ if (direct) candidates.push(direct);
210
+ const trailingMatch = text.match(/\{[\s\S]*\}\s*$/);
211
+ if (trailingMatch) {
212
+ const tail = tryParseJson(trailingMatch[0]);
213
+ if (tail && tail !== direct) candidates.push(tail);
214
+ }
215
+ for (const parsed of candidates) {
216
+ if (!parsed || typeof parsed !== 'object') continue;
217
+ if (parsed.is_error === true && typeof parsed.result === 'string' && parsed.result.trim()) {
218
+ return `${command} CLI: ${parsed.result.trim()}`;
219
+ }
220
+ if (typeof parsed.message === 'string' && parsed.message.trim()) {
221
+ return `${command} CLI: ${parsed.message.trim()}`;
222
+ }
223
+ if (typeof parsed.error === 'string' && parsed.error.trim()) {
224
+ return `${command} CLI: ${parsed.error.trim()}`;
225
+ }
226
+ }
227
+ const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
228
+ if (lines.length > 0) {
229
+ const last = lines[lines.length - 1];
230
+ if (last.length > 0 && last.length < 400) return `${command}: ${last}`;
231
+ }
232
+ return null;
233
+ }
234
+
235
+ /**
236
+ * Pre-flight a CLI provider with a trivial prompt and report whether it can
237
+ * actually do work. Cached per process so the `auto` branch of
238
+ * chooseCopyEditAgent only pays the cost once per server boot.
239
+ *
240
+ * For claude we run the same `--print --output-format json` invocation we use
241
+ * for real batches; an unauthenticated CLI fails in ~36 ms with
242
+ * { is_error: true, result: "Not logged in · ..." }.
243
+ * For codex we only confirm the binary exists — `codex exec` always burns a
244
+ * real LLM call, so checking auth without spending tokens is not possible
245
+ * here; if the user has codex installed but unauthed, the runtime error from
246
+ * runCodex (now improved by extractRunnerErrorMessage) will surface clearly.
247
+ */
248
+ const COMMAND_AUTH_CACHE = new Map();
249
+
250
+ function commandAuthed(command) {
251
+ if (COMMAND_AUTH_CACHE.has(command)) return COMMAND_AUTH_CACHE.get(command);
252
+ const ok = computeCommandAuthed(command);
253
+ COMMAND_AUTH_CACHE.set(command, ok);
254
+ return ok;
255
+ }
256
+
257
+ function computeCommandAuthed(command) {
258
+ if (!commandExists(command)) return false;
259
+ if (command === 'codex') return true;
260
+ if (command !== 'claude') return false;
261
+ let result;
262
+ try {
263
+ result = spawnSync('claude', [
264
+ '--print',
265
+ '--output-format', 'json',
266
+ 'ping',
267
+ ], {
268
+ encoding: 'utf-8',
269
+ timeout: 10000,
270
+ env: process.env,
271
+ });
272
+ } catch {
273
+ return false;
274
+ }
275
+ if (result.error || result.signal) return false;
276
+ const stdout = String(result.stdout || '').trim();
277
+ if (result.status !== 0) {
278
+ // Non-zero exit: probably an auth or config error. Definitely not usable.
279
+ return false;
280
+ }
281
+ if (!stdout) return true;
282
+ const parsed = tryParseJson(stdout) || tryParseJson(stdout.match(/\{[\s\S]*\}\s*$/)?.[0] || '');
283
+ if (parsed && parsed.is_error === true) return false;
284
+ return true;
285
+ }
286
+
287
+ export {
288
+ runCodex,
289
+ runClaude,
290
+ runAgentProcess,
291
+ isPathInsideOrEqual,
292
+ tryParseJson,
293
+ truncate,
294
+ commandExists,
295
+ commandAuthed,
296
+ computeCommandAuthed,
297
+ };