feat(wave2): @mosaic/openclaw-context plugin migrated to monorepo (#3)
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #3.
This commit is contained in:
97
plugins/openclaw-context/README.md
Normal file
97
plugins/openclaw-context/README.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# @mosaic/openclaw-context
|
||||
|
||||
OpenBrain-backed `ContextEngine` plugin for OpenClaw.
|
||||
|
||||
This plugin stores session context in OpenBrain over REST so context can be reassembled from recent history plus semantic matches instead of relying only on in-session compaction state.
|
||||
|
||||
## Features
|
||||
|
||||
- Registers context engine id: `openbrain`
|
||||
- Typed OpenBrain REST client with Bearer auth
|
||||
- Session-aware ingest + batch ingest
|
||||
- Context assembly from recent + semantic search under token budget
|
||||
- Compaction summaries archived to OpenBrain
|
||||
- Subagent seed/result handoff helpers
|
||||
|
||||
## Requirements
|
||||
|
||||
- OpenClaw with plugin/context-engine support (`openclaw >= 2026.3.2`)
|
||||
- Reachable OpenBrain REST API
|
||||
- OpenBrain API key
|
||||
|
||||
## Install (local workspace plugin)
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm build
|
||||
```
|
||||
|
||||
Then reference this plugin in your OpenClaw config.
|
||||
|
||||
## OpenBrain Setup (self-host or hosted)
|
||||
|
||||
You must provide both of these in plugin config:
|
||||
|
||||
- `baseUrl`: your OpenBrain API root (example: `https://brain.your-domain.com`)
|
||||
- `apiKey`: Bearer token for your OpenBrain instance
|
||||
|
||||
No host or key fallback is built in. Missing `baseUrl` or `apiKey` throws `OpenBrainConfigError` at `bootstrap()`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Plugin entry id: `openclaw-openbrain-context`
|
||||
Context engine slot id: `openbrain`
|
||||
|
||||
### Config fields
|
||||
|
||||
- `baseUrl` (required, string): OpenBrain API base URL
|
||||
- `apiKey` (required, string): OpenBrain Bearer token
|
||||
- `source` (optional, string, default `openclaw`): source prefix; engine stores thoughts under `<source>:<sessionId>`
|
||||
- `recentMessages` (optional, integer, default `20`): recent thoughts to fetch for bootstrap/assemble
|
||||
- `semanticSearchLimit` (optional, integer, default `10`): semantic matches fetched in assemble
|
||||
- `subagentRecentMessages` (optional, integer, default `8`): context lines used for subagent seed/result exchange
|
||||
|
||||
## Environment Variable Pattern
|
||||
|
||||
Use OpenClaw variable interpolation in `openclaw.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"apiKey": "${OPENBRAIN_API_KEY}"
|
||||
}
|
||||
```
|
||||
|
||||
Then set it in your shell/runtime environment before starting OpenClaw.
|
||||
|
||||
## Example `openclaw.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"plugins": {
|
||||
"slots": {
|
||||
"contextEngine": "openbrain"
|
||||
},
|
||||
"entries": {
|
||||
"openclaw-openbrain-context": {
|
||||
"enabled": true,
|
||||
"config": {
|
||||
"baseUrl": "https://brain.example.com",
|
||||
"apiKey": "${OPENBRAIN_API_KEY}",
|
||||
"source": "openclaw",
|
||||
"recentMessages": 20,
|
||||
"semanticSearchLimit": 10,
|
||||
"subagentRecentMessages": 8
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
pnpm build
|
||||
pnpm test
|
||||
```
|
||||
58
plugins/openclaw-context/openclaw.plugin.json
Normal file
58
plugins/openclaw-context/openclaw.plugin.json
Normal file
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"id": "openclaw-openbrain-context",
|
||||
"name": "OpenBrain Context Engine",
|
||||
"description": "OpenBrain-backed ContextEngine plugin for OpenClaw",
|
||||
"version": "0.0.1",
|
||||
"kind": "context-engine",
|
||||
"configSchema": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["baseUrl", "apiKey"],
|
||||
"properties": {
|
||||
"baseUrl": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Base URL of your OpenBrain REST API"
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Bearer token used to authenticate against OpenBrain"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"default": "openclaw",
|
||||
"description": "Source prefix stored in OpenBrain (session id is appended)"
|
||||
},
|
||||
"recentMessages": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 20,
|
||||
"description": "How many recent thoughts to fetch during assemble/bootstrap"
|
||||
},
|
||||
"semanticSearchLimit": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 10,
|
||||
"description": "How many semantic matches to request during assemble"
|
||||
},
|
||||
"subagentRecentMessages": {
|
||||
"type": "integer",
|
||||
"minimum": 1,
|
||||
"default": 8,
|
||||
"description": "How many thoughts to use when seeding/summarizing subagents"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uiHints": {
|
||||
"baseUrl": {
|
||||
"label": "OpenBrain Base URL",
|
||||
"placeholder": "https://brain.example.com"
|
||||
},
|
||||
"apiKey": {
|
||||
"label": "OpenBrain API Key",
|
||||
"sensitive": true
|
||||
}
|
||||
}
|
||||
}
|
||||
38
plugins/openclaw-context/package.json
Normal file
38
plugins/openclaw-context/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@mosaic/openclaw-context",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "OpenClaw \u2192 OpenBrain context engine plugin",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"openclaw.plugin.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint src/",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaic/types": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"vitest": "^2",
|
||||
"@types/node": "^22"
|
||||
},
|
||||
"keywords": [
|
||||
"openclaw",
|
||||
"openbrain",
|
||||
"context-engine",
|
||||
"plugin"
|
||||
]
|
||||
}
|
||||
3
plugins/openclaw-context/src/constants.ts
Normal file
3
plugins/openclaw-context/src/constants.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const OPENBRAIN_CONTEXT_ENGINE_ID = "openbrain";
|
||||
export const OPENBRAIN_PLUGIN_ID = "openclaw-openbrain-context";
|
||||
export const OPENBRAIN_PLUGIN_VERSION = "0.0.1";
|
||||
774
plugins/openclaw-context/src/engine.ts
Normal file
774
plugins/openclaw-context/src/engine.ts
Normal file
@@ -0,0 +1,774 @@
|
||||
import { OPENBRAIN_CONTEXT_ENGINE_ID, OPENBRAIN_PLUGIN_VERSION } from "./constants.js";
|
||||
import { OpenBrainConfigError } from "./errors.js";
|
||||
import type {
|
||||
AgentMessage,
|
||||
AssembleResult,
|
||||
BootstrapResult,
|
||||
CompactResult,
|
||||
ContextEngine,
|
||||
ContextEngineInfo,
|
||||
IngestBatchResult,
|
||||
IngestResult,
|
||||
PluginLogger,
|
||||
SubagentEndReason,
|
||||
SubagentSpawnPreparation,
|
||||
} from "./openclaw-types.js";
|
||||
import {
|
||||
OpenBrainClient,
|
||||
type OpenBrainClientLike,
|
||||
type OpenBrainSearchInput,
|
||||
type OpenBrainThought,
|
||||
type OpenBrainThoughtMetadata,
|
||||
} from "./openbrain-client.js";
|
||||
|
||||
export type OpenBrainContextEngineConfig = {
|
||||
baseUrl?: string;
|
||||
apiKey?: string;
|
||||
recentMessages?: number;
|
||||
semanticSearchLimit?: number;
|
||||
source?: string;
|
||||
subagentRecentMessages?: number;
|
||||
};
|
||||
|
||||
type ResolvedOpenBrainContextEngineConfig = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
recentMessages: number;
|
||||
semanticSearchLimit: number;
|
||||
source: string;
|
||||
subagentRecentMessages: number;
|
||||
};
|
||||
|
||||
export type OpenBrainContextEngineDeps = {
|
||||
createClient?: (config: ResolvedOpenBrainContextEngineConfig) => OpenBrainClientLike;
|
||||
now?: () => number;
|
||||
logger?: PluginLogger;
|
||||
};
|
||||
|
||||
type SubagentState = {
|
||||
parentSessionKey: string;
|
||||
seedThoughtId?: string;
|
||||
};
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function parsePositiveInteger(value: unknown, fallback: number): number {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
const rounded = Math.floor(value);
|
||||
return rounded > 0 ? rounded : fallback;
|
||||
}
|
||||
|
||||
function normalizeRole(role: unknown): string {
|
||||
if (typeof role !== "string" || role.length === 0) {
|
||||
return "assistant";
|
||||
}
|
||||
|
||||
if (role === "user" || role === "assistant" || role === "tool" || role === "system") {
|
||||
return role;
|
||||
}
|
||||
|
||||
return "assistant";
|
||||
}
|
||||
|
||||
function serializeContent(value: unknown): string {
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map((part) => serializeContent(part))
|
||||
.filter((part) => part.length > 0)
|
||||
.join("\n")
|
||||
.trim();
|
||||
}
|
||||
|
||||
if (isRecord(value) && typeof value.text === "string") {
|
||||
return value.text;
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.stringify(value);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
function estimateTextTokens(text: string): number {
|
||||
const normalized = text.trim();
|
||||
if (normalized.length === 0) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.max(1, Math.ceil(normalized.length / 4) + 4);
|
||||
}
|
||||
|
||||
function thoughtTimestamp(thought: OpenBrainThought, fallbackTimestamp: number): number {
|
||||
const createdAt =
|
||||
thought.createdAt ??
|
||||
(typeof thought.created_at === "string" ? thought.created_at : undefined);
|
||||
|
||||
if (createdAt === undefined) {
|
||||
return fallbackTimestamp;
|
||||
}
|
||||
|
||||
const parsed = Date.parse(createdAt);
|
||||
return Number.isFinite(parsed) ? parsed : fallbackTimestamp;
|
||||
}
|
||||
|
||||
function thoughtFingerprint(thought: OpenBrainThought): string {
|
||||
const role = typeof thought.metadata?.role === "string" ? thought.metadata.role : "assistant";
|
||||
return `${role}\n${thought.content}`;
|
||||
}
|
||||
|
||||
function truncateLine(value: string, maxLength: number): string {
|
||||
if (value.length <= maxLength) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return `${value.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
export class OpenBrainContextEngine implements ContextEngine {
|
||||
readonly info: ContextEngineInfo = {
|
||||
id: OPENBRAIN_CONTEXT_ENGINE_ID,
|
||||
name: "OpenBrain Context Engine",
|
||||
version: OPENBRAIN_PLUGIN_VERSION,
|
||||
ownsCompaction: true,
|
||||
};
|
||||
|
||||
private readonly rawConfig: unknown;
|
||||
private readonly createClientFn:
|
||||
| ((config: ResolvedOpenBrainContextEngineConfig) => OpenBrainClientLike)
|
||||
| undefined;
|
||||
private readonly now: () => number;
|
||||
private readonly logger: PluginLogger | undefined;
|
||||
|
||||
private config: ResolvedOpenBrainContextEngineConfig | undefined;
|
||||
private client: OpenBrainClientLike | undefined;
|
||||
private readonly sessionTurns = new Map<string, number>();
|
||||
private readonly subagentState = new Map<string, SubagentState>();
|
||||
private disposed = false;
|
||||
|
||||
constructor(rawConfig: unknown, deps?: OpenBrainContextEngineDeps) {
|
||||
this.rawConfig = rawConfig;
|
||||
this.createClientFn = deps?.createClient;
|
||||
this.now = deps?.now ?? (() => Date.now());
|
||||
this.logger = deps?.logger;
|
||||
}
|
||||
|
||||
async bootstrap(params: { sessionId: string; sessionFile: string }): Promise<BootstrapResult> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const config = this.getConfig();
|
||||
const client = this.getClient();
|
||||
const source = this.sourceForSession(params.sessionId);
|
||||
|
||||
const recentThoughts = await client.listRecent({
|
||||
limit: config.recentMessages,
|
||||
source,
|
||||
});
|
||||
|
||||
const sessionThoughts = this.filterSessionThoughts(recentThoughts, params.sessionId);
|
||||
|
||||
let maxTurn = -1;
|
||||
for (const thought of sessionThoughts) {
|
||||
const turn = thought.metadata?.turn;
|
||||
if (typeof turn === "number" && Number.isFinite(turn) && turn > maxTurn) {
|
||||
maxTurn = turn;
|
||||
}
|
||||
}
|
||||
|
||||
this.sessionTurns.set(params.sessionId, maxTurn + 1);
|
||||
|
||||
return {
|
||||
bootstrapped: true,
|
||||
importedMessages: sessionThoughts.length,
|
||||
};
|
||||
}
|
||||
|
||||
async ingest(params: {
|
||||
sessionId: string;
|
||||
message: AgentMessage;
|
||||
isHeartbeat?: boolean;
|
||||
}): Promise<IngestResult> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const client = this.getClient();
|
||||
const content = serializeContent(params.message.content).trim();
|
||||
if (content.length === 0) {
|
||||
return { ingested: false };
|
||||
}
|
||||
|
||||
const metadata: OpenBrainThoughtMetadata = {
|
||||
sessionId: params.sessionId,
|
||||
turn: this.nextTurn(params.sessionId),
|
||||
role: normalizeRole(params.message.role),
|
||||
type: "message",
|
||||
};
|
||||
|
||||
if (params.isHeartbeat === true) {
|
||||
metadata.isHeartbeat = true;
|
||||
}
|
||||
|
||||
await client.createThought({
|
||||
content,
|
||||
source: this.sourceForSession(params.sessionId),
|
||||
metadata,
|
||||
});
|
||||
|
||||
return { ingested: true };
|
||||
}
|
||||
|
||||
async ingestBatch(params: {
|
||||
sessionId: string;
|
||||
messages: AgentMessage[];
|
||||
isHeartbeat?: boolean;
|
||||
}): Promise<IngestBatchResult> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const maxConcurrency = 5;
|
||||
let ingestedCount = 0;
|
||||
for (let i = 0; i < params.messages.length; i += maxConcurrency) {
|
||||
const chunk = params.messages.slice(i, i + maxConcurrency);
|
||||
const results = await Promise.all(
|
||||
chunk.map((message) => {
|
||||
const ingestParams: {
|
||||
sessionId: string;
|
||||
message: AgentMessage;
|
||||
isHeartbeat?: boolean;
|
||||
} = {
|
||||
sessionId: params.sessionId,
|
||||
message,
|
||||
};
|
||||
if (params.isHeartbeat !== undefined) {
|
||||
ingestParams.isHeartbeat = params.isHeartbeat;
|
||||
}
|
||||
return this.ingest(ingestParams);
|
||||
}),
|
||||
);
|
||||
|
||||
for (const result of results) {
|
||||
if (result.ingested) {
|
||||
ingestedCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { ingestedCount };
|
||||
}
|
||||
|
||||
async assemble(params: {
|
||||
sessionId: string;
|
||||
messages: AgentMessage[];
|
||||
tokenBudget?: number;
|
||||
}): Promise<AssembleResult> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const config = this.getConfig();
|
||||
const client = this.getClient();
|
||||
const source = this.sourceForSession(params.sessionId);
|
||||
|
||||
const recentThoughts = this.filterSessionThoughts(
|
||||
await client.listRecent({
|
||||
limit: config.recentMessages,
|
||||
source,
|
||||
}),
|
||||
params.sessionId,
|
||||
);
|
||||
|
||||
const semanticThoughts = await this.searchSemanticThoughts({
|
||||
client,
|
||||
source,
|
||||
config,
|
||||
sessionId: params.sessionId,
|
||||
messages: params.messages,
|
||||
});
|
||||
|
||||
const mergedThoughts = this.mergeThoughts(recentThoughts, semanticThoughts);
|
||||
const mergedMessages =
|
||||
mergedThoughts.length > 0
|
||||
? mergedThoughts.map((thought, index) => this.toAgentMessage(thought, index))
|
||||
: params.messages;
|
||||
|
||||
const tokenBudget = params.tokenBudget;
|
||||
const budgetedMessages =
|
||||
typeof tokenBudget === "number" && tokenBudget > 0
|
||||
? this.trimToBudget(mergedMessages, tokenBudget)
|
||||
: mergedMessages;
|
||||
|
||||
return {
|
||||
messages: budgetedMessages,
|
||||
estimatedTokens: this.estimateTokensForMessages(budgetedMessages),
|
||||
};
|
||||
}
|
||||
|
||||
async compact(params: {
|
||||
sessionId: string;
|
||||
sessionFile: string;
|
||||
tokenBudget?: number;
|
||||
force?: boolean;
|
||||
currentTokenCount?: number;
|
||||
compactionTarget?: "budget" | "threshold";
|
||||
customInstructions?: string;
|
||||
legacyParams?: Record<string, unknown>;
|
||||
}): Promise<CompactResult> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const config = this.getConfig();
|
||||
const client = this.getClient();
|
||||
const source = this.sourceForSession(params.sessionId);
|
||||
|
||||
const recentThoughts = this.filterSessionThoughts(
|
||||
await client.listRecent({
|
||||
limit: Math.max(config.recentMessages, config.subagentRecentMessages),
|
||||
source,
|
||||
}),
|
||||
params.sessionId,
|
||||
);
|
||||
|
||||
if (recentThoughts.length === 0) {
|
||||
return {
|
||||
ok: true,
|
||||
compacted: false,
|
||||
reason: "no-session-context",
|
||||
result: {
|
||||
tokensBefore: 0,
|
||||
tokensAfter: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const summarizedThoughts = this.selectSummaryThoughts(recentThoughts);
|
||||
const summary = this.buildSummary(
|
||||
params.customInstructions !== undefined
|
||||
? {
|
||||
sessionId: params.sessionId,
|
||||
thoughts: summarizedThoughts,
|
||||
customInstructions: params.customInstructions,
|
||||
}
|
||||
: {
|
||||
sessionId: params.sessionId,
|
||||
thoughts: summarizedThoughts,
|
||||
},
|
||||
);
|
||||
|
||||
const summaryTokens = estimateTextTokens(summary);
|
||||
const tokensBefore = this.estimateTokensForThoughts(summarizedThoughts);
|
||||
|
||||
await client.createThought({
|
||||
content: summary,
|
||||
source,
|
||||
metadata: {
|
||||
sessionId: params.sessionId,
|
||||
turn: this.nextTurn(params.sessionId),
|
||||
role: "assistant",
|
||||
type: "summary",
|
||||
},
|
||||
});
|
||||
|
||||
const summaryThoughtIds = Array.from(
|
||||
new Set(
|
||||
summarizedThoughts
|
||||
.map((thought) => thought.id.trim())
|
||||
.filter((id) => id.length > 0),
|
||||
),
|
||||
);
|
||||
await Promise.all(summaryThoughtIds.map((thoughtId) => client.deleteThought(thoughtId)));
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
compacted: true,
|
||||
reason: "summary-archived",
|
||||
result: {
|
||||
summary,
|
||||
tokensBefore,
|
||||
tokensAfter: summaryTokens,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async prepareSubagentSpawn(params: {
|
||||
parentSessionKey: string;
|
||||
childSessionKey: string;
|
||||
ttlMs?: number;
|
||||
}): Promise<SubagentSpawnPreparation | undefined> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const config = this.getConfig();
|
||||
const client = this.getClient();
|
||||
|
||||
const parentThoughts = this.filterSessionThoughts(
|
||||
await client.listRecent({
|
||||
limit: config.subagentRecentMessages,
|
||||
source: this.sourceForSession(params.parentSessionKey),
|
||||
}),
|
||||
params.parentSessionKey,
|
||||
);
|
||||
|
||||
const seedContent = this.buildSubagentSeedContent({
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
childSessionKey: params.childSessionKey,
|
||||
thoughts: parentThoughts,
|
||||
});
|
||||
|
||||
const createdThought = await client.createThought({
|
||||
content: seedContent,
|
||||
source: this.sourceForSession(params.childSessionKey),
|
||||
metadata: {
|
||||
sessionId: params.childSessionKey,
|
||||
role: "assistant",
|
||||
type: "summary",
|
||||
parentSessionId: params.parentSessionKey,
|
||||
ttlMs: params.ttlMs,
|
||||
},
|
||||
});
|
||||
|
||||
this.subagentState.set(params.childSessionKey, {
|
||||
parentSessionKey: params.parentSessionKey,
|
||||
seedThoughtId: createdThought.id,
|
||||
});
|
||||
|
||||
return {
|
||||
rollback: async () => {
|
||||
const state = this.subagentState.get(params.childSessionKey);
|
||||
this.subagentState.delete(params.childSessionKey);
|
||||
|
||||
if (state?.seedThoughtId !== undefined && state.seedThoughtId.length > 0) {
|
||||
await client.deleteThought(state.seedThoughtId);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async onSubagentEnded(params: {
|
||||
childSessionKey: string;
|
||||
reason: SubagentEndReason;
|
||||
}): Promise<void> {
|
||||
this.assertNotDisposed();
|
||||
|
||||
const state = this.subagentState.get(params.childSessionKey);
|
||||
if (state === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const client = this.getClient();
|
||||
const config = this.getConfig();
|
||||
|
||||
const childThoughts = this.filterSessionThoughts(
|
||||
await client.listRecent({
|
||||
limit: config.subagentRecentMessages,
|
||||
source: this.sourceForSession(params.childSessionKey),
|
||||
}),
|
||||
params.childSessionKey,
|
||||
);
|
||||
|
||||
const summary = this.buildSubagentResultSummary({
|
||||
childSessionKey: params.childSessionKey,
|
||||
reason: params.reason,
|
||||
thoughts: childThoughts,
|
||||
});
|
||||
|
||||
await client.createThought({
|
||||
content: summary,
|
||||
source: this.sourceForSession(state.parentSessionKey),
|
||||
metadata: {
|
||||
sessionId: state.parentSessionKey,
|
||||
turn: this.nextTurn(state.parentSessionKey),
|
||||
role: "tool",
|
||||
type: "subagent-result",
|
||||
childSessionId: params.childSessionKey,
|
||||
reason: params.reason,
|
||||
},
|
||||
});
|
||||
|
||||
this.subagentState.delete(params.childSessionKey);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.sessionTurns.clear();
|
||||
this.subagentState.clear();
|
||||
this.disposed = true;
|
||||
}
|
||||
|
||||
private searchSemanticThoughts(params: {
|
||||
client: OpenBrainClientLike;
|
||||
source: string;
|
||||
config: ResolvedOpenBrainContextEngineConfig;
|
||||
sessionId: string;
|
||||
messages: AgentMessage[];
|
||||
}): Promise<OpenBrainThought[]> {
|
||||
const query = this.pickSemanticQuery(params.messages);
|
||||
if (query === undefined || query.length === 0 || params.config.semanticSearchLimit <= 0) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const request: OpenBrainSearchInput = {
|
||||
query,
|
||||
limit: params.config.semanticSearchLimit,
|
||||
source: params.source,
|
||||
};
|
||||
|
||||
return params.client
|
||||
.search(request)
|
||||
.then((results) => this.filterSessionThoughts(results, params.sessionId))
|
||||
.catch((error) => {
|
||||
this.logger?.warn?.("OpenBrain semantic search failed", error);
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
private pickSemanticQuery(messages: AgentMessage[]): string | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const message = messages[i];
|
||||
if (message === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (normalizeRole(message.role) !== "user") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = serializeContent(message.content).trim();
|
||||
if (content.length > 0) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const message = messages[i];
|
||||
if (message === undefined) {
|
||||
continue;
|
||||
}
|
||||
const content = serializeContent(message.content).trim();
|
||||
if (content.length > 0) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private mergeThoughts(recentThoughts: OpenBrainThought[], semanticThoughts: OpenBrainThought[]): OpenBrainThought[] {
|
||||
const merged: OpenBrainThought[] = [];
|
||||
const seenIds = new Set<string>();
|
||||
const seenFingerprints = new Set<string>();
|
||||
|
||||
for (const thought of [...recentThoughts, ...semanticThoughts]) {
|
||||
const id = thought.id.trim();
|
||||
const fingerprint = thoughtFingerprint(thought);
|
||||
|
||||
if (id.length > 0 && seenIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (seenFingerprints.has(fingerprint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (id.length > 0) {
|
||||
seenIds.add(id);
|
||||
}
|
||||
seenFingerprints.add(fingerprint);
|
||||
merged.push(thought);
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
private filterSessionThoughts(thoughts: OpenBrainThought[], sessionId: string): OpenBrainThought[] {
|
||||
return thoughts.filter((thought) => {
|
||||
const thoughtSessionId = thought.metadata?.sessionId;
|
||||
if (typeof thoughtSessionId === "string" && thoughtSessionId.length > 0) {
|
||||
return thoughtSessionId === sessionId;
|
||||
}
|
||||
|
||||
return thought.source === this.sourceForSession(sessionId);
|
||||
});
|
||||
}
|
||||
|
||||
private toAgentMessage(thought: OpenBrainThought, index: number): AgentMessage {
|
||||
return {
|
||||
role: normalizeRole(thought.metadata?.role),
|
||||
content: thought.content,
|
||||
timestamp: thoughtTimestamp(thought, this.now() + index),
|
||||
};
|
||||
}
|
||||
|
||||
private trimToBudget(messages: AgentMessage[], tokenBudget: number): AgentMessage[] {
|
||||
if (messages.length === 0 || tokenBudget <= 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let total = 0;
|
||||
const budgeted: AgentMessage[] = [];
|
||||
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
const message = messages[i];
|
||||
if (message === undefined) {
|
||||
continue;
|
||||
}
|
||||
const tokens = estimateTextTokens(serializeContent(message.content));
|
||||
if (total + tokens > tokenBudget) {
|
||||
break;
|
||||
}
|
||||
|
||||
total += tokens;
|
||||
budgeted.unshift(message);
|
||||
}
|
||||
|
||||
if (budgeted.length === 0) {
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
return lastMessage === undefined ? [] : [lastMessage];
|
||||
}
|
||||
|
||||
return budgeted;
|
||||
}
|
||||
|
||||
private estimateTokensForMessages(messages: AgentMessage[]): number {
|
||||
return messages.reduce((total, message) => {
|
||||
return total + estimateTextTokens(serializeContent(message.content));
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private estimateTokensForThoughts(thoughts: OpenBrainThought[]): number {
|
||||
return thoughts.reduce((total, thought) => total + estimateTextTokens(thought.content), 0);
|
||||
}
|
||||
|
||||
private buildSummary(params: {
|
||||
sessionId: string;
|
||||
thoughts: OpenBrainThought[];
|
||||
customInstructions?: string;
|
||||
}): string {
|
||||
const lines = params.thoughts.map((thought) => {
|
||||
const role = normalizeRole(thought.metadata?.role);
|
||||
const content = truncateLine(thought.content.replace(/\s+/g, " ").trim(), 180);
|
||||
return `- ${role}: ${content}`;
|
||||
});
|
||||
|
||||
const header = `Context summary for session ${params.sessionId}`;
|
||||
const instruction =
|
||||
params.customInstructions !== undefined && params.customInstructions.trim().length > 0
|
||||
? `Custom instructions: ${params.customInstructions.trim()}\n`
|
||||
: "";
|
||||
|
||||
return `${header}\n${instruction}${lines.join("\n")}`;
|
||||
}
|
||||
|
||||
private selectSummaryThoughts(thoughts: OpenBrainThought[]): OpenBrainThought[] {
|
||||
const ordered = [...thoughts].sort((a, b) => {
|
||||
return thoughtTimestamp(a, 0) - thoughtTimestamp(b, 0);
|
||||
});
|
||||
|
||||
const maxLines = Math.min(ordered.length, 10);
|
||||
return ordered.slice(Math.max(ordered.length - maxLines, 0));
|
||||
}
|
||||
|
||||
private buildSubagentSeedContent(params: {
|
||||
parentSessionKey: string;
|
||||
childSessionKey: string;
|
||||
thoughts: OpenBrainThought[];
|
||||
}): string {
|
||||
const lines = params.thoughts.slice(-5).map((thought) => {
|
||||
const role = normalizeRole(thought.metadata?.role);
|
||||
return `- ${role}: ${truncateLine(thought.content.replace(/\s+/g, " ").trim(), 160)}`;
|
||||
});
|
||||
|
||||
const contextBlock = lines.length > 0 ? lines.join("\n") : "- (no prior context found)";
|
||||
|
||||
return [
|
||||
`Subagent context seed`,
|
||||
`Parent session: ${params.parentSessionKey}`,
|
||||
`Child session: ${params.childSessionKey}`,
|
||||
contextBlock,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
private buildSubagentResultSummary(params: {
|
||||
childSessionKey: string;
|
||||
reason: SubagentEndReason;
|
||||
thoughts: OpenBrainThought[];
|
||||
}): string {
|
||||
const lines = params.thoughts.slice(-5).map((thought) => {
|
||||
const role = normalizeRole(thought.metadata?.role);
|
||||
return `- ${role}: ${truncateLine(thought.content.replace(/\s+/g, " ").trim(), 160)}`;
|
||||
});
|
||||
|
||||
const contextBlock = lines.length > 0 ? lines.join("\n") : "- (no child messages found)";
|
||||
|
||||
return [
|
||||
`Subagent ended (${params.reason})`,
|
||||
`Child session: ${params.childSessionKey}`,
|
||||
contextBlock,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
private sourceForSession(sessionId: string): string {
|
||||
return `${this.getConfig().source}:${sessionId}`;
|
||||
}
|
||||
|
||||
private nextTurn(sessionId: string): number {
|
||||
const next = this.sessionTurns.get(sessionId) ?? 0;
|
||||
this.sessionTurns.set(sessionId, next + 1);
|
||||
return next;
|
||||
}
|
||||
|
||||
private getClient(): OpenBrainClientLike {
|
||||
if (this.client !== undefined) {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
const config = this.getConfig();
|
||||
this.client =
|
||||
this.createClientFn?.(config) ??
|
||||
new OpenBrainClient({
|
||||
baseUrl: config.baseUrl,
|
||||
apiKey: config.apiKey,
|
||||
});
|
||||
|
||||
return this.client;
|
||||
}
|
||||
|
||||
private getConfig(): ResolvedOpenBrainContextEngineConfig {
|
||||
if (this.config !== undefined) {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
const raw = isRecord(this.rawConfig) ? this.rawConfig : {};
|
||||
|
||||
const baseUrl = typeof raw.baseUrl === "string" ? raw.baseUrl.trim() : "";
|
||||
if (baseUrl.length === 0) {
|
||||
throw new OpenBrainConfigError("Missing required OpenBrain config: baseUrl");
|
||||
}
|
||||
|
||||
const apiKey = typeof raw.apiKey === "string" ? raw.apiKey.trim() : "";
|
||||
if (apiKey.length === 0) {
|
||||
throw new OpenBrainConfigError("Missing required OpenBrain config: apiKey");
|
||||
}
|
||||
|
||||
this.config = {
|
||||
baseUrl,
|
||||
apiKey,
|
||||
recentMessages: parsePositiveInteger(raw.recentMessages, 20),
|
||||
semanticSearchLimit: parsePositiveInteger(raw.semanticSearchLimit, 10),
|
||||
source: typeof raw.source === "string" && raw.source.trim().length > 0 ? raw.source.trim() : "openclaw",
|
||||
subagentRecentMessages: parsePositiveInteger(raw.subagentRecentMessages, 8),
|
||||
};
|
||||
|
||||
return this.config;
|
||||
}
|
||||
|
||||
private assertNotDisposed(): void {
|
||||
if (this.disposed) {
|
||||
throw new Error("OpenBrainContextEngine has already been disposed");
|
||||
}
|
||||
}
|
||||
}
|
||||
40
plugins/openclaw-context/src/errors.ts
Normal file
40
plugins/openclaw-context/src/errors.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
export class OpenBrainError extends Error {
|
||||
constructor(message: string, cause?: unknown) {
|
||||
super(message);
|
||||
this.name = "OpenBrainError";
|
||||
if (cause !== undefined) {
|
||||
(this as Error & { cause?: unknown }).cause = cause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenBrainConfigError extends OpenBrainError {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "OpenBrainConfigError";
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenBrainHttpError extends OpenBrainError {
|
||||
readonly status: number;
|
||||
readonly endpoint: string;
|
||||
readonly responseBody: string | undefined;
|
||||
|
||||
constructor(params: { endpoint: string; status: number; responseBody: string | undefined }) {
|
||||
super(`OpenBrain request failed (${params.status}) for ${params.endpoint}`);
|
||||
this.name = "OpenBrainHttpError";
|
||||
this.status = params.status;
|
||||
this.endpoint = params.endpoint;
|
||||
this.responseBody = params.responseBody;
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenBrainRequestError extends OpenBrainError {
|
||||
readonly endpoint: string;
|
||||
|
||||
constructor(params: { endpoint: string; cause: unknown }) {
|
||||
super(`OpenBrain request failed for ${params.endpoint}`, params.cause);
|
||||
this.name = "OpenBrainRequestError";
|
||||
this.endpoint = params.endpoint;
|
||||
}
|
||||
}
|
||||
31
plugins/openclaw-context/src/index.ts
Normal file
31
plugins/openclaw-context/src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
OPENBRAIN_CONTEXT_ENGINE_ID,
|
||||
OPENBRAIN_PLUGIN_ID,
|
||||
OPENBRAIN_PLUGIN_VERSION,
|
||||
} from "./constants.js";
|
||||
import { OpenBrainContextEngine } from "./engine.js";
|
||||
import type { OpenClawPluginApi } from "./openclaw-types.js";
|
||||
|
||||
export { OPENBRAIN_CONTEXT_ENGINE_ID } from "./constants.js";
|
||||
export { OpenBrainContextEngine } from "./engine.js";
|
||||
export { OpenBrainConfigError, OpenBrainHttpError, OpenBrainRequestError } from "./errors.js";
|
||||
export { OpenBrainClient } from "./openbrain-client.js";
|
||||
export type { OpenBrainContextEngineConfig } from "./engine.js";
|
||||
export type { OpenClawPluginApi } from "./openclaw-types.js";
|
||||
|
||||
export function register(api: OpenClawPluginApi): void {
|
||||
api.registerContextEngine(OPENBRAIN_CONTEXT_ENGINE_ID, () => {
|
||||
const deps = api.logger !== undefined ? { logger: api.logger } : undefined;
|
||||
return new OpenBrainContextEngine(api.pluginConfig, deps);
|
||||
});
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
id: OPENBRAIN_PLUGIN_ID,
|
||||
name: "OpenBrain Context Engine",
|
||||
version: OPENBRAIN_PLUGIN_VERSION,
|
||||
kind: "context-engine",
|
||||
register,
|
||||
};
|
||||
|
||||
export default plugin;
|
||||
333
plugins/openclaw-context/src/openbrain-client.ts
Normal file
333
plugins/openclaw-context/src/openbrain-client.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
import { OpenBrainConfigError, OpenBrainHttpError, OpenBrainRequestError } from "./errors.js";
|
||||
|
||||
export type OpenBrainThoughtMetadata = Record<string, unknown> & {
|
||||
sessionId?: string;
|
||||
turn?: number;
|
||||
role?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export type OpenBrainThought = {
|
||||
id: string;
|
||||
content: string;
|
||||
source: string;
|
||||
metadata: OpenBrainThoughtMetadata | undefined;
|
||||
createdAt: string | undefined;
|
||||
updatedAt: string | undefined;
|
||||
score: number | undefined;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type OpenBrainThoughtInput = {
|
||||
content: string;
|
||||
source: string;
|
||||
metadata?: OpenBrainThoughtMetadata;
|
||||
};
|
||||
|
||||
export type OpenBrainSearchInput = {
|
||||
query: string;
|
||||
limit: number;
|
||||
source?: string;
|
||||
};
|
||||
|
||||
export type OpenBrainClientOptions = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
fetchImpl?: typeof fetch;
|
||||
};
|
||||
|
||||
export interface OpenBrainClientLike {
|
||||
createThought(input: OpenBrainThoughtInput): Promise<OpenBrainThought>;
|
||||
search(input: OpenBrainSearchInput): Promise<OpenBrainThought[]>;
|
||||
listRecent(input: { limit: number; source?: string }): Promise<OpenBrainThought[]>;
|
||||
updateThought(
|
||||
id: string,
|
||||
payload: { content?: string; metadata?: OpenBrainThoughtMetadata },
|
||||
): Promise<OpenBrainThought>;
|
||||
deleteThought(id: string): Promise<void>;
|
||||
deleteThoughts(params: { source?: string; metadataId?: string }): Promise<void>;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null;
|
||||
}
|
||||
|
||||
function readString(record: Record<string, unknown>, key: string): string | undefined {
|
||||
const value = record[key];
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function readNumber(record: Record<string, unknown>, key: string): number | undefined {
|
||||
const value = record[key];
|
||||
return typeof value === "number" ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
const normalized = baseUrl.trim().replace(/\/+$/, "");
|
||||
if (normalized.length === 0) {
|
||||
throw new OpenBrainConfigError("Missing required OpenBrain config: baseUrl");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeApiKey(apiKey: string): string {
|
||||
const normalized = apiKey.trim();
|
||||
if (normalized.length === 0) {
|
||||
throw new OpenBrainConfigError("Missing required OpenBrain config: apiKey");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeHeaders(headers: unknown): Record<string, string> {
|
||||
if (headers === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (Array.isArray(headers)) {
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const pair of headers) {
|
||||
if (!Array.isArray(pair) || pair.length < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = pair[0];
|
||||
const value = pair[1];
|
||||
if (typeof key !== "string" || typeof value !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
normalized[key] = value;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [key, value] of headers.entries()) {
|
||||
normalized[key] = value;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
if (!isRecord(headers)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const normalized: Record<string, string> = {};
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (typeof value === "string") {
|
||||
normalized[key] = value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
normalized[key] = value.join(", ");
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function readResponseBody(response: Response): Promise<string | undefined> {
|
||||
try {
|
||||
const body = await response.text();
|
||||
return body.length > 0 ? body : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class OpenBrainClient implements OpenBrainClientLike {
|
||||
private readonly baseUrl: string;
|
||||
private readonly apiKey: string;
|
||||
private readonly fetchImpl: typeof fetch;
|
||||
|
||||
constructor(options: OpenBrainClientOptions) {
|
||||
this.baseUrl = normalizeBaseUrl(options.baseUrl);
|
||||
this.apiKey = normalizeApiKey(options.apiKey);
|
||||
this.fetchImpl = options.fetchImpl ?? fetch;
|
||||
}
|
||||
|
||||
async createThought(input: OpenBrainThoughtInput): Promise<OpenBrainThought> {
|
||||
const payload = await this.request<unknown>("/v1/thoughts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return this.extractThought(payload);
|
||||
}
|
||||
|
||||
async search(input: OpenBrainSearchInput): Promise<OpenBrainThought[]> {
|
||||
const payload = await this.request<unknown>("/v1/search", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
return this.extractThoughtArray(payload);
|
||||
}
|
||||
|
||||
async listRecent(input: { limit: number; source?: string }): Promise<OpenBrainThought[]> {
|
||||
const params = new URLSearchParams({
|
||||
limit: String(input.limit),
|
||||
});
|
||||
|
||||
if (input.source !== undefined && input.source.length > 0) {
|
||||
params.set("source", input.source);
|
||||
}
|
||||
|
||||
const payload = await this.request<unknown>(`/v1/thoughts/recent?${params.toString()}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return this.extractThoughtArray(payload);
|
||||
}
|
||||
|
||||
async updateThought(
|
||||
id: string,
|
||||
payload: { content?: string; metadata?: OpenBrainThoughtMetadata },
|
||||
): Promise<OpenBrainThought> {
|
||||
const responsePayload = await this.request<unknown>(`/v1/thoughts/${encodeURIComponent(id)}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
return this.extractThought(responsePayload);
|
||||
}
|
||||
|
||||
async deleteThought(id: string): Promise<void> {
|
||||
await this.request<unknown>(`/v1/thoughts/${encodeURIComponent(id)}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
async deleteThoughts(params: { source?: string; metadataId?: string }): Promise<void> {
|
||||
const query = new URLSearchParams();
|
||||
if (params.source !== undefined && params.source.length > 0) {
|
||||
query.set("source", params.source);
|
||||
}
|
||||
if (params.metadataId !== undefined && params.metadataId.length > 0) {
|
||||
query.set("metadata_id", params.metadataId);
|
||||
}
|
||||
|
||||
const suffix = query.size > 0 ? `?${query.toString()}` : "";
|
||||
await this.request<unknown>(`/v1/thoughts${suffix}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, init: RequestInit): Promise<T> {
|
||||
const headers = normalizeHeaders(init.headers);
|
||||
headers.Authorization = `Bearer ${this.apiKey}`;
|
||||
|
||||
if (init.body !== undefined && headers["Content-Type"] === undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await this.fetchImpl(url, {
|
||||
...init,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new OpenBrainRequestError({ endpoint, cause: error });
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new OpenBrainHttpError({
|
||||
endpoint,
|
||||
status: response.status,
|
||||
responseBody: await readResponseBody(response),
|
||||
});
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (!contentType.toLowerCase().includes("application/json")) {
|
||||
return undefined as T;
|
||||
}
|
||||
|
||||
return (await response.json()) as T;
|
||||
}
|
||||
|
||||
private extractThoughtArray(payload: unknown): OpenBrainThought[] {
|
||||
if (Array.isArray(payload)) {
|
||||
return payload.map((item) => this.normalizeThought(item));
|
||||
}
|
||||
|
||||
if (!isRecord(payload)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates = [payload.thoughts, payload.data, payload.results, payload.items];
|
||||
for (const candidate of candidates) {
|
||||
if (Array.isArray(candidate)) {
|
||||
return candidate.map((item) => this.normalizeThought(item));
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private extractThought(payload: unknown): OpenBrainThought {
|
||||
if (isRecord(payload)) {
|
||||
const nested = payload.thought;
|
||||
if (nested !== undefined) {
|
||||
return this.normalizeThought(nested);
|
||||
}
|
||||
|
||||
const data = payload.data;
|
||||
if (data !== undefined && !Array.isArray(data)) {
|
||||
return this.normalizeThought(data);
|
||||
}
|
||||
}
|
||||
|
||||
return this.normalizeThought(payload);
|
||||
}
|
||||
|
||||
private normalizeThought(value: unknown): OpenBrainThought {
|
||||
if (!isRecord(value)) {
|
||||
return {
|
||||
id: "",
|
||||
content: "",
|
||||
source: "",
|
||||
metadata: undefined,
|
||||
createdAt: undefined,
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const metadataValue = value.metadata;
|
||||
const metadata = isRecord(metadataValue)
|
||||
? ({ ...metadataValue } as OpenBrainThoughtMetadata)
|
||||
: undefined;
|
||||
|
||||
const id = readString(value, "id") ?? readString(value, "thought_id") ?? "";
|
||||
const content =
|
||||
readString(value, "content") ??
|
||||
readString(value, "text") ??
|
||||
(value.content === undefined ? "" : String(value.content));
|
||||
const source = readString(value, "source") ?? "";
|
||||
|
||||
const createdAt = readString(value, "createdAt") ?? readString(value, "created_at");
|
||||
const updatedAt = readString(value, "updatedAt") ?? readString(value, "updated_at");
|
||||
const score = readNumber(value, "score");
|
||||
|
||||
return {
|
||||
...value,
|
||||
id,
|
||||
content,
|
||||
source,
|
||||
metadata,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
score,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { normalizeApiKey, normalizeBaseUrl };
|
||||
128
plugins/openclaw-context/src/openclaw-types.ts
Normal file
128
plugins/openclaw-context/src/openclaw-types.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
export type AgentMessageRole = "user" | "assistant" | "tool" | "system" | string;
|
||||
|
||||
export type AgentMessage = {
|
||||
role: AgentMessageRole;
|
||||
content: unknown;
|
||||
timestamp?: number;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type AssembleResult = {
|
||||
messages: AgentMessage[];
|
||||
estimatedTokens: number;
|
||||
systemPromptAddition?: string;
|
||||
};
|
||||
|
||||
export type CompactResult = {
|
||||
ok: boolean;
|
||||
compacted: boolean;
|
||||
reason?: string;
|
||||
result?: {
|
||||
summary?: string;
|
||||
firstKeptEntryId?: string;
|
||||
tokensBefore: number;
|
||||
tokensAfter?: number;
|
||||
details?: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export type IngestResult = {
|
||||
ingested: boolean;
|
||||
};
|
||||
|
||||
export type IngestBatchResult = {
|
||||
ingestedCount: number;
|
||||
};
|
||||
|
||||
export type BootstrapResult = {
|
||||
bootstrapped: boolean;
|
||||
importedMessages?: number;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export type ContextEngineInfo = {
|
||||
id: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
ownsCompaction?: boolean;
|
||||
};
|
||||
|
||||
export type SubagentSpawnPreparation = {
|
||||
rollback: () => void | Promise<void>;
|
||||
};
|
||||
|
||||
export type SubagentEndReason = "deleted" | "completed" | "swept" | "released";
|
||||
|
||||
export interface ContextEngine {
|
||||
readonly info: ContextEngineInfo;
|
||||
|
||||
bootstrap?(params: { sessionId: string; sessionFile: string }): Promise<BootstrapResult>;
|
||||
|
||||
ingest(params: {
|
||||
sessionId: string;
|
||||
message: AgentMessage;
|
||||
isHeartbeat?: boolean;
|
||||
}): Promise<IngestResult>;
|
||||
|
||||
ingestBatch?(params: {
|
||||
sessionId: string;
|
||||
messages: AgentMessage[];
|
||||
isHeartbeat?: boolean;
|
||||
}): Promise<IngestBatchResult>;
|
||||
|
||||
afterTurn?(params: {
|
||||
sessionId: string;
|
||||
sessionFile: string;
|
||||
messages: AgentMessage[];
|
||||
prePromptMessageCount: number;
|
||||
autoCompactionSummary?: string;
|
||||
isHeartbeat?: boolean;
|
||||
tokenBudget?: number;
|
||||
legacyCompactionParams?: Record<string, unknown>;
|
||||
}): Promise<void>;
|
||||
|
||||
assemble(params: {
|
||||
sessionId: string;
|
||||
messages: AgentMessage[];
|
||||
tokenBudget?: number;
|
||||
}): Promise<AssembleResult>;
|
||||
|
||||
compact(params: {
|
||||
sessionId: string;
|
||||
sessionFile: string;
|
||||
tokenBudget?: number;
|
||||
force?: boolean;
|
||||
currentTokenCount?: number;
|
||||
compactionTarget?: "budget" | "threshold";
|
||||
customInstructions?: string;
|
||||
legacyParams?: Record<string, unknown>;
|
||||
}): Promise<CompactResult>;
|
||||
|
||||
prepareSubagentSpawn?(params: {
|
||||
parentSessionKey: string;
|
||||
childSessionKey: string;
|
||||
ttlMs?: number;
|
||||
}): Promise<SubagentSpawnPreparation | undefined>;
|
||||
|
||||
onSubagentEnded?(params: {
|
||||
childSessionKey: string;
|
||||
reason: SubagentEndReason;
|
||||
}): Promise<void>;
|
||||
|
||||
dispose?(): Promise<void>;
|
||||
}
|
||||
|
||||
export type ContextEngineFactory = () => ContextEngine | Promise<ContextEngine>;
|
||||
|
||||
export type PluginLogger = {
|
||||
debug?: (...args: unknown[]) => void;
|
||||
info?: (...args: unknown[]) => void;
|
||||
warn?: (...args: unknown[]) => void;
|
||||
error?: (...args: unknown[]) => void;
|
||||
};
|
||||
|
||||
export type OpenClawPluginApi = {
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
logger?: PluginLogger;
|
||||
registerContextEngine: (id: string, factory: ContextEngineFactory) => void;
|
||||
};
|
||||
414
plugins/openclaw-context/tests/engine.test.ts
Normal file
414
plugins/openclaw-context/tests/engine.test.ts
Normal file
@@ -0,0 +1,414 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OpenBrainConfigError } from "../src/errors.js";
|
||||
import { OpenBrainContextEngine } from "../src/engine.js";
|
||||
import type { AgentMessage } from "../src/openclaw-types.js";
|
||||
import type {
|
||||
OpenBrainClientLike,
|
||||
OpenBrainThought,
|
||||
OpenBrainThoughtInput,
|
||||
} from "../src/openbrain-client.js";
|
||||
|
||||
function makeThought(
|
||||
id: string,
|
||||
content: string,
|
||||
sessionId: string,
|
||||
role: string,
|
||||
createdAt: string,
|
||||
): OpenBrainThought {
|
||||
return {
|
||||
id,
|
||||
content,
|
||||
source: `openclaw:${sessionId}`,
|
||||
metadata: {
|
||||
sessionId,
|
||||
role,
|
||||
type: "message",
|
||||
},
|
||||
createdAt,
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMockClient(): OpenBrainClientLike {
|
||||
return {
|
||||
createThought: vi.fn(async (input: OpenBrainThoughtInput) => ({
|
||||
id: `thought-${Math.random().toString(36).slice(2)}`,
|
||||
content: input.content,
|
||||
source: input.source,
|
||||
metadata: input.metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
})),
|
||||
search: vi.fn(async () => []),
|
||||
listRecent: vi.fn(async () => []),
|
||||
updateThought: vi.fn(async (id, payload) => ({
|
||||
id,
|
||||
content: payload.content ?? "",
|
||||
source: "openclaw:session",
|
||||
metadata: payload.metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
})),
|
||||
deleteThought: vi.fn(async () => undefined),
|
||||
deleteThoughts: vi.fn(async () => undefined),
|
||||
};
|
||||
}
|
||||
|
||||
const sessionId = "session-main";
|
||||
|
||||
const userMessage: AgentMessage = {
|
||||
role: "user",
|
||||
content: "What did we decide yesterday?",
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
describe("OpenBrainContextEngine", () => {
|
||||
it("throws OpenBrainConfigError at bootstrap when baseUrl/apiKey are missing", async () => {
|
||||
const engine = new OpenBrainContextEngine({});
|
||||
|
||||
await expect(
|
||||
engine.bootstrap({
|
||||
sessionId,
|
||||
sessionFile: "/tmp/session.json",
|
||||
}),
|
||||
).rejects.toBeInstanceOf(OpenBrainConfigError);
|
||||
});
|
||||
|
||||
it("ingests messages with session metadata", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
source: "openclaw",
|
||||
},
|
||||
{
|
||||
createClient: () => mockClient,
|
||||
},
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.ingest({ sessionId, message: userMessage });
|
||||
|
||||
expect(result.ingested).toBe(true);
|
||||
expect(mockClient.createThought).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:session-main",
|
||||
metadata: expect.objectContaining({
|
||||
sessionId,
|
||||
role: "user",
|
||||
type: "message",
|
||||
turn: 0,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("ingests batches and returns ingested count", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.ingestBatch({
|
||||
sessionId,
|
||||
messages: [
|
||||
{ role: "user", content: "one", timestamp: 1 },
|
||||
{ role: "assistant", content: "two", timestamp: 2 },
|
||||
],
|
||||
});
|
||||
|
||||
expect(result.ingestedCount).toBe(2);
|
||||
expect(mockClient.createThought).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("ingests batches in parallel chunks of five", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
let inFlight = 0;
|
||||
let maxInFlight = 0;
|
||||
let createdCount = 0;
|
||||
|
||||
vi.mocked(mockClient.createThought).mockImplementation(async (input: OpenBrainThoughtInput) => {
|
||||
inFlight += 1;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 20);
|
||||
});
|
||||
inFlight -= 1;
|
||||
createdCount += 1;
|
||||
return {
|
||||
id: `thought-${createdCount}`,
|
||||
content: input.content,
|
||||
source: input.source,
|
||||
metadata: input.metadata,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.ingestBatch({
|
||||
sessionId,
|
||||
messages: Array.from({ length: 10 }, (_, index) => ({
|
||||
role: index % 2 === 0 ? "user" : "assistant",
|
||||
content: `message-${index + 1}`,
|
||||
timestamp: index + 1,
|
||||
})),
|
||||
});
|
||||
|
||||
expect(result.ingestedCount).toBe(10);
|
||||
expect(maxInFlight).toBe(5);
|
||||
expect(mockClient.createThought).toHaveBeenCalledTimes(10);
|
||||
});
|
||||
|
||||
it("assembles context from recent + semantic search, deduped and budget-aware", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue([
|
||||
makeThought("t1", "recent user context", sessionId, "user", "2026-03-06T12:00:00.000Z"),
|
||||
makeThought(
|
||||
"t2",
|
||||
"recent assistant context",
|
||||
sessionId,
|
||||
"assistant",
|
||||
"2026-03-06T12:01:00.000Z",
|
||||
),
|
||||
]);
|
||||
vi.mocked(mockClient.search).mockResolvedValue([
|
||||
makeThought(
|
||||
"t2",
|
||||
"recent assistant context",
|
||||
sessionId,
|
||||
"assistant",
|
||||
"2026-03-06T12:01:00.000Z",
|
||||
),
|
||||
makeThought("t3", "semantic match", sessionId, "assistant", "2026-03-06T12:02:00.000Z"),
|
||||
]);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
recentMessages: 10,
|
||||
semanticSearchLimit: 10,
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
|
||||
const result = await engine.assemble({
|
||||
sessionId,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "Find the semantic context",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tokenBudget: 40,
|
||||
});
|
||||
|
||||
expect(mockClient.search).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: "Find the semantic context",
|
||||
limit: 10,
|
||||
}),
|
||||
);
|
||||
expect(result.estimatedTokens).toBeLessThanOrEqual(40);
|
||||
expect(result.messages.map((message) => String(message.content))).toEqual([
|
||||
"recent user context",
|
||||
"recent assistant context",
|
||||
"semantic match",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compact archives a summary thought and deletes summarized inputs", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue(
|
||||
Array.from({ length: 12 }, (_, index) => {
|
||||
return makeThought(
|
||||
`t${index + 1}`,
|
||||
`message ${index + 1}`,
|
||||
sessionId,
|
||||
index % 2 === 0 ? "user" : "assistant",
|
||||
`2026-03-06T12:${String(index).padStart(2, "0")}:00.000Z`,
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
|
||||
const result = await engine.compact({
|
||||
sessionId,
|
||||
sessionFile: "/tmp/session.json",
|
||||
tokenBudget: 128,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.compacted).toBe(true);
|
||||
expect(mockClient.createThought).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:session-main",
|
||||
metadata: expect.objectContaining({
|
||||
sessionId,
|
||||
type: "summary",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const deletedIds = vi
|
||||
.mocked(mockClient.deleteThought)
|
||||
.mock.calls.map(([id]) => id)
|
||||
.sort((left, right) => left.localeCompare(right));
|
||||
expect(deletedIds).toEqual([
|
||||
"t10",
|
||||
"t11",
|
||||
"t12",
|
||||
"t3",
|
||||
"t4",
|
||||
"t5",
|
||||
"t6",
|
||||
"t7",
|
||||
"t8",
|
||||
"t9",
|
||||
]);
|
||||
});
|
||||
|
||||
it("stops trimming once the newest message exceeds budget", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
const oversizedNewest = "z".repeat(400);
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue([
|
||||
makeThought("t1", "small older message", sessionId, "assistant", "2026-03-06T12:00:00.000Z"),
|
||||
makeThought("t2", oversizedNewest, sessionId, "assistant", "2026-03-06T12:01:00.000Z"),
|
||||
]);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
const result = await engine.assemble({
|
||||
sessionId,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: "query",
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
],
|
||||
tokenBudget: 12,
|
||||
});
|
||||
|
||||
expect(result.messages.map((message) => String(message.content))).toEqual([oversizedNewest]);
|
||||
});
|
||||
|
||||
it("prepares subagent spawn and rollback deletes seeded context", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent).mockResolvedValue([
|
||||
makeThought("t1", "parent context", sessionId, "assistant", "2026-03-06T12:00:00.000Z"),
|
||||
]);
|
||||
vi.mocked(mockClient.createThought).mockResolvedValue({
|
||||
id: "seed-thought",
|
||||
content: "seed",
|
||||
source: "openclaw:child",
|
||||
metadata: undefined,
|
||||
createdAt: "2026-03-06T12:01:00.000Z",
|
||||
updatedAt: undefined,
|
||||
score: undefined,
|
||||
});
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
|
||||
const prep = await engine.prepareSubagentSpawn({
|
||||
parentSessionKey: sessionId,
|
||||
childSessionKey: "child-session",
|
||||
});
|
||||
|
||||
expect(prep).toBeDefined();
|
||||
expect(mockClient.createThought).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:child-session",
|
||||
}),
|
||||
);
|
||||
|
||||
await prep?.rollback();
|
||||
expect(mockClient.deleteThought).toHaveBeenCalledWith("seed-thought");
|
||||
});
|
||||
|
||||
it("stores child outcome back into parent on subagent end", async () => {
|
||||
const mockClient = makeMockClient();
|
||||
vi.mocked(mockClient.listRecent)
|
||||
.mockResolvedValueOnce([
|
||||
makeThought("p1", "parent context", sessionId, "assistant", "2026-03-06T12:00:00.000Z"),
|
||||
])
|
||||
.mockResolvedValueOnce([
|
||||
makeThought("c1", "child result detail", "child-session", "assistant", "2026-03-06T12:05:00.000Z"),
|
||||
]);
|
||||
|
||||
const engine = new OpenBrainContextEngine(
|
||||
{
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
{ createClient: () => mockClient },
|
||||
);
|
||||
|
||||
await engine.bootstrap({ sessionId, sessionFile: "/tmp/session.json" });
|
||||
await engine.prepareSubagentSpawn({
|
||||
parentSessionKey: sessionId,
|
||||
childSessionKey: "child-session",
|
||||
});
|
||||
|
||||
await engine.onSubagentEnded({
|
||||
childSessionKey: "child-session",
|
||||
reason: "completed",
|
||||
});
|
||||
|
||||
expect(mockClient.createThought).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
source: "openclaw:session-main",
|
||||
metadata: expect.objectContaining({
|
||||
type: "subagent-result",
|
||||
sessionId,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
81
plugins/openclaw-context/tests/openbrain-client.test.ts
Normal file
81
plugins/openclaw-context/tests/openbrain-client.test.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OpenBrainConfigError, OpenBrainHttpError } from "../src/errors.js";
|
||||
import { OpenBrainClient } from "../src/openbrain-client.js";
|
||||
|
||||
function jsonResponse(body: unknown, init?: ResponseInit): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: init?.status ?? 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("OpenBrainClient", () => {
|
||||
it("sends bearer auth and normalized URL for createThought", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
jsonResponse({
|
||||
id: "thought-1",
|
||||
content: "hello",
|
||||
source: "openclaw:main",
|
||||
}),
|
||||
);
|
||||
|
||||
const client = new OpenBrainClient({
|
||||
baseUrl: "https://brain.example.com/",
|
||||
apiKey: "secret",
|
||||
fetchImpl: fetchMock as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
await client.createThought({
|
||||
content: "hello",
|
||||
source: "openclaw:main",
|
||||
metadata: { sessionId: "session-1" },
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
const firstCall = fetchMock.mock.calls[0];
|
||||
expect(firstCall).toBeDefined();
|
||||
if (firstCall === undefined) {
|
||||
throw new Error("Expected fetch call arguments");
|
||||
}
|
||||
const [url, init] = firstCall as unknown as [string, RequestInit];
|
||||
expect(url).toBe("https://brain.example.com/v1/thoughts");
|
||||
expect(init.method).toBe("POST");
|
||||
expect(init.headers).toMatchObject({
|
||||
Authorization: "Bearer secret",
|
||||
"Content-Type": "application/json",
|
||||
});
|
||||
});
|
||||
|
||||
it("throws OpenBrainHttpError on non-2xx responses", async () => {
|
||||
const fetchMock = vi.fn(async () =>
|
||||
jsonResponse({ error: "unauthorized" }, { status: 401 }),
|
||||
);
|
||||
|
||||
const client = new OpenBrainClient({
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
fetchImpl: fetchMock as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
await expect(client.listRecent({ limit: 5, source: "openclaw:main" })).rejects.toBeInstanceOf(
|
||||
OpenBrainHttpError,
|
||||
);
|
||||
|
||||
await expect(client.listRecent({ limit: 5, source: "openclaw:main" })).rejects.toMatchObject({
|
||||
status: 401,
|
||||
});
|
||||
});
|
||||
|
||||
it("throws OpenBrainConfigError when initialized without baseUrl or apiKey", () => {
|
||||
expect(
|
||||
() => new OpenBrainClient({ baseUrl: "", apiKey: "secret", fetchImpl: fetch }),
|
||||
).toThrow(OpenBrainConfigError);
|
||||
expect(
|
||||
() => new OpenBrainClient({ baseUrl: "https://brain.example.com", apiKey: "", fetchImpl: fetch }),
|
||||
).toThrow(OpenBrainConfigError);
|
||||
});
|
||||
});
|
||||
30
plugins/openclaw-context/tests/register.test.ts
Normal file
30
plugins/openclaw-context/tests/register.test.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { OPENBRAIN_CONTEXT_ENGINE_ID, register } from "../src/index.js";
|
||||
|
||||
describe("plugin register()", () => {
|
||||
it("registers the openbrain context engine factory", async () => {
|
||||
const registerContextEngine = vi.fn();
|
||||
|
||||
register({
|
||||
registerContextEngine,
|
||||
pluginConfig: {
|
||||
baseUrl: "https://brain.example.com",
|
||||
apiKey: "secret",
|
||||
},
|
||||
logger: {
|
||||
debug: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(registerContextEngine).toHaveBeenCalledTimes(1);
|
||||
const [id, factory] = registerContextEngine.mock.calls[0] as [string, () => Promise<unknown> | unknown];
|
||||
expect(id).toBe(OPENBRAIN_CONTEXT_ENGINE_ID);
|
||||
|
||||
const engine = await factory();
|
||||
expect(engine).toHaveProperty("info.id", OPENBRAIN_CONTEXT_ENGINE_ID);
|
||||
});
|
||||
});
|
||||
9
plugins/openclaw-context/tests/smoke.test.ts
Normal file
9
plugins/openclaw-context/tests/smoke.test.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { OPENBRAIN_CONTEXT_ENGINE_ID } from "../src/index.js";
|
||||
|
||||
describe("project scaffold", () => {
|
||||
it("exports openbrain context engine id", () => {
|
||||
expect(OPENBRAIN_CONTEXT_ENGINE_ID).toBe("openbrain");
|
||||
});
|
||||
});
|
||||
24
plugins/openclaw-context/tsconfig.json
Normal file
24
plugins/openclaw-context/tsconfig.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"isolatedModules": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"types": ["node", "vitest/globals"],
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
508
pnpm-lock.yaml
generated
508
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
||||
devDependencies:
|
||||
'@changesets/cli':
|
||||
specifier: ^2
|
||||
version: 2.30.0
|
||||
version: 2.30.0(@types/node@22.19.15)
|
||||
prettier:
|
||||
specifier: ^3
|
||||
version: 3.8.1
|
||||
@@ -41,7 +41,7 @@ importers:
|
||||
devDependencies:
|
||||
vitest:
|
||||
specifier: ^3
|
||||
version: 3.2.4
|
||||
version: 3.2.4(@types/node@22.19.15)
|
||||
|
||||
packages/types:
|
||||
devDependencies:
|
||||
@@ -49,6 +49,22 @@ importers:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
|
||||
plugins/openclaw-context:
|
||||
dependencies:
|
||||
'@mosaic/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^22
|
||||
version: 22.19.15
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: ^2
|
||||
version: 2.1.9(@types/node@22.19.15)
|
||||
|
||||
packages:
|
||||
|
||||
'@babel/runtime@7.28.6':
|
||||
@@ -110,102 +126,204 @@ packages:
|
||||
'@changesets/write@0.4.0':
|
||||
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.21.5':
|
||||
resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.21.5':
|
||||
resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.21.5':
|
||||
resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.21.5':
|
||||
resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.21.5':
|
||||
resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.21.5':
|
||||
resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.21.5':
|
||||
resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.21.5':
|
||||
resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.21.5':
|
||||
resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.21.5':
|
||||
resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.21.5':
|
||||
resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -218,6 +336,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.21.5':
|
||||
resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -230,6 +354,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.21.5':
|
||||
resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -242,24 +372,48 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@esbuild/sunos-x64@0.21.5':
|
||||
resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.21.5':
|
||||
resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.21.5':
|
||||
resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.21.5':
|
||||
resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==}
|
||||
engines: {node: '>=12'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -452,9 +606,26 @@ packages:
|
||||
'@types/node@12.20.55':
|
||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==}
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==}
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==}
|
||||
|
||||
'@vitest/mocker@2.1.9':
|
||||
resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^5.0.0
|
||||
peerDependenciesMeta:
|
||||
msw:
|
||||
optional: true
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@3.2.4':
|
||||
resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==}
|
||||
peerDependencies:
|
||||
@@ -466,18 +637,33 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==}
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==}
|
||||
|
||||
'@vitest/runner@2.1.9':
|
||||
resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==}
|
||||
|
||||
'@vitest/runner@3.2.4':
|
||||
resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==}
|
||||
|
||||
'@vitest/snapshot@2.1.9':
|
||||
resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==}
|
||||
|
||||
'@vitest/snapshot@3.2.4':
|
||||
resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==}
|
||||
|
||||
'@vitest/spy@2.1.9':
|
||||
resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==}
|
||||
|
||||
'@vitest/spy@3.2.4':
|
||||
resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==}
|
||||
|
||||
'@vitest/utils@2.1.9':
|
||||
resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==}
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==}
|
||||
|
||||
@@ -648,6 +834,11 @@ packages:
|
||||
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
esbuild@0.21.5:
|
||||
resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==}
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.3:
|
||||
resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -989,6 +1180,9 @@ packages:
|
||||
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
pathe@1.1.2:
|
||||
resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
@@ -1193,10 +1387,18 @@ packages:
|
||||
resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
|
||||
tinyrainbow@1.2.0:
|
||||
resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyrainbow@2.0.0:
|
||||
resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@3.0.2:
|
||||
resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
||||
tinyspy@4.0.4:
|
||||
resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -1252,6 +1454,9 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
universalify@0.1.2:
|
||||
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
|
||||
engines: {node: '>= 4.0.0'}
|
||||
@@ -1264,11 +1469,47 @@ packages:
|
||||
resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
vite-node@2.1.9:
|
||||
resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite-node@3.2.4:
|
||||
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
hasBin: true
|
||||
|
||||
vite@5.4.21:
|
||||
resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
less: '*'
|
||||
lightningcss: ^1.21.0
|
||||
sass: '*'
|
||||
sass-embedded: '*'
|
||||
stylus: '*'
|
||||
sugarss: '*'
|
||||
terser: ^5.4.0
|
||||
peerDependenciesMeta:
|
||||
'@types/node':
|
||||
optional: true
|
||||
less:
|
||||
optional: true
|
||||
lightningcss:
|
||||
optional: true
|
||||
sass:
|
||||
optional: true
|
||||
sass-embedded:
|
||||
optional: true
|
||||
stylus:
|
||||
optional: true
|
||||
sugarss:
|
||||
optional: true
|
||||
terser:
|
||||
optional: true
|
||||
|
||||
vite@7.3.1:
|
||||
resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1309,6 +1550,31 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@2.1.9:
|
||||
resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@types/node': ^18.0.0 || >=20.0.0
|
||||
'@vitest/browser': 2.1.9
|
||||
'@vitest/ui': 2.1.9
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
peerDependenciesMeta:
|
||||
'@edge-runtime/vm':
|
||||
optional: true
|
||||
'@types/node':
|
||||
optional: true
|
||||
'@vitest/browser':
|
||||
optional: true
|
||||
'@vitest/ui':
|
||||
optional: true
|
||||
happy-dom:
|
||||
optional: true
|
||||
jsdom:
|
||||
optional: true
|
||||
|
||||
vitest@3.2.4:
|
||||
resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==}
|
||||
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
|
||||
@@ -1391,7 +1657,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@changesets/types': 6.1.0
|
||||
|
||||
'@changesets/cli@2.30.0':
|
||||
'@changesets/cli@2.30.0(@types/node@22.19.15)':
|
||||
dependencies:
|
||||
'@changesets/apply-release-plan': 7.1.0
|
||||
'@changesets/assemble-release-plan': 6.0.9
|
||||
@@ -1407,7 +1673,7 @@ snapshots:
|
||||
'@changesets/should-skip-package': 0.1.2
|
||||
'@changesets/types': 6.1.0
|
||||
'@changesets/write': 0.4.0
|
||||
'@inquirer/external-editor': 1.0.3
|
||||
'@inquirer/external-editor': 1.0.3(@types/node@22.19.15)
|
||||
'@manypkg/get-packages': 1.1.3
|
||||
ansi-colors: 4.1.3
|
||||
enquirer: 2.4.1
|
||||
@@ -1505,81 +1771,150 @@ snapshots:
|
||||
human-id: 4.1.3
|
||||
prettier: 2.8.8
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openharmony-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.3':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.3':
|
||||
optional: true
|
||||
|
||||
@@ -1587,10 +1922,12 @@ snapshots:
|
||||
dependencies:
|
||||
hono: 4.12.5
|
||||
|
||||
'@inquirer/external-editor@1.0.3':
|
||||
'@inquirer/external-editor@1.0.3(@types/node@22.19.15)':
|
||||
dependencies:
|
||||
chardet: 2.1.1
|
||||
iconv-lite: 0.7.2
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
|
||||
'@ioredis/commands@1.5.1': {}
|
||||
|
||||
@@ -1732,6 +2069,17 @@ snapshots:
|
||||
|
||||
'@types/node@12.20.55': {}
|
||||
|
||||
'@types/node@22.19.15':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@vitest/expect@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/expect@3.2.4':
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
@@ -1740,34 +2088,67 @@ snapshots:
|
||||
chai: 5.3.3
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1)':
|
||||
'@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.15))':
|
||||
dependencies:
|
||||
'@vitest/spy': 2.1.9
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
|
||||
'@vitest/mocker@3.2.4(vite@7.3.1(@types/node@22.19.15))':
|
||||
dependencies:
|
||||
'@vitest/spy': 3.2.4
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 0.30.21
|
||||
optionalDependencies:
|
||||
vite: 7.3.1
|
||||
vite: 7.3.1(@types/node@22.19.15)
|
||||
|
||||
'@vitest/pretty-format@2.1.9':
|
||||
dependencies:
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/pretty-format@3.2.4':
|
||||
dependencies:
|
||||
tinyrainbow: 2.0.0
|
||||
|
||||
'@vitest/runner@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/utils': 2.1.9
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/runner@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/utils': 3.2.4
|
||||
pathe: 2.0.3
|
||||
strip-literal: 3.1.0
|
||||
|
||||
'@vitest/snapshot@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
magic-string: 0.30.21
|
||||
pathe: 1.1.2
|
||||
|
||||
'@vitest/snapshot@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
magic-string: 0.30.21
|
||||
pathe: 2.0.3
|
||||
|
||||
'@vitest/spy@2.1.9':
|
||||
dependencies:
|
||||
tinyspy: 3.0.2
|
||||
|
||||
'@vitest/spy@3.2.4':
|
||||
dependencies:
|
||||
tinyspy: 4.0.4
|
||||
|
||||
'@vitest/utils@2.1.9':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
loupe: 3.2.1
|
||||
tinyrainbow: 1.2.0
|
||||
|
||||
'@vitest/utils@3.2.4':
|
||||
dependencies:
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
@@ -1916,6 +2297,32 @@ snapshots:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
||||
esbuild@0.21.5:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.21.5
|
||||
'@esbuild/android-arm': 0.21.5
|
||||
'@esbuild/android-arm64': 0.21.5
|
||||
'@esbuild/android-x64': 0.21.5
|
||||
'@esbuild/darwin-arm64': 0.21.5
|
||||
'@esbuild/darwin-x64': 0.21.5
|
||||
'@esbuild/freebsd-arm64': 0.21.5
|
||||
'@esbuild/freebsd-x64': 0.21.5
|
||||
'@esbuild/linux-arm': 0.21.5
|
||||
'@esbuild/linux-arm64': 0.21.5
|
||||
'@esbuild/linux-ia32': 0.21.5
|
||||
'@esbuild/linux-loong64': 0.21.5
|
||||
'@esbuild/linux-mips64el': 0.21.5
|
||||
'@esbuild/linux-ppc64': 0.21.5
|
||||
'@esbuild/linux-riscv64': 0.21.5
|
||||
'@esbuild/linux-s390x': 0.21.5
|
||||
'@esbuild/linux-x64': 0.21.5
|
||||
'@esbuild/netbsd-x64': 0.21.5
|
||||
'@esbuild/openbsd-x64': 0.21.5
|
||||
'@esbuild/sunos-x64': 0.21.5
|
||||
'@esbuild/win32-arm64': 0.21.5
|
||||
'@esbuild/win32-ia32': 0.21.5
|
||||
'@esbuild/win32-x64': 0.21.5
|
||||
|
||||
esbuild@0.27.3:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.3
|
||||
@@ -2269,6 +2676,8 @@ snapshots:
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
||||
pathe@1.1.2: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
pathval@2.0.1: {}
|
||||
@@ -2490,8 +2899,12 @@ snapshots:
|
||||
|
||||
tinypool@1.1.1: {}
|
||||
|
||||
tinyrainbow@1.2.0: {}
|
||||
|
||||
tinyrainbow@2.0.0: {}
|
||||
|
||||
tinyspy@3.0.2: {}
|
||||
|
||||
tinyspy@4.0.4: {}
|
||||
|
||||
to-regex-range@5.0.1:
|
||||
@@ -2535,19 +2948,39 @@ snapshots:
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
universalify@0.1.2: {}
|
||||
|
||||
unpipe@1.0.0: {}
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
vite-node@3.2.4:
|
||||
vite-node@2.1.9(@types/node@22.19.15):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 1.1.2
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- less
|
||||
- lightningcss
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vite-node@3.2.4(@types/node@22.19.15):
|
||||
dependencies:
|
||||
cac: 6.7.14
|
||||
debug: 4.4.3
|
||||
es-module-lexer: 1.7.0
|
||||
pathe: 2.0.3
|
||||
vite: 7.3.1
|
||||
vite: 7.3.1(@types/node@22.19.15)
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- jiti
|
||||
@@ -2562,7 +2995,16 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite@7.3.1:
|
||||
vite@5.4.21(@types/node@22.19.15):
|
||||
dependencies:
|
||||
esbuild: 0.21.5
|
||||
postcss: 8.5.8
|
||||
rollup: 4.59.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
fsevents: 2.3.3
|
||||
|
||||
vite@7.3.1(@types/node@22.19.15):
|
||||
dependencies:
|
||||
esbuild: 0.27.3
|
||||
fdir: 6.5.0(picomatch@4.0.3)
|
||||
@@ -2571,13 +3013,49 @@ snapshots:
|
||||
rollup: 4.59.0
|
||||
tinyglobby: 0.2.15
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@3.2.4:
|
||||
vitest@2.1.9(@types/node@22.19.15):
|
||||
dependencies:
|
||||
'@vitest/expect': 2.1.9
|
||||
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.15))
|
||||
'@vitest/pretty-format': 2.1.9
|
||||
'@vitest/runner': 2.1.9
|
||||
'@vitest/snapshot': 2.1.9
|
||||
'@vitest/spy': 2.1.9
|
||||
'@vitest/utils': 2.1.9
|
||||
chai: 5.3.3
|
||||
debug: 4.4.3
|
||||
expect-type: 1.3.0
|
||||
magic-string: 0.30.21
|
||||
pathe: 1.1.2
|
||||
std-env: 3.10.0
|
||||
tinybench: 2.9.0
|
||||
tinyexec: 0.3.2
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 1.2.0
|
||||
vite: 5.4.21(@types/node@22.19.15)
|
||||
vite-node: 2.1.9(@types/node@22.19.15)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
transitivePeerDependencies:
|
||||
- less
|
||||
- lightningcss
|
||||
- msw
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- supports-color
|
||||
- terser
|
||||
|
||||
vitest@3.2.4(@types/node@22.19.15):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/expect': 3.2.4
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1)
|
||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@22.19.15))
|
||||
'@vitest/pretty-format': 3.2.4
|
||||
'@vitest/runner': 3.2.4
|
||||
'@vitest/snapshot': 3.2.4
|
||||
@@ -2595,9 +3073,11 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
tinypool: 1.1.1
|
||||
tinyrainbow: 2.0.0
|
||||
vite: 7.3.1
|
||||
vite-node: 3.2.4
|
||||
vite: 7.3.1(@types/node@22.19.15)
|
||||
vite-node: 3.2.4(@types/node@22.19.15)
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
'@types/node': 22.19.15
|
||||
transitivePeerDependencies:
|
||||
- jiti
|
||||
- less
|
||||
|
||||
Reference in New Issue
Block a user