@stage-labs/metro 0.1.0-beta.224 → 0.1.0-beta.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stage-labs/metro",
3
- "version": "0.1.0-beta.224",
3
+ "version": "0.1.0-beta.225",
4
4
  "description": "The metro command line: run your agent on this machine, relaying its chat channels and connectors to Claude Code.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metro",
3
- "version": "0.1.0-beta.224",
3
+ "version": "0.1.0-beta.225",
4
4
  "description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
5
5
  "author": {
6
6
  "name": "Bonustrack",
@@ -1,4 +1,5 @@
1
1
  import { readFile, statfs } from 'node:fs/promises';
2
+ import { uptime } from 'node:os';
2
3
  import { errMsg, log } from '@metro-labs/core/log';
3
4
  import { isRecord } from '@metro-labs/core/is-record';
4
5
  import { signV4 } from '@metro-labs/http/sigv4';
@@ -10,12 +11,19 @@ const IMDS_TIMEOUT_MS = 2_000;
10
11
  const SEND_TIMEOUT_MS = 15_000;
11
12
  const NAMESPACE = 'CWAgent';
12
13
  const CONTENT_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';
14
+ const BOOT_WINDOW_S = 300;
15
+ const SENT = 'sending memory and disk to CloudWatch every minute';
13
16
 
14
17
  export interface Readings {
15
18
  memory: number;
16
19
  disk: number;
17
20
  }
18
21
 
22
+ export interface Started {
23
+ name: 'server_booted' | 'metro_started';
24
+ at: number;
25
+ }
26
+
19
27
  interface RoleKeys {
20
28
  accessKeyId: string;
21
29
  secretAccessKey: string;
@@ -95,10 +103,15 @@ export async function thisInstance(deps: PublisherDeps): Promise<Instance | null
95
103
  return { instanceId, region, keys: keysOf(await imdsGet(deps, token, `meta-data/iam/security-credentials/${role}`)) };
96
104
  }
97
105
 
98
- export function readingParams(instanceId: string, readings: Readings): Record<string, string> {
99
- const datum = (at: number, name: string, value: number, dims: [string, string][]): [string, string][] => [
106
+ export function startedOf(nowMs: number, processSeconds: number, bootSeconds: number): Started {
107
+ const at = (secondsAgo: number): number => Math.round(nowMs / 1000 - secondsAgo);
108
+ return bootSeconds < BOOT_WINDOW_S ? { name: 'server_booted', at: at(bootSeconds) } : { name: 'metro_started', at: at(processSeconds) };
109
+ }
110
+
111
+ export function readingParams(instanceId: string, readings: Readings, started: Started | null = null): Record<string, string> {
112
+ const datum = (at: number, name: string, value: number, dims: [string, string][], unit = 'Percent'): [string, string][] => [
100
113
  [`MetricData.member.${String(at)}.MetricName`, name],
101
- [`MetricData.member.${String(at)}.Unit`, 'Percent'],
114
+ [`MetricData.member.${String(at)}.Unit`, unit],
102
115
  [`MetricData.member.${String(at)}.Value`, value.toFixed(2)],
103
116
  ...dims.flatMap(([key, v], i): [string, string][] => [
104
117
  [`MetricData.member.${String(at)}.Dimensions.member.${String(i + 1)}.Name`, key],
@@ -114,12 +127,13 @@ export function readingParams(instanceId: string, readings: Readings): Record<st
114
127
  ['InstanceId', instanceId],
115
128
  ['path', '/'],
116
129
  ]),
130
+ ...(started === null ? [] : datum(3, started.name, started.at, [['InstanceId', instanceId]], 'Seconds')),
117
131
  ]);
118
132
  }
119
133
 
120
- async function send(deps: PublisherDeps, instance: Instance, keys: RoleKeys, readings: Readings): Promise<void> {
134
+ async function send(deps: PublisherDeps, instance: Instance, keys: RoleKeys, readings: Readings, started: Started | null): Promise<void> {
121
135
  const url = `https://monitoring.${instance.region}.amazonaws.com/`;
122
- const body = new URLSearchParams(readingParams(instance.instanceId, readings)).toString();
136
+ const body = new URLSearchParams(readingParams(instance.instanceId, readings, started)).toString();
123
137
  const signed = await signV4({ method: 'POST', url, headers: { 'content-type': CONTENT_TYPE }, body, region: instance.region, service: 'monitoring', ...keys });
124
138
  const res = await deps.fetch(url, { method: 'POST', headers: signed.headers, body, signal: AbortSignal.timeout(SEND_TIMEOUT_MS) });
125
139
  if (res.ok) return;
@@ -129,18 +143,19 @@ async function send(deps: PublisherDeps, instance: Instance, keys: RoleKeys, rea
129
143
  throw new Error(`CloudWatch refused the readings: ${code}${message === undefined ? '' : ` (${message})`}`);
130
144
  }
131
145
 
132
- export async function publishOnce(deps: PublisherDeps = LIVE): Promise<string> {
146
+ export async function publishOnce(deps: PublisherDeps = LIVE, started: Started | null = null): Promise<string> {
133
147
  const instance = await thisInstance(deps);
134
148
  if (instance === null) return 'off: this server is not an EC2 instance';
135
149
  if (instance.keys === null) return `waiting: instance ${instance.instanceId} has no IAM role, so memory and disk are not sent`;
136
150
  const readings = await deps.readings();
137
151
  if (readings === null) return 'skipped: could not read memory or disk';
138
- await send(deps, instance, instance.keys, readings);
139
- return 'sending memory and disk to CloudWatch every minute';
152
+ await send(deps, instance, instance.keys, readings, started);
153
+ return SENT;
140
154
  }
141
155
 
142
156
  export function startCloudWatchPublisher(): void {
143
157
  if (process.platform !== 'linux') return;
158
+ let started: Started | null = startedOf(Date.now(), process.uptime(), uptime());
144
159
  let last = '';
145
160
  const note = (status: string, warn: boolean): void => {
146
161
  if (status === last) return;
@@ -149,8 +164,9 @@ export function startCloudWatchPublisher(): void {
149
164
  else log.info({ status }, 'cloudwatch');
150
165
  };
151
166
  const run = (): void => {
152
- publishOnce()
167
+ publishOnce(LIVE, started)
153
168
  .then((status) => {
169
+ if (status === SENT) started = null;
154
170
  note(status, false);
155
171
  })
156
172
  .catch((err: unknown) => {
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.1.0-beta.224"
2
+ "version": "0.1.0-beta.225"
3
3
  }