From ff65f18f80f8a4974afcbbc1c3beb1673f7f3c1e Mon Sep 17 00:00:00 2001 From: Shivering Ocelot Date: Fri, 9 Oct 2026 14:44:23 -0500 Subject: [PATCH] import libraries, increase file read limit (still low). verify wip --- src/horde.js | 3 --- src/main.js | 59 ++++++++++++++++++++++++++++++++++++++++++++-------- src/run.js | 13 +++++++++--- 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/horde.js b/src/horde.js index 8be4272..d7b4579 100644 --- a/src/horde.js +++ b/src/horde.js @@ -130,9 +130,6 @@ export default class Horde { } sendAction(action, body = {}) { - if (!this.socket || this.socket.readyState !== WebSocket.OPEN) { - return Promise.reject(new Error('WebSocket is not open')); - } const nonce = this.nextNonce++; const promise = new Promise((resolve, reject) => { this.pending.set(nonce, { resolve, reject }); diff --git a/src/main.js b/src/main.js index 87747da..0b28ddd 100755 --- a/src/main.js +++ b/src/main.js @@ -19,7 +19,8 @@ import * as fs from 'fs'; import * as path from 'path'; -import { register } from 'node:module'; +import { registerHooks } from 'node:module'; +import { pathToFileURL, fileURLToPath } from 'url'; import Horde from './horde.js'; import run from './run.js'; @@ -28,6 +29,9 @@ const CONFIG_FILE = 'config.json'; const FUNCTIONS_DIR = 'functions'; const LIB_DIR = 'lib'; +// Prefix used by function modules to import shared libraries from '.gral/lib'. +const LIB_PREFIX = '@lib/'; + const DEFAULT_CONFIG = { host: 'wss://hub.hordelab.com', token: null, @@ -43,7 +47,6 @@ if (process.env.HORDE_TOKEN) DEFAULT_CONFIG.token = process.env.HORDE_TOKEN; /** * Global execution context exposed to function modules as `Gral`. - * For now it only carries the current working directory. */ const Gral = { path: process.cwd(), @@ -53,6 +56,49 @@ const Gral = { global.Gral = Gral; +/** + * Determine the ESM format for any file living inside the project's + * '.gral' directory, so function and library modules load as modules + * regardless of the surrounding workdir's 'package.json'. + * + * @param {string} url A file URL. + * @returns {?string} 'module' for Gral-owned files, otherwise null. + */ +function gralFormat(url) { + let filePath; + try { + filePath = fileURLToPath(url); + } catch (err) { + return null; + } + + const rel = path.relative(path.join(process.cwd(), CONFIG_DIR), filePath); + if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null; + return 'module'; +} + +/** + * Build the resolve hook used to load Gral modules. + * + * @param {string} root Directory the command was launched in. + * @param {string} libDir Absolute path to '.gral/lib'. + * @returns {object} Argument for `registerHooks`. + */ +function createGralHooks(root, libDir) { + return { + resolve(specifier, context, nextResolve) { + if (specifier.startsWith(LIB_PREFIX)) { + const target = path.join(libDir, specifier.slice(LIB_PREFIX.length)); + return { url: pathToFileURL(target).href, format: 'module', shortCircuit: true }; + } + + const result = nextResolve(specifier, context); + const format = result && result.url ? gralFormat(result.url) : null; + return format ? { ...result, format } : result; + }, + }; +} + /* ------------------------------------------------------------------ * * 3. Config loading * ------------------------------------------------------------------ */ @@ -215,7 +261,7 @@ async function main(argv) { // Make '.gral/lib/*' importable from function modules via '@lib/'. const libDir = path.join(root, CONFIG_DIR, LIB_DIR); - await register('./loader.js', import.meta.url, { data: { libDir } }); + registerHooks(createGralHooks(root, libDir)); const functions = await loadFunctions(root); Gral.horde = new Horde({host: config.host, token: config.token}); @@ -253,12 +299,7 @@ async function main(argv) { return 1; } - if (command.prompt === null) { - console.error("Error: a prompt is required (-p PROMPT or -f PROMPT_FILE)."); - return 1; - } - - const input = { prompt: command.prompt }; + const input = { prompt: command.prompt || null }; // Connect before executing await Gral.horde.connect(); diff --git a/src/run.js b/src/run.js index 3ac05d0..a1daf04 100644 --- a/src/run.js +++ b/src/run.js @@ -16,7 +16,7 @@ import * as fs from 'fs'; import * as path from 'path'; -const MAX_READ_BYTES = 1024; +const MAX_READ_BYTES = 10*1024; const MAX_ITERATIONS = 20; /* ------------------------------------------------------------------ * @@ -66,12 +66,13 @@ function readFile(args, { root, scope }) { let lines = fs.readFileSync(abs, 'utf8').split('\n'); const from = args.lineStart != null ? args.lineStart - 1 : 0; const to = args.lineEnd != null ? args.lineEnd : lines.length; + const lineCount = lines.length; lines = lines.slice(from, to); let content = lines.join('\n'); const bytes = Buffer.from(content, 'utf8'); if (bytes.length > MAX_READ_BYTES) { - content = bytes.slice(0, MAX_READ_BYTES).toString('utf8'); + throw new Error(`too large: ${bytes.length} bytes requested (max ${MAX_READ_BYTES}, ${lineCount} lines in ${file})`); } return content; } @@ -127,7 +128,7 @@ const DEFAULT_TOOLS = { 'read-file': { scope: 'read', handler: readFile, - description: 'Read a file.', + description: 'Read up to 10KB from a file, with optional line range.', parameters: { type: 'object', properties: { @@ -239,6 +240,7 @@ function makeExecutor(bundle, root) { try { return tool.handler(args || {}, { root, scope: tool.scope }); } catch (err) { + console.error(err); return `Error: ${err.message}`; } }; @@ -355,6 +357,7 @@ async function run({ open = null, tools = null, defaultTools = true, + verify = null, } = {}) { const Gral = global.Gral; const root = Gral && Gral.path ? Gral.path : process.cwd(); @@ -420,6 +423,10 @@ async function run({ if (calls.length) { console.log('called: ' + calls.map(c => c.tool_name)); } + + if (verify && verify()) { + return null; + } } throw new Error(`run: exceeded ${MAX_ITERATIONS} tool-call iterations`);