@causyne/agent 1.10.5 → 1.10.11
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 +19 -1
- package/bin/causyne.js +5 -0
- package/package.json +1 -1
- package/src/autoUpdate.js +221 -7
- package/src/cliMode.js +35 -2
- package/src/commands.js +19 -4
- package/src/entrypoint.js +10 -2
- package/src/gitSnapshot.js +7 -0
- package/src/tui/home.js +16 -7
- package/src/tui/prompt.js +19 -14
- package/src/version.js +2 -2
package/README.md
CHANGED
|
@@ -12,11 +12,29 @@ documentation is intentionally concise while the project is evolving.
|
|
|
12
12
|
|
|
13
13
|
Learn more at [causyne.com](https://causyne.com/).
|
|
14
14
|
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
On macOS, Linux, or WSL:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
curl -fsSL https://causyne.com/install.sh | bash
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On Windows PowerShell:
|
|
24
|
+
|
|
25
|
+
```powershell
|
|
26
|
+
irm https://causyne.com/install.ps1 | iex
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Open a project directory and run `causyne`. Managed installations check the
|
|
30
|
+
official npm release channel for updates. Run `causyne doctor` to inspect the
|
|
31
|
+
runtime and update channel.
|
|
32
|
+
|
|
15
33
|
## Contact
|
|
16
34
|
|
|
17
35
|
- General inquiries and business communication: [contact@causyne.com](mailto:contact@causyne.com)
|
|
18
36
|
- Product support and troubleshooting: [help@causyne.com](mailto:help@causyne.com)
|
|
19
|
-
- Partnerships and direct
|
|
37
|
+
- Partnerships and direct communication: [ikbal@causyne.com](mailto:ikbal@causyne.com)
|
|
20
38
|
|
|
21
39
|
## Development
|
|
22
40
|
|
package/bin/causyne.js
CHANGED
|
@@ -5,6 +5,11 @@ if (Number(process.versions.node.split('.')[0]) < 24) {
|
|
|
5
5
|
process.exit(1);
|
|
6
6
|
}
|
|
7
7
|
try {
|
|
8
|
+
const { startUpdateChecker } = await import('../src/autoUpdate.js');
|
|
9
|
+
const update = await startUpdateChecker();
|
|
10
|
+
if (update && update.updated) {
|
|
11
|
+
process.stderr.write(`Causyne updated to ${update.latest}. Starting the new version.\n`);
|
|
12
|
+
}
|
|
8
13
|
await import('../src/entrypoint.js');
|
|
9
14
|
} catch (error) {
|
|
10
15
|
if (process.stdout.isTTY) process.stdout.write('\x1b[?1006l\x1b[?1000l\x1b[?2004l\x1b[?25h');
|
package/package.json
CHANGED
package/src/autoUpdate.js
CHANGED
|
@@ -1,12 +1,226 @@
|
|
|
1
|
+
// Causyne package updates.
|
|
2
|
+
//
|
|
3
|
+
// Updates are intentionally limited to installations managed by npm or by the
|
|
4
|
+
// causyne.com installer. A source checkout is never rewritten. Commands are
|
|
5
|
+
// passed as argv arrays with shell disabled, and the only accepted package is
|
|
6
|
+
// the public @causyne/agent package from the npm registry.
|
|
7
|
+
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
|
|
1
13
|
import { UI } from './utils/cli_ui.js';
|
|
14
|
+
import { causyneHome } from './utils/paths.js';
|
|
15
|
+
import { getPackageVersion } from './version.js';
|
|
16
|
+
|
|
17
|
+
const PACKAGE_NAME = '@causyne/agent';
|
|
18
|
+
const REGISTRY_LATEST = 'https://registry.npmjs.org/@causyne%2fagent/latest';
|
|
19
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
20
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
21
|
+
|
|
22
|
+
function updateStateFile(options = {}) {
|
|
23
|
+
if (options.stateFile) return path.resolve(options.stateFile);
|
|
24
|
+
const env = options.env || process.env;
|
|
25
|
+
const root = env.CAUSYNE_HOME && String(env.CAUSYNE_HOME).trim()
|
|
26
|
+
? path.resolve(String(env.CAUSYNE_HOME).trim())
|
|
27
|
+
: causyneHome();
|
|
28
|
+
return path.join(root, 'update-state.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function versionParts(version) {
|
|
32
|
+
const match = String(version || '').trim().match(/^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
33
|
+
if (!match) return null;
|
|
34
|
+
return { numbers: match.slice(1, 4).map(Number), prerelease: match[4] || '' };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function compareVersions(left, right) {
|
|
38
|
+
const a = versionParts(left);
|
|
39
|
+
const b = versionParts(right);
|
|
40
|
+
if (!a || !b) return 0;
|
|
41
|
+
for (let index = 0; index < 3; index++) {
|
|
42
|
+
if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] > b.numbers[index] ? 1 : -1;
|
|
43
|
+
}
|
|
44
|
+
if (a.prerelease === b.prerelease) return 0;
|
|
45
|
+
if (!a.prerelease) return 1;
|
|
46
|
+
if (!b.prerelease) return -1;
|
|
47
|
+
const aParts = a.prerelease.split('.');
|
|
48
|
+
const bParts = b.prerelease.split('.');
|
|
49
|
+
const length = Math.max(aParts.length, bParts.length);
|
|
50
|
+
for (let index = 0; index < length; index++) {
|
|
51
|
+
if (aParts[index] === undefined) return -1;
|
|
52
|
+
if (bParts[index] === undefined) return 1;
|
|
53
|
+
if (aParts[index] === bParts[index]) continue;
|
|
54
|
+
const aNumeric = /^\d+$/.test(aParts[index]);
|
|
55
|
+
const bNumeric = /^\d+$/.test(bParts[index]);
|
|
56
|
+
if (aNumeric && bNumeric) return Number(aParts[index]) > Number(bParts[index]) ? 1 : -1;
|
|
57
|
+
if (aNumeric !== bNumeric) return aNumeric ? -1 : 1;
|
|
58
|
+
return aParts[index].localeCompare(bParts[index]);
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function managedPrefix(packageRoot = PACKAGE_ROOT, env = process.env) {
|
|
64
|
+
if (env.CAUSYNE_INSTALL_ROOT) return path.resolve(env.CAUSYNE_INSTALL_ROOT);
|
|
65
|
+
|
|
66
|
+
const markers = [
|
|
67
|
+
path.join('lib', 'node_modules', '@causyne', 'agent'),
|
|
68
|
+
path.join('node_modules', '@causyne', 'agent')
|
|
69
|
+
];
|
|
70
|
+
for (const marker of markers) {
|
|
71
|
+
const index = packageRoot.lastIndexOf(marker);
|
|
72
|
+
if (index >= 0 && index + marker.length === packageRoot.length) {
|
|
73
|
+
return packageRoot.slice(0, index).replace(/[\\/]$/, '');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function installationInfo(options = {}) {
|
|
80
|
+
const env = options.env || process.env;
|
|
81
|
+
const packageRoot = options.packageRoot || PACKAGE_ROOT;
|
|
82
|
+
const prefix = managedPrefix(packageRoot, env);
|
|
83
|
+
return {
|
|
84
|
+
managed: Boolean(prefix),
|
|
85
|
+
type: env.CAUSYNE_INSTALL_ROOT ? 'causyne.com installer' : (prefix ? 'global npm' : 'source checkout'),
|
|
86
|
+
prefix,
|
|
87
|
+
packageRoot,
|
|
88
|
+
npmCommand: env.CAUSYNE_NPM_COMMAND || (process.platform === 'win32' ? 'npm.cmd' : 'npm')
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readState(options = {}) {
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(fs.readFileSync(updateStateFile(options), 'utf8'));
|
|
95
|
+
} catch {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function writeState(state, options = {}) {
|
|
101
|
+
try {
|
|
102
|
+
const stateFile = updateStateFile(options);
|
|
103
|
+
fs.mkdirSync(path.dirname(stateFile), { recursive: true, mode: 0o700 });
|
|
104
|
+
fs.writeFileSync(stateFile, JSON.stringify(state, null, 2), { mode: 0o600 });
|
|
105
|
+
} catch {
|
|
106
|
+
// Update state is an optimisation. Failure must never stop the CLI.
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function latestPackageVersion(options = {}) {
|
|
111
|
+
const fetchImpl = options.fetchImpl || globalThis.fetch;
|
|
112
|
+
if (typeof fetchImpl !== 'function') throw new Error('This Node.js runtime has no fetch implementation.');
|
|
113
|
+
const timeoutMs = options.timeoutMs || 3500;
|
|
114
|
+
const signal = options.signal || AbortSignal.timeout(timeoutMs);
|
|
115
|
+
const response = await fetchImpl(REGISTRY_LATEST, {
|
|
116
|
+
headers: { accept: 'application/json', 'user-agent': `causyne/${getPackageVersion()}` },
|
|
117
|
+
signal
|
|
118
|
+
});
|
|
119
|
+
if (!response.ok) throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
120
|
+
const metadata = await response.json();
|
|
121
|
+
if (metadata.name !== PACKAGE_NAME || !versionParts(metadata.version)) {
|
|
122
|
+
throw new Error('npm registry returned invalid package metadata');
|
|
123
|
+
}
|
|
124
|
+
return metadata.version;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function installLatest(info, options = {}) {
|
|
128
|
+
const runner = options.spawnSyncImpl || spawnSync;
|
|
129
|
+
const args = [
|
|
130
|
+
'install', '--global', '--prefix', info.prefix,
|
|
131
|
+
`${PACKAGE_NAME}@${options.version}`, '--ignore-scripts', '--no-audit', '--no-fund'
|
|
132
|
+
];
|
|
133
|
+
const result = runner(info.npmCommand, args, {
|
|
134
|
+
shell: false,
|
|
135
|
+
stdio: options.verbose ? 'inherit' : 'pipe',
|
|
136
|
+
encoding: 'utf8',
|
|
137
|
+
env: options.env || process.env
|
|
138
|
+
});
|
|
139
|
+
if (result.error) throw result.error;
|
|
140
|
+
if (result.status !== 0) {
|
|
141
|
+
const detail = String(result.stderr || result.stdout || '').trim();
|
|
142
|
+
throw new Error(detail || `npm exited with status ${result.status}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function checkForUpdate(options = {}) {
|
|
147
|
+
const current = options.currentVersion || getPackageVersion();
|
|
148
|
+
const latest = await latestPackageVersion(options);
|
|
149
|
+
return { current, latest, available: compareVersions(latest, current) > 0 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export async function runSelfUpdate(verbose = false, options = {}) {
|
|
153
|
+
const info = installationInfo(options);
|
|
154
|
+
if (!info.managed) {
|
|
155
|
+
if (verbose) UI.printInfo('This is a source checkout. Update it with Git; Causyne will not rewrite developer source.');
|
|
156
|
+
return { updated: false, reason: 'source-checkout', ...info };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const status = await checkForUpdate(options);
|
|
161
|
+
if (!status.available) {
|
|
162
|
+
writeState({ checkedAt: Date.now(), latest: status.latest }, options);
|
|
163
|
+
if (verbose) UI.printSuccess(`Causyne ${status.current} is up to date.`);
|
|
164
|
+
return { updated: false, reason: 'current', ...status, ...info };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (verbose) UI.printInfo(`Updating Causyne ${status.current} → ${status.latest}...`);
|
|
168
|
+
installLatest(info, { ...options, version: status.latest, verbose });
|
|
169
|
+
writeState({ checkedAt: Date.now(), latest: status.latest, installedAt: Date.now() }, options);
|
|
170
|
+
if (verbose) UI.printSuccess(`Updated to Causyne ${status.latest}.`);
|
|
171
|
+
return { updated: true, ...status, ...info };
|
|
172
|
+
} catch (error) {
|
|
173
|
+
writeState({ checkedAt: Date.now(), error: error.message }, options);
|
|
174
|
+
if (verbose) UI.printError(`Update failed: ${error.message}`);
|
|
175
|
+
return { updated: false, reason: 'failed', error: error.message, ...info };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Check on launch, at most once every six hours. */
|
|
180
|
+
export async function startUpdateChecker(options = {}) {
|
|
181
|
+
const env = options.env || process.env;
|
|
182
|
+
if (env.CAUSYNE_DISABLE_AUTOUPDATE === '1' || env.CAUSYNE_DISABLE_AUTOUPDATE === 'true') {
|
|
183
|
+
return { updated: false, reason: 'disabled' };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const info = installationInfo(options);
|
|
187
|
+
if (!info.managed) return { updated: false, reason: 'source-checkout', ...info };
|
|
188
|
+
|
|
189
|
+
const state = readState(options);
|
|
190
|
+
if (!options.force && Number.isFinite(state.checkedAt)
|
|
191
|
+
&& Date.now() - state.checkedAt < CHECK_INTERVAL_MS) {
|
|
192
|
+
return { updated: false, reason: 'recently-checked', latest: state.latest, ...info };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return runSelfUpdate(false, options);
|
|
196
|
+
}
|
|
2
197
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
198
|
+
export async function diagnoseInstallation(options = {}) {
|
|
199
|
+
const info = installationInfo(options);
|
|
200
|
+
let update;
|
|
201
|
+
try {
|
|
202
|
+
update = await checkForUpdate(options);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
update = { current: getPackageVersion(), latest: null, available: false, error: error.message };
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
...info,
|
|
208
|
+
...update,
|
|
209
|
+
autoUpdates: !['1', 'true'].includes(String((options.env || process.env).CAUSYNE_DISABLE_AUTOUPDATE || '').toLowerCase())
|
|
210
|
+
};
|
|
8
211
|
}
|
|
9
212
|
|
|
10
|
-
export function
|
|
11
|
-
|
|
213
|
+
export function formatDoctorReport(status, runtime = process) {
|
|
214
|
+
const rows = [
|
|
215
|
+
`Version : ${status.current}`,
|
|
216
|
+
`Latest : ${status.latest || `unavailable (${status.error || 'unknown error'})`}`,
|
|
217
|
+
`Installation : ${status.type}`,
|
|
218
|
+
`Package root : ${status.packageRoot}`,
|
|
219
|
+
`Node.js : ${runtime.version}`,
|
|
220
|
+
`Platform : ${runtime.platform} ${runtime.arch}`,
|
|
221
|
+
`Auto updates : ${status.autoUpdates ? 'enabled' : 'disabled'}`,
|
|
222
|
+
`Workspace : ${runtime.cwd()}`
|
|
223
|
+
];
|
|
224
|
+
if (status.available) rows.push('Update : available — run `causyne update`');
|
|
225
|
+
return rows.join('\n');
|
|
12
226
|
}
|
package/src/cliMode.js
CHANGED
|
@@ -11,6 +11,7 @@ import { wrapBlock } from './tui/text.js';
|
|
|
11
11
|
import { getPackageVersion } from './version.js';
|
|
12
12
|
import { resolveCharPool } from './contextManager.js';
|
|
13
13
|
import { PRIMARY_ACTIONS, homeStatusLine, homeHint, moreLayer, configureInPrompt, mcpPicker, PROVIDERS } from './tui/home.js';
|
|
14
|
+
import { SelectLayer } from './tui/dialogs.js';
|
|
14
15
|
import { askSelect, askText, askConfirm } from './tui/ask.js';
|
|
15
16
|
import { renderReport } from './reporting.js';
|
|
16
17
|
import { grantAlways } from './approvalGrants.js';
|
|
@@ -352,8 +353,8 @@ export async function chatMode({ home = false } = {}) {
|
|
|
352
353
|
root: process.cwd(),
|
|
353
354
|
commands: listCommands('terminal'),
|
|
354
355
|
// The rule for "is this a command" lives in commands.js and is handed
|
|
355
|
-
// down rather than reimplemented, so the prompt
|
|
356
|
-
//
|
|
356
|
+
// down rather than reimplemented, so the prompt keeps `/usage` out of
|
|
357
|
+
// the conversation instead of drawing it as something the user said.
|
|
357
358
|
isCommand: isCommandInput,
|
|
358
359
|
surface,
|
|
359
360
|
onInterrupt: () => loop.abort(),
|
|
@@ -458,6 +459,38 @@ export async function chatMode({ home = false } = {}) {
|
|
|
458
459
|
continue;
|
|
459
460
|
}
|
|
460
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
|
+
|
|
461
494
|
// A skill was invoked: it becomes part of the REQUEST, not terminal output.
|
|
462
495
|
//
|
|
463
496
|
// This is the whole point. `/skills <id>` used to print the file and call
|
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
|
-
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { program } from 'commander';
|
|
2
2
|
|
|
3
3
|
import { runTask, chatMode, execCommand, interactiveConfig } from './cliMode.js';
|
|
4
|
-
import { runSelfUpdate } from './autoUpdate.js';
|
|
4
|
+
import { runSelfUpdate, diagnoseInstallation, formatDoctorReport } 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';
|
|
@@ -70,11 +70,19 @@ program
|
|
|
70
70
|
|
|
71
71
|
program
|
|
72
72
|
.command('update')
|
|
73
|
-
.description('
|
|
73
|
+
.description('Check for and install the latest Causyne Agent release')
|
|
74
74
|
.action(async () => {
|
|
75
75
|
await runSelfUpdate(true);
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
+
program
|
|
79
|
+
.command('doctor')
|
|
80
|
+
.description('Inspect the installation, runtime and update channel')
|
|
81
|
+
.action(async () => {
|
|
82
|
+
const status = await diagnoseInstallation();
|
|
83
|
+
printReport(formatDoctorReport(status), { title: 'Doctor' });
|
|
84
|
+
});
|
|
85
|
+
|
|
78
86
|
program
|
|
79
87
|
.command('status')
|
|
80
88
|
.description('Show local provider, workspace and runtime status')
|
package/src/gitSnapshot.js
CHANGED
|
@@ -164,6 +164,13 @@ export function initShadow(worktree) {
|
|
|
164
164
|
}
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
// A checkpoint promises the exact bytes that were present when it was
|
|
168
|
+
// captured. Git for Windows commonly inherits core.autocrlf=true from the
|
|
169
|
+
// host, which would turn an LF blob into CRLF during restore. This shadow
|
|
170
|
+
// repository is a byte store, so text conversion must stay disabled even
|
|
171
|
+
// when an existing shadow is reused after an upgrade.
|
|
172
|
+
runGit(['--git-dir', gitDir, 'config', 'core.autocrlf', 'false']);
|
|
173
|
+
|
|
167
174
|
// Point at the user's object database so blobs are never re-hashed, and adopt
|
|
168
175
|
// their index so already-hashed entries are reused. Both are best-effort: a
|
|
169
176
|
// missing or incompatible source just means a full, slower add.
|
package/src/tui/home.js
CHANGED
|
@@ -42,17 +42,19 @@ export const PRIMARY_ACTIONS = [
|
|
|
42
42
|
run: async () => (await import('../reporting.js')).buildUsageText()
|
|
43
43
|
},
|
|
44
44
|
{ id: 'config', label: 'config', hint: 'provider, model, API key', kind: 'config' },
|
|
45
|
-
{ id: 'more', label: 'more…', hint: '
|
|
45
|
+
{ id: 'more', label: 'more…', hint: 'sessions · model · approvals · tools', kind: 'more' }
|
|
46
46
|
];
|
|
47
47
|
|
|
48
48
|
/**
|
|
49
49
|
* The second page.
|
|
50
50
|
*
|
|
51
|
-
* These are real
|
|
52
|
-
*
|
|
53
|
-
* it would make it undiscoverable.
|
|
51
|
+
* These are real local capabilities kept one level down so the first screen
|
|
52
|
+
* stays readable while the complete command surface remains discoverable.
|
|
54
53
|
*/
|
|
55
54
|
export const MORE_ACTIONS = [
|
|
55
|
+
{ id: 'sessions', label: 'sessions', category: 'workspace', hint: 'browse saved local sessions', kind: 'command', command: '/sessions' },
|
|
56
|
+
{ id: 'model', label: 'model', category: 'workspace', hint: 'show the active provider and model', kind: 'command', command: '/model' },
|
|
57
|
+
{ id: 'approvals', label: 'approvals', category: 'workspace', hint: 'review standing session approvals', kind: 'command', command: '/approvals' },
|
|
56
58
|
{
|
|
57
59
|
id: 'context', label: 'context', category: 'reports', hint: 'what fills the context window', kind: 'stay',
|
|
58
60
|
run: async () => (await import('../reporting.js')).buildContextText()
|
|
@@ -69,14 +71,21 @@ export const MORE_ACTIONS = [
|
|
|
69
71
|
command: '/undo'
|
|
70
72
|
},
|
|
71
73
|
{ id: 'mcp', label: 'mcp', category: 'tools', hint: 'manage MCP servers', kind: 'mcp' },
|
|
74
|
+
{ id: 'help', label: 'commands', category: 'tools', hint: 'show every slash command', kind: 'command', command: '/help' },
|
|
75
|
+
{
|
|
76
|
+
id: 'doctor', label: 'doctor', category: 'tools', hint: 'installation, runtime and update channel', kind: 'stay',
|
|
77
|
+
run: async () => {
|
|
78
|
+
const { diagnoseInstallation, formatDoctorReport } = await import('../autoUpdate.js');
|
|
79
|
+
return formatDoctorReport(await diagnoseInstallation());
|
|
80
|
+
}
|
|
81
|
+
},
|
|
72
82
|
{
|
|
73
83
|
id: 'update', label: 'update', category: 'tools', hint: 'update to the latest version', kind: 'takeover',
|
|
74
84
|
run: async () => {
|
|
75
85
|
const { runSelfUpdate } = await import('../autoUpdate.js');
|
|
76
86
|
await runSelfUpdate(true);
|
|
77
87
|
}
|
|
78
|
-
}
|
|
79
|
-
|
|
88
|
+
}
|
|
80
89
|
];
|
|
81
90
|
|
|
82
91
|
/** Items in the shape SelectList wants, for the `more…` page. */
|
|
@@ -207,7 +216,7 @@ export async function mcpPicker(prompt, onAction) {
|
|
|
207
216
|
items: rows.map((server) => ({
|
|
208
217
|
id: server.name,
|
|
209
218
|
label: server.name,
|
|
210
|
-
description: `${server.granted ? 'granted' : 'not granted'}${server.
|
|
219
|
+
description: `${server.granted ? 'granted' : 'not granted'}${server.target ? ` ${GLYPHS.dot} ${server.target}` : ''}`,
|
|
211
220
|
category: server.granted ? 'granted' : 'available',
|
|
212
221
|
value: server
|
|
213
222
|
})),
|
package/src/tui/prompt.js
CHANGED
|
@@ -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
|
*
|
|
@@ -549,19 +562,11 @@ export class ChatPrompt {
|
|
|
549
562
|
return;
|
|
550
563
|
}
|
|
551
564
|
|
|
552
|
-
// A
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
//
|
|
556
|
-
// rendered exactly like a sentence you had sent to the model — the command
|
|
557
|
-
// did run, and its output appeared underneath, but the transcript showed a
|
|
558
|
-
// conversation turn that never happened. opencode draws an invocation here,
|
|
559
|
-
// not a message, and the difference is the whole reason a command line
|
|
560
|
-
// 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.
|
|
561
569
|
if (this.isCommand(text)) {
|
|
562
|
-
this.commit('command', (width) => [
|
|
563
|
-
`${paint('accent', GLYPHS.arrow)} ${bold(truncate(text, Math.max(8, width - 3), GLYPHS.ellipsis))}`
|
|
564
|
-
]);
|
|
565
570
|
const settle = this.waiter;
|
|
566
571
|
this.waiter = null;
|
|
567
572
|
if (settle) settle(text);
|
package/src/version.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
|
|
4
5
|
function getDir() {
|
|
5
6
|
if (typeof __dirname !== 'undefined') {
|
|
@@ -10,7 +11,7 @@ function getDir() {
|
|
|
10
11
|
if (metaUrl && typeof metaUrl === 'string') {
|
|
11
12
|
const url = new URL(metaUrl);
|
|
12
13
|
if (url.protocol === 'file:') {
|
|
13
|
-
return path.dirname(url
|
|
14
|
+
return path.dirname(fileURLToPath(url));
|
|
14
15
|
}
|
|
15
16
|
}
|
|
16
17
|
} catch (e) {}
|
|
@@ -35,4 +36,3 @@ export function getPackageVersion() {
|
|
|
35
36
|
|
|
36
37
|
return '1.3.14';
|
|
37
38
|
}
|
|
38
|
-
|