nexrall-code 0.5.107 → 0.5.108

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/dist/audit.js ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `--audit [path]` — the CLI's opt-in tool-call trail.
3
+ *
4
+ * WHY THE CLI HAS THIS AT ALL
5
+ *
6
+ * The trail was originally skipped here: an interactive terminal already has a
7
+ * human watching every permission prompt, so the human IS the oversight layer,
8
+ * and shipping a flag nobody asked for is just maintenance surface.
9
+ *
10
+ * Headless mode is what changes the answer. `--output-format json|stream-json`
11
+ * auto-approves every tool call (there is no TTY to ask on) and is meant for CI
12
+ * and scripted runs — i.e. an agent acting with nobody watching. That is where a
13
+ * trail is worth MOST, not least: without one there is no way to answer "what
14
+ * did last night's job actually touch?". The consumer that makes it useful also
15
+ * already exists, since stream-json output is machine-readable by design.
16
+ *
17
+ * Still OPT-IN, including in headless: writing an unrequested file into a user's
18
+ * working directory is not a default worth taking, and every other host
19
+ * (code-core, the SDK, Desktop) keeps auditing off unless asked. Opt-in
20
+ * consistency is itself a property worth having.
21
+ *
22
+ * State lives at module level rather than being threaded through runTurn()'s
23
+ * parameter list, matching setMode()/setAutoApprove() in permissions/. That list
24
+ * already carries ten positional parameters across six call sites; an eleventh
25
+ * would have to be added correctly six times, and the failure mode of missing
26
+ * one is a silently unaudited turn — the exact defect this feature exists to
27
+ * prevent, and the one already hit once in @nexrall/agent.
28
+ *
29
+ * See docs/NEXRALL_AUDIT_TRAIL_LAYER.md in the Nexrall repo.
30
+ */
31
+ import * as path from 'path';
32
+ import { createJsonlAuditSink, loadAuth } from '@nexrall/code-core';
33
+ let _state;
34
+ /**
35
+ * One sink per resolved path.
36
+ *
37
+ * Cached because the default path is derived from the session id, so `/clear`
38
+ * legitimately starts a NEW trail file, while an explicit `--audit <path>`
39
+ * keeps appending to one. Re-creating a sink per turn would also re-run the
40
+ * mkdir bookkeeping on every call for no benefit.
41
+ */
42
+ const _sinks = new Map();
43
+ /** Default trail location when `--audit` is given with no path. */
44
+ export function defaultAuditPath(workDir, sessionId) {
45
+ return path.join(workDir, '.nexrall', 'audit', `${sessionId}.jsonl`);
46
+ }
47
+ /**
48
+ * Resolve where this session's trail is written.
49
+ *
50
+ * An explicit path is resolved against the CWD the user invoked from, not
51
+ * `workDir`: a CI job typically wants the trail in its artifacts directory,
52
+ * which is usually not the repo being worked on.
53
+ */
54
+ export function resolveAuditPath(state) {
55
+ return state.filePath
56
+ ? path.resolve(state.filePath)
57
+ : defaultAuditPath(state.workDir, state.getSessionId());
58
+ }
59
+ /** Enable auditing for this process. Returns the path used for the first session. */
60
+ export function enableAudit(opts) {
61
+ _state = opts;
62
+ return resolveAuditPath(opts);
63
+ }
64
+ /**
65
+ * The audit config for the CURRENT turn, or undefined when `--audit` was not
66
+ * passed.
67
+ *
68
+ * Built per call (cheap: a map lookup plus an object literal) so a session id
69
+ * changed by `/clear` or `/resume` is picked up on the very next turn.
70
+ */
71
+ export function currentAudit() {
72
+ if (!_state)
73
+ return undefined;
74
+ const target = resolveAuditPath(_state);
75
+ let sink = _sinks.get(target);
76
+ if (!sink) {
77
+ sink = createJsonlAuditSink(target);
78
+ _sinks.set(target, sink);
79
+ }
80
+ const cfg = {
81
+ sink,
82
+ // Pinned per turn to the CLI's live session id, so a whole conversation —
83
+ // every turn, and every sub-agent it delegates to — shares one session in
84
+ // the trail. code-core mints an id per runAgentLoop() call and the REPL
85
+ // calls it once PER TURN, so leaving this unset would make each turn look
86
+ // like a separate session and break the link between a later write and the
87
+ // request that caused it. Same trap as @nexrall/agent's and Desktop's.
88
+ sessionId: _state.getSessionId(),
89
+ };
90
+ // The human half of dual attribution. Absent when the account has no email on
91
+ // record — never substituted with a placeholder, because a fabricated
92
+ // attribution in a compliance record is worse than a visibly missing one.
93
+ const email = loadAuth()?.email;
94
+ if (email)
95
+ cfg.userId = email;
96
+ return cfg;
97
+ }
98
+ /** Test seam — resets module state between cases. */
99
+ export function resetAudit() {
100
+ _state = undefined;
101
+ _sinks.clear();
102
+ }
@@ -0,0 +1,134 @@
1
+ import * as readline from 'readline';
2
+ import chalk from 'chalk';
3
+ import { login, saveAuth, clearAuth, isAuthenticated, getBalance, revokeRefreshToken } from '@nexrall/code-core';
4
+ // ─── Hidden Password Prompt ───────────────────────────────────────────────────
5
+ function prompt(question, hidden = false) {
6
+ return new Promise((resolve) => {
7
+ const rl = readline.createInterface({
8
+ input: process.stdin,
9
+ output: process.stdout,
10
+ terminal: true,
11
+ });
12
+ if (hidden) {
13
+ // Temporarily suppress echoing for password input
14
+ process.stdout.write(question);
15
+ let value = '';
16
+ const { stdin } = process;
17
+ const wasRaw = stdin.isRaw;
18
+ stdin.setRawMode(true);
19
+ stdin.resume();
20
+ stdin.setEncoding('utf8');
21
+ const onData = (char) => {
22
+ if (char === '\n' || char === '\r' || char === '') {
23
+ // Enter or Ctrl+C
24
+ stdin.setRawMode(wasRaw ?? false);
25
+ stdin.pause();
26
+ stdin.removeListener('data', onData);
27
+ rl.close();
28
+ process.stdout.write('\n');
29
+ if (char === '') {
30
+ process.exit(0);
31
+ }
32
+ resolve(value);
33
+ }
34
+ else if (char === '' || char === '\b') {
35
+ // Backspace
36
+ if (value.length > 0) {
37
+ value = value.slice(0, -1);
38
+ }
39
+ }
40
+ else {
41
+ value += char;
42
+ }
43
+ };
44
+ stdin.on('data', onData);
45
+ }
46
+ else {
47
+ rl.question(question, (answer) => {
48
+ rl.close();
49
+ resolve(answer.trim());
50
+ });
51
+ }
52
+ });
53
+ }
54
+ // ─── Auth Command ─────────────────────────────────────────────────────────────
55
+ export async function authCommand() {
56
+ console.log();
57
+ console.log(chalk.bold.blue(' Nexrall Code — Login'));
58
+ console.log(chalk.dim(' ─────────────────────'));
59
+ console.log();
60
+ if (isAuthenticated()) {
61
+ const overwrite = await prompt(chalk.yellow(' Already logged in. Login again? ') + chalk.dim('[y/n] > '));
62
+ if (overwrite !== 'y' && overwrite !== 'yes') {
63
+ console.log(chalk.dim(' Cancelled.'));
64
+ return;
65
+ }
66
+ console.log();
67
+ }
68
+ const email = await prompt(chalk.bold(' Email ') + chalk.dim('> '));
69
+ const password = await prompt(chalk.bold(' Password ') + chalk.dim('> '), true);
70
+ console.log();
71
+ const spinner = startSpinner('Logging in…');
72
+ try {
73
+ const authConfig = await login(email, password);
74
+ saveAuth(authConfig);
75
+ stopSpinner(spinner);
76
+ console.log(chalk.green(' ✓ Logged in as ') + chalk.bold(authConfig.email ?? email));
77
+ console.log();
78
+ // Show balance
79
+ try {
80
+ const balance = await getBalance();
81
+ console.log(chalk.dim(' Wallet balance: ') + chalk.cyan(`$${balance.toFixed(4)}`));
82
+ }
83
+ catch {
84
+ // Non-fatal — balance fetch can fail
85
+ }
86
+ console.log();
87
+ console.log(chalk.dim(' Run ') + chalk.bold('nex') + chalk.dim(' to start coding.'));
88
+ console.log();
89
+ }
90
+ catch (err) {
91
+ stopSpinner(spinner);
92
+ console.log();
93
+ console.log(chalk.red(' ✗ Login failed: ') + String(err.message));
94
+ console.log();
95
+ process.exit(1);
96
+ }
97
+ }
98
+ // ─── Logout Command ───────────────────────────────────────────────────────────
99
+ export async function logoutCommand() {
100
+ console.log();
101
+ if (!isAuthenticated()) {
102
+ console.log(chalk.dim(' Not logged in.'));
103
+ console.log();
104
+ return;
105
+ }
106
+ const confirm = await prompt(chalk.yellow(' Log out? ') + chalk.dim('[y/n] > '));
107
+ if (confirm !== 'y' && confirm !== 'yes') {
108
+ console.log(chalk.dim(' Cancelled.'));
109
+ console.log();
110
+ return;
111
+ }
112
+ // Revoke server-side BEFORE clearing the local file — the refresh token has to be
113
+ // read from it. Without this, "logging out" only deleted the local copy while the
114
+ // 60-day refresh token stayed valid and could keep minting access tokens for
115
+ // anyone who recovered it (a synced dotfile, a backup, a shared machine).
116
+ await revokeRefreshToken();
117
+ clearAuth();
118
+ console.log(chalk.green(' ✓ Logged out.'));
119
+ console.log();
120
+ }
121
+ function startSpinner(text) {
122
+ const frames = ['◐', '◓', '◑', '◒'];
123
+ let i = 0;
124
+ process.stdout.write(' ');
125
+ const interval = setInterval(() => {
126
+ process.stdout.write(`\r ${chalk.cyan(frames[i % frames.length])} ${chalk.dim(text)}`);
127
+ i++;
128
+ }, 120);
129
+ return { interval };
130
+ }
131
+ function stopSpinner(handle) {
132
+ clearInterval(handle.interval);
133
+ process.stdout.write('\r\x1b[K'); // Clear the spinner line
134
+ }