@dot-ai/adapter-openclaw 0.5.2 → 0.7.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.
@@ -0,0 +1,233 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { DotAiRuntime, EventBus, ADAPTER_CAPABILITIES } from '@dot-ai/core';
3
+
4
+ // ── Mock OpenClaw Plugin API ──
5
+
6
+ interface CapturedHook {
7
+ event: string;
8
+ handler: (...args: unknown[]) => unknown;
9
+ options?: { priority?: number };
10
+ }
11
+
12
+ interface CapturedService {
13
+ id: string;
14
+ start: (ctx: { logger: { info: (msg: string) => void } }) => void;
15
+ stop: (ctx: { logger: { info: (msg: string) => void } }) => void;
16
+ }
17
+
18
+ interface CapturedTool {
19
+ factory?: (ctx: Record<string, unknown>) => unknown;
20
+ opts?: { name?: string; names?: string[] };
21
+ }
22
+
23
+ function createMockOpenClawApi() {
24
+ const logs: string[] = [];
25
+ const hooks: CapturedHook[] = [];
26
+ const services: CapturedService[] = [];
27
+ const tools: CapturedTool[] = [];
28
+
29
+ const api = {
30
+ logger: {
31
+ info: (msg: string) => logs.push(msg),
32
+ debug: (msg: string) => logs.push(`[debug] ${msg}`),
33
+ },
34
+ pluginConfig: undefined as Record<string, unknown> | undefined,
35
+ on(
36
+ event: string,
37
+ handler: (...args: unknown[]) => unknown,
38
+ options?: { priority?: number },
39
+ ) {
40
+ hooks.push({ event, handler, options });
41
+ },
42
+ registerService(service: CapturedService) {
43
+ services.push(service);
44
+ },
45
+ registerTool(factory: (ctx: Record<string, unknown>) => unknown, opts?: { name?: string; names?: string[] }) {
46
+ tools.push({ factory, opts });
47
+ },
48
+ };
49
+
50
+ return { api, logs, hooks, services, tools };
51
+ }
52
+
53
+ // ── Tests ──
54
+
55
+ describe('OpenClaw Plugin Integration', () => {
56
+ describe('plugin.register() structure', () => {
57
+ it('registers before_agent_start hook with priority 10', async () => {
58
+ const { api, hooks } = createMockOpenClawApi();
59
+
60
+ const { default: plugin } = await import('../index.js');
61
+ plugin.register(api as never);
62
+
63
+ const beforeStart = hooks.find(h => h.event === 'before_agent_start');
64
+ expect(beforeStart).toBeDefined();
65
+ expect(beforeStart!.options?.priority).toBe(10);
66
+ });
67
+
68
+ it('registers after_agent_end hook', async () => {
69
+ const { api, hooks } = createMockOpenClawApi();
70
+ const { default: plugin } = await import('../index.js');
71
+ plugin.register(api as never);
72
+
73
+ const afterEnd = hooks.find(h => h.event === 'after_agent_end');
74
+ expect(afterEnd).toBeDefined();
75
+ });
76
+
77
+ it('registers dot-ai service', async () => {
78
+ const { api, services } = createMockOpenClawApi();
79
+ const { default: plugin } = await import('../index.js');
80
+ plugin.register(api as never);
81
+
82
+ const svc = services.find(s => s.id === 'dot-ai');
83
+ expect(svc).toBeDefined();
84
+ });
85
+
86
+ it('registers tool factory for capabilities', async () => {
87
+ const { api, tools } = createMockOpenClawApi();
88
+ const { default: plugin } = await import('../index.js');
89
+ plugin.register(api as never);
90
+
91
+ expect(tools.length).toBe(1);
92
+ expect(tools[0].opts?.names).toContain('memory_recall');
93
+ expect(tools[0].opts?.names).toContain('memory_store');
94
+ expect(tools[0].opts?.names).toContain('task_list');
95
+ });
96
+
97
+ it('logs plugin version v6', async () => {
98
+ const { api, logs } = createMockOpenClawApi();
99
+ const { default: plugin } = await import('../index.js');
100
+ plugin.register(api as never);
101
+
102
+ expect(logs.some(l => l.includes('v6'))).toBe(true);
103
+ });
104
+
105
+ it('has correct plugin metadata', async () => {
106
+ const { default: plugin } = await import('../index.js');
107
+ expect(plugin.id).toBe('dot-ai');
108
+ expect(plugin.version).toBe('6.0.0');
109
+ expect(plugin.kind).toBe('memory');
110
+ });
111
+ });
112
+
113
+ describe('before_agent_start → boot → processPrompt', () => {
114
+ it('skips sub-agent sessions', async () => {
115
+ const { api, hooks, logs } = createMockOpenClawApi();
116
+ const { default: plugin } = await import('../index.js');
117
+ plugin.register(api as never);
118
+
119
+ const beforeStart = hooks.find(h => h.event === 'before_agent_start')!;
120
+ const result = await beforeStart.handler(
121
+ {},
122
+ { workspaceDir: '/tmp/test', sessionKey: 'session:subagent:1', prompt: 'hello' },
123
+ );
124
+
125
+ expect(result).toBeUndefined();
126
+ expect(logs.some(l => l.includes('Sub-agent'))).toBe(true);
127
+ });
128
+
129
+ it('skips cron sessions', async () => {
130
+ const { api, hooks, logs } = createMockOpenClawApi();
131
+ const { default: plugin } = await import('../index.js');
132
+ plugin.register(api as never);
133
+
134
+ const beforeStart = hooks.find(h => h.event === 'before_agent_start')!;
135
+ const result = await beforeStart.handler(
136
+ {},
137
+ { workspaceDir: '/tmp/test', sessionKey: 'session:cron:cleanup', prompt: 'cleanup' },
138
+ );
139
+
140
+ expect(result).toBeUndefined();
141
+ expect(logs.some(l => l.includes('Sub-agent') || l.includes('cron'))).toBe(true);
142
+ });
143
+
144
+ it('skips when no workspaceDir', async () => {
145
+ const { api, hooks, logs } = createMockOpenClawApi();
146
+ const { default: plugin } = await import('../index.js');
147
+ plugin.register(api as never);
148
+
149
+ const beforeStart = hooks.find(h => h.event === 'before_agent_start')!;
150
+ const result = await beforeStart.handler(
151
+ {},
152
+ { prompt: 'hello' },
153
+ );
154
+
155
+ expect(result).toBeUndefined();
156
+ expect(logs.some(l => l.includes('No workspaceDir'))).toBe(true);
157
+ });
158
+ });
159
+
160
+ describe('DotAiRuntime lifecycle (v6 extension-only)', () => {
161
+ it('boots and processes prompt', async () => {
162
+ const runtime = new DotAiRuntime({
163
+ workspaceRoot: '/tmp/nonexistent',
164
+ skipIdentities: true,
165
+ });
166
+ await runtime.boot();
167
+ expect(runtime.isBooted).toBe(true);
168
+
169
+ const { formatted } = await runtime.processPrompt('hello world');
170
+ expect(formatted).toBeDefined();
171
+ });
172
+
173
+ it('diagnostics show extension info', async () => {
174
+ const runtime = new DotAiRuntime({
175
+ workspaceRoot: '/tmp/nonexistent',
176
+ });
177
+ await runtime.boot();
178
+
179
+ const diag = runtime.diagnostics;
180
+ expect(diag.extensions).toEqual([]);
181
+ expect(diag.capabilityCount).toBe(0);
182
+ });
183
+
184
+ it('learn fires agent_end without throwing', async () => {
185
+ const runtime = new DotAiRuntime({
186
+ workspaceRoot: '/tmp/nonexistent',
187
+ });
188
+ await runtime.boot();
189
+ await runtime.learn('test response');
190
+ });
191
+ });
192
+
193
+ describe('OpenClaw adapter capability matrix', () => {
194
+ it('OpenClaw supports context_inject, agent_end, session_start', () => {
195
+ const supported = ADAPTER_CAPABILITIES['openclaw'];
196
+ expect(supported.has('context_inject')).toBe(true);
197
+ expect(supported.has('agent_end')).toBe(true);
198
+ expect(supported.has('session_start')).toBe(true);
199
+ });
200
+
201
+ it('OpenClaw does NOT support tool_call, tool_result, context_modify', () => {
202
+ const supported = ADAPTER_CAPABILITIES['openclaw'];
203
+ expect(supported.has('tool_call')).toBe(false);
204
+ expect(supported.has('tool_result')).toBe(false);
205
+ expect(supported.has('context_modify')).toBe(false);
206
+ expect(supported.has('turn_start')).toBe(false);
207
+ expect(supported.has('turn_end')).toBe(false);
208
+ });
209
+ });
210
+
211
+ describe('EventBus inter-extension communication', () => {
212
+ it('extensions can communicate via EventBus', async () => {
213
+ const eventBus = new EventBus();
214
+ const received: unknown[] = [];
215
+
216
+ eventBus.on('custom:auth-fix', (data: unknown) => {
217
+ received.push(data);
218
+ });
219
+
220
+ eventBus.emit('custom:auth-fix', { file: 'auth.ts', action: 'refactored' });
221
+
222
+ expect(received).toHaveLength(1);
223
+ expect(received[0]).toEqual({ file: 'auth.ts', action: 'refactored' });
224
+ });
225
+
226
+ it('EventBus errors dont propagate', () => {
227
+ const eventBus = new EventBus();
228
+ eventBus.on('test', () => { throw new Error('boom'); });
229
+
230
+ expect(() => eventBus.emit('test')).not.toThrow();
231
+ });
232
+ });
233
+ });
package/src/index.ts CHANGED
@@ -1,23 +1,11 @@
1
1
  /**
2
- * dot-ai OpenClaw plugin v4
3
- *
4
- * Hooks into before_agent_start to run the full dot-ai pipeline:
5
- * loadConfig → registerDefaults → createProviders → boot → enrich → formatContext
2
+ * dot-ai OpenClaw plugin v6
6
3
  *
4
+ * Hooks into before_agent_start to run the full dot-ai pipeline via DotAiRuntime.
5
+ * Uses the v6 extension-based pipeline — no providers required.
7
6
  * Returns enriched context as prependContext for the agent.
8
7
  */
9
- import {
10
- loadConfig,
11
- registerDefaults,
12
- registerProvider,
13
- createProviders,
14
- boot,
15
- enrich,
16
- injectRoot,
17
- formatContext,
18
- type Providers,
19
- type BootCache,
20
- } from '@dot-ai/core';
8
+ import { DotAiRuntime } from '@dot-ai/core';
21
9
 
22
10
  // Inline OpenClaw plugin API types
23
11
  interface OpenClawLogger {
@@ -41,67 +29,59 @@ interface OpenClawPluginApi {
41
29
  start(ctx: { logger: OpenClawLogger }): void;
42
30
  stop(ctx: { logger: OpenClawLogger }): void;
43
31
  }): void;
32
+ registerTool(tool: OpenClawTool | OpenClawToolFactory, opts?: { name?: string; names?: string[] }): void;
44
33
  }
45
34
 
46
- /**
47
- * Load custom providers declared in pluginConfig.
48
- * Workspaces declare custom providers via openclaw.json:
49
- * plugins.entries.dot-ai.config.customProviders: [
50
- * { type: "cockpit", module: "/abs/path/to/provider.ts" }
51
- * ]
52
- */
53
- async function loadCustomProviders(
54
- config: Record<string, unknown>,
55
- logger: OpenClawLogger,
56
- ): Promise<void> {
57
- const providers = config.customProviders;
58
- if (!Array.isArray(providers)) return;
59
-
60
- for (const entry of providers) {
61
- if (!entry || typeof entry !== 'object' || !('type' in entry) || !('module' in entry)) continue;
62
-
63
- const { type, module: modulePath } = entry as { type: string; module: string };
64
- try {
65
- const mod = await import(modulePath);
66
- // Find the exported provider class or factory
67
- for (const [name, exported] of Object.entries(mod)) {
68
- if (typeof exported === 'function') {
69
- if (name.endsWith('Provider')) {
70
- const ProviderClass = exported as new (opts: Record<string, unknown>) => unknown;
71
- registerProvider(`@custom/${type}`, (opts) => new ProviderClass(opts));
72
- logger.info(`[dot-ai] Registered custom provider: ${type} (${name})`);
73
- break;
74
- }
75
- }
76
- }
77
- } catch (err) {
78
- logger.info(`[dot-ai] Failed to load custom provider "${type}" from ${modulePath}: ${err}`);
79
- }
80
- }
35
+ interface OpenClawToolResult {
36
+ content: Array<{ type: string; text: string }>;
37
+ details?: Record<string, unknown>;
81
38
  }
82
39
 
40
+ interface OpenClawTool {
41
+ name: string;
42
+ label: string;
43
+ description: string;
44
+ parameters: Record<string, unknown>;
45
+ execute(toolCallId: string, params: Record<string, unknown>): Promise<OpenClawToolResult>;
46
+ }
47
+
48
+ type OpenClawToolFactory = (ctx: Record<string, unknown>) => OpenClawTool | OpenClawTool[] | null;
49
+
83
50
  // Session-level cache
84
- let cachedProviders: Providers | null = null;
85
- let cachedBoot: BootCache | null = null;
51
+ let cachedRuntime: DotAiRuntime | null = null;
86
52
  let cachedWorkspace: string | null = null;
87
53
 
88
54
  const plugin = {
89
55
  id: 'dot-ai',
90
56
  name: 'dot-ai — Universal AI Workspace Convention',
91
- version: '0.4.0',
57
+ version: '6.0.0',
92
58
  description: 'Deterministic context enrichment for OpenClaw agents',
59
+ kind: 'memory' as const,
93
60
 
94
61
  register(api: OpenClawPluginApi) {
95
- api.logger.info('[dot-ai] Plugin loaded (v4)');
96
-
97
- // Load custom providers if configured capture the promise so before_agent_start can await it
98
- let providerPromise: Promise<void> = Promise.resolve();
99
- if (api.pluginConfig) {
100
- providerPromise = loadCustomProviders(api.pluginConfig, api.logger);
101
- }
102
-
103
- // Register default file-based providers
104
- registerDefaults();
62
+ api.logger.info('[dot-ai] Plugin loaded (v6)');
63
+
64
+ // Register tools from core capabilities (delegates to extensions)
65
+ api.registerTool(
66
+ (_ctx: Record<string, unknown>) => {
67
+ if (!cachedRuntime?.isBooted) return null;
68
+ const capabilities = cachedRuntime.capabilities;
69
+ return capabilities.map((cap): OpenClawTool => ({
70
+ name: cap.name,
71
+ label: cap.name.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
72
+ description: cap.description,
73
+ parameters: cap.parameters,
74
+ async execute(_toolCallId: string, params: Record<string, unknown>): Promise<OpenClawToolResult> {
75
+ const result = await cap.execute(params);
76
+ return {
77
+ content: [{ type: 'text', text: result.text }],
78
+ ...(result.details && { details: result.details }),
79
+ };
80
+ },
81
+ }));
82
+ },
83
+ { names: ['memory_recall', 'memory_store', 'task_list', 'task_create', 'task_update'] },
84
+ );
105
85
 
106
86
  // Hook: before_agent_start — run the full pipeline
107
87
  api.on(
@@ -110,16 +90,12 @@ const plugin = {
110
90
  _event: unknown,
111
91
  ctx: { workspaceDir?: string; sessionKey?: string; prompt?: string },
112
92
  ) => {
113
- // Ensure custom providers are registered before proceeding
114
- await providerPromise;
115
-
116
93
  const workspaceDir = ctx.workspaceDir;
117
94
  if (!workspaceDir) {
118
95
  api.logger.info('[dot-ai] No workspaceDir, skipping');
119
96
  return;
120
97
  }
121
98
 
122
- // Skip sub-agent/cron sessions
123
99
  const isSubagent = ctx.sessionKey?.includes(':subagent:') || ctx.sessionKey?.includes(':cron:');
124
100
  if (isSubagent) {
125
101
  api.logger.debug?.('[dot-ai] Sub-agent/cron session, skipping');
@@ -127,33 +103,25 @@ const plugin = {
127
103
  }
128
104
 
129
105
  try {
130
- // Boot once per workspace (cache across prompts in same session)
131
- if (!cachedProviders || cachedWorkspace !== workspaceDir) {
132
- const rawConfig = await loadConfig(workspaceDir);
133
-
134
- // Inject workspaceDir into all provider options
135
- const config = injectRoot(rawConfig, workspaceDir);
136
- cachedProviders = await createProviders(config);
137
- cachedBoot = await boot(cachedProviders);
106
+ if (!cachedRuntime || cachedWorkspace !== workspaceDir) {
107
+ cachedRuntime = new DotAiRuntime({ workspaceRoot: workspaceDir });
108
+ await cachedRuntime.boot();
138
109
  cachedWorkspace = workspaceDir;
139
- api.logger.info(`[dot-ai] Booted: ${cachedBoot.identities.length} identities, ${cachedBoot.vocabulary.length} vocabulary, ${cachedBoot.skills.length} skills`);
140
- }
141
-
142
- // Enrich the prompt
143
- const prompt = ctx.prompt ?? '';
144
- const enriched = await enrich(prompt, cachedProviders, cachedBoot!);
110
+ api.logger.info('[dot-ai] Runtime booted (v6)');
145
111
 
146
- // Load skill content for matched skills
147
- for (const skill of enriched.skills) {
148
- if (!skill.content && skill.name) {
149
- skill.content = await cachedProviders.skills.load(skill.name) ?? undefined;
112
+ // Log extension diagnostics
113
+ const diag = cachedRuntime.diagnostics;
114
+ api.logger.info(`[dot-ai] extensions=${diag.extensions.length}, capabilities=${diag.capabilityCount}`);
115
+ if (diag.vocabularySize !== undefined) {
116
+ api.logger.info(`[dot-ai] Vocabulary size: ${diag.vocabularySize}`);
150
117
  }
151
118
  }
152
119
 
153
- // Format and inject
154
- const formatted = formatContext(enriched);
120
+ const prompt = ctx.prompt ?? '';
121
+ const { formatted, enriched } = await cachedRuntime.processPrompt(prompt);
122
+
155
123
  if (formatted) {
156
- api.logger.info(`[dot-ai] Injected: ${enriched.identities.length} identities, ${enriched.memories.length} memories, ${enriched.skills.length} skills`);
124
+ api.logger.info(`[dot-ai] Injected: ${enriched.identities.length} ids, ${enriched.memories.length} mems, ${enriched.skills.length} skills`);
157
125
  return { prependContext: formatted };
158
126
  }
159
127
  } catch (err) {
@@ -164,6 +132,15 @@ const plugin = {
164
132
  { priority: 10 },
165
133
  );
166
134
 
135
+ // Hook: after_agent_end — feed response back to runtime for learning + extension events
136
+ api.on('after_agent_end', async (_event, ctx) => {
137
+ if (!cachedRuntime) return;
138
+ const response = (ctx as { response?: string }).response ?? '';
139
+ if (response) {
140
+ await cachedRuntime.learn(response);
141
+ }
142
+ });
143
+
167
144
  // Service registration
168
145
  api.registerService({
169
146
  id: 'dot-ai',