atris 3.24.0 → 3.25.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/bin/atris.js CHANGED
@@ -361,12 +361,6 @@ function showHelp() {
361
361
  console.log(' plan - Create build spec with visualization');
362
362
  console.log(' do - Execute tasks');
363
363
  console.log(' review - Validate work (tests, safety checks, docs)');
364
- console.log(' slop - Detect frontend AI-slop tells (deterministic, exit 1 = found)');
365
- console.log(' deck - Generate a premium Google Slides deck from a content spec');
366
- console.log(' site - Build a beautiful static site from a folder of markdown');
367
- console.log(' theme - Brand themes: "theme create" builds your own by feel (deck/html/site)');
368
- console.log(' card - One line of text into an on-brand image (uses your theme)');
369
- console.log(' reel - One line of text into a short on-brand video (animated card)');
370
364
  console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
371
365
  console.log(' run logs - Browse glass run logs (phase reasoning persisted to disk)');
372
366
  console.log(' run search - Search phase reasoning across all run logs');
@@ -380,7 +374,7 @@ function showHelp() {
380
374
  console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
381
375
  console.log(' ctop - Show a process-first live agent CPU/memory view');
382
376
  console.log(' status - See local work and completions (`atris status <business>` for remote)');
383
- console.log(' recap - What your AI team did, in plain English (--share, or --html for a memory page)');
377
+ console.log(' recap - What your AI team did, in plain English (--share for paste-ready)');
384
378
  console.log(' xp - Show Career XP and contribution graph');
385
379
  console.log(' analytics - Show recent productivity from journals');
386
380
  console.log(' search - Search journal history (atris search <keyword>)');
@@ -832,7 +826,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
832
826
  const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
833
827
  'activate', '_activate', 'agent', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
834
828
  'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
835
- 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
829
+ 'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
836
830
  'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
837
831
  'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship'];
838
832
 
@@ -1346,36 +1340,6 @@ if (command === 'init') {
1346
1340
  Promise.resolve(require('../commands/codex-goal').codexGoalCommand(process.argv.slice(3)))
1347
1341
  .then(() => process.exit(process.exitCode || 0))
1348
1342
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1349
- } else if (command === 'slop') {
1350
- // Slop: deterministic frontend-slop detector (no LLM). Exit 1 = slop found, for CI + the autopilot gate.
1351
- Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
1352
- .then((code) => process.exit(typeof code === 'number' ? code : 0))
1353
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1354
- } else if (command === 'deck') {
1355
- // Deck: premium Google Slides from a plain content spec, via the Atris deck engine (anti-slop design system).
1356
- Promise.resolve(require('../commands/deck').run(process.argv.slice(3)))
1357
- .then((code) => process.exit(typeof code === 'number' ? code : 0))
1358
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1359
- } else if (command === 'site') {
1360
- // Site: beautiful static site from a folder of markdown, in the anti-slop design system.
1361
- Promise.resolve(require('../commands/site').run(process.argv.slice(3)))
1362
- .then((code) => process.exit(typeof code === 'number' ? code : 0))
1363
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1364
- } else if (command === 'theme') {
1365
- // Theme: brand themes (.atris/theme.json) for the whole design system (deck/html/site).
1366
- Promise.resolve(require('../commands/theme').run(process.argv.slice(3)))
1367
- .then((code) => process.exit(typeof code === 'number' ? code : 0))
1368
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1369
- } else if (command === 'card') {
1370
- // Card: one line of text into an on-brand image (uses your theme + the design system).
1371
- Promise.resolve(require('../commands/card').run(process.argv.slice(3)))
1372
- .then((code) => process.exit(typeof code === 'number' ? code : 0))
1373
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1374
- } else if (command === 'reel') {
1375
- // Reel: one line of text into a short on-brand video (an animated card; frames via Chrome + ffmpeg).
1376
- Promise.resolve(require('../commands/reel').run(process.argv.slice(3)))
1377
- .then((code) => process.exit(typeof code === 'number' ? code : 0))
1378
- .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
1379
1343
  } else if (command === 'aeo') {
1380
1344
  // AEO: AI Engine Optimization — credit-metered citation drafting against the customer workspace.
1381
1345
  Promise.resolve(require('../commands/aeo').run(process.argv.slice(3)))
package/commands/aeo.js CHANGED
@@ -5,17 +5,17 @@
5
5
  * atris aeo init # create entity-graph skeleton
6
6
  * atris aeo draft "<topic>" [opts] # generate citation-optimized article
7
7
  *
8
- * Local-read against ~/arena/atrisos-backend/atris/features/aeo/proof/:
8
+ * Local-read against a developer-provided AEO workspace:
9
9
  * atris aeo log [--engine X] [--limit N] # citation attempt log
10
10
  * atris aeo status # engine + proof + buyer summary
11
11
  * atris aeo packet <slug> # buyer packet for a surface
12
12
  * atris aeo proofs [--filter X] # list proof receipt categories
13
13
  *
14
- * Shell out to atrisos-backend/scripts/aeo_*.py:
14
+ * Shell out to the configured workspace scripts/aeo_*.py:
15
15
  * atris aeo discover <source> [...] # discovery audit
16
16
  * atris aeo audit <source> [...] # agent-usability audit
17
17
  *
18
- * Backend root resolution: $ATRIS_BACKEND_ROOT or ~/arena/atrisos-backend.
18
+ * Local workspace resolution: $ATRIS_AEO_ROOT or legacy $ATRIS_BACKEND_ROOT.
19
19
  */
20
20
 
21
21
  const fs = require('fs');
@@ -28,8 +28,8 @@ const { loadBusinesses, saveBusinesses } = require('./business');
28
28
 
29
29
  function resolveBackendRoot() {
30
30
  const candidates = [
31
+ process.env.ATRIS_AEO_ROOT,
31
32
  process.env.ATRIS_BACKEND_ROOT,
32
- path.join(os.homedir(), 'arena', 'atrisos-backend'),
33
33
  ].filter(Boolean);
34
34
  for (const root of candidates) {
35
35
  if (fs.existsSync(path.join(root, 'atris', 'features', 'aeo'))) return root;
@@ -40,7 +40,7 @@ function resolveBackendRoot() {
40
40
  function requireBackendRoot() {
41
41
  const root = resolveBackendRoot();
42
42
  if (!root) {
43
- console.error('Cannot find atrisos-backend. Set $ATRIS_BACKEND_ROOT or clone to ~/arena/atrisos-backend.');
43
+ console.error('Cannot find local AEO workspace. Set ATRIS_AEO_ROOT to a workspace containing atris/features/aeo.');
44
44
  process.exit(1);
45
45
  }
46
46
  return root;
@@ -664,7 +664,6 @@ function findAtrisCodeTerminal() {
664
664
  envPath,
665
665
  path.join(__dirname, '..', 'cli', 'atris_code.py'),
666
666
  path.join(process.cwd(), 'cli', 'atris_code.py'),
667
- path.join(os.homedir(), 'arena', 'atrisos-backend', 'cli', 'atris_code.py'),
668
667
  ].filter(Boolean);
669
668
 
670
669
  let dir = process.cwd();
@@ -1627,8 +1627,9 @@ async function runMission(args) {
1627
1627
  process.exit(0);
1628
1628
  }
1629
1629
 
1630
+ const preLockRunner = String(mission.runner || '').trim().toLowerCase();
1630
1631
  const preLockCallerSession = runnerUsesCallerSession(mission.runner);
1631
- if (!skipClaude && !preLockCallerSession) {
1632
+ if (!skipClaude && !preLockCallerSession && preLockRunner !== 'atris2') {
1632
1633
  const probe = probeClaudeBinary();
1633
1634
  if (!probe.ok) {
1634
1635
  console.error(`[mission run] claude probe failed: ${probe.error}`);
package/commands/recap.js CHANGED
@@ -220,22 +220,6 @@ function recapAtris(args = []) {
220
220
  printRecapHelp();
221
221
  return;
222
222
  }
223
- if (args.includes('--html')) {
224
- // another way to view memory updates: a beautiful HTML page of what the workspace learned
225
- const { buildMemorySpec } = require('../lib/memory-view');
226
- const { renderHtml, renderBlock, THEMES: HTML_THEMES } = require('../lib/html-render');
227
- const { mergedThemes } = require('../lib/theme');
228
- const themes = mergedThemes(HTML_THEMES);
229
- const flagVal = (n) => { const i = args.indexOf(n); return i !== -1 ? args[i + 1] : null; };
230
- const spec = buildMemorySpec(process.cwd(), { theme: flagVal('--theme'), brand: flagVal('--brand') });
231
- if (!themes[spec.theme]) spec.theme = 'atris';
232
- if (args.includes('--block')) { console.log(JSON.stringify(renderBlock(spec, { title: 'Workspace memory', themes }), null, 2)); return; }
233
- const html = renderHtml(spec, { title: 'Workspace memory', themes });
234
- const out = flagVal('--out');
235
- if (out) { fs.writeFileSync(out, html); console.log(`\n ✓ memory view written: ${out}\n`); }
236
- else process.stdout.write(html + '\n');
237
- return;
238
- }
239
223
  const daysIdx = args.indexOf('--days');
240
224
  const days = daysIdx !== -1 ? Number(args[daysIdx + 1]) : DEFAULT_DAYS;
241
225
  const data = buildRecapData(process.cwd(), { days });
package/commands/sync.js CHANGED
@@ -463,6 +463,7 @@ function syncAtris() {
463
463
 
464
464
  const filesToSync = [
465
465
  { source: 'atris.md', target: 'atris.md' },
466
+ { source: 'atris/atrisDev.md', target: 'atrisDev.md' },
466
467
  { source: 'PERSONA.md', target: 'PERSONA.md' },
467
468
  { source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
468
469
  { source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
@@ -807,6 +808,7 @@ function _findAtrisProjects(rootDir, maxDepth = 8) {
807
808
  // Canonical files shipped from the package root. Must match syncAtris's filesToSync.
808
809
  const SYNC_ALL_FILES = [
809
810
  { source: 'atris.md', target: 'atris.md' },
811
+ { source: 'atris/atrisDev.md', target: 'atrisDev.md' },
810
812
  { source: 'PERSONA.md', target: 'PERSONA.md' },
811
813
  { source: 'GETTING_STARTED.md', target: 'GETTING_STARTED.md' },
812
814
  { source: 'atris/CLAUDE.md', target: 'CLAUDE.md' },
@@ -1626,8 +1626,7 @@ async function executeAgentSDKFast(userInput) {
1626
1626
  } catch (error) {
1627
1627
  console.error(`✗ Error: ${error.message}`);
1628
1628
  console.log('');
1629
- console.log('💡 Make sure the AtrisOS backend is running on port 8000');
1630
- console.log(' Start it with: cd /Users/keshavrao/arena/atrisos-backend/backend && python -m uvicorn main:app --reload --port 8000');
1629
+ console.log('💡 Hosted turns use https://api.atris.ai. For local development, set ATRIS_API_BASE to your own backend URL.');
1631
1630
  process.exit(1);
1632
1631
  }
1633
1632
  }
@@ -0,0 +1,183 @@
1
+ const { apiRequestJson } = require('../utils/api');
2
+ const { ensureValidCredentials } = require('../utils/auth');
3
+
4
+ const DEFAULT_QUERY = 'Extract main topics, key insights, and actionable takeaways.';
5
+ const DEFAULT_TIMEOUT_MS = 300000;
6
+
7
+ function showYoutubeHelp(output = console.log, commandName = 'atris youtube') {
8
+ output('');
9
+ output(`Usage: ${commandName} process <youtube-url> [options]`);
10
+ output(` ${commandName} <youtube-url> [options]`);
11
+ output('');
12
+ output('Process a YouTube video through Atris using Gemini native video analysis.');
13
+ output('');
14
+ output('Options:');
15
+ output(' --query, -q <text> Focus question for the analysis');
16
+ output(' --agent <id> Agent id to store knowledge against');
17
+ output(' --store Save as agent knowledge (requires --agent)');
18
+ output(' --timeout <sec> Request timeout in seconds (default: 300)');
19
+ output(' --json Print the raw JSON response');
20
+ output(' -h, --help This help');
21
+ output('');
22
+ output('Examples:');
23
+ output(` ${commandName} https://www.youtube.com/watch?v=VIDEO_ID`);
24
+ output(` ${commandName} process https://youtu.be/VIDEO_ID --query "Key takeaways"`);
25
+ output('');
26
+ }
27
+
28
+ function readValue(args, index, name) {
29
+ if (index >= args.length - 1 || String(args[index + 1]).startsWith('--')) {
30
+ throw new Error(`${name} requires a value`);
31
+ }
32
+ return args[index + 1];
33
+ }
34
+
35
+ function parseTimeoutMs(raw) {
36
+ const seconds = Number(raw);
37
+ if (!Number.isFinite(seconds) || seconds <= 0) {
38
+ throw new Error('--timeout must be a positive number of seconds');
39
+ }
40
+ return Math.round(seconds * 1000);
41
+ }
42
+
43
+ function parseYoutubeArgs(argv = []) {
44
+ const args = [...argv];
45
+ const options = {
46
+ help: false,
47
+ json: false,
48
+ youtubeUrl: null,
49
+ query: DEFAULT_QUERY,
50
+ agentId: null,
51
+ storeAsKnowledge: false,
52
+ timeoutMs: DEFAULT_TIMEOUT_MS,
53
+ };
54
+
55
+ if (args.length === 0 || ['help', '--help', '-h'].includes(args[0])) {
56
+ options.help = true;
57
+ return options;
58
+ }
59
+
60
+ if (['process', 'analyze', 'watch'].includes(args[0])) {
61
+ args.shift();
62
+ }
63
+
64
+ for (let i = 0; i < args.length; i++) {
65
+ const arg = args[i];
66
+ if (arg === '--help' || arg === '-h' || arg === 'help') {
67
+ options.help = true;
68
+ } else if (arg === '--json') {
69
+ options.json = true;
70
+ } else if (arg === '--store' || arg === '--store-as-knowledge') {
71
+ options.storeAsKnowledge = true;
72
+ } else if (arg === '--query' || arg === '-q') {
73
+ options.query = readValue(args, i, arg);
74
+ i++;
75
+ } else if (arg.startsWith('--query=')) {
76
+ options.query = arg.slice('--query='.length);
77
+ } else if (arg === '--agent' || arg === '--agent-id') {
78
+ options.agentId = readValue(args, i, arg);
79
+ i++;
80
+ } else if (arg.startsWith('--agent=')) {
81
+ options.agentId = arg.slice('--agent='.length);
82
+ } else if (arg === '--timeout') {
83
+ options.timeoutMs = parseTimeoutMs(readValue(args, i, arg));
84
+ i++;
85
+ } else if (arg.startsWith('--timeout=')) {
86
+ options.timeoutMs = parseTimeoutMs(arg.slice('--timeout='.length));
87
+ } else if (arg.startsWith('-')) {
88
+ throw new Error(`Unknown option: ${arg}`);
89
+ } else if (!options.youtubeUrl) {
90
+ options.youtubeUrl = arg;
91
+ } else {
92
+ throw new Error(`Unexpected argument: ${arg}`);
93
+ }
94
+ }
95
+
96
+ if (options.help) return options;
97
+ if (!options.youtubeUrl) throw new Error('Missing YouTube URL. Run "atris youtube --help".');
98
+ if (options.storeAsKnowledge && !options.agentId) {
99
+ throw new Error('--store requires --agent <id>');
100
+ }
101
+ return options;
102
+ }
103
+
104
+ function buildYoutubePayload(options) {
105
+ const payload = {
106
+ youtube_url: options.youtubeUrl,
107
+ query: options.query || DEFAULT_QUERY,
108
+ };
109
+ if (options.agentId) payload.agent_id = options.agentId;
110
+ if (options.storeAsKnowledge) payload.store_as_knowledge = true;
111
+ return payload;
112
+ }
113
+
114
+ async function processYoutube(options, deps = {}) {
115
+ const apiFn = deps.apiRequestJson || apiRequestJson;
116
+ const ensureFn = deps.ensureValidCredentials || ensureValidCredentials;
117
+ const ensured = await ensureFn(apiFn);
118
+ const creds = ensured && ensured.credentials;
119
+ if (!creds?.token) {
120
+ const detail = ensured?.detail || ensured?.error;
121
+ throw new Error(detail ? `Authentication failed: ${detail}. Run "atris login".` : 'Not logged in. Run "atris login".');
122
+ }
123
+
124
+ const result = await apiFn('/agent/process_youtube', {
125
+ method: 'POST',
126
+ token: creds.token,
127
+ timeoutMs: options.timeoutMs,
128
+ body: buildYoutubePayload(options),
129
+ });
130
+
131
+ if (!result.ok) {
132
+ const hint = result.status === 401
133
+ ? ' Run "atris login --force".'
134
+ : result.status === 402
135
+ ? ' Check Atris credits.'
136
+ : '';
137
+ throw new Error(`YouTube processing failed (${result.status}): ${result.error || result.text || 'unknown error'}.${hint}`);
138
+ }
139
+
140
+ return result.data;
141
+ }
142
+
143
+ function formatYoutubeResult(data) {
144
+ const lines = [];
145
+ const metadata = data?.metadata || {};
146
+ lines.push(data?.message || 'YouTube video processed successfully');
147
+ if (metadata.title) lines.push(`Title: ${metadata.title}`);
148
+ if (metadata.channel) lines.push(`Channel: ${metadata.channel}`);
149
+ if (data?.credits_used !== undefined || data?.credits_remaining !== undefined) {
150
+ const used = data.credits_used !== undefined ? data.credits_used : '?';
151
+ const remaining = data.credits_remaining !== undefined ? data.credits_remaining : '?';
152
+ lines.push(`Credits: ${used} used, ${remaining} remaining`);
153
+ }
154
+ const analysis = data?.video_analysis || data?.analysis || data?.result;
155
+ if (analysis) {
156
+ lines.push('');
157
+ lines.push(String(analysis).trim());
158
+ }
159
+ return lines.join('\n');
160
+ }
161
+
162
+ async function youtubeCommand(argv = process.argv.slice(3), deps = {}) {
163
+ const output = deps.output || ((line = '') => console.log(line));
164
+ const options = parseYoutubeArgs(argv);
165
+ if (options.help) {
166
+ showYoutubeHelp(output, deps.commandName || 'atris youtube');
167
+ return 0;
168
+ }
169
+ const data = await processYoutube(options, deps);
170
+ output(options.json ? JSON.stringify(data, null, 2) : formatYoutubeResult(data));
171
+ return 0;
172
+ }
173
+
174
+ module.exports = {
175
+ DEFAULT_QUERY,
176
+ DEFAULT_TIMEOUT_MS,
177
+ showYoutubeHelp,
178
+ parseYoutubeArgs,
179
+ buildYoutubePayload,
180
+ processYoutube,
181
+ formatYoutubeResult,
182
+ youtubeCommand,
183
+ };
@@ -0,0 +1,164 @@
1
+ const MULTILINE_CSI = [
2
+ '\x1b[13;2~',
3
+ '\x1b[27;2;13~',
4
+ '\x1b[23~',
5
+ '\x1b[13;2u',
6
+ ];
7
+
8
+ const MULTILINE_CSI_RE = /\x1b\[(?:13;2~|27;2;13~|23~|13;2u)/;
9
+ const MULTILINE_CSI_VISIBLE_RE = /\[(?:13;2~|27;2;13~|23~|13;2u)/g;
10
+
11
+ // Node's readline cannot emit a 'line' whose buffer contains a literal "\n"
12
+ // (it reorders the text around the newline on submit). So newlines live in the
13
+ // buffer as this sentinel while typing is rendered, then are restored to "\n"
14
+ // when the answer is read back via stripMultilineCsiText.
15
+ const MULTILINE_PLACEHOLDER = '\x1f';
16
+ const MULTILINE_PLACEHOLDER_RE = /\x1f/g;
17
+
18
+ function isMultilineCsiPrefix(buffer) {
19
+ const value = String(buffer || '');
20
+ if (!value) return false;
21
+ return MULTILINE_CSI.some((seq) => seq.startsWith(value));
22
+ }
23
+
24
+ function isMultilineCsiComplete(buffer) {
25
+ return MULTILINE_CSI.includes(String(buffer || ''));
26
+ }
27
+
28
+ function isMultilineCsiKey(key) {
29
+ const sequence = String(key?.sequence || '');
30
+ return MULTILINE_CSI_RE.test(sequence);
31
+ }
32
+
33
+ function isMultilineInsertKey(str, key) {
34
+ if (!key) return str === '\n';
35
+ if (key.shift && (key.name === 'return' || key.name === 'enter')) return true;
36
+ if (key.meta && (key.name === 'return' || key.name === 'enter')) return true;
37
+ if (str === '\n' && key.name !== 'return') return true;
38
+ return false;
39
+ }
40
+
41
+ function isSubmitKey(str, key) {
42
+ if (key && (key.name === 'return' || key.name === 'enter') && !key.shift && !key.meta) return true;
43
+ if (!key && str === '\r') return true;
44
+ return false;
45
+ }
46
+
47
+ function stripMultilineCsiText(text) {
48
+ return String(text || '')
49
+ .replace(MULTILINE_PLACEHOLDER_RE, '\n')
50
+ .replace(/\x1b\[20[01]~/g, '')
51
+ .replace(/\x1b\[(?:13;2~|27;2;13~|23~|13;2u)/g, '\n')
52
+ .replace(MULTILINE_CSI_VISIBLE_RE, '\n')
53
+ .replace(/\^?\[\[27;2;13~/g, '\n');
54
+ }
55
+
56
+ // The raw bytes a keypress represents. readline's keypress decoder mangles
57
+ // some CSI sequences (e.g. it splits \x1b[27;2;13~ into a malformed escape
58
+ // whose `sequence` is \x1b[27;2; followed by literal "1", "3", "~"), so when
59
+ // `str` is empty we fall back to the decoded `sequence`.
60
+ function keyBytes(str, key) {
61
+ if (typeof str === 'string' && str.length > 0) return str;
62
+ if (key && typeof key.sequence === 'string' && key.sequence.length > 0) return key.sequence;
63
+ return typeof str === 'string' ? str : '';
64
+ }
65
+
66
+ function defaultInsertBreak(rl) {
67
+ if (!rl) return;
68
+ // _insertString splices a literal newline at the cursor; rl.write('\n')
69
+ // would instead SUBMIT the line, so it must not be used here.
70
+ if (typeof rl._insertString === 'function') {
71
+ rl._insertString('\n');
72
+ return;
73
+ }
74
+ if (typeof rl.line === 'string') {
75
+ const cursor = Number.isFinite(rl.cursor) ? rl.cursor : rl.line.length;
76
+ rl.line = rl.line.slice(0, cursor) + '\n' + rl.line.slice(cursor);
77
+ rl.cursor = cursor + 1;
78
+ if (typeof rl._refreshLine === 'function') rl._refreshLine();
79
+ return;
80
+ }
81
+ if (typeof rl.write === 'function') rl.write('\n');
82
+ }
83
+
84
+ function attachMultilineChatInput(rl, { insertBreak = defaultInsertBreak, onSubmit } = {}) {
85
+ if (!rl || typeof rl._ttyWrite !== 'function') return () => {};
86
+ const ttyWrite = rl._ttyWrite.bind(rl);
87
+ let escBuffer = null;
88
+
89
+ rl._ttyWrite = (str, key) => {
90
+ const bytes = keyBytes(str, key);
91
+
92
+ // Mid-sequence: keypress decoder shredded a multiline CSI across calls and
93
+ // we're reassembling it (e.g. \x1b[27;2; then "1", "3", "~").
94
+ if (escBuffer !== null) {
95
+ escBuffer += bytes;
96
+ if (isMultilineCsiComplete(escBuffer)) {
97
+ escBuffer = null;
98
+ insertBreak(rl);
99
+ return;
100
+ }
101
+ if (isMultilineCsiPrefix(escBuffer) && escBuffer.length <= 24) {
102
+ return;
103
+ }
104
+ // Diverged from every target sequence: replay the swallowed bytes
105
+ // literally (current keypress is already included in the buffer).
106
+ const replay = escBuffer;
107
+ escBuffer = null;
108
+ for (const ch of replay) ttyWrite(ch);
109
+ return;
110
+ }
111
+
112
+ // Whole multiline CSI delivered in a single keypress (terminals or Node
113
+ // versions that don't shred it).
114
+ if (isMultilineCsiKey(key) || MULTILINE_CSI_RE.test(bytes)) {
115
+ insertBreak(rl);
116
+ return;
117
+ }
118
+
119
+ // Start of a shredded multiline CSI: begin buffering from the raw escape.
120
+ if (bytes && bytes.charCodeAt(0) === 0x1b && isMultilineCsiPrefix(bytes)) {
121
+ escBuffer = bytes;
122
+ if (isMultilineCsiComplete(escBuffer)) {
123
+ escBuffer = null;
124
+ insertBreak(rl);
125
+ }
126
+ return;
127
+ }
128
+
129
+ // Plain Shift+Enter / Meta+Enter / synthetic \n.
130
+ if (isMultilineInsertKey(str, key)) {
131
+ insertBreak(rl);
132
+ return;
133
+ }
134
+
135
+ // Submit (Enter): the line buffer keeps its literal "\n" so readline's own
136
+ // multiline cursor math advances past every wrapped row (otherwise the box
137
+ // bottom border overwrites a row). readline mangles the emitted 'line'
138
+ // string when it holds a newline, so we hand the true buffer to onSubmit
139
+ // and the caller reads that instead of the line event.
140
+ if (isSubmitKey(str, key) && typeof rl.line === 'string' && rl.line.includes('\n')) {
141
+ if (typeof onSubmit === 'function') onSubmit(rl.line);
142
+ }
143
+
144
+ ttyWrite(str, key);
145
+ };
146
+
147
+ return () => {
148
+ rl._ttyWrite = ttyWrite;
149
+ escBuffer = null;
150
+ };
151
+ }
152
+
153
+ module.exports = {
154
+ MULTILINE_CSI,
155
+ MULTILINE_PLACEHOLDER,
156
+ attachMultilineChatInput,
157
+ isMultilineCsiComplete,
158
+ isMultilineCsiKey,
159
+ isMultilineInsertKey,
160
+ isMultilineCsiPrefix,
161
+ isSubmitKey,
162
+ stripMultilineCsiText,
163
+ keyBytes,
164
+ };