@intent-systems/nexus 2026.1.5-3 → 2026.1.5-4

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.
Files changed (39) hide show
  1. package/dist/capabilities/detector.js +214 -0
  2. package/dist/capabilities/registry.js +98 -0
  3. package/dist/channels/location.js +44 -0
  4. package/dist/channels/web/index.js +2 -0
  5. package/dist/control-plane/broker/broker.js +969 -0
  6. package/dist/control-plane/compaction.js +284 -0
  7. package/dist/control-plane/factory.js +31 -0
  8. package/dist/control-plane/index.js +10 -0
  9. package/dist/control-plane/odu/agents.js +187 -0
  10. package/dist/control-plane/odu/interaction-tools.js +196 -0
  11. package/dist/control-plane/odu/prompt-loader.js +95 -0
  12. package/dist/control-plane/odu/runtime.js +467 -0
  13. package/dist/control-plane/odu/types.js +6 -0
  14. package/dist/control-plane/odu-control-plane.js +314 -0
  15. package/dist/control-plane/single-agent.js +249 -0
  16. package/dist/control-plane/types.js +11 -0
  17. package/dist/credentials/store.js +323 -0
  18. package/dist/logging/redact.js +109 -0
  19. package/dist/markdown/fences.js +58 -0
  20. package/dist/memory/embeddings.js +146 -0
  21. package/dist/memory/index.js +382 -0
  22. package/dist/memory/internal.js +163 -0
  23. package/dist/pairing/pairing-store.js +194 -0
  24. package/dist/plugins/cli.js +42 -0
  25. package/dist/plugins/discovery.js +253 -0
  26. package/dist/plugins/install.js +181 -0
  27. package/dist/plugins/loader.js +290 -0
  28. package/dist/plugins/registry.js +105 -0
  29. package/dist/plugins/status.js +29 -0
  30. package/dist/plugins/tools.js +39 -0
  31. package/dist/plugins/types.js +1 -0
  32. package/dist/routing/resolve-route.js +144 -0
  33. package/dist/routing/session-key.js +63 -0
  34. package/dist/utils/provider-utils.js +28 -0
  35. package/package.json +4 -29
  36. package/patches/@mariozechner__pi-ai.patch +215 -0
  37. package/patches/playwright-core@1.57.0.patch +13 -0
  38. package/patches/qrcode-terminal.patch +12 -0
  39. package/scripts/postinstall.js +202 -0
@@ -0,0 +1,314 @@
1
+ /**
2
+ * ODUControlPlane - AgentControlPlane implementation using broker + ODU runtime.
3
+ *
4
+ * This implementation uses the ActiveMessageBroker to route messages between
5
+ * an Interaction Agent (IA) singleton and dynamically created Execution Agents (EAs).
6
+ *
7
+ * Architecture:
8
+ * - User messages → IA (singleton, always-on)
9
+ * - IA delegates tasks → EAs (created on-demand)
10
+ * - EAs complete and respond → IA
11
+ * - IA responds → User
12
+ */
13
+ import { loadSession, writeSessionMetadata, listAgentSessions, resolveSessionDir, } from "../config/sessions.js";
14
+ import { normalizeAgentId, parseAgentSessionKey, DEFAULT_AGENT_ID, } from "../routing/session-key.js";
15
+ import { ActiveMessageBroker } from "./broker/broker.js";
16
+ import { ODUInteractionAgent } from "./odu/runtime.js";
17
+ import { loadConfig } from "../config/config.js";
18
+ import { createSubsystemLogger } from "../logging.js";
19
+ import crypto from "node:crypto";
20
+ import fs from "node:fs/promises";
21
+ /**
22
+ * ODUControlPlane implements AgentControlPlane using the broker pattern.
23
+ *
24
+ * Key differences from SingleAgentControlPlane:
25
+ * - Creates a singleton IA per user/ODU
26
+ * - IA receives all messages and delegates to EAs via broker
27
+ * - Session continuity enables learning across tasks
28
+ */
29
+ export class ODUControlPlane {
30
+ config;
31
+ workspaceDir;
32
+ broker;
33
+ logger;
34
+ oduConfig;
35
+ // Track created IAs (userId -> IA instance)
36
+ interactionAgents = new Map();
37
+ constructor(options) {
38
+ this.config = options?.config ?? loadConfig();
39
+ this.workspaceDir = options?.workspaceDir ?? process.cwd();
40
+ this.logger = createSubsystemLogger("odu-control-plane");
41
+ // Create broker instance
42
+ this.broker = new ActiveMessageBroker(this.config);
43
+ // Default ODU config (can be overridden via options)
44
+ this.oduConfig = options?.oduConfig ?? {
45
+ name: "nexus",
46
+ purpose: "AI operating system - universal agent orchestration",
47
+ };
48
+ this.logger.info("ODUControlPlane initialized", {
49
+ oduName: this.oduConfig.name,
50
+ workspaceDir: this.workspaceDir,
51
+ });
52
+ }
53
+ /**
54
+ * Get or create the IA for a user/session
55
+ */
56
+ getOrCreateIA(userId, sessionId) {
57
+ let ia = this.interactionAgents.get(userId);
58
+ if (!ia) {
59
+ this.logger.info("Creating new IA for user", { userId, sessionId });
60
+ ia = new ODUInteractionAgent({
61
+ userId,
62
+ sessionId,
63
+ oduPath: this.workspaceDir,
64
+ config: this.config,
65
+ oduConfig: this.oduConfig,
66
+ broker: this.broker,
67
+ });
68
+ this.interactionAgents.set(userId, ia);
69
+ }
70
+ return ia;
71
+ }
72
+ /**
73
+ * Convert session key to userId
74
+ * For ODU mode, we use the session key as the userId (e.g., "whatsapp:+15551234567")
75
+ */
76
+ sessionKeyToUserId(sessionKey) {
77
+ // In ODU mode, each session key represents a unique user/conversation
78
+ return sessionKey;
79
+ }
80
+ async sendMessage(options) {
81
+ const { sessionKey, message, agentId: optAgentId, thinkingLevel, verboseLevel, isHeartbeat, streaming, onChunk, onComplete, onError, } = options;
82
+ try {
83
+ // Parse session key to get agentId
84
+ const parsed = parseAgentSessionKey(sessionKey);
85
+ const agentId = normalizeAgentId(optAgentId ?? parsed?.agentId ?? DEFAULT_AGENT_ID);
86
+ // Convert session key to userId for ODU
87
+ const userId = this.sessionKeyToUserId(sessionKey);
88
+ // Load or create session
89
+ let session = await loadSession(agentId, sessionKey);
90
+ let sessionId;
91
+ if (!session) {
92
+ // Create new session
93
+ sessionId = crypto.randomUUID();
94
+ const sessionMetadata = {
95
+ sessionId,
96
+ agentId,
97
+ created: new Date().toISOString(),
98
+ chatType: "direct",
99
+ thinkingLevel: thinkingLevel ?? this.config.agent?.thinkingDefault,
100
+ verboseLevel: verboseLevel ?? this.config.agent?.verboseDefault,
101
+ compactionCount: 0,
102
+ };
103
+ await writeSessionMetadata(agentId, sessionKey, sessionMetadata);
104
+ }
105
+ else {
106
+ sessionId = session.metadata.sessionId;
107
+ // Update session metadata (writeSessionMetadata handles updated timestamp)
108
+ await writeSessionMetadata(agentId, sessionKey, session.metadata);
109
+ }
110
+ // Get or create IA for this user
111
+ const ia = this.getOrCreateIA(userId, sessionId);
112
+ // Build broker message
113
+ const brokerMessage = {
114
+ id: crypto.randomUUID(),
115
+ from: "user",
116
+ to: `${this.oduConfig.name}-ia`,
117
+ content: message,
118
+ priority: isHeartbeat ? "low" : "normal",
119
+ timestamp: Date.now(),
120
+ conversationId: sessionId,
121
+ metadata: {
122
+ source: "user",
123
+ sessionKey,
124
+ thinkingLevel,
125
+ verboseLevel,
126
+ streaming,
127
+ },
128
+ };
129
+ // Send message via broker
130
+ const startTime = Date.now();
131
+ // For now, we send and wait for acknowledgment
132
+ // Future: implement streaming support
133
+ this.logger.debug("Sending message to IA via broker", {
134
+ sessionKey,
135
+ userId,
136
+ messagePreview: message.substring(0, 100),
137
+ });
138
+ let responseText = "";
139
+ try {
140
+ // Send message via broker and wait for ack
141
+ responseText = await this.broker.sendAndWaitForAck(brokerMessage);
142
+ }
143
+ catch (error) {
144
+ this.logger.error("Broker send failed", { error, sessionKey });
145
+ throw error;
146
+ }
147
+ const durationMs = Date.now() - startTime;
148
+ // Update session metadata with latest timestamp
149
+ const updatedSession = await loadSession(agentId, sessionKey);
150
+ if (updatedSession) {
151
+ await writeSessionMetadata(agentId, sessionKey, updatedSession.metadata);
152
+ }
153
+ // Call completion callback if provided
154
+ if (onComplete) {
155
+ onComplete();
156
+ }
157
+ // Return result
158
+ // Note: config.agent.model can be a string or AgentModelListConfig object
159
+ // For simplicity, we just use "odu" provider and don't try to extract the model string
160
+ return {
161
+ success: true,
162
+ sessionId,
163
+ payloads: [
164
+ {
165
+ text: responseText,
166
+ },
167
+ ],
168
+ meta: {
169
+ durationMs,
170
+ provider: "odu",
171
+ model: updatedSession?.metadata.model,
172
+ },
173
+ };
174
+ }
175
+ catch (error) {
176
+ this.logger.error("sendMessage failed", { error, sessionKey });
177
+ if (onError) {
178
+ onError(error);
179
+ }
180
+ return {
181
+ success: false,
182
+ error: error.message,
183
+ };
184
+ }
185
+ }
186
+ async getSession(sessionKey) {
187
+ try {
188
+ const parsed = parseAgentSessionKey(sessionKey);
189
+ const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
190
+ const session = await loadSession(agentId, sessionKey);
191
+ if (!session) {
192
+ return null;
193
+ }
194
+ // Session metadata has 'updated' (ISO string) from new format
195
+ const updated = session.metadata.updated || new Date().toISOString();
196
+ return {
197
+ key: sessionKey,
198
+ sessionId: session.metadata.sessionId,
199
+ displayName: session.metadata.displayName,
200
+ updatedAt: new Date(updated).getTime(),
201
+ model: session.metadata.model,
202
+ usage: {
203
+ inputTokens: 0, // TODO: calculate from history
204
+ outputTokens: 0,
205
+ totalTokens: 0,
206
+ },
207
+ };
208
+ }
209
+ catch (error) {
210
+ this.logger.error("getSession failed", { error, sessionKey });
211
+ return null;
212
+ }
213
+ }
214
+ async listSessions() {
215
+ try {
216
+ // List sessions for default agent
217
+ const agentId = DEFAULT_AGENT_ID;
218
+ const sessionKeys = await listAgentSessions(agentId);
219
+ const sessions = [];
220
+ // Load each session
221
+ for (const sessionKey of sessionKeys) {
222
+ const session = await loadSession(agentId, sessionKey);
223
+ if (session) {
224
+ // Session metadata has 'updated' (ISO string) from new format
225
+ const updated = session.metadata.updated || new Date().toISOString();
226
+ sessions.push({
227
+ key: sessionKey,
228
+ sessionId: session.metadata.sessionId,
229
+ displayName: session.metadata.displayName,
230
+ updatedAt: new Date(updated).getTime(),
231
+ model: session.metadata.model,
232
+ usage: {
233
+ inputTokens: 0, // TODO: calculate from history
234
+ outputTokens: 0,
235
+ totalTokens: 0,
236
+ },
237
+ });
238
+ }
239
+ }
240
+ // Sort by updatedAt descending (most recent first)
241
+ sessions.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
242
+ return sessions;
243
+ }
244
+ catch (error) {
245
+ this.logger.error("listSessions failed", { error });
246
+ return [];
247
+ }
248
+ }
249
+ async getStatus() {
250
+ try {
251
+ const sessions = await this.listSessions();
252
+ const runningAgents = this.broker.getRunningAgents();
253
+ const queues = this.broker.getAllQueues();
254
+ let queuedMessages = 0;
255
+ for (const count of queues.values()) {
256
+ queuedMessages += count;
257
+ }
258
+ return {
259
+ mode: "odu",
260
+ activeSessions: sessions.length,
261
+ queuedMessages,
262
+ healthy: true,
263
+ metadata: {
264
+ runningAgents: runningAgents.length,
265
+ interactionAgents: this.interactionAgents.size,
266
+ oduName: this.oduConfig.name,
267
+ },
268
+ };
269
+ }
270
+ catch (error) {
271
+ this.logger.error("getStatus failed", { error });
272
+ return {
273
+ mode: "odu",
274
+ activeSessions: 0,
275
+ queuedMessages: 0,
276
+ healthy: false,
277
+ metadata: {
278
+ error: error.message,
279
+ },
280
+ };
281
+ }
282
+ }
283
+ async resetSession(sessionKey) {
284
+ try {
285
+ const parsed = parseAgentSessionKey(sessionKey);
286
+ const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
287
+ // Delete session directory
288
+ const sessionDir = resolveSessionDir(agentId, sessionKey);
289
+ await fs.rm(sessionDir, { recursive: true, force: true });
290
+ // Also remove IA if it exists
291
+ const userId = this.sessionKeyToUserId(sessionKey);
292
+ this.interactionAgents.delete(userId);
293
+ this.logger.info("Session reset", { sessionKey });
294
+ }
295
+ catch (error) {
296
+ this.logger.error("resetSession failed", { error, sessionKey });
297
+ throw error;
298
+ }
299
+ }
300
+ async shutdown() {
301
+ try {
302
+ this.logger.info("Shutting down ODUControlPlane");
303
+ // Clear all IAs
304
+ this.interactionAgents.clear();
305
+ // Broker doesn't have a shutdown method yet, but we could add one later
306
+ // For now, just clear our state
307
+ this.logger.info("ODUControlPlane shutdown complete");
308
+ }
309
+ catch (error) {
310
+ this.logger.error("shutdown failed", { error });
311
+ throw error;
312
+ }
313
+ }
314
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * SingleAgentControlPlane - Wraps the current NexusBot embedded agent behavior.
3
+ *
4
+ * This implementation maintains the existing single-agent-per-session pattern.
5
+ * No behavior changes - just adapting the existing code to the AgentControlPlane interface.
6
+ */
7
+ import { runEmbeddedPiAgent, } from "../agents/pi-embedded-runner.js";
8
+ import { queueEmbeddedPiMessage } from "../agents/pi-embedded.js";
9
+ import { loadConfig } from "../config/config.js";
10
+ import { loadSession, writeSessionMetadata, listAgentSessions, resolveSessionDir, } from "../config/sessions.js";
11
+ import { normalizeAgentId, parseAgentSessionKey, DEFAULT_AGENT_ID, } from "../routing/session-key.js";
12
+ import path from "node:path";
13
+ import { randomUUID } from "node:crypto";
14
+ import { buildWorkspaceSkillSnapshot } from "../agents/skills.js";
15
+ import { resolveNexusAgentDir } from "../agents/agent-paths.js";
16
+ import fs from "node:fs";
17
+ /**
18
+ * SingleAgentControlPlane wraps the existing runEmbeddedPiAgent function
19
+ * to implement the AgentControlPlane interface.
20
+ *
21
+ * This maintains the current behavior where each session has its own
22
+ * embedded agent, and messages are processed through the existing
23
+ * runEmbeddedPiAgent flow.
24
+ */
25
+ export class SingleAgentControlPlane {
26
+ config;
27
+ workspaceDir;
28
+ agentDir;
29
+ constructor(options) {
30
+ this.config = options?.config ?? loadConfig();
31
+ this.workspaceDir = options?.workspaceDir ?? process.cwd();
32
+ this.agentDir = options?.agentDir ?? resolveNexusAgentDir();
33
+ }
34
+ async sendMessage(options) {
35
+ const { sessionKey, message, agentId: optAgentId, thinkingLevel, verboseLevel, isHeartbeat, streaming, onChunk, onComplete, onError, } = options;
36
+ try {
37
+ // Parse session key to get agentId and sessionId
38
+ const parsed = parseAgentSessionKey(sessionKey);
39
+ const agentId = normalizeAgentId(optAgentId ?? parsed?.agentId ?? DEFAULT_AGENT_ID);
40
+ // In the new format, we use sessionKey as the directory name for O(1) lookup
41
+ // The sessionId in metadata is still a UUID for uniqueness
42
+ const existingSession = await loadSession(agentId, sessionKey);
43
+ let sessionId;
44
+ let sessionEntry;
45
+ if (!existingSession) {
46
+ // Create new session
47
+ sessionId = randomUUID(); // Internal UUID
48
+ sessionEntry = {
49
+ sessionId,
50
+ updatedAt: Date.now(),
51
+ thinkingLevel: thinkingLevel ?? this.config.agent?.thinkingDefault,
52
+ verboseLevel: verboseLevel ?? this.config.agent?.verboseDefault,
53
+ };
54
+ // Write initial metadata
55
+ // Directory name = sessionKey, sessionId in metadata = UUID
56
+ await writeSessionMetadata(agentId, sessionKey, {
57
+ ...sessionEntry,
58
+ created: new Date().toISOString(),
59
+ });
60
+ }
61
+ else {
62
+ sessionId = existingSession.metadata.sessionId;
63
+ sessionEntry = existingSession.metadata;
64
+ }
65
+ const sessionDir = resolveSessionDir(agentId, sessionKey);
66
+ const sessionFile = path.join(sessionDir, "history.jsonl");
67
+ // If streaming and already has an active agent, try to queue the message
68
+ if (streaming && queueEmbeddedPiMessage(sessionId, message)) {
69
+ return {
70
+ success: true,
71
+ sessionId,
72
+ };
73
+ }
74
+ // Load skills snapshot
75
+ const skillsSnapshot = await buildWorkspaceSkillSnapshot(this.workspaceDir, {
76
+ config: this.config,
77
+ });
78
+ // Build callbacks for streaming/chunks
79
+ let partialPayloads = [];
80
+ const onPartialReply = async (payload) => {
81
+ if (onChunk) {
82
+ onChunk({
83
+ type: "text",
84
+ content: payload.text,
85
+ metadata: { mediaUrls: payload.mediaUrls },
86
+ });
87
+ }
88
+ partialPayloads.push(payload);
89
+ };
90
+ const onToolResult = async (payload) => {
91
+ if (onChunk) {
92
+ onChunk({
93
+ type: "tool",
94
+ content: payload.text,
95
+ metadata: { mediaUrls: payload.mediaUrls },
96
+ });
97
+ }
98
+ };
99
+ // Run the embedded agent
100
+ const runId = randomUUID();
101
+ const result = await runEmbeddedPiAgent({
102
+ sessionId,
103
+ sessionKey,
104
+ sessionFile,
105
+ workspaceDir: this.workspaceDir,
106
+ agentDir: this.agentDir,
107
+ config: this.config,
108
+ skillsSnapshot,
109
+ prompt: message,
110
+ provider: sessionEntry.providerOverride,
111
+ model: sessionEntry.modelOverride,
112
+ authProfileId: sessionEntry.authProfileOverride,
113
+ thinkLevel: thinkingLevel,
114
+ verboseLevel: verboseLevel,
115
+ timeoutMs: (this.config.agent?.timeoutSeconds ?? 600) * 1000,
116
+ runId,
117
+ onPartialReply: streaming ? onPartialReply : undefined,
118
+ onToolResult: streaming ? onToolResult : undefined,
119
+ });
120
+ // Call onComplete if provided
121
+ if (onComplete) {
122
+ onComplete();
123
+ }
124
+ // Convert result to SendMessageResult
125
+ // Note: EmbeddedPiRunResult doesn't have an error field; errors are thrown as exceptions
126
+ return {
127
+ success: true,
128
+ sessionId,
129
+ payloads: result.payloads,
130
+ meta: {
131
+ durationMs: result.meta.durationMs,
132
+ provider: result.meta.agentMeta?.provider,
133
+ model: result.meta.agentMeta?.model,
134
+ usage: result.meta.agentMeta?.usage,
135
+ },
136
+ };
137
+ }
138
+ catch (error) {
139
+ if (onError) {
140
+ onError(error);
141
+ }
142
+ return {
143
+ success: false,
144
+ error: error.message,
145
+ };
146
+ }
147
+ }
148
+ async getSession(sessionKey) {
149
+ try {
150
+ const parsed = parseAgentSessionKey(sessionKey);
151
+ const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
152
+ // Load session from new format
153
+ const session = await loadSession(agentId, sessionKey);
154
+ if (!session) {
155
+ return null;
156
+ }
157
+ const entry = session.metadata;
158
+ return {
159
+ key: sessionKey,
160
+ sessionId: entry.sessionId,
161
+ displayName: entry.displayName,
162
+ updatedAt: entry.updatedAt,
163
+ model: entry.model,
164
+ usage: {
165
+ inputTokens: entry.inputTokens,
166
+ outputTokens: entry.outputTokens,
167
+ totalTokens: entry.totalTokens,
168
+ },
169
+ entry,
170
+ };
171
+ }
172
+ catch (error) {
173
+ return null;
174
+ }
175
+ }
176
+ async listSessions() {
177
+ try {
178
+ const agentId = DEFAULT_AGENT_ID;
179
+ // List all session keys (directory names) for the agent using new format
180
+ const sessionKeys = await listAgentSessions(agentId);
181
+ const sessions = [];
182
+ for (const sessionKey of sessionKeys) {
183
+ // Load each session (sessionKey is the directory name)
184
+ const session = await loadSession(agentId, sessionKey);
185
+ if (session) {
186
+ const entry = session.metadata;
187
+ sessions.push({
188
+ key: sessionKey, // sessionKey is the directory name
189
+ sessionId: entry.sessionId, // UUID from metadata
190
+ displayName: entry.displayName,
191
+ updatedAt: entry.updatedAt,
192
+ model: entry.model,
193
+ usage: {
194
+ inputTokens: entry.inputTokens,
195
+ outputTokens: entry.outputTokens,
196
+ totalTokens: entry.totalTokens,
197
+ },
198
+ entry,
199
+ });
200
+ }
201
+ }
202
+ // Sort by updatedAt descending (most recent first)
203
+ sessions.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
204
+ return sessions;
205
+ }
206
+ catch (error) {
207
+ return [];
208
+ }
209
+ }
210
+ async getStatus() {
211
+ try {
212
+ const sessions = await this.listSessions();
213
+ return {
214
+ mode: "single",
215
+ activeSessions: sessions.length,
216
+ healthy: true,
217
+ };
218
+ }
219
+ catch (error) {
220
+ return {
221
+ mode: "single",
222
+ activeSessions: 0,
223
+ healthy: false,
224
+ metadata: {
225
+ error: error.message,
226
+ },
227
+ };
228
+ }
229
+ }
230
+ async resetSession(sessionKey) {
231
+ const parsed = parseAgentSessionKey(sessionKey);
232
+ const agentId = normalizeAgentId(parsed?.agentId ?? DEFAULT_AGENT_ID);
233
+ // Remove the session directory (new format)
234
+ const sessionDir = resolveSessionDir(agentId, sessionKey);
235
+ try {
236
+ await fs.promises.rm(sessionDir, { recursive: true, force: true });
237
+ }
238
+ catch (error) {
239
+ // If directory doesn't exist, that's fine
240
+ if (error.code !== "ENOENT") {
241
+ throw error;
242
+ }
243
+ }
244
+ }
245
+ async shutdown() {
246
+ // Single agent mode doesn't need cleanup
247
+ // Future: could flush any pending operations
248
+ }
249
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * AgentControlPlane - Abstract interface for agent orchestration.
3
+ *
4
+ * This interface allows swapping between different agent control strategies:
5
+ * - SingleAgentControlPlane: Current NexusBot behavior (one agent per session)
6
+ * - ODUControlPlane: Orchestration Domain Unit pattern (Interaction Agent + Execution Agents)
7
+ *
8
+ * The Gateway (Access Plane) interacts with agents through this interface,
9
+ * not directly with agent implementations.
10
+ */
11
+ export {};