neuralos 2.9.2 → 2.9.4
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 +228 -9
- package/bin/gybackend.js +228 -9
- package/package.json +2 -2
package/bin/gybackend.cjs
CHANGED
|
@@ -288865,6 +288865,32 @@ var TerminalService = class {
|
|
|
288865
288865
|
setSessionLogger(logger) {
|
|
288866
288866
|
this.sessionLogger = logger;
|
|
288867
288867
|
}
|
|
288868
|
+
/** Wire the asciinema-style session recorder (v2.9.x). Output events feed all active recordings. */
|
|
288869
|
+
setSessionRecorder(recorder) {
|
|
288870
|
+
this.sessionRecorder = recorder;
|
|
288871
|
+
}
|
|
288872
|
+
/** Live-recording state: terminalId -> recordingId. */
|
|
288873
|
+
activeRecordings = /* @__PURE__ */ new Map();
|
|
288874
|
+
sessionRecorder = null;
|
|
288875
|
+
/** Begin recording a terminal's output. Returns the recordingId. */
|
|
288876
|
+
startRecording(terminalId, opts = {}) {
|
|
288877
|
+
if (!this.sessionRecorder) throw new Error("session recorder is not wired");
|
|
288878
|
+
const id = this.sessionRecorder.start(terminalId, opts);
|
|
288879
|
+
this.activeRecordings.set(terminalId, id);
|
|
288880
|
+
return id;
|
|
288881
|
+
}
|
|
288882
|
+
/** Stop the active recording for a terminal (if any). Returns the recordingId or null. */
|
|
288883
|
+
stopRecording(terminalId) {
|
|
288884
|
+
const id = this.activeRecordings.get(terminalId);
|
|
288885
|
+
if (!id) return null;
|
|
288886
|
+
this.activeRecordings.delete(terminalId);
|
|
288887
|
+
this.sessionRecorder?.stop(id);
|
|
288888
|
+
return id;
|
|
288889
|
+
}
|
|
288890
|
+
/** True if a terminal is currently being recorded. */
|
|
288891
|
+
isRecording(terminalId) {
|
|
288892
|
+
return this.activeRecordings.has(terminalId);
|
|
288893
|
+
}
|
|
288868
288894
|
setRawEventPublisher(publisher) {
|
|
288869
288895
|
this.rawEventPublisher = publisher;
|
|
288870
288896
|
}
|
|
@@ -289326,6 +289352,13 @@ var TerminalService = class {
|
|
|
289326
289352
|
handleData(terminalId, data) {
|
|
289327
289353
|
const sanitizedData = stripInternalControlMarkers(data);
|
|
289328
289354
|
const tab = this.terminals.get(terminalId);
|
|
289355
|
+
const recordingId = this.activeRecordings.get(terminalId);
|
|
289356
|
+
if (recordingId && this.sessionRecorder && sanitizedData) {
|
|
289357
|
+
try {
|
|
289358
|
+
this.sessionRecorder.out(recordingId, sanitizedData);
|
|
289359
|
+
} catch {
|
|
289360
|
+
}
|
|
289361
|
+
}
|
|
289329
289362
|
if (this.sessionLogger && tab) {
|
|
289330
289363
|
if (!this.sessionLogStarted.has(terminalId)) {
|
|
289331
289364
|
this.sessionLogStarted.add(terminalId);
|
|
@@ -342195,6 +342228,22 @@ var BUILTIN_TOOL_INFO = [
|
|
|
342195
342228
|
name: "create_or_edit",
|
|
342196
342229
|
description: CREATE_OR_EDIT_TOOL_DESCRIPTION
|
|
342197
342230
|
},
|
|
342231
|
+
{
|
|
342232
|
+
name: "write_file",
|
|
342233
|
+
description: WRITE_FILE_TOOL_DESCRIPTION
|
|
342234
|
+
},
|
|
342235
|
+
{
|
|
342236
|
+
name: "edit_file",
|
|
342237
|
+
description: EDIT_FILE_TOOL_DESCRIPTION
|
|
342238
|
+
},
|
|
342239
|
+
{
|
|
342240
|
+
name: "skill",
|
|
342241
|
+
description: "Load a skill to get detailed instructions for a specific task. Skills provide specialized knowledge, step-by-step guidance, and may include supporting files (scripts, references)."
|
|
342242
|
+
},
|
|
342243
|
+
{
|
|
342244
|
+
name: "create_skill",
|
|
342245
|
+
description: "Create a new skill in GyShell skills. This tool only creates new skills and does not modify or overwrite existing ones. If the skill name already exists, the call must fail and you should choose a different name. If you need to modify an existing skill, use edit_file to edit that skill's md file directly, or write_file only when intentionally replacing the full file."
|
|
342246
|
+
},
|
|
342198
342247
|
{
|
|
342199
342248
|
name: "wait",
|
|
342200
342249
|
description: WAIT_TOOL_DESCRIPTION
|
|
@@ -342294,6 +342343,42 @@ var BUILTIN_TOOL_INFO = [
|
|
|
342294
342343
|
{
|
|
342295
342344
|
name: "manage_trigger",
|
|
342296
342345
|
description: MANAGE_TRIGGER_DESCRIPTION
|
|
342346
|
+
},
|
|
342347
|
+
{
|
|
342348
|
+
name: "get_metrics",
|
|
342349
|
+
description: 'Read host metrics as Prometheus exposition text (for a scraper) or a one-line dashboard summary. Use to answer "how are my hosts doing" or to feed an external Prometheus/OTel collector.'
|
|
342350
|
+
},
|
|
342351
|
+
{
|
|
342352
|
+
name: "manage_secret",
|
|
342353
|
+
description: "Manage the encrypted secrets vault \u2014 list metadata (never values), set, delete, or check a secret. Secrets are materialized only at exec time and never enter the conversation. Vault must be unlocked (RTERM_SECRETS_MASTER_KEY)."
|
|
342354
|
+
},
|
|
342355
|
+
{
|
|
342356
|
+
name: "manage_oncall",
|
|
342357
|
+
description: "Incident on-call paging \u2014 list open pages, raise a page for an incident under an escalation policy, acknowledge or resolve a page, list policies, or advance the escalation clock."
|
|
342358
|
+
},
|
|
342359
|
+
{
|
|
342360
|
+
name: "get_cost",
|
|
342361
|
+
description: "AI cost & budgets \u2014 summarize token spend in dollars (per model/profile, daily/monthly), check an intended run against budgets (ok/warn/throttle/deny), or list budgets."
|
|
342362
|
+
},
|
|
342363
|
+
{
|
|
342364
|
+
name: "manage_recording",
|
|
342365
|
+
description: "asciinema-style session recording \u2014 list, start, stop, replay (scrub), export (.cast), or delete a terminal session recording."
|
|
342366
|
+
},
|
|
342367
|
+
{
|
|
342368
|
+
name: "manage_gitops",
|
|
342369
|
+
description: "GitOps for desired state \u2014 export the live estate (connections/playbooks/triggers/etc.) as a content-hashed manifest, or detect drift / verify a manifest against live."
|
|
342370
|
+
},
|
|
342371
|
+
{
|
|
342372
|
+
name: "manage_playbook_version",
|
|
342373
|
+
description: "Playbook/runbook versioning + lint \u2014 statically lint a playbook definition (undefined params, dependsOn cycles, empty steps, missing rollback), list version history, roll back, or diff two versions."
|
|
342374
|
+
},
|
|
342375
|
+
{
|
|
342376
|
+
name: "get_cloud_inventory",
|
|
342377
|
+
description: "Cloud resource inventory (AWS/GCP/Azure) \u2014 summary counts by provider/state, query instances (filter by provider/state/region), or pull fresh inventory."
|
|
342378
|
+
},
|
|
342379
|
+
{
|
|
342380
|
+
name: "get_live_dashboard",
|
|
342381
|
+
description: "Live multi-client dashboard \u2014 read the current unified dashboard state/summary, or the number of connected dashboard subscribers."
|
|
342297
342382
|
}
|
|
342298
342383
|
];
|
|
342299
342384
|
function buildReadFileDescription(support) {
|
|
@@ -365944,6 +366029,29 @@ var WebSocketGatewayAdapter = class {
|
|
|
365944
366029
|
`${request.method} is not available on this websocket gateway.`
|
|
365945
366030
|
);
|
|
365946
366031
|
}
|
|
366032
|
+
if (request.method === "observability:liveDashboardSubscribe") {
|
|
366033
|
+
const hub = bridge.liveDashboard;
|
|
366034
|
+
const filter = params.filter;
|
|
366035
|
+
const subId = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
366036
|
+
await hub.subscribe({
|
|
366037
|
+
id: subId,
|
|
366038
|
+
filter,
|
|
366039
|
+
send: (payload) => {
|
|
366040
|
+
this.safeSocketSend(socket, {
|
|
366041
|
+
type: "gateway:event",
|
|
366042
|
+
event: "observability:dashboard",
|
|
366043
|
+
data: payload
|
|
366044
|
+
});
|
|
366045
|
+
}
|
|
366046
|
+
});
|
|
366047
|
+
const sock = socket;
|
|
366048
|
+
const onClose = () => {
|
|
366049
|
+
hub.unsubscribe(subId);
|
|
366050
|
+
sock.off?.("close", onClose);
|
|
366051
|
+
};
|
|
366052
|
+
sock.on?.("close", onClose);
|
|
366053
|
+
return { subscribed: true, subscriberId: subId };
|
|
366054
|
+
}
|
|
365947
366055
|
const fnName = request.method.slice("observability:".length);
|
|
365948
366056
|
const fn = bridge[fnName];
|
|
365949
366057
|
if (typeof fn !== "function") {
|
|
@@ -384404,15 +384512,73 @@ function createObservability(deps) {
|
|
|
384404
384512
|
const escalationService = new EscalationService({ channels: deps.pagingChannels ?? [] });
|
|
384405
384513
|
const costBudgetService = new CostBudgetService({ prices: deps.modelPrices });
|
|
384406
384514
|
const sessionRecorder = new SessionRecorder({});
|
|
384515
|
+
const defaultGitopsReadLive = () => {
|
|
384516
|
+
const out = [];
|
|
384517
|
+
try {
|
|
384518
|
+
const settings = deps.settingsService?.getSettings?.();
|
|
384519
|
+
const conns = settings?.connections ?? {};
|
|
384520
|
+
for (const [kind, list] of Object.entries(conns)) {
|
|
384521
|
+
if (!Array.isArray(list)) continue;
|
|
384522
|
+
for (const c of list) {
|
|
384523
|
+
const id = c?.id ?? c?.name;
|
|
384524
|
+
if (id) out.push({ id: `connection:${kind}:${String(id)}`, kind: "connection", spec: c });
|
|
384525
|
+
}
|
|
384526
|
+
}
|
|
384527
|
+
} catch {
|
|
384528
|
+
}
|
|
384529
|
+
try {
|
|
384530
|
+
const am = deps.automationManager;
|
|
384531
|
+
const groups = am?.listGroups?.() ?? [];
|
|
384532
|
+
for (const g of groups) {
|
|
384533
|
+
const id = g?.id ?? g?.name;
|
|
384534
|
+
if (id) out.push({ id: `group:${String(id)}`, kind: "group", spec: g });
|
|
384535
|
+
}
|
|
384536
|
+
const scripts = am?.listScripts?.() ?? [];
|
|
384537
|
+
for (const s of scripts) {
|
|
384538
|
+
const id = s?.id ?? s?.name;
|
|
384539
|
+
if (id) out.push({ id: `script:${String(id)}`, kind: "script", spec: s });
|
|
384540
|
+
}
|
|
384541
|
+
const playbooks = am?.listPlaybooks?.() ?? [];
|
|
384542
|
+
for (const p of playbooks) {
|
|
384543
|
+
const id = p?.id ?? p?.name;
|
|
384544
|
+
if (id) out.push({ id: `playbook:${String(id)}`, kind: "playbook", spec: p });
|
|
384545
|
+
}
|
|
384546
|
+
const templates = am?.listTemplates?.() ?? [];
|
|
384547
|
+
for (const t of templates) {
|
|
384548
|
+
const id = t?.id ?? t?.name;
|
|
384549
|
+
if (id) out.push({ id: `template:${String(id)}`, kind: "template", spec: t });
|
|
384550
|
+
}
|
|
384551
|
+
const tasks = am?.listScheduledTasks?.() ?? [];
|
|
384552
|
+
for (const t of tasks) {
|
|
384553
|
+
const id = t?.id ?? t?.name;
|
|
384554
|
+
if (id) out.push({ id: `scheduledTask:${String(id)}`, kind: "scheduledTask", spec: t });
|
|
384555
|
+
}
|
|
384556
|
+
} catch {
|
|
384557
|
+
}
|
|
384558
|
+
return out;
|
|
384559
|
+
};
|
|
384407
384560
|
const gitopsService = new GitOpsService({
|
|
384408
|
-
readLive: deps.gitopsReadLive ??
|
|
384561
|
+
readLive: deps.gitopsReadLive ?? defaultGitopsReadLive,
|
|
384409
384562
|
applyEntity: deps.gitopsApplyEntity
|
|
384410
384563
|
});
|
|
384411
384564
|
const playbookVersioning = new PlaybookVersioning({});
|
|
384565
|
+
const runCliJson = async (bin, args) => {
|
|
384566
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
384567
|
+
return new Promise((resolve2, reject) => {
|
|
384568
|
+
execFile2(bin, args, { timeout: 6e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
|
|
384569
|
+
if (err) return reject(err);
|
|
384570
|
+
try {
|
|
384571
|
+
resolve2(JSON.parse(stdout));
|
|
384572
|
+
} catch (e) {
|
|
384573
|
+
reject(e instanceof Error ? e : new Error("invalid JSON"));
|
|
384574
|
+
}
|
|
384575
|
+
});
|
|
384576
|
+
});
|
|
384577
|
+
};
|
|
384412
384578
|
const cloudInventory = new CloudInventory({
|
|
384413
|
-
fetchAwsInstances: deps.fetchAwsInstances,
|
|
384414
|
-
fetchGcpInstances: deps.fetchGcpInstances,
|
|
384415
|
-
fetchAzureVms: deps.fetchAzureVms
|
|
384579
|
+
fetchAwsInstances: deps.fetchAwsInstances ?? (async () => runCliJson("aws", ["ec2", "describe-instances", "--output", "json"])),
|
|
384580
|
+
fetchGcpInstances: deps.fetchGcpInstances ?? (async () => runCliJson("gcloud", ["compute", "instances", "list", "--format", "json"])),
|
|
384581
|
+
fetchAzureVms: deps.fetchAzureVms ?? (async () => runCliJson("az", ["vm", "list", "--show-details", "--output", "json"]))
|
|
384416
384582
|
});
|
|
384417
384583
|
const liveDashboardHub = new LiveDashboardHub({
|
|
384418
384584
|
getState: () => dashboard.state()
|
|
@@ -384428,6 +384594,45 @@ function createObservability(deps) {
|
|
|
384428
384594
|
} catch {
|
|
384429
384595
|
}
|
|
384430
384596
|
});
|
|
384597
|
+
let lastCostRunId = null;
|
|
384598
|
+
const feedCostFromRuns = () => {
|
|
384599
|
+
try {
|
|
384600
|
+
const runs = deps.agentRunLedger?.listRuns?.({ limit: 100 }) ?? [];
|
|
384601
|
+
for (const r of runs) {
|
|
384602
|
+
if (r.runId === lastCostRunId) break;
|
|
384603
|
+
if (!r.model || r.promptTokens === 0 && r.completionTokens === 0) continue;
|
|
384604
|
+
costBudgetService.record({ model: r.model, promptTokens: r.promptTokens, completionTokens: r.completionTokens });
|
|
384605
|
+
}
|
|
384606
|
+
const newest = runs[0];
|
|
384607
|
+
if (newest) lastCostRunId = newest.runId;
|
|
384608
|
+
} catch {
|
|
384609
|
+
}
|
|
384610
|
+
};
|
|
384611
|
+
const costFeedTimer = setInterval(feedCostFromRuns, 6e4);
|
|
384612
|
+
if (typeof costFeedTimer.unref === "function") costFeedTimer.unref();
|
|
384613
|
+
feedCostFromRuns();
|
|
384614
|
+
let otelPushTimer;
|
|
384615
|
+
if (otelExporter) {
|
|
384616
|
+
const intervalMs = Number(process.env.OTEL_EXPORTER_OTLP_INTERVAL_MS ?? 3e4) || 3e4;
|
|
384617
|
+
const pushOnce = async () => {
|
|
384618
|
+
try {
|
|
384619
|
+
renderPrometheus();
|
|
384620
|
+
await otelExporter.push(prometheusRegistry);
|
|
384621
|
+
} catch {
|
|
384622
|
+
}
|
|
384623
|
+
};
|
|
384624
|
+
otelPushTimer = setInterval(() => {
|
|
384625
|
+
void pushOnce();
|
|
384626
|
+
}, intervalMs);
|
|
384627
|
+
if (typeof otelPushTimer.unref === "function") otelPushTimer.unref();
|
|
384628
|
+
void pushOnce();
|
|
384629
|
+
}
|
|
384630
|
+
const oncallTickTimer = setInterval(() => {
|
|
384631
|
+
void escalationService.tick().catch(() => {
|
|
384632
|
+
});
|
|
384633
|
+
}, 3e4);
|
|
384634
|
+
if (typeof oncallTickTimer.unref === "function") oncallTickTimer.unref();
|
|
384635
|
+
deps.onBackgroundDrivers?.({ otelPushTimer, oncallTickTimer });
|
|
384431
384636
|
return {
|
|
384432
384637
|
metricsLedger,
|
|
384433
384638
|
goldenSignals,
|
|
@@ -384520,10 +384725,17 @@ function createObservabilityBridge(deps) {
|
|
|
384520
384725
|
costRemoveBudget: async (params) => ({ removed: requireObs(deps).cost.removeBudget(params.id) }),
|
|
384521
384726
|
// ── session recording ───────────────────────────────────────────────────
|
|
384522
384727
|
recordingList: async () => requireObs(deps).recording.list(),
|
|
384523
|
-
recordingStart: async (params) =>
|
|
384524
|
-
|
|
384525
|
-
|
|
384728
|
+
recordingStart: async (params) => {
|
|
384729
|
+
const ts = deps.terminalService?.();
|
|
384730
|
+
if (ts) return { recordingId: ts.startRecording(params.terminalId, params) };
|
|
384731
|
+
return { recordingId: requireObs(deps).recording.start(params.terminalId, params) };
|
|
384732
|
+
},
|
|
384526
384733
|
recordingStop: async (params) => requireObs(deps).recording.stop(params.recordingId),
|
|
384734
|
+
recordingStopTerminal: async (params) => {
|
|
384735
|
+
const ts = deps.terminalService?.();
|
|
384736
|
+
const id = ts?.stopRecording(params.terminalId);
|
|
384737
|
+
return { recordingId: id, stopped: id !== null && id !== void 0 };
|
|
384738
|
+
},
|
|
384527
384739
|
recordingReplay: async (params) => requireObs(deps).recording.replay(params.recordingId, params),
|
|
384528
384740
|
recordingExportCast: async (params) => requireObs(deps).recording.exportCast(params.recordingId),
|
|
384529
384741
|
recordingDelete: async (params) => ({ deleted: requireObs(deps).recording.delete(params.recordingId) }),
|
|
@@ -384550,7 +384762,11 @@ function createObservabilityBridge(deps) {
|
|
|
384550
384762
|
},
|
|
384551
384763
|
// ── live dashboard hub ──────────────────────────────────────────────────
|
|
384552
384764
|
liveDashboardState: async () => requireObs(deps).liveDashboard.lastState() ?? await requireObs(deps).dashboard.state(),
|
|
384553
|
-
liveDashboardSubscriberCount: async () => ({ count: requireObs(deps).liveDashboard.subscriberCount() })
|
|
384765
|
+
liveDashboardSubscriberCount: async () => ({ count: requireObs(deps).liveDashboard.subscriberCount() }),
|
|
384766
|
+
/** the raw hub (used by the gateway adapter for liveDashboardSubscribe). */
|
|
384767
|
+
get liveDashboard() {
|
|
384768
|
+
return requireObs(deps).liveDashboard;
|
|
384769
|
+
}
|
|
384554
384770
|
};
|
|
384555
384771
|
}
|
|
384556
384772
|
|
|
@@ -386556,6 +386772,7 @@ async function startGyBackend() {
|
|
|
386556
386772
|
agentRunLedger,
|
|
386557
386773
|
gatewayService,
|
|
386558
386774
|
resourceMonitorService,
|
|
386775
|
+
settingsService,
|
|
386559
386776
|
setMonitorPublisher: (pub) => resourceMonitorService.setPublisher((channel, data) => {
|
|
386560
386777
|
pub(channel, data);
|
|
386561
386778
|
}),
|
|
@@ -386564,6 +386781,7 @@ async function startGyBackend() {
|
|
|
386564
386781
|
});
|
|
386565
386782
|
console.log(`[gybackend] Observability wired: dashboard state available (hosts=${observability.metricsLedger.hosts().length})`);
|
|
386566
386783
|
agentService.setObservability(observability);
|
|
386784
|
+
terminalService.setSessionRecorder(observability.recording);
|
|
386567
386785
|
if (settingsService.getSettings().sessionLogging?.enabled) {
|
|
386568
386786
|
const logDir = import_node_path28.default.join(
|
|
386569
386787
|
import_node_process2.default.env.GYSHELL_STORE_DIR || "",
|
|
@@ -387008,7 +387226,8 @@ async function startGyBackend() {
|
|
|
387008
387226
|
// (metrics export, secrets, on-call, cost, recording, gitops, playbooks,
|
|
387009
387227
|
// cloud, live dashboard) as observability:* RPC methods.
|
|
387010
387228
|
observabilityBridge: createObservabilityBridge({
|
|
387011
|
-
observability: () => observability
|
|
387229
|
+
observability: () => observability,
|
|
387230
|
+
terminalService: () => terminalService
|
|
387012
387231
|
})
|
|
387013
387232
|
})
|
|
387014
387233
|
});
|
package/bin/gybackend.js
CHANGED
|
@@ -288865,6 +288865,32 @@ var TerminalService = class {
|
|
|
288865
288865
|
setSessionLogger(logger) {
|
|
288866
288866
|
this.sessionLogger = logger;
|
|
288867
288867
|
}
|
|
288868
|
+
/** Wire the asciinema-style session recorder (v2.9.x). Output events feed all active recordings. */
|
|
288869
|
+
setSessionRecorder(recorder) {
|
|
288870
|
+
this.sessionRecorder = recorder;
|
|
288871
|
+
}
|
|
288872
|
+
/** Live-recording state: terminalId -> recordingId. */
|
|
288873
|
+
activeRecordings = /* @__PURE__ */ new Map();
|
|
288874
|
+
sessionRecorder = null;
|
|
288875
|
+
/** Begin recording a terminal's output. Returns the recordingId. */
|
|
288876
|
+
startRecording(terminalId, opts = {}) {
|
|
288877
|
+
if (!this.sessionRecorder) throw new Error("session recorder is not wired");
|
|
288878
|
+
const id = this.sessionRecorder.start(terminalId, opts);
|
|
288879
|
+
this.activeRecordings.set(terminalId, id);
|
|
288880
|
+
return id;
|
|
288881
|
+
}
|
|
288882
|
+
/** Stop the active recording for a terminal (if any). Returns the recordingId or null. */
|
|
288883
|
+
stopRecording(terminalId) {
|
|
288884
|
+
const id = this.activeRecordings.get(terminalId);
|
|
288885
|
+
if (!id) return null;
|
|
288886
|
+
this.activeRecordings.delete(terminalId);
|
|
288887
|
+
this.sessionRecorder?.stop(id);
|
|
288888
|
+
return id;
|
|
288889
|
+
}
|
|
288890
|
+
/** True if a terminal is currently being recorded. */
|
|
288891
|
+
isRecording(terminalId) {
|
|
288892
|
+
return this.activeRecordings.has(terminalId);
|
|
288893
|
+
}
|
|
288868
288894
|
setRawEventPublisher(publisher) {
|
|
288869
288895
|
this.rawEventPublisher = publisher;
|
|
288870
288896
|
}
|
|
@@ -289326,6 +289352,13 @@ var TerminalService = class {
|
|
|
289326
289352
|
handleData(terminalId, data) {
|
|
289327
289353
|
const sanitizedData = stripInternalControlMarkers(data);
|
|
289328
289354
|
const tab = this.terminals.get(terminalId);
|
|
289355
|
+
const recordingId = this.activeRecordings.get(terminalId);
|
|
289356
|
+
if (recordingId && this.sessionRecorder && sanitizedData) {
|
|
289357
|
+
try {
|
|
289358
|
+
this.sessionRecorder.out(recordingId, sanitizedData);
|
|
289359
|
+
} catch {
|
|
289360
|
+
}
|
|
289361
|
+
}
|
|
289329
289362
|
if (this.sessionLogger && tab) {
|
|
289330
289363
|
if (!this.sessionLogStarted.has(terminalId)) {
|
|
289331
289364
|
this.sessionLogStarted.add(terminalId);
|
|
@@ -342195,6 +342228,22 @@ var BUILTIN_TOOL_INFO = [
|
|
|
342195
342228
|
name: "create_or_edit",
|
|
342196
342229
|
description: CREATE_OR_EDIT_TOOL_DESCRIPTION
|
|
342197
342230
|
},
|
|
342231
|
+
{
|
|
342232
|
+
name: "write_file",
|
|
342233
|
+
description: WRITE_FILE_TOOL_DESCRIPTION
|
|
342234
|
+
},
|
|
342235
|
+
{
|
|
342236
|
+
name: "edit_file",
|
|
342237
|
+
description: EDIT_FILE_TOOL_DESCRIPTION
|
|
342238
|
+
},
|
|
342239
|
+
{
|
|
342240
|
+
name: "skill",
|
|
342241
|
+
description: "Load a skill to get detailed instructions for a specific task. Skills provide specialized knowledge, step-by-step guidance, and may include supporting files (scripts, references)."
|
|
342242
|
+
},
|
|
342243
|
+
{
|
|
342244
|
+
name: "create_skill",
|
|
342245
|
+
description: "Create a new skill in GyShell skills. This tool only creates new skills and does not modify or overwrite existing ones. If the skill name already exists, the call must fail and you should choose a different name. If you need to modify an existing skill, use edit_file to edit that skill's md file directly, or write_file only when intentionally replacing the full file."
|
|
342246
|
+
},
|
|
342198
342247
|
{
|
|
342199
342248
|
name: "wait",
|
|
342200
342249
|
description: WAIT_TOOL_DESCRIPTION
|
|
@@ -342294,6 +342343,42 @@ var BUILTIN_TOOL_INFO = [
|
|
|
342294
342343
|
{
|
|
342295
342344
|
name: "manage_trigger",
|
|
342296
342345
|
description: MANAGE_TRIGGER_DESCRIPTION
|
|
342346
|
+
},
|
|
342347
|
+
{
|
|
342348
|
+
name: "get_metrics",
|
|
342349
|
+
description: 'Read host metrics as Prometheus exposition text (for a scraper) or a one-line dashboard summary. Use to answer "how are my hosts doing" or to feed an external Prometheus/OTel collector.'
|
|
342350
|
+
},
|
|
342351
|
+
{
|
|
342352
|
+
name: "manage_secret",
|
|
342353
|
+
description: "Manage the encrypted secrets vault \u2014 list metadata (never values), set, delete, or check a secret. Secrets are materialized only at exec time and never enter the conversation. Vault must be unlocked (RTERM_SECRETS_MASTER_KEY)."
|
|
342354
|
+
},
|
|
342355
|
+
{
|
|
342356
|
+
name: "manage_oncall",
|
|
342357
|
+
description: "Incident on-call paging \u2014 list open pages, raise a page for an incident under an escalation policy, acknowledge or resolve a page, list policies, or advance the escalation clock."
|
|
342358
|
+
},
|
|
342359
|
+
{
|
|
342360
|
+
name: "get_cost",
|
|
342361
|
+
description: "AI cost & budgets \u2014 summarize token spend in dollars (per model/profile, daily/monthly), check an intended run against budgets (ok/warn/throttle/deny), or list budgets."
|
|
342362
|
+
},
|
|
342363
|
+
{
|
|
342364
|
+
name: "manage_recording",
|
|
342365
|
+
description: "asciinema-style session recording \u2014 list, start, stop, replay (scrub), export (.cast), or delete a terminal session recording."
|
|
342366
|
+
},
|
|
342367
|
+
{
|
|
342368
|
+
name: "manage_gitops",
|
|
342369
|
+
description: "GitOps for desired state \u2014 export the live estate (connections/playbooks/triggers/etc.) as a content-hashed manifest, or detect drift / verify a manifest against live."
|
|
342370
|
+
},
|
|
342371
|
+
{
|
|
342372
|
+
name: "manage_playbook_version",
|
|
342373
|
+
description: "Playbook/runbook versioning + lint \u2014 statically lint a playbook definition (undefined params, dependsOn cycles, empty steps, missing rollback), list version history, roll back, or diff two versions."
|
|
342374
|
+
},
|
|
342375
|
+
{
|
|
342376
|
+
name: "get_cloud_inventory",
|
|
342377
|
+
description: "Cloud resource inventory (AWS/GCP/Azure) \u2014 summary counts by provider/state, query instances (filter by provider/state/region), or pull fresh inventory."
|
|
342378
|
+
},
|
|
342379
|
+
{
|
|
342380
|
+
name: "get_live_dashboard",
|
|
342381
|
+
description: "Live multi-client dashboard \u2014 read the current unified dashboard state/summary, or the number of connected dashboard subscribers."
|
|
342297
342382
|
}
|
|
342298
342383
|
];
|
|
342299
342384
|
function buildReadFileDescription(support) {
|
|
@@ -365944,6 +366029,29 @@ var WebSocketGatewayAdapter = class {
|
|
|
365944
366029
|
`${request.method} is not available on this websocket gateway.`
|
|
365945
366030
|
);
|
|
365946
366031
|
}
|
|
366032
|
+
if (request.method === "observability:liveDashboardSubscribe") {
|
|
366033
|
+
const hub = bridge.liveDashboard;
|
|
366034
|
+
const filter = params.filter;
|
|
366035
|
+
const subId = `ws-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
366036
|
+
await hub.subscribe({
|
|
366037
|
+
id: subId,
|
|
366038
|
+
filter,
|
|
366039
|
+
send: (payload) => {
|
|
366040
|
+
this.safeSocketSend(socket, {
|
|
366041
|
+
type: "gateway:event",
|
|
366042
|
+
event: "observability:dashboard",
|
|
366043
|
+
data: payload
|
|
366044
|
+
});
|
|
366045
|
+
}
|
|
366046
|
+
});
|
|
366047
|
+
const sock = socket;
|
|
366048
|
+
const onClose = () => {
|
|
366049
|
+
hub.unsubscribe(subId);
|
|
366050
|
+
sock.off?.("close", onClose);
|
|
366051
|
+
};
|
|
366052
|
+
sock.on?.("close", onClose);
|
|
366053
|
+
return { subscribed: true, subscriberId: subId };
|
|
366054
|
+
}
|
|
365947
366055
|
const fnName = request.method.slice("observability:".length);
|
|
365948
366056
|
const fn = bridge[fnName];
|
|
365949
366057
|
if (typeof fn !== "function") {
|
|
@@ -384404,15 +384512,73 @@ function createObservability(deps) {
|
|
|
384404
384512
|
const escalationService = new EscalationService({ channels: deps.pagingChannels ?? [] });
|
|
384405
384513
|
const costBudgetService = new CostBudgetService({ prices: deps.modelPrices });
|
|
384406
384514
|
const sessionRecorder = new SessionRecorder({});
|
|
384515
|
+
const defaultGitopsReadLive = () => {
|
|
384516
|
+
const out = [];
|
|
384517
|
+
try {
|
|
384518
|
+
const settings = deps.settingsService?.getSettings?.();
|
|
384519
|
+
const conns = settings?.connections ?? {};
|
|
384520
|
+
for (const [kind, list] of Object.entries(conns)) {
|
|
384521
|
+
if (!Array.isArray(list)) continue;
|
|
384522
|
+
for (const c of list) {
|
|
384523
|
+
const id = c?.id ?? c?.name;
|
|
384524
|
+
if (id) out.push({ id: `connection:${kind}:${String(id)}`, kind: "connection", spec: c });
|
|
384525
|
+
}
|
|
384526
|
+
}
|
|
384527
|
+
} catch {
|
|
384528
|
+
}
|
|
384529
|
+
try {
|
|
384530
|
+
const am = deps.automationManager;
|
|
384531
|
+
const groups = am?.listGroups?.() ?? [];
|
|
384532
|
+
for (const g of groups) {
|
|
384533
|
+
const id = g?.id ?? g?.name;
|
|
384534
|
+
if (id) out.push({ id: `group:${String(id)}`, kind: "group", spec: g });
|
|
384535
|
+
}
|
|
384536
|
+
const scripts = am?.listScripts?.() ?? [];
|
|
384537
|
+
for (const s of scripts) {
|
|
384538
|
+
const id = s?.id ?? s?.name;
|
|
384539
|
+
if (id) out.push({ id: `script:${String(id)}`, kind: "script", spec: s });
|
|
384540
|
+
}
|
|
384541
|
+
const playbooks = am?.listPlaybooks?.() ?? [];
|
|
384542
|
+
for (const p of playbooks) {
|
|
384543
|
+
const id = p?.id ?? p?.name;
|
|
384544
|
+
if (id) out.push({ id: `playbook:${String(id)}`, kind: "playbook", spec: p });
|
|
384545
|
+
}
|
|
384546
|
+
const templates = am?.listTemplates?.() ?? [];
|
|
384547
|
+
for (const t of templates) {
|
|
384548
|
+
const id = t?.id ?? t?.name;
|
|
384549
|
+
if (id) out.push({ id: `template:${String(id)}`, kind: "template", spec: t });
|
|
384550
|
+
}
|
|
384551
|
+
const tasks = am?.listScheduledTasks?.() ?? [];
|
|
384552
|
+
for (const t of tasks) {
|
|
384553
|
+
const id = t?.id ?? t?.name;
|
|
384554
|
+
if (id) out.push({ id: `scheduledTask:${String(id)}`, kind: "scheduledTask", spec: t });
|
|
384555
|
+
}
|
|
384556
|
+
} catch {
|
|
384557
|
+
}
|
|
384558
|
+
return out;
|
|
384559
|
+
};
|
|
384407
384560
|
const gitopsService = new GitOpsService({
|
|
384408
|
-
readLive: deps.gitopsReadLive ??
|
|
384561
|
+
readLive: deps.gitopsReadLive ?? defaultGitopsReadLive,
|
|
384409
384562
|
applyEntity: deps.gitopsApplyEntity
|
|
384410
384563
|
});
|
|
384411
384564
|
const playbookVersioning = new PlaybookVersioning({});
|
|
384565
|
+
const runCliJson = async (bin, args) => {
|
|
384566
|
+
const { execFile: execFile2 } = await import("node:child_process");
|
|
384567
|
+
return new Promise((resolve2, reject) => {
|
|
384568
|
+
execFile2(bin, args, { timeout: 6e4, maxBuffer: 16 * 1024 * 1024 }, (err, stdout) => {
|
|
384569
|
+
if (err) return reject(err);
|
|
384570
|
+
try {
|
|
384571
|
+
resolve2(JSON.parse(stdout));
|
|
384572
|
+
} catch (e) {
|
|
384573
|
+
reject(e instanceof Error ? e : new Error("invalid JSON"));
|
|
384574
|
+
}
|
|
384575
|
+
});
|
|
384576
|
+
});
|
|
384577
|
+
};
|
|
384412
384578
|
const cloudInventory = new CloudInventory({
|
|
384413
|
-
fetchAwsInstances: deps.fetchAwsInstances,
|
|
384414
|
-
fetchGcpInstances: deps.fetchGcpInstances,
|
|
384415
|
-
fetchAzureVms: deps.fetchAzureVms
|
|
384579
|
+
fetchAwsInstances: deps.fetchAwsInstances ?? (async () => runCliJson("aws", ["ec2", "describe-instances", "--output", "json"])),
|
|
384580
|
+
fetchGcpInstances: deps.fetchGcpInstances ?? (async () => runCliJson("gcloud", ["compute", "instances", "list", "--format", "json"])),
|
|
384581
|
+
fetchAzureVms: deps.fetchAzureVms ?? (async () => runCliJson("az", ["vm", "list", "--show-details", "--output", "json"]))
|
|
384416
384582
|
});
|
|
384417
384583
|
const liveDashboardHub = new LiveDashboardHub({
|
|
384418
384584
|
getState: () => dashboard.state()
|
|
@@ -384428,6 +384594,45 @@ function createObservability(deps) {
|
|
|
384428
384594
|
} catch {
|
|
384429
384595
|
}
|
|
384430
384596
|
});
|
|
384597
|
+
let lastCostRunId = null;
|
|
384598
|
+
const feedCostFromRuns = () => {
|
|
384599
|
+
try {
|
|
384600
|
+
const runs = deps.agentRunLedger?.listRuns?.({ limit: 100 }) ?? [];
|
|
384601
|
+
for (const r of runs) {
|
|
384602
|
+
if (r.runId === lastCostRunId) break;
|
|
384603
|
+
if (!r.model || r.promptTokens === 0 && r.completionTokens === 0) continue;
|
|
384604
|
+
costBudgetService.record({ model: r.model, promptTokens: r.promptTokens, completionTokens: r.completionTokens });
|
|
384605
|
+
}
|
|
384606
|
+
const newest = runs[0];
|
|
384607
|
+
if (newest) lastCostRunId = newest.runId;
|
|
384608
|
+
} catch {
|
|
384609
|
+
}
|
|
384610
|
+
};
|
|
384611
|
+
const costFeedTimer = setInterval(feedCostFromRuns, 6e4);
|
|
384612
|
+
if (typeof costFeedTimer.unref === "function") costFeedTimer.unref();
|
|
384613
|
+
feedCostFromRuns();
|
|
384614
|
+
let otelPushTimer;
|
|
384615
|
+
if (otelExporter) {
|
|
384616
|
+
const intervalMs = Number(process.env.OTEL_EXPORTER_OTLP_INTERVAL_MS ?? 3e4) || 3e4;
|
|
384617
|
+
const pushOnce = async () => {
|
|
384618
|
+
try {
|
|
384619
|
+
renderPrometheus();
|
|
384620
|
+
await otelExporter.push(prometheusRegistry);
|
|
384621
|
+
} catch {
|
|
384622
|
+
}
|
|
384623
|
+
};
|
|
384624
|
+
otelPushTimer = setInterval(() => {
|
|
384625
|
+
void pushOnce();
|
|
384626
|
+
}, intervalMs);
|
|
384627
|
+
if (typeof otelPushTimer.unref === "function") otelPushTimer.unref();
|
|
384628
|
+
void pushOnce();
|
|
384629
|
+
}
|
|
384630
|
+
const oncallTickTimer = setInterval(() => {
|
|
384631
|
+
void escalationService.tick().catch(() => {
|
|
384632
|
+
});
|
|
384633
|
+
}, 3e4);
|
|
384634
|
+
if (typeof oncallTickTimer.unref === "function") oncallTickTimer.unref();
|
|
384635
|
+
deps.onBackgroundDrivers?.({ otelPushTimer, oncallTickTimer });
|
|
384431
384636
|
return {
|
|
384432
384637
|
metricsLedger,
|
|
384433
384638
|
goldenSignals,
|
|
@@ -384520,10 +384725,17 @@ function createObservabilityBridge(deps) {
|
|
|
384520
384725
|
costRemoveBudget: async (params) => ({ removed: requireObs(deps).cost.removeBudget(params.id) }),
|
|
384521
384726
|
// ── session recording ───────────────────────────────────────────────────
|
|
384522
384727
|
recordingList: async () => requireObs(deps).recording.list(),
|
|
384523
|
-
recordingStart: async (params) =>
|
|
384524
|
-
|
|
384525
|
-
|
|
384728
|
+
recordingStart: async (params) => {
|
|
384729
|
+
const ts = deps.terminalService?.();
|
|
384730
|
+
if (ts) return { recordingId: ts.startRecording(params.terminalId, params) };
|
|
384731
|
+
return { recordingId: requireObs(deps).recording.start(params.terminalId, params) };
|
|
384732
|
+
},
|
|
384526
384733
|
recordingStop: async (params) => requireObs(deps).recording.stop(params.recordingId),
|
|
384734
|
+
recordingStopTerminal: async (params) => {
|
|
384735
|
+
const ts = deps.terminalService?.();
|
|
384736
|
+
const id = ts?.stopRecording(params.terminalId);
|
|
384737
|
+
return { recordingId: id, stopped: id !== null && id !== void 0 };
|
|
384738
|
+
},
|
|
384527
384739
|
recordingReplay: async (params) => requireObs(deps).recording.replay(params.recordingId, params),
|
|
384528
384740
|
recordingExportCast: async (params) => requireObs(deps).recording.exportCast(params.recordingId),
|
|
384529
384741
|
recordingDelete: async (params) => ({ deleted: requireObs(deps).recording.delete(params.recordingId) }),
|
|
@@ -384550,7 +384762,11 @@ function createObservabilityBridge(deps) {
|
|
|
384550
384762
|
},
|
|
384551
384763
|
// ── live dashboard hub ──────────────────────────────────────────────────
|
|
384552
384764
|
liveDashboardState: async () => requireObs(deps).liveDashboard.lastState() ?? await requireObs(deps).dashboard.state(),
|
|
384553
|
-
liveDashboardSubscriberCount: async () => ({ count: requireObs(deps).liveDashboard.subscriberCount() })
|
|
384765
|
+
liveDashboardSubscriberCount: async () => ({ count: requireObs(deps).liveDashboard.subscriberCount() }),
|
|
384766
|
+
/** the raw hub (used by the gateway adapter for liveDashboardSubscribe). */
|
|
384767
|
+
get liveDashboard() {
|
|
384768
|
+
return requireObs(deps).liveDashboard;
|
|
384769
|
+
}
|
|
384554
384770
|
};
|
|
384555
384771
|
}
|
|
384556
384772
|
|
|
@@ -386556,6 +386772,7 @@ async function startGyBackend() {
|
|
|
386556
386772
|
agentRunLedger,
|
|
386557
386773
|
gatewayService,
|
|
386558
386774
|
resourceMonitorService,
|
|
386775
|
+
settingsService,
|
|
386559
386776
|
setMonitorPublisher: (pub) => resourceMonitorService.setPublisher((channel, data) => {
|
|
386560
386777
|
pub(channel, data);
|
|
386561
386778
|
}),
|
|
@@ -386564,6 +386781,7 @@ async function startGyBackend() {
|
|
|
386564
386781
|
});
|
|
386565
386782
|
console.log(`[gybackend] Observability wired: dashboard state available (hosts=${observability.metricsLedger.hosts().length})`);
|
|
386566
386783
|
agentService.setObservability(observability);
|
|
386784
|
+
terminalService.setSessionRecorder(observability.recording);
|
|
386567
386785
|
if (settingsService.getSettings().sessionLogging?.enabled) {
|
|
386568
386786
|
const logDir = import_node_path28.default.join(
|
|
386569
386787
|
import_node_process2.default.env.GYSHELL_STORE_DIR || "",
|
|
@@ -387008,7 +387226,8 @@ async function startGyBackend() {
|
|
|
387008
387226
|
// (metrics export, secrets, on-call, cost, recording, gitops, playbooks,
|
|
387009
387227
|
// cloud, live dashboard) as observability:* RPC methods.
|
|
387010
387228
|
observabilityBridge: createObservabilityBridge({
|
|
387011
|
-
observability: () => observability
|
|
387229
|
+
observability: () => observability,
|
|
387230
|
+
terminalService: () => terminalService
|
|
387012
387231
|
})
|
|
387013
387232
|
})
|
|
387014
387233
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "neuralos",
|
|
3
|
-
"version": "2.9.
|
|
4
|
-
"description": "neuralOS
|
|
3
|
+
"version": "2.9.4",
|
|
4
|
+
"description": "neuralOS \u2014 the headless AI-native backend for RTerm. run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation, SRE observability, Netdata, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory). The RTerm desktop app stays RTerm; neuralOS is the standalone backend daemon. Dual-published as rterm-backend. v2.9.4: the 9 capabilities actually work out of the box (no placeholders).",
|
|
5
5
|
"main": "bin/gybackend.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"neuralos": "bin/gybackend.js",
|