@clinebot/core 0.0.6 → 0.0.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/dist/agents/hooks-config-loader.d.ts +1 -0
- package/dist/auth/cline.d.ts +2 -0
- package/dist/auth/codex.d.ts +5 -1
- package/dist/auth/oca.d.ts +7 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.node.d.ts +2 -0
- package/dist/index.node.js +164 -162
- package/dist/input/mention-enricher.d.ts +1 -0
- package/dist/providers/local-provider-service.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/session/default-session-manager.d.ts +13 -17
- package/dist/session/rpc-spawn-lease.d.ts +7 -0
- package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
- package/dist/session/session-agent-events.d.ts +15 -0
- package/dist/session/session-config-builder.d.ts +13 -0
- package/dist/session/session-manager.d.ts +2 -2
- package/dist/session/session-team-coordination.d.ts +12 -0
- package/dist/session/session-telemetry.d.ts +9 -0
- package/dist/session/unified-session-persistence-service.d.ts +12 -16
- package/dist/session/utils/helpers.d.ts +1 -1
- package/dist/session/utils/types.d.ts +1 -1
- package/dist/storage/provider-settings-legacy-migration.d.ts +25 -0
- package/dist/telemetry/core-events.d.ts +122 -0
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/executors/file-read.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/presets.d.ts +1 -1
- package/dist/tools/schemas.d.ts +48 -11
- package/dist/tools/types.d.ts +3 -3
- package/dist/types/config.d.ts +1 -1
- package/dist/types/events.d.ts +1 -1
- package/dist/types/provider-settings.d.ts +4 -4
- package/dist/types.d.ts +1 -1
- package/package.json +4 -3
- package/src/agents/hooks-config-loader.ts +2 -0
- package/src/auth/cline.ts +35 -1
- package/src/auth/codex.ts +27 -2
- package/src/auth/oca.ts +31 -4
- package/src/auth/types.ts +3 -0
- package/src/index.node.ts +4 -0
- package/src/index.ts +27 -0
- package/src/input/file-indexer.test.ts +40 -0
- package/src/input/file-indexer.ts +21 -0
- package/src/input/mention-enricher.test.ts +3 -0
- package/src/input/mention-enricher.ts +3 -0
- package/src/providers/local-provider-service.ts +6 -7
- package/src/runtime/hook-file-hooks.test.ts +51 -1
- package/src/runtime/hook-file-hooks.ts +91 -11
- package/src/runtime/session-runtime.ts +1 -1
- package/src/session/default-session-manager.e2e.test.ts +2 -1
- package/src/session/default-session-manager.ts +367 -601
- package/src/session/rpc-spawn-lease.test.ts +49 -0
- package/src/session/rpc-spawn-lease.ts +122 -0
- package/src/session/runtime-oauth-token-manager.ts +21 -14
- package/src/session/session-agent-events.ts +159 -0
- package/src/session/session-config-builder.ts +111 -0
- package/src/session/session-graph.ts +2 -0
- package/src/session/session-host.ts +21 -0
- package/src/session/session-manager.ts +2 -2
- package/src/session/session-team-coordination.ts +198 -0
- package/src/session/session-telemetry.ts +95 -0
- package/src/session/unified-session-persistence-service.test.ts +81 -0
- package/src/session/unified-session-persistence-service.ts +470 -469
- package/src/session/utils/helpers.ts +1 -1
- package/src/session/utils/types.ts +1 -1
- package/src/storage/provider-settings-legacy-migration.test.ts +133 -1
- package/src/storage/provider-settings-legacy-migration.ts +63 -11
- package/src/telemetry/core-events.ts +344 -0
- package/src/tools/definitions.test.ts +203 -36
- package/src/tools/definitions.ts +66 -28
- package/src/tools/executors/editor.test.ts +35 -0
- package/src/tools/executors/editor.ts +33 -46
- package/src/tools/executors/file-read.test.ts +29 -5
- package/src/tools/executors/file-read.ts +17 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/presets.ts +1 -1
- package/src/tools/schemas.ts +88 -38
- package/src/tools/types.ts +7 -3
- package/src/types/config.ts +1 -1
- package/src/types/events.ts +6 -1
- package/src/types/provider-settings.ts +6 -6
- package/src/types.ts +1 -1
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinebot/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.10",
|
|
4
4
|
"main": "./dist/index.node.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@clinebot/agents": "0.0.
|
|
7
|
-
"@clinebot/llms": "0.0.
|
|
6
|
+
"@clinebot/agents": "0.0.10",
|
|
7
|
+
"@clinebot/llms": "0.0.10",
|
|
8
|
+
"@clinebot/shared": "0.0.10",
|
|
8
9
|
"@opentelemetry/api": "^1.9.0",
|
|
9
10
|
"@opentelemetry/api-logs": "^0.56.0",
|
|
10
11
|
"@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
|
|
@@ -20,6 +20,7 @@ export enum HookConfigFileName {
|
|
|
20
20
|
TaskResume = "TaskResume",
|
|
21
21
|
TaskCancel = "TaskCancel",
|
|
22
22
|
TaskComplete = "TaskComplete",
|
|
23
|
+
TaskError = "TaskError",
|
|
23
24
|
PreToolUse = "PreToolUse",
|
|
24
25
|
PostToolUse = "PostToolUse",
|
|
25
26
|
UserPromptSubmit = "UserPromptSubmit",
|
|
@@ -34,6 +35,7 @@ export const HOOK_CONFIG_FILE_EVENT_MAP: Readonly<
|
|
|
34
35
|
[HookConfigFileName.TaskResume]: "agent_resume",
|
|
35
36
|
[HookConfigFileName.TaskCancel]: "agent_abort",
|
|
36
37
|
[HookConfigFileName.TaskComplete]: "agent_end",
|
|
38
|
+
[HookConfigFileName.TaskError]: "agent_error",
|
|
37
39
|
[HookConfigFileName.PreToolUse]: "tool_call",
|
|
38
40
|
[HookConfigFileName.PostToolUse]: "tool_result",
|
|
39
41
|
[HookConfigFileName.UserPromptSubmit]: "prompt_submit",
|
package/src/auth/cline.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
import {
|
|
3
|
+
captureAuthFailed,
|
|
4
|
+
captureAuthLoggedOut,
|
|
5
|
+
captureAuthStarted,
|
|
6
|
+
captureAuthSucceeded,
|
|
7
|
+
identifyAccount,
|
|
8
|
+
} from "../telemetry/core-events";
|
|
1
9
|
import { startLocalOAuthServer } from "./server.js";
|
|
2
10
|
import type {
|
|
3
11
|
OAuthCredentials,
|
|
@@ -63,6 +71,7 @@ export interface ClineOAuthProviderOptions {
|
|
|
63
71
|
callbackPath?: string;
|
|
64
72
|
callbackPorts?: number[];
|
|
65
73
|
requestTimeoutMs?: number;
|
|
74
|
+
telemetry?: ITelemetryService;
|
|
66
75
|
/**
|
|
67
76
|
* Optional identity provider name for token exchange.
|
|
68
77
|
*/
|
|
@@ -253,6 +262,7 @@ export async function loginClineOAuth(
|
|
|
253
262
|
callbacks: OAuthLoginCallbacks;
|
|
254
263
|
},
|
|
255
264
|
): Promise<ClineOAuthCredentials> {
|
|
265
|
+
captureAuthStarted(options.telemetry, options.provider ?? "cline");
|
|
256
266
|
const callbackPorts = options.callbackPorts?.length
|
|
257
267
|
? options.callbackPorts
|
|
258
268
|
: DEFAULT_CALLBACK_PORTS;
|
|
@@ -312,7 +322,26 @@ export async function loginClineOAuth(
|
|
|
312
322
|
throw new Error("Missing authorization code");
|
|
313
323
|
}
|
|
314
324
|
|
|
315
|
-
|
|
325
|
+
const credentials = await exchangeAuthorizationCode(
|
|
326
|
+
code,
|
|
327
|
+
callbackUrl,
|
|
328
|
+
options,
|
|
329
|
+
provider,
|
|
330
|
+
);
|
|
331
|
+
captureAuthSucceeded(options.telemetry, provider ?? "cline");
|
|
332
|
+
identifyAccount(options.telemetry, {
|
|
333
|
+
id: credentials.accountId,
|
|
334
|
+
email: credentials.email,
|
|
335
|
+
provider: provider ?? "cline",
|
|
336
|
+
});
|
|
337
|
+
return credentials;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
captureAuthFailed(
|
|
340
|
+
options.telemetry,
|
|
341
|
+
options.provider ?? "cline",
|
|
342
|
+
error instanceof Error ? error.message : String(error),
|
|
343
|
+
);
|
|
344
|
+
throw error;
|
|
316
345
|
} finally {
|
|
317
346
|
localServer.close();
|
|
318
347
|
}
|
|
@@ -384,6 +413,11 @@ export async function getValidClineCredentials(
|
|
|
384
413
|
return await refreshClineToken(currentCredentials, providerOptions);
|
|
385
414
|
} catch (error) {
|
|
386
415
|
if (error instanceof ClineOAuthTokenError && error.isLikelyInvalidGrant()) {
|
|
416
|
+
captureAuthLoggedOut(
|
|
417
|
+
providerOptions.telemetry,
|
|
418
|
+
providerOptions.provider ?? "cline",
|
|
419
|
+
"invalid_grant",
|
|
420
|
+
);
|
|
387
421
|
return null;
|
|
388
422
|
}
|
|
389
423
|
if (currentCredentials.expires - Date.now() > retryableTokenGraceMs) {
|
package/src/auth/codex.ts
CHANGED
|
@@ -5,7 +5,15 @@
|
|
|
5
5
|
* It is only intended for CLI use, not browser environments.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
8
9
|
import { nanoid } from "nanoid";
|
|
10
|
+
import {
|
|
11
|
+
captureAuthFailed,
|
|
12
|
+
captureAuthLoggedOut,
|
|
13
|
+
captureAuthStarted,
|
|
14
|
+
captureAuthSucceeded,
|
|
15
|
+
identifyAccount,
|
|
16
|
+
} from "../telemetry/core-events";
|
|
9
17
|
import { startLocalOAuthServer } from "./server.js";
|
|
10
18
|
import type {
|
|
11
19
|
OAuthCredentials,
|
|
@@ -295,7 +303,9 @@ export async function loginOpenAICodex(options: {
|
|
|
295
303
|
onProgress?: (message: string) => void;
|
|
296
304
|
onManualCodeInput?: () => Promise<string>;
|
|
297
305
|
originator?: string;
|
|
306
|
+
telemetry?: ITelemetryService;
|
|
298
307
|
}): Promise<OAuthCredentials> {
|
|
308
|
+
captureAuthStarted(options.telemetry, "openai-codex");
|
|
299
309
|
const callbackConfig = resolveCallbackServerConfig();
|
|
300
310
|
const { verifier, state, url } = await createAuthorizationFlow(
|
|
301
311
|
options.originator,
|
|
@@ -352,7 +362,21 @@ export async function loginOpenAICodex(options: {
|
|
|
352
362
|
throw new Error("Token exchange failed");
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
|
|
365
|
+
const credentials = toCodexCredentials(tokenResult);
|
|
366
|
+
captureAuthSucceeded(options.telemetry, "openai-codex");
|
|
367
|
+
identifyAccount(options.telemetry, {
|
|
368
|
+
id: credentials.accountId,
|
|
369
|
+
email: credentials.email,
|
|
370
|
+
provider: "openai-codex",
|
|
371
|
+
});
|
|
372
|
+
return credentials;
|
|
373
|
+
} catch (error) {
|
|
374
|
+
captureAuthFailed(
|
|
375
|
+
options.telemetry,
|
|
376
|
+
"openai-codex",
|
|
377
|
+
error instanceof Error ? error.message : String(error),
|
|
378
|
+
);
|
|
379
|
+
throw error;
|
|
356
380
|
} finally {
|
|
357
381
|
server.close();
|
|
358
382
|
}
|
|
@@ -378,7 +402,7 @@ export async function refreshOpenAICodexToken(
|
|
|
378
402
|
|
|
379
403
|
export async function getValidOpenAICodexCredentials(
|
|
380
404
|
currentCredentials: OAuthCredentials | null,
|
|
381
|
-
options?: RefreshTokenResolution,
|
|
405
|
+
options?: RefreshTokenResolution & { telemetry?: ITelemetryService },
|
|
382
406
|
): Promise<OAuthCredentials | null> {
|
|
383
407
|
if (!currentCredentials) {
|
|
384
408
|
return null;
|
|
@@ -409,6 +433,7 @@ export async function getValidOpenAICodexCredentials(
|
|
|
409
433
|
error instanceof OpenAICodexOAuthTokenError &&
|
|
410
434
|
error.isLikelyInvalidGrant()
|
|
411
435
|
) {
|
|
436
|
+
captureAuthLoggedOut(options?.telemetry, "openai-codex", "invalid_grant");
|
|
412
437
|
return null;
|
|
413
438
|
}
|
|
414
439
|
if (currentCredentials.expires - Date.now() > retryableTokenGraceMs) {
|
package/src/auth/oca.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
1
2
|
import { nanoid } from "nanoid";
|
|
3
|
+
import {
|
|
4
|
+
captureAuthFailed,
|
|
5
|
+
captureAuthLoggedOut,
|
|
6
|
+
captureAuthStarted,
|
|
7
|
+
captureAuthSucceeded,
|
|
8
|
+
identifyAccount,
|
|
9
|
+
} from "../telemetry/core-events";
|
|
2
10
|
import { startLocalOAuthServer } from "./server.js";
|
|
3
11
|
import type {
|
|
4
12
|
OAuthCredentials,
|
|
@@ -325,8 +333,12 @@ function buildAuthorizationUrl(input: {
|
|
|
325
333
|
}
|
|
326
334
|
|
|
327
335
|
export async function loginOcaOAuth(
|
|
328
|
-
options: OcaOAuthProviderOptions & {
|
|
336
|
+
options: OcaOAuthProviderOptions & {
|
|
337
|
+
callbacks: OAuthLoginCallbacks;
|
|
338
|
+
telemetry?: ITelemetryService;
|
|
339
|
+
},
|
|
329
340
|
): Promise<OAuthCredentials> {
|
|
341
|
+
captureAuthStarted(options.telemetry, "oca");
|
|
330
342
|
const config = resolveConfig(options.config);
|
|
331
343
|
const mode = resolveMode(options.mode);
|
|
332
344
|
const callbackPorts = options.callbackPorts?.length
|
|
@@ -391,13 +403,27 @@ export async function loginOcaOAuth(
|
|
|
391
403
|
throw new Error("State mismatch");
|
|
392
404
|
}
|
|
393
405
|
|
|
394
|
-
|
|
406
|
+
const credentials = await exchangeAuthorizationCode({
|
|
395
407
|
code,
|
|
396
408
|
state: returnedState,
|
|
397
409
|
mode,
|
|
398
410
|
config,
|
|
399
411
|
requestTimeoutMs,
|
|
400
412
|
});
|
|
413
|
+
captureAuthSucceeded(options.telemetry, "oca");
|
|
414
|
+
identifyAccount(options.telemetry, {
|
|
415
|
+
id: credentials.accountId,
|
|
416
|
+
email: credentials.email,
|
|
417
|
+
provider: "oca",
|
|
418
|
+
});
|
|
419
|
+
return credentials;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
captureAuthFailed(
|
|
422
|
+
options.telemetry,
|
|
423
|
+
"oca",
|
|
424
|
+
error instanceof Error ? error.message : String(error),
|
|
425
|
+
);
|
|
426
|
+
throw error;
|
|
401
427
|
} finally {
|
|
402
428
|
localServer.close();
|
|
403
429
|
}
|
|
@@ -447,8 +473,8 @@ export async function refreshOcaToken(
|
|
|
447
473
|
|
|
448
474
|
export async function getValidOcaCredentials(
|
|
449
475
|
currentCredentials: OAuthCredentials | null,
|
|
450
|
-
options?: OcaTokenResolution,
|
|
451
|
-
providerOptions?: OcaOAuthProviderOptions,
|
|
476
|
+
options?: OcaTokenResolution & { telemetry?: ITelemetryService },
|
|
477
|
+
providerOptions?: OcaOAuthProviderOptions & { telemetry?: ITelemetryService },
|
|
452
478
|
): Promise<OAuthCredentials | null> {
|
|
453
479
|
if (!currentCredentials) {
|
|
454
480
|
return null;
|
|
@@ -475,6 +501,7 @@ export async function getValidOcaCredentials(
|
|
|
475
501
|
return await refreshOcaToken(currentCredentials, providerOptions);
|
|
476
502
|
} catch (error) {
|
|
477
503
|
if (error instanceof OcaOAuthTokenError && error.isLikelyInvalidGrant()) {
|
|
504
|
+
captureAuthLoggedOut(providerOptions?.telemetry, "oca", "invalid_grant");
|
|
478
505
|
return null;
|
|
479
506
|
}
|
|
480
507
|
if (currentCredentials.expires - Date.now() > retryableTokenGraceMs) {
|
package/src/auth/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
|
|
1
3
|
export interface OAuthPrompt {
|
|
2
4
|
message: string;
|
|
3
5
|
defaultValue?: string;
|
|
@@ -69,6 +71,7 @@ export interface OcaOAuthProviderOptions {
|
|
|
69
71
|
requestTimeoutMs?: number;
|
|
70
72
|
refreshBufferMs?: number;
|
|
71
73
|
retryableTokenGraceMs?: number;
|
|
74
|
+
telemetry?: ITelemetryService;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
export interface OcaTokenResolution {
|
package/src/index.node.ts
CHANGED
|
@@ -170,6 +170,10 @@ export {
|
|
|
170
170
|
} from "./runtime/workflows";
|
|
171
171
|
export { DefaultSessionManager } from "./session/default-session-manager";
|
|
172
172
|
export { RpcCoreSessionService } from "./session/rpc-session-service";
|
|
173
|
+
export {
|
|
174
|
+
type RpcSpawnLease,
|
|
175
|
+
tryAcquireRpcSpawnLease,
|
|
176
|
+
} from "./session/rpc-spawn-lease";
|
|
173
177
|
export {
|
|
174
178
|
deriveSubsessionStatus,
|
|
175
179
|
makeSubSessionId,
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
type ToolApprovalResult,
|
|
14
14
|
type ToolContext,
|
|
15
15
|
} from "@clinebot/agents";
|
|
16
|
+
export { LlmsModels, LlmsProviders } from "@clinebot/llms";
|
|
16
17
|
// Shared contracts and path helpers re-exported for app consumers.
|
|
17
18
|
export type {
|
|
18
19
|
AgentMode,
|
|
@@ -57,7 +58,10 @@ export {
|
|
|
57
58
|
ensureHookLogDir,
|
|
58
59
|
ensureParentDir,
|
|
59
60
|
resolveClineDataDir,
|
|
61
|
+
resolveClineDir,
|
|
60
62
|
resolveSessionDataDir,
|
|
63
|
+
setClineDir,
|
|
64
|
+
setClineDirIfUnset,
|
|
61
65
|
setHomeDir,
|
|
62
66
|
setHomeDirIfUnset,
|
|
63
67
|
} from "@clinebot/shared/storage";
|
|
@@ -123,6 +127,29 @@ export {
|
|
|
123
127
|
buildTeamProgressSummary,
|
|
124
128
|
toTeamProgressLifecycleEvent,
|
|
125
129
|
} from "./team";
|
|
130
|
+
export {
|
|
131
|
+
captureAuthFailed,
|
|
132
|
+
captureAuthLoggedOut,
|
|
133
|
+
captureAuthStarted,
|
|
134
|
+
captureAuthSucceeded,
|
|
135
|
+
captureConversationTurnEvent,
|
|
136
|
+
captureDiffEditFailure,
|
|
137
|
+
captureHookDiscovery,
|
|
138
|
+
captureMentionFailed,
|
|
139
|
+
captureMentionSearchResults,
|
|
140
|
+
captureMentionUsed,
|
|
141
|
+
captureModeSwitch,
|
|
142
|
+
captureProviderApiError,
|
|
143
|
+
captureSkillUsed,
|
|
144
|
+
captureSubagentExecution,
|
|
145
|
+
captureTaskCompleted,
|
|
146
|
+
captureTaskCreated,
|
|
147
|
+
captureTaskRestarted,
|
|
148
|
+
captureTokenUsage,
|
|
149
|
+
captureToolUsage,
|
|
150
|
+
identifyAccount,
|
|
151
|
+
LegacyTelemetryEvents,
|
|
152
|
+
} from "./telemetry/core-events";
|
|
126
153
|
export type { ITelemetryAdapter } from "./telemetry/ITelemetryAdapter";
|
|
127
154
|
export {
|
|
128
155
|
LoggerTelemetryAdapter,
|
|
@@ -84,4 +84,44 @@ describe("file indexer", () => {
|
|
|
84
84
|
await rm(cwd, { recursive: true, force: true });
|
|
85
85
|
}
|
|
86
86
|
});
|
|
87
|
+
|
|
88
|
+
it("evicts stale workspace indexes after 10 minutes when multiple workspaces exist", async () => {
|
|
89
|
+
vi.useFakeTimers();
|
|
90
|
+
const firstWorkspace = await createTempWorkspace();
|
|
91
|
+
const secondWorkspace = await createTempWorkspace();
|
|
92
|
+
try {
|
|
93
|
+
await writeFile(
|
|
94
|
+
path.join(firstWorkspace, "first.ts"),
|
|
95
|
+
"export const first = 1\n",
|
|
96
|
+
"utf8",
|
|
97
|
+
);
|
|
98
|
+
await writeFile(
|
|
99
|
+
path.join(secondWorkspace, "second.ts"),
|
|
100
|
+
"export const second = 2\n",
|
|
101
|
+
"utf8",
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const firstIndex = await getFileIndex(firstWorkspace, { ttlMs: 60_000 });
|
|
105
|
+
expect(firstIndex.has("first.ts")).toBe(true);
|
|
106
|
+
|
|
107
|
+
await getFileIndex(secondWorkspace, { ttlMs: 60_000 });
|
|
108
|
+
vi.advanceTimersByTime(10 * 60_000 + 1);
|
|
109
|
+
|
|
110
|
+
await getFileIndex(secondWorkspace, { ttlMs: 60_000 });
|
|
111
|
+
await writeFile(
|
|
112
|
+
path.join(firstWorkspace, "later.ts"),
|
|
113
|
+
"export const later = 3\n",
|
|
114
|
+
"utf8",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const rebuiltFirstIndex = await getFileIndex(firstWorkspace, {
|
|
118
|
+
ttlMs: 60_000,
|
|
119
|
+
});
|
|
120
|
+
expect(rebuiltFirstIndex.has("later.ts")).toBe(true);
|
|
121
|
+
} finally {
|
|
122
|
+
vi.useRealTimers();
|
|
123
|
+
await rm(firstWorkspace, { recursive: true, force: true });
|
|
124
|
+
await rm(secondWorkspace, { recursive: true, force: true });
|
|
125
|
+
}
|
|
126
|
+
});
|
|
87
127
|
});
|
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { isMainThread, parentPort, Worker } from "node:worker_threads";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_INDEX_TTL_MS = 15_000;
|
|
7
|
+
const STALE_CACHE_EVICTION_MS = 10 * 60_000;
|
|
7
8
|
const WORKER_INDEX_REQUEST_TIMEOUT_MS = 1_000;
|
|
8
9
|
const DEFAULT_EXCLUDE_DIRS = new Set([
|
|
9
10
|
".git",
|
|
@@ -21,6 +22,7 @@ const DEFAULT_EXCLUDE_DIRS = new Set([
|
|
|
21
22
|
interface CacheEntry {
|
|
22
23
|
files: Set<string>;
|
|
23
24
|
lastBuiltAt: number;
|
|
25
|
+
lastAccessedAt: number;
|
|
24
26
|
pending: Promise<Set<string>> | null;
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -43,6 +45,20 @@ interface IndexResponseMessage {
|
|
|
43
45
|
|
|
44
46
|
const CACHE = new Map<string, CacheEntry>();
|
|
45
47
|
|
|
48
|
+
function pruneStaleCacheEntries(now: number): void {
|
|
49
|
+
if (CACHE.size <= 1) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const [cwd, entry] of CACHE.entries()) {
|
|
53
|
+
if (entry.pending) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (now - entry.lastAccessedAt > STALE_CACHE_EVICTION_MS) {
|
|
57
|
+
CACHE.delete(cwd);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
46
62
|
function toPosixRelative(cwd: string, absolutePath: string): string {
|
|
47
63
|
return path.relative(cwd, absolutePath).split(path.sep).join("/");
|
|
48
64
|
}
|
|
@@ -265,6 +281,7 @@ export async function getFileIndex(
|
|
|
265
281
|
): Promise<Set<string>> {
|
|
266
282
|
const ttlMs = options.ttlMs ?? DEFAULT_INDEX_TTL_MS;
|
|
267
283
|
const now = Date.now();
|
|
284
|
+
pruneStaleCacheEntries(now);
|
|
268
285
|
const existing = CACHE.get(cwd);
|
|
269
286
|
|
|
270
287
|
if (
|
|
@@ -273,10 +290,12 @@ export async function getFileIndex(
|
|
|
273
290
|
now - existing.lastBuiltAt <= ttlMs &&
|
|
274
291
|
existing.files.size > 0
|
|
275
292
|
) {
|
|
293
|
+
existing.lastAccessedAt = now;
|
|
276
294
|
return existing.files;
|
|
277
295
|
}
|
|
278
296
|
|
|
279
297
|
if (existing?.pending) {
|
|
298
|
+
existing.lastAccessedAt = now;
|
|
280
299
|
return existing.pending;
|
|
281
300
|
}
|
|
282
301
|
|
|
@@ -284,6 +303,7 @@ export async function getFileIndex(
|
|
|
284
303
|
CACHE.set(cwd, {
|
|
285
304
|
files,
|
|
286
305
|
lastBuiltAt: Date.now(),
|
|
306
|
+
lastAccessedAt: Date.now(),
|
|
287
307
|
pending: null,
|
|
288
308
|
});
|
|
289
309
|
return files;
|
|
@@ -292,6 +312,7 @@ export async function getFileIndex(
|
|
|
292
312
|
CACHE.set(cwd, {
|
|
293
313
|
files: existing?.files ?? new Set<string>(),
|
|
294
314
|
lastBuiltAt: existing?.lastBuiltAt ?? 0,
|
|
315
|
+
lastAccessedAt: now,
|
|
295
316
|
pending,
|
|
296
317
|
});
|
|
297
318
|
|
|
@@ -32,6 +32,7 @@ describe("enrichPromptWithMentions", () => {
|
|
|
32
32
|
cwd,
|
|
33
33
|
);
|
|
34
34
|
|
|
35
|
+
expect(result.mentions).toEqual(["src/index.ts"]);
|
|
35
36
|
expect(result.matchedFiles).toEqual(["src/index.ts"]);
|
|
36
37
|
expect(result.ignoredMentions).toEqual([]);
|
|
37
38
|
expect(result.prompt).toBe("Review @src/index.ts");
|
|
@@ -50,6 +51,7 @@ describe("enrichPromptWithMentions", () => {
|
|
|
50
51
|
cwd,
|
|
51
52
|
);
|
|
52
53
|
|
|
54
|
+
expect(result.mentions).toEqual(["missing/file.ts"]);
|
|
53
55
|
expect(result.matchedFiles).toEqual([]);
|
|
54
56
|
expect(result.ignoredMentions).toEqual(["missing/file.ts"]);
|
|
55
57
|
expect(result.prompt).toBe(
|
|
@@ -72,6 +74,7 @@ describe("enrichPromptWithMentions", () => {
|
|
|
72
74
|
{ maxTotalBytes: 5, maxFiles: 2, maxFileBytes: 5 },
|
|
73
75
|
);
|
|
74
76
|
|
|
77
|
+
expect(result.mentions).toEqual(["a.ts", "b.ts"]);
|
|
75
78
|
expect(result.matchedFiles).toEqual(["a.ts"]);
|
|
76
79
|
expect(result.ignoredMentions).toEqual(["b.ts"]);
|
|
77
80
|
expect(result.prompt).toBe("Use @a.ts and @b.ts");
|
|
@@ -13,6 +13,7 @@ export interface MentionEnricherOptions extends FastFileIndexOptions {
|
|
|
13
13
|
|
|
14
14
|
export interface MentionEnrichmentResult {
|
|
15
15
|
prompt: string;
|
|
16
|
+
mentions: string[];
|
|
16
17
|
matchedFiles: string[];
|
|
17
18
|
ignoredMentions: string[];
|
|
18
19
|
}
|
|
@@ -60,6 +61,7 @@ export async function enrichPromptWithMentions(
|
|
|
60
61
|
if (mentions.length === 0) {
|
|
61
62
|
return {
|
|
62
63
|
prompt: input,
|
|
64
|
+
mentions: [],
|
|
63
65
|
matchedFiles: [],
|
|
64
66
|
ignoredMentions: [],
|
|
65
67
|
};
|
|
@@ -113,6 +115,7 @@ export async function enrichPromptWithMentions(
|
|
|
113
115
|
|
|
114
116
|
return {
|
|
115
117
|
prompt: input,
|
|
118
|
+
mentions,
|
|
116
119
|
matchedFiles: matched,
|
|
117
120
|
ignoredMentions: ignored,
|
|
118
121
|
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
-
import
|
|
4
|
-
import { models } from "@clinebot/llms";
|
|
3
|
+
import { LlmsModels, type LlmsProviders } from "@clinebot/llms";
|
|
5
4
|
import type {
|
|
6
5
|
RpcAddProviderActionRequest,
|
|
7
6
|
RpcOAuthProviderId,
|
|
@@ -215,7 +214,7 @@ function registerCustomProvider(
|
|
|
215
214
|
]),
|
|
216
215
|
);
|
|
217
216
|
|
|
218
|
-
|
|
217
|
+
LlmsModels.registerProvider({
|
|
219
218
|
provider: {
|
|
220
219
|
id: providerId,
|
|
221
220
|
name: entry.provider.name.trim() || titleCaseFromId(providerId),
|
|
@@ -318,7 +317,7 @@ export async function addLocalProvider(
|
|
|
318
317
|
}> {
|
|
319
318
|
const providerId = request.providerId.trim().toLowerCase();
|
|
320
319
|
if (!providerId) throw new Error("providerId is required");
|
|
321
|
-
if (
|
|
320
|
+
if (LlmsModels.hasProvider(providerId)) {
|
|
322
321
|
throw new Error(`provider "${providerId}" already exists`);
|
|
323
322
|
}
|
|
324
323
|
const providerName = request.name.trim();
|
|
@@ -411,10 +410,10 @@ export async function listLocalProviders(
|
|
|
411
410
|
settingsPath: string;
|
|
412
411
|
}> {
|
|
413
412
|
const state = manager.read();
|
|
414
|
-
const ids =
|
|
413
|
+
const ids = LlmsModels.getProviderIds().sort((a, b) => a.localeCompare(b));
|
|
415
414
|
const providerItems = await Promise.all(
|
|
416
415
|
ids.map(async (id): Promise<RpcProviderListItem> => {
|
|
417
|
-
const info = await
|
|
416
|
+
const info = await LlmsModels.getProvider(id);
|
|
418
417
|
const providerModels = await getLocalProviderModels(id);
|
|
419
418
|
const persistedSettings = state.providers[id]?.settings;
|
|
420
419
|
const providerName = info?.name ?? titleCaseFromId(id);
|
|
@@ -450,7 +449,7 @@ export async function getLocalProviderModels(
|
|
|
450
449
|
providerId: string,
|
|
451
450
|
): Promise<{ providerId: string; models: RpcProviderModel[] }> {
|
|
452
451
|
const id = providerId.trim();
|
|
453
|
-
const modelMap = await
|
|
452
|
+
const modelMap = await LlmsModels.getModelsForProvider(id);
|
|
454
453
|
const items = Object.entries(modelMap)
|
|
455
454
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
456
455
|
.map(([modelId, info]) => toRpcProviderModel(modelId, info));
|
|
@@ -1,9 +1,30 @@
|
|
|
1
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { describe, expect, it } from "vitest";
|
|
5
5
|
import { createHookConfigFileHooks } from "./hook-file-hooks";
|
|
6
6
|
|
|
7
|
+
async function waitForFile(
|
|
8
|
+
filePath: string,
|
|
9
|
+
timeoutMs = 1500,
|
|
10
|
+
): Promise<string> {
|
|
11
|
+
const started = Date.now();
|
|
12
|
+
for (;;) {
|
|
13
|
+
try {
|
|
14
|
+
return await readFile(filePath, "utf8");
|
|
15
|
+
} catch (error) {
|
|
16
|
+
const code =
|
|
17
|
+
error && typeof error === "object" && "code" in error
|
|
18
|
+
? String((error as { code?: unknown }).code)
|
|
19
|
+
: undefined;
|
|
20
|
+
if (code !== "ENOENT" || Date.now() - started >= timeoutMs) {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
7
28
|
async function createWorkspaceWithHook(
|
|
8
29
|
fileName: string,
|
|
9
30
|
body: string,
|
|
@@ -150,4 +171,33 @@ describe("createHookConfigFileHooks", () => {
|
|
|
150
171
|
await rm(workspace, { recursive: true, force: true });
|
|
151
172
|
}
|
|
152
173
|
});
|
|
174
|
+
|
|
175
|
+
it("maps TaskError hook files to agent_error stop events", async () => {
|
|
176
|
+
const outputPath = join(tmpdir(), `hooks-task-error-${Date.now()}.json`);
|
|
177
|
+
const { workspace } = await createWorkspaceWithHook(
|
|
178
|
+
"TaskError.js",
|
|
179
|
+
`let data='';process.stdin.on('data',c=>data+=c);process.stdin.on('end',()=>{require('node:fs').writeFileSync(${JSON.stringify(outputPath)}, data);});\n`,
|
|
180
|
+
);
|
|
181
|
+
try {
|
|
182
|
+
const hooks = createHookConfigFileHooks({
|
|
183
|
+
cwd: workspace,
|
|
184
|
+
workspacePath: workspace,
|
|
185
|
+
});
|
|
186
|
+
await hooks?.onStopError?.({
|
|
187
|
+
agentId: "agent_1",
|
|
188
|
+
conversationId: "conv_1",
|
|
189
|
+
parentAgentId: null,
|
|
190
|
+
iteration: 3,
|
|
191
|
+
error: new Error("401 unauthorized"),
|
|
192
|
+
});
|
|
193
|
+
const payload = JSON.parse(await waitForFile(outputPath)) as {
|
|
194
|
+
hookName: string;
|
|
195
|
+
error?: { message?: string };
|
|
196
|
+
};
|
|
197
|
+
expect(payload.hookName).toBe("agent_error");
|
|
198
|
+
expect(payload.error?.message).toBe("401 unauthorized");
|
|
199
|
+
} finally {
|
|
200
|
+
await rm(workspace, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
153
203
|
});
|