@causyne/agent 1.10.1 → 1.10.6

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/README.md CHANGED
@@ -12,6 +12,12 @@ documentation is intentionally concise while the project is evolving.
12
12
 
13
13
  Learn more at [causyne.com](https://causyne.com/).
14
14
 
15
+ ## Contact
16
+
17
+ - General inquiries and business communication: [contact@causyne.com](mailto:contact@causyne.com)
18
+ - Product support and troubleshooting: [help@causyne.com](mailto:help@causyne.com)
19
+ - Partnerships and direct founder communication: [ikbal@causyne.com](mailto:ikbal@causyne.com)
20
+
15
21
  ## Development
16
22
 
17
23
  Node.js 24 or newer is required.
@@ -9,23 +9,17 @@ license identifiers:
9
9
 
10
10
  | License expression | Lockfile package entries |
11
11
  | --- | ---: |
12
- | MIT | 87 |
13
- | Apache-2.0 | 22 |
14
- | BSD-2-Clause | 8 |
15
- | ISC | 5 |
16
- | BSD-3-Clause | 1 |
17
- | BlueOak-1.0.0 | 1 |
18
- | 0BSD | 1 |
19
- | MIT OR CC0-1.0 | 1 |
20
-
21
- The lockfile contained 126 dependency entries in total. npm's audit metadata
22
- reported 106 production dependencies; platform-specific optional entries and
23
- development dependencies account for the differing views of the graph.
24
-
25
- The direct runtime dependencies are `@babel/parser`, `acorn-walk`, `commander`,
26
- `duck-duck-scrape`, `googlethis`, and `inquirer`. Direct
27
- development dependencies are `@types/node`, `acorn`, and `typescript`. Exact
28
- versions and integrity hashes are recorded in `package-lock.json`.
12
+ | Apache-2.0 | 21 |
13
+ | MIT | 9 |
14
+
15
+ The lockfile contained 30 dependency entries in total. Three are production
16
+ entries; development and platform-specific optional entries account for the
17
+ rest. npm's audit metadata reported 12 installed packages.
18
+
19
+ The direct runtime dependencies are `acorn-walk` and `commander`. Direct
20
+ development dependencies are `@babel/parser`, `@types/node`, `acorn`, and
21
+ `typescript`. Exact versions and integrity hashes are recorded in
22
+ `package-lock.json`.
29
23
 
30
24
  No dependency in the reviewed main lockfile was marked with an install script.
31
25
  Optional capability dependencies are installed only after approval into a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@causyne/agent",
3
- "version": "1.10.1",
3
+ "version": "1.10.6",
4
4
  "description": "Agentic AI for software, tools, and computer workflows",
5
5
  "main": "bin/causyne.js",
6
6
  "type": "module",
@@ -49,19 +49,12 @@
49
49
  "bugs": {
50
50
  "url": "https://github.com/ikbal-causyne/causyne-agent/issues"
51
51
  },
52
- "overrides": {
53
- "form-data": "^4.0.0",
54
- "axios": "^1.20.0"
55
- },
56
52
  "dependencies": {
57
- "@babel/parser": "^8.0.4",
58
53
  "acorn-walk": "^8.3.5",
59
- "commander": "^11.1.0",
60
- "duck-duck-scrape": "^2.2.7",
61
- "googlethis": "^1.8.0",
62
- "inquirer": "^9.2.12"
54
+ "commander": "^11.1.0"
63
55
  },
64
56
  "devDependencies": {
57
+ "@babel/parser": "^8.0.7",
65
58
  "@types/node": "^22.20.2",
66
59
  "acorn": "^8.18.0",
67
60
  "typescript": "^7.0.2"
package/src/cliMode.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { AgentLoop } from './agentLoop.js';
2
2
  import { loadConfig, saveConfig } from './config.js';
3
- import inquirer from './utils/inquirer.js';
4
3
  import readline from 'readline';
5
4
  import { ChatPrompt, isInteractive } from './tui/prompt.js';
6
5
  import { handleInput, listCommands, isCommandInput } from './commands.js';
@@ -12,6 +11,7 @@ import { wrapBlock } from './tui/text.js';
12
11
  import { getPackageVersion } from './version.js';
13
12
  import { resolveCharPool } from './contextManager.js';
14
13
  import { PRIMARY_ACTIONS, homeStatusLine, homeHint, moreLayer, configureInPrompt, mcpPicker, PROVIDERS } from './tui/home.js';
14
+ import { SelectLayer } from './tui/dialogs.js';
15
15
  import { askSelect, askText, askConfirm } from './tui/ask.js';
16
16
  import { renderReport } from './reporting.js';
17
17
  import { grantAlways } from './approvalGrants.js';
@@ -78,7 +78,7 @@ function makeApprovalHandler(conn, dialogs = null) {
78
78
  conn.stopSpinner();
79
79
 
80
80
  // An interactive session asks in a panel that shows the risk, the arguments
81
- // and a scrollable diff in one frame. The inquirer path below asks the same
81
+ // and a scrollable diff in one frame. The plain fallback asks the same
82
82
  // question in the scrollback, which is all a non-TTY session can do.
83
83
  if (dialogs && detail) {
84
84
  // Loops only for "Allow always" → Cancel: backing out of the standing
@@ -100,7 +100,11 @@ function makeApprovalHandler(conn, dialogs = null) {
100
100
  // The text says exactly what is being given up and for how long.
101
101
  const confirmed = await dialogs.confirm(
102
102
  `Allow ${bold(detail.tool)} to run WITHOUT asking, for the rest of this session?\n`
103
- + ` This lasts until Causyne restarts. Undo it any time with /approvals clear.`
103
+ + ` This lasts until Causyne restarts. Undo it any time with /approvals clear.`,
104
+ // Reaching this question already required moving onto Allow always
105
+ // and pressing Enter. A second Enter is therefore an explicit second
106
+ // confirmation, rather than a reflex approval on the first dialog.
107
+ { defaultAnswer: true }
104
108
  );
105
109
  if (confirmed) break;
106
110
  // Cancelled: ask again rather than denying something they never rejected.
@@ -139,7 +143,7 @@ function makeApprovalHandler(conn, dialogs = null) {
139
143
 
140
144
  // No live region to draw a panel into (a piped session, or raw mode
141
145
  // refused). askConfirm degrades to readline on its own, so this path keeps
142
- // the same wording as the panel instead of inquirer's.
146
+ // the same wording as the panel instead of a second prompt framework's.
143
147
  const approved = await askConfirm(message);
144
148
 
145
149
  if (conn.pendingApprovals) conn.pendingApprovals.delete(toolCallId);
@@ -322,7 +326,7 @@ export async function chatMode({ home = false } = {}) {
322
326
  // The prompt owns the keyboard so the chat can offer history, completion and
323
327
  // typing while a turn runs. It is skipped when stdin is not a terminal (piped
324
328
  // input, CI) or when CAUSYNE_SIMPLE_INPUT=1, and any failure to take raw mode
325
- // falls back to the inquirer path below — a better editor must never be the
329
+ // falls back to the plain readline path below — a better editor must never be the
326
330
  // reason chat refuses to start.
327
331
  let prompt = null;
328
332
  if (isInteractive()) {
@@ -349,8 +353,8 @@ export async function chatMode({ home = false } = {}) {
349
353
  root: process.cwd(),
350
354
  commands: listCommands('terminal'),
351
355
  // The rule for "is this a command" lives in commands.js and is handed
352
- // down rather than reimplemented, so the prompt draws a `/usage` as an
353
- // invocation instead of as a sentence the user supposedly said.
356
+ // down rather than reimplemented, so the prompt keeps `/usage` out of
357
+ // the conversation instead of drawing it as something the user said.
354
358
  isCommand: isCommandInput,
355
359
  surface,
356
360
  onInterrupt: () => loop.abort(),
@@ -367,9 +371,9 @@ export async function chatMode({ home = false } = {}) {
367
371
  // one that was asked for.
368
372
  if (surface) prompt.draw();
369
373
 
370
- // Ask for approval in the prompt's own panel, not inquirer's.
374
+ // Ask for approval in the prompt's own panel, not a competing reader.
371
375
  //
372
- // inquirer.prompt() takes stdin while this prompt is already holding it in
376
+ // A second prompt reader would take stdin while this prompt already holds it in
373
377
  // raw mode: the two contend, and the question can land on a half-drawn
374
378
  // prompt. The panel shows the risk, the arguments and a scrollable diff in
375
379
  // one frame, and dismissing it denies.
@@ -394,12 +398,11 @@ export async function chatMode({ home = false } = {}) {
394
398
 
395
399
  const readTask = async () => {
396
400
  if (prompt) return prompt.read();
397
- const { task } = await inquirer.prompt([{
398
- type: 'input',
399
- name: 'task',
400
- message: '\x1b[36mYou:\x1b[0m'
401
- }]);
402
- return task;
401
+ // The shared question helper already provides the same plain-readline
402
+ // fallback when a terminal cannot host the live TUI. Keeping a second
403
+ // prompt framework here pulled a large, unused transitive graph into every
404
+ // installation and made the fallback look different from the main CLI.
405
+ return askText({ title: 'You' });
403
406
  };
404
407
 
405
408
  const finish = () => { if (prompt) prompt.stop(); };
@@ -456,6 +459,38 @@ export async function chatMode({ home = false } = {}) {
456
459
  continue;
457
460
  }
458
461
 
462
+ if (commandResult.effect === 'pick-session' && prompt) {
463
+ const active = commandResult.active;
464
+ prompt.openLayer(new SelectLayer({
465
+ title: 'Sessions',
466
+ emptyText: 'No sessions yet',
467
+ items: (commandResult.items || []).map((session) => ({
468
+ id: session.id,
469
+ label: `${session.id === active ? `${GLYPHS.bullet} ` : ''}${session.title}`,
470
+ description: `${session.messageCount} messages ${GLYPHS.dot} ${session.workspace || 'no workspace'}`,
471
+ value: session
472
+ })),
473
+ onPick: (item) => {
474
+ if (item) prompt.deliver(`/sessions switch ${item.value.id}`);
475
+ }
476
+ }));
477
+ continue;
478
+ }
479
+
480
+ if (commandResult.effect === 'pick-mcp' && prompt) {
481
+ const { empty } = await mcpPicker(prompt, (server) => {
482
+ const verb = server.granted ? 'revoke' : 'grant';
483
+ prompt.deliver(`/mcp ${verb} ${server.name}`);
484
+ });
485
+ if (empty) prompt.showText('MCP servers', commandResult.output);
486
+ continue;
487
+ }
488
+
489
+ if (commandResult.effect === 'dialog' && prompt) {
490
+ prompt.showText(commandResult.title, commandResult.output, { danger: !commandResult.ok });
491
+ continue;
492
+ }
493
+
459
494
  // A skill was invoked: it becomes part of the REQUEST, not terminal output.
460
495
  //
461
496
  // This is the whole point. `/skills <id>` used to print the file and call
@@ -568,7 +603,7 @@ export async function interactiveConfig(options) {
568
603
  return;
569
604
  }
570
605
 
571
- // Asked in the product's own panels rather than inquirer's.
606
+ // Asked in the product's own panels rather than a second prompt framework.
572
607
  //
573
608
  // These were the last two surfaces on a second design system — different
574
609
  // colours, different glyphs, no NO_COLOR handling, and a list widget nothing
package/src/commands.js CHANGED
@@ -47,6 +47,7 @@ export const COMMANDS = [
47
47
  scope: 'local',
48
48
  usage: '/help [command]',
49
49
  description: 'List the available commands',
50
+ dialogTitle: 'Commands',
50
51
  run: (argv, ctx) => {
51
52
  const [wanted] = argv;
52
53
  if (wanted) {
@@ -335,6 +336,7 @@ export const COMMANDS = [
335
336
  scope: 'local',
336
337
  usage: '/checkpoints [count]',
337
338
  description: 'List recent undo checkpoints',
339
+ dialogTitle: 'Checkpoints',
338
340
  run: (argv) => ok(buildCheckpointsText(parseInt(argv[0], 10) || 15))
339
341
  },
340
342
 
@@ -344,6 +346,7 @@ export const COMMANDS = [
344
346
  scope: 'local',
345
347
  usage: '/context',
346
348
  description: 'Show what fills the context window',
349
+ dialogTitle: 'Context',
347
350
  run: () => ok(buildContextText())
348
351
  },
349
352
 
@@ -353,6 +356,7 @@ export const COMMANDS = [
353
356
  scope: 'local',
354
357
  usage: '/usage',
355
358
  description: 'Show token usage and estimated cost',
359
+ dialogTitle: 'Usage',
356
360
  run: async () => ok(await buildUsageText())
357
361
  },
358
362
 
@@ -362,6 +366,7 @@ export const COMMANDS = [
362
366
  scope: 'local',
363
367
  usage: '/cortex [list | forget <id> | forget-here]',
364
368
  description: 'Inspect what the agent has learned, and forget any of it',
369
+ dialogTitle: 'Cortex',
365
370
  // A memory store you cannot read is not a memory, it is a black box that
366
371
  // occasionally changes the agent's behaviour. Section 4 asks for intelligent
367
372
  // safety rather than opacity, and section 10 layer 5 asks for a trail a
@@ -426,6 +431,7 @@ export const COMMANDS = [
426
431
  scope: 'local',
427
432
  usage: '/sessions [switch <id> | delete <id>]',
428
433
  description: 'List chat sessions, or switch to one',
434
+ dialogTitle: 'Sessions',
429
435
  // Sessions replaced ONE global chat_history.json that every directory read.
430
436
  // A session you cannot list or switch to is not a session, so this ships
431
437
  // with the store rather than after it.
@@ -462,7 +468,8 @@ export const COMMANDS = [
462
468
  return ok(
463
469
  `${rows.length} session${rows.length === 1 ? '' : 's'} (* = active):\n`
464
470
  + lines.join('\n')
465
- + '\n\nSwitch with /sessions switch <id>. A new directory always starts a new session.'
471
+ + '\n\nSwitch with /sessions switch <id>. A new directory always starts a new session.',
472
+ { effect: 'pick-session', items: rows, active }
466
473
  );
467
474
  }
468
475
  },
@@ -472,6 +479,7 @@ export const COMMANDS = [
472
479
  scope: 'local',
473
480
  usage: '/approvals [clear]',
474
481
  description: 'List tools granted "Allow always", or revoke them',
482
+ dialogTitle: 'Approvals',
475
483
  // A standing grant with no way to see or revoke it is a trap, not a
476
484
  // convenience. This is the way out, and the confirm dialog that grants one
477
485
  // names this command so the user is told about it at the moment it matters.
@@ -541,6 +549,7 @@ export const COMMANDS = [
541
549
  scope: 'local',
542
550
  usage: '/model [provider/model | model]',
543
551
  description: 'Show or change the model in use',
552
+ dialogTitle: 'Model',
544
553
  run: (argv) => {
545
554
  const config = loadConfig();
546
555
  if (argv.length === 0) {
@@ -567,6 +576,7 @@ export const COMMANDS = [
567
576
  scope: 'local',
568
577
  usage: '/mcp [add <name> <cmd…> | add <name> --url <url> | remove|grant|revoke <name>]',
569
578
  description: 'Manage MCP servers: list, add, remove, grant, revoke',
579
+ dialogTitle: 'MCP servers',
570
580
  run: (argv) => {
571
581
  const [action, serverName, ...rest] = argv;
572
582
 
@@ -622,7 +632,7 @@ export const COMMANDS = [
622
632
 
623
633
  const servers = serverStatus();
624
634
  if (servers.length === 0) {
625
- return ok('No MCP servers are configured.\n\nAdd one with:\n mcp add <name> <command> [args…]\n mcp add <name> --url <https://…>\n\n(prefix with / in chat, or `causyne-agent mcp …` in a terminal)');
635
+ return ok('No MCP servers are configured.\n\nAdd one with:\n mcp add <name> <command> [args…]\n mcp add <name> --url <https://…>\n\n(prefix with / in chat, or `causyne-agent mcp …` in a terminal)', { effect: 'pick-mcp' });
626
636
  }
627
637
  const lines = ['MCP servers', ''];
628
638
  for (const server of servers) {
@@ -632,7 +642,7 @@ export const COMMANDS = [
632
642
  }
633
643
  lines.push('');
634
644
  lines.push(' mcp grant <name> to enable, mcp remove <name> to delete.');
635
- return ok(lines.join('\n'));
645
+ return ok(lines.join('\n'), { effect: 'pick-mcp' });
636
646
  }
637
647
  },
638
648
 
@@ -703,6 +713,7 @@ export const COMMANDS = [
703
713
  scope: 'local',
704
714
  usage: '/status',
705
715
  description: 'Show the current provider, autonomy mode and workspace scope',
716
+ dialogTitle: 'Status',
706
717
  // One builder, shared with `causyne status`, so status output cannot drift.
707
718
  run: (argv, ctx) => ok(buildStatusText({ loop: ctx.loop || null }))
708
719
  },
@@ -791,7 +802,11 @@ export async function runCommand(name, argv = [], ctx = {}) {
791
802
  }
792
803
 
793
804
  try {
794
- return await command.run(argv, { ...ctx, surface });
805
+ const result = await command.run(argv, { ...ctx, surface });
806
+ if (command.dialogTitle && result && !result.effect) {
807
+ return { ...result, effect: 'dialog', title: command.dialogTitle };
808
+ }
809
+ return result;
795
810
  } catch (e) {
796
811
  return fail(`/${command.name} failed: ${e.message}`);
797
812
  }
package/src/entrypoint.js CHANGED
@@ -5,6 +5,29 @@ import { runSelfUpdate } from './autoUpdate.js';
5
5
  import { printContextReport, printUsageReport, printCheckpoints, printUndo, buildStatusText, printReport } from './reporting.js';
6
6
  import { runCommand } from './commands.js';
7
7
  import { resolveInvocation } from './tui/invocation.js';
8
+ import { resolveUserPath } from './utils/paths.js';
9
+
10
+ function requestedWorkingDirectory(argv = []) {
11
+ const tokens = Array.isArray(argv) ? argv.slice(2) : [];
12
+ for (let index = 0; index < tokens.length; index++) {
13
+ const token = String(tokens[index]);
14
+ if (token === '--') break;
15
+ if (token === '-C' || token === '--cwd') return tokens[index + 1] || '';
16
+ if (token.startsWith('--cwd=')) return token.slice('--cwd='.length);
17
+ if (!token.startsWith('-')) break;
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function applyRequestedWorkingDirectory(argv) {
23
+ const requested = requestedWorkingDirectory(argv);
24
+ if (requested === null) return;
25
+ if (!requested) throw new Error('The --cwd option requires a directory.');
26
+ const resolved = resolveUserPath(requested);
27
+ process.chdir(resolved);
28
+ }
29
+
30
+ applyRequestedWorkingDirectory(process.argv);
8
31
 
9
32
  // The old flag remains parseable only to explain its retirement.
10
33
  function announceDevEmulator(requested) {
@@ -42,6 +65,7 @@ program
42
65
  // "Usage: causyne-agent" reasonably concludes they ran the wrong thing.
43
66
  .name('causyne')
44
67
  .description('Causyne — Local desktop AI harness (alias: causyne-agent)')
68
+ .option('-C, --cwd <directory>', 'Use this directory as the workspace')
45
69
  .version(pkgVersion);
46
70
 
47
71
  program
package/src/sandbox.js CHANGED
@@ -96,13 +96,10 @@ const INTERPRETER_INVOCATION = /(^|\s)(sh|bash|zsh|ksh|dash|fish|eval|xargs|ssh|
96
96
  /**
97
97
  * Credential stores and key material.
98
98
  *
99
- * L4 (local-path audit): the DEFAULT workspace scope ends with the home
100
- * directory itself as a fallback, which silently puts ~/.ssh, ~/.aws and
101
- * ~/.causyne (the encrypted API key AND the .vault_key that decrypts it) inside
102
- * the agent's reach with no prompt. Vision section 4 asks for freedom WITH a
103
- * human in the loop rather than a wall, so these are not blocked — touching one
104
- * is classified 'extreme', which always requires explicit approval in every
105
- * autonomy mode, including Mode C.
99
+ * A user may deliberately select a workspace that contains credential material.
100
+ * These paths are not silently trusted just because they sit below that root:
101
+ * touching one is classified 'extreme', which always requires explicit approval
102
+ * in every autonomy mode, including Mode C.
106
103
  */
107
104
  const CREDENTIAL_PATHS = [
108
105
  /(^|\/)\.ssh(\/|$)/,
@@ -168,22 +165,17 @@ function redirectsIntoSensitiveTarget(maskedCommand) {
168
165
  }
169
166
 
170
167
  export class Sandbox {
171
- constructor() {
168
+ constructor({ workspaceRoot = process.cwd() } = {}) {
172
169
  this.config = loadConfig();
173
-
174
- // V5 Upgrade: Expanded default workspace paths
175
- // Instead of only os.homedir() (which was too restrictive and caused path errors),
176
- // we now default to common user directories while still blocking system dirs.
177
- const home = os.homedir();
178
- this.workspacePaths = this.config.workspacePaths || [
179
- path.join(home, 'Desktop'),
180
- path.join(home, 'Documents'),
181
- path.join(home, 'Downloads'),
182
- path.join(home, 'Projects'),
183
- path.join(home, 'CausyneWorkspace'),
184
- path.join(home, 'Causyne'),
185
- home // Fallback: allow home dir itself (covers edge cases like ~/scripts, ~/.config)
186
- ];
170
+
171
+ // The launch directory is the workspace. A previous default advertised
172
+ // Desktop, Documents, Downloads, ~/Causyne and the whole home directory as
173
+ // equally valid roots. That made the model drift into the agent's own source
174
+ // even when the user launched it from another project, while also making the
175
+ // sandbox effectively home-wide. Explicit runtime grants can still use
176
+ // updateWorkspacePaths(), but stale config never overrides the folder the
177
+ // user selected for this invocation.
178
+ this.workspacePaths = [path.resolve(workspaceRoot)];
187
179
 
188
180
  // Developer emulator mode (hardened, secret-gated — see devMode.js) grants
189
181
  // full local access for agent development on a Fast-tier emulator. It cannot
@@ -243,9 +235,9 @@ export class Sandbox {
243
235
  }
244
236
  }
245
237
 
246
- // L4: a credential store is in reach by default (the home fallback), so it
247
- // gets a human in the loop rather than a wall. 'extreme' is the one risk
248
- // level Mode C cannot auto-approve.
238
+ // L4: when an explicitly selected workspace contains credential material,
239
+ // it gets a human in the loop rather than silent access. 'extreme' is the
240
+ // one risk level Mode C cannot auto-approve.
249
241
  for (const p of pathsToCheck) {
250
242
  if (typeof p === 'string' && isCredentialPath(p)) {
251
243
  return { allowed: true, risk: 'extreme', reason: `Touches credential material: ${p}` };
@@ -352,20 +344,6 @@ export class Sandbox {
352
344
  return false;
353
345
  }
354
346
 
355
- // V7 Ultimate Self-Preservation: Block access to the agent's own source code
356
- // The AGI cannot read, modify, or delete causyne-agent itself.
357
- const agentBinOrScript = process.argv[1] || '';
358
- let agentRoot = '';
359
- if (agentBinOrScript && agentBinOrScript.includes('causyne-agent')) {
360
- const idx = agentBinOrScript.lastIndexOf('causyne-agent');
361
- agentRoot = path.resolve(agentBinOrScript.slice(0, idx + 'causyne-agent'.length));
362
- } else if (typeof __dirname !== 'undefined') {
363
- agentRoot = path.resolve(__dirname, '..');
364
- }
365
- if (agentRoot && resolved.startsWith(agentRoot) && resolved !== path.resolve(os.homedir())) {
366
- return false;
367
- }
368
-
369
347
  // V6: Override workspace limits if grantAllAccess is active
370
348
  if (this.grantAllAccess) {
371
349
  return true;
package/src/tui/ask.js CHANGED
@@ -2,8 +2,8 @@
2
2
  //
3
3
  // `causyne config` and `causyne pair` run with no chat session behind them, so
4
4
  // there is no live region for a dialog to draw into. That is why they were the
5
- // last two surfaces still on inquirer — and inquirer is a different design
6
- // system: its own colours, its own glyphs, no NO_COLOR handling, and a list
5
+ // last two surfaces still on a second prompt framework, with its own colours,
6
+ // glyphs, NO_COLOR handling, and list
7
7
  // widget that looks nothing like the picker the same user sees inside `causyne`.
8
8
  //
9
9
  // The fix is NOT a second app shell. That was tried once: screen.js, layout.js,
package/src/tui/banner.js CHANGED
@@ -23,9 +23,44 @@ const WORDMARK = [
23
23
  ' ████ █ █ ███ ████ █ █ █ █████'
24
24
  ];
25
25
 
26
- // A terminal without box-drawing support would render the art as mojibake, so
27
- // it gets the wordmark spelled out instead of a field of question marks.
28
- const WORDMARK_PLAIN = ['C A U S Y N E'];
26
+ // A compact terminal rendering of the six-part Causyne aperture. The shape is
27
+ // sampled from the canonical SVG rather than invented as a second logo. Its
28
+ // upper-right blade carries the signal green; the other blades use the neutral
29
+ // brand ink. Each row remains ordinary terminal text and degrades to #/+ ASCII.
30
+ const LOGO_SEGMENTS = [
31
+ [['ink', ' ███▓']],
32
+ [['ink', ' ▓███'], ['accent', '███'], ['ink', '▓']],
33
+ [['ink', '▓███ ▓▓'], ['accent', '██'], ['ink', '▓']],
34
+ [['ink', '▓██ ▓██▓']],
35
+ [['ink', '▓███▓ ███▓']],
36
+ [['ink', ' ███████▓']],
37
+ [['ink', ' ▓███']]
38
+ ];
39
+
40
+ function logoRows(unicode) {
41
+ return LOGO_SEGMENTS.map((segments) => segments.map(([role, text]) => {
42
+ const rendered = unicode
43
+ ? text
44
+ : text.replace(/[█▓]/g, role === 'accent' ? '+' : '#');
45
+ return paint(role, rendered);
46
+ }).join(''));
47
+ }
48
+
49
+ function brandArt(width, unicode) {
50
+ if (!unicode || width < 64) {
51
+ const logo = logoRows(false);
52
+ logo[2] = `${padEnd(logo[2], 12)}${bold(paint('ink', 'CAUSYNE AGENT'))}`;
53
+ return logo;
54
+ }
55
+
56
+ const logo = logoRows(true);
57
+ return logo.map((line, index) => {
58
+ const word = WORDMARK[index - 1];
59
+ return word === undefined
60
+ ? line
61
+ : `${padEnd(line, 10)} ${paint('ink', word)}`;
62
+ });
63
+ }
29
64
 
30
65
  /**
31
66
  * The banner rows.
@@ -35,10 +70,9 @@ const WORDMARK_PLAIN = ['C A U S Y N E'];
35
70
  * never be exercised at all.
36
71
  */
37
72
  export function wordmark(width = 80, { unicode = HAS_UNICODE } = {}) {
38
- const art = (unicode && width >= 54) ? WORDMARK : WORDMARK_PLAIN;
39
- const rows = art.map((line) => paint('accent', line));
73
+ const rows = brandArt(width, unicode);
40
74
  rows.push('');
41
- rows.push(dim('Causyne AI Agent'));
75
+ rows.push(dim('Causyne Agent'));
42
76
  return rows;
43
77
  }
44
78
 
@@ -60,8 +94,8 @@ export function wordmark(width = 80, { unicode = HAS_UNICODE } = {}) {
60
94
  * header that wraps is worse than a header that is tall.
61
95
  */
62
96
  export function sessionBanner(info = {}, width = 80, { unicode = HAS_UNICODE } = {}) {
63
- const art = (unicode && width >= 54) ? WORDMARK : WORDMARK_PLAIN;
64
- const artWidth = Math.max(...art.map(visibleWidth));
97
+ let art = brandArt(width, unicode);
98
+ let artWidth = Math.max(...art.map(visibleWidth));
65
99
 
66
100
  const facts = [];
67
101
  if (info.version) facts.push([bold(paint('accent', 'Causyne')), dim(info.version)]);
@@ -78,11 +112,22 @@ export function sessionBanner(info = {}, width = 80, { unicode = HAS_UNICODE } =
78
112
  // deferred-wrap state: the cursor sits past the last column, and the next
79
113
  // write decides whether that becomes a wrap. Measured on an 80-column pty,
80
114
  // the workspace row came back at exactly 80 while every other row was 79.
81
- const room = width - artWidth - GAP - 1;
115
+ let room = width - artWidth - GAP - 1;
116
+
117
+ // A coloured 80-column terminal can render the full block wordmark, but it
118
+ // leaves too little room for the session facts beside it. Stacking those
119
+ // facts made startup 16 rows tall and overflowed short terminals. Prefer the
120
+ // compact ASCII aperture in that case: identity and facts remain one object,
121
+ // and the first prompt still fits without scrolling the banner away.
122
+ if (room < 24 && unicode && width >= 64) {
123
+ art = brandArt(width, false);
124
+ artWidth = Math.max(...art.map(visibleWidth));
125
+ room = width - artWidth - GAP - 1;
126
+ }
82
127
 
83
128
  // Not enough width for two columns: stack, rather than wrap either one.
84
129
  if (room < 24) {
85
- return art.map((line) => paint('accent', line))
130
+ return art
86
131
  .concat([''], facts.map(([left, right]) =>
87
132
  truncate(right ? `${left} ${right}` : left, width - 1, GLYPHS.ellipsis)));
88
133
  }
@@ -90,7 +135,7 @@ export function sessionBanner(info = {}, width = 80, { unicode = HAS_UNICODE } =
90
135
  const rows = [];
91
136
  const height = Math.max(art.length, facts.length);
92
137
  for (let index = 0; index < height; index++) {
93
- const artLine = art[index] === undefined ? '' : paint('accent', art[index]);
138
+ const artLine = art[index] === undefined ? '' : art[index];
94
139
  const fact = facts[index];
95
140
  const left = padEnd(artLine, artWidth) + ' '.repeat(GAP);
96
141
  if (!fact) { rows.push(artLine); continue; }
@@ -299,9 +299,9 @@ export class TextLayer extends Layer {
299
299
  * how dangerous is this, what exactly will it do, and what will change on disk.
300
300
  * The diff scrolls, so a 400-line rewrite is inspectable rather than truncated.
301
301
  *
302
- * Deliberately has NO default. `y` and `n` are explicit, enter is not bound, and
303
- * escape denies. A dialog where the safe answer is one keypress away from the
304
- * dangerous one is how a person approves something they meant to read.
302
+ * The highlighted default is Reject. `y` and `n` remain explicit shortcuts;
303
+ * arrows choose a visible option and Enter commits it. A reflex Enter therefore
304
+ * denies, while an intentional move to either Allow option works consistently.
305
305
  */
306
306
  export class ApprovalLayer extends Layer {
307
307
  constructor(options = {}) {
@@ -495,7 +495,10 @@ export class ConfirmLayer extends Layer {
495
495
  super({ width: options.width || 60, height: options.height || 8, ...options });
496
496
  this.message = options.message || 'Are you sure?';
497
497
  this.onAnswer = options.onAnswer || null;
498
- this.footer = `y yes ${GLYPHS.dot} n no ${GLYPHS.dot} esc cancel`;
498
+ this.defaultAnswer = options.defaultAnswer === true;
499
+ this.footer = this.defaultAnswer
500
+ ? `enter confirm ${GLYPHS.dot} y yes ${GLYPHS.dot} n no ${GLYPHS.dot} esc cancel`
501
+ : `enter cancel ${GLYPHS.dot} y yes ${GLYPHS.dot} n no ${GLYPHS.dot} esc cancel`;
499
502
  this.settled = false;
500
503
  }
501
504
 
@@ -523,10 +526,15 @@ export class ConfirmLayer extends Layer {
523
526
 
524
527
  key(event) {
525
528
  if (!event) return null;
526
- // Only an explicit `y` approves. Every other key either says no or is
527
- // ignored — a mistyped keypress must never approve.
529
+ // `y` is always an explicit approval. Enter follows the default selected by
530
+ // the caller: false for ordinary destructive confirmations, true only for
531
+ // the second step after the user has already selected Allow always.
528
532
  if (isChar(event, 'y')) { this.answer(true); return 'close'; }
529
533
  if (isChar(event, 'n') || event.name === 'escape') { this.answer(false); return 'close'; }
534
+ if (event.name === 'enter' || event.name === 'return') {
535
+ this.answer(this.defaultAnswer);
536
+ return 'close';
537
+ }
530
538
  return 'handled';
531
539
  }
532
540
  }
package/src/tui/home.js CHANGED
@@ -207,7 +207,7 @@ export async function mcpPicker(prompt, onAction) {
207
207
  items: rows.map((server) => ({
208
208
  id: server.name,
209
209
  label: server.name,
210
- description: `${server.granted ? 'granted' : 'not granted'}${server.command ? ` ${GLYPHS.dot} ${server.command}` : ''}`,
210
+ description: `${server.granted ? 'granted' : 'not granted'}${server.target ? ` ${GLYPHS.dot} ${server.target}` : ''}`,
211
211
  category: server.granted ? 'granted' : 'available',
212
212
  value: server
213
213
  })),
@@ -34,6 +34,8 @@ export const ROOT_OPTIONS = new Map([
34
34
  ['--help', 0],
35
35
  ['-V', 0],
36
36
  ['--version', 0],
37
+ ['-C', 1],
38
+ ['--cwd', 1],
37
39
  ['--dev-emulator', 0]
38
40
  ]);
39
41
 
package/src/tui/keys.js CHANGED
@@ -273,6 +273,17 @@ export class KeyDecoder {
273
273
  } else if (final === 'u') {
274
274
  // Kitty keyboard protocol: ESC [ codepoint ; modifiers u
275
275
  const codepoint = numbers[0];
276
+ const controlName = {
277
+ 9: 'tab',
278
+ 10: 'enter',
279
+ 13: 'enter',
280
+ 27: 'escape',
281
+ 127: 'backspace'
282
+ }[codepoint];
283
+ if (controlName) {
284
+ events.push(applyModifier(makeKey(controlName, { sequence }), numbers[1]));
285
+ return sequence.length;
286
+ }
276
287
  if (Number.isFinite(codepoint) && codepoint >= 32) {
277
288
  const character = String.fromCodePoint(codepoint);
278
289
  const key = applyModifier(
package/src/tui/prompt.js CHANGED
@@ -12,7 +12,7 @@
12
12
  // * ESC INTERRUPTS — wired to the AgentLoop.abort() that already existed and
13
13
  // was only reachable by typing /stop after the turn had finished.
14
14
  // * FAIL OPEN — if raw mode is unavailable (piped stdin, a CI runner, an
15
- // unusual terminal) the caller falls back to inquirer. A fancy editor must
15
+ // unusual terminal) the caller falls back to plain readline. A fancy editor must
16
16
  // never be the reason chat does not start.
17
17
  //
18
18
  // Pure Node. No dependency, no shell.
@@ -23,10 +23,10 @@ import { PromptHistory } from './history.js';
23
23
  import { FileIndex, suggest, applyCompletion, renderPicker, MAX_VISIBLE } from './completion.js';
24
24
  import { paint, dim, bold, GLYPHS, terminalWidth, HAS_COLOR } from './theme.js';
25
25
  import { barBlock, GUTTER } from './block.js';
26
- import { visibleWidth, truncate, wrapBlock, shortenHome } from './text.js';
26
+ import { visibleWidth, truncate, wrapBlock, shortenHome, safeDisplay } from './text.js';
27
27
  import { pickExample, fitHints } from './banner.js';
28
28
  import { panelRows } from './overlay.js';
29
- import { commandPalette, skillPicker, ApprovalLayer, ConfirmLayer, HomeLayer } from './dialogs.js';
29
+ import { commandPalette, skillPicker, ApprovalLayer, ConfirmLayer, HomeLayer, TextLayer } from './dialogs.js';
30
30
  import { describeTool } from './toolDisplay.js';
31
31
 
32
32
  const CTRL_C_WINDOW_MS = 2000;
@@ -419,6 +419,19 @@ export class ChatPrompt {
419
419
  }));
420
420
  }
421
421
 
422
+ /** Show local command output inside the live region, never in chat scrollback. */
423
+ showText(title, text, options = {}) {
424
+ const panelWidth = Math.max(28, Math.min(this.width() - 2, options.width || 76));
425
+ const contentWidth = Math.max(12, panelWidth - 4);
426
+ const lines = wrapBlock(safeDisplay(String(text == null ? '' : text)), contentWidth);
427
+ return this.openLayer(new TextLayer({
428
+ title: String(title || 'Details'),
429
+ lines: options.danger ? lines.map((line) => paint('danger', line)) : lines,
430
+ width: panelWidth,
431
+ height: options.height || 20
432
+ }));
433
+ }
434
+
422
435
  /**
423
436
  * Ask for approval of a tool call: risk, arguments and a scrollable diff.
424
437
  *
@@ -444,12 +457,15 @@ export class ChatPrompt {
444
457
  }
445
458
 
446
459
  /** A plain yes/no, for the questions that are not tool approvals. */
447
- confirm(message) {
460
+ confirm(message, options = {}) {
448
461
  return new Promise((resolve) => {
449
462
  let settled = false;
450
463
  const settle = (answer) => { if (!settled) { settled = true; resolve(Boolean(answer)); } };
451
- const layer = new ConfirmLayer({ message, onAnswer: settle });
452
- layer.answer = () => settle(false);
464
+ const layer = new ConfirmLayer({
465
+ message,
466
+ defaultAnswer: options.defaultAnswer === true,
467
+ onAnswer: settle
468
+ });
453
469
  this.openLayer(layer);
454
470
  });
455
471
  }
@@ -546,19 +562,11 @@ export class ChatPrompt {
546
562
  return;
547
563
  }
548
564
 
549
- // A COMMAND IS NOT A MESSAGE, so it must not be drawn as one.
550
- //
551
- // This echoed every submitted line as a filled user block BEFORE the caller
552
- // had any chance to decide what the line was. Typing `/usage` therefore
553
- // rendered exactly like a sentence you had sent to the model — the command
554
- // did run, and its output appeared underneath, but the transcript showed a
555
- // conversation turn that never happened. opencode draws an invocation here,
556
- // not a message, and the difference is the whole reason a command line
557
- // exists.
565
+ // A command is control input, not conversation. Its handler decides whether
566
+ // to open a picker, show a text dialog, or perform an action. Keeping the
567
+ // invocation out of the transcript preserves the boundary between local UI
568
+ // and messages that were actually sent to the model.
558
569
  if (this.isCommand(text)) {
559
- this.commit('command', (width) => [
560
- `${paint('accent', GLYPHS.arrow)} ${bold(truncate(text, Math.max(8, width - 3), GLYPHS.ellipsis))}`
561
- ]);
562
570
  const settle = this.waiter;
563
571
  this.waiter = null;
564
572
  if (settle) settle(text);
package/src/tui/theme.js CHANGED
@@ -90,52 +90,54 @@ export const HAS_UNICODE = HAS_COLOR
90
90
  // Roles, not colours. A caller asking for `danger` should never care whether
91
91
  // that is truecolor crimson or plain ANSI red.
92
92
  const DARK_PALETTE = {
93
- accent: [ 96, 205, 255],
94
- agent: [138, 180, 248],
95
- user: [ 90, 200, 160],
96
- success: [126, 199, 108],
93
+ // Causyne brand palette from the canonical site/logo reference:
94
+ // signal #b6e06f, text #e9edf2, muted #8d96a4, line #272c35,
95
+ // panel #161920 and background #0f1115.
96
+ accent: [182, 224, 111],
97
+ agent: [182, 224, 111],
98
+ user: [154, 199, 86],
99
+ success: [182, 224, 111],
97
100
  warn: [230, 190, 90],
98
101
  danger: [235, 110, 110],
99
- muted: [130, 138, 150],
100
- ink: [225, 228, 234],
101
- rule: [ 62, 68, 80],
102
- // The scroll rail. A deliberately DARK blue, not the accent.
102
+ muted: [141, 150, 164],
103
+ ink: [233, 237, 242],
104
+ rule: [ 39, 44, 53],
105
+ // The scroll rail uses a deliberately darker brand green than the accent.
103
106
  //
104
107
  // The rail is orientation, not information you act on — it says where you are
105
108
  // while you read something else, so it must be legible without competing with
106
- // the transcript for attention. In `accent` cyan it was the brightest thing on
107
- // a screen full of prose. Dark enough to recede, blue enough to read as one
108
- // continuous object beside the markers, which keep their own roles.
109
- scroll: [ 58, 110, 170],
109
+ // the transcript for attention. Dark enough to recede, while remaining part
110
+ // of the same graphite-and-signal-green visual system as the rest of the CLI.
111
+ scroll: [ 99, 125, 63],
110
112
  // Background roles. opencode fills its input surface with theme
111
113
  // backgroundElement and marks it with a left border in the agent's colour
112
114
  // (component/prompt/index.tsx: border={["left"]} + backgroundColor). These are
113
115
  // that fill: one step off the terminal's own background, never a block of
114
116
  // colour that fights the text sitting on it.
115
- surface: [ 28, 31, 38],
116
- surfaceHi:[ 38, 42, 51]
117
+ surface: [ 22, 25, 32],
118
+ surfaceHi:[ 27, 31, 39]
117
119
  };
118
120
 
119
121
  const LIGHT_PALETTE = {
120
- accent: [ 0, 110, 170],
121
- agent: [ 40, 80, 170],
122
- user: [ 20, 120, 90],
123
- success: [ 30, 120, 40],
122
+ accent: [ 91, 130, 29],
123
+ agent: [ 91, 130, 29],
124
+ user: [ 74, 112, 26],
125
+ success: [ 74, 112, 26],
124
126
  warn: [155, 110, 0],
125
127
  danger: [185, 40, 40],
126
- muted: [ 95, 102, 112],
127
- ink: [ 25, 28, 34],
128
- rule: [200, 204, 212],
129
- scroll: [ 60, 100, 160],
128
+ muted: [105, 112, 123],
129
+ ink: [ 58, 63, 71],
130
+ rule: [198, 203, 211],
131
+ scroll: [ 91, 130, 29],
130
132
  surface: [242, 244, 247],
131
133
  surfaceHi:[232, 235, 240]
132
134
  };
133
135
 
134
136
  // Fallback for 16-colour terminals: the closest base ANSI code per role.
135
137
  const BASIC_CODES = {
136
- accent: 36, agent: 34, user: 32, success: 32,
138
+ accent: 92, agent: 92, user: 32, success: 92,
137
139
  warn: 33, danger: 31, muted: 90, ink: 39, rule: 90,
138
- scroll: 34,
140
+ scroll: 32,
139
141
  surface: 39, surfaceHi: 39
140
142
  };
141
143
 
@@ -1,12 +0,0 @@
1
- import rawInquirer from 'inquirer';
2
-
3
-
4
- const inquirer = rawInquirer?.prompt
5
- ? rawInquirer
6
- : (rawInquirer?.default?.prompt
7
- ? rawInquirer.default
8
- : (rawInquirer?.default?.default?.prompt
9
- ? rawInquirer.default.default
10
- : rawInquirer));
11
-
12
- export default inquirer;