neuralos 3.8.9 → 3.9.1

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/package.json CHANGED
@@ -1,7 +1,22 @@
1
1
  {
2
- "name": "neuralos",
3
- "version": "3.8.9",
4
- "description": "Standalone neuralOS backend (rterm-backend): AI-native terminal & agentic-AI operations platform.",
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": "neuralos",
53
+ "version": "3.9.1",
54
+ "description": "Standalone neuralOS backend (rterm-backend): AI-native terminal & agentic-AI operations platform. Ships the neuralOS plugin — on-device data agents (verified probe menus over real data sources, engine + weights auto-provisioned on first use)."
55
55
  }
@@ -0,0 +1,137 @@
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
+ const settings = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
39
+ if (settings.neuralos?.enabled === false) {
40
+ log('disabled in settings — not registering tools')
41
+ return
42
+ }
43
+
44
+ registerList(ctx, cfg, log)
45
+ registerAsk(ctx, cfg, exec, log)
46
+ registerGraph(ctx, cfg, exec, log)
47
+ registerAdmin(ctx, cfg, exec, log)
48
+
49
+ log('registered neuralos tools (list_instances, ask, graph, admin)')
50
+ }
51
+
52
+ function registerList(ctx, cfg, log) {
53
+ ctx.registerTool({
54
+ name: 'neuralos_list_instances',
55
+ description:
56
+ '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.',
57
+ params: {},
58
+ handler: async () => {
59
+ const result = await listInstances(cfg)
60
+ log('list_instances')
61
+ return result
62
+ },
63
+ })
64
+ }
65
+
66
+ function registerAsk(ctx, cfg, exec, log) {
67
+ ctx.registerTool({
68
+ name: 'neuralos_ask',
69
+ description:
70
+ '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.',
71
+ params: {
72
+ instance: { type: 'string', description: 'instance name from neuralos_list_instances' },
73
+ question: { type: 'string', description: 'the question in plain English, verbatim' },
74
+ },
75
+ handler: async (p) => {
76
+ const instance = String(p?.instance ?? '')
77
+ const question = String(p?.question ?? '')
78
+ const dir = await instanceDirFor(cfg, instance)
79
+ if (typeof dir !== 'string') return dir
80
+ const engine = await resolveEngine(cfg)
81
+ if (engine.error) return engine
82
+ const selection = await engineSelect(exec, engine, `${dir}/needle_menu.json`, question, { timeoutMs: cfg.timeoutMs })
83
+ if (selection.error) return { instance, question, ...selection }
84
+ const result = await executeProbe(exec, cfg, dir, selection.pick, selection.args, { timeoutMs: cfg.timeoutMs })
85
+ log(`ask ${instance} -> ${selection.pick} (conf ${selection.confidence})`)
86
+ return truncate({ instance, question, pick: selection.pick, confidence: selection.confidence, result })
87
+ },
88
+ })
89
+ }
90
+
91
+ function registerGraph(ctx, cfg, exec, log) {
92
+ ctx.registerTool({
93
+ name: 'neuralos_graph',
94
+ description:
95
+ '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.',
96
+ params: {
97
+ instance: { type: 'string', description: 'instance name' },
98
+ op: { type: 'string', description: 'overview | neighbors | connect' },
99
+ node: { type: 'string', description: 'node fragment for neighbors', optional: true },
100
+ a: { type: 'string', description: 'first entity for connect', optional: true },
101
+ b: { type: 'string', description: 'second entity for connect', optional: true },
102
+ },
103
+ handler: async (p) => {
104
+ const dir = await instanceDirFor(cfg, String(p?.instance ?? ''))
105
+ if (typeof dir !== 'string') return dir
106
+ const result = await graphProbe(exec, cfg, dir, { op: String(p?.op ?? 'overview'), node: p?.node, a: p?.a, b: p?.b })
107
+ log(`graph ${p?.op}`)
108
+ return truncate(result)
109
+ },
110
+ })
111
+ }
112
+
113
+ function registerAdmin(ctx, cfg, exec, log) {
114
+ ctx.registerTool({
115
+ name: 'neuralos_admin',
116
+ description:
117
+ '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.',
118
+ params: {
119
+ instance: { type: 'string', description: 'instance name' },
120
+ probe: { type: 'string', description: 'admin probe name, e.g. aws_power' },
121
+ args: { type: 'object', description: 'probe arguments as JSON', optional: true },
122
+ confirm: { type: 'string', description: 'must be the literal "yes"' },
123
+ },
124
+ handler: async (p) => {
125
+ if (p?.confirm !== 'yes') return { error: 'admin probes require confirm="yes" — restate what you intend to run and pass confirm="yes"' }
126
+ const dir = await instanceDirFor(cfg, String(p?.instance ?? ''))
127
+ if (typeof dir !== 'string') return dir
128
+ const result = await adminProbe(exec, cfg, dir, String(p?.probe ?? ''), p?.args ?? {})
129
+ log(`admin ${p?.probe} on ${p?.instance}`)
130
+ return truncate(result)
131
+ },
132
+ })
133
+ }
134
+
135
+ export function unregister() {
136
+ /* tools deregister with the plugin record; nothing to stop here */
137
+ }
@@ -0,0 +1,298 @@
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
+ // the enabled=false settings gate: the Settings panel switch is honest
262
+ const off: Array<{ name: string }> = []
263
+ register({
264
+ registerTool: (t) => off.push(t as never),
265
+ log: () => {},
266
+ getSettings: () => ({ neuralos: { enabled: false } }),
267
+ })
268
+ ok(off.length === 0, 'enabled=false registers zero tools')
269
+ }
270
+
271
+ // ---------------------------------------------------------------------------
272
+ // 10. Digest truncation keeps tool results bounded.
273
+ // ---------------------------------------------------------------------------
274
+ {
275
+ const big = { rows: 'x'.repeat(20_000) }
276
+ const text = truncate(big, 6000)
277
+ ok(text.length < 21_000 && text.includes('truncated at 6000 chars'), 'truncate caps large digests with an honest marker')
278
+ ok(truncate('small') === 'small', 'small digests pass through untouched')
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // 11. The REAL defaultExec forwards opts.env (probe transport) to the child
283
+ // while keeping the parent environment (PATH etc.) intact.
284
+ // ---------------------------------------------------------------------------
285
+ {
286
+ const exec = defaultExec()
287
+ const out = await exec(process.execPath, ['-e', 'console.log(process.env.NEURALOS_SPEC_PROBE ?? "missing")'], { env: { NEURALOS_SPEC_PROBE: 'forwarded' } })
288
+ ok(out.code === 0 && out.stdout.trim() === 'forwarded', 'defaultExec forwards opts.env to the child process')
289
+ const plain = await exec(process.execPath, ['-e', 'console.log(Boolean(process.env.PATH))'])
290
+ ok(plain.code === 0 && plain.stdout.trim() === 'true', 'defaultExec keeps the parent environment for plain calls')
291
+ }
292
+
293
+ console.log(`\n${pass} passed, ${fail} failed`)
294
+ if (fail > 0) {
295
+ console.log(`FAILURES:\n${failures.map((f) => ` - ${f}`).join('\n')}`)
296
+ process.exit(1)
297
+ }
298
+ 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
+ }