@koi-language/koi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/QUICKSTART.md +89 -0
- package/README.md +545 -0
- package/examples/actions-demo.koi +177 -0
- package/examples/cache-test.koi +29 -0
- package/examples/calculator.koi +61 -0
- package/examples/clear-registry.js +33 -0
- package/examples/clear-registry.koi +30 -0
- package/examples/code-introspection-test.koi +149 -0
- package/examples/counter.koi +132 -0
- package/examples/delegation-test.koi +52 -0
- package/examples/directory-import-test.koi +84 -0
- package/examples/hello-world-claude.koi +52 -0
- package/examples/hello-world.koi +52 -0
- package/examples/hello.koi +24 -0
- package/examples/mcp-example.koi +70 -0
- package/examples/multi-event-handler-test.koi +144 -0
- package/examples/new-import-test.koi +89 -0
- package/examples/pipeline.koi +162 -0
- package/examples/registry-demo.koi +184 -0
- package/examples/registry-playbook-demo.koi +162 -0
- package/examples/registry-playbook-email-compositor-2.koi +140 -0
- package/examples/registry-playbook-email-compositor.koi +140 -0
- package/examples/sentiment.koi +90 -0
- package/examples/simple.koi +48 -0
- package/examples/skill-import-test.koi +76 -0
- package/examples/skills/advanced/index.koi +95 -0
- package/examples/skills/math-operations.koi +69 -0
- package/examples/skills/string-operations.koi +56 -0
- package/examples/task-chaining-demo.koi +244 -0
- package/examples/test-await.koi +22 -0
- package/examples/test-crypto-sha256.koi +196 -0
- package/examples/test-delegation.koi +41 -0
- package/examples/test-multi-team-routing.koi +258 -0
- package/examples/test-no-handler.koi +35 -0
- package/examples/test-npm-import.koi +67 -0
- package/examples/test-parse.koi +10 -0
- package/examples/test-peers-with-team.koi +59 -0
- package/examples/test-permissions-fail.koi +20 -0
- package/examples/test-permissions.koi +36 -0
- package/examples/test-simple-registry.koi +31 -0
- package/examples/test-typescript-import.koi +64 -0
- package/examples/test-uses-team-syntax.koi +25 -0
- package/examples/test-uses-team.koi +31 -0
- package/examples/utils/calculator.test.ts +144 -0
- package/examples/utils/calculator.ts +56 -0
- package/examples/utils/math-helpers.js +50 -0
- package/examples/utils/math-helpers.ts +55 -0
- package/examples/web-delegation-demo.koi +165 -0
- package/package.json +78 -0
- package/src/cli/koi.js +793 -0
- package/src/compiler/build-optimizer.js +447 -0
- package/src/compiler/cache-manager.js +274 -0
- package/src/compiler/import-resolver.js +369 -0
- package/src/compiler/parser.js +7542 -0
- package/src/compiler/transpiler.js +1105 -0
- package/src/compiler/typescript-transpiler.js +148 -0
- package/src/grammar/koi.pegjs +767 -0
- package/src/runtime/action-registry.js +172 -0
- package/src/runtime/actions/call-skill.js +45 -0
- package/src/runtime/actions/format.js +115 -0
- package/src/runtime/actions/print.js +42 -0
- package/src/runtime/actions/registry-delete.js +37 -0
- package/src/runtime/actions/registry-get.js +37 -0
- package/src/runtime/actions/registry-keys.js +33 -0
- package/src/runtime/actions/registry-search.js +34 -0
- package/src/runtime/actions/registry-set.js +50 -0
- package/src/runtime/actions/return.js +31 -0
- package/src/runtime/actions/send-message.js +58 -0
- package/src/runtime/actions/update-state.js +36 -0
- package/src/runtime/agent.js +1368 -0
- package/src/runtime/cli-logger.js +205 -0
- package/src/runtime/incremental-json-parser.js +201 -0
- package/src/runtime/index.js +33 -0
- package/src/runtime/llm-provider.js +1372 -0
- package/src/runtime/mcp-client.js +1171 -0
- package/src/runtime/planner.js +273 -0
- package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
- package/src/runtime/registry-backends/local.js +260 -0
- package/src/runtime/registry.js +162 -0
- package/src/runtime/role.js +14 -0
- package/src/runtime/router.js +395 -0
- package/src/runtime/runtime.js +113 -0
- package/src/runtime/skill-selector.js +173 -0
- package/src/runtime/skill.js +25 -0
- package/src/runtime/team.js +162 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic Planning System
|
|
3
|
+
*
|
|
4
|
+
* Allows agents to automatically decompose complex tasks into executable steps
|
|
5
|
+
* using LLM-based planning.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { LLMProvider } from './llm-provider.js';
|
|
9
|
+
import { actionRegistry } from './action-registry.js';
|
|
10
|
+
|
|
11
|
+
export class Planner {
|
|
12
|
+
constructor(config) {
|
|
13
|
+
this.name = config.name || 'DefaultPlanner';
|
|
14
|
+
this.llm = config.llm || { provider: 'openai', model: 'gpt-4o-mini', temperature: 0.3 };
|
|
15
|
+
this.maxSteps = config.maxSteps || 10;
|
|
16
|
+
this.allowReplanning = config.allowReplanning !== false;
|
|
17
|
+
this.llmProvider = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a plan for a given goal/task
|
|
22
|
+
* @param {string} goal - The goal to plan for
|
|
23
|
+
* @param {object} context - Execution context
|
|
24
|
+
* @param {Agent} agent - The agent that will execute the plan (for permission filtering)
|
|
25
|
+
*/
|
|
26
|
+
async createPlan(goal, context = {}, agent = null) {
|
|
27
|
+
console.log(`[Planner:${this.name}] 📋 Creating plan for: ${goal}`);
|
|
28
|
+
|
|
29
|
+
if (!this.llmProvider) {
|
|
30
|
+
this.llmProvider = new LLMProvider(this.llm);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const planningPrompt = this.buildPlanningPrompt(goal, context, agent);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const plan = await this.llmProvider.executePlanning(planningPrompt);
|
|
37
|
+
|
|
38
|
+
if (!plan || !plan.steps || !Array.isArray(plan.steps)) {
|
|
39
|
+
throw new Error('Invalid plan format: missing steps array');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`[Planner:${this.name}] ✓ Created plan with ${plan.steps.length} steps`);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
goal,
|
|
46
|
+
steps: plan.steps,
|
|
47
|
+
context: plan.context || {},
|
|
48
|
+
created_at: Date.now()
|
|
49
|
+
};
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error(`[Planner:${this.name}] ✗ Planning failed:`, error.message);
|
|
52
|
+
throw new Error(`Planning failed: ${error.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Replan after a step failure
|
|
58
|
+
*/
|
|
59
|
+
async replan(originalGoal, failedStep, executedSteps, error, context = {}, agent = null) {
|
|
60
|
+
if (!this.allowReplanning) {
|
|
61
|
+
throw new Error('Re-planning is disabled');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`[Planner:${this.name}] 🔄 Re-planning after failure at step ${failedStep}`);
|
|
65
|
+
|
|
66
|
+
const replanPrompt = this.buildReplanningPrompt(
|
|
67
|
+
originalGoal,
|
|
68
|
+
executedSteps,
|
|
69
|
+
failedStep,
|
|
70
|
+
error,
|
|
71
|
+
context,
|
|
72
|
+
agent
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (!this.llmProvider) {
|
|
76
|
+
this.llmProvider = new LLMProvider(this.llm);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const newPlan = await this.llmProvider.executePlanning(replanPrompt);
|
|
80
|
+
|
|
81
|
+
if (!newPlan || !newPlan.steps || !Array.isArray(newPlan.steps)) {
|
|
82
|
+
throw new Error('Invalid re-plan format: missing steps array');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`[Planner:${this.name}] ✓ Created new plan with ${newPlan.steps.length} steps`);
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
goal: originalGoal,
|
|
89
|
+
steps: newPlan.steps,
|
|
90
|
+
context: newPlan.context || context,
|
|
91
|
+
replanned: true,
|
|
92
|
+
replanned_at: Date.now()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Build the planning prompt
|
|
98
|
+
*/
|
|
99
|
+
buildPlanningPrompt(goal, context, agent = null) {
|
|
100
|
+
return `Break down this goal into executable steps.
|
|
101
|
+
|
|
102
|
+
Goal: ${goal}
|
|
103
|
+
|
|
104
|
+
${actionRegistry.generatePromptDocumentation(agent)}
|
|
105
|
+
|
|
106
|
+
Return ONLY JSON (no markdown):
|
|
107
|
+
{
|
|
108
|
+
"steps": [
|
|
109
|
+
{ "intent": "action_name", ... }
|
|
110
|
+
]
|
|
111
|
+
}`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build the re-planning prompt
|
|
116
|
+
*/
|
|
117
|
+
buildReplanningPrompt(goal, executedSteps, failedStep, error, context, agent = null) {
|
|
118
|
+
return `Plan failed at step ${failedStep}. Create recovery plan.
|
|
119
|
+
|
|
120
|
+
Goal: ${goal}
|
|
121
|
+
Error: ${error}
|
|
122
|
+
|
|
123
|
+
${actionRegistry.generatePromptDocumentation(agent)}
|
|
124
|
+
|
|
125
|
+
Return ONLY JSON (no markdown):
|
|
126
|
+
{
|
|
127
|
+
"steps": [
|
|
128
|
+
{ "intent": "action_name", ... }
|
|
129
|
+
]
|
|
130
|
+
}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Planning-enabled Agent
|
|
136
|
+
* Extends regular agents with automatic planning capabilities
|
|
137
|
+
*/
|
|
138
|
+
export class PlanningAgent {
|
|
139
|
+
constructor(agent, plannerConfig) {
|
|
140
|
+
this.agent = agent;
|
|
141
|
+
this.planner = new Planner(plannerConfig);
|
|
142
|
+
this.currentPlan = null;
|
|
143
|
+
this.executionHistory = [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Execute a goal with automatic planning
|
|
148
|
+
*/
|
|
149
|
+
async executeWithPlanning(goal, context = {}) {
|
|
150
|
+
console.log(`[PlanningAgent:${this.agent.name}] 🎯 Starting planned execution`);
|
|
151
|
+
console.log(`[PlanningAgent:${this.agent.name}] Goal: ${goal}`);
|
|
152
|
+
|
|
153
|
+
// Create initial plan (pass the agent for permission filtering)
|
|
154
|
+
this.currentPlan = await this.planner.createPlan(goal, context, this.agent);
|
|
155
|
+
this.executionHistory = [];
|
|
156
|
+
|
|
157
|
+
let stepIndex = 0;
|
|
158
|
+
let retryCount = 0;
|
|
159
|
+
const maxRetries = 2;
|
|
160
|
+
|
|
161
|
+
while (stepIndex < this.currentPlan.steps.length) {
|
|
162
|
+
const step = this.currentPlan.steps[stepIndex];
|
|
163
|
+
|
|
164
|
+
console.log(`[PlanningAgent:${this.agent.name}] 📍 Step ${stepIndex + 1}/${this.currentPlan.steps.length}: ${step.description || step.type}`);
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
const result = await this.executeStep(step, context);
|
|
168
|
+
|
|
169
|
+
this.executionHistory.push({
|
|
170
|
+
step: stepIndex,
|
|
171
|
+
action: step,
|
|
172
|
+
result,
|
|
173
|
+
success: true
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Update context with result
|
|
177
|
+
if (result && typeof result === 'object') {
|
|
178
|
+
context = { ...context, ...result };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
stepIndex++;
|
|
182
|
+
retryCount = 0;
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error(`[PlanningAgent:${this.agent.name}] ✗ Step ${stepIndex + 1} failed:`, error.message);
|
|
186
|
+
|
|
187
|
+
this.executionHistory.push({
|
|
188
|
+
step: stepIndex,
|
|
189
|
+
action: step,
|
|
190
|
+
error: error.message,
|
|
191
|
+
success: false
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Try re-planning
|
|
195
|
+
if (retryCount < maxRetries && this.planner.allowReplanning) {
|
|
196
|
+
retryCount++;
|
|
197
|
+
console.log(`[PlanningAgent:${this.agent.name}] 🔄 Attempting re-plan (${retryCount}/${maxRetries})`);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
this.currentPlan = await this.planner.replan(
|
|
201
|
+
goal,
|
|
202
|
+
stepIndex,
|
|
203
|
+
this.executionHistory.filter(h => h.success),
|
|
204
|
+
error.message,
|
|
205
|
+
context,
|
|
206
|
+
this.agent
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
// Restart from beginning of new plan
|
|
210
|
+
stepIndex = 0;
|
|
211
|
+
|
|
212
|
+
} catch (replanError) {
|
|
213
|
+
console.error(`[PlanningAgent:${this.agent.name}] ✗ Re-planning failed:`, replanError.message);
|
|
214
|
+
throw new Error(`Planning failed: ${error.message}. Re-planning also failed: ${replanError.message}`);
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
throw new Error(`Step ${stepIndex + 1} failed after ${retryCount} retries: ${error.message}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
console.log(`[PlanningAgent:${this.agent.name}] ✓ Plan execution complete`);
|
|
223
|
+
|
|
224
|
+
// Return final result from last step
|
|
225
|
+
const lastHistory = this.executionHistory[this.executionHistory.length - 1];
|
|
226
|
+
return lastHistory?.result || { success: true };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Execute a single step from the plan
|
|
231
|
+
*/
|
|
232
|
+
async executeStep(step, context) {
|
|
233
|
+
// Delegate to the agent's action execution system
|
|
234
|
+
if (this.agent.executeActions) {
|
|
235
|
+
return await this.agent.executeActions([step]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Fallback: execute based on step type
|
|
239
|
+
switch (step.type) {
|
|
240
|
+
case 'call_skill':
|
|
241
|
+
return await this.agent.callSkill(step.skill, step.input);
|
|
242
|
+
|
|
243
|
+
case 'send_message':
|
|
244
|
+
case 'update_state':
|
|
245
|
+
// Use action registry executor
|
|
246
|
+
const actionDef = actionRegistry.get(step.type);
|
|
247
|
+
if (actionDef && actionDef.execute) {
|
|
248
|
+
return await actionDef.execute(step, this.agent);
|
|
249
|
+
}
|
|
250
|
+
throw new Error(`Action type "${step.type}" has no executor registered`);
|
|
251
|
+
|
|
252
|
+
case 'return':
|
|
253
|
+
return step.data || step.result || {};
|
|
254
|
+
|
|
255
|
+
default:
|
|
256
|
+
throw new Error(`Unknown step type: ${step.type}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Get execution summary
|
|
262
|
+
*/
|
|
263
|
+
getSummary() {
|
|
264
|
+
return {
|
|
265
|
+
goal: this.currentPlan?.goal,
|
|
266
|
+
totalSteps: this.currentPlan?.steps.length,
|
|
267
|
+
executedSteps: this.executionHistory.length,
|
|
268
|
+
successfulSteps: this.executionHistory.filter(h => h.success).length,
|
|
269
|
+
failedSteps: this.executionHistory.filter(h => !h.success).length,
|
|
270
|
+
replanned: this.currentPlan?.replanned || false
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keyv + SQLite Backend for Registry
|
|
3
|
+
*
|
|
4
|
+
* Uses Keyv with SQLite adapter for persistent, queryable storage.
|
|
5
|
+
* Production-ready with transaction support and efficient queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import Keyv from 'keyv';
|
|
9
|
+
import KeyvSqlite from '@keyv/sqlite';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
|
|
13
|
+
export default class KeyvSqliteBackend {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
this.dbPath = options.path || '.koi-registry/registry.sqlite';
|
|
16
|
+
this.keyv = null;
|
|
17
|
+
this.namespace = options.namespace || 'koi';
|
|
18
|
+
this._keysCache = new Set();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init() {
|
|
22
|
+
// Ensure directory exists
|
|
23
|
+
const dbDir = path.dirname(this.dbPath);
|
|
24
|
+
if (!fs.existsSync(dbDir)) {
|
|
25
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Initialize Keyv with SQLite adapter
|
|
29
|
+
this.keyv = new Keyv({
|
|
30
|
+
store: new KeyvSqlite(`sqlite://${this.dbPath}`),
|
|
31
|
+
namespace: this.namespace
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Handle errors
|
|
35
|
+
this.keyv.on('error', err => {
|
|
36
|
+
console.error('[Registry:KeyvSQLite] Connection error:', err);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async get(key) {
|
|
41
|
+
const value = await this.keyv.get(key);
|
|
42
|
+
return value !== undefined ? value : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async set(key, value) {
|
|
46
|
+
await this.keyv.set(key, value);
|
|
47
|
+
this._keysCache.add(key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async delete(key) {
|
|
51
|
+
const existed = await this.keyv.has(key);
|
|
52
|
+
await this.keyv.delete(key);
|
|
53
|
+
this._keysCache.delete(key);
|
|
54
|
+
return existed;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async has(key) {
|
|
58
|
+
return await this.keyv.has(key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async keys(prefix = '') {
|
|
62
|
+
// Return keys from in-memory cache
|
|
63
|
+
// Cache is maintained by set/delete operations
|
|
64
|
+
const keys = Array.from(this._keysCache);
|
|
65
|
+
|
|
66
|
+
if (!prefix) {
|
|
67
|
+
return keys;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return keys.filter(key => key.startsWith(prefix));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async search(query) {
|
|
74
|
+
// Scan all possible keys by trying common patterns
|
|
75
|
+
// This is a workaround since Keyv doesn't provide a native keys() method
|
|
76
|
+
|
|
77
|
+
// Start with cached keys, then expand the search
|
|
78
|
+
const keysToCheck = new Set(this._keysCache);
|
|
79
|
+
const results = [];
|
|
80
|
+
|
|
81
|
+
// Get values for all known keys
|
|
82
|
+
for (const key of keysToCheck) {
|
|
83
|
+
const value = await this.keyv.get(key);
|
|
84
|
+
|
|
85
|
+
if (value !== undefined) {
|
|
86
|
+
if (this.matchesQuery(value, query)) {
|
|
87
|
+
results.push({ key, value });
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
// Key no longer exists, remove from cache
|
|
91
|
+
this._keysCache.delete(key);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return results;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
matchesQuery(obj, query) {
|
|
99
|
+
// Handle null/undefined
|
|
100
|
+
if (obj === null || obj === undefined) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Empty query matches all
|
|
105
|
+
if (Object.keys(query).length === 0) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Query must be an object
|
|
110
|
+
if (typeof query !== 'object' || query === null) {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check all query conditions
|
|
115
|
+
for (const [field, condition] of Object.entries(query)) {
|
|
116
|
+
const fieldValue = this.getNestedValue(obj, field);
|
|
117
|
+
|
|
118
|
+
// Direct value comparison
|
|
119
|
+
if (typeof condition !== 'object' || condition === null) {
|
|
120
|
+
if (fieldValue !== condition) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Operator-based comparison
|
|
127
|
+
for (const [operator, value] of Object.entries(condition)) {
|
|
128
|
+
switch (operator) {
|
|
129
|
+
case '$eq':
|
|
130
|
+
if (fieldValue !== value) return false;
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case '$ne':
|
|
134
|
+
if (fieldValue === value) return false;
|
|
135
|
+
break;
|
|
136
|
+
|
|
137
|
+
case '$gt':
|
|
138
|
+
if (fieldValue <= value) return false;
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case '$gte':
|
|
142
|
+
if (fieldValue < value) return false;
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case '$lt':
|
|
146
|
+
if (fieldValue >= value) return false;
|
|
147
|
+
break;
|
|
148
|
+
|
|
149
|
+
case '$lte':
|
|
150
|
+
if (fieldValue > value) return false;
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case '$in':
|
|
154
|
+
if (!Array.isArray(value) || !value.includes(fieldValue)) return false;
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case '$regex':
|
|
158
|
+
const regex = new RegExp(value);
|
|
159
|
+
if (!regex.test(String(fieldValue))) return false;
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
default:
|
|
163
|
+
console.warn(`[Registry:KeyvSQLite] Unknown operator: ${operator}`);
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getNestedValue(obj, path) {
|
|
173
|
+
// Support dot notation: 'user.name' -> obj.user.name
|
|
174
|
+
const parts = path.split('.');
|
|
175
|
+
let current = obj;
|
|
176
|
+
|
|
177
|
+
for (const part of parts) {
|
|
178
|
+
if (current === null || current === undefined) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
current = current[part];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return current;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async clear() {
|
|
188
|
+
await this.keyv.clear();
|
|
189
|
+
this._keysCache.clear();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async stats() {
|
|
193
|
+
const allKeys = await this.keys();
|
|
194
|
+
|
|
195
|
+
// Get file size
|
|
196
|
+
let size = 0;
|
|
197
|
+
if (fs.existsSync(this.dbPath)) {
|
|
198
|
+
size = fs.statSync(this.dbPath).size;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
backend: 'keyv-sqlite',
|
|
203
|
+
count: allKeys.length,
|
|
204
|
+
file: this.dbPath,
|
|
205
|
+
size: size
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async close() {
|
|
210
|
+
// Keyv handles cleanup automatically
|
|
211
|
+
if (this.keyv && this.keyv.opts.store && this.keyv.opts.store.close) {
|
|
212
|
+
await this.keyv.opts.store.close();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|