@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,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local File Backend for Registry
|
|
3
|
+
*
|
|
4
|
+
* Stores data in a JSON file with in-memory cache for performance.
|
|
5
|
+
* Simple, no dependencies, perfect for development and single-machine deployments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
|
|
11
|
+
export default class LocalBackend {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.dataDir = options.path || '.koi-registry';
|
|
14
|
+
this.dataFile = path.join(this.dataDir, 'data.json');
|
|
15
|
+
this.cache = new Map();
|
|
16
|
+
this.dirty = false;
|
|
17
|
+
this.autoSaveInterval = options.autoSaveInterval || 5000; // 5 seconds
|
|
18
|
+
this.autoSaveTimer = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async init() {
|
|
22
|
+
// Ensure directory exists
|
|
23
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
24
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Load existing data
|
|
28
|
+
if (fs.existsSync(this.dataFile)) {
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(this.dataFile, 'utf-8');
|
|
31
|
+
const data = JSON.parse(content);
|
|
32
|
+
|
|
33
|
+
// Load into cache
|
|
34
|
+
for (const [key, value] of Object.entries(data)) {
|
|
35
|
+
this.cache.set(key, value);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.warn(`[Registry:Local] Failed to load data file: ${error.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Start auto-save timer
|
|
43
|
+
this.startAutoSave();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
startAutoSave() {
|
|
47
|
+
if (this.autoSaveTimer) return;
|
|
48
|
+
|
|
49
|
+
this.autoSaveTimer = setInterval(() => {
|
|
50
|
+
if (this.dirty) {
|
|
51
|
+
this.persist().catch(err => {
|
|
52
|
+
console.error(`[Registry:Local] Auto-save failed: ${err.message}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}, this.autoSaveInterval);
|
|
56
|
+
|
|
57
|
+
// Don't keep process alive
|
|
58
|
+
if (this.autoSaveTimer.unref) {
|
|
59
|
+
this.autoSaveTimer.unref();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
stopAutoSave() {
|
|
64
|
+
if (this.autoSaveTimer) {
|
|
65
|
+
clearInterval(this.autoSaveTimer);
|
|
66
|
+
this.autoSaveTimer = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async persist() {
|
|
71
|
+
if (!this.dirty) return;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Convert Map to plain object
|
|
75
|
+
const data = {};
|
|
76
|
+
for (const [key, value] of this.cache.entries()) {
|
|
77
|
+
data[key] = value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Write to file
|
|
81
|
+
fs.writeFileSync(this.dataFile, JSON.stringify(data, null, 2), 'utf-8');
|
|
82
|
+
this.dirty = false;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`[Registry:Local] Failed to persist: ${error.message}`);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async get(key) {
|
|
90
|
+
return this.cache.get(key) || null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async set(key, value) {
|
|
94
|
+
this.cache.set(key, value);
|
|
95
|
+
this.dirty = true;
|
|
96
|
+
|
|
97
|
+
// Immediate persist for important operations
|
|
98
|
+
// (optional: could debounce this)
|
|
99
|
+
await this.persist();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async delete(key) {
|
|
103
|
+
const existed = this.cache.has(key);
|
|
104
|
+
this.cache.delete(key);
|
|
105
|
+
|
|
106
|
+
if (existed) {
|
|
107
|
+
this.dirty = true;
|
|
108
|
+
await this.persist();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return existed;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async has(key) {
|
|
115
|
+
return this.cache.has(key);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async keys(prefix = '') {
|
|
119
|
+
const allKeys = Array.from(this.cache.keys());
|
|
120
|
+
|
|
121
|
+
if (!prefix) {
|
|
122
|
+
return allKeys;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return allKeys.filter(key => key.startsWith(prefix));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async search(query) {
|
|
129
|
+
// Simple search implementation for local backend
|
|
130
|
+
// Supports basic query patterns:
|
|
131
|
+
// - { field: value } - exact match
|
|
132
|
+
// - { field: { $eq: value } } - exact match
|
|
133
|
+
// - { field: { $ne: value } } - not equal
|
|
134
|
+
// - { field: { $gt: value } } - greater than
|
|
135
|
+
// - { field: { $gte: value } } - greater than or equal
|
|
136
|
+
// - { field: { $lt: value } } - less than
|
|
137
|
+
// - { field: { $lte: value } } - less than or equal
|
|
138
|
+
// - { field: { $in: [values] } } - in array
|
|
139
|
+
// - { field: { $regex: pattern } } - regex match
|
|
140
|
+
|
|
141
|
+
const results = [];
|
|
142
|
+
|
|
143
|
+
for (const [key, value] of this.cache.entries()) {
|
|
144
|
+
if (this.matchesQuery(value, query)) {
|
|
145
|
+
results.push({ key, value });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return results;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
matchesQuery(obj, query) {
|
|
153
|
+
// Handle null/undefined
|
|
154
|
+
if (obj === null || obj === undefined) {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Query must be an object
|
|
159
|
+
if (typeof query !== 'object' || query === null) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check all query conditions
|
|
164
|
+
for (const [field, condition] of Object.entries(query)) {
|
|
165
|
+
const fieldValue = this.getNestedValue(obj, field);
|
|
166
|
+
|
|
167
|
+
// Direct value comparison
|
|
168
|
+
if (typeof condition !== 'object' || condition === null) {
|
|
169
|
+
if (fieldValue !== condition) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Operator-based comparison
|
|
176
|
+
for (const [operator, value] of Object.entries(condition)) {
|
|
177
|
+
switch (operator) {
|
|
178
|
+
case '$eq':
|
|
179
|
+
if (fieldValue !== value) return false;
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
case '$ne':
|
|
183
|
+
if (fieldValue === value) return false;
|
|
184
|
+
break;
|
|
185
|
+
|
|
186
|
+
case '$gt':
|
|
187
|
+
if (fieldValue <= value) return false;
|
|
188
|
+
break;
|
|
189
|
+
|
|
190
|
+
case '$gte':
|
|
191
|
+
if (fieldValue < value) return false;
|
|
192
|
+
break;
|
|
193
|
+
|
|
194
|
+
case '$lt':
|
|
195
|
+
if (fieldValue >= value) return false;
|
|
196
|
+
break;
|
|
197
|
+
|
|
198
|
+
case '$lte':
|
|
199
|
+
if (fieldValue > value) return false;
|
|
200
|
+
break;
|
|
201
|
+
|
|
202
|
+
case '$in':
|
|
203
|
+
if (!Array.isArray(value) || !value.includes(fieldValue)) return false;
|
|
204
|
+
break;
|
|
205
|
+
|
|
206
|
+
case '$regex':
|
|
207
|
+
const regex = new RegExp(value);
|
|
208
|
+
if (!regex.test(String(fieldValue))) return false;
|
|
209
|
+
break;
|
|
210
|
+
|
|
211
|
+
default:
|
|
212
|
+
console.warn(`[Registry:Local] Unknown operator: ${operator}`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getNestedValue(obj, path) {
|
|
222
|
+
// Support dot notation: 'user.name' -> obj.user.name
|
|
223
|
+
const parts = path.split('.');
|
|
224
|
+
let current = obj;
|
|
225
|
+
|
|
226
|
+
for (const part of parts) {
|
|
227
|
+
if (current === null || current === undefined) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
current = current[part];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return current;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async clear() {
|
|
237
|
+
this.cache.clear();
|
|
238
|
+
this.dirty = true;
|
|
239
|
+
await this.persist();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async stats() {
|
|
243
|
+
return {
|
|
244
|
+
backend: 'local',
|
|
245
|
+
count: this.cache.size,
|
|
246
|
+
file: this.dataFile,
|
|
247
|
+
size: fs.existsSync(this.dataFile)
|
|
248
|
+
? fs.statSync(this.dataFile).size
|
|
249
|
+
: 0
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Cleanup on exit
|
|
254
|
+
async close() {
|
|
255
|
+
this.stopAutoSave();
|
|
256
|
+
if (this.dirty) {
|
|
257
|
+
await this.persist();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry - Shared data store for agent collaboration
|
|
3
|
+
*
|
|
4
|
+
* Provides a simple, transparent API for agents to share information.
|
|
5
|
+
* Backend is configurable (local file, Redis, MongoDB, etc.)
|
|
6
|
+
*
|
|
7
|
+
* Usage from agents:
|
|
8
|
+
* await registry.set('user:123', { name: 'Alice', age: 30 })
|
|
9
|
+
* const user = await registry.get('user:123')
|
|
10
|
+
* const users = await registry.search({ age: { $gte: 25 } })
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'fs';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
18
|
+
const __dirname = path.dirname(__filename);
|
|
19
|
+
|
|
20
|
+
class Registry {
|
|
21
|
+
constructor(config = {}) {
|
|
22
|
+
this.backend = null;
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.initialized = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async init() {
|
|
28
|
+
if (this.initialized) return;
|
|
29
|
+
|
|
30
|
+
// Load configuration from .koi-config.json if exists
|
|
31
|
+
const configPath = path.join(process.cwd(), '.koi-config.json');
|
|
32
|
+
let fileConfig = {};
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(configPath)) {
|
|
35
|
+
try {
|
|
36
|
+
const content = fs.readFileSync(configPath, 'utf-8');
|
|
37
|
+
fileConfig = JSON.parse(content);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.warn(`[Registry] Failed to load .koi-config.json: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Merge configs: constructor config > file config > defaults
|
|
44
|
+
const mergedConfig = {
|
|
45
|
+
backend: 'local',
|
|
46
|
+
options: {},
|
|
47
|
+
...fileConfig.registry,
|
|
48
|
+
...this.config
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Load backend
|
|
52
|
+
const backendName = mergedConfig.backend;
|
|
53
|
+
try {
|
|
54
|
+
const backendModule = await import(`./registry-backends/${backendName}.js`);
|
|
55
|
+
this.backend = new backendModule.default(mergedConfig.options);
|
|
56
|
+
await this.backend.init();
|
|
57
|
+
this.initialized = true;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(`[Registry] Failed to load backend '${backendName}': ${error.message}`);
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async ensureInit() {
|
|
65
|
+
if (!this.initialized) {
|
|
66
|
+
await this.init();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Get a value by key
|
|
72
|
+
* @param {string} key - The key to retrieve
|
|
73
|
+
* @returns {Promise<any>} The stored value or null if not found
|
|
74
|
+
*/
|
|
75
|
+
async get(key) {
|
|
76
|
+
await this.ensureInit();
|
|
77
|
+
return await this.backend.get(key);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Set a value by key
|
|
82
|
+
* @param {string} key - The key to store
|
|
83
|
+
* @param {any} value - The value to store (will be JSON serialized)
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
async set(key, value) {
|
|
87
|
+
await this.ensureInit();
|
|
88
|
+
return await this.backend.set(key, value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Delete a value by key
|
|
93
|
+
* @param {string} key - The key to delete
|
|
94
|
+
* @returns {Promise<boolean>} True if deleted, false if not found
|
|
95
|
+
*/
|
|
96
|
+
async delete(key) {
|
|
97
|
+
await this.ensureInit();
|
|
98
|
+
return await this.backend.delete(key);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Check if a key exists
|
|
103
|
+
* @param {string} key - The key to check
|
|
104
|
+
* @returns {Promise<boolean>}
|
|
105
|
+
*/
|
|
106
|
+
async has(key) {
|
|
107
|
+
await this.ensureInit();
|
|
108
|
+
return await this.backend.has(key);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* List all keys matching a prefix
|
|
113
|
+
* @param {string} prefix - The prefix to match (e.g., 'user:')
|
|
114
|
+
* @returns {Promise<string[]>} Array of matching keys
|
|
115
|
+
*/
|
|
116
|
+
async keys(prefix = '') {
|
|
117
|
+
await this.ensureInit();
|
|
118
|
+
return await this.backend.keys(prefix);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Search for entries matching a query
|
|
123
|
+
* @param {object} query - Query object (syntax depends on backend)
|
|
124
|
+
* @returns {Promise<object[]>} Array of matching {key, value} objects
|
|
125
|
+
*/
|
|
126
|
+
async search(query) {
|
|
127
|
+
await this.ensureInit();
|
|
128
|
+
return await this.backend.search(query);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Clear all data (use with caution!)
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
async clear() {
|
|
136
|
+
await this.ensureInit();
|
|
137
|
+
return await this.backend.clear();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get statistics about the registry
|
|
142
|
+
* @returns {Promise<object>} Stats object with count, size, etc.
|
|
143
|
+
*/
|
|
144
|
+
async stats() {
|
|
145
|
+
await this.ensureInit();
|
|
146
|
+
return await this.backend.stats();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Singleton instance
|
|
151
|
+
let registryInstance = null;
|
|
152
|
+
|
|
153
|
+
export function getRegistry(config = {}) {
|
|
154
|
+
if (!registryInstance) {
|
|
155
|
+
registryInstance = new Registry(config);
|
|
156
|
+
}
|
|
157
|
+
return registryInstance;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export const registry = getRegistry();
|
|
161
|
+
|
|
162
|
+
export default Registry;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class Role {
|
|
2
|
+
constructor(name, capabilities = []) {
|
|
3
|
+
this.name = name;
|
|
4
|
+
this.capabilities = new Set(capabilities);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
can(capability) {
|
|
8
|
+
return this.capabilities.has(capability);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
toString() {
|
|
12
|
+
return `Role(${this.name})`;
|
|
13
|
+
}
|
|
14
|
+
}
|