@link-assistant/agent 0.8.7 → 0.8.10
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/package.json +1 -1
- package/src/cli/cmd/export.ts +1 -1
- package/src/cli/continuous-mode.js +35 -21
- package/src/cli/event-handler.js +11 -1
- package/src/cli/output.ts +203 -0
- package/src/cli/ui.ts +5 -5
- package/src/flag/flag.ts +18 -0
- package/src/index.js +75 -71
- package/src/json-standard/index.ts +15 -5
- package/src/session/agent.js +14 -18
- package/src/session/index.ts +12 -0
- package/src/session/processor.ts +3 -3
- package/src/tool/read.ts +4 -5
- package/src/util/log-lazy.ts +29 -9
- package/src/util/log.ts +67 -29
package/package.json
CHANGED
package/src/cli/cmd/export.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const ExportCommand = cmd({
|
|
|
18
18
|
handler: async (args) => {
|
|
19
19
|
await bootstrap(process.cwd(), async () => {
|
|
20
20
|
let sessionID = args.sessionID;
|
|
21
|
-
process.
|
|
21
|
+
process.stdout.write(`Exporting session: ${sessionID ?? 'latest'}`);
|
|
22
22
|
|
|
23
23
|
if (!sessionID) {
|
|
24
24
|
UI.empty();
|
|
@@ -11,6 +11,8 @@ import { SessionPrompt } from '../session/prompt.ts';
|
|
|
11
11
|
import { createEventHandler } from '../json-standard/index.ts';
|
|
12
12
|
import { createContinuousStdinReader } from './input-queue.js';
|
|
13
13
|
import { Log } from '../util/log.ts';
|
|
14
|
+
import { Flag } from '../flag/flag.ts';
|
|
15
|
+
import { outputStatus, outputError, outputInput } from './output.ts';
|
|
14
16
|
|
|
15
17
|
// Shared error tracking
|
|
16
18
|
let hasError = false;
|
|
@@ -31,17 +33,8 @@ export function getHasError() {
|
|
|
31
33
|
return hasError;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* @param {object} status - Status object to output
|
|
37
|
-
* @param {boolean} compact - If true, output compact JSON (single line)
|
|
38
|
-
*/
|
|
39
|
-
function outputStatus(status, compact = false) {
|
|
40
|
-
const json = compact
|
|
41
|
-
? JSON.stringify(status)
|
|
42
|
-
: JSON.stringify(status, null, 2);
|
|
43
|
-
console.error(json);
|
|
44
|
-
}
|
|
36
|
+
// outputStatus is now imported from './output.ts'
|
|
37
|
+
// It outputs to stdout for non-error messages, stderr for errors
|
|
45
38
|
|
|
46
39
|
// Logger for resume operations
|
|
47
40
|
const log = Log.create({ service: 'resume' });
|
|
@@ -84,9 +77,8 @@ export async function resolveResumeSession(argv, compactJson) {
|
|
|
84
77
|
}
|
|
85
78
|
|
|
86
79
|
if (!mostRecentSession) {
|
|
87
|
-
|
|
80
|
+
outputError(
|
|
88
81
|
{
|
|
89
|
-
type: 'error',
|
|
90
82
|
errorType: 'SessionNotFound',
|
|
91
83
|
message:
|
|
92
84
|
'No existing sessions found to continue. Start a new session first.',
|
|
@@ -110,9 +102,8 @@ export async function resolveResumeSession(argv, compactJson) {
|
|
|
110
102
|
existingSession = await Session.get(targetSessionID);
|
|
111
103
|
} catch (_error) {
|
|
112
104
|
// Session.get throws an error when the session doesn't exist
|
|
113
|
-
|
|
105
|
+
outputError(
|
|
114
106
|
{
|
|
115
|
-
type: 'error',
|
|
116
107
|
errorType: 'SessionNotFound',
|
|
117
108
|
message: `Session not found: ${targetSessionID}`,
|
|
118
109
|
},
|
|
@@ -122,9 +113,8 @@ export async function resolveResumeSession(argv, compactJson) {
|
|
|
122
113
|
}
|
|
123
114
|
|
|
124
115
|
if (!existingSession) {
|
|
125
|
-
|
|
116
|
+
outputError(
|
|
126
117
|
{
|
|
127
|
-
type: 'error',
|
|
128
118
|
errorType: 'SessionNotFound',
|
|
129
119
|
message: `Session not found: ${targetSessionID}`,
|
|
130
120
|
},
|
|
@@ -204,7 +194,8 @@ export async function runContinuousServerMode(
|
|
|
204
194
|
appendSystemMessage,
|
|
205
195
|
jsonStandard
|
|
206
196
|
) {
|
|
207
|
-
|
|
197
|
+
// Check both CLI flag and environment variable for compact JSON mode
|
|
198
|
+
const compactJson = argv['compact-json'] === true || Flag.COMPACT_JSON();
|
|
208
199
|
const isInteractive = argv.interactive !== false;
|
|
209
200
|
const autoMerge = argv['auto-merge-queued-messages'] !== false;
|
|
210
201
|
|
|
@@ -255,6 +246,17 @@ export async function runContinuousServerMode(
|
|
|
255
246
|
}
|
|
256
247
|
|
|
257
248
|
isProcessing = true;
|
|
249
|
+
|
|
250
|
+
// Output input confirmation in JSON format
|
|
251
|
+
outputInput(
|
|
252
|
+
{
|
|
253
|
+
raw: message.raw || message.message,
|
|
254
|
+
parsed: message,
|
|
255
|
+
format: message.format || 'text',
|
|
256
|
+
},
|
|
257
|
+
compactJson
|
|
258
|
+
);
|
|
259
|
+
|
|
258
260
|
const messageText = message.message || 'hi';
|
|
259
261
|
const parts = [{ type: 'text', text: messageText }];
|
|
260
262
|
|
|
@@ -339,7 +341,7 @@ export async function runContinuousServerMode(
|
|
|
339
341
|
});
|
|
340
342
|
}
|
|
341
343
|
|
|
342
|
-
if (part.type === 'tool'
|
|
344
|
+
if (part.type === 'tool') {
|
|
343
345
|
eventHandler.output({
|
|
344
346
|
type: 'tool_use',
|
|
345
347
|
timestamp: Date.now(),
|
|
@@ -427,7 +429,8 @@ export async function runContinuousDirectMode(
|
|
|
427
429
|
appendSystemMessage,
|
|
428
430
|
jsonStandard
|
|
429
431
|
) {
|
|
430
|
-
|
|
432
|
+
// Check both CLI flag and environment variable for compact JSON mode
|
|
433
|
+
const compactJson = argv['compact-json'] === true || Flag.COMPACT_JSON();
|
|
431
434
|
const isInteractive = argv.interactive !== false;
|
|
432
435
|
const autoMerge = argv['auto-merge-queued-messages'] !== false;
|
|
433
436
|
|
|
@@ -466,6 +469,17 @@ export async function runContinuousDirectMode(
|
|
|
466
469
|
}
|
|
467
470
|
|
|
468
471
|
isProcessing = true;
|
|
472
|
+
|
|
473
|
+
// Output input confirmation in JSON format
|
|
474
|
+
outputInput(
|
|
475
|
+
{
|
|
476
|
+
raw: message.raw || message.message,
|
|
477
|
+
parsed: message,
|
|
478
|
+
format: message.format || 'text',
|
|
479
|
+
},
|
|
480
|
+
compactJson
|
|
481
|
+
);
|
|
482
|
+
|
|
469
483
|
const messageText = message.message || 'hi';
|
|
470
484
|
const parts = [{ type: 'text', text: messageText }];
|
|
471
485
|
|
|
@@ -544,7 +558,7 @@ export async function runContinuousDirectMode(
|
|
|
544
558
|
});
|
|
545
559
|
}
|
|
546
560
|
|
|
547
|
-
if (part.type === 'tool'
|
|
561
|
+
if (part.type === 'tool') {
|
|
548
562
|
eventHandler.output({
|
|
549
563
|
type: 'tool_use',
|
|
550
564
|
timestamp: Date.now(),
|
package/src/cli/event-handler.js
CHANGED
|
@@ -61,13 +61,23 @@ export function createBusEventSubscription({
|
|
|
61
61
|
});
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
if (part.type === 'tool'
|
|
64
|
+
if (part.type === 'tool') {
|
|
65
65
|
eventHandler.output({
|
|
66
66
|
type: 'tool_use',
|
|
67
67
|
timestamp: Date.now(),
|
|
68
68
|
sessionID,
|
|
69
69
|
part,
|
|
70
70
|
});
|
|
71
|
+
|
|
72
|
+
// If tool failed, also output an error event
|
|
73
|
+
if (part.state?.status === 'failed') {
|
|
74
|
+
eventHandler.output({
|
|
75
|
+
type: 'error',
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
sessionID,
|
|
78
|
+
error: part.state.error || 'Tool execution failed',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Output Module
|
|
3
|
+
*
|
|
4
|
+
* Centralized output handling for the Agent CLI.
|
|
5
|
+
* Ensures consistent JSON formatting and proper stream routing:
|
|
6
|
+
* - stdout: Normal output (status, events, data, logs, warnings)
|
|
7
|
+
* - stderr: Errors only
|
|
8
|
+
*
|
|
9
|
+
* All output includes a `type` field for easy parsing.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { EOL } from 'os';
|
|
13
|
+
import { Flag } from '../flag/flag';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Output types for JSON messages
|
|
17
|
+
*/
|
|
18
|
+
export type OutputType =
|
|
19
|
+
| 'status'
|
|
20
|
+
| 'error'
|
|
21
|
+
| 'warning'
|
|
22
|
+
| 'log'
|
|
23
|
+
| 'step_start'
|
|
24
|
+
| 'step_finish'
|
|
25
|
+
| 'text'
|
|
26
|
+
| 'tool_use'
|
|
27
|
+
| 'result'
|
|
28
|
+
| 'init'
|
|
29
|
+
| 'message'
|
|
30
|
+
| 'tool_result'
|
|
31
|
+
| 'input';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Base interface for all output messages
|
|
35
|
+
*/
|
|
36
|
+
export interface OutputMessage {
|
|
37
|
+
type: OutputType;
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Global compact JSON setting (can be set once at startup)
|
|
43
|
+
* Initialized lazily from Flag.COMPACT_JSON() which checks AGENT_CLI_COMPACT env var
|
|
44
|
+
*/
|
|
45
|
+
let globalCompactJson: boolean | null = null;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Set the global compact JSON setting
|
|
49
|
+
*/
|
|
50
|
+
export function setCompactJson(compact: boolean): void {
|
|
51
|
+
globalCompactJson = compact;
|
|
52
|
+
// Also update the Flag so other modules stay in sync
|
|
53
|
+
Flag.setCompactJson(compact);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Get the current compact JSON setting
|
|
58
|
+
*/
|
|
59
|
+
export function isCompactJson(): boolean {
|
|
60
|
+
if (globalCompactJson !== null) return globalCompactJson;
|
|
61
|
+
return Flag.COMPACT_JSON();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Format a message as JSON string
|
|
66
|
+
* @param message - The message object to format
|
|
67
|
+
* @param compact - Override the global compact setting
|
|
68
|
+
*/
|
|
69
|
+
export function formatJson(message: OutputMessage, compact?: boolean): string {
|
|
70
|
+
// Check local, global, and Flag settings for compact mode
|
|
71
|
+
const useCompact = compact ?? isCompactJson();
|
|
72
|
+
return useCompact
|
|
73
|
+
? JSON.stringify(message)
|
|
74
|
+
: JSON.stringify(message, null, 2);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Write a message to stdout (for normal output)
|
|
79
|
+
* @param message - The message object to output
|
|
80
|
+
* @param compact - Override the global compact setting
|
|
81
|
+
*/
|
|
82
|
+
export function writeStdout(message: OutputMessage, compact?: boolean): void {
|
|
83
|
+
const json = formatJson(message, compact);
|
|
84
|
+
process.stdout.write(json + EOL);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Write a message to stderr (for errors only)
|
|
89
|
+
* @param message - The message object to output
|
|
90
|
+
* @param compact - Override the global compact setting
|
|
91
|
+
*/
|
|
92
|
+
export function writeStderr(message: OutputMessage, compact?: boolean): void {
|
|
93
|
+
const json = formatJson(message, compact);
|
|
94
|
+
process.stderr.write(json + EOL);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Output a message to stdout (for normal output)
|
|
99
|
+
* - stdout: All output except errors (status, events, data, logs, warnings)
|
|
100
|
+
*
|
|
101
|
+
* @param message - The message object to output
|
|
102
|
+
* @param compact - Override the global compact setting
|
|
103
|
+
*/
|
|
104
|
+
export function output(message: OutputMessage, compact?: boolean): void {
|
|
105
|
+
writeStdout(message, compact);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Output a status message to stdout
|
|
110
|
+
*/
|
|
111
|
+
export function outputStatus(
|
|
112
|
+
status: Omit<OutputMessage, 'type'> & {
|
|
113
|
+
type?: 'status' | 'error' | 'warning';
|
|
114
|
+
},
|
|
115
|
+
compact?: boolean
|
|
116
|
+
): void {
|
|
117
|
+
const message: OutputMessage = {
|
|
118
|
+
type: status.type || 'status',
|
|
119
|
+
...status,
|
|
120
|
+
};
|
|
121
|
+
output(message, compact);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Output an error message to stderr
|
|
126
|
+
*/
|
|
127
|
+
export function outputError(
|
|
128
|
+
error: {
|
|
129
|
+
errorType?: string;
|
|
130
|
+
message: string;
|
|
131
|
+
hint?: string;
|
|
132
|
+
stack?: string;
|
|
133
|
+
[key: string]: unknown;
|
|
134
|
+
},
|
|
135
|
+
compact?: boolean
|
|
136
|
+
): void {
|
|
137
|
+
const message: OutputMessage = {
|
|
138
|
+
type: 'error',
|
|
139
|
+
...error,
|
|
140
|
+
};
|
|
141
|
+
writeStderr(message, compact);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Output a warning message to stdout
|
|
146
|
+
*/
|
|
147
|
+
export function outputWarning(
|
|
148
|
+
warning: {
|
|
149
|
+
message: string;
|
|
150
|
+
hint?: string;
|
|
151
|
+
[key: string]: unknown;
|
|
152
|
+
},
|
|
153
|
+
compact?: boolean
|
|
154
|
+
): void {
|
|
155
|
+
const message: OutputMessage = {
|
|
156
|
+
type: 'warning',
|
|
157
|
+
...warning,
|
|
158
|
+
};
|
|
159
|
+
writeStdout(message, compact);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Output a log message to stdout
|
|
164
|
+
* This uses the flattened format: { "type": "log", "level": "...", ... }
|
|
165
|
+
*/
|
|
166
|
+
export function outputLog(
|
|
167
|
+
log: {
|
|
168
|
+
level: 'debug' | 'info' | 'warn' | 'error';
|
|
169
|
+
message?: string;
|
|
170
|
+
timestamp?: string;
|
|
171
|
+
[key: string]: unknown;
|
|
172
|
+
},
|
|
173
|
+
compact?: boolean
|
|
174
|
+
): void {
|
|
175
|
+
const message: OutputMessage = {
|
|
176
|
+
type: 'log',
|
|
177
|
+
...log,
|
|
178
|
+
};
|
|
179
|
+
writeStdout(message, compact);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Output user input confirmation to stdout
|
|
184
|
+
*/
|
|
185
|
+
export function outputInput(
|
|
186
|
+
input: {
|
|
187
|
+
raw: string;
|
|
188
|
+
parsed?: unknown;
|
|
189
|
+
format?: 'json' | 'text';
|
|
190
|
+
[key: string]: unknown;
|
|
191
|
+
},
|
|
192
|
+
compact?: boolean
|
|
193
|
+
): void {
|
|
194
|
+
const message: OutputMessage = {
|
|
195
|
+
type: 'input',
|
|
196
|
+
timestamp: new Date().toISOString(),
|
|
197
|
+
...input,
|
|
198
|
+
};
|
|
199
|
+
writeStdout(message, compact);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Re-export for backward compatibility
|
|
203
|
+
export { output as write };
|
package/src/cli/ui.ts
CHANGED
|
@@ -23,31 +23,31 @@ export namespace UI {
|
|
|
23
23
|
|
|
24
24
|
// Print an empty line
|
|
25
25
|
export function empty() {
|
|
26
|
-
process.
|
|
26
|
+
process.stdout.write('\n');
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
// Print a line with optional formatting
|
|
30
30
|
export function println(...args: string[]) {
|
|
31
|
-
process.
|
|
31
|
+
process.stdout.write(args.join('') + Style.TEXT_NORMAL + '\n');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Print an error message
|
|
35
35
|
export function error(message: string) {
|
|
36
|
-
process.
|
|
36
|
+
process.stdout.write(
|
|
37
37
|
Style.TEXT_DANGER_BOLD + 'Error: ' + Style.TEXT_NORMAL + message + '\n'
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Print a success message
|
|
42
42
|
export function success(message: string) {
|
|
43
|
-
process.
|
|
43
|
+
process.stdout.write(
|
|
44
44
|
Style.TEXT_SUCCESS_BOLD + 'Success: ' + Style.TEXT_NORMAL + message + '\n'
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Print an info message
|
|
49
49
|
export function info(message: string) {
|
|
50
|
-
process.
|
|
50
|
+
process.stdout.write(
|
|
51
51
|
Style.TEXT_INFO_BOLD + 'Info: ' + Style.TEXT_NORMAL + message + '\n'
|
|
52
52
|
);
|
|
53
53
|
}
|
package/src/flag/flag.ts
CHANGED
|
@@ -63,6 +63,19 @@ export namespace Flag {
|
|
|
63
63
|
'OPENCODE_DRY_RUN'
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
+
// Compact JSON mode - output JSON on single lines (NDJSON format)
|
|
67
|
+
// Enabled by AGENT_CLI_COMPACT env var or --compact-json flag
|
|
68
|
+
// Uses getter to check env var at runtime for tests
|
|
69
|
+
let _compactJson: boolean | null = null;
|
|
70
|
+
|
|
71
|
+
export function COMPACT_JSON(): boolean {
|
|
72
|
+
if (_compactJson !== null) return _compactJson;
|
|
73
|
+
return (
|
|
74
|
+
truthy('AGENT_CLI_COMPACT') ||
|
|
75
|
+
truthyCompat('LINK_ASSISTANT_AGENT_COMPACT_JSON', 'OPENCODE_COMPACT_JSON')
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
// Allow setting verbose mode programmatically (e.g., from CLI --verbose flag)
|
|
67
80
|
export function setVerbose(value: boolean) {
|
|
68
81
|
OPENCODE_VERBOSE = value;
|
|
@@ -73,6 +86,11 @@ export namespace Flag {
|
|
|
73
86
|
OPENCODE_DRY_RUN = value;
|
|
74
87
|
}
|
|
75
88
|
|
|
89
|
+
// Allow setting compact JSON mode programmatically (e.g., from CLI --compact-json flag)
|
|
90
|
+
export function setCompactJson(value: boolean) {
|
|
91
|
+
_compactJson = value;
|
|
92
|
+
}
|
|
93
|
+
|
|
76
94
|
function truthy(key: string) {
|
|
77
95
|
const value = process.env[key]?.toLowerCase();
|
|
78
96
|
return value === 'true' || value === '1';
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,12 @@ import {
|
|
|
24
24
|
resolveResumeSession,
|
|
25
25
|
} from './cli/continuous-mode.js';
|
|
26
26
|
import { createBusEventSubscription } from './cli/event-handler.js';
|
|
27
|
+
import {
|
|
28
|
+
outputStatus,
|
|
29
|
+
outputError,
|
|
30
|
+
setCompactJson,
|
|
31
|
+
outputInput,
|
|
32
|
+
} from './cli/output.ts';
|
|
27
33
|
import { createRequire } from 'module';
|
|
28
34
|
import { readFileSync } from 'fs';
|
|
29
35
|
import { dirname, join } from 'path';
|
|
@@ -47,35 +53,21 @@ let hasError = false;
|
|
|
47
53
|
// Install global error handlers to ensure non-zero exit codes
|
|
48
54
|
process.on('uncaughtException', (error) => {
|
|
49
55
|
hasError = true;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
message: error.message,
|
|
56
|
-
stack: error.stack,
|
|
57
|
-
},
|
|
58
|
-
null,
|
|
59
|
-
2
|
|
60
|
-
)
|
|
61
|
-
);
|
|
56
|
+
outputError({
|
|
57
|
+
errorType: error.name || 'UncaughtException',
|
|
58
|
+
message: error.message,
|
|
59
|
+
stack: error.stack,
|
|
60
|
+
});
|
|
62
61
|
process.exit(1);
|
|
63
62
|
});
|
|
64
63
|
|
|
65
64
|
process.on('unhandledRejection', (reason, _promise) => {
|
|
66
65
|
hasError = true;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
message: reason?.message || String(reason),
|
|
73
|
-
stack: reason?.stack,
|
|
74
|
-
},
|
|
75
|
-
null,
|
|
76
|
-
2
|
|
77
|
-
)
|
|
78
|
-
);
|
|
66
|
+
outputError({
|
|
67
|
+
errorType: 'UnhandledRejection',
|
|
68
|
+
message: reason?.message || String(reason),
|
|
69
|
+
stack: reason?.stack,
|
|
70
|
+
});
|
|
79
71
|
process.exit(1);
|
|
80
72
|
});
|
|
81
73
|
|
|
@@ -134,18 +126,8 @@ function readStdinWithTimeout(timeout = null) {
|
|
|
134
126
|
});
|
|
135
127
|
}
|
|
136
128
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
* This prevents the status message from interfering with JSON output parsing
|
|
140
|
-
* @param {object} status - Status object to output
|
|
141
|
-
* @param {boolean} compact - If true, output compact JSON (single line)
|
|
142
|
-
*/
|
|
143
|
-
function outputStatus(status, compact = false) {
|
|
144
|
-
const json = compact
|
|
145
|
-
? JSON.stringify(status)
|
|
146
|
-
: JSON.stringify(status, null, 2);
|
|
147
|
-
console.error(json);
|
|
148
|
-
}
|
|
129
|
+
// outputStatus is now imported from './cli/output.ts'
|
|
130
|
+
// It outputs to stdout for non-error messages, stderr for errors
|
|
149
131
|
|
|
150
132
|
/**
|
|
151
133
|
* Parse model configuration from argv
|
|
@@ -168,9 +150,8 @@ async function parseModelConfig(argv) {
|
|
|
168
150
|
|
|
169
151
|
if (!creds?.accessToken) {
|
|
170
152
|
const compactJson = argv['compact-json'] === true;
|
|
171
|
-
|
|
153
|
+
outputError(
|
|
172
154
|
{
|
|
173
|
-
type: 'error',
|
|
174
155
|
errorType: 'AuthenticationError',
|
|
175
156
|
message:
|
|
176
157
|
'No Claude OAuth credentials found in ~/.claude/.credentials.json. Either authenticate with Claude Code CLI first, or use: agent auth login (select Anthropic > Claude Pro/Max)',
|
|
@@ -221,9 +202,10 @@ async function readSystemMessages(argv) {
|
|
|
221
202
|
);
|
|
222
203
|
const file = Bun.file(resolvedPath);
|
|
223
204
|
if (!(await file.exists())) {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
205
|
+
outputError({
|
|
206
|
+
errorType: 'FileNotFound',
|
|
207
|
+
message: `System message file not found: ${argv['system-message-file']}`,
|
|
208
|
+
});
|
|
227
209
|
process.exit(1);
|
|
228
210
|
}
|
|
229
211
|
systemMessage = await file.text();
|
|
@@ -236,9 +218,10 @@ async function readSystemMessages(argv) {
|
|
|
236
218
|
);
|
|
237
219
|
const file = Bun.file(resolvedPath);
|
|
238
220
|
if (!(await file.exists())) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
221
|
+
outputError({
|
|
222
|
+
errorType: 'FileNotFound',
|
|
223
|
+
message: `Append system message file not found: ${argv['append-system-message-file']}`,
|
|
224
|
+
});
|
|
242
225
|
process.exit(1);
|
|
243
226
|
}
|
|
244
227
|
appendSystemMessage = await file.text();
|
|
@@ -692,7 +675,9 @@ async function main() {
|
|
|
692
675
|
default: false,
|
|
693
676
|
}),
|
|
694
677
|
handler: async (argv) => {
|
|
695
|
-
|
|
678
|
+
// Check both CLI flag and environment variable for compact JSON mode
|
|
679
|
+
const compactJson =
|
|
680
|
+
argv['compact-json'] === true || Flag.COMPACT_JSON();
|
|
696
681
|
|
|
697
682
|
// Check if --prompt flag was provided
|
|
698
683
|
if (argv.prompt) {
|
|
@@ -705,9 +690,9 @@ async function main() {
|
|
|
705
690
|
// Check if --disable-stdin was set without --prompt
|
|
706
691
|
if (argv['disable-stdin']) {
|
|
707
692
|
// Output a helpful message suggesting to use --prompt
|
|
708
|
-
|
|
693
|
+
outputError(
|
|
709
694
|
{
|
|
710
|
-
|
|
695
|
+
errorType: 'ValidationError',
|
|
711
696
|
message:
|
|
712
697
|
'No prompt provided. Use -p/--prompt to specify a message, or remove --disable-stdin to read from stdin.',
|
|
713
698
|
hint: 'Example: agent -p "Hello, how are you?"',
|
|
@@ -726,9 +711,9 @@ async function main() {
|
|
|
726
711
|
|
|
727
712
|
// Exit if --no-always-accept-stdin is set (single message mode not supported in TTY)
|
|
728
713
|
if (!alwaysAcceptStdin) {
|
|
729
|
-
|
|
714
|
+
outputError(
|
|
730
715
|
{
|
|
731
|
-
|
|
716
|
+
errorType: 'ValidationError',
|
|
732
717
|
message:
|
|
733
718
|
'Single message mode (--no-always-accept-stdin) is not supported in interactive terminal mode.',
|
|
734
719
|
hint: 'Use piped input or --prompt for single messages.',
|
|
@@ -820,9 +805,9 @@ async function main() {
|
|
|
820
805
|
// Not JSON
|
|
821
806
|
if (!isInteractive) {
|
|
822
807
|
// In non-interactive mode, only accept JSON
|
|
823
|
-
|
|
808
|
+
outputError(
|
|
824
809
|
{
|
|
825
|
-
|
|
810
|
+
errorType: 'ValidationError',
|
|
826
811
|
message:
|
|
827
812
|
'Invalid JSON input. In non-interactive mode (--no-interactive), only JSON input is accepted.',
|
|
828
813
|
hint: 'Use --interactive to accept plain text, or provide valid JSON: {"message": "your text"}',
|
|
@@ -837,6 +822,16 @@ async function main() {
|
|
|
837
822
|
};
|
|
838
823
|
}
|
|
839
824
|
|
|
825
|
+
// Output input confirmation in JSON format
|
|
826
|
+
outputInput(
|
|
827
|
+
{
|
|
828
|
+
raw: trimmedInput,
|
|
829
|
+
parsed: request,
|
|
830
|
+
format: isInteractive ? 'text' : 'json',
|
|
831
|
+
},
|
|
832
|
+
compactJson
|
|
833
|
+
);
|
|
834
|
+
|
|
840
835
|
// Run agent mode
|
|
841
836
|
await runAgentMode(argv, request);
|
|
842
837
|
},
|
|
@@ -844,6 +839,12 @@ async function main() {
|
|
|
844
839
|
// Initialize logging early for all CLI commands
|
|
845
840
|
// This prevents debug output from appearing in CLI unless --verbose is used
|
|
846
841
|
.middleware(async (argv) => {
|
|
842
|
+
// Set global compact JSON setting (CLI flag or environment variable)
|
|
843
|
+
const isCompact = argv['compact-json'] === true || Flag.COMPACT_JSON();
|
|
844
|
+
if (isCompact) {
|
|
845
|
+
setCompactJson(true);
|
|
846
|
+
}
|
|
847
|
+
|
|
847
848
|
// Set verbose flag if requested
|
|
848
849
|
if (argv.verbose) {
|
|
849
850
|
Flag.setVerbose(true);
|
|
@@ -855,11 +856,12 @@ async function main() {
|
|
|
855
856
|
}
|
|
856
857
|
|
|
857
858
|
// Initialize logging system
|
|
858
|
-
// -
|
|
859
|
-
// -
|
|
859
|
+
// - Print logs to stdout only when verbose for clean CLI output
|
|
860
|
+
// - Use verbose flag to enable DEBUG level logging
|
|
860
861
|
await Log.init({
|
|
861
|
-
print: Flag.OPENCODE_VERBOSE,
|
|
862
|
+
print: Flag.OPENCODE_VERBOSE, // Output logs only when verbose for clean CLI output
|
|
862
863
|
level: Flag.OPENCODE_VERBOSE ? 'DEBUG' : 'INFO',
|
|
864
|
+
compactJson: isCompact,
|
|
863
865
|
});
|
|
864
866
|
})
|
|
865
867
|
.fail((msg, err, yargs) => {
|
|
@@ -874,18 +876,27 @@ async function main() {
|
|
|
874
876
|
// Format other errors using FormatError
|
|
875
877
|
const formatted = FormatError(err);
|
|
876
878
|
if (formatted) {
|
|
877
|
-
|
|
879
|
+
outputError({
|
|
880
|
+
errorType: err.name || 'Error',
|
|
881
|
+
message: formatted,
|
|
882
|
+
});
|
|
878
883
|
} else {
|
|
879
884
|
// Fallback to default error formatting
|
|
880
|
-
|
|
885
|
+
outputError({
|
|
886
|
+
errorType: err.name || 'Error',
|
|
887
|
+
message: err.message || String(err),
|
|
888
|
+
});
|
|
881
889
|
}
|
|
882
890
|
process.exit(1);
|
|
883
891
|
}
|
|
884
892
|
|
|
885
893
|
// Handle validation errors (msg without err)
|
|
886
894
|
if (msg) {
|
|
887
|
-
|
|
888
|
-
|
|
895
|
+
outputError({
|
|
896
|
+
errorType: 'ValidationError',
|
|
897
|
+
message: msg,
|
|
898
|
+
hint: yargs.help(),
|
|
899
|
+
});
|
|
889
900
|
process.exit(1);
|
|
890
901
|
}
|
|
891
902
|
})
|
|
@@ -895,19 +906,12 @@ async function main() {
|
|
|
895
906
|
await yargsInstance.argv;
|
|
896
907
|
} catch (error) {
|
|
897
908
|
hasError = true;
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
message: error instanceof Error ? error.message : String(error),
|
|
905
|
-
stack: error instanceof Error ? error.stack : undefined,
|
|
906
|
-
},
|
|
907
|
-
null,
|
|
908
|
-
2
|
|
909
|
-
)
|
|
910
|
-
);
|
|
909
|
+
outputError({
|
|
910
|
+
timestamp: Date.now(),
|
|
911
|
+
errorType: error instanceof Error ? error.name : 'Error',
|
|
912
|
+
message: error instanceof Error ? error.message : String(error),
|
|
913
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
914
|
+
});
|
|
911
915
|
process.exit(1);
|
|
912
916
|
}
|
|
913
917
|
}
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* JSON Standard Format Handlers
|
|
3
3
|
*
|
|
4
4
|
* Provides adapters for different JSON output formats:
|
|
5
|
-
* - opencode: OpenCode format (default) -
|
|
5
|
+
* - opencode: OpenCode format (default) - configurable JSON formatting
|
|
6
6
|
* - claude: Claude CLI stream-json format - NDJSON (newline-delimited JSON)
|
|
7
|
+
*
|
|
8
|
+
* Output goes to stdout for normal messages, stderr for errors.
|
|
9
|
+
* Use AGENT_CLI_COMPACT env var or --compact-json flag for NDJSON output.
|
|
7
10
|
*/
|
|
8
11
|
|
|
9
12
|
import { EOL } from 'os';
|
|
13
|
+
import { Flag } from '../flag/flag';
|
|
10
14
|
|
|
11
15
|
export type JsonStandard = 'opencode' | 'claude';
|
|
12
16
|
|
|
@@ -46,16 +50,20 @@ export interface ClaudeEvent {
|
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Serialize JSON output based on the selected standard
|
|
53
|
+
* Respects AGENT_CLI_COMPACT env var for OpenCode format
|
|
49
54
|
*/
|
|
50
55
|
export function serializeOutput(
|
|
51
56
|
event: OpenCodeEvent | ClaudeEvent,
|
|
52
57
|
standard: JsonStandard
|
|
53
58
|
): string {
|
|
54
59
|
if (standard === 'claude') {
|
|
55
|
-
// NDJSON format - compact, one line
|
|
60
|
+
// NDJSON format - always compact, one line
|
|
61
|
+
return JSON.stringify(event) + EOL;
|
|
62
|
+
}
|
|
63
|
+
// OpenCode format - compact if AGENT_CLI_COMPACT is set
|
|
64
|
+
if (Flag.COMPACT_JSON()) {
|
|
56
65
|
return JSON.stringify(event) + EOL;
|
|
57
66
|
}
|
|
58
|
-
// OpenCode format - pretty-printed
|
|
59
67
|
return JSON.stringify(event, null, 2) + EOL;
|
|
60
68
|
}
|
|
61
69
|
|
|
@@ -146,13 +154,15 @@ export function createEventHandler(standard: JsonStandard, sessionID: string) {
|
|
|
146
154
|
* Format and output an event
|
|
147
155
|
*/
|
|
148
156
|
output(event: OpenCodeEvent): void {
|
|
157
|
+
const outputStream =
|
|
158
|
+
event.type === 'error' ? process.stderr : process.stdout;
|
|
149
159
|
if (standard === 'claude') {
|
|
150
160
|
const claudeEvent = convertOpenCodeToClaude(event, startTime);
|
|
151
161
|
if (claudeEvent) {
|
|
152
|
-
|
|
162
|
+
outputStream.write(serializeOutput(claudeEvent, standard));
|
|
153
163
|
}
|
|
154
164
|
} else {
|
|
155
|
-
|
|
165
|
+
outputStream.write(serializeOutput(event, standard));
|
|
156
166
|
}
|
|
157
167
|
},
|
|
158
168
|
|
package/src/session/agent.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Permalink: https://github.com/sst/opencode/blob/main/packages/opencode/src/provider/provider.ts
|
|
4
4
|
|
|
5
5
|
import { ToolRegistry } from '../tool/registry.ts';
|
|
6
|
+
import { outputError } from '../cli/output.ts';
|
|
6
7
|
|
|
7
8
|
export class Agent {
|
|
8
9
|
constructor() {
|
|
@@ -96,22 +97,17 @@ export class Agent {
|
|
|
96
97
|
const errorTime = Date.now();
|
|
97
98
|
const callID = `call_${Math.floor(Math.random() * 100000000)}`;
|
|
98
99
|
|
|
99
|
-
// Log full error to stderr
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
stack: error.stack,
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
})
|
|
114
|
-
);
|
|
100
|
+
// Log full error to stderr in flattened JSON format
|
|
101
|
+
outputError({
|
|
102
|
+
errorType: 'ToolExecutionError',
|
|
103
|
+
message: 'Tool execution error',
|
|
104
|
+
tool: tool.name,
|
|
105
|
+
error: {
|
|
106
|
+
name: error.name,
|
|
107
|
+
message: error.message,
|
|
108
|
+
stack: error.stack,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
115
111
|
|
|
116
112
|
// Emit tool_use event with error
|
|
117
113
|
this.emitEvent('tool_use', {
|
|
@@ -217,8 +213,8 @@ export class Agent {
|
|
|
217
213
|
// Pretty-print JSON for human readability, compact for programmatic use
|
|
218
214
|
// Use AGENT_CLI_COMPACT=1 for compact output (tests, automation)
|
|
219
215
|
const compact = process.env.AGENT_CLI_COMPACT === '1';
|
|
220
|
-
|
|
221
|
-
compact ? JSON.stringify(event) : JSON.stringify(event, null, 2)
|
|
216
|
+
process.stdout.write(
|
|
217
|
+
`${compact ? JSON.stringify(event) : JSON.stringify(event, null, 2)}\n`
|
|
222
218
|
);
|
|
223
219
|
}
|
|
224
220
|
}
|
package/src/session/index.ts
CHANGED
|
@@ -547,6 +547,18 @@ export namespace Session {
|
|
|
547
547
|
return obj.reason;
|
|
548
548
|
}
|
|
549
549
|
|
|
550
|
+
// Handle AI SDK unified/raw format: {unified: "tool-calls", raw: "tool_calls"}
|
|
551
|
+
// See: https://github.com/link-assistant/agent/issues/129
|
|
552
|
+
if (typeof obj.unified === 'string') {
|
|
553
|
+
if (Flag.OPENCODE_VERBOSE) {
|
|
554
|
+
log.debug(() => ({
|
|
555
|
+
message: 'toFinishReason extracted unified from object',
|
|
556
|
+
result: obj.unified,
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
return obj.unified;
|
|
560
|
+
}
|
|
561
|
+
|
|
550
562
|
// If we can't extract a specific field, return JSON representation
|
|
551
563
|
if (Flag.OPENCODE_VERBOSE) {
|
|
552
564
|
log.debug(() => ({
|
package/src/session/processor.ts
CHANGED
|
@@ -188,7 +188,7 @@ export namespace SessionProcessor {
|
|
|
188
188
|
await Session.updatePart({
|
|
189
189
|
...match,
|
|
190
190
|
state: {
|
|
191
|
-
status: '
|
|
191
|
+
status: 'failed',
|
|
192
192
|
input: value.input,
|
|
193
193
|
error: (value.error as any).toString(),
|
|
194
194
|
metadata: undefined,
|
|
@@ -367,13 +367,13 @@ export namespace SessionProcessor {
|
|
|
367
367
|
if (
|
|
368
368
|
part.type === 'tool' &&
|
|
369
369
|
part.state.status !== 'completed' &&
|
|
370
|
-
part.state.status !== '
|
|
370
|
+
part.state.status !== 'failed'
|
|
371
371
|
) {
|
|
372
372
|
await Session.updatePart({
|
|
373
373
|
...part,
|
|
374
374
|
state: {
|
|
375
375
|
...part.state,
|
|
376
|
-
status: '
|
|
376
|
+
status: 'failed',
|
|
377
377
|
error: 'Tool execution aborted',
|
|
378
378
|
time: {
|
|
379
379
|
start: Date.now(),
|
package/src/tool/read.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const ReadTool = Tool.define('read', {
|
|
|
29
29
|
async execute(params, ctx) {
|
|
30
30
|
let filepath = params.filePath;
|
|
31
31
|
if (!path.isAbsolute(filepath)) {
|
|
32
|
-
filepath = path.join(
|
|
32
|
+
filepath = path.join(Instance.worktree, filepath);
|
|
33
33
|
}
|
|
34
34
|
const title = path.relative(Instance.worktree, filepath);
|
|
35
35
|
|
|
@@ -70,14 +70,13 @@ export const ReadTool = Tool.define('read', {
|
|
|
70
70
|
return model.info.modalities?.input?.includes('image') ?? false;
|
|
71
71
|
})();
|
|
72
72
|
if (isImage) {
|
|
73
|
-
|
|
73
|
+
// Image format validation (can be disabled via environment variable)
|
|
74
|
+
const verifyImages = process.env.VERIFY_IMAGES_AT_READ_TOOL !== 'false';
|
|
75
|
+
if (verifyImages && !supportsImages) {
|
|
74
76
|
throw new Error(
|
|
75
77
|
`Failed to read image: ${filepath}, model may not be able to read images`
|
|
76
78
|
);
|
|
77
79
|
}
|
|
78
|
-
|
|
79
|
-
// Image format validation (can be disabled via environment variable)
|
|
80
|
-
const verifyImages = process.env.VERIFY_IMAGES_AT_READ_TOOL !== 'false';
|
|
81
80
|
if (verifyImages) {
|
|
82
81
|
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
83
82
|
|
package/src/util/log-lazy.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import makeLog, { levels, LogLevel } from 'log-lazy';
|
|
2
|
-
import { Flag } from '../flag/flag
|
|
2
|
+
import { Flag } from '../flag/flag';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* JSON Lazy Logger
|
|
6
6
|
*
|
|
7
7
|
* Implements lazy logging pattern using log-lazy library.
|
|
8
|
-
* All log output is JSON formatted
|
|
9
|
-
* for
|
|
8
|
+
* All log output is JSON formatted with { "type": "log", "level": "...", ... } structure
|
|
9
|
+
* for consistent parsing with other CLI JSON output.
|
|
10
10
|
*
|
|
11
11
|
* Key features:
|
|
12
12
|
* - Lazy evaluation: log arguments are only computed if logging is enabled
|
|
13
|
-
* - JSON output: all logs are parsable JSON
|
|
13
|
+
* - JSON output: all logs are parsable JSON with `type: "log"` field
|
|
14
|
+
* - Stdout output: logs go to stdout (not stderr) following Unix conventions
|
|
14
15
|
* - Level control: logs respect --verbose flag and LINK_ASSISTANT_AGENT_VERBOSE env
|
|
15
16
|
* - Type-safe: full TypeScript support
|
|
16
17
|
*
|
|
@@ -52,8 +53,21 @@ const LEVEL_PRESETS = {
|
|
|
52
53
|
|
|
53
54
|
type LevelPreset = keyof typeof LEVEL_PRESETS;
|
|
54
55
|
|
|
56
|
+
import { Flag } from '../flag/flag';
|
|
57
|
+
|
|
58
|
+
// Compact JSON mode (can be set at runtime, initialized from Flag)
|
|
59
|
+
let compactJsonMode = Flag.COMPACT_JSON();
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set compact JSON output mode
|
|
63
|
+
*/
|
|
64
|
+
export function setCompactJson(compact: boolean): void {
|
|
65
|
+
compactJsonMode = compact;
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
/**
|
|
56
|
-
* Format a log entry as JSON object
|
|
69
|
+
* Format a log entry as JSON object with { "type": "log", "level": "...", ... } structure
|
|
70
|
+
* This flattened format is consistent with other CLI JSON output.
|
|
57
71
|
*/
|
|
58
72
|
function formatLogEntry(
|
|
59
73
|
level: string,
|
|
@@ -62,6 +76,7 @@ function formatLogEntry(
|
|
|
62
76
|
): string {
|
|
63
77
|
const timestamp = new Date().toISOString();
|
|
64
78
|
const logEntry: Record<string, unknown> = {
|
|
79
|
+
type: 'log',
|
|
65
80
|
level,
|
|
66
81
|
timestamp,
|
|
67
82
|
...tags,
|
|
@@ -84,11 +99,16 @@ function formatLogEntry(
|
|
|
84
99
|
logEntry.message = String(data);
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
// Check both local setting and global Flag
|
|
103
|
+
const useCompact = compactJsonMode || Flag.COMPACT_JSON();
|
|
104
|
+
return useCompact
|
|
105
|
+
? JSON.stringify(logEntry)
|
|
106
|
+
: JSON.stringify(logEntry, null, 2);
|
|
88
107
|
}
|
|
89
108
|
|
|
90
109
|
/**
|
|
91
|
-
* Create the output function that writes to
|
|
110
|
+
* Create the output function that writes to stdout
|
|
111
|
+
* Using stdout follows Unix conventions (stdout for data, stderr for errors)
|
|
92
112
|
*/
|
|
93
113
|
function createOutput(
|
|
94
114
|
level: string,
|
|
@@ -96,8 +116,8 @@ function createOutput(
|
|
|
96
116
|
): (data: unknown) => void {
|
|
97
117
|
return (data: unknown) => {
|
|
98
118
|
const json = formatLogEntry(level, data, tags);
|
|
99
|
-
// Use
|
|
100
|
-
process.
|
|
119
|
+
// Use stdout following Unix conventions
|
|
120
|
+
process.stdout.write(json + '\n');
|
|
101
121
|
};
|
|
102
122
|
}
|
|
103
123
|
|
package/src/util/log.ts
CHANGED
|
@@ -3,18 +3,19 @@ import fs from 'fs/promises';
|
|
|
3
3
|
import { Global } from '../global';
|
|
4
4
|
import z from 'zod';
|
|
5
5
|
import makeLog, { levels } from 'log-lazy';
|
|
6
|
-
import { Flag } from '../flag/flag
|
|
6
|
+
import { Flag } from '../flag/flag';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Logging module with JSON output and lazy evaluation support.
|
|
10
10
|
*
|
|
11
11
|
* Features:
|
|
12
|
-
* - JSON formatted output: All logs
|
|
12
|
+
* - JSON formatted output: All logs use { "type": "log", "level": "...", ... } structure
|
|
13
13
|
* - Lazy evaluation: Use lazy() methods to defer expensive computations
|
|
14
14
|
* - Level control: Respects --verbose flag and log level settings
|
|
15
15
|
* - File logging: Writes to file when not in verbose/print mode
|
|
16
|
+
* - Stdout by default: Logs go to stdout for JSON output consistency
|
|
16
17
|
*
|
|
17
|
-
* The JSON format ensures all output is
|
|
18
|
+
* The JSON format with `type` field ensures all output is consistent with other CLI output.
|
|
18
19
|
*/
|
|
19
20
|
export namespace Log {
|
|
20
21
|
export const Level = z
|
|
@@ -31,6 +32,7 @@ export namespace Log {
|
|
|
31
32
|
|
|
32
33
|
let level: Level = 'INFO';
|
|
33
34
|
let jsonOutput = false; // Whether to output JSON format (enabled in verbose mode)
|
|
35
|
+
let compactJsonOutput = Flag.COMPACT_JSON(); // Whether to use compact JSON (single line)
|
|
34
36
|
|
|
35
37
|
function shouldLog(input: Level): boolean {
|
|
36
38
|
return levelPriority[input] >= levelPriority[level];
|
|
@@ -84,19 +86,24 @@ export namespace Log {
|
|
|
84
86
|
print: boolean;
|
|
85
87
|
dev?: boolean;
|
|
86
88
|
level?: Level;
|
|
89
|
+
compactJson?: boolean;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
92
|
let logpath = '';
|
|
90
93
|
export function file() {
|
|
91
94
|
return logpath;
|
|
92
95
|
}
|
|
93
|
-
|
|
96
|
+
// Default to file for log output (to keep CLI output clean)
|
|
97
|
+
let write = (msg: any) => {}; // Placeholder, set in init
|
|
98
|
+
let fileWrite = (msg: any) => {}; // Placeholder, set in init
|
|
94
99
|
|
|
95
100
|
// Initialize log-lazy for controlling lazy log execution
|
|
96
101
|
let lazyLogInstance = makeLog({ level: 0 }); // Start disabled
|
|
97
102
|
|
|
98
103
|
export async function init(options: Options) {
|
|
99
104
|
if (options.level) level = options.level;
|
|
105
|
+
if (options.compactJson !== undefined)
|
|
106
|
+
compactJsonOutput = options.compactJson;
|
|
100
107
|
cleanup(Global.Path.log);
|
|
101
108
|
|
|
102
109
|
// Always use JSON output format for logs
|
|
@@ -109,30 +116,36 @@ export namespace Log {
|
|
|
109
116
|
level: levels.debug | levels.info | levels.warn | levels.error,
|
|
110
117
|
});
|
|
111
118
|
} else {
|
|
112
|
-
//
|
|
113
|
-
lazyLogInstance = makeLog({
|
|
119
|
+
// Enable info, warn, error by default for JSON output consistency
|
|
120
|
+
lazyLogInstance = makeLog({
|
|
121
|
+
level: levels.info | levels.warn | levels.error,
|
|
122
|
+
});
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
// Output logs to stdout by default for JSON formatting consistency
|
|
126
|
+
// Also write to file for debugging purposes
|
|
127
|
+
logpath = path.join(
|
|
128
|
+
Global.Path.log,
|
|
129
|
+
options.dev
|
|
130
|
+
? 'dev.log'
|
|
131
|
+
: new Date().toISOString().split('.')[0].replace(/:/g, '') + '.log'
|
|
132
|
+
);
|
|
133
|
+
const logfile = Bun.file(logpath);
|
|
134
|
+
await fs.truncate(logpath).catch(() => {});
|
|
135
|
+
const writer = logfile.writer();
|
|
136
|
+
// Write to file
|
|
137
|
+
fileWrite = async (msg: any) => {
|
|
138
|
+
const num = writer.write(msg);
|
|
139
|
+
writer.flush();
|
|
140
|
+
return num;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// Always write to stdout for JSON output consistency
|
|
144
|
+
// Also write to file for debugging purposes
|
|
145
|
+
write = async (msg: any) => {
|
|
146
|
+
process.stdout.write(msg);
|
|
147
|
+
fileWrite(msg);
|
|
148
|
+
};
|
|
136
149
|
}
|
|
137
150
|
|
|
138
151
|
async function cleanup(dir: string) {
|
|
@@ -159,7 +172,8 @@ export namespace Log {
|
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
/**
|
|
162
|
-
* Format log entry as JSON object
|
|
175
|
+
* Format log entry as JSON object with { "type": "log", "level": "...", ... } structure
|
|
176
|
+
* This flattened format is consistent with other CLI JSON output.
|
|
163
177
|
*/
|
|
164
178
|
function formatJson(
|
|
165
179
|
logLevel: Level,
|
|
@@ -169,6 +183,7 @@ export namespace Log {
|
|
|
169
183
|
): string {
|
|
170
184
|
const timestamp = new Date().toISOString();
|
|
171
185
|
const logEntry: Record<string, any> = {
|
|
186
|
+
type: 'log',
|
|
172
187
|
level: logLevel.toLowerCase(),
|
|
173
188
|
timestamp,
|
|
174
189
|
...tags,
|
|
@@ -192,7 +207,12 @@ export namespace Log {
|
|
|
192
207
|
}
|
|
193
208
|
}
|
|
194
209
|
|
|
195
|
-
|
|
210
|
+
// Use compact or pretty format based on configuration
|
|
211
|
+
// Check both local setting and global Flag
|
|
212
|
+
const useCompact = compactJsonOutput || Flag.COMPACT_JSON();
|
|
213
|
+
return useCompact
|
|
214
|
+
? JSON.stringify(logEntry)
|
|
215
|
+
: JSON.stringify(logEntry, null, 2);
|
|
196
216
|
}
|
|
197
217
|
|
|
198
218
|
let last = Date.now();
|
|
@@ -239,7 +259,9 @@ export namespace Log {
|
|
|
239
259
|
) {
|
|
240
260
|
if (jsonOutput) {
|
|
241
261
|
// Use our custom JSON formatting for { log: { ... } } format
|
|
242
|
-
|
|
262
|
+
const jsonMsg = formatJson(logLevel, message, tags || {}, extra) + '\n';
|
|
263
|
+
// All logs go to stdout for consistency with other JSON output
|
|
264
|
+
write(jsonMsg);
|
|
243
265
|
} else {
|
|
244
266
|
write(logLevel.padEnd(5) + ' ' + buildLegacy(message, extra));
|
|
245
267
|
}
|
|
@@ -354,6 +376,8 @@ export namespace Log {
|
|
|
354
376
|
export function syncWithVerboseFlag(): void {
|
|
355
377
|
if (Flag.OPENCODE_VERBOSE) {
|
|
356
378
|
jsonOutput = true;
|
|
379
|
+
// Use stdout for verbose output (following Unix conventions)
|
|
380
|
+
write = (msg: any) => process.stdout.write(msg);
|
|
357
381
|
lazyLogInstance = makeLog({
|
|
358
382
|
level: levels.debug | levels.info | levels.warn | levels.error,
|
|
359
383
|
});
|
|
@@ -361,4 +385,18 @@ export namespace Log {
|
|
|
361
385
|
lazyLogInstance = makeLog({ level: 0 });
|
|
362
386
|
}
|
|
363
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Set compact JSON output mode
|
|
391
|
+
*/
|
|
392
|
+
export function setCompactJson(compact: boolean): void {
|
|
393
|
+
compactJsonOutput = compact;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Check if compact JSON output mode is enabled
|
|
398
|
+
*/
|
|
399
|
+
export function isCompactJson(): boolean {
|
|
400
|
+
return compactJsonOutput;
|
|
401
|
+
}
|
|
364
402
|
}
|