OverviewFilesCommitsIssuesDocsReleasesPeers

knowledge-ratchet / atomspace / stream-tool.js

84 lines · javascript ref:
#!/usr/bin/env node

/**
 * stream-tool.js — Stream tool calls (Edit/Write) into AtomSpace in real-time
 *
 * Runs as async PostToolUse hook. On Edit/Write, creates an edge linking
 * the file to the current session. Used by preflight queries to detect
 * "you just edited this file" and "correction just happened near this file."
 */

const fs = require('fs')
const { isCogServerUp, evalScheme } = require('./cogserver')

const SESSION_FILE = '/tmp/claude-atomspace-session'

function esc (s) {
  return String(s || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')
}

function readSessionId () {
  try { return fs.readFileSync(SESSION_FILE, 'utf8').trim() } catch { return null }
}

function normalizePath (p) {
  if (!p) return ''
  return p
    .replace(/^\/storage\/projects\/code\/hypercore-kb\/hyper-fleeky\//, '')
    .replace(/^\/storage\/projects\/code\/hypercore-kb\//, '')
    .replace(/^\/storage\/projects\/code\//, '')
}

async function main () {
  let input = ''
  for await (const chunk of process.stdin) input += chunk

  let data
  try { data = JSON.parse(input) } catch { process.exit(0) }

  const toolName = data.tool_name || data.tool
  if (toolName !== 'Edit' && toolName !== 'Write') process.exit(0)

  const toolInput = data.tool_input || {}
  const filePath = toolInput.file_path || ''
  if (!filePath) process.exit(0)

  // Only track edits that actually succeeded (check tool_response if available)
  const response = data.tool_response || {}
  if (response.isError) process.exit(0)

  if (!await isCogServerUp()) process.exit(0)
  const sessionId = readSessionId()
  if (!sessionId) process.exit(0)

  const normalPath = normalizePath(filePath)
  if (!normalPath) process.exit(0)

  const timestamp = Math.floor(Date.now() / 1000)
  const sessionNode = `(ConceptNode "session:${sessionId}")`
  const fileNode = `(ConceptNode "file:${esc(normalPath)}")`
  const editId = `live:edit:${sessionId}:${timestamp}`
  const editNode = `(ConceptNode "${esc(editId)}")`

  const exprs = [
    fileNode,
    editNode,
    `(cog-set-value! ${editNode} (Predicate "kind") (StringValue "${toolName}"))`,
    `(cog-set-value! ${editNode} (Predicate "timestamp") (FloatValue ${timestamp}))`,
    // Link edit event → session
    `(EdgeLink (BondNode "edit-in-session") (ListLink ${editNode} ${sessionNode}))`,
    // Link edit event → file
    `(EdgeLink (BondNode "edit-of-file") (ListLink ${editNode} ${fileNode}))`,
    // Direct: session edited file (deduplicated because atoms are unique)
    `(EdgeLink (BondNode "session-edited") (ListLink ${sessionNode} ${fileNode}))`
  ]

  try {
    await evalScheme('(begin ' + exprs.join(' ') + ')')
  } catch {}

  process.exit(0)
}

main().catch(() => process.exit(0))