@o-lang/olang 1.0.2 → 1.0.4

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 CHANGED
@@ -5,6 +5,21 @@ const { execute } = require('./src/runtime');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
7
 
8
+ /**
9
+ * Enforce .ol extension ONLY
10
+ */
11
+ function ensureOlExtension(filename) {
12
+ if (!filename.endsWith('.ol')) {
13
+ throw new Error(
14
+ `Invalid file: "${filename}".\n` +
15
+ `O-Lang workflows must use the ".ol" extension.`
16
+ );
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Default mock resolver (for demo use)
22
+ */
8
23
  async function defaultMockResolver(action, context) {
9
24
  if (action.startsWith('Search for ')) {
10
25
  return {
@@ -13,20 +28,55 @@ async function defaultMockResolver(action, context) {
13
28
  url: "mock://hr-policy"
14
29
  };
15
30
  }
31
+
16
32
  if (action.startsWith('Ask ')) {
17
- return "✅ [Mock] Summarized for staff.";
33
+ return "✅ [Mock] Summarized for demonstration.";
18
34
  }
35
+
19
36
  if (action.startsWith('Notify ')) {
20
37
  const recipient = action.match(/Notify (\S+)/)?.[1] || 'user@example.com';
21
38
  return `📬 Notification sent to ${recipient}`;
22
39
  }
40
+
23
41
  if (action.startsWith('Debrief ') || action.startsWith('Evolve ')) {
24
42
  console.log(`[O-Lang] ${action}`);
25
43
  return 'Acknowledged';
26
44
  }
45
+
27
46
  return `[Unhandled: ${action}]`;
28
47
  }
29
48
 
49
+ /**
50
+ * Built-in Math Resolver
51
+ * Supports minimal math operations for workflows
52
+ */
53
+ async function builtInMathResolver(action, context) {
54
+ // Replace variables in context
55
+ action = action.replace(/\{([^\}]+)\}/g, (_, key) => {
56
+ const val = context[key.trim()];
57
+ return val !== undefined ? val : `{${key}}`;
58
+ });
59
+
60
+ let match;
61
+
62
+ match = action.match(/^add\(([^,]+),\s*([^)]+)\)$/);
63
+ if (match) return parseFloat(match[1]) + parseFloat(match[2]);
64
+
65
+ match = action.match(/^subtract\(([^,]+),\s*([^)]+)\)$/);
66
+ if (match) return parseFloat(match[1]) - parseFloat(match[2]);
67
+
68
+ match = action.match(/^multiply\(([^,]+),\s*([^)]+)\)$/);
69
+ if (match) return parseFloat(match[1]) * parseFloat(match[2]);
70
+
71
+ match = action.match(/^divide\(([^,]+),\s*([^)]+)\)$/);
72
+ if (match) return parseFloat(match[1]) / parseFloat(match[2]);
73
+
74
+ return null; // not handled
75
+ }
76
+
77
+ /**
78
+ * Resolver chaining mechanism
79
+ */
30
80
  function createResolverChain(resolvers) {
31
81
  return async (action, context) => {
32
82
  for (const resolver of resolvers) {
@@ -61,42 +111,65 @@ function loadSingleResolver(specifier) {
61
111
  console.log(`📁 Loaded resolver: ${absolutePath}`);
62
112
  return resolver;
63
113
  } catch (e2) {
64
- throw new Error(`Failed to load resolver '${specifier}':\n npm: ${e1.message}\n file: ${e2.message}`);
114
+ throw new Error(
115
+ `Failed to load resolver '${specifier}':\n npm: ${e1.message}\n file: ${e2.message}`
116
+ );
65
117
  }
66
118
  }
67
119
  }
68
120
 
121
+ /**
122
+ * Updated resolver chain
123
+ * Built-in math resolver is added first, then user resolvers, then default mock
124
+ */
69
125
  function loadResolverChain(specifiers) {
126
+ const userResolvers = specifiers?.map(loadSingleResolver) || [];
127
+ const resolvers = [builtInMathResolver, ...userResolvers, defaultMockResolver];
128
+
70
129
  if (!specifiers || specifiers.length === 0) {
71
- console.log('ℹ️ No resolver provided. Using default mock resolver.');
72
- return defaultMockResolver;
130
+ console.log('ℹ️ No resolver provided. Using built-in math + default mock resolver.');
131
+ } else {
132
+ console.log(`📦 Loaded user resolvers: ${specifiers.join(', ')}`);
73
133
  }
74
134
 
75
- const resolvers = specifiers.map(loadSingleResolver);
76
135
  return createResolverChain(resolvers);
77
136
  }
78
137
 
138
+ /**
139
+ * CLI Setup
140
+ */
79
141
  const program = new Command();
80
142
 
81
143
  program
82
144
  .name('olang')
83
- .description('O-Lang CLI: run .olang workflows')
145
+ .description('O-Lang CLI: run .ol workflows with rule-enforced agent governance')
84
146
  .command('run <file>')
85
- .option('-r, --resolver <specifier>', 'Resolver (npm package or local path). Can be used multiple times.\nExample:\n -r @o-lang/llm-groq\n -r @o-lang/notify-telegram', (val, acc) => {
86
- acc.push(val);
87
- return acc;
88
- }, [])
89
- .option('-i, --input <k=v>', 'Input parameters', (val, acc = {}) => {
90
- const [k, v] = val.split('=');
91
- acc[k] = v;
92
- return acc;
93
- }, {})
147
+ .option(
148
+ '-r, --resolver <specifier>',
149
+ 'Resolver (npm package or local path). Can be used multiple times.\nExample:\n -r @o-lang/llm-groq\n -r @o-lang/notify-telegram',
150
+ (val, acc) => { acc.push(val); return acc; },
151
+ []
152
+ )
153
+ .option(
154
+ '-i, --input <k=v>',
155
+ 'Input parameters',
156
+ (val, acc = {}) => {
157
+ const [k, v] = val.split('=');
158
+ acc[k] = isNaN(v) ? v : parseFloat(v);
159
+ return acc;
160
+ },
161
+ {}
162
+ )
94
163
  .action(async (file, options) => {
95
164
  try {
165
+ ensureOlExtension(file);
166
+
96
167
  const content = fs.readFileSync(file, 'utf8');
97
168
  const workflow = parse(content);
169
+
98
170
  const resolver = loadResolverChain(options.resolver);
99
171
  const result = await execute(workflow, options.input, resolver);
172
+
100
173
  console.log('\n=== Workflow Result ===');
101
174
  console.log(JSON.stringify(result, null, 2));
102
175
  } catch (err) {
@@ -106,10 +179,3 @@ program
106
179
  });
107
180
 
108
181
  program.parse(process.argv);
109
-
110
-
111
-
112
-
113
-
114
-
115
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
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",
package/src/parser.js CHANGED
@@ -1,5 +1,11 @@
1
1
  // src/parser.js
2
- function parse(code) {
2
+
3
+ function parse(code, fileName = null) {
4
+ // --- New: Enforce .ol extension if filename provided ---
5
+ if (fileName && !fileName.endsWith(".ol")) {
6
+ throw new Error(`Expected .ol workflow, got: ${fileName}`);
7
+ }
8
+
3
9
  const lines = code
4
10
  .split(/\r?\n/)
5
11
  .map(l => l.trim())
@@ -59,16 +65,11 @@ function parse(code) {
59
65
  let key = eq[1].trim();
60
66
  let value = eq[2].trim();
61
67
 
62
- // Parse list: [a, b, c]
63
68
  if (value.startsWith('[') && value.endsWith(']')) {
64
69
  value = value.slice(1, -1).split(',').map(v => v.trim().replace(/^"/, '').replace(/"$/, ''));
65
- }
66
- // Parse number
67
- else if (!isNaN(value)) {
70
+ } else if (!isNaN(value)) {
68
71
  value = Number(value);
69
- }
70
- // Keep string (remove quotes if present)
71
- else if (value.startsWith('"') && value.endsWith('"')) {
72
+ } else if (value.startsWith('"') && value.endsWith('"')) {
72
73
  value = value.slice(1, -1);
73
74
  }
74
75
 
@@ -119,7 +120,7 @@ function parse(code) {
119
120
  continue;
120
121
  }
121
122
 
122
- // Agent ... uses ...
123
+ // Agent uses
123
124
  const agentUseMatch = line.match(/^Agent\s+"([^"]+)"\s+uses\s+"([^"]+)"$/i);
124
125
  if (agentUseMatch) {
125
126
  workflow.steps.push({
@@ -199,7 +200,7 @@ function parse(code) {
199
200
  continue;
200
201
  }
201
202
 
202
- // --- New: Use <Tool> ---
203
+ // Use <Tool>
203
204
  const useMatch = line.match(/^Use\s+(.+)$/i);
204
205
  if (useMatch) {
205
206
  workflow.steps.push({
@@ -212,7 +213,7 @@ function parse(code) {
212
213
  continue;
213
214
  }
214
215
 
215
- // --- New: Ask <Target> ---
216
+ // Ask <Target>
216
217
  const askMatch = line.match(/^Ask\s+(.+)$/i);
217
218
  if (askMatch) {
218
219
  workflow.steps.push({
@@ -268,13 +269,11 @@ function parseBlock(lines) {
268
269
  steps.push({ type: 'prompt', question: promptMatch[1], saveAs: null });
269
270
  }
270
271
 
271
- // New: Use <Tool>
272
272
  const useMatch = line.match(/^Use\s+(.+)$/i);
273
273
  if (useMatch) {
274
274
  steps.push({ type: 'use', tool: useMatch[1].trim(), saveAs: null, constraints: {} });
275
275
  }
276
276
 
277
- // New: Ask <Target>
278
277
  const askMatch = line.match(/^Ask\s+(.+)$/i);
279
278
  if (askMatch) {
280
279
  steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
package/src/runtime.js CHANGED
@@ -24,9 +24,11 @@ class RuntimeAPI {
24
24
  evaluateCondition(cond, ctx) {
25
25
  cond = cond.trim();
26
26
  const eq = cond.match(/^\{(.+)\}\s+equals\s+"(.*)"$/);
27
- if (eq) return this.getNested(ctx, eq[1]) === eq[2];
27
+ if (eq) return this.getNested(ctx, eq[1]) == eq[2];
28
28
  const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
29
29
  if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
30
+ const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
31
+ if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
30
32
  return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
31
33
  }
32
34
 
@@ -45,8 +47,66 @@ class RuntimeAPI {
45
47
  return null;
46
48
  }
47
49
 
50
+ // --------------------------
51
+ // Math helper functions
52
+ // --------------------------
53
+ mathFunctions = {
54
+ add: (a, b) => a + b,
55
+ subtract: (a, b) => a - b,
56
+ multiply: (a, b) => a * b,
57
+ divide: (a, b) => a / b,
58
+ equals: (a, b) => a === b,
59
+ greater: (a, b) => a > b,
60
+ less: (a, b) => a < b,
61
+ sum: arr => arr.reduce((acc, val) => acc + val, 0),
62
+ avg: arr => arr.reduce((acc, val) => acc + val, 0) / arr.length,
63
+ min: arr => Math.min(...arr),
64
+ max: arr => Math.max(...arr),
65
+ increment: a => a + 1,
66
+ decrement: a => a - 1,
67
+ round: a => Math.round(a),
68
+ floor: a => Math.floor(a),
69
+ ceil: a => Math.ceil(a),
70
+ abs: a => Math.abs(a)
71
+ };
72
+
73
+ evaluateMath(expr) {
74
+ // Replace context variables in curly braces
75
+ expr = expr.replace(/\{([^\}]+)\}/g, (_, path) => {
76
+ const value = this.getNested(this.context, path.trim());
77
+ return value !== undefined ? value : 0;
78
+ });
79
+
80
+ // Create a function for supported math functions only
81
+ const funcNames = Object.keys(this.mathFunctions);
82
+ const safeFunc = {};
83
+ funcNames.forEach(fn => {
84
+ safeFunc[fn] = this.mathFunctions[fn];
85
+ });
86
+
87
+ try {
88
+ // eslint-disable-next-line no-new-func
89
+ const f = new Function(...funcNames, `return ${expr};`);
90
+ return f(...funcNames.map(fn => safeFunc[fn]));
91
+ } catch (e) {
92
+ console.warn(`[O-Lang] Failed to evaluate math expression "${expr}": ${e.message}`);
93
+ return 0;
94
+ }
95
+ }
96
+
97
+ // --------------------------
98
+ // Execute workflow step
99
+ // --------------------------
48
100
  async executeStep(step, agentResolver) {
49
101
  switch (step.type) {
102
+ case 'calculate': {
103
+ // e.g., "add({x}, {y})"
104
+ const expr = step.expression || step.actionRaw;
105
+ const result = this.evaluateMath(expr);
106
+ if (step.saveAs) this.context[step.saveAs] = result;
107
+ break;
108
+ }
109
+
50
110
  case 'action': {
51
111
  const action = step.actionRaw.replace(/\{([^\}]+)\}/g, (_, path) => {
52
112
  const value = this.getNested(this.context, path.trim());
@@ -58,14 +118,12 @@ class RuntimeAPI {
58
118
  }
59
119
 
60
120
  case 'use': {
61
- // "Use <Tool>" step
62
121
  const res = await agentResolver(`Use ${step.tool}`, this.context);
63
122
  if (step.saveAs) this.context[step.saveAs] = res;
64
123
  break;
65
124
  }
66
125
 
67
126
  case 'ask': {
68
- // "Ask <Target>" step
69
127
  const res = await agentResolver(`Ask ${step.target}`, this.context);
70
128
  if (step.saveAs) this.context[step.saveAs] = res;
71
129
  break;