@link-assistant/agent 0.5.2 → 0.6.0
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 +5 -3
- package/src/auth/claude-oauth.ts +50 -24
- package/src/auth/plugins.ts +28 -16
- 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/cache.ts +259 -0
- package/src/provider/echo.ts +174 -0
- package/src/provider/models.ts +4 -5
- package/src/provider/provider.ts +164 -29
- 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 +95 -60
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -68,6 +68,8 @@
|
|
|
68
68
|
"@opentui/solid": "^0.1.46",
|
|
69
69
|
"@parcel/watcher": "^2.5.1",
|
|
70
70
|
"@solid-primitives/event-bus": "^1.1.2",
|
|
71
|
+
"@standard-community/standard-json": "^0.3.5",
|
|
72
|
+
"@standard-community/standard-openapi": "^0.2.9",
|
|
71
73
|
"@zip.js/zip.js": "^2.8.10",
|
|
72
74
|
"ai": "6.0.0-beta.99",
|
|
73
75
|
"chokidar": "^4.0.3",
|
|
@@ -82,9 +84,9 @@
|
|
|
82
84
|
"hono-openapi": "^1.1.1",
|
|
83
85
|
"ignore": "^7.0.5",
|
|
84
86
|
"jsonc-parser": "^3.3.1",
|
|
87
|
+
"lino-objects-codec": "^0.1.1",
|
|
88
|
+
"log-lazy": "^1.0.4",
|
|
85
89
|
"minimatch": "^10.1.1",
|
|
86
|
-
"open": "^11.0.0",
|
|
87
|
-
"partial-json": "^0.1.7",
|
|
88
90
|
"remeda": "^2.32.0",
|
|
89
91
|
"solid-js": "^1.9.10",
|
|
90
92
|
"strip-ansi": "^7.1.2",
|
package/src/auth/claude-oauth.ts
CHANGED
|
@@ -147,9 +147,10 @@ export namespace ClaudeOAuth {
|
|
|
147
147
|
*/
|
|
148
148
|
export async function saveState(state: OAuthState): Promise<void> {
|
|
149
149
|
await Bun.write(statePath, JSON.stringify(state, null, 2));
|
|
150
|
-
log.info(
|
|
150
|
+
log.info(() => ({
|
|
151
|
+
message: 'saved oauth state',
|
|
151
152
|
expiresAt: new Date(state.expiresAt).toISOString(),
|
|
152
|
-
});
|
|
153
|
+
}));
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
@@ -165,14 +166,14 @@ export namespace ClaudeOAuth {
|
|
|
165
166
|
const parsed = OAuthState.parse(content);
|
|
166
167
|
|
|
167
168
|
if (parsed.expiresAt < Date.now()) {
|
|
168
|
-
log.warn('oauth state expired');
|
|
169
|
+
log.warn(() => ({ message: 'oauth state expired' }));
|
|
169
170
|
await clearState();
|
|
170
171
|
return undefined;
|
|
171
172
|
}
|
|
172
173
|
|
|
173
174
|
return parsed;
|
|
174
175
|
} catch (error) {
|
|
175
|
-
log.error('failed to load oauth state',
|
|
176
|
+
log.error(() => ({ message: 'failed to load oauth state', error }));
|
|
176
177
|
return undefined;
|
|
177
178
|
}
|
|
178
179
|
}
|
|
@@ -190,7 +191,7 @@ export namespace ClaudeOAuth {
|
|
|
190
191
|
await fs.unlink(statePath).catch(() => {});
|
|
191
192
|
}
|
|
192
193
|
} catch (error) {
|
|
193
|
-
log.error('failed to clear oauth state',
|
|
194
|
+
log.error(() => ({ message: 'failed to clear oauth state', error }));
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
|
|
@@ -213,7 +214,9 @@ export namespace ClaudeOAuth {
|
|
|
213
214
|
code_verifier: codeVerifier,
|
|
214
215
|
});
|
|
215
216
|
|
|
216
|
-
log.info(
|
|
217
|
+
log.info(() => ({
|
|
218
|
+
message: 'exchanging authorization code for tokens',
|
|
219
|
+
}));
|
|
217
220
|
|
|
218
221
|
const response = await fetch(Config.tokenUrl, {
|
|
219
222
|
method: 'POST',
|
|
@@ -225,7 +228,11 @@ export namespace ClaudeOAuth {
|
|
|
225
228
|
|
|
226
229
|
if (!response.ok) {
|
|
227
230
|
const error = await response.text();
|
|
228
|
-
log.error(
|
|
231
|
+
log.error(() => ({
|
|
232
|
+
message: 'token exchange failed',
|
|
233
|
+
status: response.status,
|
|
234
|
+
error,
|
|
235
|
+
}));
|
|
229
236
|
throw new Error(`Token exchange failed: ${response.status} ${error}`);
|
|
230
237
|
}
|
|
231
238
|
|
|
@@ -267,9 +274,10 @@ export namespace ClaudeOAuth {
|
|
|
267
274
|
};
|
|
268
275
|
|
|
269
276
|
await Bun.write(credentialsPath, JSON.stringify(credentials, null, 2));
|
|
270
|
-
log.info(
|
|
277
|
+
log.info(() => ({
|
|
278
|
+
message: 'saved credentials',
|
|
271
279
|
expiresAt: new Date(credentials.claudeAiOauth!.expiresAt).toISOString(),
|
|
272
|
-
});
|
|
280
|
+
}));
|
|
273
281
|
}
|
|
274
282
|
|
|
275
283
|
/**
|
|
@@ -283,7 +291,10 @@ export namespace ClaudeOAuth {
|
|
|
283
291
|
try {
|
|
284
292
|
const file = Bun.file(credentialsPath);
|
|
285
293
|
if (!(await file.exists())) {
|
|
286
|
-
log.info(
|
|
294
|
+
log.info(() => ({
|
|
295
|
+
message: 'credentials file not found',
|
|
296
|
+
path: credentialsPath,
|
|
297
|
+
}));
|
|
287
298
|
return undefined;
|
|
288
299
|
}
|
|
289
300
|
|
|
@@ -291,27 +302,31 @@ export namespace ClaudeOAuth {
|
|
|
291
302
|
const parsed = Credentials.parse(content);
|
|
292
303
|
|
|
293
304
|
if (!parsed.claudeAiOauth) {
|
|
294
|
-
log.info(
|
|
305
|
+
log.info(() => ({
|
|
306
|
+
message: 'no claudeAiOauth credentials found',
|
|
307
|
+
}));
|
|
295
308
|
return undefined;
|
|
296
309
|
}
|
|
297
310
|
|
|
298
311
|
// Check if token is expired
|
|
299
312
|
if (parsed.claudeAiOauth.expiresAt < Date.now()) {
|
|
300
|
-
log.warn(
|
|
313
|
+
log.warn(() => ({
|
|
314
|
+
message: 'token expired',
|
|
301
315
|
expiresAt: new Date(parsed.claudeAiOauth.expiresAt).toISOString(),
|
|
302
|
-
});
|
|
316
|
+
}));
|
|
303
317
|
// TODO: Implement token refresh using refreshToken
|
|
304
318
|
// For now, user needs to re-authenticate
|
|
305
319
|
}
|
|
306
320
|
|
|
307
|
-
log.info(
|
|
321
|
+
log.info(() => ({
|
|
322
|
+
message: 'loaded oauth credentials',
|
|
308
323
|
subscriptionType: parsed.claudeAiOauth.subscriptionType,
|
|
309
324
|
scopes: parsed.claudeAiOauth.scopes,
|
|
310
|
-
});
|
|
325
|
+
}));
|
|
311
326
|
|
|
312
327
|
return parsed.claudeAiOauth;
|
|
313
328
|
} catch (error) {
|
|
314
|
-
log.error('failed to read credentials',
|
|
329
|
+
log.error(() => ({ message: 'failed to read credentials', error }));
|
|
315
330
|
return undefined;
|
|
316
331
|
}
|
|
317
332
|
}
|
|
@@ -344,7 +359,9 @@ export namespace ClaudeOAuth {
|
|
|
344
359
|
export async function completeAuth(code: string): Promise<boolean> {
|
|
345
360
|
const state = await loadState();
|
|
346
361
|
if (!state) {
|
|
347
|
-
log.error(
|
|
362
|
+
log.error(() => ({
|
|
363
|
+
message: 'no oauth state found - please start login flow first',
|
|
364
|
+
}));
|
|
348
365
|
return false;
|
|
349
366
|
}
|
|
350
367
|
|
|
@@ -352,10 +369,15 @@ export namespace ClaudeOAuth {
|
|
|
352
369
|
const tokens = await exchangeCode(code, state.codeVerifier);
|
|
353
370
|
await saveCredentials(tokens);
|
|
354
371
|
await clearState();
|
|
355
|
-
log.info(
|
|
372
|
+
log.info(() => ({
|
|
373
|
+
message: 'authentication completed successfully',
|
|
374
|
+
}));
|
|
356
375
|
return true;
|
|
357
376
|
} catch (error) {
|
|
358
|
-
log.error(
|
|
377
|
+
log.error(() => ({
|
|
378
|
+
message: 'failed to complete authentication',
|
|
379
|
+
error,
|
|
380
|
+
}));
|
|
359
381
|
await clearState();
|
|
360
382
|
return false;
|
|
361
383
|
}
|
|
@@ -367,7 +389,7 @@ export namespace ClaudeOAuth {
|
|
|
367
389
|
export async function refreshToken(): Promise<boolean> {
|
|
368
390
|
const creds = await getCredentials();
|
|
369
391
|
if (!creds?.refreshToken) {
|
|
370
|
-
log.error('no refresh token available');
|
|
392
|
+
log.error(() => ({ message: 'no refresh token available' }));
|
|
371
393
|
return false;
|
|
372
394
|
}
|
|
373
395
|
|
|
@@ -377,7 +399,7 @@ export namespace ClaudeOAuth {
|
|
|
377
399
|
refresh_token: creds.refreshToken,
|
|
378
400
|
});
|
|
379
401
|
|
|
380
|
-
log.info('refreshing access token');
|
|
402
|
+
log.info(() => ({ message: 'refreshing access token' }));
|
|
381
403
|
|
|
382
404
|
try {
|
|
383
405
|
const response = await fetch(Config.tokenUrl, {
|
|
@@ -390,16 +412,20 @@ export namespace ClaudeOAuth {
|
|
|
390
412
|
|
|
391
413
|
if (!response.ok) {
|
|
392
414
|
const error = await response.text();
|
|
393
|
-
log.error(
|
|
415
|
+
log.error(() => ({
|
|
416
|
+
message: 'token refresh failed',
|
|
417
|
+
status: response.status,
|
|
418
|
+
error,
|
|
419
|
+
}));
|
|
394
420
|
return false;
|
|
395
421
|
}
|
|
396
422
|
|
|
397
423
|
const tokens = TokenResponse.parse(await response.json());
|
|
398
424
|
await saveCredentials(tokens);
|
|
399
|
-
log.info('token refreshed successfully');
|
|
425
|
+
log.info(() => ({ message: 'token refreshed successfully' }));
|
|
400
426
|
return true;
|
|
401
427
|
} catch (error) {
|
|
402
|
-
log.error('failed to refresh token',
|
|
428
|
+
log.error(() => ({ message: 'failed to refresh token', error }));
|
|
403
429
|
return false;
|
|
404
430
|
}
|
|
405
431
|
}
|
package/src/auth/plugins.ts
CHANGED
|
@@ -162,9 +162,10 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
162
162
|
);
|
|
163
163
|
|
|
164
164
|
if (!result.ok) {
|
|
165
|
-
log.error(
|
|
165
|
+
log.error(() => ({
|
|
166
|
+
message: 'anthropic oauth token exchange failed',
|
|
166
167
|
status: result.status,
|
|
167
|
-
});
|
|
168
|
+
}));
|
|
168
169
|
return { type: 'failed' };
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -229,9 +230,10 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
229
230
|
);
|
|
230
231
|
|
|
231
232
|
if (!tokenResult.ok) {
|
|
232
|
-
log.error(
|
|
233
|
+
log.error(() => ({
|
|
234
|
+
message: 'anthropic oauth token exchange failed',
|
|
233
235
|
status: tokenResult.status,
|
|
234
|
-
});
|
|
236
|
+
}));
|
|
235
237
|
return { type: 'failed' };
|
|
236
238
|
}
|
|
237
239
|
|
|
@@ -286,7 +288,9 @@ const AnthropicPlugin: AuthPlugin = {
|
|
|
286
288
|
|
|
287
289
|
// Refresh token if expired
|
|
288
290
|
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
289
|
-
log.info(
|
|
291
|
+
log.info(() => ({
|
|
292
|
+
message: 'refreshing anthropic oauth token',
|
|
293
|
+
}));
|
|
290
294
|
const response = await fetch(
|
|
291
295
|
'https://console.anthropic.com/v1/oauth/token',
|
|
292
296
|
{
|
|
@@ -566,7 +570,7 @@ const GitHubCopilotPlugin: AuthPlugin = {
|
|
|
566
570
|
: 'github.com';
|
|
567
571
|
const urls = getCopilotUrls(domain);
|
|
568
572
|
|
|
569
|
-
log.info('refreshing github copilot token');
|
|
573
|
+
log.info(() => ({ message: 'refreshing github copilot token' }));
|
|
570
574
|
const response = await fetch(urls.COPILOT_API_KEY_URL, {
|
|
571
575
|
headers: {
|
|
572
576
|
Accept: 'application/json',
|
|
@@ -720,7 +724,9 @@ const OpenAIPlugin: AuthPlugin = {
|
|
|
720
724
|
}
|
|
721
725
|
|
|
722
726
|
if (!code) {
|
|
723
|
-
log.error(
|
|
727
|
+
log.error(() => ({
|
|
728
|
+
message: 'openai oauth no code provided',
|
|
729
|
+
}));
|
|
724
730
|
return { type: 'failed' };
|
|
725
731
|
}
|
|
726
732
|
|
|
@@ -740,9 +746,10 @@ const OpenAIPlugin: AuthPlugin = {
|
|
|
740
746
|
});
|
|
741
747
|
|
|
742
748
|
if (!tokenResult.ok) {
|
|
743
|
-
log.error(
|
|
749
|
+
log.error(() => ({
|
|
750
|
+
message: 'openai oauth token exchange failed',
|
|
744
751
|
status: tokenResult.status,
|
|
745
|
-
});
|
|
752
|
+
}));
|
|
746
753
|
return { type: 'failed' };
|
|
747
754
|
}
|
|
748
755
|
|
|
@@ -752,7 +759,9 @@ const OpenAIPlugin: AuthPlugin = {
|
|
|
752
759
|
!json.refresh_token ||
|
|
753
760
|
typeof json.expires_in !== 'number'
|
|
754
761
|
) {
|
|
755
|
-
log.error(
|
|
762
|
+
log.error(() => ({
|
|
763
|
+
message: 'openai oauth token response missing fields',
|
|
764
|
+
}));
|
|
756
765
|
return { type: 'failed' };
|
|
757
766
|
}
|
|
758
767
|
|
|
@@ -787,7 +796,7 @@ const OpenAIPlugin: AuthPlugin = {
|
|
|
787
796
|
|
|
788
797
|
// Refresh token if expired
|
|
789
798
|
if (!currentAuth.access || currentAuth.expires < Date.now()) {
|
|
790
|
-
log.info('refreshing openai oauth token');
|
|
799
|
+
log.info(() => ({ message: 'refreshing openai oauth token' }));
|
|
791
800
|
const response = await fetch(OPENAI_TOKEN_URL, {
|
|
792
801
|
method: 'POST',
|
|
793
802
|
headers: {
|
|
@@ -992,9 +1001,10 @@ const GooglePlugin: AuthPlugin = {
|
|
|
992
1001
|
});
|
|
993
1002
|
|
|
994
1003
|
if (!tokenResult.ok) {
|
|
995
|
-
log.error(
|
|
1004
|
+
log.error(() => ({
|
|
1005
|
+
message: 'google oauth token exchange failed',
|
|
996
1006
|
status: tokenResult.status,
|
|
997
|
-
});
|
|
1007
|
+
}));
|
|
998
1008
|
return { type: 'failed' };
|
|
999
1009
|
}
|
|
1000
1010
|
|
|
@@ -1004,7 +1014,9 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1004
1014
|
!json.refresh_token ||
|
|
1005
1015
|
typeof json.expires_in !== 'number'
|
|
1006
1016
|
) {
|
|
1007
|
-
log.error(
|
|
1017
|
+
log.error(() => ({
|
|
1018
|
+
message: 'google oauth token response missing fields',
|
|
1019
|
+
}));
|
|
1008
1020
|
return { type: 'failed' };
|
|
1009
1021
|
}
|
|
1010
1022
|
|
|
@@ -1015,7 +1027,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1015
1027
|
expires: Date.now() + json.expires_in * 1000,
|
|
1016
1028
|
};
|
|
1017
1029
|
} catch (error) {
|
|
1018
|
-
log.error('google oauth failed',
|
|
1030
|
+
log.error(() => ({ message: 'google oauth failed', error }));
|
|
1019
1031
|
return { type: 'failed' };
|
|
1020
1032
|
}
|
|
1021
1033
|
},
|
|
@@ -1058,7 +1070,7 @@ const GooglePlugin: AuthPlugin = {
|
|
|
1058
1070
|
!currentAuth.access ||
|
|
1059
1071
|
currentAuth.expires < Date.now() + FIVE_MIN_MS
|
|
1060
1072
|
) {
|
|
1061
|
-
log.info('refreshing google oauth token');
|
|
1073
|
+
log.info(() => ({ message: 'refreshing google oauth token' }));
|
|
1062
1074
|
const response = await fetch(GOOGLE_TOKEN_URL, {
|
|
1063
1075
|
method: 'POST',
|
|
1064
1076
|
headers: {
|
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();
|