@link-assistant/agent 0.5.3 → 0.6.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/package.json +4 -3
- package/src/auth/claude-oauth.ts +50 -24
- package/src/auth/plugins.ts +245 -28
- package/src/bun/index.ts +33 -27
- package/src/bus/index.ts +3 -5
- package/src/config/config.ts +39 -22
- package/src/file/ripgrep.ts +1 -1
- package/src/file/time.ts +1 -1
- package/src/file/watcher.ts +10 -5
- package/src/format/index.ts +12 -10
- package/src/index.js +30 -35
- package/src/mcp/index.ts +32 -15
- package/src/patch/index.ts +8 -4
- package/src/project/project.ts +1 -1
- package/src/project/state.ts +15 -7
- package/src/provider/models.ts +4 -5
- package/src/provider/provider.ts +29 -21
- package/src/server/server.ts +4 -5
- package/src/session/agent.js +16 -2
- package/src/session/compaction.ts +4 -6
- package/src/session/index.ts +2 -2
- package/src/session/processor.ts +3 -7
- package/src/session/prompt.ts +78 -49
- package/src/session/revert.ts +1 -1
- package/src/session/summary.ts +2 -2
- package/src/snapshot/index.ts +27 -12
- package/src/storage/storage.ts +18 -18
- package/src/util/log-lazy.ts +291 -0
- package/src/util/log.ts +205 -28
package/src/bun/index.ts
CHANGED
|
@@ -17,10 +17,11 @@ export namespace BunProc {
|
|
|
17
17
|
cmd: string[],
|
|
18
18
|
options?: Bun.SpawnOptions.OptionsObject<any, any, any>
|
|
19
19
|
) {
|
|
20
|
-
log.info(
|
|
20
|
+
log.info(() => ({
|
|
21
|
+
message: 'running',
|
|
21
22
|
cmd: [which(), ...cmd],
|
|
22
23
|
...options,
|
|
23
|
-
});
|
|
24
|
+
}));
|
|
24
25
|
const result = Bun.spawn([which(), ...cmd], {
|
|
25
26
|
...options,
|
|
26
27
|
stdout: 'pipe',
|
|
@@ -42,11 +43,7 @@ export namespace BunProc {
|
|
|
42
43
|
? result.stderr
|
|
43
44
|
: await readableStreamToText(result.stderr)
|
|
44
45
|
: undefined;
|
|
45
|
-
log.info('done',
|
|
46
|
-
code,
|
|
47
|
-
stdout,
|
|
48
|
-
stderr,
|
|
49
|
-
});
|
|
46
|
+
log.info(() => ({ message: 'done', code, stdout, stderr }));
|
|
50
47
|
if (code !== 0) {
|
|
51
48
|
const parts = [`Command failed with exit code ${result.exitCode}`];
|
|
52
49
|
if (stderr) parts.push(`stderr: ${stderr}`);
|
|
@@ -111,14 +108,13 @@ export namespace BunProc {
|
|
|
111
108
|
|
|
112
109
|
// Check for dry-run mode
|
|
113
110
|
if (Flag.OPENCODE_DRY_RUN) {
|
|
114
|
-
log.info(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
);
|
|
111
|
+
log.info(() => ({
|
|
112
|
+
message:
|
|
113
|
+
'[DRY RUN] Would install package (skipping actual installation)',
|
|
114
|
+
pkg,
|
|
115
|
+
version,
|
|
116
|
+
targetPath: mod,
|
|
117
|
+
}));
|
|
122
118
|
// In dry-run mode, pretend the package is installed
|
|
123
119
|
return mod;
|
|
124
120
|
}
|
|
@@ -137,10 +133,11 @@ export namespace BunProc {
|
|
|
137
133
|
// - If .npmrc files exist, Bun will use them automatically
|
|
138
134
|
// - If no .npmrc files exist, Bun will default to https://registry.npmjs.org
|
|
139
135
|
// - No need to pass --registry flag
|
|
140
|
-
log.info(
|
|
136
|
+
log.info(() => ({
|
|
137
|
+
message: "installing package using Bun's default registry resolution",
|
|
141
138
|
pkg,
|
|
142
139
|
version,
|
|
143
|
-
});
|
|
140
|
+
}));
|
|
144
141
|
|
|
145
142
|
// Retry logic for cache-related errors
|
|
146
143
|
let lastError: Error | undefined;
|
|
@@ -150,7 +147,12 @@ export namespace BunProc {
|
|
|
150
147
|
cwd: Global.Path.cache,
|
|
151
148
|
});
|
|
152
149
|
|
|
153
|
-
log.info(
|
|
150
|
+
log.info(() => ({
|
|
151
|
+
message: 'package installed successfully',
|
|
152
|
+
pkg,
|
|
153
|
+
version,
|
|
154
|
+
attempt,
|
|
155
|
+
}));
|
|
154
156
|
parsed.dependencies[pkg] = version;
|
|
155
157
|
await Bun.write(pkgjson.name!, JSON.stringify(parsed, null, 2));
|
|
156
158
|
return mod;
|
|
@@ -158,43 +160,47 @@ export namespace BunProc {
|
|
|
158
160
|
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
159
161
|
const isCacheError = isCacheRelatedError(errorMsg);
|
|
160
162
|
|
|
161
|
-
log.warn(
|
|
163
|
+
log.warn(() => ({
|
|
164
|
+
message: 'package installation attempt failed',
|
|
162
165
|
pkg,
|
|
163
166
|
version,
|
|
164
167
|
attempt,
|
|
165
168
|
maxRetries: MAX_RETRIES,
|
|
166
169
|
error: errorMsg,
|
|
167
170
|
isCacheError,
|
|
168
|
-
});
|
|
171
|
+
}));
|
|
169
172
|
|
|
170
173
|
if (isCacheError && attempt < MAX_RETRIES) {
|
|
171
|
-
log.info(
|
|
174
|
+
log.info(() => ({
|
|
175
|
+
message: 'retrying installation after cache-related error',
|
|
172
176
|
pkg,
|
|
173
177
|
version,
|
|
174
178
|
attempt,
|
|
175
179
|
nextAttempt: attempt + 1,
|
|
176
180
|
delayMs: RETRY_DELAY_MS,
|
|
177
|
-
});
|
|
181
|
+
}));
|
|
178
182
|
await delay(RETRY_DELAY_MS);
|
|
179
183
|
lastError = e instanceof Error ? e : new Error(errorMsg);
|
|
180
184
|
continue;
|
|
181
185
|
}
|
|
182
186
|
|
|
183
187
|
// Non-cache error or final attempt - log and throw
|
|
184
|
-
log.error(
|
|
188
|
+
log.error(() => ({
|
|
189
|
+
message: 'package installation failed',
|
|
185
190
|
pkg,
|
|
186
191
|
version,
|
|
187
192
|
error: errorMsg,
|
|
188
193
|
stack: e instanceof Error ? e.stack : undefined,
|
|
189
194
|
possibleCacheCorruption: isCacheError,
|
|
190
195
|
attempts: attempt,
|
|
191
|
-
});
|
|
196
|
+
}));
|
|
192
197
|
|
|
193
198
|
// Provide helpful recovery instructions for cache-related errors
|
|
194
199
|
if (isCacheError) {
|
|
195
|
-
log.error(
|
|
196
|
-
|
|
197
|
-
|
|
200
|
+
log.error(() => ({
|
|
201
|
+
message:
|
|
202
|
+
'Bun package cache may be corrupted. Try clearing the cache with: bun pm cache rm',
|
|
203
|
+
}));
|
|
198
204
|
}
|
|
199
205
|
|
|
200
206
|
throw new InstallFailedError(
|
package/src/bus/index.ts
CHANGED
|
@@ -63,9 +63,7 @@ export namespace Bus {
|
|
|
63
63
|
type: def.type,
|
|
64
64
|
properties,
|
|
65
65
|
};
|
|
66
|
-
log.info('publishing',
|
|
67
|
-
type: def.type,
|
|
68
|
-
});
|
|
66
|
+
log.info(() => ({ message: 'publishing', type: def.type }));
|
|
69
67
|
const pending = [];
|
|
70
68
|
for (const key of [def.type, '*']) {
|
|
71
69
|
const match = state().subscriptions.get(key);
|
|
@@ -107,14 +105,14 @@ export namespace Bus {
|
|
|
107
105
|
}
|
|
108
106
|
|
|
109
107
|
function raw(type: string, callback: (event: any) => void) {
|
|
110
|
-
log.info('subscribing',
|
|
108
|
+
log.info(() => ({ message: 'subscribing', type }));
|
|
111
109
|
const subscriptions = state().subscriptions;
|
|
112
110
|
let match = subscriptions.get(type) ?? [];
|
|
113
111
|
match.push(callback);
|
|
114
112
|
subscriptions.set(type, match);
|
|
115
113
|
|
|
116
114
|
return () => {
|
|
117
|
-
log.info('unsubscribing',
|
|
115
|
+
log.info(() => ({ message: 'unsubscribing', type }));
|
|
118
116
|
const match = subscriptions.get(type);
|
|
119
117
|
if (!match) return;
|
|
120
118
|
const index = match.indexOf(callback);
|
package/src/config/config.ts
CHANGED
|
@@ -53,16 +53,21 @@ export namespace Config {
|
|
|
53
53
|
if (!newDirExists) {
|
|
54
54
|
try {
|
|
55
55
|
// Perform migration by copying the entire directory
|
|
56
|
-
log.info(
|
|
57
|
-
`Migrating config from ${oldDir} to ${newDir} for smooth transition
|
|
58
|
-
);
|
|
56
|
+
log.info(() => ({
|
|
57
|
+
message: `Migrating config from ${oldDir} to ${newDir} for smooth transition`,
|
|
58
|
+
}));
|
|
59
59
|
|
|
60
60
|
// Use fs-extra style recursive copy
|
|
61
61
|
await copyDirectory(oldDir, newDir);
|
|
62
62
|
|
|
63
|
-
log.info(
|
|
63
|
+
log.info(() => ({
|
|
64
|
+
message: `Successfully migrated config to ${newDir}`,
|
|
65
|
+
}));
|
|
64
66
|
} catch (error) {
|
|
65
|
-
log.error(
|
|
67
|
+
log.error(() => ({
|
|
68
|
+
message: `Failed to migrate config from ${oldDir}:`,
|
|
69
|
+
error,
|
|
70
|
+
}));
|
|
66
71
|
// Don't throw - allow the app to continue with the old config
|
|
67
72
|
}
|
|
68
73
|
}
|
|
@@ -83,14 +88,19 @@ export namespace Config {
|
|
|
83
88
|
.catch(() => false);
|
|
84
89
|
|
|
85
90
|
if (oldGlobalExists && !newGlobalExists) {
|
|
86
|
-
log.info(
|
|
87
|
-
`Migrating global config from ${oldGlobalPath} to ${newGlobalPath}
|
|
88
|
-
);
|
|
91
|
+
log.info(() => ({
|
|
92
|
+
message: `Migrating global config from ${oldGlobalPath} to ${newGlobalPath}`,
|
|
93
|
+
}));
|
|
89
94
|
await copyDirectory(oldGlobalPath, newGlobalPath);
|
|
90
|
-
log.info(
|
|
95
|
+
log.info(() => ({
|
|
96
|
+
message: `Successfully migrated global config to ${newGlobalPath}`,
|
|
97
|
+
}));
|
|
91
98
|
}
|
|
92
99
|
} catch (error) {
|
|
93
|
-
log.error(
|
|
100
|
+
log.error(() => ({
|
|
101
|
+
message: 'Failed to migrate global config:',
|
|
102
|
+
error,
|
|
103
|
+
}));
|
|
94
104
|
// Don't throw - allow the app to continue
|
|
95
105
|
}
|
|
96
106
|
}
|
|
@@ -126,7 +136,10 @@ export namespace Config {
|
|
|
126
136
|
// Override with custom config if provided
|
|
127
137
|
if (Flag.OPENCODE_CONFIG) {
|
|
128
138
|
result = mergeDeep(result, await loadFile(Flag.OPENCODE_CONFIG));
|
|
129
|
-
log.debug(
|
|
139
|
+
log.debug(() => ({
|
|
140
|
+
message: 'loaded custom config',
|
|
141
|
+
path: Flag.OPENCODE_CONFIG,
|
|
142
|
+
}));
|
|
130
143
|
}
|
|
131
144
|
|
|
132
145
|
for (const file of ['opencode.jsonc', 'opencode.json']) {
|
|
@@ -142,7 +155,9 @@ export namespace Config {
|
|
|
142
155
|
|
|
143
156
|
if (Flag.OPENCODE_CONFIG_CONTENT) {
|
|
144
157
|
result = mergeDeep(result, JSON.parse(Flag.OPENCODE_CONFIG_CONTENT));
|
|
145
|
-
log.debug(
|
|
158
|
+
log.debug(() => ({
|
|
159
|
+
message: 'loaded custom config from OPENCODE_CONFIG_CONTENT',
|
|
160
|
+
}));
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
for (const [key, value] of Object.entries(auth)) {
|
|
@@ -182,12 +197,11 @@ export namespace Config {
|
|
|
182
197
|
const filteredDirs = foundDirs.filter((dir) => {
|
|
183
198
|
// If .link-assistant-agent exists, exclude .opencode directories
|
|
184
199
|
if (hasNewConfig && dir.endsWith('.opencode')) {
|
|
185
|
-
log.debug(
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
);
|
|
200
|
+
log.debug(() => ({
|
|
201
|
+
message:
|
|
202
|
+
'Skipping .opencode directory (using .link-assistant-agent):',
|
|
203
|
+
path: dir,
|
|
204
|
+
}));
|
|
191
205
|
return false;
|
|
192
206
|
}
|
|
193
207
|
return true;
|
|
@@ -197,9 +211,10 @@ export namespace Config {
|
|
|
197
211
|
|
|
198
212
|
if (Flag.OPENCODE_CONFIG_DIR) {
|
|
199
213
|
directories.push(Flag.OPENCODE_CONFIG_DIR);
|
|
200
|
-
log.debug(
|
|
214
|
+
log.debug(() => ({
|
|
215
|
+
message: 'loading config from LINK_ASSISTANT_AGENT_CONFIG_DIR',
|
|
201
216
|
path: Flag.OPENCODE_CONFIG_DIR,
|
|
202
|
-
});
|
|
217
|
+
}));
|
|
203
218
|
}
|
|
204
219
|
|
|
205
220
|
const promises: Promise<void>[] = [];
|
|
@@ -212,7 +227,9 @@ export namespace Config {
|
|
|
212
227
|
dir === Flag.OPENCODE_CONFIG_DIR
|
|
213
228
|
) {
|
|
214
229
|
for (const file of ['opencode.jsonc', 'opencode.json']) {
|
|
215
|
-
log.debug(
|
|
230
|
+
log.debug(() => ({
|
|
231
|
+
message: `loading config from ${path.join(dir, file)}`,
|
|
232
|
+
}));
|
|
216
233
|
result = mergeDeep(result, await loadFile(path.join(dir, file)));
|
|
217
234
|
// to satisy the type checker
|
|
218
235
|
result.agent ??= {};
|
|
@@ -932,7 +949,7 @@ export namespace Config {
|
|
|
932
949
|
});
|
|
933
950
|
|
|
934
951
|
async function loadFile(filepath: string): Promise<Info> {
|
|
935
|
-
log.info('loading',
|
|
952
|
+
log.info(() => ({ message: 'loading', path: filepath }));
|
|
936
953
|
let text = await Bun.file(filepath)
|
|
937
954
|
.text()
|
|
938
955
|
.catch((err) => {
|
package/src/file/ripgrep.ts
CHANGED
|
@@ -275,7 +275,7 @@ export namespace Ripgrep {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
export async function tree(input: { cwd: string; limit?: number }) {
|
|
278
|
-
log.info('tree', input);
|
|
278
|
+
log.info(() => ({ message: 'tree', ...input }));
|
|
279
279
|
const files = await Array.fromAsync(Ripgrep.files({ cwd: input.cwd }));
|
|
280
280
|
interface Node {
|
|
281
281
|
path: string[];
|
package/src/file/time.ts
CHANGED
|
@@ -15,7 +15,7 @@ export namespace FileTime {
|
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
export function read(sessionID: string, file: string) {
|
|
18
|
-
log.info('read',
|
|
18
|
+
log.info(() => ({ message: 'read', sessionID, file }));
|
|
19
19
|
const { read } = state();
|
|
20
20
|
read[sessionID] = read[sessionID] || {};
|
|
21
21
|
read[sessionID][file] = new Date();
|
package/src/file/watcher.ts
CHANGED
|
@@ -36,7 +36,7 @@ export namespace FileWatcher {
|
|
|
36
36
|
const state = Instance.state(
|
|
37
37
|
async () => {
|
|
38
38
|
if (Instance.project.vcs !== 'git') return {};
|
|
39
|
-
log.info('init');
|
|
39
|
+
log.info(() => ({ message: 'init' }));
|
|
40
40
|
const cfg = await Config.get();
|
|
41
41
|
const backend = (() => {
|
|
42
42
|
if (process.platform === 'win32') return 'windows';
|
|
@@ -44,18 +44,23 @@ export namespace FileWatcher {
|
|
|
44
44
|
if (process.platform === 'linux') return 'inotify';
|
|
45
45
|
})();
|
|
46
46
|
if (!backend) {
|
|
47
|
-
log.error(
|
|
47
|
+
log.error(() => ({
|
|
48
|
+
message: 'watcher backend not supported',
|
|
48
49
|
platform: process.platform,
|
|
49
|
-
});
|
|
50
|
+
}));
|
|
50
51
|
return {};
|
|
51
52
|
}
|
|
52
|
-
log.info(
|
|
53
|
+
log.info(() => ({
|
|
54
|
+
message: 'watcher backend',
|
|
55
|
+
platform: process.platform,
|
|
56
|
+
backend,
|
|
57
|
+
}));
|
|
53
58
|
const sub = await watcher().subscribe(
|
|
54
59
|
Instance.directory,
|
|
55
60
|
(err, evts) => {
|
|
56
61
|
if (err) return;
|
|
57
62
|
for (const evt of evts) {
|
|
58
|
-
log.info('event', evt);
|
|
63
|
+
log.info(() => ({ message: 'event', ...evt }));
|
|
59
64
|
if (evt.type === 'create')
|
|
60
65
|
Bus.publish(Event.Updated, { file: evt.path, event: 'add' });
|
|
61
66
|
if (evt.type === 'update')
|
package/src/format/index.ts
CHANGED
|
@@ -29,7 +29,7 @@ export namespace Format {
|
|
|
29
29
|
|
|
30
30
|
const formatters: Record<string, Formatter.Info> = {};
|
|
31
31
|
if (cfg.formatter === false) {
|
|
32
|
-
log.info('all formatters are disabled');
|
|
32
|
+
log.info(() => ({ message: 'all formatters are disabled' }));
|
|
33
33
|
return {
|
|
34
34
|
enabled,
|
|
35
35
|
formatters,
|
|
@@ -77,10 +77,10 @@ export namespace Format {
|
|
|
77
77
|
const formatters = await state().then((x) => x.formatters);
|
|
78
78
|
const result = [];
|
|
79
79
|
for (const item of Object.values(formatters)) {
|
|
80
|
-
log.info('checking',
|
|
80
|
+
log.info(() => ({ message: 'checking', name: item.name, ext }));
|
|
81
81
|
if (!item.extensions.includes(ext)) continue;
|
|
82
82
|
if (!(await isEnabled(item))) continue;
|
|
83
|
-
log.info('enabled',
|
|
83
|
+
log.info(() => ({ message: 'enabled', name: item.name, ext }));
|
|
84
84
|
result.push(item);
|
|
85
85
|
}
|
|
86
86
|
return result;
|
|
@@ -101,14 +101,14 @@ export namespace Format {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
export function init() {
|
|
104
|
-
log.info('init');
|
|
104
|
+
log.info(() => ({ message: 'init' }));
|
|
105
105
|
Bus.subscribe(File.Event.Edited, async (payload) => {
|
|
106
106
|
const file = payload.properties.file;
|
|
107
|
-
log.info('formatting',
|
|
107
|
+
log.info(() => ({ message: 'formatting', file }));
|
|
108
108
|
const ext = path.extname(file);
|
|
109
109
|
|
|
110
110
|
for (const item of await getFormatter(ext)) {
|
|
111
|
-
log.info('running',
|
|
111
|
+
log.info(() => ({ message: 'running', command: item.command }));
|
|
112
112
|
try {
|
|
113
113
|
const proc = Bun.spawn({
|
|
114
114
|
cmd: item.command.map((x) => x.replace('$FILE', file)),
|
|
@@ -119,17 +119,19 @@ export namespace Format {
|
|
|
119
119
|
});
|
|
120
120
|
const exit = await proc.exited;
|
|
121
121
|
if (exit !== 0)
|
|
122
|
-
log.error(
|
|
122
|
+
log.error(() => ({
|
|
123
|
+
message: 'failed',
|
|
123
124
|
command: item.command,
|
|
124
125
|
...item.environment,
|
|
125
|
-
});
|
|
126
|
+
}));
|
|
126
127
|
} catch (error) {
|
|
127
|
-
log.error(
|
|
128
|
+
log.error(() => ({
|
|
129
|
+
message: 'failed to format file',
|
|
128
130
|
error,
|
|
129
131
|
command: item.command,
|
|
130
132
|
...item.environment,
|
|
131
133
|
file,
|
|
132
|
-
});
|
|
134
|
+
}));
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
});
|
package/src/index.js
CHANGED
|
@@ -246,22 +246,19 @@ async function readSystemMessages(argv) {
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
async function runAgentMode(argv, request) {
|
|
249
|
-
//
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
console.error(`Script path: ${import.meta.path}`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Log dry-run mode if enabled
|
|
249
|
+
// Log version and command info in verbose mode using lazy logging
|
|
250
|
+
Log.Default.lazy.info(() => ({
|
|
251
|
+
message: 'Agent started',
|
|
252
|
+
version: pkg.version,
|
|
253
|
+
command: process.argv.join(' '),
|
|
254
|
+
workingDirectory: process.cwd(),
|
|
255
|
+
scriptPath: import.meta.path,
|
|
256
|
+
}));
|
|
261
257
|
if (Flag.OPENCODE_DRY_RUN) {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
258
|
+
Log.Default.lazy.info(() => ({
|
|
259
|
+
message: 'Dry run mode enabled',
|
|
260
|
+
mode: 'dry-run',
|
|
261
|
+
}));
|
|
265
262
|
}
|
|
266
263
|
|
|
267
264
|
const { providerID, modelID } = await parseModelConfig(argv);
|
|
@@ -269,9 +266,11 @@ async function runAgentMode(argv, request) {
|
|
|
269
266
|
// Validate and get JSON standard
|
|
270
267
|
const jsonStandard = argv['json-standard'];
|
|
271
268
|
if (!isValidJsonStandard(jsonStandard)) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
269
|
+
outputStatus({
|
|
270
|
+
type: 'error',
|
|
271
|
+
errorType: 'ValidationError',
|
|
272
|
+
message: `Invalid JSON standard: ${jsonStandard}. Use "opencode" or "claude".`,
|
|
273
|
+
});
|
|
275
274
|
process.exit(1);
|
|
276
275
|
}
|
|
277
276
|
|
|
@@ -317,24 +316,20 @@ async function runAgentMode(argv, request) {
|
|
|
317
316
|
* @param {object} argv - Command line arguments
|
|
318
317
|
*/
|
|
319
318
|
async function runContinuousAgentMode(argv) {
|
|
320
|
-
// Note: verbose flag and logging are now initialized in middleware
|
|
321
|
-
// See main() function for the middleware that sets up Flag and Log.init()
|
|
322
|
-
|
|
323
319
|
const compactJson = argv['compact-json'] === true;
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Log dry-run mode if enabled
|
|
320
|
+
// Log version and command info in verbose mode using lazy logging
|
|
321
|
+
Log.Default.lazy.info(() => ({
|
|
322
|
+
message: 'Agent started (continuous mode)',
|
|
323
|
+
version: pkg.version,
|
|
324
|
+
command: process.argv.join(' '),
|
|
325
|
+
workingDirectory: process.cwd(),
|
|
326
|
+
scriptPath: import.meta.path,
|
|
327
|
+
}));
|
|
334
328
|
if (Flag.OPENCODE_DRY_RUN) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
329
|
+
Log.Default.lazy.info(() => ({
|
|
330
|
+
message: 'Dry run mode enabled',
|
|
331
|
+
mode: 'dry-run',
|
|
332
|
+
}));
|
|
338
333
|
}
|
|
339
334
|
|
|
340
335
|
const { providerID, modelID } = await parseModelConfig(argv);
|
|
@@ -935,7 +930,7 @@ async function main() {
|
|
|
935
930
|
}
|
|
936
931
|
|
|
937
932
|
// Initialize logging system
|
|
938
|
-
// - If verbose: print logs to stderr for debugging
|
|
933
|
+
// - If verbose: print logs to stderr for debugging in JSON format
|
|
939
934
|
// - Otherwise: write logs to file to keep CLI output clean
|
|
940
935
|
await Log.init({
|
|
941
936
|
print: Flag.OPENCODE_VERBOSE,
|
package/src/mcp/index.ts
CHANGED
|
@@ -81,9 +81,10 @@ export namespace MCP {
|
|
|
81
81
|
await Promise.all(
|
|
82
82
|
Object.values(state.clients).map((client) =>
|
|
83
83
|
client.close().catch((error) => {
|
|
84
|
-
log.error(
|
|
84
|
+
log.error(() => ({
|
|
85
|
+
message: 'Failed to close MCP client',
|
|
85
86
|
error,
|
|
86
|
-
});
|
|
87
|
+
}));
|
|
87
88
|
})
|
|
88
89
|
)
|
|
89
90
|
);
|
|
@@ -119,10 +120,10 @@ export namespace MCP {
|
|
|
119
120
|
|
|
120
121
|
async function create(key: string, mcp: Config.Mcp) {
|
|
121
122
|
if (mcp.enabled === false) {
|
|
122
|
-
log.info('mcp server disabled',
|
|
123
|
+
log.info(() => ({ message: 'mcp server disabled', key }));
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
|
-
log.info('found',
|
|
126
|
+
log.info(() => ({ message: 'found', key, type: mcp.type }));
|
|
126
127
|
let mcpClient: MCPClient | undefined;
|
|
127
128
|
let status: Status | undefined = undefined;
|
|
128
129
|
|
|
@@ -152,7 +153,11 @@ export namespace MCP {
|
|
|
152
153
|
transport,
|
|
153
154
|
})
|
|
154
155
|
.then((client) => {
|
|
155
|
-
log.info(
|
|
156
|
+
log.info(() => ({
|
|
157
|
+
message: 'connected',
|
|
158
|
+
key,
|
|
159
|
+
transport: name,
|
|
160
|
+
}));
|
|
156
161
|
mcpClient = client;
|
|
157
162
|
status = { status: 'connected' };
|
|
158
163
|
return true;
|
|
@@ -160,12 +165,13 @@ export namespace MCP {
|
|
|
160
165
|
.catch((error) => {
|
|
161
166
|
lastError =
|
|
162
167
|
error instanceof Error ? error : new Error(String(error));
|
|
163
|
-
log.debug(
|
|
168
|
+
log.debug(() => ({
|
|
169
|
+
message: 'transport connection failed',
|
|
164
170
|
key,
|
|
165
171
|
transport: name,
|
|
166
172
|
url: mcp.url,
|
|
167
173
|
error: lastError.message,
|
|
168
|
-
});
|
|
174
|
+
}));
|
|
169
175
|
status = {
|
|
170
176
|
status: 'failed' as const,
|
|
171
177
|
error: lastError.message,
|
|
@@ -198,11 +204,12 @@ export namespace MCP {
|
|
|
198
204
|
};
|
|
199
205
|
})
|
|
200
206
|
.catch((error) => {
|
|
201
|
-
log.error(
|
|
207
|
+
log.error(() => ({
|
|
208
|
+
message: 'local mcp startup failed',
|
|
202
209
|
key,
|
|
203
210
|
command: mcp.command,
|
|
204
211
|
error: error instanceof Error ? error.message : String(error),
|
|
205
|
-
});
|
|
212
|
+
}));
|
|
206
213
|
status = {
|
|
207
214
|
status: 'failed' as const,
|
|
208
215
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -228,14 +235,19 @@ export namespace MCP {
|
|
|
228
235
|
mcpClient.tools(),
|
|
229
236
|
mcp.timeout ?? 5000
|
|
230
237
|
).catch((err) => {
|
|
231
|
-
log.error(
|
|
238
|
+
log.error(() => ({
|
|
239
|
+
message: 'failed to get tools from client',
|
|
240
|
+
key,
|
|
241
|
+
error: err,
|
|
242
|
+
}));
|
|
232
243
|
return undefined;
|
|
233
244
|
});
|
|
234
245
|
if (!result) {
|
|
235
246
|
await mcpClient.close().catch((error) => {
|
|
236
|
-
log.error(
|
|
247
|
+
log.error(() => ({
|
|
248
|
+
message: 'Failed to close MCP client',
|
|
237
249
|
error,
|
|
238
|
-
});
|
|
250
|
+
}));
|
|
239
251
|
});
|
|
240
252
|
status = {
|
|
241
253
|
status: 'failed',
|
|
@@ -250,10 +262,11 @@ export namespace MCP {
|
|
|
250
262
|
};
|
|
251
263
|
}
|
|
252
264
|
|
|
253
|
-
log.info(
|
|
265
|
+
log.info(() => ({
|
|
266
|
+
message: 'create() successfully created client',
|
|
254
267
|
key,
|
|
255
268
|
toolCount: Object.keys(result).length,
|
|
256
|
-
});
|
|
269
|
+
}));
|
|
257
270
|
return {
|
|
258
271
|
mcpClient,
|
|
259
272
|
status,
|
|
@@ -274,7 +287,11 @@ export namespace MCP {
|
|
|
274
287
|
const clientsSnapshot = await clients();
|
|
275
288
|
for (const [clientName, client] of Object.entries(clientsSnapshot)) {
|
|
276
289
|
const tools = await client.tools().catch((e) => {
|
|
277
|
-
log.error(
|
|
290
|
+
log.error(() => ({
|
|
291
|
+
message: 'failed to get tools',
|
|
292
|
+
clientName,
|
|
293
|
+
error: e.message,
|
|
294
|
+
}));
|
|
278
295
|
const failedStatus = {
|
|
279
296
|
status: 'failed' as const,
|
|
280
297
|
error: e instanceof Error ? e.message : String(e),
|
package/src/patch/index.ts
CHANGED
|
@@ -532,13 +532,13 @@ export namespace Patch {
|
|
|
532
532
|
|
|
533
533
|
await fs.writeFile(hunk.path, hunk.contents, 'utf-8');
|
|
534
534
|
added.push(hunk.path);
|
|
535
|
-
log.info(
|
|
535
|
+
log.info(() => ({ message: 'Added file', path: hunk.path }));
|
|
536
536
|
break;
|
|
537
537
|
|
|
538
538
|
case 'delete':
|
|
539
539
|
await fs.unlink(hunk.path);
|
|
540
540
|
deleted.push(hunk.path);
|
|
541
|
-
log.info(
|
|
541
|
+
log.info(() => ({ message: 'Deleted file', path: hunk.path }));
|
|
542
542
|
break;
|
|
543
543
|
|
|
544
544
|
case 'update':
|
|
@@ -557,12 +557,16 @@ export namespace Patch {
|
|
|
557
557
|
await fs.writeFile(hunk.move_path, fileUpdate.content, 'utf-8');
|
|
558
558
|
await fs.unlink(hunk.path);
|
|
559
559
|
modified.push(hunk.move_path);
|
|
560
|
-
log.info(
|
|
560
|
+
log.info(() => ({
|
|
561
|
+
message: 'Moved file',
|
|
562
|
+
from: hunk.path,
|
|
563
|
+
to: hunk.move_path,
|
|
564
|
+
}));
|
|
561
565
|
} else {
|
|
562
566
|
// Regular update
|
|
563
567
|
await fs.writeFile(hunk.path, fileUpdate.content, 'utf-8');
|
|
564
568
|
modified.push(hunk.path);
|
|
565
|
-
log.info(
|
|
569
|
+
log.info(() => ({ message: 'Updated file', path: hunk.path }));
|
|
566
570
|
}
|
|
567
571
|
break;
|
|
568
572
|
}
|
package/src/project/project.ts
CHANGED
|
@@ -24,7 +24,7 @@ export namespace Project {
|
|
|
24
24
|
export type Info = z.infer<typeof Info>;
|
|
25
25
|
|
|
26
26
|
export async function fromDirectory(directory: string) {
|
|
27
|
-
log.info('fromDirectory',
|
|
27
|
+
log.info(() => ({ message: 'fromDirectory', directory }));
|
|
28
28
|
const matches = Filesystem.up({ targets: ['.git'], start: directory });
|
|
29
29
|
const git = await matches.next().then((x) => x.value);
|
|
30
30
|
await matches.return();
|