rterm-backend 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 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
- recordingId: requireObs(deps).recording.start(params.terminalId, params)
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
- recordingId: requireObs(deps).recording.start(params.terminalId, params)
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": "rterm-backend",
3
- "version": "2.9.2",
4
- "description": "RTerm headless backend run RTerm-as-a-service (AI agent, SSH/WinRM/Serial/local terminals, fleet orchestration, advanced automation with event-driven triggers + NATS event mesh, scheduled automation, SRE observability, Netdata integration, AWS APerf, plugin system, governance/audit, Prometheus/OTel metrics export, secrets vault, on-call paging, AI cost budgets, GitOps, cloud inventory). v2.9.2: 9 capabilities now exposed as gateway RPC + agent tools.",
3
+ "version": "2.9.4",
4
+ "description": "RTerm headless backend \u2014 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). Dual-published as neuralos. 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
  "gybackend": "bin/gybackend.cjs"