OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / cogserver.js

130 lines · javascript ref:
/**
 * cogserver.js — CogServer TCP client for Node.js
 *
 * Sends Scheme s-expressions to CogServer and reads results.
 * Protocol: connect to TCP port, send "scm\n" to enter Scheme shell,
 * send expressions, read until "guile> " prompt.
 *
 * All functions gracefully return null/false if CogServer is not running.
 */

const net = require('net')

const DEFAULT_PORT = 17001
const DEFAULT_HOST = 'localhost'
const PROMPT_RE = /guile> /  // matches inside ANSI-colored output too
const OPENCOG_PROMPT_RE = /opencog> /
const ANSI_RE = /\x1b\[[0-9;]*m/g  // strip ANSI color codes
const CONNECT_TIMEOUT = 2000
const EVAL_TIMEOUT = 10000

function stripAnsi (str) {
  return str.replace(ANSI_RE, '')
}

/**
 * Check if CogServer is listening on the given port.
 * Returns true/false, never throws.
 */
async function isCogServerUp (port = DEFAULT_PORT, host = DEFAULT_HOST) {
  return new Promise((resolve) => {
    const sock = new net.Socket()
    sock.setTimeout(CONNECT_TIMEOUT)
    sock.once('connect', () => { sock.destroy(); resolve(true) })
    sock.once('error', () => { sock.destroy(); resolve(false) })
    sock.once('timeout', () => { sock.destroy(); resolve(false) })
    sock.connect(port, host)
  })
}

/**
 * Send a Scheme expression to CogServer and return the result string.
 * Returns null if CogServer is not reachable.
 *
 * @param {string} code - Scheme expression to evaluate
 * @param {number} port - CogServer port (default 17001)
 * @param {string} host - CogServer host (default localhost)
 * @returns {Promise<string|null>} - Result string or null on failure
 */
async function evalScheme (code, port = DEFAULT_PORT, host = DEFAULT_HOST) {
  return new Promise((resolve) => {
    const sock = new net.Socket()
    let buffer = ''
    let phase = 'wait-opencog'  // wait-opencog → wait-guile → wait-result
    let settled = false

    const finish = (result) => {
      if (settled) return
      settled = true
      sock.destroy()
      resolve(result)
    }

    sock.setTimeout(EVAL_TIMEOUT)
    sock.once('error', () => finish(null))
    sock.once('timeout', () => finish(null))

    sock.on('data', (chunk) => {
      buffer += chunk.toString()
      const clean = stripAnsi(buffer)

      if (phase === 'wait-opencog' && OPENCOG_PROMPT_RE.test(clean)) {
        // Got the initial opencog> prompt — enter Scheme shell
        phase = 'wait-guile'
        buffer = ''
        sock.write('scm\n')
        return
      }

      if (phase === 'wait-guile' && PROMPT_RE.test(clean)) {
        // Got the first guile> prompt — send our code
        phase = 'wait-result'
        buffer = ''
        sock.write(code + '\n')
        return
      }

      if (phase === 'wait-result' && PROMPT_RE.test(clean)) {
        // Got the guile> prompt after our code — extract result
        const cleanBuf = stripAnsi(buffer)
        const result = cleanBuf.replace(/guile>\s*/g, '').trim()
        finish(result)
      }
    })

    sock.connect(port, host)
  })
}

/**
 * Send multiple Scheme expressions in one connection.
 * More efficient than multiple evalScheme calls.
 * Returns array of result strings (null entries on failure).
 */
async function evalSchemeBatch (expressions, port = DEFAULT_PORT, host = DEFAULT_HOST) {
  if (!expressions.length) return []

  // Wrap all expressions in a begin block
  const combined = '(begin\n' + expressions.join('\n') + '\n)'
  const result = await evalScheme(combined, port, host)
  return result
}

/**
 * Send Scheme code without waiting for a meaningful result.
 * Useful for bulk atom creation where we don't need the return value.
 */
async function evalSchemeVoid (code, port = DEFAULT_PORT, host = DEFAULT_HOST) {
  const result = await evalScheme(code, port, host)
  return result !== null
}

module.exports = {
  isCogServerUp,
  evalScheme,
  evalSchemeBatch,
  evalSchemeVoid,
  DEFAULT_PORT,
  DEFAULT_HOST
}