@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.
Files changed (85) hide show
  1. package/QUICKSTART.md +89 -0
  2. package/README.md +545 -0
  3. package/examples/actions-demo.koi +177 -0
  4. package/examples/cache-test.koi +29 -0
  5. package/examples/calculator.koi +61 -0
  6. package/examples/clear-registry.js +33 -0
  7. package/examples/clear-registry.koi +30 -0
  8. package/examples/code-introspection-test.koi +149 -0
  9. package/examples/counter.koi +132 -0
  10. package/examples/delegation-test.koi +52 -0
  11. package/examples/directory-import-test.koi +84 -0
  12. package/examples/hello-world-claude.koi +52 -0
  13. package/examples/hello-world.koi +52 -0
  14. package/examples/hello.koi +24 -0
  15. package/examples/mcp-example.koi +70 -0
  16. package/examples/multi-event-handler-test.koi +144 -0
  17. package/examples/new-import-test.koi +89 -0
  18. package/examples/pipeline.koi +162 -0
  19. package/examples/registry-demo.koi +184 -0
  20. package/examples/registry-playbook-demo.koi +162 -0
  21. package/examples/registry-playbook-email-compositor-2.koi +140 -0
  22. package/examples/registry-playbook-email-compositor.koi +140 -0
  23. package/examples/sentiment.koi +90 -0
  24. package/examples/simple.koi +48 -0
  25. package/examples/skill-import-test.koi +76 -0
  26. package/examples/skills/advanced/index.koi +95 -0
  27. package/examples/skills/math-operations.koi +69 -0
  28. package/examples/skills/string-operations.koi +56 -0
  29. package/examples/task-chaining-demo.koi +244 -0
  30. package/examples/test-await.koi +22 -0
  31. package/examples/test-crypto-sha256.koi +196 -0
  32. package/examples/test-delegation.koi +41 -0
  33. package/examples/test-multi-team-routing.koi +258 -0
  34. package/examples/test-no-handler.koi +35 -0
  35. package/examples/test-npm-import.koi +67 -0
  36. package/examples/test-parse.koi +10 -0
  37. package/examples/test-peers-with-team.koi +59 -0
  38. package/examples/test-permissions-fail.koi +20 -0
  39. package/examples/test-permissions.koi +36 -0
  40. package/examples/test-simple-registry.koi +31 -0
  41. package/examples/test-typescript-import.koi +64 -0
  42. package/examples/test-uses-team-syntax.koi +25 -0
  43. package/examples/test-uses-team.koi +31 -0
  44. package/examples/utils/calculator.test.ts +144 -0
  45. package/examples/utils/calculator.ts +56 -0
  46. package/examples/utils/math-helpers.js +50 -0
  47. package/examples/utils/math-helpers.ts +55 -0
  48. package/examples/web-delegation-demo.koi +165 -0
  49. package/package.json +78 -0
  50. package/src/cli/koi.js +793 -0
  51. package/src/compiler/build-optimizer.js +447 -0
  52. package/src/compiler/cache-manager.js +274 -0
  53. package/src/compiler/import-resolver.js +369 -0
  54. package/src/compiler/parser.js +7542 -0
  55. package/src/compiler/transpiler.js +1105 -0
  56. package/src/compiler/typescript-transpiler.js +148 -0
  57. package/src/grammar/koi.pegjs +767 -0
  58. package/src/runtime/action-registry.js +172 -0
  59. package/src/runtime/actions/call-skill.js +45 -0
  60. package/src/runtime/actions/format.js +115 -0
  61. package/src/runtime/actions/print.js +42 -0
  62. package/src/runtime/actions/registry-delete.js +37 -0
  63. package/src/runtime/actions/registry-get.js +37 -0
  64. package/src/runtime/actions/registry-keys.js +33 -0
  65. package/src/runtime/actions/registry-search.js +34 -0
  66. package/src/runtime/actions/registry-set.js +50 -0
  67. package/src/runtime/actions/return.js +31 -0
  68. package/src/runtime/actions/send-message.js +58 -0
  69. package/src/runtime/actions/update-state.js +36 -0
  70. package/src/runtime/agent.js +1368 -0
  71. package/src/runtime/cli-logger.js +205 -0
  72. package/src/runtime/incremental-json-parser.js +201 -0
  73. package/src/runtime/index.js +33 -0
  74. package/src/runtime/llm-provider.js +1372 -0
  75. package/src/runtime/mcp-client.js +1171 -0
  76. package/src/runtime/planner.js +273 -0
  77. package/src/runtime/registry-backends/keyv-sqlite.js +215 -0
  78. package/src/runtime/registry-backends/local.js +260 -0
  79. package/src/runtime/registry.js +162 -0
  80. package/src/runtime/role.js +14 -0
  81. package/src/runtime/router.js +395 -0
  82. package/src/runtime/runtime.js +113 -0
  83. package/src/runtime/skill-selector.js +173 -0
  84. package/src/runtime/skill.js +25 -0
  85. 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
+ }