@o-lang/olang 1.0.20 → 1.0.22

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
@@ -64,25 +64,32 @@ async function builtInMathResolver(action, context) {
64
64
  builtInMathResolver.resolverName = 'builtInMathResolver';
65
65
 
66
66
  /**
67
- * Resolver chaining with verbose + context logging
67
+ * Resolver chaining: returns the FIRST resolver that returns a non-undefined result.
68
68
  */
69
69
  function createResolverChain(resolvers, verbose = false) {
70
- const chain = resolvers.slice();
71
70
  const wrapped = async (action, context) => {
72
- let lastResult;
73
- for (let i = 0; i < chain.length; i++) {
71
+ for (let i = 0; i < resolvers.length; i++) {
72
+ const resolver = resolvers[i];
74
73
  try {
75
- const res = await chain[i](action, context);
76
- context[`__resolver_${i}`] = res;
77
- lastResult = res;
74
+ const res = await resolver(action, context);
75
+ if (res !== undefined) {
76
+ // Store result in context for debugging
77
+ context[`__resolver_${i}`] = res;
78
+ if (verbose) {
79
+ console.log(`[✅ ${resolver.resolverName || 'anonymous'}] handled action`);
80
+ }
81
+ return res;
82
+ }
78
83
  } catch (e) {
79
- console.error(` ❌ Resolver ${i} failed for action "${action}":`, e.message);
84
+ console.error(` ❌ Resolver ${resolver.resolverName || 'anonymous'} failed:`, e.message);
80
85
  }
81
86
  }
82
- if (verbose) console.log(`[Resolver Chain] action="${action}" lastResult=`, lastResult);
83
- return lastResult;
87
+ if (verbose) {
88
+ console.log(`[⏭️] No resolver handled action: "${action}"`);
89
+ }
90
+ return undefined;
84
91
  };
85
- wrapped._chain = chain;
92
+ wrapped._chain = resolvers;
86
93
  return wrapped;
87
94
  }
88
95
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@o-lang/olang",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
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
@@ -29,25 +29,50 @@ function parse(code, fileName = null) {
29
29
  let line = lines[i];
30
30
 
31
31
  // ✅ FIXED: Match both "Allow resolvers" and "Allowed resolvers"
32
- const allowMatch = line.match(/^Allow(?:ed)?\s+resolvers\s*:\s*$/i);
32
+ const allowMatch = line.match(/^Allow resolvers\s*:\s*$/i);
33
33
  if (allowMatch) {
34
34
  i++;
35
- // Read all subsequent non-empty lines until a new top-level section (starts with capital letter)
36
35
  while (i < lines.length) {
37
36
  const nextLine = lines[i].trim();
38
- // Stop if line is empty or appears to be a new top-level keyword (e.g., Step, Workflow, Return)
37
+ // Stop if line is empty or looks like a new top-level section
39
38
  if (nextLine === '' || /^[A-Z][a-z]/.test(nextLine)) {
40
39
  break;
41
40
  }
42
- if (nextLine) {
43
- workflow.allowedResolvers.push(nextLine);
44
- workflow.resolverPolicy.declared.push(nextLine);
41
+ // Strip optional YAML list marker: "- name" → "name"
42
+ const cleanVal = nextLine.replace(/^\-\s*/, '').trim();
43
+ if (cleanVal) {
44
+ workflow.allowedResolvers.push(cleanVal);
45
+ workflow.resolverPolicy.declared.push(cleanVal);
45
46
  }
46
47
  i++;
47
48
  }
48
49
  continue;
49
50
  }
50
51
 
52
+ // ✅ NEW: Parse database Persist steps
53
+ const dbPersistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+db:([^\s]+)$/i);
54
+ if (dbPersistMatch) {
55
+ workflow.steps.push({
56
+ type: 'persist-db',
57
+ source: dbPersistMatch[1].trim(),
58
+ collection: dbPersistMatch[2].trim()
59
+ });
60
+ i++;
61
+ continue;
62
+ }
63
+
64
+ // ✅ Parse file Persist steps
65
+ const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]+)"$/i);
66
+ if (persistMatch) {
67
+ workflow.steps.push({
68
+ type: 'persist',
69
+ source: persistMatch[1].trim(),
70
+ destination: persistMatch[2].trim()
71
+ });
72
+ i++;
73
+ continue;
74
+ }
75
+
51
76
  // ---- Math step patterns (standalone) ----
52
77
  let mathAdd = line.match(/^Add\s+\{(.+?)\}\s+and\s+\{(.+?)\}\s+Save as\s+(.+)$/i);
53
78
  if (mathAdd) {
@@ -66,7 +91,7 @@ function parse(code, fileName = null) {
66
91
  workflow.__requiresMath = true;
67
92
  workflow.steps.push({
68
93
  type: 'calculate',
69
- expression: `subtract({${mathAdd[2]}}, {${mathAdd[1]}})`,
94
+ expression: `subtract({${mathSub[2]}}, {${mathSub[1]}})`, // Fixed: was mathAdd
70
95
  saveAs: mathSub[3].trim()
71
96
  });
72
97
  i++;
@@ -229,7 +254,7 @@ function parse(code, fileName = null) {
229
254
  }
230
255
 
231
256
  // ---------------------------
232
- // Parse nested blocks (unchanged)
257
+ // Parse nested blocks (updated)
233
258
  // ---------------------------
234
259
  function parseBlock(lines) {
235
260
  const steps = [];
@@ -269,6 +294,24 @@ function parseBlock(lines) {
269
294
  if (askMatch) {
270
295
  steps.push({ type: 'ask', target: askMatch[1].trim(), saveAs: null, constraints: {} });
271
296
  }
297
+ // ✅ Parse file Persist in blocks
298
+ const persistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+"([^"]+)"$/i);
299
+ if (persistMatch) {
300
+ steps.push({
301
+ type: 'persist',
302
+ source: persistMatch[1].trim(),
303
+ destination: persistMatch[2].trim()
304
+ });
305
+ }
306
+ // ✅ NEW: Parse database Persist in blocks
307
+ const dbPersistMatch = line.match(/^Persist\s+([^\s]+)\s+to\s+db:([^\s]+)$/i);
308
+ if (dbPersistMatch) {
309
+ steps.push({
310
+ type: 'persist-db',
311
+ source: dbPersistMatch[1].trim(),
312
+ collection: dbPersistMatch[2].trim()
313
+ });
314
+ }
272
315
  }
273
316
  return steps;
274
317
  }
package/src/runtime.js CHANGED
@@ -16,6 +16,80 @@ class RuntimeAPI {
16
16
  if (!fs.existsSync(logsDir)) fs.mkdirSync(logsDir, { recursive: true });
17
17
  this.disallowedLogFile = path.join(logsDir, 'disallowed_resolvers.json');
18
18
  this.disallowedAttempts = [];
19
+
20
+ // ✅ NEW: Database client setup
21
+ this.dbClient = null;
22
+ this._initDbClient();
23
+ }
24
+
25
+ // ✅ NEW: Initialize database client
26
+ _initDbClient() {
27
+ const dbType = process.env.OLANG_DB_TYPE; // 'postgres', 'mysql', 'mongodb', 'sqlite'
28
+
29
+ if (!dbType) return; // DB persistence disabled
30
+
31
+ try {
32
+ switch (dbType.toLowerCase()) {
33
+ case 'postgres':
34
+ case 'postgresql':
35
+ const { Pool } = require('pg');
36
+ this.dbClient = {
37
+ type: 'postgres',
38
+ client: new Pool({
39
+ host: process.env.DB_HOST || 'localhost',
40
+ port: parseInt(process.env.DB_PORT) || 5432,
41
+ user: process.env.DB_USER,
42
+ password: process.env.DB_PASSWORD,
43
+ database: process.env.DB_NAME
44
+ })
45
+ };
46
+ break;
47
+
48
+ case 'mysql':
49
+ const mysql = require('mysql2/promise');
50
+ this.dbClient = {
51
+ type: 'mysql',
52
+ client: mysql.createPool({
53
+ host: process.env.DB_HOST || 'localhost',
54
+ port: parseInt(process.env.DB_PORT) || 3306,
55
+ user: process.env.DB_USER,
56
+ password: process.env.DB_PASSWORD,
57
+ database: process.env.DB_NAME
58
+ })
59
+ };
60
+ break;
61
+
62
+ case 'mongodb':
63
+ const { MongoClient } = require('mongodb');
64
+ const uri = process.env.MONGO_URI || `mongodb://${process.env.DB_HOST || 'localhost'}:${process.env.DB_PORT || 27017}`;
65
+ this.dbClient = {
66
+ type: 'mongodb',
67
+ client: new MongoClient(uri)
68
+ };
69
+ break;
70
+
71
+ case 'sqlite':
72
+ const Database = require('better-sqlite3');
73
+ const dbPath = process.env.SQLITE_PATH || './olang.db';
74
+ const dbDir = path.dirname(path.resolve(dbPath));
75
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
76
+ this.dbClient = {
77
+ type: 'sqlite',
78
+ client: new Database(dbPath)
79
+ };
80
+ break;
81
+
82
+ default:
83
+ throw new Error(`Unsupported database type: ${dbType}`);
84
+ }
85
+
86
+ if (this.verbose) {
87
+ console.log(`🗄️ Database client initialized: ${dbType}`);
88
+ }
89
+ } catch (e) {
90
+ this.addWarning(`Failed to initialize DB client: ${e.message}`);
91
+ this.dbClient = null;
92
+ }
19
93
  }
20
94
 
21
95
  // -----------------------------
@@ -86,7 +160,7 @@ class RuntimeAPI {
86
160
  const gt = cond.match(/^\{(.+)\}\s+greater than\s+(\d+\.?\d*)$/);
87
161
  if (gt) return parseFloat(this.getNested(ctx, gt[1])) > parseFloat(gt[2]);
88
162
  const lt = cond.match(/^\{(.+)\}\s+less than\s+(\d+\.?\d*)$/);
89
- if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(gt[2]);
163
+ if (lt) return parseFloat(this.getNested(ctx, lt[1])) < parseFloat(lt[2]);
90
164
  return Boolean(this.getNested(ctx, cond.replace(/\{|\}/g, '')));
91
165
  }
92
166
 
@@ -265,6 +339,95 @@ class RuntimeAPI {
265
339
  this.emit('debrief', { agent: step.agent, message: step.message });
266
340
  break;
267
341
  }
342
+
343
+ // ✅ File Persist step handler
344
+ case 'persist': {
345
+ const sourceValue = this.getNested(this.context, step.source);
346
+ if (sourceValue === undefined) {
347
+ this.addWarning(`Cannot persist undefined value from "${step.source}" to "${step.destination}"`);
348
+ break;
349
+ }
350
+
351
+ const outputPath = path.resolve(process.cwd(), step.destination);
352
+ const outputDir = path.dirname(outputPath);
353
+ if (!fs.existsSync(outputDir)) {
354
+ fs.mkdirSync(outputDir, { recursive: true });
355
+ }
356
+
357
+ let content;
358
+ if (step.destination.endsWith('.json')) {
359
+ content = JSON.stringify(sourceValue, null, 2);
360
+ } else {
361
+ content = String(sourceValue);
362
+ }
363
+
364
+ fs.writeFileSync(outputPath, content, 'utf8');
365
+
366
+ if (this.verbose) {
367
+ console.log(`💾 Persisted "${step.source}" to ${step.destination}`);
368
+ }
369
+ break;
370
+ }
371
+
372
+ // ✅ NEW: Database persist handler
373
+ case 'persist-db': {
374
+ if (!this.dbClient) {
375
+ this.addWarning(`DB persistence skipped (no DB configured). Set OLANG_DB_TYPE env var.`);
376
+ break;
377
+ }
378
+
379
+ const sourceValue = this.getNested(this.context, step.source);
380
+ if (sourceValue === undefined) {
381
+ this.addWarning(`Cannot persist undefined value from "${step.source}" to DB collection "${step.collection}"`);
382
+ break;
383
+ }
384
+
385
+ try {
386
+ switch (this.dbClient.type) {
387
+ case 'postgres':
388
+ case 'mysql':
389
+ if (this.dbClient.type === 'postgres') {
390
+ await this.dbClient.client.query(
391
+ `INSERT INTO "${step.collection}" (workflow_name, data, created_at) VALUES ($1, $2, NOW())`,
392
+ [this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
393
+ );
394
+ } else {
395
+ await this.dbClient.client.execute(
396
+ `INSERT INTO ?? (workflow_name, data, created_at) VALUES (?, ?, NOW())`,
397
+ [step.collection, this.context.workflow_name || 'unknown', JSON.stringify(sourceValue)]
398
+ );
399
+ }
400
+ break;
401
+
402
+ case 'mongodb':
403
+ const db = this.dbClient.client.db(process.env.DB_NAME || 'olang');
404
+ await db.collection(step.collection).insertOne({
405
+ workflow_name: this.context.workflow_name || 'unknown',
406
+ sourceValue,
407
+ created_at: new Date()
408
+ });
409
+ break;
410
+
411
+ case 'sqlite':
412
+ const stmt = this.dbClient.client.prepare(
413
+ `INSERT INTO ${step.collection} (workflow_name, data, created_at) VALUES (?, ?, ?)`
414
+ );
415
+ stmt.run(
416
+ this.context.workflow_name || 'unknown',
417
+ JSON.stringify(sourceValue),
418
+ new Date().toISOString()
419
+ );
420
+ break;
421
+ }
422
+
423
+ if (this.verbose) {
424
+ console.log(`🗄️ Persisted "${step.source}" to DB collection ${step.collection}`);
425
+ }
426
+ } catch (e) {
427
+ this.addWarning(`DB persist failed for "${step.source}": ${e.message}`);
428
+ }
429
+ break;
430
+ }
268
431
  }
269
432
 
270
433
  if (this.verbose) {
@@ -274,7 +437,11 @@ class RuntimeAPI {
274
437
  }
275
438
 
276
439
  async executeWorkflow(workflow, inputs, agentResolver) {
277
- this.context = { ...inputs };
440
+ // Inject workflow name into context
441
+ this.context = {
442
+ ...inputs,
443
+ workflow_name: workflow.name
444
+ };
278
445
  this.workflowSteps = workflow.steps;
279
446
  this.allowedResolvers = new Set(workflow.allowedResolvers || []);
280
447
 
@@ -313,4 +480,4 @@ async function execute(workflow, inputs, agentResolver, verbose = false) {
313
480
  return rt.executeWorkflow(workflow, inputs, agentResolver);
314
481
  }
315
482
 
316
- module.exports = { execute, RuntimeAPI };
483
+ module.exports = { execute, RuntimeAPI };