@aragon-agent/cli 0.4.3 → 0.5.1
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/CHANGELOG.md +131 -0
- package/README.md +227 -3
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +183 -20
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +45 -1
- package/dist/config/load.d.ts +8 -0
- package/dist/config/load.js +59 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +61 -0
- package/dist/config/schema.js +83 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +4 -1
- package/dist/ui/App.js +182 -11
- package/dist/ui/Composer.d.ts +4 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +8 -1
- package/dist/ui/PromptInput.js +12 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +8 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +13 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/use-startup-notices.d.ts +15 -0
- package/dist/ui/use-startup-notices.js +38 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assembly: bootstrap level resolution, replay of what happened before the
|
|
3
|
+
* logger existed, ownership of the four process hooks, and the agent-event
|
|
4
|
+
* mapping.
|
|
5
|
+
*
|
|
6
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
* SINGLE-OWNER MODEL — the part that is easiest to get wrong and hardest to
|
|
8
|
+
* notice, because getting it wrong still compiles and still passes a smoke test.
|
|
9
|
+
*
|
|
10
|
+
* The logger owns FLUSHING and never owns EXITING.
|
|
11
|
+
* The screen owns RESTORING and never owns FLUSHING.
|
|
12
|
+
*
|
|
13
|
+
* All four hooks below change Node's default behaviour merely by existing:
|
|
14
|
+
*
|
|
15
|
+
* `uncaughtException` — registering a listener REPLACES Node's "print the
|
|
16
|
+
* stack and exit". `main().catch()` in `cli.tsx` is not that hook; it is a
|
|
17
|
+
* promise chain's catch and only ever sees rejections of that one chain.
|
|
18
|
+
* So there is no prior behaviour to inherit: without an explicit
|
|
19
|
+
* `process.exit(1)` here, an exception thrown inside an Ink render
|
|
20
|
+
* callback or a timer stops terminating the process. In full-screen that
|
|
21
|
+
* means the alternate screen is never restored and Ink keeps drawing onto
|
|
22
|
+
* a broken controller — the user's only way out is `reset`.
|
|
23
|
+
*
|
|
24
|
+
* `unhandledRejection` — Node 18 defaults to `--unhandled-rejections=throw`.
|
|
25
|
+
* Adding a listener suppresses it, turning a crash into a silent hang.
|
|
26
|
+
*
|
|
27
|
+
* `SIGINT` / `SIGTERM` / `SIGHUP` — the handlers in `runInteractive()` exist
|
|
28
|
+
* ONLY in its full-screen branch, so inline mode, `-p`, and every
|
|
29
|
+
* subcommand had no handler at all and took Node's default termination,
|
|
30
|
+
* which does not run `'exit'` listeners: the whole queue was lost. But a
|
|
31
|
+
* signal listener registered here runs BEFORE the one `runInteractive`
|
|
32
|
+
* would add (EventEmitter order), and merely having one stops Node from
|
|
33
|
+
* terminating — so it must exit itself, and would then beat the screen
|
|
34
|
+
* restore. Hence: this module flushes and then calls a REPLACEABLE
|
|
35
|
+
* terminator. The default is a bare exit, which covers headless and every
|
|
36
|
+
* subcommand; the full-screen branch swaps in one that restores first.
|
|
37
|
+
* ══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
*/
|
|
39
|
+
import process from 'node:process';
|
|
40
|
+
import { getHomeResolutionWarning } from '../config/app-paths.js';
|
|
41
|
+
import { readConfigFile } from '../config/store.js';
|
|
42
|
+
import { clampLogConfig } from '../config/schema.js';
|
|
43
|
+
import { clampLogLevel } from './levels.js';
|
|
44
|
+
import { Logger, setActiveLogger } from './logger.js';
|
|
45
|
+
import { registerSecretsFrom } from './secret-registry.js';
|
|
46
|
+
/** POSIX signal numbers for the `128 + signo` exit-code convention. */
|
|
47
|
+
const SIGNAL_NUMBERS = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 };
|
|
48
|
+
let screenRestore = () => { };
|
|
49
|
+
/**
|
|
50
|
+
* `null` means "use the default", which is a bare exit through the same process
|
|
51
|
+
* port the hooks were installed on. Holding the default here as a closure over
|
|
52
|
+
* the global `process` instead would make it a second exit channel — invisible
|
|
53
|
+
* to anything that injected a port, which is exactly the sort of divergence
|
|
54
|
+
* these hooks must not have.
|
|
55
|
+
*/
|
|
56
|
+
let signalTerminator = null;
|
|
57
|
+
/**
|
|
58
|
+
* Hand the screen-restore duty to whoever actually owns the screen.
|
|
59
|
+
* Called by `runInteractive()`'s full-screen branch and by nobody else.
|
|
60
|
+
*/
|
|
61
|
+
export function setScreenRestore(fn) {
|
|
62
|
+
screenRestore = fn;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Replace what a signal does after the flush. The default (a bare
|
|
66
|
+
* `exit(128 + signo)`) is correct for headless, inline and every subcommand;
|
|
67
|
+
* full-screen substitutes one that restores the terminal first.
|
|
68
|
+
*/
|
|
69
|
+
export function setSignalTerminator(fn) {
|
|
70
|
+
signalTerminator = fn;
|
|
71
|
+
}
|
|
72
|
+
/** Tests only: undo the hook swaps so cases cannot leak into each other. */
|
|
73
|
+
export function resetProcessHooksForTest() {
|
|
74
|
+
screenRestore = () => { };
|
|
75
|
+
signalTerminator = null;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve the BOOTSTRAP level.
|
|
79
|
+
*
|
|
80
|
+
* Inputs are only `process.env`, the config file, and a deliberately tiny argv
|
|
81
|
+
* scan. Not commander (it has not parsed yet) and not `.env` (`loadDotenv()`
|
|
82
|
+
* runs inside `loadConfig()`, much later). This level covers exactly the window
|
|
83
|
+
* before `loadConfig()` — migrations, path warnings, an early crash — after
|
|
84
|
+
* which `Logger.reconfigure()` installs the authoritative one.
|
|
85
|
+
*
|
|
86
|
+
* The duplication with `loadConfig()` is intentional and cheap. The alternative
|
|
87
|
+
* is to install the logger after config resolution, which would leave precisely
|
|
88
|
+
* the events that are hardest to debug with nowhere to be recorded.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveBootstrapLogConfig(argv = process.argv) {
|
|
91
|
+
const file = readConfigFile().config ?? {};
|
|
92
|
+
const base = clampLogConfig({
|
|
93
|
+
...(file.log ?? {}),
|
|
94
|
+
...(process.env.ARAGON_LOG_LEVEL ? { level: process.env.ARAGON_LOG_LEVEL } : {}),
|
|
95
|
+
...(process.env.ARAGON_LOG_DIR ? { dir: process.env.ARAGON_LOG_DIR } : {}),
|
|
96
|
+
});
|
|
97
|
+
const scanned = scanLoggingArgv(argv);
|
|
98
|
+
const toFile = scanned.toFile ?? readEnvToFile() ?? (file.log?.toFile === false ? false : true);
|
|
99
|
+
return clampLogConfig({
|
|
100
|
+
...base,
|
|
101
|
+
...(scanned.level ? { level: scanned.level } : {}),
|
|
102
|
+
...(scanned.dir ? { dir: scanned.dir } : {}),
|
|
103
|
+
toFile,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
function readEnvToFile() {
|
|
107
|
+
const raw = process.env.ARAGON_LOG_FILE?.trim().toLowerCase();
|
|
108
|
+
if (raw === undefined || raw.length === 0)
|
|
109
|
+
return undefined;
|
|
110
|
+
return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no');
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The minimal hand scan of argv.
|
|
114
|
+
*
|
|
115
|
+
* `-vv` is NOT accepted, and must not be added: `-v` is already `--version`
|
|
116
|
+
* (`cli.tsx`), so commander would reject `-vv` as an unknown option before this
|
|
117
|
+
* level was ever used. A flag that internal docs promise and the parser refuses
|
|
118
|
+
* is worse than no flag.
|
|
119
|
+
*/
|
|
120
|
+
function scanLoggingArgv(argv) {
|
|
121
|
+
const out = {};
|
|
122
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
123
|
+
const arg = argv[i];
|
|
124
|
+
if (arg === '--verbose')
|
|
125
|
+
out.level = 'debug';
|
|
126
|
+
else if (arg === '--no-log-file')
|
|
127
|
+
out.toFile = false;
|
|
128
|
+
else if (arg === '--log-file')
|
|
129
|
+
out.toFile = true;
|
|
130
|
+
else if (arg === '--log-level')
|
|
131
|
+
out.level = clampLogLevel(argv[i + 1], 'info');
|
|
132
|
+
else if (arg.startsWith('--log-level='))
|
|
133
|
+
out.level = clampLogLevel(arg.slice(12), 'info');
|
|
134
|
+
else if (arg === '--log-dir')
|
|
135
|
+
out.dir = argv[i + 1];
|
|
136
|
+
else if (arg.startsWith('--log-dir='))
|
|
137
|
+
out.dir = arg.slice(10);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Build the logger, replay what came before it, and take ownership of the four
|
|
143
|
+
* process hooks. Returns the logger, which is also installed as the singleton.
|
|
144
|
+
*/
|
|
145
|
+
export function installLogging(opts = {}) {
|
|
146
|
+
const port = opts.processPort ?? process;
|
|
147
|
+
const logger = new Logger(resolveBootstrapLogConfig(opts.argv));
|
|
148
|
+
setActiveLogger(logger);
|
|
149
|
+
// Registration site 1 (§4.4.3): whatever the config file already holds. Sites
|
|
150
|
+
// 2, 3, 4 and 5 add the env, the merged view plus `--api-key`, the settings
|
|
151
|
+
// screen, and every persisted write respectively.
|
|
152
|
+
registerSecretsFrom(readConfigFile().config?.apiKeys ?? undefined);
|
|
153
|
+
const warning = getHomeResolutionWarning();
|
|
154
|
+
if (warning)
|
|
155
|
+
logger.warn('cli', 'home_resolution_fallback', { reason: warning });
|
|
156
|
+
for (const note of opts.pending ?? []) {
|
|
157
|
+
logger[note.level](note.scope, note.msg, note.data);
|
|
158
|
+
}
|
|
159
|
+
installProcessHooks(logger, port);
|
|
160
|
+
return logger;
|
|
161
|
+
}
|
|
162
|
+
function installProcessHooks(logger, port) {
|
|
163
|
+
port.on('uncaughtException', (err) => {
|
|
164
|
+
handleFatal(logger, port, 'uncaught_exception', err);
|
|
165
|
+
});
|
|
166
|
+
port.on('unhandledRejection', (reason) => {
|
|
167
|
+
handleFatal(logger, port, 'unhandled_rejection', reason);
|
|
168
|
+
});
|
|
169
|
+
// Normal exit. Only synchronous work is possible here, and the whole write
|
|
170
|
+
// path is synchronous precisely so this can be one line.
|
|
171
|
+
port.on('exit', () => {
|
|
172
|
+
logger.flushSync();
|
|
173
|
+
});
|
|
174
|
+
for (const [name, signo] of Object.entries(SIGNAL_NUMBERS)) {
|
|
175
|
+
port.on(name, () => {
|
|
176
|
+
logger.flushSync();
|
|
177
|
+
// Exiting is NOT optional here: merely having a signal listener stops Node
|
|
178
|
+
// from terminating by default, so skipping this would turn Ctrl+C into
|
|
179
|
+
// "nothing happens" for every path that has not replaced the terminator.
|
|
180
|
+
if (signalTerminator) {
|
|
181
|
+
signalTerminator(signo);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
port.exit(128 + signo);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* The crash path, in an order that is not negotiable:
|
|
190
|
+
*
|
|
191
|
+
* record -> flush -> restore the screen -> write the stack -> EXIT
|
|
192
|
+
*
|
|
193
|
+
* Restoring before writing matters: a stack printed into the alternate screen
|
|
194
|
+
* disappears together with it. Exiting at all matters more — see the module
|
|
195
|
+
* header. A failure inside logging must not stop the last two steps, so the
|
|
196
|
+
* first two are wrapped.
|
|
197
|
+
*/
|
|
198
|
+
function handleFatal(logger, port, msg, err) {
|
|
199
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
200
|
+
try {
|
|
201
|
+
logger.error('cli', msg, { message: error.message, stack: error.stack });
|
|
202
|
+
logger.flushSync();
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Logging must never be the reason a crash fails to surface.
|
|
206
|
+
}
|
|
207
|
+
screenRestore();
|
|
208
|
+
port.stderr.write(`${error.stack ?? error.message}\n`);
|
|
209
|
+
return port.exit(1);
|
|
210
|
+
}
|
|
211
|
+
/** Trim user content to the configured preview budget (debug only). */
|
|
212
|
+
function preview(value, limit) {
|
|
213
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value) ?? '';
|
|
214
|
+
return text.length > limit ? `${text.slice(0, limit)}…` : text;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Subscribe to the agent's events and record them.
|
|
218
|
+
*
|
|
219
|
+
* Returns the unsubscribe function `controller.subscribe()` gives back.
|
|
220
|
+
*
|
|
221
|
+
* `message_update` fires thousands of times per answer, so it is `trace`-only
|
|
222
|
+
* AND records no content — a `debug` run would otherwise write a log larger
|
|
223
|
+
* than the conversation. The turn timer is a closure variable rather than
|
|
224
|
+
* module state because `turn_end` carries `{message, usage}` and NO duration,
|
|
225
|
+
* so the elapsed time has to be measured here; module state would let one test
|
|
226
|
+
* (or a future second controller) read another's clock.
|
|
227
|
+
*/
|
|
228
|
+
export function attachAgentEvents(logger, controller) {
|
|
229
|
+
let turnStartedAt = 0;
|
|
230
|
+
return controller.subscribe((event) => {
|
|
231
|
+
switch (event.type) {
|
|
232
|
+
case 'agent_start': {
|
|
233
|
+
// The event itself carries no fields; the three the reader wants are
|
|
234
|
+
// the controller's current configuration.
|
|
235
|
+
const cfg = controller.getConfig();
|
|
236
|
+
logger.info('agent', 'run_start', {
|
|
237
|
+
provider: cfg.provider,
|
|
238
|
+
model: cfg.model,
|
|
239
|
+
thinking: cfg.thinkingLevel,
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'turn_start':
|
|
244
|
+
turnStartedAt = Date.now();
|
|
245
|
+
logger.debug('agent', 'turn_start');
|
|
246
|
+
break;
|
|
247
|
+
case 'turn_end':
|
|
248
|
+
logger.info('agent', 'turn_end', {
|
|
249
|
+
in: event.usage?.inputTokens ?? 0,
|
|
250
|
+
out: event.usage?.outputTokens ?? 0,
|
|
251
|
+
ms: turnStartedAt > 0 ? Date.now() - turnStartedAt : 0,
|
|
252
|
+
});
|
|
253
|
+
break;
|
|
254
|
+
case 'message_update':
|
|
255
|
+
logger.trace('agent', 'stream', {
|
|
256
|
+
type: event.streamEvent?.type ?? 'unknown',
|
|
257
|
+
// The SIZE of the increment, never the increment. It is what makes a
|
|
258
|
+
// trace log usable for "where did the time go" without turning it
|
|
259
|
+
// into a second copy of the conversation.
|
|
260
|
+
delta: typeof event.streamEvent?.delta === 'string'
|
|
261
|
+
? event.streamEvent.delta.length
|
|
262
|
+
: undefined,
|
|
263
|
+
});
|
|
264
|
+
break;
|
|
265
|
+
case 'tool_execution_start':
|
|
266
|
+
logger.debug('tool', 'tool_start', {
|
|
267
|
+
toolCallId: event.toolCallId,
|
|
268
|
+
toolName: event.toolName,
|
|
269
|
+
...(logger.isEnabled('debug') ? { args: preview(event.args, logger.previewChars) } : {}),
|
|
270
|
+
});
|
|
271
|
+
break;
|
|
272
|
+
case 'tool_execution_end':
|
|
273
|
+
logger.info('tool', 'tool_end', {
|
|
274
|
+
toolName: event.toolName,
|
|
275
|
+
ms: event.duration ?? 0,
|
|
276
|
+
isError: event.isError === true,
|
|
277
|
+
...(logger.isEnabled('debug')
|
|
278
|
+
? { result: preview(event.result, logger.previewChars) }
|
|
279
|
+
: {}),
|
|
280
|
+
});
|
|
281
|
+
break;
|
|
282
|
+
case 'code_execution_start':
|
|
283
|
+
logger.debug('tool', 'code_start', {
|
|
284
|
+
language: event.language,
|
|
285
|
+
...(logger.isEnabled('debug') ? { code: preview(event.code, logger.previewChars) } : {}),
|
|
286
|
+
});
|
|
287
|
+
break;
|
|
288
|
+
case 'code_execution_end':
|
|
289
|
+
logger.info('tool', 'code_end', {
|
|
290
|
+
ms: event.duration ?? 0,
|
|
291
|
+
isError: Boolean(event.error),
|
|
292
|
+
...(logger.isEnabled('debug')
|
|
293
|
+
? { output: preview(event.output, logger.previewChars) }
|
|
294
|
+
: {}),
|
|
295
|
+
});
|
|
296
|
+
break;
|
|
297
|
+
case 'agent_end':
|
|
298
|
+
logger.info('agent', 'run_end', { messages: event.messages?.length ?? 0 });
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log levels — the ordering, the names, and the two coercion helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module because it is the one piece of the logging subsystem
|
|
5
|
+
* the config layer (`schema.ts`) and the settings screen need, and neither
|
|
6
|
+
* should have to pull in a file sink to learn what "debug" is called.
|
|
7
|
+
*
|
|
8
|
+
* PRIVACY IS PART OF THE LADDER, not a side effect of it:
|
|
9
|
+
*
|
|
10
|
+
* error crashes, failed requests, config write/parse failures
|
|
11
|
+
* warn degraded migrations, clamped values, dropped records
|
|
12
|
+
* info lifecycle, config writes, turn boundaries + token counts, tool
|
|
13
|
+
* names and durations — NO conversation content at all
|
|
14
|
+
* debug the above plus prompts / replies / tool args truncated to
|
|
15
|
+
* `previewChars`
|
|
16
|
+
* trace full message bodies, full tool arguments and output
|
|
17
|
+
*
|
|
18
|
+
* `info` is the default precisely so that "send me your log" is a safe thing to
|
|
19
|
+
* ask a user. `debug` and above are not, and the README says so.
|
|
20
|
+
*/
|
|
21
|
+
export declare const LOG_LEVELS: {
|
|
22
|
+
readonly silent: 0;
|
|
23
|
+
readonly error: 10;
|
|
24
|
+
readonly warn: 20;
|
|
25
|
+
readonly info: 30;
|
|
26
|
+
readonly debug: 40;
|
|
27
|
+
readonly trace: 50;
|
|
28
|
+
};
|
|
29
|
+
export type LogLevelName = keyof typeof LOG_LEVELS;
|
|
30
|
+
export declare const LOG_LEVEL_NAMES: readonly LogLevelName[];
|
|
31
|
+
export declare function isLogLevel(v: unknown): v is LogLevelName;
|
|
32
|
+
/**
|
|
33
|
+
* Coerce anything into a level, falling back rather than throwing.
|
|
34
|
+
*
|
|
35
|
+
* Same discipline as `clampTheme` / `clampThinkingLevel`: a nonsense value in
|
|
36
|
+
* the config file must not stop the CLI from starting.
|
|
37
|
+
*/
|
|
38
|
+
export declare function clampLogLevel(v: unknown, fallback: LogLevelName): LogLevelName;
|
|
39
|
+
/** True when a record at `level` should be emitted under `threshold`. */
|
|
40
|
+
export declare function isLevelEnabled(level: LogLevelName, threshold: LogLevelName): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log levels — the ordering, the names, and the two coercion helpers.
|
|
3
|
+
*
|
|
4
|
+
* Kept in its own module because it is the one piece of the logging subsystem
|
|
5
|
+
* the config layer (`schema.ts`) and the settings screen need, and neither
|
|
6
|
+
* should have to pull in a file sink to learn what "debug" is called.
|
|
7
|
+
*
|
|
8
|
+
* PRIVACY IS PART OF THE LADDER, not a side effect of it:
|
|
9
|
+
*
|
|
10
|
+
* error crashes, failed requests, config write/parse failures
|
|
11
|
+
* warn degraded migrations, clamped values, dropped records
|
|
12
|
+
* info lifecycle, config writes, turn boundaries + token counts, tool
|
|
13
|
+
* names and durations — NO conversation content at all
|
|
14
|
+
* debug the above plus prompts / replies / tool args truncated to
|
|
15
|
+
* `previewChars`
|
|
16
|
+
* trace full message bodies, full tool arguments and output
|
|
17
|
+
*
|
|
18
|
+
* `info` is the default precisely so that "send me your log" is a safe thing to
|
|
19
|
+
* ask a user. `debug` and above are not, and the README says so.
|
|
20
|
+
*/
|
|
21
|
+
export const LOG_LEVELS = {
|
|
22
|
+
silent: 0,
|
|
23
|
+
error: 10,
|
|
24
|
+
warn: 20,
|
|
25
|
+
info: 30,
|
|
26
|
+
debug: 40,
|
|
27
|
+
trace: 50,
|
|
28
|
+
};
|
|
29
|
+
export const LOG_LEVEL_NAMES = [
|
|
30
|
+
'silent',
|
|
31
|
+
'error',
|
|
32
|
+
'warn',
|
|
33
|
+
'info',
|
|
34
|
+
'debug',
|
|
35
|
+
'trace',
|
|
36
|
+
];
|
|
37
|
+
export function isLogLevel(v) {
|
|
38
|
+
return typeof v === 'string' && Object.prototype.hasOwnProperty.call(LOG_LEVELS, v);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Coerce anything into a level, falling back rather than throwing.
|
|
42
|
+
*
|
|
43
|
+
* Same discipline as `clampTheme` / `clampThinkingLevel`: a nonsense value in
|
|
44
|
+
* the config file must not stop the CLI from starting.
|
|
45
|
+
*/
|
|
46
|
+
export function clampLogLevel(v, fallback) {
|
|
47
|
+
if (isLogLevel(v))
|
|
48
|
+
return v;
|
|
49
|
+
if (typeof v === 'string' && isLogLevel(v.trim().toLowerCase())) {
|
|
50
|
+
return v.trim().toLowerCase();
|
|
51
|
+
}
|
|
52
|
+
return fallback;
|
|
53
|
+
}
|
|
54
|
+
/** True when a record at `level` should be emitted under `threshold`. */
|
|
55
|
+
export function isLevelEnabled(level, threshold) {
|
|
56
|
+
if (threshold === 'silent')
|
|
57
|
+
return false;
|
|
58
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[threshold];
|
|
59
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The logger: level filtering, record shape, redaction, and the module-level
|
|
3
|
+
* singleton every other file reaches through `getLogger()`.
|
|
4
|
+
*
|
|
5
|
+
* WIRE FORMAT — one `JSON.stringify(record)` per line, UTF-8, no BOM:
|
|
6
|
+
*
|
|
7
|
+
* {"ts":"2026-07-27T12:11:02.913+08:00","lv":"info","sid":"7f3a91c2",
|
|
8
|
+
* "pid":24188,"scope":"agent","msg":"turn_end",
|
|
9
|
+
* "data":{"provider":"anthropic","in":1284,"out":377,"ms":4210}}
|
|
10
|
+
*
|
|
11
|
+
* THE SHORT KEYS ARE DELIBERATE AND MUST NOT BE "FIXED". The package's clean-code
|
|
12
|
+
* guide forbids names like `data`, and rightly so for TypeScript identifiers.
|
|
13
|
+
* These are not identifiers: they are the on-disk contract, repeated on every
|
|
14
|
+
* line of a file that reaches megabytes, and already parsed by whatever `jq`
|
|
15
|
+
* one-liner a user has written. Renaming them breaks every existing log and
|
|
16
|
+
* every existing script for a readability gain nobody experiences. Variable and
|
|
17
|
+
* function names in this file get no such exemption.
|
|
18
|
+
*
|
|
19
|
+
* `sid` and `pid` both exist because several `aragon` processes append to the
|
|
20
|
+
* same day's file: `sid` separates their interleaved lines, and `pid` is what
|
|
21
|
+
* still matches up with what the OS recorded after a crash.
|
|
22
|
+
*/
|
|
23
|
+
import type { LogConfig } from '../config/schema.js';
|
|
24
|
+
import { type LogLevelName } from './levels.js';
|
|
25
|
+
/** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
|
|
26
|
+
export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log';
|
|
27
|
+
export interface LogRecord {
|
|
28
|
+
ts: string;
|
|
29
|
+
lv: LogLevelName;
|
|
30
|
+
sid: string;
|
|
31
|
+
pid: number;
|
|
32
|
+
scope: LogScope;
|
|
33
|
+
msg: string;
|
|
34
|
+
data?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
export interface ScopedLogger {
|
|
37
|
+
error(msg: string, data?: Record<string, unknown>): void;
|
|
38
|
+
warn(msg: string, data?: Record<string, unknown>): void;
|
|
39
|
+
info(msg: string, data?: Record<string, unknown>): void;
|
|
40
|
+
debug(msg: string, data?: Record<string, unknown>): void;
|
|
41
|
+
trace(msg: string, data?: Record<string, unknown>): void;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* ISO 8601 with the LOCAL offset, matching the local date in the file name.
|
|
45
|
+
* `toISOString()` would put an evening record west of UTC into the next day
|
|
46
|
+
* while the file it lands in is named for the previous one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function localIsoTimestamp(date?: Date): string;
|
|
49
|
+
export declare class Logger {
|
|
50
|
+
private config;
|
|
51
|
+
private sink;
|
|
52
|
+
private readonly sid;
|
|
53
|
+
private failureHandler;
|
|
54
|
+
constructor(config?: LogConfig);
|
|
55
|
+
error(scope: LogScope, msg: string, data?: Record<string, unknown>): void;
|
|
56
|
+
warn(scope: LogScope, msg: string, data?: Record<string, unknown>): void;
|
|
57
|
+
info(scope: LogScope, msg: string, data?: Record<string, unknown>): void;
|
|
58
|
+
debug(scope: LogScope, msg: string, data?: Record<string, unknown>): void;
|
|
59
|
+
trace(scope: LogScope, msg: string, data?: Record<string, unknown>): void;
|
|
60
|
+
child(scope: LogScope): ScopedLogger;
|
|
61
|
+
/** Whether a record at `level` would be written — lets callers skip the work. */
|
|
62
|
+
isEnabled(level: LogLevelName): boolean;
|
|
63
|
+
/** Truncation budget for user content at `debug`. `trace` ignores it. */
|
|
64
|
+
get previewChars(): number;
|
|
65
|
+
get level(): LogLevelName;
|
|
66
|
+
setLevel(level: LogLevelName): void;
|
|
67
|
+
/**
|
|
68
|
+
* THE authoritative-level hand-off (§4.4.5).
|
|
69
|
+
*
|
|
70
|
+
* `installLogging()` resolves a bootstrap level from `process.env`, the config
|
|
71
|
+
* file and a tiny argv scan, because it has to run before commander exists and
|
|
72
|
+
* before `loadDotenv()`. That bootstrap level covers migrations and early
|
|
73
|
+
* crashes and nothing else. Once `loadConfig()` has produced the real answer,
|
|
74
|
+
* every path calls this exactly once. Without it the fully-resolved level —
|
|
75
|
+
* including anything from `.env` — would simply never take effect.
|
|
76
|
+
*
|
|
77
|
+
* Idempotent, and safe after writing has already begun.
|
|
78
|
+
*/
|
|
79
|
+
reconfigure(config: LogConfig): void;
|
|
80
|
+
/** Registered by whoever owns a user-visible channel (toast, or stderr). */
|
|
81
|
+
onFailure(cb: (reason: string) => void): void;
|
|
82
|
+
/** Idempotent; `exit`, the signal hook and the crash hooks all call it. */
|
|
83
|
+
flushSync(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Where records are going, for `aragon logs path`.
|
|
86
|
+
*
|
|
87
|
+
* Answers with a FILE path even before the sink has been created — the
|
|
88
|
+
* question is "where would I look?", and a bare directory is not that answer.
|
|
89
|
+
*/
|
|
90
|
+
currentLogPath(): string;
|
|
91
|
+
get sinkError(): string | undefined;
|
|
92
|
+
get sinkDisabled(): boolean;
|
|
93
|
+
/** Closes the fd so a caller may delete the files (`aragon logs clear`). */
|
|
94
|
+
closeSink(): void;
|
|
95
|
+
private emit;
|
|
96
|
+
private resolveDir;
|
|
97
|
+
private sinkOptions;
|
|
98
|
+
/**
|
|
99
|
+
* Created on the FIRST record that actually passes the filters, never in
|
|
100
|
+
* `installLogging()`. `aragon --version`, `aragon config path` and anyone
|
|
101
|
+
* running with `--no-log-file` must not leave an empty directory behind.
|
|
102
|
+
*/
|
|
103
|
+
private ensureSink;
|
|
104
|
+
private renderDropNotice;
|
|
105
|
+
}
|
|
106
|
+
export declare function getLogger(): Logger;
|
|
107
|
+
/** Called by `installLogging()`; not part of the general API. */
|
|
108
|
+
export declare function setActiveLogger(logger: Logger): void;
|
|
109
|
+
/** Tests only — returns the singleton to its pre-install state. */
|
|
110
|
+
export declare function resetLoggerForTest(): void;
|