neuralos 3.9.4 → 3.9.6

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.
Files changed (68) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +48 -347
  3. package/cli.js +78 -0
  4. package/engine.js +124 -0
  5. package/index.js +160 -0
  6. package/package.json +37 -43
  7. package/test.js +20 -0
  8. package/tools.js +47 -0
  9. package/bin/gybackend.cjs +0 -398530
  10. package/plugins/agentspan-bridge/agentspan-bridge.extreme.spec.ts +0 -328
  11. package/plugins/agentspan-bridge/agentspan-bridge.phase2.extreme.spec.ts +0 -282
  12. package/plugins/agentspan-bridge/conductorClient.mjs +0 -182
  13. package/plugins/agentspan-bridge/index.mjs +0 -389
  14. package/plugins/agentspan-bridge/playbookToWorkflowDef.mjs +0 -209
  15. package/plugins/agentspan-bridge/plugin.json +0 -27
  16. package/plugins/fraudops/index.d.mts +0 -9
  17. package/plugins/fraudops/index.mjs +0 -185
  18. package/plugins/fraudops/plugin.json +0 -10
  19. package/plugins/iam-connector/index.d.mts +0 -10
  20. package/plugins/iam-connector/index.mjs +0 -229
  21. package/plugins/iam-connector/plugin.json +0 -10
  22. package/plugins/mitmproxy-bridge/index.d.mts +0 -20
  23. package/plugins/mitmproxy-bridge/index.mjs +0 -256
  24. package/plugins/mitmproxy-bridge/plugin.json +0 -17
  25. package/plugins/monid-bridge/index.mjs +0 -228
  26. package/plugins/monid-bridge/monid-bridge.extreme.spec.mjs +0 -75
  27. package/plugins/monid-bridge/plugin.json +0 -15
  28. package/plugins/netdata-rterm/index.mjs +0 -229
  29. package/plugins/netdata-rterm/netdata-rterm.extreme.spec.ts +0 -252
  30. package/plugins/netdata-rterm/plugin.json +0 -22
  31. package/plugins/netexec-bridge/index.d.mts +0 -28
  32. package/plugins/netexec-bridge/index.mjs +0 -325
  33. package/plugins/netexec-bridge/plugin.json +0 -20
  34. package/plugins/neuralos/index.mjs +0 -137
  35. package/plugins/neuralos/neuralos.extreme.spec.mts +0 -386
  36. package/plugins/neuralos/neuralosEngine.mjs +0 -369
  37. package/plugins/neuralos/plugin.json +0 -15
  38. package/plugins/numbat-bridge/index.mjs +0 -229
  39. package/plugins/numbat-bridge/numbat-bridge.extreme.spec.mjs +0 -148
  40. package/plugins/numbat-bridge/plugin.json +0 -21
  41. package/plugins/patch-manager/index.d.mts +0 -10
  42. package/plugins/patch-manager/index.mjs +0 -288
  43. package/plugins/patch-manager/plugin.json +0 -10
  44. package/plugins/promptfoo-redteam/index.d.mts +0 -15
  45. package/plugins/promptfoo-redteam/index.mjs +0 -277
  46. package/plugins/promptfoo-redteam/plugin.json +0 -16
  47. package/plugins/reactorpro-bridge/index.mjs +0 -499
  48. package/plugins/reactorpro-bridge/plugin.json +0 -26
  49. package/plugins/reactorpro-bridge/reactorpro-bridge.extreme.spec.mts +0 -409
  50. package/plugins/reactorpro-bridge/reactorproAgent.mjs +0 -399
  51. package/plugins/request-router/index.d.mts +0 -9
  52. package/plugins/request-router/index.mjs +0 -239
  53. package/plugins/request-router/plugin.json +0 -10
  54. package/plugins/sample-k8s-slo/index.ts +0 -58
  55. package/plugins/sample-k8s-slo/plugin.json +0 -11
  56. package/plugins/sop-assistant/index.d.mts +0 -9
  57. package/plugins/sop-assistant/index.mjs +0 -325
  58. package/plugins/sop-assistant/plugin.json +0 -10
  59. package/plugins/synapse-bridge/index.mjs +0 -484
  60. package/plugins/synapse-bridge/plugin.json +0 -29
  61. package/plugins/synapse-bridge/synapse-bridge.extreme.spec.mjs +0 -204
  62. package/plugins/synapse-bridge/synapseAgent.extreme.spec.mjs +0 -236
  63. package/plugins/synapse-bridge/synapseAgent.mjs +0 -254
  64. package/plugins/web-intel/index.mjs +0 -381
  65. package/plugins/web-intel/plugin.json +0 -28
  66. package/plugins/web-intel/sidecar.mjs +0 -134
  67. package/plugins/web-intel/web-intel.extreme.spec.ts +0 -263
  68. package/plugins/web-intel/wigoloClient.mjs +0 -145
@@ -1,254 +0,0 @@
1
- /**
2
- * synapseAgent — full-duplex Synapse agent capabilities for RTerm/neuralOS.
3
- *
4
- * Implements the four pieces that turn RTerm from a Synapse *client* into a full
5
- * Synapse *agent* (EXT-GOVERNANCE + EXT-REPUTATION aware):
6
- *
7
- * 1. RESPONDER — listen on mesh.agent.{id}.inbox and respond() to incoming
8
- * Synapse requests by mapping them to RTerm skills (playbooks/tools).
9
- * 2. EMIT/SUBSCRIBE — formal Synapse `emit` on mesh.event.{type} + wildcard
10
- * subscribe to mesh.event.* / mesh.task.* streams.
11
- * 3. REPUTATION (EXT-REPUTATION) — observe task outcomes and maintain a local
12
- * ReputationRecord per (agent, skill): success_rate, speed_score, freshness,
13
- * composite score with lying-penalty + confidence, per Formula 11.5.
14
- * 4. GOVERNANCE (EXT-GOVERNANCE) — speak mesh.approval.{task_id}.request/.response:
15
- * request approval for gated actions and answer approval requests (approver side).
16
- *
17
- * Transport is the injected NATS connection from the plugin (connectMesh). All pure
18
- * logic (score formula, response building, record updates) is dependency-free + testable.
19
- */
20
-
21
- import { randomUUID } from 'node:crypto'
22
- import { envelope } from './index.mjs'
23
-
24
- const enc = new TextEncoder()
25
- const dec = new TextDecoder()
26
- const j = (v) => enc.encode(JSON.stringify(v))
27
- const uj = (b) => JSON.parse(dec.decode(b))
28
-
29
- // ─── 1. RESPONDER ────────────────────────────────────────────────────────────
30
-
31
- /** Build a Synapse respond envelope (payload has output XOR error). */
32
- export function buildRespond(requestEnv, cfg, { output, error } = {}) {
33
- if (output && error) throw new Error('respond must contain output OR error, not both')
34
- const payload = error ? { error } : { output: output ?? {} }
35
- return envelope('respond', payload, cfg, {
36
- to: requestEnv?.from,
37
- task_id: requestEnv?.task_id,
38
- in_reply_to: requestEnv?.id,
39
- })
40
- }
41
-
42
- /** Map a Synapse skill id to an RTerm handler. The plugin supplies handlers for
43
- * the skills RTerm advertises (playbooks/tools). Returns {output} or {error}. */
44
- export async function executeSkill(skillId, input, ctx) {
45
- const skills = (typeof ctx.getRtermSkills === 'function' ? ctx.getRtermSkills() : ctx.rtermSkills) || {}
46
- const handler = skills[skillId]
47
- if (!handler) {
48
- return { error: { code: 3001, message: `SKILL_NOT_FOUND: ${skillId}`, retryable: false } }
49
- }
50
- try {
51
- const output = await handler(input ?? {}, ctx)
52
- return { output: output ?? {} }
53
- } catch (e) {
54
- return { error: { code: 5000, message: String(e?.message ?? e), retryable: true } }
55
- }
56
- }
57
-
58
- /** Start the responder loop: subscribe to mesh.agent.{id}.inbox, execute each
59
- * request, and respond on the reply inbox. Returns a stop function. */
60
- export async function startResponder(nc, cfg, ctx, log = () => {}) {
61
- const inbox = `${cfg.prefix}.agent.${cfg.agentId}.inbox`
62
- const sub = nc.subscribe(inbox)
63
- let stopped = false
64
- const loop = (async () => {
65
- for await (const msg of sub) {
66
- if (stopped) break
67
- ;(async () => {
68
- try {
69
- const req = uj(msg.data)
70
- const skillId = req?.payload?.skill
71
- const input = req?.payload?.input
72
- log(`[synapse] inbound request from ${req?.from} skill=${skillId} task=${req?.task_id}`)
73
- const result = await executeSkill(skillId, input, ctx)
74
- const respond = buildRespond(req, cfg, result)
75
- msg.respond(j(respond))
76
- } catch (e) {
77
- try { msg.respond(j({ error: { code: 5000, message: String(e?.message ?? e), retryable: true } })) } catch { /* best-effort */ }
78
- }
79
- })()
80
- }
81
- })()
82
- loop.catch(() => {})
83
- return () => { stopped = true; try { sub.unsubscribe() } catch {} }
84
- }
85
-
86
- // ─── 2. EMIT / SUBSCRIBE ─────────────────────────────────────────────────────
87
-
88
- /** Emit a formal Synapse event on mesh.event.{type}. */
89
- export function emitEvent(nc, cfg, eventType, payload, extra = {}) {
90
- nc.publish(`${cfg.prefix}.event.${eventType}`, j(envelope('emit', payload, cfg, extra)))
91
- }
92
-
93
- /** Subscribe to a Synapse event/task subject (supports wildcards). Returns stop fn. */
94
- export async function subscribeEvents(nc, subject, handler) {
95
- const sub = nc.subscribe(subject)
96
- const loop = (async () => {
97
- for await (const msg of sub) {
98
- try { handler(uj(msg.data), msg.subject) } catch { /* ignore malformed */ }
99
- }
100
- })()
101
- loop.catch(() => {})
102
- return () => { try { sub.unsubscribe() } catch {} }
103
- }
104
-
105
- // ─── 3. REPUTATION (EXT-REPUTATION) ──────────────────────────────────────────
106
-
107
- export const REP_WEIGHTS = { success: 0.7, speed: 0.2, freshness: 0.1 }
108
- export const REP_DEFAULTS = { maxLatencyMs: 30000, freshnessHalfLifeHours: 24, minSampleSize: 3 }
109
-
110
- /** A ReputationRecord for one (agent, skill) pair. */
111
- export function newRecord(agentId, skill) {
112
- return {
113
- agent_id: agentId, skill,
114
- total: 0, successes: 0, failures: 0, timeouts: 0,
115
- skill_not_found: 0, overloaded: 0, rate_limited: 0,
116
- latencies_ms: [],
117
- success_rate: 0, speed_score: 0, freshness: 1, score: 0, confidence: 0.5,
118
- flags: { misleading_capabilities: false, consecutive_skill_not_found: 0, last_penalty_at: null, penalty_reason: null },
119
- last_seen: null,
120
- }
121
- }
122
-
123
- /** Compute composite score per Formula 11.5. Pure. */
124
- export function computeScore(rec, weights = REP_WEIGHTS, defaults = REP_DEFAULTS, now = Date.now()) {
125
- const outcomes = rec.successes + rec.failures + rec.timeouts
126
- const success_rate = rec.successes / Math.max(1, outcomes)
127
- const avgLatency = rec.latencies_ms.length ? rec.latencies_ms.reduce((a, b) => a + b, 0) / rec.latencies_ms.length : 0
128
- const speed_score = success_rate > 0 ? 1 - Math.min(1, Math.max(0, avgLatency / defaults.maxLatencyMs)) : 0
129
- const hoursSince = rec.last_seen ? (now - rec.last_seen) / 3600000 : 0
130
- const freshness = Math.exp(-hoursSince / defaults.freshnessHalfLifeHours)
131
- const confidence = outcomes >= defaults.minSampleSize ? 1.0 : 0.5
132
- const lying_penalty = rec.flags.misleading_capabilities ? 0.0 : 1.0
133
- const raw = weights.success * success_rate + weights.speed * speed_score + weights.freshness * freshness
134
- const score = raw * lying_penalty * confidence
135
- return { success_rate, speed_score, freshness, confidence, score }
136
- }
137
-
138
- /** Update a record from an observed task outcome. Pure-ish (returns the mutated record).
139
- * outcome: { status: 'completed'|'failed'|'timeout'|'skill_not_found'|'overloaded'|'rate_limited', latencyMs? } */
140
- export function updateRecord(rec, outcome, defaults = REP_DEFAULTS, now = Date.now()) {
141
- rec.total += 1
142
- rec.last_seen = now
143
- const st = outcome.status
144
- if (st === 'completed') {
145
- rec.successes += 1
146
- rec.flags.consecutive_skill_not_found = 0
147
- if (typeof outcome.latencyMs === 'number') rec.latencies_ms.push(outcome.latencyMs)
148
- } else if (st === 'failed') {
149
- rec.failures += 1
150
- rec.flags.consecutive_skill_not_found = 0
151
- } else if (st === 'timeout') {
152
- rec.timeouts += 1
153
- rec.flags.consecutive_skill_not_found = 0
154
- } else if (st === 'skill_not_found') {
155
- rec.skill_not_found += 1
156
- rec.flags.consecutive_skill_not_found += 1
157
- if (rec.flags.consecutive_skill_not_found >= 3) {
158
- rec.flags.misleading_capabilities = true
159
- rec.flags.penalty_reason = 'repeated SKILL_NOT_FOUND (lying-agent)'
160
- rec.flags.last_penalty_at = new Date(now).toISOString()
161
- }
162
- } else if (st === 'overloaded') {
163
- rec.overloaded += 1 // recorded, not scored
164
- } else if (st === 'rate_limited') {
165
- rec.rate_limited += 1 // recorded, not scored
166
- }
167
- const s = computeScore(rec, REP_WEIGHTS, defaults, now)
168
- rec.success_rate = s.success_rate
169
- rec.speed_score = s.speed_score
170
- rec.freshness = s.freshness
171
- rec.confidence = s.confidence
172
- rec.score = s.score
173
- return rec
174
- }
175
-
176
- /** Local ReputationStore: keyed {agent}::{skill}, fed by task_update events. */
177
- export class ReputationStore {
178
- constructor(defaults = REP_DEFAULTS) {
179
- this.defaults = defaults
180
- this.records = new Map()
181
- }
182
- key(agentId, skill) { return `${agentId}::${skill}` }
183
- get(agentId, skill) { return this.records.get(this.key(agentId, skill)) }
184
- observe(agentId, skill, outcome, now = Date.now()) {
185
- const k = this.key(agentId, skill)
186
- let rec = this.records.get(k)
187
- if (!rec) { rec = newRecord(agentId, skill); this.records.set(k, rec) }
188
- return updateRecord(rec, outcome, this.defaults, now)
189
- }
190
- /** discover-ranked: all records at/above minScore, sorted by score desc. */
191
- ranked(minScore = 0) {
192
- return [...this.records.values()].filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score)
193
- }
194
- /** Feed a mesh.task.{id}.update event (type task_update with payload {agent, skill, status, latencyMs?}). */
195
- handleTaskUpdate(env, now = Date.now()) {
196
- const p = env?.payload ?? {}
197
- if (!p.agent || !p.skill || !p.status) return null
198
- return this.observe(p.agent, p.skill, { status: p.status, latencyMs: p.latencyMs }, now)
199
- }
200
- }
201
-
202
- // ─── 4. GOVERNANCE (EXT-GOVERNANCE) ──────────────────────────────────────────
203
-
204
- /** Request approval for a gated action: publish mesh.approval.{taskId}.request and
205
- * await the response. Returns { approved, approver } or { approved: false, error }. */
206
- export async function requestApproval(nc, cfg, { taskId, originalRequest, policyId, ruleId, reason, timeout = 30000 }) {
207
- const tid = taskId ?? randomUUID()
208
- const env = envelope('approval_request', {
209
- original_request: originalRequest ?? {},
210
- policy_id: policyId,
211
- rule_id: ruleId,
212
- reason,
213
- }, cfg, { task_id: tid })
214
- const msg = await nc.request(`${cfg.prefix}.approval.${tid}.request`, j(env), { timeout })
215
- const reply = uj(msg.data)
216
- const p = reply?.payload ?? reply
217
- return { approved: p?.approved === true, approver: p?.approver, taskId: tid, raw: reply }
218
- }
219
-
220
- /** Answer an approval request (approver side): publish mesh.approval.{taskId}.response. */
221
- export function respondApproval(nc, cfg, requestEnv, { approved, approver }) {
222
- const tid = requestEnv?.task_id
223
- const env = envelope('approval_response', { approved: approved === true, approver }, cfg, {
224
- to: requestEnv?.from,
225
- task_id: tid,
226
- in_reply_to: requestEnv?.id,
227
- })
228
- nc.publish(`${cfg.prefix}.approval.${tid}.response`, j(env))
229
- return { answered: tid, approved: approved === true }
230
- }
231
-
232
- /** Start an approver loop: subscribe to mesh.approval.*.request and answer each per
233
- * the supplied decide() function ({approved, approver} or a policy name). Returns stop fn. */
234
- export async function startApprover(nc, cfg, decide, log = () => {}) {
235
- const sub = nc.subscribe(`${cfg.prefix}.approval.*.request`)
236
- let stopped = false
237
- const loop = (async () => {
238
- for await (const msg of sub) {
239
- if (stopped) break
240
- ;(async () => {
241
- try {
242
- const req = uj(msg.data)
243
- const decision = await decide(req)
244
- respondApproval(nc, cfg, req, decision)
245
- log(`[synapse] approval ${decision.approved ? 'granted' : 'denied'} for task ${req?.task_id}`)
246
- } catch (e) {
247
- log(`[synapse] approval handling error: ${e?.message ?? e}`)
248
- }
249
- })()
250
- }
251
- })()
252
- loop.catch(() => {})
253
- return () => { stopped = true; try { sub.unsubscribe() } catch {} }
254
- }
@@ -1,381 +0,0 @@
1
- /**
2
- * web-intel plugin — local-first web intelligence for RTerm's agent via wigolo.
3
- *
4
- * Gives the agent first-class web tools it doesn't have: multi-engine search,
5
- * clean-page fetch, site crawl, structured extract, similar-pages, cache,
6
- * research, and page-watch → RTerm trigger automation. All local-first, keyless
7
- * (search/fetch/crawl), and $0/query — the daemon runs on the same box.
8
- *
9
- * Synthesis uses RTerm's OWN agent, not a wigolo LLM — so there's NO LLM key to
10
- * manage. `web_research` asks wigolo for the decomposed evidence + citations and
11
- * RTerm's agent writes the cited answer from that brief.
12
- *
13
- * Lean by default: the daemon starts lazily with WIGOLO_NO_WARMUP=1 so the
14
- * ~1.5 GB browser engine + on-device models are NOT downloaded at init (search/
15
- * fetch/crawl work keyless without them). Set `warmupOnInit: true` to pre-fetch
16
- * them in the background.
17
- *
18
- * Config (Settings → webIntel block, resolved from ctx.settings / env):
19
- * enabled — master switch (default true)
20
- * restUrl — wigolo daemon base URL (default http://127.0.0.1:3333)
21
- * token — bearer token (optional; only if the daemon uses WIGOLO_API_TOKEN)
22
- * autoStart — start the daemon on first use (default true)
23
- * warmupOnInit — download the full browser engine + models in the background (default false = lean)
24
- *
25
- * The plugin never crashes RTerm when the daemon is down — every tool returns a
26
- * clear {error, hint} result instead of throwing (the agentspan-bridge pattern).
27
- */
28
-
29
- import { WigoloClient, DEFAULT_BASE_URL } from './wigoloClient.mjs'
30
- import { WigoloSidecar } from './sidecar.mjs'
31
-
32
- // ─── config resolution ──────────────────────────────────────────────────────
33
-
34
- /** Read the webIntel config block from RTerm settings (ctx.settings) or env. */
35
- export function resolveConfig(ctx = {}, env = process.env) {
36
- const s = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
37
- const block = s.webIntel || {}
38
- return {
39
- enabled: block.enabled !== false,
40
- restUrl: String(block.restUrl || env.WIGOLO_REST_URL || DEFAULT_BASE_URL).trim().replace(/\/+$/, ''),
41
- token: block.token || env.WIGOLO_API_TOKEN || undefined,
42
- autoStart: block.autoStart !== false,
43
- warmupOnInit: block.warmupOnInit === true,
44
- }
45
- }
46
-
47
- /** Real fetch adapter (matches wigoloClient's expected {ok,status,text}). */
48
- async function realFetch(url, init) {
49
- const res = await fetch(url, { method: init.method, headers: init.headers, body: init.body })
50
- return { ok: res.ok, status: res.status, text: () => res.text() }
51
- }
52
-
53
- /** Build a configured WigoloClient from ctx (settings + vault). */
54
- export function buildClient(ctx = {}, fetchImpl) {
55
- const cfg = resolveConfig(ctx)
56
- const impl = typeof fetchImpl === 'function' ? fetchImpl : realFetch
57
- return { client: new WigoloClient({ baseUrl: cfg.restUrl, token: cfg.token, fetchImpl: impl }), config: cfg }
58
- }
59
-
60
- // ─── formatting helpers (pure) ─────────────────────────────────────────────
61
-
62
- /** Normalize a search result set into compact rows for the agent + panel. */
63
- export function toResultRows(payload) {
64
- const results = payload?.results ?? (Array.isArray(payload) ? payload : [])
65
- if (!Array.isArray(results)) return []
66
- return results.map((r) => ({
67
- title: r.title ?? r.url,
68
- url: r.url,
69
- excerpt: (r.excerpt ?? r.description ?? '').slice(0, 240),
70
- citation: r.citation_id ?? r.id,
71
- score: r.evidence_score?.final ?? r.score,
72
- freshness: payload?.freshness_signal?.published ?? r.published,
73
- }))
74
- }
75
-
76
- /** Normalize a fetch/crawl result into a compact summary. */
77
- export function toPageSummary(payload) {
78
- if (!payload || typeof payload !== 'object') return {}
79
- const links = Array.isArray(payload.links) ? payload.links : []
80
- return {
81
- url: payload.url ?? payload.final_url,
82
- title: payload.title,
83
- markdown: typeof payload.markdown === 'string' ? payload.markdown.slice(0, 4000) : undefined,
84
- linkCount: links.length,
85
- links: links.slice(0, 20),
86
- blocked: payload.blocked_by_challenge === true || payload.status === 'blocked',
87
- sections: Array.isArray(payload.sections) ? payload.sections.length : undefined,
88
- }
89
- }
90
-
91
- /** Normalize a research result: the evidence + citations RTerm's agent synthesizes from. */
92
- export function toResearchBrief(payload) {
93
- if (!payload || typeof payload !== 'object') return { evidence: [] }
94
- const evidence = payload.evidence ?? payload.results ?? payload.sources ?? []
95
- const citations = payload.citations ?? []
96
- return {
97
- question: payload.question,
98
- brief: payload.brief ?? payload.summary,
99
- evidence: (Array.isArray(evidence) ? evidence : []).map((e) => ({
100
- title: e.title ?? e.url,
101
- url: e.url,
102
- excerpt: (e.excerpt ?? e.snippet ?? '').slice(0, 300),
103
- citation: e.citation_id ?? e.id,
104
- })),
105
- citations: Array.isArray(citations) ? citations : [],
106
- note: 'Synthesis is done by the RTerm agent from this evidence — no LLM key needed.',
107
- }
108
- }
109
-
110
- /** Map a watch list payload into rows. */
111
- export function toWatchRows(payload) {
112
- const items = payload?.watches ?? payload?.items ?? (Array.isArray(payload) ? payload : [])
113
- if (!Array.isArray(items)) return []
114
- return items.map((w) => ({
115
- id: w.id ?? w.watch_id,
116
- url: w.url,
117
- lastChecked: w.last_checked ?? w.checked_at,
118
- changed: w.changed === true,
119
- webhook: w.webhook,
120
- }))
121
- }
122
-
123
- /** Fires when a watch reports a page change. */
124
- export function isPageChangedEvent(event) {
125
- if (event?.source !== 'webintel') return false
126
- return event?.changed === true || event?.kind === 'page_changed'
127
- }
128
-
129
- // ─── unreachable-daemon helper ─────────────────────────────────────────────
130
-
131
- async function guarded(fn, log) {
132
- try {
133
- return await fn()
134
- } catch (e) {
135
- const msg = e?.message ?? String(e)
136
- log?.(`[web-intel] ${msg}`)
137
- return { error: msg, hint: 'Is the wigolo daemon running? The web-intel plugin starts it on first use (npx -y wigolo serve), or run it yourself. Configure the URL in Settings → webIntel.' }
138
- }
139
- }
140
-
141
- // ─── plugin entry ───────────────────────────────────────────────────────────
142
-
143
- export function register(ctx) {
144
- const { registerTool, registerTrigger, registerPanel, log } = ctx
145
- // Allow tests/runtimes to inject a fetch; default to the real one.
146
- const { client, config } = buildClient(ctx, typeof ctx.fetchImpl === 'function' ? ctx.fetchImpl : undefined)
147
-
148
- // Sidecar lifecycle (lazy, lean). Spawn is best-effort — in runtimes where a
149
- // real child_process spawn is available the daemon starts on first use; in
150
- // tests the spawnImpl is injected/mocked.
151
- const sidecar = new WigoloSidecar({
152
- spawnImpl: ctx.spawnProcess,
153
- log,
154
- config: { warmup: config.warmupOnInit, token: config.token },
155
- })
156
-
157
- async function ensureDaemon() {
158
- if (!config.enabled) throw new Error('web-intel is disabled in Settings → webIntel')
159
- // Probe the daemon; start it if it's down and autoStart is on.
160
- const h = await client.health()
161
- if (h.ok) return true
162
- if (!config.autoStart) throw new Error(`wigolo daemon not reachable at ${config.restUrl} and autoStart is off`)
163
- if (typeof ctx.spawnProcess !== 'function') {
164
- throw new Error(`wigolo daemon not reachable at ${config.restUrl}. Start it: npx -y wigolo serve`)
165
- }
166
- await sidecar.start()
167
- // Best-effort: wait briefly for it to come up, but never block hard.
168
- const deadline = Date.now() + 8000
169
- for (;;) {
170
- const probe = await client.health()
171
- if (probe.ok) break
172
- if (Date.now() > deadline) throw new Error(`wigolo daemon did not become ready at ${config.restUrl} in time`)
173
- await new Promise((r) => setTimeout(r, 300))
174
- }
175
- // Lean default: only pre-download the heavy models if the user opted in.
176
- if (config.warmupOnInit) void sidecar.warmupInBackground()
177
- return true
178
- }
179
-
180
- // Tool: webintel_health — is the daemon up + what's its status.
181
- registerTool({
182
- name: 'webintel_health',
183
- description: 'Check the wigolo web-intelligence daemon status: reachable, lean vs full warmup, and whether it auto-started. Use this first if any web_* tool errors.',
184
- params: {},
185
- handler: async () => guarded(async () => {
186
- const h = await client.health()
187
- return {
188
- restUrl: config.restUrl,
189
- enabled: config.enabled,
190
- autoStart: config.autoStart,
191
- daemonUp: h.ok,
192
- sidecar: sidecar.status(),
193
- ...(h.ok ? { daemon: h.status } : { hint: 'Start it: npx -y wigolo serve (the plugin also auto-starts it on first use).' }),
194
- }
195
- }, log),
196
- })
197
-
198
- // Tool: web_search — multi-engine web search with ranked, citation-carrying results.
199
- registerTool({
200
- name: 'web_search',
201
- description: 'Search the web (multi-engine, ranked, citation-carrying) for an ops question. Pass a query string or an array for parallel breadth. Returns ranked results with excerpts + citations the agent can quote. Keyless, $0.',
202
- params: {
203
- query: { type: ['string', 'array'], description: 'Search query (string) or array of queries for parallel breadth' },
204
- timeRange: { type: 'string', description: "Optional time scope e.g. 'day'|'week'|'month'|'year'", optional: true },
205
- domain: { type: 'string', description: 'Optional domain to scope the search to', optional: true },
206
- maxResults: { type: 'number', description: 'Max results (default daemon setting)', optional: true },
207
- },
208
- handler: async (p) => guarded(async () => {
209
- await ensureDaemon()
210
- if (!p?.query) return { error: 'web_search needs a query' }
211
- const r = await client.search(p.query, {
212
- ...(p.timeRange ? { time_range: p.timeRange } : {}),
213
- ...(p.domain ? { domain: p.domain } : {}),
214
- ...(typeof p.maxResults === 'number' ? { max_results: p.maxResults } : {}),
215
- })
216
- return { results: toResultRows(r), freshness: r?.freshness_signal }
217
- }, log),
218
- })
219
-
220
- // Tool: web_fetch — fetch one URL as clean markdown (handles JS/SPA/anti-bot).
221
- registerTool({
222
- name: 'web_fetch',
223
- description: 'Fetch one URL as clean markdown + metadata + links (tiered router escalates to a browser engine for JS/SPA/anti-bot pages). Use for a specific doc/advisory/page the agent needs to read.',
224
- params: {
225
- url: { type: 'string', description: 'The URL to fetch' },
226
- section: { type: 'string', description: 'Optional single heading/section to extract', optional: true },
227
- mode: { type: 'string', description: "Optional 'cache'|'default'|'stealth'", optional: true },
228
- },
229
- handler: async (p) => guarded(async () => {
230
- await ensureDaemon()
231
- if (!p?.url) return { error: 'web_fetch needs a url' }
232
- const r = await client.fetch(p.url, {
233
- ...(p.section ? { section: p.section } : {}),
234
- ...(p.mode ? { mode: p.mode } : {}),
235
- })
236
- return toPageSummary(r)
237
- }, log),
238
- })
239
-
240
- // Tool: web_crawl — multi-page crawl of a site (BFS/DFS/sitemap/map-only).
241
- registerTool({
242
- name: 'web_crawl',
243
- description: 'Crawl a site (BFS/DFS/sitemap/map-only) with rate limits + robots.txt respect. Use to map a docs site or pull many pages. Returns per-page summaries + links.',
244
- params: {
245
- url: { type: 'string', description: 'Start URL' },
246
- strategy: { type: 'string', description: "Optional 'bfs'|'dfs'|'sitemap'|'map'", optional: true },
247
- maxPages: { type: 'number', description: 'Max pages to crawl', optional: true },
248
- },
249
- handler: async (p) => guarded(async () => {
250
- await ensureDaemon()
251
- if (!p?.url) return { error: 'web_crawl needs a url' }
252
- const r = await client.crawl(p.url, {
253
- ...(p.strategy ? { strategy: p.strategy } : {}),
254
- ...(typeof p.maxPages === 'number' ? { max_pages: p.maxPages } : {}),
255
- })
256
- const pages = Array.isArray(r?.pages) ? r.pages.map(toPageSummary) : [toPageSummary(r)]
257
- return { startUrl: p.url, pageCount: pages.length, pages }
258
- }, log),
259
- })
260
-
261
- // Tool: web_research — decompose a question into evidence + citations; RTerm's
262
- // agent synthesizes the cited answer (NO LLM key needed).
263
- registerTool({
264
- name: 'web_research',
265
- description: 'Research a question across the web: wigolo decomposes it, fans out sub-queries, fetches sources, and returns ranked evidence + citations. RTerm\'s agent then writes the cited answer from the brief — no LLM key needed. Use for current-doc-grounded answers (release notes, CVEs, errors, best practices).',
266
- params: {
267
- question: { type: 'string', description: 'The research question' },
268
- maxSources: { type: 'number', description: 'Max sources to gather', optional: true },
269
- },
270
- handler: async (p) => guarded(async () => {
271
- await ensureDaemon()
272
- if (!p?.question) return { error: 'web_research needs a question' }
273
- const r = await client.research(p.question, {
274
- ...(typeof p.maxSources === 'number' ? { max_sources: p.maxSources } : {}),
275
- })
276
- return toResearchBrief(r)
277
- }, log),
278
- })
279
-
280
- // Tool: web_find_similar — pages similar to a URL/concept.
281
- registerTool({
282
- name: 'web_find_similar',
283
- description: 'Find pages similar to a URL or concept (keyword + semantic + live web fusion). Use to find related advisories/docs.',
284
- params: {
285
- url: { type: 'string', description: 'The reference URL (or concept)', optional: true },
286
- concept: { type: 'string', description: 'A concept to find similar pages for', optional: true },
287
- maxResults: { type: 'number', optional: true },
288
- },
289
- handler: async (p) => guarded(async () => {
290
- await ensureDaemon()
291
- const input = p?.url ?? p?.concept
292
- if (!input) return { error: 'web_find_similar needs a url or concept' }
293
- const r = await client.findSimilar(input, {
294
- ...(typeof p.maxResults === 'number' ? { max_results: p.maxResults } : {}),
295
- })
296
- return { results: toResultRows(r) }
297
- }, log),
298
- })
299
-
300
- // Tool: web_watch_add — watch a page for changes; fires webintel_page_changed.
301
- registerTool({
302
- name: 'web_watch_add',
303
- description: 'Watch a page (vendor advisory, CVE, status page, doc) for changes. When it changes, the webintel_page_changed trigger fires so you can run a playbook or propose a change. Deliver to a webhook or poll.',
304
- params: {
305
- url: { type: 'string', description: 'The page URL to watch' },
306
- interval: { type: 'string', description: "Optional check interval e.g. '1h'|'6h'|'1d'", optional: true },
307
- webhook: { type: 'string', description: 'Optional webhook URL to deliver changes to', optional: true },
308
- },
309
- handler: async (p) => guarded(async () => {
310
- await ensureDaemon()
311
- if (!p?.url) return { error: 'web_watch_add needs a url' }
312
- const r = await client.watch('create', {
313
- url: p.url,
314
- ...(p.interval ? { interval: p.interval } : {}),
315
- ...(p.webhook ? { webhook: p.webhook } : {}),
316
- })
317
- return { created: true, watch: r }
318
- }, log),
319
- })
320
-
321
- // Tool: web_watch_list — list active page watches.
322
- registerTool({
323
- name: 'web_watch_list',
324
- description: 'List active page watches (url, last-checked, changed flag).',
325
- params: {},
326
- handler: async () => guarded(async () => {
327
- await ensureDaemon()
328
- const r = await client.watch('list')
329
- return { watches: toWatchRows(r) }
330
- }, log),
331
- })
332
-
333
- // Tool: web_watch_remove — stop watching a page.
334
- registerTool({
335
- name: 'web_watch_remove',
336
- description: 'Stop watching a page by watch id or URL.',
337
- params: {
338
- id: { type: 'string', description: 'The watch id (or URL)', optional: true },
339
- url: { type: 'string', description: 'The watched URL', optional: true },
340
- },
341
- handler: async (p) => guarded(async () => {
342
- await ensureDaemon()
343
- if (!p?.id && !p?.url) return { error: 'web_watch_remove needs an id or url' }
344
- const r = await client.watch('remove', { ...(p.id ? { id: p.id } : {}), ...(p.url ? { url: p.url } : {}) })
345
- return { removed: true, result: r }
346
- }, log),
347
- })
348
-
349
- // Trigger: webintel_page_changed — fires when a watched page changes.
350
- registerTrigger({
351
- name: 'webintel_page_changed',
352
- description: 'Fires when a watched page (web_watch_add) reports a change. Use for auto-remediation, CVE/vendor-advisory response, or doc-change playbooks.',
353
- match: (event) => isPageChangedEvent(event),
354
- action: 'propose-change',
355
- })
356
-
357
- // Panel: web-intel — watched pages + daemon status.
358
- registerPanel({
359
- name: 'web-intel',
360
- title: 'Web Intelligence',
361
- render: (data) => {
362
- const watches = (Array.isArray(data?.watches) ? data.watches : [])
363
- .map((w) => `<tr><td>${w.url ?? ''}</td><td>${w.changed ? 'changed' : '—'}</td><td>${w.lastChecked ?? ''}</td></tr>`)
364
- .join('')
365
- return `<div class="web-intel"><h3>Web Intelligence (wigolo)</h3><p>Daemon: ${config.restUrl} · warmup: ${config.warmupOnInit ? 'full' : 'lean'} · synthesis: RTerm agent (no LLM key)</p><h4>Watched pages</h4><table><thead><tr><th>URL</th><th>Changed</th><th>Last checked</th></tr></thead><tbody>${watches || '<tr><td colspan="3">No watches yet — web_watch_add to monitor a page.</td></tr>'}</tbody></table></div>`
366
- },
367
- })
368
-
369
- log(`[web-intel] web-intel registered: 8 tools, 1 trigger, 1 panel (daemon=${config.restUrl}, warmup=${config.warmupOnInit ? 'full' : 'lean'})`)
370
- }
371
-
372
- export default {
373
- register,
374
- resolveConfig,
375
- buildClient,
376
- toResultRows,
377
- toPageSummary,
378
- toResearchBrief,
379
- toWatchRows,
380
- isPageChangedEvent,
381
- }
@@ -1,28 +0,0 @@
1
- {
2
- "name": "web-intel",
3
- "version": "1.0.0",
4
- "description": "Local-first web intelligence for RTerm's agent via wigolo — multi-engine web search, clean-page fetch, site crawl, structured extract, similar-pages, cache, research, and page-watch → RTerm trigger automation. Keyless search/fetch/crawl, $0/query, local-first. Synthesis uses RTerm's own agent (no LLM key needed). Lean by default: the daemon starts lazily with no browser-engine/model warmup (~1.5 GB stays opt-in).",
5
- "entry": "index.mjs",
6
- "tools": [
7
- "webintel_health",
8
- "web_search",
9
- "web_fetch",
10
- "web_crawl",
11
- "web_research",
12
- "web_find_similar",
13
- "web_watch_add",
14
- "web_watch_list",
15
- "web_watch_remove"
16
- ],
17
- "triggers": [
18
- "webintel_page_changed"
19
- ],
20
- "panels": [
21
- "web-intel"
22
- ],
23
- "permissions": [
24
- "exec",
25
- "spawnProcess",
26
- "readLedger:metrics"
27
- ]
28
- }