@clinebot/core 0.0.32 → 0.0.34
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/auth/client.d.ts +19 -0
- package/dist/auth/client.d.ts.map +1 -1
- package/dist/auth/cline.d.ts.map +1 -1
- package/dist/auth/oca.d.ts.map +1 -1
- package/dist/auth/server.d.ts +32 -0
- package/dist/auth/server.d.ts.map +1 -1
- package/dist/auth/types.d.ts +29 -0
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/extensions/context/agentic-compaction.d.ts.map +1 -1
- package/dist/extensions/context/basic-compaction.d.ts.map +1 -1
- package/dist/extensions/context/compaction-shared.d.ts +1 -1
- package/dist/extensions/context/compaction-shared.d.ts.map +1 -1
- package/dist/extensions/context/compaction.d.ts.map +1 -1
- package/dist/extensions/index.d.ts +2 -1
- package/dist/extensions/index.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts +2 -1
- package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-load-report.d.ts +19 -0
- package/dist/extensions/plugin/plugin-load-report.d.ts.map +1 -0
- package/dist/extensions/plugin/plugin-loader.d.ts +6 -0
- package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts +2 -1
- package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
- package/dist/extensions/plugin-sandbox-bootstrap.js +148 -148
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +227 -229
- package/dist/runtime/runtime-builder.d.ts +1 -1
- package/dist/runtime/runtime-builder.d.ts.map +1 -1
- package/dist/runtime/subprocess-sandbox.d.ts +2 -0
- package/dist/runtime/subprocess-sandbox.d.ts.map +1 -1
- package/dist/runtime/tool-approval.d.ts.map +1 -1
- package/dist/session/default-session-manager.d.ts.map +1 -1
- package/dist/session/persistence-service.d.ts.map +1 -1
- package/dist/session/session-agent-events.d.ts.map +1 -1
- package/dist/session/session-artifacts.d.ts +2 -0
- package/dist/session/session-artifacts.d.ts.map +1 -1
- package/dist/session/session-config-builder.d.ts.map +1 -1
- package/dist/team/team-tools.d.ts.map +1 -1
- package/dist/types/config.d.ts +1 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/events.d.ts +4 -0
- package/dist/types/events.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/auth/client.test.ts +29 -0
- package/src/auth/client.ts +21 -0
- package/src/auth/cline.ts +2 -0
- package/src/auth/oca.ts +2 -0
- package/src/auth/server.test.ts +287 -0
- package/src/auth/server.ts +50 -1
- package/src/auth/types.ts +29 -0
- package/src/extensions/context/agentic-compaction.ts +22 -10
- package/src/extensions/context/basic-compaction.ts +43 -18
- package/src/extensions/context/compaction-shared.ts +1 -1
- package/src/extensions/context/compaction.test.ts +16 -10
- package/src/extensions/context/compaction.ts +35 -12
- package/src/extensions/index.ts +6 -0
- package/src/extensions/plugin/plugin-config-loader.test.ts +37 -0
- package/src/extensions/plugin/plugin-config-loader.ts +18 -10
- package/src/extensions/plugin/plugin-load-report.ts +20 -0
- package/src/extensions/plugin/plugin-loader.test.ts +45 -0
- package/src/extensions/plugin/plugin-loader.ts +57 -3
- package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +158 -86
- package/src/extensions/plugin/plugin-sandbox.test.ts +70 -0
- package/src/extensions/plugin/plugin-sandbox.ts +17 -6
- package/src/index.ts +11 -0
- package/src/providers/local-provider-service.test.ts +4 -4
- package/src/runtime/hook-file-hooks.test.ts +42 -7
- package/src/runtime/runtime-builder.test.ts +98 -0
- package/src/runtime/runtime-builder.ts +112 -65
- package/src/runtime/subprocess-sandbox.ts +26 -23
- package/src/runtime/tool-approval.ts +13 -15
- package/src/session/default-session-manager.ts +1 -3
- package/src/session/persistence-service.test.ts +38 -0
- package/src/session/persistence-service.ts +16 -1
- package/src/session/session-agent-events.ts +9 -1
- package/src/session/session-artifacts.ts +16 -0
- package/src/session/session-config-builder.ts +46 -0
- package/src/team/team-tools.test.ts +104 -0
- package/src/team/team-tools.ts +35 -16
- package/src/types/config.ts +1 -0
- package/src/types/events.ts +4 -0
- package/dist/runtime/team-runtime-registry.d.ts +0 -13
- package/dist/runtime/team-runtime-registry.d.ts.map +0 -1
- package/src/runtime/team-runtime-registry.ts +0 -43
package/src/auth/server.ts
CHANGED
|
@@ -19,6 +19,17 @@ function createDeferred<T>(): Deferred<T> {
|
|
|
19
19
|
return { promise, resolve };
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
export interface OAuthServerListeningInfo {
|
|
23
|
+
host: string;
|
|
24
|
+
port: number;
|
|
25
|
+
callbackUrl: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OAuthServerCloseInfo {
|
|
29
|
+
host: string;
|
|
30
|
+
port: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
22
33
|
export interface LocalOAuthServerOptions {
|
|
23
34
|
host?: string;
|
|
24
35
|
ports: number[];
|
|
@@ -26,6 +37,29 @@ export interface LocalOAuthServerOptions {
|
|
|
26
37
|
timeoutMs?: number;
|
|
27
38
|
expectedState?: string;
|
|
28
39
|
successHtml?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Called when the local redirect server successfully binds to a port and is
|
|
42
|
+
* ready to receive the OAuth callback. Hosts can use this to display a
|
|
43
|
+
* "waiting for callback" status indicator or — in remote-development
|
|
44
|
+
* environments like JetBrains Gateway — to forward the port from the remote
|
|
45
|
+
* machine to the local machine where the user's browser is running.
|
|
46
|
+
*
|
|
47
|
+
* May be async; `startLocalOAuthServer` will **await** this callback before
|
|
48
|
+
* returning so that any setup it performs (e.g. port-forwarding) is
|
|
49
|
+
* guaranteed to complete before the caller opens the auth URL. Errors
|
|
50
|
+
* thrown by this callback are swallowed — they do not prevent the OAuth
|
|
51
|
+
* flow from proceeding.
|
|
52
|
+
*/
|
|
53
|
+
onListening?: (info: OAuthServerListeningInfo) => void | Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Called when the local redirect server closes, either because the OAuth
|
|
56
|
+
* callback was received, the flow was cancelled, or the timeout elapsed.
|
|
57
|
+
* Hosts should use this to tear down any port-forward set up in
|
|
58
|
+
* `onListening` and clear any "waiting for callback" status UI.
|
|
59
|
+
*
|
|
60
|
+
* May be async; fired after the underlying server socket is closed.
|
|
61
|
+
*/
|
|
62
|
+
onClose?: (info: OAuthServerCloseInfo) => void | Promise<void>;
|
|
29
63
|
}
|
|
30
64
|
|
|
31
65
|
export interface LocalOAuthServer {
|
|
@@ -48,6 +82,7 @@ export async function startLocalOAuthServer(
|
|
|
48
82
|
let settled = false;
|
|
49
83
|
let timeout: ReturnType<typeof setTimeout> | null = null;
|
|
50
84
|
let activeServer: import("node:http").Server | null = null;
|
|
85
|
+
let boundPort: number | null = null;
|
|
51
86
|
|
|
52
87
|
const settle = (value: OAuthCallbackPayload | null) => {
|
|
53
88
|
if (settled) return;
|
|
@@ -60,10 +95,17 @@ export async function startLocalOAuthServer(
|
|
|
60
95
|
clearTimeout(timeout);
|
|
61
96
|
timeout = null;
|
|
62
97
|
}
|
|
98
|
+
const closingPort = boundPort;
|
|
99
|
+
boundPort = null;
|
|
63
100
|
if (activeServer) {
|
|
64
101
|
activeServer.close();
|
|
65
102
|
activeServer = null;
|
|
66
103
|
}
|
|
104
|
+
if (closingPort !== null && options.onClose) {
|
|
105
|
+
void Promise.resolve(options.onClose({ host, port: closingPort })).catch(
|
|
106
|
+
() => {},
|
|
107
|
+
);
|
|
108
|
+
}
|
|
67
109
|
};
|
|
68
110
|
|
|
69
111
|
const waitForCallback = async () => {
|
|
@@ -149,8 +191,15 @@ export async function startLocalOAuthServer(
|
|
|
149
191
|
}
|
|
150
192
|
|
|
151
193
|
if (bindResult.bound) {
|
|
194
|
+
boundPort = port;
|
|
195
|
+
const callbackUrl = `http://${host}:${port}${options.callbackPath}`;
|
|
196
|
+
if (options.onListening) {
|
|
197
|
+
await Promise.resolve(
|
|
198
|
+
options.onListening({ host, port, callbackUrl }),
|
|
199
|
+
).catch(() => {});
|
|
200
|
+
}
|
|
152
201
|
return {
|
|
153
|
-
callbackUrl
|
|
202
|
+
callbackUrl,
|
|
154
203
|
waitForCallback,
|
|
155
204
|
cancelWait: () => {
|
|
156
205
|
close();
|
package/src/auth/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
import type { OAuthServerCloseInfo, OAuthServerListeningInfo } from "./server";
|
|
2
3
|
|
|
3
4
|
export interface OAuthPrompt {
|
|
4
5
|
message: string;
|
|
@@ -31,6 +32,34 @@ export interface OAuthLoginCallbacks {
|
|
|
31
32
|
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
|
32
33
|
onProgress?: (message: string) => void;
|
|
33
34
|
onManualCodeInput?: () => Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* Called when the local OAuth redirect server successfully binds to a port
|
|
37
|
+
* and is ready to receive the browser callback. The `info` object contains
|
|
38
|
+
* the host, the bound port number, and the full `callbackUrl`.
|
|
39
|
+
*
|
|
40
|
+
* Use this to:
|
|
41
|
+
* - Show a "waiting for OAuth callback on port N" status indicator in your UI.
|
|
42
|
+
* - Forward the port in remote-development environments (e.g. JetBrains
|
|
43
|
+
* Gateway) from the remote machine to the machine running the browser.
|
|
44
|
+
*
|
|
45
|
+
* Paired with `onServerClose` for teardown.
|
|
46
|
+
*
|
|
47
|
+
* Only fired when the provider uses a local callback server
|
|
48
|
+
* (`OAuthProviderInterface.usesCallbackServer === true`).
|
|
49
|
+
*/
|
|
50
|
+
onServerListening?: (info: OAuthServerListeningInfo) => void | Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Called when the local OAuth redirect server closes — either because the
|
|
53
|
+
* callback was received, the flow was cancelled, or the timeout elapsed.
|
|
54
|
+
*
|
|
55
|
+
* Use this to:
|
|
56
|
+
* - Clear any "waiting for callback" status UI shown in `onServerListening`.
|
|
57
|
+
* - Tear down port-forwards set up in `onServerListening`.
|
|
58
|
+
*
|
|
59
|
+
* Only fired when the provider uses a local callback server
|
|
60
|
+
* (`OAuthProviderInterface.usesCallbackServer === true`).
|
|
61
|
+
*/
|
|
62
|
+
onServerClose?: (info: OAuthServerCloseInfo) => void | Promise<void>;
|
|
34
63
|
}
|
|
35
64
|
|
|
36
65
|
export interface OAuthProviderInterface {
|
|
@@ -106,14 +106,26 @@ export async function runAgenticCompaction(options: {
|
|
|
106
106
|
(total, message) => total + options.estimateMessageTokens(message),
|
|
107
107
|
0,
|
|
108
108
|
);
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
const resultMessages = [
|
|
110
|
+
buildSummaryMessage({
|
|
111
|
+
summary,
|
|
112
|
+
fileOps,
|
|
113
|
+
tokensBefore,
|
|
114
|
+
}),
|
|
115
|
+
...messages.slice(cutIndex),
|
|
116
|
+
];
|
|
117
|
+
const tokensAfter = resultMessages.reduce(
|
|
118
|
+
(total, message) => total + options.estimateMessageTokens(message),
|
|
119
|
+
0,
|
|
120
|
+
);
|
|
121
|
+
options.logger?.debug("Performed agentic compaction", {
|
|
122
|
+
messagesBefore: messages.length,
|
|
123
|
+
messagesAfter: resultMessages.length,
|
|
124
|
+
messagesSummarized: cutIndex,
|
|
125
|
+
messagesPreserved: messages.length - cutIndex,
|
|
126
|
+
tokensBefore,
|
|
127
|
+
tokensAfter,
|
|
128
|
+
contextWindowTokens: options.context.contextWindowTokens,
|
|
129
|
+
});
|
|
130
|
+
return { messages: resultMessages };
|
|
119
131
|
}
|
|
@@ -33,17 +33,19 @@ function sanitizeMessageForBasic(
|
|
|
33
33
|
return text ? { ...message, content: text } : undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
}
|
|
36
|
+
// Preserve array structure: keep only text blocks with non-empty content.
|
|
37
|
+
const kept = message.content.filter(
|
|
38
|
+
(block) => block.type === "text" && block.text.trim(),
|
|
39
|
+
);
|
|
40
|
+
if (kept.length === 0) {
|
|
41
|
+
return undefined;
|
|
44
42
|
}
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
return {
|
|
44
|
+
...message,
|
|
45
|
+
content: kept.map((block) =>
|
|
46
|
+
block.type === "text" ? { ...block, text: block.text.trim() } : block,
|
|
47
|
+
),
|
|
48
|
+
};
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
function getTotalTokens(
|
|
@@ -60,14 +62,25 @@ function truncateMessageToTokens(
|
|
|
60
62
|
message: MessageWithMetadata,
|
|
61
63
|
maxTokens: number,
|
|
62
64
|
): MessageWithMetadata {
|
|
63
|
-
const content = typeof message.content === "string" ? message.content : "";
|
|
64
65
|
const safeMaxTokens = Math.max(1, maxTokens);
|
|
65
66
|
const targetChars = Math.max(16, safeMaxTokens * 4);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
content: truncated || "..."
|
|
70
|
-
}
|
|
67
|
+
|
|
68
|
+
if (typeof message.content === "string") {
|
|
69
|
+
const truncated = truncateText(message.content, targetChars).trim();
|
|
70
|
+
return { ...message, content: truncated || "..." };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Preserve content block array structure while truncating text blocks.
|
|
74
|
+
let remaining = targetChars;
|
|
75
|
+
const truncatedBlocks = message.content.map((block) => {
|
|
76
|
+
if (block.type !== "text" || remaining <= 0) {
|
|
77
|
+
return block;
|
|
78
|
+
}
|
|
79
|
+
const truncated = truncateText(block.text, remaining).trim();
|
|
80
|
+
remaining -= truncated.length;
|
|
81
|
+
return { ...block, text: truncated || "..." };
|
|
82
|
+
});
|
|
83
|
+
return { ...message, content: truncatedBlocks };
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
function buildBasicCandidates(
|
|
@@ -265,10 +278,22 @@ export function runBasicCompaction(options: {
|
|
|
265
278
|
return undefined;
|
|
266
279
|
}
|
|
267
280
|
|
|
281
|
+
const beforeTokens = getTotalTokens(
|
|
282
|
+
options.context.messages.map((m) => sanitizeMessageForBasic(m) ?? m),
|
|
283
|
+
options.estimateMessageTokens,
|
|
284
|
+
);
|
|
285
|
+
const afterTokens = getTotalTokens(
|
|
286
|
+
nextMessages,
|
|
287
|
+
options.estimateMessageTokens,
|
|
288
|
+
);
|
|
268
289
|
options.logger?.debug("Performed basic compaction", {
|
|
269
|
-
|
|
270
|
-
|
|
290
|
+
messagesBefore: options.context.messages.length,
|
|
291
|
+
messagesAfter: nextMessages.length,
|
|
292
|
+
messagesRemoved: options.context.messages.length - nextMessages.length,
|
|
293
|
+
tokensBefore: beforeTokens,
|
|
294
|
+
tokensAfter: afterTokens,
|
|
271
295
|
targetTokens,
|
|
296
|
+
contextWindowTokens: options.context.contextWindowTokens,
|
|
272
297
|
});
|
|
273
298
|
|
|
274
299
|
return { messages: nextMessages };
|
|
@@ -6,7 +6,7 @@ import type {
|
|
|
6
6
|
} from "../../types/config";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_CONTEXT_WINDOW_TOKENS = 200_000;
|
|
9
|
-
export const DEFAULT_THRESHOLD_RATIO = 0.
|
|
9
|
+
export const DEFAULT_THRESHOLD_RATIO = 0.95;
|
|
10
10
|
export const DEFAULT_RESERVE_TOKENS = 16_384;
|
|
11
11
|
export const DEFAULT_PRESERVE_RECENT_TOKENS = 20_000;
|
|
12
12
|
export const DEFAULT_SUMMARY_MAX_OUTPUT_TOKENS = 1_024;
|
|
@@ -363,16 +363,22 @@ describe("createContextCompactionPrepareTurn", () => {
|
|
|
363
363
|
);
|
|
364
364
|
expect(result?.messages).toBeDefined();
|
|
365
365
|
expect(result?.messages.length).toBeGreaterThan(0);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
366
|
+
// Compacted messages should not contain tool_result content that was pruned.
|
|
367
|
+
for (const message of result?.messages ?? []) {
|
|
368
|
+
if (typeof message.content === "string") {
|
|
369
|
+
expect(message.content).not.toContain(
|
|
370
|
+
"tool output that should be removed",
|
|
371
|
+
);
|
|
372
|
+
} else {
|
|
373
|
+
for (const block of message.content) {
|
|
374
|
+
if (block.type === "text") {
|
|
375
|
+
expect(block.text).not.toContain(
|
|
376
|
+
"tool output that should be removed",
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
376
382
|
});
|
|
377
383
|
|
|
378
384
|
it("defaults to threshold ratio when reserveTokens is not configured", async () => {
|
|
@@ -185,19 +185,42 @@ export function createContextCompactionPrepareTurn(
|
|
|
185
185
|
contextWindowTokens,
|
|
186
186
|
});
|
|
187
187
|
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
const beforeMessageCount = context.messages.length;
|
|
189
|
+
|
|
190
|
+
const result = userCompaction?.compact
|
|
191
|
+
? await userCompaction.compact(compactionContext)
|
|
192
|
+
: await runBuiltinStrategy({
|
|
193
|
+
context: compactionContext,
|
|
194
|
+
providerConfig: {
|
|
195
|
+
...providerConfig,
|
|
196
|
+
abortSignal: context.abortSignal,
|
|
197
|
+
},
|
|
198
|
+
compaction: userCompaction,
|
|
199
|
+
estimateMessageTokens,
|
|
200
|
+
logger: config.logger,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (result?.messages) {
|
|
204
|
+
const afterTokens = result.messages.reduce(
|
|
205
|
+
(total: number, message) => total + estimateMessageTokens(message),
|
|
206
|
+
0,
|
|
207
|
+
);
|
|
208
|
+
config.logger?.log("Context compaction completed", {
|
|
209
|
+
severity: "info",
|
|
210
|
+
strategy: strategy,
|
|
211
|
+
contextWindowTokens,
|
|
212
|
+
inputTokens,
|
|
213
|
+
afterTokens,
|
|
214
|
+
tokensSaved: inputTokens - afterTokens,
|
|
215
|
+
utilizationBefore: `${((inputTokens / contextWindowTokens) * 100).toFixed(1)}%`,
|
|
216
|
+
utilizationAfter: `${((afterTokens / contextWindowTokens) * 100).toFixed(1)}%`,
|
|
217
|
+
thresholdTrigger: `${(triggerState.thresholdRatio * 100).toFixed(1)}%`,
|
|
218
|
+
messagesBefore: beforeMessageCount,
|
|
219
|
+
messagesAfter: result.messages.length,
|
|
220
|
+
messagesRemoved: beforeMessageCount - result.messages.length,
|
|
221
|
+
} as Record<string, unknown>);
|
|
190
222
|
}
|
|
191
223
|
|
|
192
|
-
return
|
|
193
|
-
context: compactionContext,
|
|
194
|
-
providerConfig: {
|
|
195
|
-
...providerConfig,
|
|
196
|
-
abortSignal: context.abortSignal,
|
|
197
|
-
},
|
|
198
|
-
compaction: userCompaction,
|
|
199
|
-
estimateMessageTokens,
|
|
200
|
-
logger: config.logger,
|
|
201
|
-
});
|
|
224
|
+
return result;
|
|
202
225
|
};
|
|
203
226
|
}
|
package/src/extensions/index.ts
CHANGED
|
@@ -5,8 +5,14 @@ export {
|
|
|
5
5
|
resolveAndLoadAgentPlugins,
|
|
6
6
|
resolvePluginConfigSearchPaths,
|
|
7
7
|
} from "./plugin/plugin-config-loader";
|
|
8
|
+
export type {
|
|
9
|
+
PluginInitializationFailure,
|
|
10
|
+
PluginInitializationWarning,
|
|
11
|
+
PluginLoadDiagnostics,
|
|
12
|
+
} from "./plugin/plugin-load-report";
|
|
8
13
|
export type { LoadAgentPluginFromPathOptions } from "./plugin/plugin-loader";
|
|
9
14
|
export {
|
|
10
15
|
loadAgentPluginFromPath,
|
|
11
16
|
loadAgentPluginsFromPaths,
|
|
17
|
+
loadAgentPluginsFromPathsWithDiagnostics,
|
|
12
18
|
} from "./plugin/plugin-loader";
|
|
@@ -6,6 +6,7 @@ import { afterEach, describe, expect, it } from "vitest";
|
|
|
6
6
|
import {
|
|
7
7
|
discoverPluginModulePaths,
|
|
8
8
|
resolveAgentPluginPaths,
|
|
9
|
+
resolveAndLoadAgentPlugins,
|
|
9
10
|
resolvePluginConfigSearchPaths,
|
|
10
11
|
} from "./plugin-config-loader";
|
|
11
12
|
|
|
@@ -142,4 +143,40 @@ describe("plugin-config-loader", () => {
|
|
|
142
143
|
await rm(workspace, { recursive: true, force: true });
|
|
143
144
|
}
|
|
144
145
|
});
|
|
146
|
+
|
|
147
|
+
it("loads valid plugins while reporting failures and duplicate overrides", async () => {
|
|
148
|
+
const root = await mkdtemp(join(tmpdir(), "core-plugin-config-loader-"));
|
|
149
|
+
try {
|
|
150
|
+
const first = join(root, "duplicate-one.js");
|
|
151
|
+
const second = join(root, "duplicate-two.js");
|
|
152
|
+
const invalid = join(root, "invalid.js");
|
|
153
|
+
await writeFile(
|
|
154
|
+
first,
|
|
155
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['tools'] } };",
|
|
156
|
+
"utf8",
|
|
157
|
+
);
|
|
158
|
+
await writeFile(
|
|
159
|
+
second,
|
|
160
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['commands'] } };",
|
|
161
|
+
"utf8",
|
|
162
|
+
);
|
|
163
|
+
await writeFile(invalid, "export default { name: 'broken' };", "utf8");
|
|
164
|
+
|
|
165
|
+
const loaded = await resolveAndLoadAgentPlugins({
|
|
166
|
+
mode: "in_process",
|
|
167
|
+
pluginPaths: [first, invalid, second],
|
|
168
|
+
cwd: root,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
expect(loaded.extensions.map((plugin) => plugin.name)).toEqual([
|
|
172
|
+
"duplicate-plugin",
|
|
173
|
+
]);
|
|
174
|
+
expect(loaded.extensions[0]?.manifest.capabilities).toEqual(["commands"]);
|
|
175
|
+
expect(loaded.failures).toHaveLength(1);
|
|
176
|
+
expect(loaded.warnings).toHaveLength(1);
|
|
177
|
+
expect(loaded.warnings[0]?.overriddenPluginPath).toBe(first);
|
|
178
|
+
} finally {
|
|
179
|
+
await rm(root, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
145
182
|
});
|
|
@@ -5,7 +5,8 @@ import {
|
|
|
5
5
|
resolveConfiguredPluginModulePaths,
|
|
6
6
|
resolvePluginConfigSearchPaths as resolvePluginConfigSearchPathsFromShared,
|
|
7
7
|
} from "@clinebot/shared/storage";
|
|
8
|
-
import {
|
|
8
|
+
import type { PluginLoadDiagnostics } from "./plugin-load-report";
|
|
9
|
+
import { loadAgentPluginsFromPathsWithDiagnostics } from "./plugin-loader";
|
|
9
10
|
import { loadSandboxedPlugins } from "./plugin-sandbox";
|
|
10
11
|
|
|
11
12
|
type AgentPlugin = NonNullable<AgentConfig["extensions"]>[number];
|
|
@@ -64,21 +65,26 @@ export interface ResolveAndLoadAgentPluginsOptions
|
|
|
64
65
|
|
|
65
66
|
export async function resolveAndLoadAgentPlugins(
|
|
66
67
|
options: ResolveAndLoadAgentPluginsOptions = {},
|
|
67
|
-
): Promise<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
): Promise<
|
|
69
|
+
{
|
|
70
|
+
extensions: AgentPlugin[];
|
|
71
|
+
shutdown?: () => Promise<void>;
|
|
72
|
+
} & PluginLoadDiagnostics
|
|
73
|
+
> {
|
|
71
74
|
const paths = resolveAgentPluginPaths(options);
|
|
72
75
|
if (paths.length === 0) {
|
|
73
|
-
return { extensions: [] };
|
|
76
|
+
return { extensions: [], failures: [], warnings: [] };
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
if (options.mode === "in_process") {
|
|
80
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics(paths, {
|
|
81
|
+
cwd: options.cwd,
|
|
82
|
+
exportName: options.exportName,
|
|
83
|
+
});
|
|
77
84
|
return {
|
|
78
|
-
extensions:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}),
|
|
85
|
+
extensions: report.plugins,
|
|
86
|
+
failures: report.failures,
|
|
87
|
+
warnings: report.warnings,
|
|
82
88
|
};
|
|
83
89
|
}
|
|
84
90
|
|
|
@@ -93,5 +99,7 @@ export async function resolveAndLoadAgentPlugins(
|
|
|
93
99
|
return {
|
|
94
100
|
extensions: sandboxed.extensions ?? [],
|
|
95
101
|
shutdown: sandboxed.shutdown,
|
|
102
|
+
failures: sandboxed.failures,
|
|
103
|
+
warnings: sandboxed.warnings,
|
|
96
104
|
};
|
|
97
105
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface PluginInitializationFailure {
|
|
2
|
+
pluginPath: string;
|
|
3
|
+
pluginName?: string;
|
|
4
|
+
phase: "load" | "setup";
|
|
5
|
+
message: string;
|
|
6
|
+
stack?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PluginInitializationWarning {
|
|
10
|
+
type: "duplicate_plugin_override";
|
|
11
|
+
pluginPath: string;
|
|
12
|
+
pluginName: string;
|
|
13
|
+
overriddenPluginPath: string;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PluginLoadDiagnostics {
|
|
18
|
+
failures: PluginInitializationFailure[];
|
|
19
|
+
warnings: PluginInitializationWarning[];
|
|
20
|
+
}
|
|
@@ -5,6 +5,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
|
5
5
|
import {
|
|
6
6
|
loadAgentPluginFromPath,
|
|
7
7
|
loadAgentPluginsFromPaths,
|
|
8
|
+
loadAgentPluginsFromPathsWithDiagnostics,
|
|
8
9
|
} from "./plugin-loader";
|
|
9
10
|
|
|
10
11
|
describe("plugin-loader", () => {
|
|
@@ -158,6 +159,17 @@ describe("plugin-loader", () => {
|
|
|
158
159
|
"export default { name: 'invalid-plugin' };",
|
|
159
160
|
"utf8",
|
|
160
161
|
);
|
|
162
|
+
|
|
163
|
+
await writeFile(
|
|
164
|
+
join(dir, "duplicate-one.mjs"),
|
|
165
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['tools'] } };",
|
|
166
|
+
"utf8",
|
|
167
|
+
);
|
|
168
|
+
await writeFile(
|
|
169
|
+
join(dir, "duplicate-two.mjs"),
|
|
170
|
+
"export default { name: 'duplicate-plugin', manifest: { capabilities: ['commands'] } };",
|
|
171
|
+
"utf8",
|
|
172
|
+
);
|
|
161
173
|
});
|
|
162
174
|
|
|
163
175
|
afterAll(async () => {
|
|
@@ -244,4 +256,37 @@ describe("plugin-loader", () => {
|
|
|
244
256
|
loadAgentPluginFromPath(join(dir, "invalid-plugin.mjs")),
|
|
245
257
|
).rejects.toThrow(/missing required "manifest"/i);
|
|
246
258
|
});
|
|
259
|
+
|
|
260
|
+
it("continues loading valid plugins when one plugin fails", async () => {
|
|
261
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics([
|
|
262
|
+
join(dir, "plugin-a.mjs"),
|
|
263
|
+
join(dir, "invalid-plugin.mjs"),
|
|
264
|
+
join(dir, "plugin-b.mjs"),
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
expect(report.plugins.map((plugin) => plugin.name)).toEqual([
|
|
268
|
+
"plugin-a",
|
|
269
|
+
"plugin-b",
|
|
270
|
+
]);
|
|
271
|
+
expect(report.failures).toHaveLength(1);
|
|
272
|
+
expect(report.failures[0]?.pluginPath).toBe(
|
|
273
|
+
join(dir, "invalid-plugin.mjs"),
|
|
274
|
+
);
|
|
275
|
+
expect(report.warnings).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("keeps the later duplicate plugin and reports the override", async () => {
|
|
279
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics([
|
|
280
|
+
join(dir, "duplicate-one.mjs"),
|
|
281
|
+
join(dir, "duplicate-two.mjs"),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
expect(report.plugins).toHaveLength(1);
|
|
285
|
+
expect(report.plugins[0]?.name).toBe("duplicate-plugin");
|
|
286
|
+
expect(report.plugins[0]?.manifest.capabilities).toEqual(["commands"]);
|
|
287
|
+
expect(report.warnings).toHaveLength(1);
|
|
288
|
+
expect(report.warnings[0]?.overriddenPluginPath).toBe(
|
|
289
|
+
join(dir, "duplicate-one.mjs"),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
247
292
|
});
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import type { AgentConfig } from "@clinebot/shared";
|
|
3
|
+
import type {
|
|
4
|
+
PluginInitializationFailure,
|
|
5
|
+
PluginInitializationWarning,
|
|
6
|
+
} from "./plugin-load-report";
|
|
3
7
|
import { importPluginModule } from "./plugin-module-import";
|
|
4
8
|
|
|
5
9
|
type AgentPlugin = NonNullable<AgentConfig["extensions"]>[number];
|
|
@@ -98,9 +102,59 @@ export async function loadAgentPluginsFromPaths(
|
|
|
98
102
|
pluginPaths: string[],
|
|
99
103
|
options: LoadAgentPluginFromPathOptions = {},
|
|
100
104
|
): Promise<AgentPlugin[]> {
|
|
101
|
-
const
|
|
105
|
+
const report = await loadAgentPluginsFromPathsWithDiagnostics(
|
|
106
|
+
pluginPaths,
|
|
107
|
+
options,
|
|
108
|
+
);
|
|
109
|
+
return report.plugins;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function loadAgentPluginsFromPathsWithDiagnostics(
|
|
113
|
+
pluginPaths: string[],
|
|
114
|
+
options: LoadAgentPluginFromPathOptions = {},
|
|
115
|
+
): Promise<{
|
|
116
|
+
plugins: AgentPlugin[];
|
|
117
|
+
failures: PluginInitializationFailure[];
|
|
118
|
+
warnings: PluginInitializationWarning[];
|
|
119
|
+
}> {
|
|
120
|
+
const failures: PluginInitializationFailure[] = [];
|
|
121
|
+
const warnings: PluginInitializationWarning[] = [];
|
|
122
|
+
const loadedByName = new Map<
|
|
123
|
+
string,
|
|
124
|
+
{ plugin: AgentPlugin; pluginPath: string; order: number }
|
|
125
|
+
>();
|
|
126
|
+
let order = 0;
|
|
127
|
+
|
|
102
128
|
for (const pluginPath of pluginPaths) {
|
|
103
|
-
|
|
129
|
+
try {
|
|
130
|
+
const plugin = await loadAgentPluginFromPath(pluginPath, options);
|
|
131
|
+
const existing = loadedByName.get(plugin.name);
|
|
132
|
+
if (existing) {
|
|
133
|
+
warnings.push({
|
|
134
|
+
type: "duplicate_plugin_override",
|
|
135
|
+
pluginName: plugin.name,
|
|
136
|
+
pluginPath,
|
|
137
|
+
overriddenPluginPath: existing.pluginPath,
|
|
138
|
+
message: `Plugin "${plugin.name}" from ${pluginPath} overrides ${existing.pluginPath}`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
loadedByName.set(plugin.name, { plugin, pluginPath, order: order++ });
|
|
142
|
+
} catch (error) {
|
|
143
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
144
|
+
failures.push({
|
|
145
|
+
pluginPath,
|
|
146
|
+
phase: "load",
|
|
147
|
+
message,
|
|
148
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
104
151
|
}
|
|
105
|
-
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
plugins: [...loadedByName.values()]
|
|
155
|
+
.sort((left, right) => left.order - right.order)
|
|
156
|
+
.map((entry) => entry.plugin),
|
|
157
|
+
failures,
|
|
158
|
+
warnings,
|
|
159
|
+
};
|
|
106
160
|
}
|