@koi-language/koi 1.0.5 → 1.1.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 (113) hide show
  1. package/README.md +4 -125
  2. package/examples/.build/agent-dialogue.ts +138 -0
  3. package/examples/.build/agent-dialogue.ts.map +1 -0
  4. package/examples/.build/chess.ts +77 -0
  5. package/examples/.build/chess.ts.map +1 -0
  6. package/examples/.build/delegation-test.ts +140 -0
  7. package/examples/.build/delegation-test.ts.map +1 -0
  8. package/examples/.build/dialog-demo.ts +77 -0
  9. package/examples/.build/dialog-demo.ts.map +1 -0
  10. package/examples/.build/hello-world.ts +77 -0
  11. package/examples/.build/hello-world.ts.map +1 -0
  12. package/examples/.build/lover-dialog-demo.ts +77 -0
  13. package/examples/.build/lover-dialog-demo.ts.map +1 -0
  14. package/examples/.build/package.json +3 -0
  15. package/examples/.build/registry-interactive-demo.ts +202 -0
  16. package/examples/.build/registry-interactive-demo.ts.map +1 -0
  17. package/examples/.build/registry-playbook-demo.ts +201 -0
  18. package/examples/.build/registry-playbook-demo.ts.map +1 -0
  19. package/examples/.build/tic-tac-toe.ts +77 -0
  20. package/examples/.build/tic-tac-toe.ts.map +1 -0
  21. package/examples/actions-demo.koi +8 -9
  22. package/examples/activists-dialogue.koi +75 -0
  23. package/examples/agent-dialogue.koi +66 -0
  24. package/examples/chess.koi +19 -0
  25. package/examples/counter.koi +20 -69
  26. package/examples/delegation-test.koi +16 -18
  27. package/examples/dialog-demo.koi +20 -0
  28. package/examples/hello-world.koi +7 -43
  29. package/examples/mcp-stdio-demo.koi +29 -0
  30. package/examples/memory-test.koi +49 -0
  31. package/examples/mobile-mcp-demo.koi +32 -0
  32. package/examples/multi-event-handler-test.koi +16 -18
  33. package/examples/pipeline.koi +15 -17
  34. package/examples/prompt-demo.koi +20 -0
  35. package/examples/{registry-playbook-email-compositor.koi → registry-interactive-demo.koi} +27 -27
  36. package/examples/registry-playbook-demo.koi +28 -28
  37. package/examples/skill-import-test.koi +7 -9
  38. package/examples/skills/.build/math-operations.ts +1656 -0
  39. package/examples/skills/.build/math-operations.ts.map +1 -0
  40. package/examples/skills/.build/package.json +3 -0
  41. package/examples/skills/.build/string-operations.ts +1643 -0
  42. package/examples/skills/.build/string-operations.ts.map +1 -0
  43. package/examples/skills/advanced/.build/index.ts +3223 -0
  44. package/examples/skills/advanced/.build/index.ts.map +1 -0
  45. package/examples/skills/advanced/.build/package.json +3 -0
  46. package/examples/skills/advanced/index.koi +3 -5
  47. package/examples/skills/math-operations.koi +1 -3
  48. package/examples/skills/string-operations.koi +1 -3
  49. package/examples/tic-tac-toe.koi +19 -0
  50. package/examples/utils/echo-mcp-server.js +141 -0
  51. package/examples/web-delegation-demo.koi +15 -17
  52. package/package.json +2 -1
  53. package/src/cli/koi.js +30 -41
  54. package/src/compiler/build-optimizer.js +204 -289
  55. package/src/compiler/cache-manager.js +1 -1
  56. package/src/compiler/import-resolver.js +5 -9
  57. package/src/compiler/parser.js +6072 -3476
  58. package/src/compiler/transpiler.js +346 -38
  59. package/src/grammar/koi.pegjs +302 -62
  60. package/src/runtime/actions/{format.js → call-llm.js} +37 -44
  61. package/src/runtime/actions/call-mcp.js +97 -0
  62. package/src/runtime/actions/if.js +179 -0
  63. package/src/runtime/actions/print.js +3 -1
  64. package/src/runtime/actions/prompt-user.js +75 -0
  65. package/src/runtime/actions/repeat.js +147 -0
  66. package/src/runtime/actions/shell.js +185 -0
  67. package/src/runtime/actions/while.js +205 -0
  68. package/src/runtime/agent.js +592 -178
  69. package/src/runtime/cli-display.js +26 -0
  70. package/src/runtime/cli-input.js +421 -0
  71. package/src/runtime/cli-logger.js +2 -5
  72. package/src/runtime/cli-markdown.js +61 -0
  73. package/src/runtime/cli-select.js +106 -0
  74. package/src/runtime/incremental-json-parser.js +51 -17
  75. package/src/runtime/index.js +1 -0
  76. package/src/runtime/llm-provider.js +1083 -572
  77. package/src/runtime/mcp-registry.js +141 -0
  78. package/src/runtime/mcp-stdio-client.js +334 -0
  79. package/src/runtime/planner.js +1 -1
  80. package/src/runtime/playbook-session.js +259 -0
  81. package/src/runtime/registry-backends/keyv-sqlite.js +1 -1
  82. package/src/runtime/registry-backends/local.js +1 -1
  83. package/src/runtime/router.js +22 -26
  84. package/src/runtime/runtime.js +7 -1
  85. package/examples/cache-test.koi +0 -29
  86. package/examples/calculator.koi +0 -61
  87. package/examples/clear-registry.js +0 -33
  88. package/examples/clear-registry.koi +0 -30
  89. package/examples/code-introspection-test.koi +0 -149
  90. package/examples/directory-import-test.koi +0 -84
  91. package/examples/hello-world-claude.koi +0 -52
  92. package/examples/hello.koi +0 -24
  93. package/examples/mcp-example.koi +0 -70
  94. package/examples/new-import-test.koi +0 -89
  95. package/examples/registry-demo.koi +0 -184
  96. package/examples/registry-playbook-email-compositor-2.koi +0 -140
  97. package/examples/sentiment.koi +0 -90
  98. package/examples/simple.koi +0 -48
  99. package/examples/task-chaining-demo.koi +0 -244
  100. package/examples/test-await.koi +0 -22
  101. package/examples/test-crypto-sha256.koi +0 -196
  102. package/examples/test-delegation.koi +0 -41
  103. package/examples/test-multi-team-routing.koi +0 -258
  104. package/examples/test-no-handler.koi +0 -35
  105. package/examples/test-npm-import.koi +0 -67
  106. package/examples/test-parse.koi +0 -10
  107. package/examples/test-peers-with-team.koi +0 -59
  108. package/examples/test-permissions-fail.koi +0 -20
  109. package/examples/test-permissions.koi +0 -36
  110. package/examples/test-simple-registry.koi +0 -31
  111. package/examples/test-typescript-import.koi +0 -64
  112. package/examples/test-uses-team-syntax.koi +0 -25
  113. package/examples/test-uses-team.koi +0 -31
@@ -0,0 +1,141 @@
1
+ import { MCPStdioClient } from './mcp-stdio-client.js';
2
+
3
+ /**
4
+ * MCP Registry - Global registry of MCP client instances.
5
+ * Similar to SkillRegistry but for MCP stdio servers.
6
+ */
7
+ class MCPRegistry {
8
+ constructor() {
9
+ this.clients = new Map();
10
+ }
11
+
12
+ /**
13
+ * Register an MCP server configuration.
14
+ * Does NOT connect immediately (lazy connection on first use).
15
+ * @param {string} name - MCP server name (e.g., "mobileMCP")
16
+ * @param {object} config - { command, args, env }
17
+ */
18
+ register(name, config) {
19
+ if (this.clients.has(name)) {
20
+ if (process.env.KOI_DEBUG_LLM) {
21
+ console.error(`[MCPRegistry] Re-registering MCP: ${name}`);
22
+ }
23
+ }
24
+
25
+ const client = new MCPStdioClient(name, config);
26
+ this.clients.set(name, client);
27
+
28
+ if (process.env.KOI_DEBUG_LLM) {
29
+ console.error(`[MCPRegistry] Registered MCP: ${name} (${config.command} ${(config.args || []).join(' ')})`);
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Get a client by name.
35
+ * @param {string} name - MCP server name
36
+ * @returns {MCPStdioClient|undefined}
37
+ */
38
+ get(name) {
39
+ return this.clients.get(name);
40
+ }
41
+
42
+ /**
43
+ * Connect a specific MCP client (lazy initialization).
44
+ * @param {string} name - MCP server name
45
+ */
46
+ async connect(name) {
47
+ const client = this.clients.get(name);
48
+ if (!client) {
49
+ throw new Error(`MCP '${name}' not registered`);
50
+ }
51
+ if (!client.initialized) {
52
+ await client.connect();
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Connect all registered MCP clients.
58
+ */
59
+ async connectAll() {
60
+ const promises = [];
61
+ for (const [name, client] of this.clients) {
62
+ if (!client.initialized) {
63
+ promises.push(client.connect().catch(err => {
64
+ console.error(`[MCPRegistry] Failed to connect ${name}: ${err.message}`);
65
+ }));
66
+ }
67
+ }
68
+ await Promise.all(promises);
69
+ }
70
+
71
+ /**
72
+ * Call a tool on a specific MCP server.
73
+ * Connects lazily if not already connected.
74
+ * @param {string} mcpName - MCP server name
75
+ * @param {string} toolName - Tool name
76
+ * @param {object} args - Tool arguments
77
+ * @returns {object} Tool result
78
+ */
79
+ async callTool(mcpName, toolName, args = {}) {
80
+ const client = this.clients.get(mcpName);
81
+ if (!client) {
82
+ throw new Error(`MCP '${mcpName}' not registered`);
83
+ }
84
+
85
+ if (!client.initialized) {
86
+ await client.connect();
87
+ }
88
+
89
+ return await client.callTool(toolName, args);
90
+ }
91
+
92
+ /**
93
+ * Get tool summaries for all registered MCPs.
94
+ * Used for building system prompts.
95
+ * @returns {Array<{name: string, tools: Array}>}
96
+ */
97
+ getToolSummaries() {
98
+ const summaries = [];
99
+ for (const [name, client] of this.clients) {
100
+ if (client.tools.length > 0) {
101
+ summaries.push({
102
+ name,
103
+ tools: client.tools.map(t => ({
104
+ name: t.name,
105
+ description: t.description || '',
106
+ inputSchema: t.inputSchema
107
+ }))
108
+ });
109
+ }
110
+ }
111
+ return summaries;
112
+ }
113
+
114
+ /**
115
+ * Disconnect all MCP clients gracefully.
116
+ */
117
+ async disconnectAll() {
118
+ const promises = [];
119
+ for (const [name, client] of this.clients) {
120
+ if (client.initialized) {
121
+ promises.push(client.disconnect().catch(err => {
122
+ console.error(`[MCPRegistry] Failed to disconnect ${name}: ${err.message}`);
123
+ }));
124
+ }
125
+ }
126
+ await Promise.all(promises);
127
+ }
128
+
129
+ /**
130
+ * Check if any MCP servers are registered.
131
+ */
132
+ hasRegistered() {
133
+ return this.clients.size > 0;
134
+ }
135
+ }
136
+
137
+ // Singleton instance
138
+ export const mcpRegistry = new MCPRegistry();
139
+
140
+ // Make available globally for transpiled code
141
+ globalThis.mcpRegistry = mcpRegistry;
@@ -0,0 +1,334 @@
1
+ import { spawn } from 'child_process';
2
+
3
+ /**
4
+ * MCP Stdio Client - Manages a single MCP server subprocess
5
+ * and communicates via JSON-RPC 2.0 over stdin/stdout.
6
+ */
7
+ export class MCPStdioClient {
8
+ constructor(name, config) {
9
+ this.name = name;
10
+ this.command = config.command;
11
+ this.args = config.args || [];
12
+ this.env = config.env || {};
13
+ this.process = null;
14
+ this.tools = [];
15
+ this.initialized = false;
16
+ this._requestId = 0;
17
+ this._pendingRequests = new Map();
18
+ this._buffer = '';
19
+ this._stderrLines = [];
20
+ this.lastError = null; // Human-readable error cause when process crashes
21
+ }
22
+
23
+ /**
24
+ * Spawn subprocess, perform MCP initialize handshake, and cache tools/list.
25
+ */
26
+ async connect() {
27
+ if (this.initialized) return;
28
+
29
+ // Clean up old state from previous crash before reconnecting
30
+ if (this.process) {
31
+ try { this.process.kill('SIGTERM'); } catch (e) { /* already dead */ }
32
+ this.process = null;
33
+ }
34
+ this._buffer = '';
35
+ this._stderrLines = [];
36
+ this._pendingRequests.clear();
37
+ this.lastError = null;
38
+
39
+ return new Promise((resolve, reject) => {
40
+ const timeout = setTimeout(() => {
41
+ reject(new Error(`[MCP:${this.name}] Connection timeout after 30s`));
42
+ }, 30000);
43
+
44
+ try {
45
+ // Build env: config values override process.env, but skip empty/null values
46
+ // so that env: { "API_KEY": "" } in .koi doesn't override a real env var
47
+ const configEnv = {};
48
+ for (const [key, value] of Object.entries(this.env)) {
49
+ if (value !== '' && value !== null && value !== undefined) {
50
+ configEnv[key] = value;
51
+ }
52
+ }
53
+
54
+ this.process = spawn(this.command, this.args, {
55
+ stdio: ['pipe', 'pipe', 'pipe'],
56
+ env: { ...process.env, ...configEnv }
57
+ });
58
+
59
+ this.process.stdout.on('data', (data) => {
60
+ this._buffer += data.toString();
61
+ this._processBuffer();
62
+ });
63
+
64
+ this.process.stderr.on('data', (data) => {
65
+ const lines = data.toString().split('\n').filter(l => l.trim());
66
+ for (const line of lines) {
67
+ this._stderrLines.push(line);
68
+ // Keep only last 20 lines to avoid unbounded growth
69
+ if (this._stderrLines.length > 20) {
70
+ this._stderrLines.shift();
71
+ }
72
+ if (process.env.KOI_DEBUG_LLM) {
73
+ console.error(`[MCP:${this.name}] ${line}`);
74
+ }
75
+ }
76
+ });
77
+
78
+ this.process.on('error', (err) => {
79
+ clearTimeout(timeout);
80
+ reject(new Error(`[MCP:${this.name}] Failed to spawn: ${err.message}`));
81
+ });
82
+
83
+ this.process.on('close', (code) => {
84
+ if (process.env.KOI_DEBUG_LLM) {
85
+ console.error(`[MCP:${this.name}] Process exited with code ${code}`);
86
+ }
87
+ this.initialized = false;
88
+ this.process = null;
89
+
90
+ // Capture full stderr buffer for the LLM to interpret (already capped at 20 lines)
91
+ const stderrOutput = this._stderrLines.join('\n');
92
+ this.lastError = stderrOutput || null;
93
+
94
+ const errorMsg = stderrOutput
95
+ ? `MCP server "${this.name}" crashed (exit code ${code}). Server output:\n${stderrOutput}`
96
+ : `MCP server "${this.name}" crashed (exit code ${code}). No output captured.`;
97
+
98
+ // Reject all pending requests (process died)
99
+ for (const [id, pending] of this._pendingRequests) {
100
+ clearTimeout(pending.timeout);
101
+ pending.reject(new Error(errorMsg));
102
+ }
103
+ this._pendingRequests.clear();
104
+
105
+ // If we were still connecting, reject the connect promise
106
+ if (!this.initialized) {
107
+ clearTimeout(timeout);
108
+ reject(new Error(errorMsg));
109
+ }
110
+ });
111
+
112
+ // Perform initialize handshake
113
+ this._initialize().then(async () => {
114
+ // Send initialized notification
115
+ this._sendNotification('notifications/initialized', {});
116
+
117
+ // Cache tools
118
+ try {
119
+ const toolsResult = await this._sendRequest('tools/list', {});
120
+ this.tools = toolsResult.tools || [];
121
+ this.initialized = true;
122
+
123
+ if (process.env.KOI_DEBUG_LLM) {
124
+ console.error(`[MCP:${this.name}] Connected. ${this.tools.length} tools available: ${this.tools.map(t => t.name).join(', ')}`);
125
+ }
126
+
127
+ clearTimeout(timeout);
128
+ resolve();
129
+ } catch (err) {
130
+ clearTimeout(timeout);
131
+ reject(new Error(`[MCP:${this.name}] tools/list failed: ${err.message}`));
132
+ }
133
+ }).catch((err) => {
134
+ clearTimeout(timeout);
135
+ reject(err);
136
+ });
137
+ } catch (err) {
138
+ clearTimeout(timeout);
139
+ reject(new Error(`[MCP:${this.name}] Spawn error: ${err.message}`));
140
+ }
141
+ });
142
+ }
143
+
144
+ /**
145
+ * Send shutdown request and kill subprocess.
146
+ */
147
+ async disconnect() {
148
+ if (!this.process) return;
149
+
150
+ try {
151
+ // Try graceful shutdown
152
+ this._sendNotification('notifications/cancelled', {});
153
+ } catch (e) {
154
+ // Ignore errors during shutdown
155
+ }
156
+
157
+ // Kill the process
158
+ try {
159
+ this.process.kill('SIGTERM');
160
+ } catch (e) {
161
+ // Already dead
162
+ }
163
+
164
+ this.initialized = false;
165
+ this.process = null;
166
+ this.tools = [];
167
+ this._pendingRequests.clear();
168
+ this._buffer = '';
169
+ this._stderrLines = [];
170
+ }
171
+
172
+ /**
173
+ * Return cached tools list.
174
+ */
175
+ async listTools() {
176
+ if (!this.initialized) {
177
+ await this.connect();
178
+ }
179
+ return this.tools;
180
+ }
181
+
182
+ /**
183
+ * Call a tool on the MCP server.
184
+ * @param {string} toolName - Name of the tool
185
+ * @param {object} args - Tool input parameters
186
+ * @returns {object} Tool result
187
+ */
188
+ async callTool(toolName, args = {}) {
189
+ if (!this.initialized) {
190
+ await this.connect();
191
+ }
192
+
193
+ const result = await this._sendRequest('tools/call', {
194
+ name: toolName,
195
+ arguments: args
196
+ });
197
+
198
+ // Extract text content from MCP response format
199
+ if (result.content && Array.isArray(result.content)) {
200
+ const textParts = result.content
201
+ .filter(c => c.type === 'text')
202
+ .map(c => c.text);
203
+
204
+ if (textParts.length === 1) {
205
+ // Try to parse as JSON
206
+ try {
207
+ return JSON.parse(textParts[0]);
208
+ } catch (e) {
209
+ return { result: textParts[0] };
210
+ }
211
+ } else if (textParts.length > 1) {
212
+ return { result: textParts.join('\n') };
213
+ }
214
+ }
215
+
216
+ return result;
217
+ }
218
+
219
+ // ---- Private Protocol Methods ----
220
+
221
+ async _initialize() {
222
+ const result = await this._sendRequest('initialize', {
223
+ protocolVersion: '2024-11-05',
224
+ capabilities: {},
225
+ clientInfo: {
226
+ name: 'koi-mcp-client',
227
+ version: '1.0.0'
228
+ }
229
+ });
230
+
231
+ if (process.env.KOI_DEBUG_LLM) {
232
+ console.error(`[MCP:${this.name}] Server: ${result.serverInfo?.name || 'unknown'} v${result.serverInfo?.version || '?'}`);
233
+ }
234
+
235
+ return result;
236
+ }
237
+
238
+ _sendRequest(method, params) {
239
+ return new Promise((resolve, reject) => {
240
+ const id = ++this._requestId;
241
+ // tools/call can take minutes (e.g. mobile automation); handshake methods are fast
242
+ const timeoutMs = method === 'tools/call' ? 5 * 60 * 1000 : 30000;
243
+ const timeoutLabel = method === 'tools/call' ? '5m' : '30s';
244
+ const timeout = setTimeout(() => {
245
+ this._pendingRequests.delete(id);
246
+ reject(new Error(`[MCP:${this.name}] Request ${method} (id=${id}) timed out after ${timeoutLabel}`));
247
+ }, timeoutMs);
248
+
249
+ this._pendingRequests.set(id, { resolve, reject, timeout });
250
+
251
+ const message = JSON.stringify({
252
+ jsonrpc: '2.0',
253
+ id,
254
+ method,
255
+ params
256
+ });
257
+
258
+ if (process.env.KOI_DEBUG_LLM) {
259
+ console.error(`[MCP:${this.name}] → ${method} (id=${id})`);
260
+ }
261
+
262
+ try {
263
+ this.process.stdin.write(message + '\n');
264
+ } catch (err) {
265
+ clearTimeout(timeout);
266
+ this._pendingRequests.delete(id);
267
+ reject(new Error(`[MCP:${this.name}] Write failed: ${err.message}`));
268
+ }
269
+ });
270
+ }
271
+
272
+ _sendNotification(method, params) {
273
+ if (!this.process || !this.process.stdin.writable) return;
274
+
275
+ const message = JSON.stringify({
276
+ jsonrpc: '2.0',
277
+ method,
278
+ params
279
+ });
280
+
281
+ try {
282
+ this.process.stdin.write(message + '\n');
283
+ } catch (e) {
284
+ // Ignore write errors for notifications
285
+ }
286
+ }
287
+
288
+ _processBuffer() {
289
+ // Process newline-delimited JSON-RPC messages
290
+ let newlineIndex;
291
+ while ((newlineIndex = this._buffer.indexOf('\n')) !== -1) {
292
+ const line = this._buffer.substring(0, newlineIndex).trim();
293
+ this._buffer = this._buffer.substring(newlineIndex + 1);
294
+
295
+ if (line) {
296
+ this._handleLine(line);
297
+ }
298
+ }
299
+ }
300
+
301
+ _handleLine(line) {
302
+ let message;
303
+ try {
304
+ message = JSON.parse(line);
305
+ } catch (e) {
306
+ if (process.env.KOI_DEBUG_LLM) {
307
+ console.error(`[MCP:${this.name}] Failed to parse: ${line.substring(0, 200)}`);
308
+ }
309
+ return;
310
+ }
311
+
312
+ // Response to a request
313
+ if (message.id !== undefined && this._pendingRequests.has(message.id)) {
314
+ const pending = this._pendingRequests.get(message.id);
315
+ this._pendingRequests.delete(message.id);
316
+ clearTimeout(pending.timeout);
317
+
318
+ if (message.error) {
319
+ pending.reject(new Error(`[MCP:${this.name}] ${message.error.message || JSON.stringify(message.error)}`));
320
+ } else {
321
+ if (process.env.KOI_DEBUG_LLM) {
322
+ const preview = JSON.stringify(message.result).substring(0, 200);
323
+ console.error(`[MCP:${this.name}] ← (id=${message.id}) ${preview}`);
324
+ }
325
+ pending.resolve(message.result);
326
+ }
327
+ } else if (message.method) {
328
+ // Server notification or request (we don't handle server-initiated requests)
329
+ if (process.env.KOI_DEBUG_LLM) {
330
+ console.error(`[MCP:${this.name}] Notification: ${message.method}`);
331
+ }
332
+ }
333
+ }
334
+ }
@@ -11,7 +11,7 @@ import { actionRegistry } from './action-registry.js';
11
11
  export class Planner {
12
12
  constructor(config) {
13
13
  this.name = config.name || 'DefaultPlanner';
14
- this.llm = config.llm || { provider: 'openai', model: 'gpt-4o-mini', temperature: 0.3 };
14
+ this.llm = config.llm || { provider: 'openai', model: 'gpt-5.2', temperature: 0 };
15
15
  this.maxSteps = config.maxSteps || 10;
16
16
  this.allowReplanning = config.allowReplanning !== false;
17
17
  this.llmProvider = null;