@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.
Files changed (4) hide show
  1. package/cli.js +125 -133
  2. package/package.json +1 -1
  3. package/src/parser.js +432 -94
  4. 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. All requests must be submitted via the HR portal.",
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 " [Mock] Summarized for demonstration.";
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 ` 📬 Notification sent to ${recipient}`;
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 null;
52
- const a = action.replace(/\{([^\}]+)\}/g, (_, k) => {
53
- const v = context[k.trim()];
54
- return v !== undefined ? v : `{${k}}`;
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); if (m) return parseFloat(m[1]) + parseFloat(m[2]);
58
- m = a.match(/^subtract\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) - parseFloat(m[2]);
59
- m = a.match(/^multiply\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) * parseFloat(m[2]);
60
- m = a.match(/^divide\(([^,]+),\s*([^)]+)\)$/i); if (m) return parseFloat(m[1]) / parseFloat(m[2]);
61
- m = a.match(/^sum\(\s*\[([^\]]+)\]\s*\)$/i); if (m) return m[1].split(',').map(s => parseFloat(s.trim())).reduce((s, v) => s + v, 0);
62
- return null;
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: returns the FIRST resolver that returns a non-undefined result.
78
+ * Resolver chaining
68
79
  */
69
80
  function createResolverChain(resolvers, verbose = false) {
70
81
  const wrapped = async (action, context) => {
71
- for (let i = 0; i < resolvers.length; i++) {
72
- const resolver = resolvers[i];
82
+ for (const resolver of resolvers) {
73
83
  try {
74
- const res = await resolver(action, context);
75
- if (res !== undefined) {
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(`[✅ ${resolver.resolverName || 'anonymous'}] handled action`);
87
+ console.log(`✅ ${resolver.resolverName} handled "${action}"`);
80
88
  }
81
- return res;
89
+ return result;
82
90
  }
83
- } catch (e) {
84
- console.error(` Resolver ${resolver.resolverName || 'anonymous'} failed:`, e.message);
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 — NO fallback to defaultMockResolver
103
+ * Load a single resolver
98
104
  */
99
105
  function loadSingleResolver(specifier) {
100
- if (!specifier) {
101
- throw new Error('Empty resolver specifier provided');
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, pkgName;
105
-
106
- // Extract clean package name (e.g., @o-lang/extractor → extractor)
107
- if (specifier.startsWith('./') || specifier.startsWith('../') || specifier.startsWith('/')) {
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 (e1) {
118
- try {
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
- // ✅ Auto-assign resolverName from package/filename if missing
134
- if (!resolver.resolverName || typeof resolver.resolverName !== 'string') {
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
- * ✅ POLICY-AWARE resolver loader: only includes resolvers in allowedResolvers
140
+ * Policy-aware resolver loader
146
141
  */
147
- function loadResolverChain(specifiers, verbose = false, allowedResolvers = new Set()) {
148
- const userResolvers = specifiers?.map(loadSingleResolver) || [];
142
+ function loadResolverChain(specifiers, verbose, allowed) {
149
143
  const resolvers = [];
150
144
 
151
- // Only add builtInMathResolver if allowed
152
- if (allowedResolvers.has('builtInMathResolver')) {
153
- resolvers.push(builtInMathResolver);
154
- }
145
+ if (allowed.has('builtInMathResolver')) resolvers.push(builtInMathResolver);
155
146
 
156
- // Add user resolvers only if their name is allowed
157
- for (const r of userResolvers) {
158
- const name = r.resolverName || r.name || 'unknown';
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
- // Only add defaultMockResolver if explicitly allowed
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 Setup
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
- '-r, --resolver <specifier>',
194
- 'Resolver (npm package or local path). Can be used multiple times.',
195
- (val, acc) => { acc.push(val); return acc; },
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
- try {
212
- ensureOlExtension(file);
213
- const content = fs.readFileSync(file, 'utf8');
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
- const allowedSet = new Set(workflow.allowedResolvers.map(r => r.trim()));
224
- const resolver = loadResolverChain(options.resolver, options.verbose, allowedSet);
181
+ const allowed = new Set(workflow.allowedResolvers);
182
+ const resolver = loadResolverChain(options.resolver, options.verbose, allowed);
225
183
 
226
- // 🔔 Warn if allowed resolvers declared but none loaded
227
- if (workflow.allowedResolvers.length > 0 && resolver._chain.length === 0) {
228
- console.warn(
229
- `\n⚠️ Warning: Workflow allows [${workflow.allowedResolvers.join(', ')}],\n` +
230
- ` but no matching resolvers were loaded. Use -r <path> to provide them.\n`
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
- const result = await execute(workflow, options.input, resolver, options.verbose);
235
- console.log('\n=== Workflow Result ===');
236
- console.log(JSON.stringify(result, null, 2));
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(' ❌ Error:', err.message);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.26",
3
+ "version": "1.1.1",
4
4
  "author": "Olalekan Ogundipe <info@workfily.com>",
5
5
  "description": "O-Lang: A governance language for user-directed, rule-enforced agent workflows",
6
6
  "main": "./src/index.js",