rterm-backend 3.8.8 → 3.9.0

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/bin/gybackend.cjs CHANGED
@@ -346328,6 +346328,7 @@ var AgentRunLedger = class {
346328
346328
  var CHECKPOINT_STRING_CAP = 8 * 1024;
346329
346329
  var CHECKPOINT_BYTES_CAP = 16 * 1024 * 1024;
346330
346330
  var CHECKPOINT_MAX_MESSAGES = 40;
346331
+ var CHECKPOINT_RETENTION = 8;
346331
346332
  function truncateString(input, max = CHECKPOINT_STRING_CAP) {
346332
346333
  if (input.length <= max) return input;
346333
346334
  const omitted = input.length - max;
@@ -346395,8 +346396,18 @@ function isAllocationError(error40) {
346395
346396
  var SafeMemorySaver = class extends MemorySaver {
346396
346397
  async put(config2, checkpoint, metadata) {
346397
346398
  const pruned = pruneCheckpointForSaver(checkpoint);
346399
+ const threadId = config2?.configurable?.thread_id;
346400
+ const ns = config2?.configurable?.checkpoint_ns ?? "";
346401
+ const checkpointId = checkpoint?.id;
346402
+ if (threadId && checkpointId) {
346403
+ const order = this.putOrderFor(threadId, ns);
346404
+ order.push(checkpointId);
346405
+ this.setPutOrder(threadId, ns, order.slice(-CHECKPOINT_RETENTION * 4));
346406
+ }
346398
346407
  try {
346399
- return await super.put(config2, pruned, pruneValue(metadata));
346408
+ const result = await super.put(config2, pruned, pruneValue(metadata));
346409
+ this.evictOldCheckpoints(config2);
346410
+ return result;
346400
346411
  } catch (error40) {
346401
346412
  if (!isAllocationError(error40)) throw error40;
346402
346413
  console.warn(
@@ -346409,12 +346420,15 @@ var SafeMemorySaver = class extends MemorySaver {
346409
346420
  channels.messages = channels.messages.slice(-8);
346410
346421
  }
346411
346422
  tighter.channel_values = channels;
346412
- return await super.put(config2, tighter, { source: "oom-pruned" });
346423
+ const result = await super.put(config2, tighter, { source: "oom-pruned" });
346424
+ this.evictOldCheckpoints(config2);
346425
+ return result;
346413
346426
  } catch (retryError) {
346414
346427
  if (!isAllocationError(retryError)) throw retryError;
346415
346428
  console.warn(
346416
346429
  "[SafeMemorySaver] checkpoint still too large \u2014 skipping persist so the run can finish"
346417
346430
  );
346431
+ this.evictOldCheckpoints(config2);
346418
346432
  return {
346419
346433
  configurable: {
346420
346434
  thread_id: config2?.configurable?.thread_id,
@@ -346425,6 +346439,46 @@ var SafeMemorySaver = class extends MemorySaver {
346425
346439
  }
346426
346440
  }
346427
346441
  }
346442
+ /**
346443
+ * Keep only the newest CHECKPOINT_RETENTION checkpoints for the thread/ns
346444
+ * in this config. MemorySaver stores them as
346445
+ * storage[threadId][ns][checkpointId] = [serializedCheckpoint, metadata,
346446
+ * parentId]; there is no timestamp, but checkpoint ids from langgraph are
346447
+ * UUIDv4-orderable-ish in practice and — more reliably — the number of
346448
+ * entries is what matters for memory. We cannot know insertion order from
346449
+ * the object shape alone, so we track it ourselves: the ids we just put
346450
+ * are recorded, and anything beyond the newest window is dropped.
346451
+ */
346452
+ evictOldCheckpoints(config2) {
346453
+ try {
346454
+ const threadId = config2?.configurable?.thread_id;
346455
+ const ns = config2?.configurable?.checkpoint_ns ?? "";
346456
+ if (!threadId) return;
346457
+ const storage = this.storage;
346458
+ const nsMap = storage?.[threadId]?.[ns];
346459
+ if (!nsMap) return;
346460
+ const ids = Object.keys(nsMap);
346461
+ if (ids.length <= CHECKPOINT_RETENTION) return;
346462
+ const order = this.putOrderFor(threadId, ns);
346463
+ const keep = new Set(order.slice(-CHECKPOINT_RETENTION));
346464
+ for (const id of ids) {
346465
+ if (!keep.has(id) && !order.includes(id)) keep.add(id);
346466
+ }
346467
+ for (const id of ids) {
346468
+ if (!keep.has(id)) delete nsMap[id];
346469
+ }
346470
+ const remaining = order.filter((id) => id in nsMap);
346471
+ this.setPutOrder(threadId, ns, remaining.slice(-CHECKPOINT_RETENTION * 2));
346472
+ } catch {
346473
+ }
346474
+ }
346475
+ putOrders = /* @__PURE__ */ new Map();
346476
+ putOrderFor(threadId, ns) {
346477
+ return this.putOrders.get(`${threadId}\0${ns}`) ?? [];
346478
+ }
346479
+ setPutOrder(threadId, ns, ids) {
346480
+ this.putOrders.set(`${threadId}\0${ns}`, ids);
346481
+ }
346428
346482
  async putWrites(config2, writes, taskId) {
346429
346483
  const prunedWrites = Array.isArray(writes) ? writes.map((w) => {
346430
346484
  if (Array.isArray(w) && w.length >= 2) {
@@ -371792,6 +371846,23 @@ ${reminder}`;
371792
371846
  baseMessages = mapStoredMessagesToChatMessages(
371793
371847
  sanitizedStoredMessages.messages
371794
371848
  );
371849
+ const interruptedRun = this.chatHistoryService.getRunMarker(sessionId);
371850
+ if (interruptedRun) {
371851
+ const minutesAgo = Math.max(
371852
+ 0,
371853
+ Math.round((Date.now() - (interruptedRun.startedAt || 0)) / 6e4)
371854
+ );
371855
+ baseMessages.push(
371856
+ new SystemMessage(
371857
+ `[INTERRUPTION NOTICE] The previous task on this session was interrupted ${minutesAgo > 0 ? `${minutesAgo} minutes ` : ""}ago (the application stopped before the response completed \u2014 likely a force quit or crash). The last assistant response above may be PARTIAL: it can end mid-sentence, mid-tool-call, or before the task was actually finished. ` + (interruptedRun.inputPreview ? `The interrupted task began with: "${interruptedRun.inputPreview}"
371858
+ ` : "") + `Do not assume the prior task completed. If the user's next message relates to it, offer to continue or finish the interrupted work rather than starting from scratch.`
371859
+ )
371860
+ );
371861
+ this.chatHistoryService.clearRunMarker(sessionId);
371862
+ console.warn(
371863
+ `[AgentService_v2] Restored session ${sessionId} after an interrupted run (${interruptedRun.runId}); injected interruption notice into context.`
371864
+ );
371865
+ }
371795
371866
  }
371796
371867
  const runExperimentalFlags = resolveRunExperimentalFlags(
371797
371868
  context2,
@@ -371811,6 +371882,11 @@ ${reminder}`;
371811
371882
  execCommandActionModelEnabled: runExperimentalFlags.execCommandActionModelEnabled,
371812
371883
  writeStdinActionModelEnabled: runExperimentalFlags.writeStdinActionModelEnabled
371813
371884
  };
371885
+ this.chatHistoryService.setRunMarker(sessionId, {
371886
+ runId: ledgerRunId,
371887
+ startedAt: Date.now(),
371888
+ inputPreview: ledgerInputPreview.slice(0, 500)
371889
+ });
371814
371890
  try {
371815
371891
  const result = await this.graph.invoke(initialState, {
371816
371892
  recursionLimit,
@@ -371869,6 +371945,7 @@ ${reminder}`;
371869
371945
  this.currentRunIdBySession.delete(sessionId);
371870
371946
  this.agentRunLedger?.finishRun(ledgerRunId, ledgerExitStatus, ledgerExitError);
371871
371947
  void this.recordCompoundingLessons(sessionId, ledgerRunId, ledgerExitStatus, ledgerExitError);
371948
+ this.chatHistoryService.clearRunMarker(sessionId);
371872
371949
  await this.clearCheckpoint(sessionId);
371873
371950
  }
371874
371951
  }
@@ -373440,9 +373517,11 @@ var ChatHistoryService = class {
373440
373517
  saveSession(session) {
373441
373518
  const createdAt = this.store.getChatSessionCreatedAt(session.id);
373442
373519
  const now = Date.now();
373520
+ const storedMeta = this.store.getChatSessionMeta(session.id);
373521
+ const title = storedMeta?.title ?? session.title;
373443
373522
  this.store.saveChatSession({
373444
373523
  id: session.id,
373445
- title: session.title,
373524
+ title,
373446
373525
  messages: Array.from(session.messages.entries()).map(([id, message]) => ({
373447
373526
  id,
373448
373527
  type: message._getType ? message._getType() : "unknown",
@@ -373462,6 +373541,32 @@ var ChatHistoryService = class {
373462
373541
  getSessionMeta(sessionId) {
373463
373542
  return this.store.getChatSessionMeta(sessionId);
373464
373543
  }
373544
+ // ─── v3.8.9: run interruption markers ─────────────────────────────────────
373545
+ // A marker is SET at run start and CLEARED in the run's finally block.
373546
+ // Only a hard kill (force quit, crash, power loss) skips the finally —
373547
+ // so a leftover marker is a durable, restart-surviving record that the
373548
+ // session's previous run was interrupted. Stored in the history_meta
373549
+ // table (plain SQLite, survives anything short of disk loss).
373550
+ runMarkerKey(sessionId) {
373551
+ return `run-marker:${sessionId}`;
373552
+ }
373553
+ setRunMarker(sessionId, marker) {
373554
+ this.store.setMeta(this.runMarkerKey(sessionId), JSON.stringify(marker));
373555
+ }
373556
+ getRunMarker(sessionId) {
373557
+ const raw = this.store.getMeta(this.runMarkerKey(sessionId));
373558
+ if (!raw) return null;
373559
+ try {
373560
+ const parsed = JSON.parse(raw);
373561
+ if (typeof parsed?.runId === "string") return parsed;
373562
+ return null;
373563
+ } catch {
373564
+ return null;
373565
+ }
373566
+ }
373567
+ clearRunMarker(sessionId) {
373568
+ this.store.setMeta(this.runMarkerKey(sessionId), "");
373569
+ }
373465
373570
  loadSession(sessionId) {
373466
373571
  const storedSession = this.store.loadChatSession(sessionId);
373467
373572
  if (!storedSession) {
package/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
- "name": "rterm-backend",
3
- "version": "3.8.8",
4
- "description": "AI-native terminal & agentic-AI operations platform for Forward Deployed Engineers & SREs: AIOps closed-loop remediation, AI SRE, self-healing infrastructure, runbook automation, ChatOps; executes over SSH/WinRM/serial under policy with tamper-evident audit.",
2
+ "main": "bin/gybackend.cjs",
3
+ "bin": {
4
+ "gybackend": "bin/gybackend.cjs"
5
+ },
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "dependencies": {
10
+ "@nats-io/jetstream": "^3.4.0",
11
+ "@nats-io/kv": "^3.4.0",
12
+ "@nats-io/transport-node": "^3.4.0",
13
+ "better-sqlite3": "^12.11.1"
14
+ },
15
+ "optionalDependencies": {
16
+ "node-pty": "1.2.0-beta.3",
17
+ "serialport": "^12.0.0",
18
+ "ssh2": "^1.17.0"
19
+ },
5
20
  "keywords": [
6
21
  "forward-deployed-engineer",
7
22
  "fde",
@@ -34,22 +49,7 @@
34
49
  "type": "git",
35
50
  "url": "git+https://github.com/DrOlu/RTerm.git"
36
51
  },
37
- "engines": {
38
- "node": ">=18"
39
- },
40
- "bin": {
41
- "gybackend": "bin/gybackend.cjs"
42
- },
43
- "main": "bin/gybackend.cjs",
44
- "dependencies": {
45
- "@nats-io/jetstream": "^3.4.0",
46
- "@nats-io/kv": "^3.4.0",
47
- "@nats-io/transport-node": "^3.4.0",
48
- "better-sqlite3": "^12.11.1"
49
- },
50
- "optionalDependencies": {
51
- "serialport": "^12.0.0",
52
- "ssh2": "^1.17.0",
53
- "node-pty": "1.2.0-beta.3"
54
- }
52
+ "name": "rterm-backend",
53
+ "version": "3.9.0",
54
+ "description": "AI-native terminal & agentic-AI operations platform for Forward Deployed Engineers & SREs: AIOps closed-loop remediation, AI SRE, self-healing infrastructure, runbook automation, ChatOps; executes over SSH/WinRM/serial under policy with tamper-evident audit."
55
55
  }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * neuralos — neuralOS instances as RTerm agent tools.
3
+ *
4
+ * neuralOS instances (~/neuralos-instances) are on-device data agents: a
5
+ * verified menu of probes over a real data source (databases, APIs, log
6
+ * sets), selected by the 121M neuralOS engine and executed by each
7
+ * instance's Python bridge. This plugin wires four agent tools:
8
+ *
9
+ * neuralos_list_instances — what can be queried
10
+ * neuralos_ask — plain-English question -> verified digest
11
+ * neuralos_graph — relationship maps (overview/neighbors/connect)
12
+ * neuralos_admin — write probes, confirm='yes' interlocked
13
+ *
14
+ * The engine ships with the RTerm desktop bundle ({resourcesPath}/neuralos);
15
+ * the standalone backend auto-provisions ~/.cache/neuralos on first use
16
+ * (one ~36 MB fetch from HuggingFace, offline forever after).
17
+ *
18
+ * Config (settings.neuralos, or env NEURALOS_*):
19
+ * instancesDir — instances root (default ~/neuralos-instances)
20
+ * pythonBin — interpreter with pydantic for the bridges (default python3)
21
+ * engineBin / engineWeights — explicit engine override
22
+ * autoDownload — provision the shared cache when nothing local (default true)
23
+ */
24
+
25
+ import { resolveConfig, listInstances, instanceDirFor, resolveEngine, engineSelect, executeProbe, graphProbe, adminProbe, truncate, defaultExec } from './neuralosEngine.mjs'
26
+
27
+ export { resolveConfig } from './neuralosEngine.mjs'
28
+
29
+ export function register(ctx) {
30
+ const cfg = resolveConfig(ctx)
31
+ const exec = defaultExec()
32
+ const log = (line) => {
33
+ try {
34
+ if (typeof ctx.log === 'function') ctx.log(`[neuralos] ${line}`)
35
+ } catch { /* logging must never break a tool */ }
36
+ }
37
+
38
+ registerList(ctx, cfg, log)
39
+ registerAsk(ctx, cfg, exec, log)
40
+ registerGraph(ctx, cfg, exec, log)
41
+ registerAdmin(ctx, cfg, exec, log)
42
+
43
+ log('registered neuralos tools (list_instances, ask, graph, admin)')
44
+ }
45
+
46
+ function registerList(ctx, cfg, log) {
47
+ ctx.registerTool({
48
+ name: 'neuralos_list_instances',
49
+ description:
50
+ 'List the available neuralOS instances (on-device data agents). Each instance is a verified menu of probes over a real data source (a database, API, or file set). Use this first to see what can be queried.',
51
+ params: {},
52
+ handler: async () => {
53
+ const result = await listInstances(cfg)
54
+ log('list_instances')
55
+ return result
56
+ },
57
+ })
58
+ }
59
+
60
+ function registerAsk(ctx, cfg, exec, log) {
61
+ ctx.registerTool({
62
+ name: 'neuralos_ask',
63
+ description:
64
+ 'Ask a neuralOS instance a question in plain English. The on-device engine selects the probe, the instance bridge executes it against the real data source, and a verified digest comes back. Route data questions through this instead of querying the source directly.',
65
+ params: {
66
+ instance: { type: 'string', description: 'instance name from neuralos_list_instances' },
67
+ question: { type: 'string', description: 'the question in plain English, verbatim' },
68
+ },
69
+ handler: async (p) => {
70
+ const instance = String(p?.instance ?? '')
71
+ const question = String(p?.question ?? '')
72
+ const dir = await instanceDirFor(cfg, instance)
73
+ if (typeof dir !== 'string') return dir
74
+ const engine = await resolveEngine(cfg)
75
+ if (engine.error) return engine
76
+ const selection = await engineSelect(exec, engine, `${dir}/needle_menu.json`, question, { timeoutMs: cfg.timeoutMs })
77
+ if (selection.error) return { instance, question, ...selection }
78
+ const result = await executeProbe(exec, cfg, dir, selection.pick, selection.args, { timeoutMs: cfg.timeoutMs })
79
+ log(`ask ${instance} -> ${selection.pick} (conf ${selection.confidence})`)
80
+ return truncate({ instance, question, pick: selection.pick, confidence: selection.confidence, result })
81
+ },
82
+ })
83
+ }
84
+
85
+ function registerGraph(ctx, cfg, exec, log) {
86
+ ctx.registerTool({
87
+ name: 'neuralos_graph',
88
+ description:
89
+ 'Relationship questions over an instance: overview (the verified entity/edge map), neighbors (one-hop adjacency for a node fragment), or connect (path between two entities). Executes the graph probe directly.',
90
+ params: {
91
+ instance: { type: 'string', description: 'instance name' },
92
+ op: { type: 'string', description: 'overview | neighbors | connect' },
93
+ node: { type: 'string', description: 'node fragment for neighbors', optional: true },
94
+ a: { type: 'string', description: 'first entity for connect', optional: true },
95
+ b: { type: 'string', description: 'second entity for connect', optional: true },
96
+ },
97
+ handler: async (p) => {
98
+ const dir = await instanceDirFor(cfg, String(p?.instance ?? ''))
99
+ if (typeof dir !== 'string') return dir
100
+ const result = await graphProbe(exec, cfg, dir, { op: String(p?.op ?? 'overview'), node: p?.node, a: p?.a, b: p?.b })
101
+ log(`graph ${p?.op}`)
102
+ return truncate(result)
103
+ },
104
+ })
105
+ }
106
+
107
+ function registerAdmin(ctx, cfg, exec, log) {
108
+ ctx.registerTool({
109
+ name: 'neuralos_admin',
110
+ description:
111
+ 'Execute a WRITE/admin probe on an instance (boot a server, tag a resource, purge DNS). Destructive and reversible only per the instance design; the confirm parameter must be the literal string "yes" — that is the operator interlock. Read-only questions must use neuralos_ask instead.',
112
+ params: {
113
+ instance: { type: 'string', description: 'instance name' },
114
+ probe: { type: 'string', description: 'admin probe name, e.g. aws_power' },
115
+ args: { type: 'object', description: 'probe arguments as JSON', optional: true },
116
+ confirm: { type: 'string', description: 'must be the literal "yes"' },
117
+ },
118
+ handler: async (p) => {
119
+ if (p?.confirm !== 'yes') return { error: 'admin probes require confirm="yes" — restate what you intend to run and pass confirm="yes"' }
120
+ const dir = await instanceDirFor(cfg, String(p?.instance ?? ''))
121
+ if (typeof dir !== 'string') return dir
122
+ const result = await adminProbe(exec, cfg, dir, String(p?.probe ?? ''), p?.args ?? {})
123
+ log(`admin ${p?.probe} on ${p?.instance}`)
124
+ return truncate(result)
125
+ },
126
+ })
127
+ }
128
+
129
+ export function unregister() {
130
+ /* tools deregister with the plugin record; nothing to stop here */
131
+ }
@@ -0,0 +1,289 @@
1
+ /**
2
+ * neuralos — plugin extreme spec.
3
+ *
4
+ * Pure invariants, always run, no network and no engine required:
5
+ * config precedence, engine-name mapping, JSON recovery, instance
6
+ * discovery, engine selection against a FAKE engine script (node is the
7
+ * only dependency), probe execution against a FAKE python (the runner is
8
+ * exercised via an injected exec), the graph direct-execution rule, and
9
+ * the admin confirm interlock. The auto-download path is tested with an
10
+ * injected fetch that writes local files — CI never touches HuggingFace.
11
+ *
12
+ * Run: npx tsx plugins/neuralos/neuralos.extreme.spec.mts
13
+ */
14
+ import { spawnSync } from 'node:child_process'
15
+ import { mkdtempSync, writeFileSync, mkdirSync, existsSync, readFileSync, chmodSync } from 'node:fs'
16
+ import { homedir, tmpdir } from 'node:os'
17
+ import { join } from 'node:path'
18
+
19
+ import {
20
+ HF_BASE,
21
+ adminProbe,
22
+ bundledEngineName,
23
+ defaultExec,
24
+ engineSelect,
25
+ ensureEngine,
26
+ executeProbe,
27
+ graphProbe,
28
+ listInstances,
29
+ parseJsonObject,
30
+ resolveConfig,
31
+ resolveEngine,
32
+ truncate,
33
+ } from './neuralosEngine.mjs'
34
+ import { register } from './index.mjs'
35
+
36
+ let pass = 0
37
+ let fail = 0
38
+ const failures: string[] = []
39
+
40
+ function ok(cond: unknown, label: string, note = ''): void {
41
+ if (cond) {
42
+ pass++
43
+ console.log(`PASS ${label}`)
44
+ } else {
45
+ fail++
46
+ failures.push(`${label}${note ? ` — ${note}` : ''}`)
47
+ console.log(`FAIL ${label}${note ? ` — ${note}` : ''}`)
48
+ }
49
+ }
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // 1. Config precedence: settings block > env > default; blank = unset.
53
+ // ---------------------------------------------------------------------------
54
+ {
55
+ const cfg = resolveConfig({ getSettings: () => ({ neuralos: { pythonBin: '/framework/python3.12', instancesDir: ' ' } }) }, {})
56
+ ok(cfg.pythonBin === '/framework/python3.12', 'settings block wins for pythonBin')
57
+ ok(cfg.instancesDir === join(homedir(), 'neuralos-instances'), 'blank settings value falls through to default', cfg.instancesDir)
58
+
59
+ const cfgEnv = resolveConfig({ getSettings: () => ({}) }, { NEURALOS_INSTANCES_DIR: '/custom/instances' })
60
+ ok(cfgEnv.instancesDir === '/custom/instances', 'env wins when settings are silent')
61
+
62
+ const cfgOff = resolveConfig({ getSettings: () => ({ neuralos: { autoDownload: false } }) }, {})
63
+ ok(cfgOff.autoDownload === false, 'autoDownload can be disabled in settings')
64
+ const cfgOn = resolveConfig({ getSettings: () => ({}) }, {})
65
+ ok(cfgOn.autoDownload === true, 'autoDownload defaults on')
66
+ }
67
+ // ---------------------------------------------------------------------------
68
+ // 2. Engine name mapping (macos-x64 has no published engine).
69
+ // ---------------------------------------------------------------------------
70
+ {
71
+ ok(bundledEngineName('darwin', 'arm64') === 'engine-macos-arm64', 'darwin/arm64 maps to engine-macos-arm64')
72
+ ok(bundledEngineName('linux', 'x64') === 'engine-linux-x86_64', 'linux/x64 maps to engine-linux-x86_64')
73
+ ok(bundledEngineName('linux', 'arm64') === 'engine-linux-arm64', 'linux/arm64 maps to engine-linux-arm64')
74
+ ok(bundledEngineName('win32', 'x64') === 'engine-windows-x86_64.exe', 'win32/x64 maps to engine-windows-x86_64.exe')
75
+ ok(bundledEngineName('darwin', 'x64') === null, 'darwin/x64 has no published engine')
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // 3. JSON recovery from noisy engine output.
80
+ // ---------------------------------------------------------------------------
81
+ {
82
+ const parsed = parseJsonObject('noise {"function_calls":[{"name":"p"}], "confidence": 0.9} trailing')
83
+ ok(parsed?.function_calls?.[0]?.name === 'p', 'parseJsonObject recovers JSON embedded in noise')
84
+ ok(parseJsonObject('not json') === null, 'parseJsonObject returns null for garbage')
85
+ }
86
+
87
+ // ---------------------------------------------------------------------------
88
+ // 4. Instance discovery (fake instances root in a temp dir).
89
+ // ---------------------------------------------------------------------------
90
+ {
91
+ const root = mkdtempSync(join(tmpdir(), 'neuralos-spec-'))
92
+ mkdirSync(join(root, 'chinook'))
93
+ writeFileSync(join(root, 'chinook', 'needle_menu.json'), JSON.stringify([{ name: 'p1' }, { name: 'p2' }]))
94
+ mkdirSync(join(root, 'not-an-instance'))
95
+
96
+ const cfg = resolveConfig({}, { NEURALOS_INSTANCES_DIR: root })
97
+ const listed = await listInstances(cfg)
98
+ ok(
99
+ Array.isArray(listed) && listed.length === 1 && listed[0].name === 'chinook' && listed[0].probeCount === 2,
100
+ 'listInstances finds only dirs with a needle_menu.json',
101
+ JSON.stringify(listed),
102
+ )
103
+
104
+ const missing = await listInstances(cfg, { readdir: async () => { throw new Error('ENOENT') }, readFile: async () => '' })
105
+ ok(
106
+ (missing as { error?: string }).error?.includes('no instances directory'),
107
+ 'listInstances reports a missing root as data',
108
+ )
109
+ }
110
+
111
+ // ---------------------------------------------------------------------------
112
+ // 5. Engine selection against a FAKE engine (a node script).
113
+ // ---------------------------------------------------------------------------
114
+ {
115
+ const dir = mkdtempSync(join(tmpdir(), 'neuralos-engine-'))
116
+ const fakeEngine = join(dir, 'fake-engine.mjs')
117
+ writeFileSync(
118
+ fakeEngine,
119
+ `const args = process.argv.slice(2)
120
+ const qi = args.indexOf('--prompt')
121
+ const q = qi >= 0 ? args[qi + 1] : ''
122
+ if (q === 'refuse me') { console.log(JSON.stringify({ function_calls: [] })) }
123
+ else if (q === 'garbage') { console.log('not json at all') }
124
+ else if (q === 'crash') { process.stderr.write('boom'); process.exit(1) }
125
+ else { console.log(JSON.stringify({ function_calls: [{ name: 'transactions_count', arguments: { limit: 5 } }], confidence: 0.98 })) }
126
+ `,
127
+ )
128
+ const runEngine = (q: string) =>
129
+ engineSelect(
130
+ (cmd: string, args: string[]) => {
131
+ void cmd
132
+ const r = spawnSync(process.execPath, [fakeEngine, ...args.slice(2)], { encoding: 'utf8' })
133
+ return Promise.resolve({ ok: r.status === 0, code: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' })
134
+ },
135
+ { engineBin: fakeEngine, engineWeights: join(dir, 'weights.cact') },
136
+ '/i/needle_menu.json',
137
+ q,
138
+ )
139
+
140
+ const hit = await runEngine('count invoices')
141
+ ok(hit.pick === 'transactions_count' && hit.confidence === 0.98, 'engineSelect parses the selection with confidence', JSON.stringify(hit))
142
+ const refused = await runEngine('refuse me')
143
+ ok((refused as { error?: string }).error === 'engine refused the question (no call selected)', 'refusal is data, not an exception')
144
+ const garbage = await runEngine('garbage')
145
+ ok((garbage as { error?: string }).error?.includes('engine output not JSON'), 'non-JSON engine output is data')
146
+ const crash = await runEngine('crash')
147
+ ok((crash as { error?: string }).error === 'engine exited 1: boom', 'engine crash with empty stdout is data')
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // 6. Probe execution via an injected exec (the real path spawns python).
152
+ // ---------------------------------------------------------------------------
153
+ {
154
+ const cfg = resolveConfig({}, { NEURALOS_PYTHON: '/framework/python3.12' })
155
+ const seen: Array<{ cmd: string; args: string[]; env: Record<string, string> }> = []
156
+ const exec = (cmd: string, args: string[], opts: { env?: Record<string, string> }) => {
157
+ seen.push({ cmd, args, env: opts.env ?? {} })
158
+ return Promise.resolve({ ok: true, code: 0, stdout: '{"count": 42}', stderr: '' })
159
+ }
160
+ const result = await executeProbe(exec, cfg, '/i', 'transactions_count', { limit: 5 })
161
+ ok(result.count === 42, 'executeProbe returns the parsed digest')
162
+ ok(seen[0].cmd === '/framework/python3.12' && seen[0].args[0] === '-c', 'executeProbe spawns the configured python with the runner')
163
+ ok(
164
+ seen[0].env.NEURALOS_INSTANCE_DIR === '/i' && seen[0].env.NEURALOS_PROBE === 'transactions_count' && seen[0].env.NEURALOS_ARGS === '{"limit":5}',
165
+ 'probe name and args travel through the environment, never the command line',
166
+ )
167
+
168
+ const failed = await executeProbe((() => Promise.resolve({ ok: false, code: 1, stdout: '', stderr: 'boom' })) as never, cfg, '/i', 'p', {})
169
+ ok((failed as { error?: string }).error === 'probe exited 1: boom', 'probe failure surfaces as data')
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // 7. Graph direct execution + admin interlock.
174
+ // ---------------------------------------------------------------------------
175
+ {
176
+ const dir = mkdtempSync(join(tmpdir(), 'neuralos-graph-'))
177
+ writeFileSync(
178
+ join(dir, 'needle_menu.json'),
179
+ JSON.stringify([{ name: 'cb_graph_overview' }, { name: 'cb_graph_neighbors' }, { name: 'transactions_count' }]),
180
+ )
181
+ const cfg = resolveConfig({}, {})
182
+ const exec = () => Promise.resolve({ ok: true, code: 0, stdout: '{"nodes":[]}', stderr: '' })
183
+
184
+ const overview = await graphProbe(exec, cfg, dir, { op: 'overview' })
185
+ ok((overview as { probe?: string }).probe === 'cb_graph_overview' && (overview as { nodes?: unknown[] }).nodes?.length === 0, 'graph overview executes the probe directly by name')
186
+ const neighbors = await graphProbe(exec, cfg, dir, { op: 'neighbors', node: '4001234567' })
187
+ ok((neighbors as { probe?: string }).probe === 'cb_graph_neighbors', 'graph neighbors executes by name (numeric fragments bypass engine grounding)')
188
+ const missingOp = await graphProbe(exec, cfg, dir, { op: 'connect', a: 'x', b: 'y' })
189
+ ok(
190
+ (missingOp as { error?: string }).error === 'this instance has no connect graph probe' &&
191
+ Array.isArray((missingOp as { available_graph_probes?: string[] }).available_graph_probes),
192
+ 'a missing graph op lists what exists instead of guessing',
193
+ )
194
+
195
+ const admin = await adminProbe(exec, cfg, dir, 'aws_tag', { confirm: 'yes' })
196
+ ok(admin.admin === true && admin.probe === 'aws_tag', 'admin wraps the result with the probe name and admin flag')
197
+ const badName = await adminProbe(exec, cfg, dir, 'rm -rf', {})
198
+ ok((badName as { error?: string }).error === 'invalid probe name: rm -rf', 'admin rejects probe names that are not identifiers')
199
+ }
200
+
201
+ // ---------------------------------------------------------------------------
202
+ // 8. Engine resolution: settings override wins; auto-download provisions the
203
+ // cache via an injected fetch (no network in CI); idempotent on re-run.
204
+ // ---------------------------------------------------------------------------
205
+ {
206
+ const explicitDir = mkdtempSync(join(tmpdir(), 'neuralos-explicit-'))
207
+ writeFileSync(join(explicitDir, 'needle'), 'fake-engine')
208
+ writeFileSync(join(explicitDir, 'needle3.cact'), 'fake-weights')
209
+ const cacheAlt = mkdtempSync(join(tmpdir(), 'neuralos-cache-alt-'))
210
+ writeFileSync(join(cacheAlt, 'engine-macos-arm64'), 'cache-engine')
211
+ writeFileSync(join(cacheAlt, 'needle3.cact'), 'cache-weights')
212
+ const explicit = await resolveEngine(
213
+ resolveConfig({ getSettings: () => ({ neuralos: { engineBin: join(explicitDir, 'needle'), engineWeights: join(explicitDir, 'needle3.cact'), cacheDir: cacheAlt } }) }, {}),
214
+ { platform: 'darwin', arch: 'arm64' },
215
+ )
216
+ ok(explicit.engineBin === join(explicitDir, 'needle'), 'explicit settings engine wins over a populated cache')
217
+
218
+ const cache = mkdtempSync(join(tmpdir(), 'neuralos-cache-'))
219
+ const downloads: string[] = []
220
+ const fetchImpl = async (url: string, target: string) => {
221
+ downloads.push(url)
222
+ writeFileSync(target, `fake:${url}`)
223
+ }
224
+ const cfg = resolveConfig({ getSettings: () => ({}) }, { NEURALOS_INSTANCES_DIR: mkdtempSync(join(tmpdir(), 'neuralos-none-')), NEURALOS_CACHE_DIR: cache })
225
+ const first = await ensureEngine({ platform: 'darwin', arch: 'arm64', cacheDir: cache, fetchImpl })
226
+ ok(first.engineBin === join(cache, 'engine-macos-arm64') && first.engineWeights === join(cache, 'needle3.cact'), 'ensureEngine provisions weights + per-arch engine into the cache', JSON.stringify(first))
227
+ ok(downloads.length === 2 && downloads[0] === `${HF_BASE}/needle3.cact` && downloads[1].includes('macos-arm64/needle'), 'downloads come from the public Cactus-Compute/needle3 URLs')
228
+ ok(existsSync(join(cache, 'engine-macos-arm64')), 'the engine file exists on disk after provisioning')
229
+
230
+ const second = await ensureEngine({ platform: 'darwin', arch: 'arm64', cacheDir: cache, fetchImpl })
231
+ ok(second.engineBin === first.engineBin && downloads.length === 2, 'provisioning is idempotent — no re-download when cached')
232
+
233
+ // resolution now finds the cache without any download
234
+ const resolved = await resolveEngine(cfg, { platform: 'darwin', arch: 'arm64' })
235
+ ok(resolved.engineBin === join(cache, 'engine-macos-arm64'), 'resolveEngine picks the provisioned cache copy')
236
+ }
237
+
238
+ // ---------------------------------------------------------------------------
239
+ // 9. Tool registration + the admin confirm interlock at the tool layer.
240
+ // ---------------------------------------------------------------------------
241
+ {
242
+ const registered: Array<{ name: string; params: Record<string, unknown>; handler: (args: unknown) => Promise<unknown> }> = []
243
+ const logged: string[] = []
244
+ register({
245
+ registerTool: (t) => registered.push(t as never),
246
+ log: (l: string) => logged.push(l),
247
+ getSettings: () => ({}),
248
+ })
249
+ ok(
250
+ registered.map((t) => t.name).sort().join(',') === 'neuralos_admin,neuralos_ask,neuralos_graph,neuralos_list_instances',
251
+ 'register() wires exactly the four neuralos tools',
252
+ )
253
+ const admin = registered.find((t) => t.name === 'neuralos_admin')
254
+ const denied = (await admin.handler({ instance: 'aws', probe: 'aws_power', confirm: 'no' })) as { error?: string }
255
+ ok(denied.error?.includes('confirm="yes"'), 'the tool layer refuses admin without confirm="yes"')
256
+ const unknown = registered.find((t) => t.name === 'neuralos_ask')
257
+ const answered = (await unknown.handler({ instance: 'does-not-exist', question: 'x' })) as { error?: string }
258
+ ok(answered.error?.includes("no instance named 'does-not-exist'"), 'ask answers an unknown instance as data')
259
+ ok(logged.some((l) => l.includes('registered neuralos tools')), 'the plugin announces registration through ctx.log')
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // 10. Digest truncation keeps tool results bounded.
264
+ // ---------------------------------------------------------------------------
265
+ {
266
+ const big = { rows: 'x'.repeat(20_000) }
267
+ const text = truncate(big, 6000)
268
+ ok(text.length < 21_000 && text.includes('truncated at 6000 chars'), 'truncate caps large digests with an honest marker')
269
+ ok(truncate('small') === 'small', 'small digests pass through untouched')
270
+ }
271
+
272
+ // ---------------------------------------------------------------------------
273
+ // 11. The REAL defaultExec forwards opts.env (probe transport) to the child
274
+ // while keeping the parent environment (PATH etc.) intact.
275
+ // ---------------------------------------------------------------------------
276
+ {
277
+ const exec = defaultExec()
278
+ const out = await exec(process.execPath, ['-e', 'console.log(process.env.NEURALOS_SPEC_PROBE ?? "missing")'], { env: { NEURALOS_SPEC_PROBE: 'forwarded' } })
279
+ ok(out.code === 0 && out.stdout.trim() === 'forwarded', 'defaultExec forwards opts.env to the child process')
280
+ const plain = await exec(process.execPath, ['-e', 'console.log(Boolean(process.env.PATH))'])
281
+ ok(plain.code === 0 && plain.stdout.trim() === 'true', 'defaultExec keeps the parent environment for plain calls')
282
+ }
283
+
284
+ console.log(`\n${pass} passed, ${fail} failed`)
285
+ if (fail > 0) {
286
+ console.log(`FAILURES:\n${failures.map((f) => ` - ${f}`).join('\n')}`)
287
+ process.exit(1)
288
+ }
289
+ console.log('neuralos: ALL TESTS PASSED')
@@ -0,0 +1,299 @@
1
+ /**
2
+ * neuralosEngine — the neuralOS runtime for the RTerm neuralos plugin.
3
+ *
4
+ * neuralOS instances are on-device data agents: each instance directory
5
+ * carries a probe menu (needle_menu.json) over a real data source, the
6
+ * 121M engine binary picks the probe that answers a question, and the
7
+ * instance's bridge (Python) executes it against the source. Everything
8
+ * here returns errors as data — a missing engine or instance is an
9
+ * answer, never a thrown exception.
10
+ *
11
+ * Engine resolution order (first existing wins):
12
+ * 1. settings.neuralos.engineBin/engineWeights (or NEURALOS_ENGINE_* env)
13
+ * 2. the desktop bundle: {process.resourcesPath}/neuralos/<per-arch engine>
14
+ * (electron-builder extraResources; present in the packaged app only)
15
+ * 3. the shared cache ~/.cache/neuralos/ (auto-downloaded on first use —
16
+ * this is what makes `npm i -g neuralos` self-provisioning: one fetch,
17
+ * then offline forever)
18
+ * 4. <instancesRoot>/engine/needle (+ needle3.cact) — the fleet convention
19
+ * If nothing exists and autoDownload is enabled, the cache is provisioned
20
+ * from https://huggingface.co/Cactus-Compute/needle3 (public, no auth).
21
+ */
22
+
23
+ import { execFile as cpExecFile, spawn } from 'node:child_process'
24
+ import { promises as fs, constants as fsConstants } from 'node:fs'
25
+ import { homedir } from 'node:os'
26
+ import path from 'node:path'
27
+
28
+ export const HF_BASE = 'https://huggingface.co/Cactus-Compute/needle3/resolve/main'
29
+ export const WEIGHTS_NAME = 'needle3.cact'
30
+ export const MAX_RESULT_CHARS = 6000
31
+
32
+ /** Per-arch bundled engine name, matching the release-workflow download. */
33
+ export function bundledEngineName(platform, arch) {
34
+ const os = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux'
35
+ if (os === 'macos') return arch === 'arm64' ? 'engine-macos-arm64' : null
36
+ if (os === 'linux') return arch === 'arm64' ? 'engine-linux-arm64' : arch === 'x64' ? 'engine-linux-x86_64' : null
37
+ return arch === 'arm64' ? 'engine-windows-arm64.exe' : arch === 'x64' ? 'engine-windows-x86_64.exe' : null
38
+ }
39
+
40
+ /** The HF platform folder for a bundled engine name. */
41
+ export function hfFolderFor(platform, arch) {
42
+ const os = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux'
43
+ if (os === 'macos') return 'macos-arm64'
44
+ if (os === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x86_64'
45
+ return arch === 'arm64' ? 'windows-arm64' : 'windows-x86_64'
46
+ }
47
+
48
+ export function cacheDir(env = process.env) {
49
+ if (env.NEURALOS_CACHE_DIR) return env.NEURALOS_CACHE_DIR
50
+ return path.join(homedir(), '.cache', 'neuralos')
51
+ }
52
+
53
+ /** Settings block -> env -> default. Blank values mean unset. */
54
+ function pick(settingsValue, envValue, fallback) {
55
+ const s = typeof settingsValue === 'string' ? settingsValue.trim() : ''
56
+ if (s) return s
57
+ const e = typeof envValue === 'string' ? envValue.trim() : ''
58
+ if (e) return e
59
+ return fallback
60
+ }
61
+
62
+ export function resolveConfig(ctx = {}, env = process.env) {
63
+ const settings = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
64
+ const block = settings.neuralos || {}
65
+ return {
66
+ instancesDir: pick(block.instancesDir, env.NEURALOS_INSTANCES_DIR, path.join(homedir(), 'neuralos-instances')),
67
+ pythonBin: pick(block.pythonBin, env.NEURALOS_PYTHON, 'python3'),
68
+ engineBin: pick(block.engineBin, env.NEURALOS_ENGINE_BIN) || null,
69
+ engineWeights: pick(block.engineWeights, env.NEURALOS_ENGINE_WEIGHTS) || null,
70
+ autoDownload: block.autoDownload !== false && env.NEURALOS_AUTO_DOWNLOAD !== '0',
71
+ cacheDir: pick(block.cacheDir, env.NEURALOS_CACHE_DIR, cacheDir(env)),
72
+ timeoutMs: Number(block.timeoutMs) > 0 ? Number(block.timeoutMs) : 120000,
73
+ }
74
+ }
75
+
76
+ /** Default execFile promisified; injectable for the spec. */
77
+ export function defaultExec() {
78
+ return (cmd, args, opts = {}) =>
79
+ new Promise((resolve) => {
80
+ const execEnv = opts.env ? { ...process.env, ...opts.env } : process.env
81
+ const child = cpExecFile(cmd, args, { timeout: opts.timeoutMs ?? 120000, maxBuffer: 16 * 1024 * 1024, env: execEnv }, (err, stdout, stderr) => {
82
+ resolve({ ok: !err, code: err ? err.code ?? 1 : 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
83
+ })
84
+ void child
85
+ })
86
+ }
87
+
88
+ async function exists(p) {
89
+ try {
90
+ await fs.access(p, fsConstants.F_OK)
91
+ return true
92
+ } catch {
93
+ return false
94
+ }
95
+ }
96
+
97
+ /** Download one file over HTTPS into dir with its basename (idempotent). */
98
+ async function download(deps, url, dir) {
99
+ await fs.mkdir(dir, { recursive: true })
100
+ const target = path.join(dir, path.basename(url))
101
+ if (await exists(target)) return target
102
+ const tmp = `${target}.part`
103
+ if (!deps.fetchImpl) {
104
+ const res = await fetch(url, { redirect: 'follow' })
105
+ if (!res.ok) throw new Error(`download ${url} -> HTTP ${res.status}`)
106
+ const buf = Buffer.from(await res.arrayBuffer())
107
+ await fs.writeFile(tmp, buf)
108
+ } else {
109
+ await deps.fetchImpl(url, tmp)
110
+ }
111
+ await fs.rename(tmp, target)
112
+ return target
113
+ }
114
+
115
+ /**
116
+ * Provision the shared cache with the weights + this platform's engine.
117
+ * One-time (~36 MB); afterwards everything resolves offline. Injectable
118
+ * fetch/download for the spec; the real path uses global fetch (Node >=18).
119
+ */
120
+ export async function ensureEngine(deps, { force = false } = {}) {
121
+ const engineName = bundledEngineName(deps.platform ?? process.platform, deps.arch ?? process.arch)
122
+ if (!engineName) return { error: `no published neuralOS engine for ${deps.platform ?? process.platform}-${deps.arch ?? process.arch}` }
123
+ const dir = deps.cacheDir ?? cacheDir()
124
+ const weightsPath = path.join(dir, WEIGHTS_NAME)
125
+ const enginePath = path.join(dir, engineName)
126
+ try {
127
+ if (force || !(await exists(weightsPath))) {
128
+ await download(deps, `${HF_BASE}/${WEIGHTS_NAME}`, dir)
129
+ }
130
+ if (force || !(await exists(enginePath))) {
131
+ await download(deps, `${HF_BASE}/${hfFolderFor(deps.platform ?? process.platform, deps.arch ?? process.arch)}/${engineName.startsWith('engine-windows') ? 'needle.exe' : 'needle'}`, dir)
132
+ // HF keeps upstream names; rename to the per-arch bundled name.
133
+ const raw = engineName.startsWith('engine-windows') ? 'needle.exe' : 'needle'
134
+ const rawPath = path.join(dir, raw)
135
+ if (rawPath !== enginePath && (await exists(rawPath))) await fs.rename(rawPath, enginePath)
136
+ }
137
+ await fs.chmod(enginePath, 0o755).catch(() => {})
138
+ return { engineBin: enginePath, engineWeights: weightsPath }
139
+ } catch (e) {
140
+ return { error: `engine auto-download failed: ${String(e?.message ?? e)}` }
141
+ }
142
+ }
143
+
144
+ export async function resolveEngine(cfg, deps = {}) {
145
+ const platform = deps.platform ?? process.platform
146
+ const arch = deps.arch ?? process.arch
147
+ const candidates = []
148
+ if (cfg.engineBin && cfg.engineWeights) candidates.push({ bin: cfg.engineBin, weights: cfg.engineWeights })
149
+ const resources = (deps.resourcesPath ?? process.resourcesPath) ?? undefined
150
+ const bundled = bundledEngineName(platform, arch)
151
+ if (resources && bundled) {
152
+ candidates.push({ bin: path.join(resources, 'neuralos', bundled), weights: path.join(resources, 'neuralos', WEIGHTS_NAME) })
153
+ }
154
+ if (bundled) {
155
+ candidates.push({ bin: path.join(cfg.cacheDir, bundled), weights: path.join(cfg.cacheDir, WEIGHTS_NAME) })
156
+ }
157
+ candidates.push({ bin: path.join(cfg.instancesDir, 'engine', 'needle'), weights: path.join(cfg.instancesDir, 'engine', WEIGHTS_NAME) })
158
+ for (const c of candidates) {
159
+ if ((await exists(c.bin)) && (await exists(c.weights))) return { engineBin: c.bin, engineWeights: c.weights }
160
+ }
161
+ if (cfg.autoDownload) {
162
+ return ensureEngine({ ...deps, cacheDir: cfg.cacheDir, platform, arch })
163
+ }
164
+ return { error: 'neuralOS engine not found — set neuralos.engineBin/engineWeights (settings), install the desktop bundle, or enable autoDownload' }
165
+ }
166
+
167
+ export function parseJsonObject(text) {
168
+ try {
169
+ return JSON.parse(text)
170
+ } catch {
171
+ const first = text.indexOf('{')
172
+ const last = text.lastIndexOf('}')
173
+ if (first === -1 || last <= first) return null
174
+ try {
175
+ return JSON.parse(text.slice(first, last + 1))
176
+ } catch {
177
+ return null
178
+ }
179
+ }
180
+ }
181
+
182
+ export async function listInstances(cfg, deps = {}) {
183
+ const readdir = deps.readdir ?? fs.readdir
184
+ const readFile = deps.readFile ?? fs.readFile
185
+ let entries
186
+ try {
187
+ entries = await readdir(cfg.instancesDir, { withFileTypes: true })
188
+ } catch {
189
+ return { error: `no instances directory at ${cfg.instancesDir} — set neuralos.instancesDir (settings) or NEURALOS_INSTANCES_DIR` }
190
+ }
191
+ const instances = []
192
+ for (const entry of entries) {
193
+ if (!entry.isDirectory()) continue
194
+ const dir = path.join(cfg.instancesDir, entry.name)
195
+ try {
196
+ const raw = await readFile(path.join(dir, 'needle_menu.json'), 'utf-8')
197
+ const menu = JSON.parse(raw)
198
+ if (!Array.isArray(menu)) continue
199
+ instances.push({ name: entry.name, probeCount: menu.length, probes: menu.slice(0, 40).map((t) => t.name) })
200
+ } catch {
201
+ continue
202
+ }
203
+ }
204
+ return instances
205
+ }
206
+
207
+ export async function instanceDirFor(cfg, instance) {
208
+ if (!/^[A-Za-z0-9._-]+$/.test(instance)) return { error: `invalid instance name: ${instance}` }
209
+ const dir = path.join(cfg.instancesDir, instance)
210
+ if (!(await exists(path.join(dir, 'needle_menu.json')))) {
211
+ return { error: `no instance named '${instance}' (see neuralos_list_instances)` }
212
+ }
213
+ return dir
214
+ }
215
+
216
+ export async function engineSelect(exec, engine, menuPath, question, opts = {}) {
217
+ let out
218
+ try {
219
+ out = await exec(engine.engineBin, ['--model', engine.engineWeights, '--tools', menuPath, '--prompt', question], { timeoutMs: opts.timeoutMs })
220
+ } catch (e) {
221
+ return { error: `engine execution failed: ${String(e?.message ?? e)}` }
222
+ }
223
+ if (out.code !== 0 && !out.stdout.trim()) return { error: `engine exited ${out.code}: ${out.stderr.slice(0, 200)}` }
224
+ const parsed = parseJsonObject(out.stdout)
225
+ if (!parsed) return { error: `engine output not JSON: ${out.stdout.slice(0, 120)}` }
226
+ const calls = parsed.function_calls
227
+ if (!Array.isArray(calls) || calls.length === 0 || !calls[0]?.name) {
228
+ return { error: 'engine refused the question (no call selected)' }
229
+ }
230
+ return { pick: calls[0].name, args: calls[0].arguments ?? {}, confidence: typeof parsed.confidence === 'number' ? parsed.confidence : null }
231
+ }
232
+
233
+ const PROBE_RUNNER = [
234
+ 'import json, os, sys',
235
+ 'sys.path.insert(0, os.environ["NEURALOS_INSTANCE_DIR"])',
236
+ 'import bridge',
237
+ 'fn = getattr(bridge, os.environ["NEURALOS_PROBE"], None)',
238
+ 'if fn is None:',
239
+ ' print(json.dumps({"error": "unknown probe", "probe": os.environ["NEURALOS_PROBE"]}))',
240
+ 'else:',
241
+ ' print(json.dumps(fn(**json.loads(os.environ.get("NEURALOS_ARGS", "{}"))), default=str))',
242
+ ].join('\n')
243
+
244
+ export async function executeProbe(exec, cfg, instanceDir, probe, args, opts = {}) {
245
+ const env = {
246
+ NEURALOS_INSTANCE_DIR: instanceDir,
247
+ NEURALOS_PROBE: probe,
248
+ NEURALOS_ARGS: JSON.stringify(args ?? {}),
249
+ }
250
+ let out
251
+ try {
252
+ out = await exec(cfg.pythonBin, ['-c', PROBE_RUNNER], { ...opts, env })
253
+ } catch (e) {
254
+ return { error: `probe execution failed: ${String(e?.message ?? e)}` }
255
+ }
256
+ if (out.code !== 0 && !out.stdout.trim()) return { error: `probe exited ${out.code}: ${out.stderr.slice(0, 200)}` }
257
+ const parsed = parseJsonObject(out.stdout)
258
+ if (!parsed || typeof parsed !== 'object') return { error: `probe output not JSON: ${out.stdout.slice(0, 120)}` }
259
+ return parsed
260
+ }
261
+
262
+ /**
263
+ * Graph probes execute DIRECTLY by name (bypassing engine selection): probe
264
+ * names follow the instance convention <prefix>_graph_<op>, and the 121M
265
+ * model grounds numeric fragments poorly — direct execution is the fix.
266
+ */
267
+ export async function graphProbe(exec, cfg, instanceDir, input) {
268
+ if (!['overview', 'neighbors', 'connect'].includes(input.op)) return { error: `unknown graph op: ${input.op}` }
269
+ const raw = await fs.readFile(path.join(instanceDir, 'needle_menu.json'), 'utf-8')
270
+ const menu = JSON.parse(raw)
271
+ const graphNames = menu.map((t) => t.name).filter((n) => n.includes('_graph_'))
272
+ const probe = graphNames.find((n) => n.endsWith(`_graph_${input.op}`))
273
+ if (!probe) return { error: `this instance has no ${input.op} graph probe`, available_graph_probes: graphNames }
274
+ const args = input.op === 'neighbors' ? { node: input.node ?? '' } : input.op === 'connect' ? { a: input.a ?? '', b: input.b ?? '' } : {}
275
+ const result = await executeProbe(exec, cfg, instanceDir, probe, args)
276
+ return { probe, ...result }
277
+ }
278
+
279
+ /**
280
+ * Admin probes are write-capable bridge functions; the confirm='yes' literal
281
+ * is the operator interlock (enforced at the tool schema) and this guard
282
+ * keeps probe names to plain identifiers.
283
+ */
284
+ export async function adminProbe(exec, cfg, instanceDir, probe, args) {
285
+ if (!/^[A-Za-z0-9_]+$/.test(probe)) return { error: `invalid probe name: ${probe}` }
286
+ const result = await executeProbe(exec, cfg, instanceDir, probe, args ?? {})
287
+ return { probe, admin: true, result }
288
+ }
289
+
290
+ export function truncate(value, max = MAX_RESULT_CHARS) {
291
+ const text = typeof value === 'string' ? value : JSON.stringify(value)
292
+ if (text.length <= max) return text
293
+ return `${text.slice(0, max)}\n… (truncated at ${max} chars)`
294
+ }
295
+
296
+ /** Streamed engine invocation for long-running selections (unused by default). */
297
+ export function engineSelectStreamed(engine, menuPath, question) {
298
+ return spawn(engine.engineBin, ['--model', engine.engineWeights, '--tools', menuPath, '--prompt', question], { stdio: ['ignore', 'pipe', 'pipe'] })
299
+ }
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "neuralos",
3
+ "version": "1.0.0",
4
+ "description": "neuralOS instances as RTerm agent tools — on-device data agents over verified probe menus (chinook, cyberbank, aws, efraud, ...). The 121M neuralOS engine selects the probe for a plain-English question and the instance's Python bridge executes it against the real data source; the engine ships with the desktop bundle and the standalone backend auto-provisions ~/.cache/neuralos on first use. Four tools: neuralos_list_instances, neuralos_ask, neuralos_graph (relationship maps), neuralos_admin (write probes behind a confirm=\"yes\" interlock). Config in Settings (neuralos block: instancesDir, pythonBin, engineBin/engineWeights, autoDownload); instances root defaults to ~/neuralos-instances.",
5
+ "entry": "index.mjs",
6
+ "tools": [
7
+ "neuralos_list_instances",
8
+ "neuralos_ask",
9
+ "neuralos_graph",
10
+ "neuralos_admin"
11
+ ],
12
+ "permissions": [
13
+ "exec"
14
+ ]
15
+ }