@causyne/agent 1.10.0 → 1.10.5
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/LICENSE +1 -1
- package/README.md +6 -0
- package/docs/PROVENANCE.md +2 -2
- package/docs/THIRD_PARTY_NOTICES.md +12 -18
- package/package.json +3 -10
- package/src/cliMode.js +16 -14
- package/src/entrypoint.js +24 -0
- package/src/sandbox.js +17 -39
- package/src/tui/ask.js +2 -2
- package/src/tui/banner.js +56 -11
- package/src/tui/dialogs.js +14 -6
- package/src/tui/invocation.js +2 -0
- package/src/tui/keys.js +11 -0
- package/src/tui/prompt.js +7 -4
- package/src/tui/theme.js +26 -24
- package/src/utils/inquirer.js +0 -12
package/LICENSE
CHANGED
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.
|
package/docs/PROVENANCE.md
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Causyne Agent is the renamed continuation of **Vanexa Agent**. It is not
|
|
4
4
|
presented as an unrelated project or as a way to bypass
|
|
5
|
-
security classification. Version 1.10.0
|
|
6
|
-
to the 1.9.20 working tree.
|
|
5
|
+
security classification. Version 1.10.0 was the first published local-source
|
|
6
|
+
migration successor to the 1.9.20 working tree.
|
|
7
7
|
|
|
8
8
|
The previous npm identities were `@ikbal_fadilah_vanexa01/vanexa-agent` and
|
|
9
9
|
`@vanexalabs-ai/vanexa-agent`. The dispute at
|
|
@@ -9,23 +9,17 @@ license identifiers:
|
|
|
9
9
|
|
|
10
10
|
| License expression | Lockfile package entries |
|
|
11
11
|
| --- | ---: |
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
@@ -42,7 +36,7 @@ the licenses of third-party dependencies.
|
|
|
42
36
|
|
|
43
37
|
The terms section of `LICENSE` was normalized and compared with the SPDX
|
|
44
38
|
BUSL-1.1 canonical text during this review; it matched. The project-specific
|
|
45
|
-
parameter block selects the named licensor, Causyne Agent 1.10.
|
|
39
|
+
parameter block selects the named licensor, Causyne Agent 1.10.1 as the licensed
|
|
46
40
|
work, 2030-08-09 as the change date, and Apache-2.0 as the change
|
|
47
41
|
license. The Additional Use Grant permits production use except a paid
|
|
48
42
|
competing commercial offering as defined in that block. These business terms
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@causyne/agent",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.5",
|
|
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';
|
|
@@ -78,7 +77,7 @@ function makeApprovalHandler(conn, dialogs = null) {
|
|
|
78
77
|
conn.stopSpinner();
|
|
79
78
|
|
|
80
79
|
// An interactive session asks in a panel that shows the risk, the arguments
|
|
81
|
-
// and a scrollable diff in one frame. The
|
|
80
|
+
// and a scrollable diff in one frame. The plain fallback asks the same
|
|
82
81
|
// question in the scrollback, which is all a non-TTY session can do.
|
|
83
82
|
if (dialogs && detail) {
|
|
84
83
|
// Loops only for "Allow always" → Cancel: backing out of the standing
|
|
@@ -100,7 +99,11 @@ function makeApprovalHandler(conn, dialogs = null) {
|
|
|
100
99
|
// The text says exactly what is being given up and for how long.
|
|
101
100
|
const confirmed = await dialogs.confirm(
|
|
102
101
|
`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
|
|
102
|
+
+ ` This lasts until Causyne restarts. Undo it any time with /approvals clear.`,
|
|
103
|
+
// Reaching this question already required moving onto Allow always
|
|
104
|
+
// and pressing Enter. A second Enter is therefore an explicit second
|
|
105
|
+
// confirmation, rather than a reflex approval on the first dialog.
|
|
106
|
+
{ defaultAnswer: true }
|
|
104
107
|
);
|
|
105
108
|
if (confirmed) break;
|
|
106
109
|
// Cancelled: ask again rather than denying something they never rejected.
|
|
@@ -139,7 +142,7 @@ function makeApprovalHandler(conn, dialogs = null) {
|
|
|
139
142
|
|
|
140
143
|
// No live region to draw a panel into (a piped session, or raw mode
|
|
141
144
|
// refused). askConfirm degrades to readline on its own, so this path keeps
|
|
142
|
-
// the same wording as the panel instead of
|
|
145
|
+
// the same wording as the panel instead of a second prompt framework's.
|
|
143
146
|
const approved = await askConfirm(message);
|
|
144
147
|
|
|
145
148
|
if (conn.pendingApprovals) conn.pendingApprovals.delete(toolCallId);
|
|
@@ -322,7 +325,7 @@ export async function chatMode({ home = false } = {}) {
|
|
|
322
325
|
// The prompt owns the keyboard so the chat can offer history, completion and
|
|
323
326
|
// typing while a turn runs. It is skipped when stdin is not a terminal (piped
|
|
324
327
|
// input, CI) or when CAUSYNE_SIMPLE_INPUT=1, and any failure to take raw mode
|
|
325
|
-
// falls back to the
|
|
328
|
+
// falls back to the plain readline path below — a better editor must never be the
|
|
326
329
|
// reason chat refuses to start.
|
|
327
330
|
let prompt = null;
|
|
328
331
|
if (isInteractive()) {
|
|
@@ -367,9 +370,9 @@ export async function chatMode({ home = false } = {}) {
|
|
|
367
370
|
// one that was asked for.
|
|
368
371
|
if (surface) prompt.draw();
|
|
369
372
|
|
|
370
|
-
// Ask for approval in the prompt's own panel, not
|
|
373
|
+
// Ask for approval in the prompt's own panel, not a competing reader.
|
|
371
374
|
//
|
|
372
|
-
//
|
|
375
|
+
// A second prompt reader would take stdin while this prompt already holds it in
|
|
373
376
|
// raw mode: the two contend, and the question can land on a half-drawn
|
|
374
377
|
// prompt. The panel shows the risk, the arguments and a scrollable diff in
|
|
375
378
|
// one frame, and dismissing it denies.
|
|
@@ -394,12 +397,11 @@ export async function chatMode({ home = false } = {}) {
|
|
|
394
397
|
|
|
395
398
|
const readTask = async () => {
|
|
396
399
|
if (prompt) return prompt.read();
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
return task;
|
|
400
|
+
// The shared question helper already provides the same plain-readline
|
|
401
|
+
// fallback when a terminal cannot host the live TUI. Keeping a second
|
|
402
|
+
// prompt framework here pulled a large, unused transitive graph into every
|
|
403
|
+
// installation and made the fallback look different from the main CLI.
|
|
404
|
+
return askText({ title: 'You' });
|
|
403
405
|
};
|
|
404
406
|
|
|
405
407
|
const finish = () => { if (prompt) prompt.stop(); };
|
|
@@ -568,7 +570,7 @@ export async function interactiveConfig(options) {
|
|
|
568
570
|
return;
|
|
569
571
|
}
|
|
570
572
|
|
|
571
|
-
// Asked in the product's own panels rather than
|
|
573
|
+
// Asked in the product's own panels rather than a second prompt framework.
|
|
572
574
|
//
|
|
573
575
|
// These were the last two surfaces on a second design system — different
|
|
574
576
|
// colours, different glyphs, no NO_COLOR handling, and a list widget nothing
|
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
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
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
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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:
|
|
247
|
-
// gets a human in the loop rather than
|
|
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
|
|
6
|
-
//
|
|
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
|
|
27
|
-
//
|
|
28
|
-
|
|
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
|
|
39
|
-
const rows = art.map((line) => paint('accent', line));
|
|
73
|
+
const rows = brandArt(width, unicode);
|
|
40
74
|
rows.push('');
|
|
41
|
-
rows.push(dim('Causyne
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
|
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 ? '' :
|
|
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; }
|
package/src/tui/dialogs.js
CHANGED
|
@@ -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
|
-
*
|
|
303
|
-
*
|
|
304
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
527
|
-
//
|
|
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/invocation.js
CHANGED
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
|
|
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.
|
|
@@ -444,12 +444,15 @@ export class ChatPrompt {
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
/** A plain yes/no, for the questions that are not tool approvals. */
|
|
447
|
-
confirm(message) {
|
|
447
|
+
confirm(message, options = {}) {
|
|
448
448
|
return new Promise((resolve) => {
|
|
449
449
|
let settled = false;
|
|
450
450
|
const settle = (answer) => { if (!settled) { settled = true; resolve(Boolean(answer)); } };
|
|
451
|
-
const layer = new ConfirmLayer({
|
|
452
|
-
|
|
451
|
+
const layer = new ConfirmLayer({
|
|
452
|
+
message,
|
|
453
|
+
defaultAnswer: options.defaultAnswer === true,
|
|
454
|
+
onAnswer: settle
|
|
455
|
+
});
|
|
453
456
|
this.openLayer(layer);
|
|
454
457
|
});
|
|
455
458
|
}
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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: [
|
|
100
|
-
ink: [
|
|
101
|
-
rule: [
|
|
102
|
-
// The scroll rail
|
|
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.
|
|
107
|
-
//
|
|
108
|
-
|
|
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: [
|
|
116
|
-
surfaceHi:[
|
|
117
|
+
surface: [ 22, 25, 32],
|
|
118
|
+
surfaceHi:[ 27, 31, 39]
|
|
117
119
|
};
|
|
118
120
|
|
|
119
121
|
const LIGHT_PALETTE = {
|
|
120
|
-
accent: [
|
|
121
|
-
agent: [
|
|
122
|
-
user: [
|
|
123
|
-
success: [
|
|
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: [
|
|
127
|
-
ink: [
|
|
128
|
-
rule: [
|
|
129
|
-
scroll: [
|
|
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:
|
|
138
|
+
accent: 92, agent: 92, user: 32, success: 92,
|
|
137
139
|
warn: 33, danger: 31, muted: 90, ink: 39, rule: 90,
|
|
138
|
-
scroll:
|
|
140
|
+
scroll: 32,
|
|
139
141
|
surface: 39, surfaceHi: 39
|
|
140
142
|
};
|
|
141
143
|
|
package/src/utils/inquirer.js
DELETED
|
@@ -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;
|