@o-lang/olang 1.0.26 → 1.1.1
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/cli.js +125 -133
- package/package.json +1 -1
- package/src/parser.js +432 -94
- package/src/runtime.js +320 -42
package/cli.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
const { Command } = require('commander');
|
|
3
3
|
const { parse } = require('./src/parser');
|
|
4
4
|
const { execute } = require('./src/runtime');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
|
|
8
|
+
// === ADDED: Load package.json for version (1 line added) ===
|
|
9
|
+
const pkg = require('./package.json');
|
|
10
|
+
|
|
8
11
|
/**
|
|
9
|
-
* Enforce .ol extension ONLY
|
|
12
|
+
* Enforce .ol extension ONLY (CLI only)
|
|
10
13
|
*/
|
|
11
14
|
function ensureOlExtension(filename) {
|
|
12
15
|
if (!filename.endsWith('.ol')) {
|
|
@@ -22,24 +25,29 @@ function ensureOlExtension(filename) {
|
|
|
22
25
|
*/
|
|
23
26
|
async function defaultMockResolver(action, context) {
|
|
24
27
|
if (!action || typeof action !== 'string') return `[Unhandled: ${String(action)}]`;
|
|
28
|
+
|
|
25
29
|
if (action.startsWith('Search for ')) {
|
|
26
30
|
return {
|
|
27
31
|
title: "HR Policy 2025",
|
|
28
|
-
text: "Employees are entitled to 20 days of paid leave per year.
|
|
32
|
+
text: "Employees are entitled to 20 days of paid leave per year.",
|
|
29
33
|
url: "mock://hr-policy"
|
|
30
34
|
};
|
|
31
35
|
}
|
|
36
|
+
|
|
32
37
|
if (action.startsWith('Ask ')) {
|
|
33
|
-
return "
|
|
38
|
+
return "✅ [Mock] Summarized for demonstration.";
|
|
34
39
|
}
|
|
40
|
+
|
|
35
41
|
if (action.startsWith('Notify ')) {
|
|
36
42
|
const recipient = action.match(/Notify (\S+)/)?.[1] || 'user@example.com';
|
|
37
|
-
return
|
|
43
|
+
return `📬 Notification sent to ${recipient}`;
|
|
38
44
|
}
|
|
45
|
+
|
|
39
46
|
if (action.startsWith('Debrief ') || action.startsWith('Evolve ')) {
|
|
40
47
|
console.log(`[O-Lang] ${action}`);
|
|
41
48
|
return 'Acknowledged';
|
|
42
49
|
}
|
|
50
|
+
|
|
43
51
|
return `[Unhandled: ${action}]`;
|
|
44
52
|
}
|
|
45
53
|
defaultMockResolver.resolverName = 'defaultMockResolver';
|
|
@@ -48,45 +56,43 @@ defaultMockResolver.resolverName = 'defaultMockResolver';
|
|
|
48
56
|
* Built-in Math Resolver
|
|
49
57
|
*/
|
|
50
58
|
async function builtInMathResolver(action, context) {
|
|
51
|
-
if (!action || typeof action !== 'string') return
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
if (!action || typeof action !== 'string') return undefined;
|
|
60
|
+
|
|
61
|
+
const a = action.replace(/\{([^\}]+)\}/g, (_, k) =>
|
|
62
|
+
context[k.trim()] ?? `{${k}}`
|
|
63
|
+
);
|
|
64
|
+
|
|
56
65
|
let m;
|
|
57
|
-
m = a.match(/^add\(([^,]+),\s*([^)]+)\)$/i)
|
|
58
|
-
m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i)
|
|
59
|
-
m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i)
|
|
60
|
-
m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i)
|
|
61
|
-
m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i)
|
|
62
|
-
|
|
66
|
+
if ((m = a.match(/^add\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] + +m[2];
|
|
67
|
+
if ((m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] - +m[2];
|
|
68
|
+
if ((m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] * +m[2];
|
|
69
|
+
if ((m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i))) return +m[1] / +m[2];
|
|
70
|
+
if ((m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i)))
|
|
71
|
+
return m[1].split(',').map(Number).reduce((a, b) => a + b, 0);
|
|
72
|
+
|
|
73
|
+
return undefined;
|
|
63
74
|
}
|
|
64
75
|
builtInMathResolver.resolverName = 'builtInMathResolver';
|
|
65
76
|
|
|
66
77
|
/**
|
|
67
|
-
* Resolver chaining
|
|
78
|
+
* Resolver chaining
|
|
68
79
|
*/
|
|
69
80
|
function createResolverChain(resolvers, verbose = false) {
|
|
70
81
|
const wrapped = async (action, context) => {
|
|
71
|
-
for (
|
|
72
|
-
const resolver = resolvers[i];
|
|
82
|
+
for (const resolver of resolvers) {
|
|
73
83
|
try {
|
|
74
|
-
const
|
|
75
|
-
if (
|
|
76
|
-
// Store result in context for debugging
|
|
77
|
-
context[`__resolver_${i}`] = res;
|
|
84
|
+
const result = await resolver(action, context);
|
|
85
|
+
if (result !== undefined) {
|
|
78
86
|
if (verbose) {
|
|
79
|
-
console.log(
|
|
87
|
+
console.log(`✅ ${resolver.resolverName} handled "${action}"`);
|
|
80
88
|
}
|
|
81
|
-
return
|
|
89
|
+
return result;
|
|
82
90
|
}
|
|
83
|
-
} catch (
|
|
84
|
-
console.error(
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error(`❌ Resolver ${resolver.resolverName} failed:`, err.message);
|
|
85
93
|
}
|
|
86
94
|
}
|
|
87
|
-
if (verbose) {
|
|
88
|
-
console.log(`[⏭️] No resolver handled action: "${action}"`);
|
|
89
|
-
}
|
|
95
|
+
if (verbose) console.log(`⏭️ No resolver handled "${action}"`);
|
|
90
96
|
return undefined;
|
|
91
97
|
};
|
|
92
98
|
wrapped._chain = resolvers;
|
|
@@ -94,150 +100,136 @@ function createResolverChain(resolvers, verbose = false) {
|
|
|
94
100
|
}
|
|
95
101
|
|
|
96
102
|
/**
|
|
97
|
-
* Load a single resolver
|
|
103
|
+
* Load a single resolver
|
|
98
104
|
*/
|
|
99
105
|
function loadSingleResolver(specifier) {
|
|
100
|
-
if (!specifier)
|
|
101
|
-
|
|
106
|
+
if (!specifier) throw new Error('Empty resolver specifier');
|
|
107
|
+
|
|
108
|
+
if (specifier.endsWith('.json')) {
|
|
109
|
+
const manifest = JSON.parse(fs.readFileSync(specifier, 'utf8'));
|
|
110
|
+
if (manifest.protocol?.startsWith('http')) {
|
|
111
|
+
const externalResolver = async () => undefined;
|
|
112
|
+
externalResolver.resolverName = manifest.name;
|
|
113
|
+
externalResolver.manifest = manifest;
|
|
114
|
+
console.log(`🌐 Loaded external resolver: ${manifest.name}`);
|
|
115
|
+
return externalResolver;
|
|
116
|
+
}
|
|
102
117
|
}
|
|
103
118
|
|
|
104
|
-
let resolver
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// Local file: use filename without extension
|
|
109
|
-
pkgName = path.basename(specifier, path.extname(specifier));
|
|
110
|
-
} else {
|
|
111
|
-
// npm package: strip scope (e.g., @o-lang/extractor → extractor)
|
|
112
|
-
pkgName = specifier.replace(/^@[^/]+\//, '');
|
|
113
|
-
}
|
|
119
|
+
let resolver;
|
|
120
|
+
const pkgName = specifier.startsWith('.') || specifier.startsWith('/')
|
|
121
|
+
? path.basename(specifier, path.extname(specifier))
|
|
122
|
+
: specifier.replace(/^@[^/]+\//, '');
|
|
114
123
|
|
|
115
124
|
try {
|
|
116
125
|
resolver = require(specifier);
|
|
117
|
-
} catch
|
|
118
|
-
|
|
119
|
-
const absolutePath = path.resolve(process.cwd(), specifier);
|
|
120
|
-
resolver = require(absolutePath);
|
|
121
|
-
console.log(` 📁 Loaded resolver: ${absolutePath}`);
|
|
122
|
-
} catch (e2) {
|
|
123
|
-
throw new Error(
|
|
124
|
-
`Failed to load resolver '${specifier}':\n npm: ${e1.message}\n file: ${e2.message}`
|
|
125
|
-
);
|
|
126
|
-
}
|
|
126
|
+
} catch {
|
|
127
|
+
resolver = require(path.resolve(process.cwd(), specifier));
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
if (typeof resolver !== 'function') {
|
|
130
131
|
throw new Error(`Resolver must export a function`);
|
|
131
132
|
}
|
|
132
133
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
resolver.resolverName = pkgName;
|
|
136
|
-
console.log(` 🏷️ Auto-assigned resolverName: "${pkgName}" (from ${specifier})`);
|
|
137
|
-
} else {
|
|
138
|
-
console.log(` 📦 Loaded resolver: ${specifier} (name: ${resolver.resolverName})`);
|
|
139
|
-
}
|
|
140
|
-
|
|
134
|
+
resolver.resolverName ||= pkgName;
|
|
135
|
+
console.log(`📦 Loaded resolver: ${resolver.resolverName}`);
|
|
141
136
|
return resolver;
|
|
142
137
|
}
|
|
143
138
|
|
|
144
139
|
/**
|
|
145
|
-
*
|
|
140
|
+
* Policy-aware resolver loader
|
|
146
141
|
*/
|
|
147
|
-
function loadResolverChain(specifiers, verbose
|
|
148
|
-
const userResolvers = specifiers?.map(loadSingleResolver) || [];
|
|
142
|
+
function loadResolverChain(specifiers, verbose, allowed) {
|
|
149
143
|
const resolvers = [];
|
|
150
144
|
|
|
151
|
-
|
|
152
|
-
if (allowedResolvers.has('builtInMathResolver')) {
|
|
153
|
-
resolvers.push(builtInMathResolver);
|
|
154
|
-
}
|
|
145
|
+
if (allowed.has('builtInMathResolver')) resolvers.push(builtInMathResolver);
|
|
155
146
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
if (allowedResolvers.has(name)) {
|
|
160
|
-
resolvers.push(r);
|
|
161
|
-
} else if (verbose) {
|
|
162
|
-
console.warn(` ⚠️ Skipping disallowed user resolver: ${name}`);
|
|
163
|
-
}
|
|
147
|
+
for (const r of specifiers.map(loadSingleResolver)) {
|
|
148
|
+
if (allowed.has(r.resolverName)) resolvers.push(r);
|
|
149
|
+
else if (verbose) console.warn(`⚠️ Skipped disallowed resolver: ${r.resolverName}`);
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
|
|
167
|
-
if (allowedResolvers.has('defaultMockResolver')) {
|
|
168
|
-
resolvers.push(defaultMockResolver);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (resolvers.length === 0) {
|
|
172
|
-
if (verbose) {
|
|
173
|
-
console.warn(' ⚠️ No allowed resolvers loaded. Actions may fail.');
|
|
174
|
-
}
|
|
175
|
-
} else {
|
|
176
|
-
if (verbose) {
|
|
177
|
-
console.log(` ℹ️ Loaded allowed resolvers: ${resolvers.map(r => r.resolverName || 'anonymous').join(', ')}`);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
152
|
+
if (allowed.has('defaultMockResolver')) resolvers.push(defaultMockResolver);
|
|
180
153
|
|
|
181
154
|
return createResolverChain(resolvers, verbose);
|
|
182
155
|
}
|
|
183
156
|
|
|
184
157
|
/**
|
|
185
|
-
* CLI
|
|
158
|
+
* CLI SETUP
|
|
186
159
|
*/
|
|
187
160
|
const program = new Command();
|
|
161
|
+
|
|
162
|
+
// === ADDED: Version support (1 line added) ===
|
|
163
|
+
program.version(pkg.version, '-V, --version', 'Show O-lang kernel version');
|
|
164
|
+
|
|
165
|
+
// === RUN COMMAND ===
|
|
188
166
|
program
|
|
189
167
|
.name('olang')
|
|
190
|
-
.description('O-Lang CLI: run .ol workflows with rule-enforced agent governance')
|
|
191
168
|
.command('run <file>')
|
|
192
|
-
.option(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
(val
|
|
196
|
-
|
|
197
|
-
)
|
|
198
|
-
.option(
|
|
199
|
-
'-i, --input <k=v>',
|
|
200
|
-
'Input parameters',
|
|
201
|
-
(val, acc = {}) => {
|
|
202
|
-
const [k, v] = val.split('=');
|
|
203
|
-
const parsed = v === undefined ? '' : (v === 'true' ? true : (v === 'false' ? false : (isNaN(v) ? v : parseFloat(v))));
|
|
204
|
-
acc[k] = parsed;
|
|
205
|
-
return acc;
|
|
206
|
-
},
|
|
207
|
-
{}
|
|
208
|
-
)
|
|
209
|
-
.option('-v, --verbose', 'Verbose mode: logs resolver outputs and context after each step')
|
|
169
|
+
.option('-r, --resolver <specifier>', 'Resolver', (v, a) => (a.push(v), a), [])
|
|
170
|
+
.option('-i, --input <k=v>', 'Input', (v, a = {}) => {
|
|
171
|
+
const [k, val] = v.split('=');
|
|
172
|
+
a[k] = isNaN(val) ? val : Number(val);
|
|
173
|
+
return a;
|
|
174
|
+
}, {})
|
|
175
|
+
.option('-v, --verbose')
|
|
210
176
|
.action(async (file, options) => {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const workflow = parse(content, file);
|
|
215
|
-
if (!workflow || typeof workflow !== 'object') {
|
|
216
|
-
console.error(' ❌ Error: Parsed workflow is invalid or empty');
|
|
217
|
-
process.exit(1);
|
|
218
|
-
}
|
|
219
|
-
if (options.verbose) {
|
|
220
|
-
console.log(' 📄 Parsed Workflow:', JSON.stringify(workflow, null, 2));
|
|
221
|
-
}
|
|
177
|
+
ensureOlExtension(file);
|
|
178
|
+
const workflowSource = fs.readFileSync(file, 'utf8');
|
|
179
|
+
const workflow = parse(workflowSource, file);
|
|
222
180
|
|
|
223
|
-
|
|
224
|
-
|
|
181
|
+
const allowed = new Set(workflow.allowedResolvers);
|
|
182
|
+
const resolver = loadResolverChain(options.resolver, options.verbose, allowed);
|
|
225
183
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
184
|
+
const result = await execute(workflow, options.input, resolver, options.verbose);
|
|
185
|
+
console.log(JSON.stringify(result, null, 2));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// === SERVER COMMAND (✅ PROPER INTEGRATION) ===
|
|
189
|
+
program
|
|
190
|
+
.command('server')
|
|
191
|
+
.description('Start O-lang kernel in HTTP server mode')
|
|
192
|
+
.option('-p, --port <port>', 'Server port', process.env.OLANG_SERVER_PORT || '3000')
|
|
193
|
+
.option('-h, --host <host>', 'Server host', '0.0.0.0')
|
|
194
|
+
.action(async (options) => {
|
|
195
|
+
const fastify = require('fastify')({ logger: false });
|
|
196
|
+
|
|
197
|
+
fastify.get('/health', () => ({
|
|
198
|
+
status: 'healthy',
|
|
199
|
+
kernel: 'o-lang',
|
|
200
|
+
uptime: process.uptime()
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
fastify.post('/execute-workflow', async (req, reply) => {
|
|
204
|
+
try {
|
|
205
|
+
const { workflowSource, inputs = {}, resolvers = [], verbose = false } = req.body;
|
|
206
|
+
|
|
207
|
+
if (typeof workflowSource !== 'string') {
|
|
208
|
+
return reply.status(400).send({ error: 'workflowSource must be a string' });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const workflow = parse(workflowSource, 'remote.ol');
|
|
212
|
+
const allowed = new Set(workflow.allowedResolvers);
|
|
213
|
+
const resolver = loadResolverChain(resolvers, verbose, allowed);
|
|
214
|
+
|
|
215
|
+
const result = await execute(workflow, inputs, resolver, verbose);
|
|
216
|
+
reply.send(result);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
reply.status(500).send({ error: err.message });
|
|
232
219
|
}
|
|
220
|
+
});
|
|
233
221
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
222
|
+
const PORT = parseInt(options.port, 10);
|
|
223
|
+
const HOST = options.host;
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await fastify.listen({ port: PORT, host: HOST });
|
|
227
|
+
console.log(`✅ O-Lang Kernel running on http://${HOST}:${PORT}`);
|
|
237
228
|
} catch (err) {
|
|
238
|
-
console.error('
|
|
229
|
+
console.error('❌ Failed to start server:', err);
|
|
239
230
|
process.exit(1);
|
|
240
231
|
}
|
|
241
232
|
});
|
|
242
233
|
|
|
234
|
+
// === PARSE CLI ===
|
|
243
235
|
program.parse(process.argv);
|
package/package.json
CHANGED