@dot-ai/core 0.5.2 → 0.8.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/dist/boot-cache.d.ts +40 -0
- package/dist/boot-cache.d.ts.map +1 -0
- package/dist/boot-cache.js +72 -0
- package/dist/boot-cache.js.map +1 -0
- package/dist/capabilities.d.ts +35 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +17 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/config.d.ts +7 -23
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +131 -108
- package/dist/config.js.map +1 -1
- package/dist/extension-api.d.ts +65 -0
- package/dist/extension-api.d.ts.map +1 -0
- package/dist/extension-api.js +2 -0
- package/dist/extension-api.js.map +1 -0
- package/dist/extension-loader.d.ts +19 -0
- package/dist/extension-loader.d.ts.map +1 -0
- package/dist/extension-loader.js +113 -0
- package/dist/extension-loader.js.map +1 -0
- package/dist/extension-runner.d.ts +62 -0
- package/dist/extension-runner.d.ts.map +1 -0
- package/dist/extension-runner.js +260 -0
- package/dist/extension-runner.js.map +1 -0
- package/dist/extension-types.d.ts +312 -0
- package/dist/extension-types.d.ts.map +1 -0
- package/dist/extension-types.js +89 -0
- package/dist/extension-types.js.map +1 -0
- package/dist/format.d.ts +13 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +131 -15
- package/dist/format.js.map +1 -1
- package/dist/format.spec.d.ts +2 -0
- package/dist/format.spec.d.ts.map +1 -0
- package/dist/format.spec.js +140 -0
- package/dist/format.spec.js.map +1 -0
- package/dist/index.d.ts +21 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -14
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/package-manager.d.ts +30 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +91 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/runtime.d.ts +119 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +441 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +29 -10
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/__tests__/capabilities.test.ts +72 -0
- package/src/__tests__/config.test.ts +22 -120
- package/src/__tests__/extension-loader.test.ts +84 -0
- package/src/__tests__/extension-runner.test.ts +228 -0
- package/src/__tests__/fixtures/extensions/ctx-aware.js +26 -0
- package/src/__tests__/fixtures/extensions/security-gate.js +20 -0
- package/src/__tests__/fixtures/extensions/session-analytics.js +28 -0
- package/src/__tests__/fixtures/extensions/smart-context.js +10 -0
- package/src/__tests__/format.test.ts +207 -2
- package/src/__tests__/runtime.test.ts +141 -0
- package/src/boot-cache.ts +104 -0
- package/src/capabilities.ts +49 -0
- package/src/config.ts +131 -133
- package/src/extension-api.ts +99 -0
- package/src/extension-loader.ts +127 -0
- package/src/extension-runner.ts +297 -0
- package/src/extension-types.ts +416 -0
- package/src/format.spec.ts +175 -0
- package/src/format.test.ts +218 -0
- package/src/format.ts +140 -16
- package/src/index.ts +68 -30
- package/src/logger.ts +1 -1
- package/src/package-manager.ts +119 -0
- package/src/runtime.ts +562 -0
- package/src/types.ts +36 -14
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/.ai/memory/2026-03-04.md +0 -2
- package/.ai/tasks.json +0 -7
- package/dist/__tests__/config.test.d.ts +0 -2
- package/dist/__tests__/config.test.d.ts.map +0 -1
- package/dist/__tests__/config.test.js +0 -128
- package/dist/__tests__/config.test.js.map +0 -1
- package/dist/__tests__/e2e.test.d.ts +0 -2
- package/dist/__tests__/e2e.test.d.ts.map +0 -1
- package/dist/__tests__/e2e.test.js +0 -211
- package/dist/__tests__/e2e.test.js.map +0 -1
- package/dist/__tests__/engine.test.d.ts +0 -2
- package/dist/__tests__/engine.test.d.ts.map +0 -1
- package/dist/__tests__/engine.test.js +0 -271
- package/dist/__tests__/engine.test.js.map +0 -1
- package/dist/__tests__/format.test.d.ts +0 -2
- package/dist/__tests__/format.test.d.ts.map +0 -1
- package/dist/__tests__/format.test.js +0 -200
- package/dist/__tests__/format.test.js.map +0 -1
- package/dist/__tests__/labels.test.d.ts +0 -2
- package/dist/__tests__/labels.test.d.ts.map +0 -1
- package/dist/__tests__/labels.test.js +0 -82
- package/dist/__tests__/labels.test.js.map +0 -1
- package/dist/__tests__/loader.test.d.ts +0 -2
- package/dist/__tests__/loader.test.d.ts.map +0 -1
- package/dist/__tests__/loader.test.js +0 -161
- package/dist/__tests__/loader.test.js.map +0 -1
- package/dist/__tests__/logger.test.d.ts +0 -2
- package/dist/__tests__/logger.test.d.ts.map +0 -1
- package/dist/__tests__/logger.test.js +0 -95
- package/dist/__tests__/logger.test.js.map +0 -1
- package/dist/__tests__/nodes.test.d.ts +0 -2
- package/dist/__tests__/nodes.test.d.ts.map +0 -1
- package/dist/__tests__/nodes.test.js +0 -83
- package/dist/__tests__/nodes.test.js.map +0 -1
- package/dist/contracts.d.ts +0 -56
- package/dist/contracts.d.ts.map +0 -1
- package/dist/contracts.js +0 -2
- package/dist/contracts.js.map +0 -1
- package/dist/engine.d.ts +0 -38
- package/dist/engine.d.ts.map +0 -1
- package/dist/engine.js +0 -88
- package/dist/engine.js.map +0 -1
- package/dist/loader.d.ts +0 -26
- package/dist/loader.d.ts.map +0 -1
- package/dist/loader.js +0 -120
- package/dist/loader.js.map +0 -1
- package/src/__tests__/e2e.test.ts +0 -257
- package/src/__tests__/engine.test.ts +0 -305
- package/src/__tests__/loader.test.ts +0 -191
- package/src/contracts.ts +0 -71
- package/src/engine.ts +0 -145
- package/src/loader.ts +0 -152
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import type { Label, RoutingResult } from './types.js';
|
|
2
|
+
|
|
3
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
4
|
+
// Extension Context
|
|
5
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base context passed to every event handler (second argument).
|
|
9
|
+
* Adapters extend this with agent-specific state (providers, session control, etc.).
|
|
10
|
+
* Mirrors Pi's `ctx` pattern: handler(event, ctx).
|
|
11
|
+
*/
|
|
12
|
+
export interface ExtensionContext {
|
|
13
|
+
/** Current workspace root */
|
|
14
|
+
workspaceRoot: string;
|
|
15
|
+
/** Inter-extension event bus */
|
|
16
|
+
events: {
|
|
17
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
18
|
+
off(event: string, handler: (...args: unknown[]) => void): void;
|
|
19
|
+
emit(event: string, ...args: unknown[]): void;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
24
|
+
// v6 Unified Extension Types
|
|
25
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
// ── Section (core output unit for context_enrich) ────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A section is the atomic output unit of `context_enrich`.
|
|
31
|
+
* Extensions return sections; the formatter assembles them by priority.
|
|
32
|
+
*/
|
|
33
|
+
export interface Section {
|
|
34
|
+
/** Unique identifier for this section (optional — anonymous sections are allowed) */
|
|
35
|
+
id?: string;
|
|
36
|
+
/** Section heading */
|
|
37
|
+
title: string;
|
|
38
|
+
/** Markdown content */
|
|
39
|
+
content: string;
|
|
40
|
+
/**
|
|
41
|
+
* Priority determines ordering and trim precedence.
|
|
42
|
+
* 100 = identity, 80 = memory, 60 = skills, 50 = tasks, 40 = tools, 30 = routing.
|
|
43
|
+
*/
|
|
44
|
+
priority: number;
|
|
45
|
+
/** Which extension produced this section */
|
|
46
|
+
source: string;
|
|
47
|
+
/**
|
|
48
|
+
* How this section should be handled when the token budget is exceeded.
|
|
49
|
+
* - 'never': never trim (identity, critical context)
|
|
50
|
+
* - 'truncate': shorten content but keep section
|
|
51
|
+
* - 'drop': remove entirely (default)
|
|
52
|
+
*/
|
|
53
|
+
trimStrategy?: 'never' | 'truncate' | 'drop';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── resources_discover ───────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
/** A single resource entry discovered by an extension */
|
|
59
|
+
export interface ResourceEntry {
|
|
60
|
+
/** Resource type: 'skill', 'identity', 'tool', 'prompt', etc. */
|
|
61
|
+
type: string;
|
|
62
|
+
/** Path to the resource (absolute or relative to workspace) */
|
|
63
|
+
path: string;
|
|
64
|
+
/** Labels for this specific resource */
|
|
65
|
+
labels?: string[];
|
|
66
|
+
/** Provider-specific metadata */
|
|
67
|
+
metadata?: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Result returned by resources_discover handlers */
|
|
71
|
+
export interface ResourcesDiscoverResult {
|
|
72
|
+
/** Labels contributed to the global vocabulary */
|
|
73
|
+
labels?: string[];
|
|
74
|
+
/** Discovered resources (paths + metadata) */
|
|
75
|
+
resources?: ResourceEntry[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── label_extract (chain-transform) ──────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Event for label_extract -- handlers modify and return the labels array */
|
|
81
|
+
export interface LabelExtractEvent {
|
|
82
|
+
/** The user's prompt text */
|
|
83
|
+
prompt: string;
|
|
84
|
+
/** Known label vocabulary (from resources_discover) */
|
|
85
|
+
vocabulary: string[];
|
|
86
|
+
/** Current labels -- handler returns modified array */
|
|
87
|
+
labels: Label[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ── context_enrich ───────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/** Event for context_enrich -- replaces context_inject in v6 */
|
|
93
|
+
export interface ContextEnrichEvent {
|
|
94
|
+
/** The user's prompt text */
|
|
95
|
+
prompt: string;
|
|
96
|
+
/** Matched labels for this turn */
|
|
97
|
+
labels: Label[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Result from context_enrich handlers */
|
|
101
|
+
export interface ContextEnrichResult {
|
|
102
|
+
/** Sections to include in the formatted context */
|
|
103
|
+
sections?: Section[];
|
|
104
|
+
/** System prompt override (for Pi-like adapters that support it) */
|
|
105
|
+
systemPrompt?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Aggregated result from fireCollectSections */
|
|
109
|
+
export interface CollectedSections {
|
|
110
|
+
sections: Section[];
|
|
111
|
+
systemPrompt: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── route ────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/** Event for model routing */
|
|
117
|
+
export interface RouteEvent {
|
|
118
|
+
/** Matched labels for this turn */
|
|
119
|
+
labels: Label[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Result from route handlers — alias for RoutingResult from types.ts */
|
|
123
|
+
export type RouteResult = RoutingResult;
|
|
124
|
+
|
|
125
|
+
// ── input ────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/** Event for input interception (Pi adapter) */
|
|
128
|
+
export interface InputEvent {
|
|
129
|
+
/** Raw user input */
|
|
130
|
+
input: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Result from input handlers */
|
|
134
|
+
export interface InputResult {
|
|
135
|
+
/** Transformed input (if modified) */
|
|
136
|
+
input?: string;
|
|
137
|
+
/** If true, input was consumed and should not be forwarded */
|
|
138
|
+
consumed?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ── Commands ─────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
/** A parameter for a command definition */
|
|
144
|
+
export interface CommandParameter {
|
|
145
|
+
/** Parameter name */
|
|
146
|
+
name: string;
|
|
147
|
+
/** Human-readable description */
|
|
148
|
+
description: string;
|
|
149
|
+
/** Whether this parameter is required */
|
|
150
|
+
required?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Result returned by command execution */
|
|
154
|
+
export interface CommandResult {
|
|
155
|
+
/** Output text to display */
|
|
156
|
+
output?: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* A command definition -- aligned with Pi's /command pattern.
|
|
161
|
+
* Extensions register commands via `exports.commands`.
|
|
162
|
+
*/
|
|
163
|
+
export interface CommandDefinition {
|
|
164
|
+
/** Command name (without leading slash) */
|
|
165
|
+
name: string;
|
|
166
|
+
/** Human-readable description */
|
|
167
|
+
description: string;
|
|
168
|
+
/** Parameter definitions */
|
|
169
|
+
parameters?: CommandParameter[];
|
|
170
|
+
/** Execute the command */
|
|
171
|
+
execute(args: Record<string, string>, ctx: ExtensionContext): Promise<CommandResult | void>;
|
|
172
|
+
/** Tab-completion provider */
|
|
173
|
+
completions?(prefix: string): string[] | Promise<string[]>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Tool Definition (v6 -- with ctx) ─────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
/** Tool definition for extensions -- structurally compatible with Pi */
|
|
179
|
+
export interface ToolDefinition {
|
|
180
|
+
name: string;
|
|
181
|
+
description: string;
|
|
182
|
+
/** JSON Schema object describing tool parameters */
|
|
183
|
+
parameters: Record<string, unknown>;
|
|
184
|
+
/** Execute the tool. ctx is optional for backward compatibility with v5 extensions. */
|
|
185
|
+
execute(input: Record<string, unknown>, ctx?: ExtensionContext): Promise<{ content: string; details?: unknown; isError?: boolean }>;
|
|
186
|
+
/** Injected into system prompt when tool is active */
|
|
187
|
+
promptSnippet?: string;
|
|
188
|
+
/** Guidelines for the LLM when using this tool */
|
|
189
|
+
promptGuidelines?: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
193
|
+
// Legacy v5 Event Types (deprecated -- will be removed in v7)
|
|
194
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated Use ContextEnrichEvent + ContextEnrichResult instead.
|
|
198
|
+
* Tier 1 (universal) -- context injection event.
|
|
199
|
+
*/
|
|
200
|
+
export interface ContextInjectEvent {
|
|
201
|
+
prompt: string;
|
|
202
|
+
labels: Label[];
|
|
203
|
+
usage?: { inputTokens: number; contextWindow: number };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @deprecated Use ContextEnrichResult instead.
|
|
208
|
+
*/
|
|
209
|
+
export interface ContextInjectResult {
|
|
210
|
+
inject?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @deprecated Use ContextEnrichEvent with context_modify support.
|
|
215
|
+
* Tier 2 (rich agents only) -- message-level context modification.
|
|
216
|
+
*/
|
|
217
|
+
export interface ContextModifyEvent {
|
|
218
|
+
messages: Message[];
|
|
219
|
+
usage?: { inputTokens: number; contextWindow: number };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* @deprecated Use ContextEnrichResult instead.
|
|
224
|
+
*/
|
|
225
|
+
export interface ContextModifyResult {
|
|
226
|
+
messages?: Message[];
|
|
227
|
+
inject?: string; // fallback for adapters that don't support message modification
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Message type for context_modify */
|
|
231
|
+
export interface Message {
|
|
232
|
+
role: 'user' | 'assistant' | 'system';
|
|
233
|
+
content: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Tool call event -- fired before tool execution */
|
|
237
|
+
export interface ToolCallEvent {
|
|
238
|
+
tool: string;
|
|
239
|
+
input: Record<string, unknown>;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export interface ToolCallResult {
|
|
243
|
+
decision?: 'allow' | 'block';
|
|
244
|
+
reason?: string;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Tool result event -- fired after tool execution */
|
|
248
|
+
export interface ToolResultEvent {
|
|
249
|
+
tool: string;
|
|
250
|
+
result: { content: string };
|
|
251
|
+
isError: boolean;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Agent end event */
|
|
255
|
+
export interface AgentEndEvent {
|
|
256
|
+
response: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
260
|
+
// Event Union & Metadata
|
|
261
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
262
|
+
|
|
263
|
+
/** Union of all extension events (v5 legacy + v6) */
|
|
264
|
+
export type ExtensionEvent =
|
|
265
|
+
// v6 events
|
|
266
|
+
| { type: 'resources_discover' }
|
|
267
|
+
| { type: 'label_extract'; data: LabelExtractEvent }
|
|
268
|
+
| { type: 'context_enrich'; data: ContextEnrichEvent }
|
|
269
|
+
| { type: 'route'; data: RouteEvent }
|
|
270
|
+
| { type: 'input'; data: InputEvent }
|
|
271
|
+
// Shared events (both v5 and v6)
|
|
272
|
+
| { type: 'context_modify'; data: ContextModifyEvent }
|
|
273
|
+
| { type: 'tool_call'; data: ToolCallEvent }
|
|
274
|
+
| { type: 'tool_result'; data: ToolResultEvent }
|
|
275
|
+
| { type: 'agent_start' }
|
|
276
|
+
| { type: 'agent_end'; data: AgentEndEvent }
|
|
277
|
+
| { type: 'session_start' }
|
|
278
|
+
| { type: 'session_end' }
|
|
279
|
+
| { type: 'turn_start' }
|
|
280
|
+
| { type: 'turn_end' }
|
|
281
|
+
| { type: 'message_start' }
|
|
282
|
+
| { type: 'message_update' }
|
|
283
|
+
| { type: 'message_end' }
|
|
284
|
+
| { type: 'model_select' }
|
|
285
|
+
| { type: 'user_bash' }
|
|
286
|
+
| { type: 'session_before_switch' }
|
|
287
|
+
| { type: 'session_switch' }
|
|
288
|
+
| { type: 'session_before_compact' }
|
|
289
|
+
| { type: 'session_compact' }
|
|
290
|
+
// v5 legacy (deprecated)
|
|
291
|
+
| { type: 'context_inject'; data: ContextInjectEvent };
|
|
292
|
+
|
|
293
|
+
/** Extension tier metadata */
|
|
294
|
+
export type ExtensionTier = 'universal' | 'rich';
|
|
295
|
+
|
|
296
|
+
/** Event name to tier mapping */
|
|
297
|
+
export const EVENT_TIERS: Record<string, ExtensionTier> = {
|
|
298
|
+
// v6 events (all universal)
|
|
299
|
+
resources_discover: 'universal',
|
|
300
|
+
label_extract: 'universal',
|
|
301
|
+
context_enrich: 'universal',
|
|
302
|
+
route: 'universal',
|
|
303
|
+
input: 'universal',
|
|
304
|
+
// Shared events
|
|
305
|
+
context_modify: 'rich',
|
|
306
|
+
tool_call: 'universal',
|
|
307
|
+
tool_result: 'rich',
|
|
308
|
+
agent_start: 'rich',
|
|
309
|
+
agent_end: 'universal',
|
|
310
|
+
session_start: 'universal',
|
|
311
|
+
session_end: 'universal',
|
|
312
|
+
turn_start: 'rich',
|
|
313
|
+
turn_end: 'rich',
|
|
314
|
+
message_start: 'rich',
|
|
315
|
+
message_update: 'rich',
|
|
316
|
+
message_end: 'rich',
|
|
317
|
+
model_select: 'rich',
|
|
318
|
+
user_bash: 'rich',
|
|
319
|
+
session_before_switch: 'rich',
|
|
320
|
+
session_switch: 'rich',
|
|
321
|
+
session_before_compact: 'rich',
|
|
322
|
+
session_compact: 'rich',
|
|
323
|
+
// v5 legacy (deprecated)
|
|
324
|
+
context_inject: 'universal',
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/** All valid event names */
|
|
328
|
+
export type ExtensionEventName = keyof typeof EVENT_TIERS;
|
|
329
|
+
|
|
330
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
331
|
+
// Loaded Extension & Adapter Capabilities
|
|
332
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
333
|
+
|
|
334
|
+
/** A loaded extension with its handlers, tools, and commands */
|
|
335
|
+
export interface LoadedExtension {
|
|
336
|
+
path: string;
|
|
337
|
+
handlers: Map<string, Function[]>;
|
|
338
|
+
tools: Map<string, ToolDefinition>;
|
|
339
|
+
/** Registered commands (v6) */
|
|
340
|
+
commands: Map<string, CommandDefinition>;
|
|
341
|
+
tiers: Set<ExtensionTier>;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Adapter capability matrix -- which events each adapter supports.
|
|
346
|
+
* v6 events listed first; legacy events marked with comments.
|
|
347
|
+
*/
|
|
348
|
+
export const ADAPTER_CAPABILITIES: Record<string, Set<string>> = {
|
|
349
|
+
pi: new Set([
|
|
350
|
+
'session_start', 'session_end',
|
|
351
|
+
'resources_discover',
|
|
352
|
+
'label_extract', 'context_enrich', 'context_modify', 'route',
|
|
353
|
+
'input',
|
|
354
|
+
'tool_call', 'tool_result',
|
|
355
|
+
'agent_start', 'agent_end',
|
|
356
|
+
'turn_start', 'turn_end',
|
|
357
|
+
'message_start', 'message_update', 'message_end',
|
|
358
|
+
'session_before_switch', 'session_switch',
|
|
359
|
+
'session_before_compact', 'session_compact',
|
|
360
|
+
'model_select', 'user_bash',
|
|
361
|
+
// Legacy (deprecated)
|
|
362
|
+
'context_inject',
|
|
363
|
+
]),
|
|
364
|
+
'claude-code': new Set([
|
|
365
|
+
'session_start', 'session_end',
|
|
366
|
+
'resources_discover',
|
|
367
|
+
'label_extract', 'context_enrich', 'route',
|
|
368
|
+
'tool_call', 'tool_result',
|
|
369
|
+
'agent_end',
|
|
370
|
+
// Legacy (deprecated)
|
|
371
|
+
'context_inject',
|
|
372
|
+
]),
|
|
373
|
+
openclaw: new Set([
|
|
374
|
+
'session_start', 'session_end',
|
|
375
|
+
'resources_discover',
|
|
376
|
+
'label_extract', 'context_enrich', 'route',
|
|
377
|
+
'agent_end',
|
|
378
|
+
// Legacy (deprecated)
|
|
379
|
+
'context_inject',
|
|
380
|
+
]),
|
|
381
|
+
sync: new Set([
|
|
382
|
+
'resources_discover',
|
|
383
|
+
'context_enrich', 'route',
|
|
384
|
+
// Legacy (deprecated)
|
|
385
|
+
'context_inject',
|
|
386
|
+
]),
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Tool integration strategy per adapter.
|
|
391
|
+
* - 'native': adapter can register tools natively (Pi, OpenClaw)
|
|
392
|
+
* - 'cli': tools are exposed as CLI commands (Claude Code)
|
|
393
|
+
* - 'text': tools are described in the system prompt only (Cursor, Copilot)
|
|
394
|
+
*/
|
|
395
|
+
export const TOOL_STRATEGY: Record<string, 'native' | 'cli' | 'text'> = {
|
|
396
|
+
pi: 'native',
|
|
397
|
+
openclaw: 'native',
|
|
398
|
+
'claude-code': 'cli',
|
|
399
|
+
sync: 'text',
|
|
400
|
+
cursor: 'text',
|
|
401
|
+
copilot: 'text',
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
405
|
+
// Diagnostics
|
|
406
|
+
// ══════════════════════════════════════════════════════════════════════════════
|
|
407
|
+
|
|
408
|
+
/** Extension diagnostics */
|
|
409
|
+
export interface ExtensionDiagnostic {
|
|
410
|
+
path: string;
|
|
411
|
+
handlerCounts: Record<string, number>;
|
|
412
|
+
toolNames: string[];
|
|
413
|
+
/** Command names registered by this extension */
|
|
414
|
+
commandNames: string[];
|
|
415
|
+
tiers: ExtensionTier[];
|
|
416
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { formatContext } from './format.js';
|
|
3
|
+
import type { EnrichedContext } from './types.js';
|
|
4
|
+
|
|
5
|
+
function makeContext(overrides: Partial<EnrichedContext> = {}): EnrichedContext {
|
|
6
|
+
return {
|
|
7
|
+
prompt: 'test',
|
|
8
|
+
labels: [],
|
|
9
|
+
identities: [],
|
|
10
|
+
memories: [],
|
|
11
|
+
skills: [],
|
|
12
|
+
tools: [],
|
|
13
|
+
routing: { model: 'default', reason: 'test' },
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('formatContext — memoryDescription', () => {
|
|
19
|
+
it('includes blockquote description before memory entries when both memories and memoryDescription are present', () => {
|
|
20
|
+
const ctx = makeContext({
|
|
21
|
+
memoryDescription: 'SQLite memory (FTS5). 42 entries.',
|
|
22
|
+
memories: [
|
|
23
|
+
{ content: 'User prefers TypeScript', type: 'fact', source: 'sqlite-memory' },
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const result = formatContext(ctx);
|
|
28
|
+
|
|
29
|
+
expect(result).toContain('> SQLite memory (FTS5). 42 entries.');
|
|
30
|
+
expect(result).toContain('User prefers TypeScript');
|
|
31
|
+
// description comes before entries
|
|
32
|
+
expect(result.indexOf('> SQLite memory')).toBeLessThan(result.indexOf('User prefers TypeScript'));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not include blockquote when memories are present but memoryDescription is absent', () => {
|
|
36
|
+
const ctx = makeContext({
|
|
37
|
+
memories: [
|
|
38
|
+
{ content: 'User prefers TypeScript', type: 'fact', source: 'file-memory' },
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const result = formatContext(ctx);
|
|
43
|
+
|
|
44
|
+
expect(result).not.toContain('> ');
|
|
45
|
+
expect(result).toContain('User prefers TypeScript');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('renders memory section with description even when memories array is empty', () => {
|
|
49
|
+
const ctx = makeContext({
|
|
50
|
+
memoryDescription: 'SQLite memory (FTS5). 0 entries.',
|
|
51
|
+
memories: [],
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = formatContext(ctx);
|
|
55
|
+
|
|
56
|
+
expect(result).toContain('## Relevant Memory');
|
|
57
|
+
expect(result).toContain('> SQLite memory (FTS5). 0 entries.');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('renders no memory section when both memories and memoryDescription are absent', () => {
|
|
61
|
+
const ctx = makeContext({
|
|
62
|
+
memories: [],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = formatContext(ctx);
|
|
66
|
+
|
|
67
|
+
expect(result).not.toContain('## Relevant Memory');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('limits memory entries to 10 entries even when more are provided', () => {
|
|
71
|
+
const memories = Array.from({ length: 15 }, (_, i) => ({
|
|
72
|
+
content: `Memory entry ${i}`,
|
|
73
|
+
type: 'log' as const,
|
|
74
|
+
source: 'sqlite-memory',
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const ctx = makeContext({ memories });
|
|
78
|
+
|
|
79
|
+
const result = formatContext(ctx);
|
|
80
|
+
|
|
81
|
+
// entries 0–9 should be present, 10–14 should not
|
|
82
|
+
expect(result).toContain('Memory entry 0');
|
|
83
|
+
expect(result).toContain('Memory entry 9');
|
|
84
|
+
expect(result).not.toContain('Memory entry 10');
|
|
85
|
+
expect(result).not.toContain('Memory entry 14');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('includes date in memory entry when present', () => {
|
|
89
|
+
const ctx = makeContext({
|
|
90
|
+
memories: [
|
|
91
|
+
{ content: 'Important decision made', type: 'decision', source: 'sqlite-memory', date: '2026-03-04' },
|
|
92
|
+
],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const result = formatContext(ctx);
|
|
96
|
+
|
|
97
|
+
expect(result).toContain('(2026-03-04)');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('description is rendered as a blockquote line (> prefix)', () => {
|
|
101
|
+
const ctx = makeContext({
|
|
102
|
+
memoryDescription: 'File-based memory. Directories: root:memory/.',
|
|
103
|
+
memories: [],
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = formatContext(ctx);
|
|
107
|
+
|
|
108
|
+
const lines = result.split('\n');
|
|
109
|
+
const descLine = lines.find(l => l.includes('File-based memory'));
|
|
110
|
+
expect(descLine).toBeDefined();
|
|
111
|
+
expect(descLine!.trim()).toMatch(/^>/);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('formatContext — recentTasks', () => {
|
|
116
|
+
it('renders tasks section when recentTasks is provided', () => {
|
|
117
|
+
const ctx = makeContext({
|
|
118
|
+
recentTasks: [
|
|
119
|
+
{ id: '1', text: 'Fix login bug', status: 'in_progress', project: 'cockpit' },
|
|
120
|
+
{ id: '2', text: 'Add dark mode', status: 'in_progress', priority: 'high' },
|
|
121
|
+
],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const result = formatContext(ctx);
|
|
125
|
+
|
|
126
|
+
expect(result).toContain('## Current Tasks (In Progress)');
|
|
127
|
+
expect(result).toContain('Fix login bug [cockpit]');
|
|
128
|
+
expect(result).toContain('Add dark mode (high)');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not render tasks section when recentTasks is undefined', () => {
|
|
132
|
+
const ctx = makeContext({});
|
|
133
|
+
const result = formatContext(ctx);
|
|
134
|
+
expect(result).not.toContain('Current Tasks');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('does not render tasks section when recentTasks is empty', () => {
|
|
138
|
+
const ctx = makeContext({ recentTasks: [] });
|
|
139
|
+
const result = formatContext(ctx);
|
|
140
|
+
expect(result).not.toContain('Current Tasks');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('limits tasks to 10 entries', () => {
|
|
144
|
+
const tasks = Array.from({ length: 15 }, (_, i) => ({
|
|
145
|
+
id: String(i),
|
|
146
|
+
text: `Task ${i}`,
|
|
147
|
+
status: 'in_progress',
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
const ctx = makeContext({ recentTasks: tasks });
|
|
151
|
+
const result = formatContext(ctx);
|
|
152
|
+
|
|
153
|
+
expect(result).toContain('Task 0');
|
|
154
|
+
expect(result).toContain('Task 9');
|
|
155
|
+
expect(result).not.toContain('Task 10');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('renders tasks between memory and skills sections', () => {
|
|
159
|
+
const ctx = makeContext({
|
|
160
|
+
memoryDescription: 'SQLite memory',
|
|
161
|
+
memories: [{ content: 'test memory', type: 'log', source: 'sqlite' }],
|
|
162
|
+
recentTasks: [{ id: '1', text: 'A task', status: 'in_progress' }],
|
|
163
|
+
skills: [{ name: 'test-skill', description: 'A skill', labels: [], content: '# Skill' }],
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const result = formatContext(ctx);
|
|
167
|
+
|
|
168
|
+
const memoryIdx = result.indexOf('## Relevant Memory');
|
|
169
|
+
const tasksIdx = result.indexOf('## Current Tasks');
|
|
170
|
+
const skillsIdx = result.indexOf('## Active Skills');
|
|
171
|
+
|
|
172
|
+
expect(memoryIdx).toBeLessThan(tasksIdx);
|
|
173
|
+
expect(tasksIdx).toBeLessThan(skillsIdx);
|
|
174
|
+
});
|
|
175
|
+
});
|