@agentuity/cli 1.0.41 → 1.0.43

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 (99) hide show
  1. package/dist/cmd/build/ast.d.ts.map +1 -1
  2. package/dist/cmd/build/ast.js +3 -3
  3. package/dist/cmd/build/ast.js.map +1 -1
  4. package/dist/cmd/build/typecheck.d.ts.map +1 -1
  5. package/dist/cmd/build/typecheck.js +52 -1
  6. package/dist/cmd/build/typecheck.js.map +1 -1
  7. package/dist/cmd/build/vite/static-renderer.d.ts.map +1 -1
  8. package/dist/cmd/build/vite/static-renderer.js +22 -8
  9. package/dist/cmd/build/vite/static-renderer.js.map +1 -1
  10. package/dist/cmd/cloud/index.d.ts.map +1 -1
  11. package/dist/cmd/cloud/index.js +4 -0
  12. package/dist/cmd/cloud/index.js.map +1 -1
  13. package/dist/cmd/cloud/monitor.d.ts +3 -0
  14. package/dist/cmd/cloud/monitor.d.ts.map +1 -0
  15. package/dist/cmd/cloud/monitor.js +300 -0
  16. package/dist/cmd/cloud/monitor.js.map +1 -0
  17. package/dist/cmd/cloud/oidc/activity.d.ts +2 -0
  18. package/dist/cmd/cloud/oidc/activity.d.ts.map +1 -0
  19. package/dist/cmd/cloud/oidc/activity.js +57 -0
  20. package/dist/cmd/cloud/oidc/activity.js.map +1 -0
  21. package/dist/cmd/cloud/oidc/create.d.ts +2 -0
  22. package/dist/cmd/cloud/oidc/create.d.ts.map +1 -0
  23. package/dist/cmd/cloud/oidc/create.js +204 -0
  24. package/dist/cmd/cloud/oidc/create.js.map +1 -0
  25. package/dist/cmd/cloud/oidc/delete.d.ts +2 -0
  26. package/dist/cmd/cloud/oidc/delete.d.ts.map +1 -0
  27. package/dist/cmd/cloud/oidc/delete.js +59 -0
  28. package/dist/cmd/cloud/oidc/delete.js.map +1 -0
  29. package/dist/cmd/cloud/oidc/get.d.ts +2 -0
  30. package/dist/cmd/cloud/oidc/get.d.ts.map +1 -0
  31. package/dist/cmd/cloud/oidc/get.js +62 -0
  32. package/dist/cmd/cloud/oidc/get.js.map +1 -0
  33. package/dist/cmd/cloud/oidc/index.d.ts +3 -0
  34. package/dist/cmd/cloud/oidc/index.d.ts.map +1 -0
  35. package/dist/cmd/cloud/oidc/index.js +32 -0
  36. package/dist/cmd/cloud/oidc/index.js.map +1 -0
  37. package/dist/cmd/cloud/oidc/list.d.ts +2 -0
  38. package/dist/cmd/cloud/oidc/list.d.ts.map +1 -0
  39. package/dist/cmd/cloud/oidc/list.js +48 -0
  40. package/dist/cmd/cloud/oidc/list.js.map +1 -0
  41. package/dist/cmd/cloud/oidc/rotate-secret.d.ts +2 -0
  42. package/dist/cmd/cloud/oidc/rotate-secret.d.ts.map +1 -0
  43. package/dist/cmd/cloud/oidc/rotate-secret.js +66 -0
  44. package/dist/cmd/cloud/oidc/rotate-secret.js.map +1 -0
  45. package/dist/cmd/cloud/oidc/users.d.ts +2 -0
  46. package/dist/cmd/cloud/oidc/users.d.ts.map +1 -0
  47. package/dist/cmd/cloud/oidc/users.js +53 -0
  48. package/dist/cmd/cloud/oidc/users.js.map +1 -0
  49. package/dist/cmd/cloud/oidc/util.d.ts +10 -0
  50. package/dist/cmd/cloud/oidc/util.d.ts.map +1 -0
  51. package/dist/cmd/cloud/oidc/util.js +13 -0
  52. package/dist/cmd/cloud/oidc/util.js.map +1 -0
  53. package/dist/cmd/coder/hub-url.d.ts +1 -0
  54. package/dist/cmd/coder/hub-url.d.ts.map +1 -1
  55. package/dist/cmd/coder/hub-url.js +4 -1
  56. package/dist/cmd/coder/hub-url.js.map +1 -1
  57. package/dist/cmd/coder/start.d.ts.map +1 -1
  58. package/dist/cmd/coder/start.js +14 -8
  59. package/dist/cmd/coder/start.js.map +1 -1
  60. package/dist/cmd/coder/tui-init.d.ts +9 -0
  61. package/dist/cmd/coder/tui-init.d.ts.map +1 -0
  62. package/dist/cmd/coder/tui-init.js +56 -0
  63. package/dist/cmd/coder/tui-init.js.map +1 -0
  64. package/dist/config.d.ts.map +1 -1
  65. package/dist/config.js +14 -5
  66. package/dist/config.js.map +1 -1
  67. package/dist/utils/jsonc.d.ts +13 -0
  68. package/dist/utils/jsonc.d.ts.map +1 -0
  69. package/dist/utils/jsonc.js +63 -0
  70. package/dist/utils/jsonc.js.map +1 -0
  71. package/dist/utils/route-migration.d.ts +2 -1
  72. package/dist/utils/route-migration.d.ts.map +1 -1
  73. package/dist/utils/route-migration.js +23 -32
  74. package/dist/utils/route-migration.js.map +1 -1
  75. package/dist/utils/zip.d.ts.map +1 -1
  76. package/dist/utils/zip.js +18 -2
  77. package/dist/utils/zip.js.map +1 -1
  78. package/package.json +6 -7
  79. package/src/cmd/build/ast.ts +6 -3
  80. package/src/cmd/build/typecheck.ts +60 -1
  81. package/src/cmd/build/vite/static-renderer.ts +24 -8
  82. package/src/cmd/cloud/index.ts +4 -0
  83. package/src/cmd/cloud/monitor.ts +375 -0
  84. package/src/cmd/cloud/oidc/activity.ts +64 -0
  85. package/src/cmd/cloud/oidc/create.ts +230 -0
  86. package/src/cmd/cloud/oidc/delete.ts +66 -0
  87. package/src/cmd/cloud/oidc/get.ts +68 -0
  88. package/src/cmd/cloud/oidc/index.ts +35 -0
  89. package/src/cmd/cloud/oidc/list.ts +53 -0
  90. package/src/cmd/cloud/oidc/rotate-secret.ts +80 -0
  91. package/src/cmd/cloud/oidc/users.ts +60 -0
  92. package/src/cmd/cloud/oidc/util.ts +28 -0
  93. package/src/cmd/coder/hub-url.ts +5 -1
  94. package/src/cmd/coder/start.ts +22 -8
  95. package/src/cmd/coder/tui-init.ts +75 -0
  96. package/src/config.ts +16 -5
  97. package/src/utils/jsonc.ts +67 -0
  98. package/src/utils/route-migration.ts +29 -40
  99. package/src/utils/zip.ts +17 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentuity/cli",
3
- "version": "1.0.41",
3
+ "version": "1.0.43",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Agentuity employees and contributors",
6
6
  "type": "module",
@@ -41,9 +41,9 @@
41
41
  "prepublishOnly": "bun run clean && bun run build"
42
42
  },
43
43
  "dependencies": {
44
- "@agentuity/auth": "1.0.41",
45
- "@agentuity/core": "1.0.41",
46
- "@agentuity/server": "1.0.41",
44
+ "@agentuity/auth": "1.0.43",
45
+ "@agentuity/core": "1.0.43",
46
+ "@agentuity/server": "1.0.43",
47
47
  "@datasert/cronjs-parser": "^1.4.0",
48
48
  "@vitejs/plugin-react": "^5.1.2",
49
49
  "acorn-loose": "^8.5.2",
@@ -54,16 +54,15 @@
54
54
  "enquirer": "^2.4.1",
55
55
  "git-url-parse": "^16.1.0",
56
56
  "json-colorizer": "^3.0.1",
57
- "json5": "^2.2.3",
58
57
  "tar": "^7.5.2",
59
58
  "tar-fs": "^3.1.1",
60
59
  "typescript": "^5.9.0",
61
60
  "vite": "^7.2.7",
62
61
  "zod": "^4.3.5",
63
- "@agentuity/frontend": "1.0.41"
62
+ "@agentuity/frontend": "1.0.43"
64
63
  },
65
64
  "devDependencies": {
66
- "@agentuity/test-utils": "1.0.41",
65
+ "@agentuity/test-utils": "1.0.43",
67
66
  "@types/adm-zip": "^0.5.7",
68
67
  "@types/bun": "latest",
69
68
  "@types/tar-fs": "^2.0.4",
@@ -9,7 +9,7 @@ import { StructuredError, type WorkbenchConfig } from '@agentuity/core';
9
9
  import type { LogLevel } from '../../types';
10
10
 
11
11
  import { existsSync, mkdirSync, statSync } from 'node:fs';
12
- import JSON5 from 'json5';
12
+ import { parseJSONC } from '../../utils/jsonc';
13
13
  import { formatSchemaCode } from './format-schema';
14
14
  import { toForwardSlash } from '../../utils/normalize-path';
15
15
  import {
@@ -2963,8 +2963,11 @@ async function updateTsconfigPathMapping(rootDir: string, shouldAdd: boolean): P
2963
2963
  try {
2964
2964
  const tsconfigContent = await Bun.file(tsconfigPath).text();
2965
2965
 
2966
- // Use JSON5 to parse tsconfig.json (handles comments in input)
2967
- const tsconfig = JSON5.parse(tsconfigContent);
2966
+ // Use JSONC parser to handle comments in tsconfig.json
2967
+ const tsconfig = parseJSONC(tsconfigContent) as {
2968
+ compilerOptions?: { paths?: Record<string, string[]> };
2969
+ [key: string]: unknown;
2970
+ };
2968
2971
  const _before = JSON.stringify(tsconfig);
2969
2972
 
2970
2973
  // Initialize compilerOptions and paths if they don't exist
@@ -24,6 +24,47 @@ export interface TypecheckOptions {
24
24
  collector?: BuildReportCollector;
25
25
  }
26
26
 
27
+ /**
28
+ * Filter tsc output to remove lines referencing node_modules paths.
29
+ *
30
+ * Some packages (e.g. @agentuity/runtime) ship .ts source files, which
31
+ * --skipLibCheck does not skip. Errors from those paths can crash the
32
+ * PEG-based tsc-output-parser because the parser expects every non-blank
33
+ * line to be a valid tsc error item. Stripping these lines before parsing
34
+ * prevents the crash and avoids surfacing errors the user cannot fix.
35
+ *
36
+ * We also strip continuation lines (indented with 2+ leading spaces) that
37
+ * follow a node_modules error line, as they are part of the same diagnostic.
38
+ */
39
+ function filterNodeModulesErrors(output: string): string {
40
+ const lines = output.split('\n');
41
+ const filtered: string[] = [];
42
+ let skipping = false;
43
+
44
+ for (const line of lines) {
45
+ // A tsc error line starts with a path, e.g. "node_modules/..." or "../node_modules/..."
46
+ // Also handle Windows-style paths with backslashes
47
+ const isNodeModulesError =
48
+ /^\.{0,2}[/\\]?node_modules[/\\]/.test(line) ||
49
+ /^[A-Za-z]:[/\\].*node_modules[/\\]/.test(line);
50
+
51
+ if (isNodeModulesError) {
52
+ skipping = true;
53
+ continue;
54
+ }
55
+
56
+ // Continuation lines of a multi-line tsc diagnostic start with whitespace
57
+ if (skipping && /^\s{2,}/.test(line)) {
58
+ continue;
59
+ }
60
+
61
+ skipping = false;
62
+ filtered.push(line);
63
+ }
64
+
65
+ return filtered.join('\n');
66
+ }
67
+
27
68
  /**
28
69
  * run the typescript compiler and result formatted results
29
70
  *
@@ -39,7 +80,25 @@ export async function typecheck(dir: string, options?: TypecheckOptions): Promis
39
80
  .nothrow();
40
81
 
41
82
  const output = await result.text();
42
- const errors = parse(output) as GrammarItem[];
83
+
84
+ // Filter out node_modules errors before parsing to prevent parser crashes.
85
+ // The PEG parser is strict and fails on lines it cannot match as tsc error items.
86
+ const filteredOutput = filterNodeModulesErrors(output);
87
+
88
+ let errors: GrammarItem[];
89
+ try {
90
+ errors = parse(filteredOutput) as GrammarItem[];
91
+ } catch {
92
+ // If the parser still fails (e.g. unexpected tsc output format), treat as
93
+ // an unknown error and show the raw output instead of crashing.
94
+ if (collector) {
95
+ collector.addGeneralError('typescript', output || result.stderr.toString());
96
+ }
97
+ return {
98
+ success: false,
99
+ output: output || result.stderr.toString(),
100
+ };
101
+ }
43
102
 
44
103
  if (result.exitCode === 0) {
45
104
  return {
@@ -29,30 +29,46 @@ interface RouteTreeNode {
29
29
  /**
30
30
  * Walks a TanStack Router route tree and extracts all non-parameterized paths.
31
31
  * Skips layout routes (no path) and parameterized routes (containing $).
32
+ *
33
+ * Accumulates the full URL path through the parent chain, since child routes
34
+ * under layout routes have relative paths (e.g., '/key-value' under a
35
+ * '/reference/api' layout should resolve to '/reference/api/key-value').
32
36
  */
33
37
  function extractRoutePaths(node: RouteTreeNode): string[] {
34
38
  const paths = new Set<string>();
35
39
 
36
- function walk(route: RouteTreeNode) {
37
- const path: string | undefined = route.path ?? route.options?.path;
38
- if (path && !path.includes('$')) {
39
- // Normalize: strip trailing slashes, ensure leading slash
40
- const normalized = path === '/' ? '/' : path.replace(/\/+$/, '');
40
+ function walk(route: RouteTreeNode, parentPath: string) {
41
+ const segment: string | undefined = route.path ?? route.options?.path;
42
+
43
+ // Build the full path by accumulating segments from parent routes.
44
+ // - Layout routes have no path (undefined) and don't contribute to the URL.
45
+ // - Index routes have path '/' and resolve to the parent path itself.
46
+ // - Leaf/layout routes have paths like '/reference/api' or '/key-value'.
47
+ let currentPath = parentPath;
48
+ if (segment && segment !== '/') {
49
+ // Non-root segment: append to parent path.
50
+ // Segments always start with '/' (TanStack Router convention).
51
+ currentPath = parentPath === '/' ? segment : parentPath + segment;
52
+ }
53
+
54
+ // Add non-parameterized, non-empty paths
55
+ if (currentPath && !currentPath.includes('$')) {
56
+ const normalized = currentPath === '/' ? '/' : currentPath.replace(/\/+$/, '');
41
57
  if (normalized) {
42
58
  paths.add(normalized);
43
59
  }
44
60
  }
45
61
 
46
- // Recurse into children (TanStack Router stores them as an object)
62
+ // Recurse into children, passing the accumulated path
47
63
  const children = route.children;
48
64
  if (children && typeof children === 'object') {
49
65
  for (const child of Object.values(children)) {
50
- if (child) walk(child);
66
+ if (child) walk(child, currentPath);
51
67
  }
52
68
  }
53
69
  }
54
70
 
55
- walk(node);
71
+ walk(node, '');
56
72
  return [...paths].sort();
57
73
  }
58
74
 
@@ -14,6 +14,7 @@ import webhookCommand from './webhook';
14
14
  import { agentCommand } from './agent';
15
15
  import envCommand from './env';
16
16
  import apikeyCommand from './apikey';
17
+ import oidcCommand from './oidc';
17
18
  import streamCommand from './stream';
18
19
  import vectorCommand from './vector';
19
20
  import { emailCommand } from './email';
@@ -23,6 +24,7 @@ import scheduleCommand from './schedule';
23
24
  import servicesCommand from './services';
24
25
  import { regionSubcommand } from './region';
25
26
  import { machineCommand } from './machine';
27
+ import { monitorSubcommand } from './monitor';
26
28
  import { evalCommand } from './eval';
27
29
  import { evalRunCommand } from './eval-run';
28
30
  import { getCommand } from '../../command-prefix';
@@ -38,6 +40,7 @@ export const command = createCommand({
38
40
  ],
39
41
  subcommands: [
40
42
  apikeyCommand,
43
+ oidcCommand,
41
44
  keyvalueCommand,
42
45
  queueCommand,
43
46
  webhookCommand,
@@ -60,6 +63,7 @@ export const command = createCommand({
60
63
  threadCommand,
61
64
  sshSubcommand,
62
65
  scpSubcommand,
66
+ monitorSubcommand,
63
67
  deploymentCommand,
64
68
  regionSubcommand,
65
69
  machineCommand,
@@ -0,0 +1,375 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ getMonitorNode,
4
+ listDistressedNodes,
5
+ listMonitorNodeContainers,
6
+ listMonitorNodes,
7
+ MonitorWebSocketClient,
8
+ type MachineMonitorState,
9
+ type MonitorMessage,
10
+ type MonitorScope,
11
+ } from '@agentuity/core';
12
+ import { getAPIBaseURL } from '../../api';
13
+ import { getCommand } from '../../command-prefix';
14
+ import { createSubcommand } from '../../types';
15
+ import * as tui from '../../tui';
16
+
17
+ const monitorOptionsSchema = z.object({
18
+ machine: z.string().optional().describe('Monitor a specific machine id'),
19
+ deployment: z.string().optional().describe('Monitor machines for a deployment id'),
20
+ distressed: z.boolean().optional().describe('Only include distressed machines'),
21
+ snapshot: z.boolean().optional().describe('One-shot snapshot (no stream watch)'),
22
+ });
23
+
24
+ export const monitorSubcommand = createSubcommand({
25
+ name: 'monitor',
26
+ description: 'Monitor infrastructure machines in real time',
27
+ tags: ['read-only', 'slow', 'requires-auth'],
28
+ requires: { auth: true, apiClient: true },
29
+ optional: { org: true },
30
+ idempotent: true,
31
+ examples: [
32
+ { command: getCommand('cloud monitor --snapshot'), description: 'Show a monitor snapshot' },
33
+ {
34
+ command: getCommand('cloud monitor --distressed'),
35
+ description: 'Watch distressed machines',
36
+ },
37
+ { command: getCommand('cloud monitor --machine mach_123'), description: 'Watch one machine' },
38
+ ],
39
+ schema: {
40
+ options: monitorOptionsSchema,
41
+ },
42
+ webUrl: '/infrastructure/monitoring',
43
+
44
+ async handler(ctx) {
45
+ const { apiClient, options, opts, auth, config, orgId } = ctx;
46
+
47
+ if (opts.machine && opts.distressed) {
48
+ ctx.logger.fatal('--machine and --distressed are mutually exclusive.');
49
+ }
50
+
51
+ if (opts.deployment && opts.distressed) {
52
+ ctx.logger.fatal('--deployment and --distressed are mutually exclusive.');
53
+ }
54
+
55
+ if (opts.snapshot || options.json) {
56
+ const machines = await getSnapshotMachines({
57
+ apiClient,
58
+ machineId: opts.machine,
59
+ deploymentId: opts.deployment,
60
+ distressed: opts.distressed,
61
+ });
62
+
63
+ if (options.json) {
64
+ console.log(JSON.stringify(machines, null, 2));
65
+ return machines;
66
+ }
67
+
68
+ renderMachineTable(machines);
69
+ return machines;
70
+ }
71
+
72
+ const machineMap = new Map<string, MachineMonitorState>();
73
+
74
+ const initialMachines = await getSnapshotMachines({
75
+ apiClient,
76
+ machineId: opts.machine,
77
+ deploymentId: opts.deployment,
78
+ distressed: opts.distressed,
79
+ });
80
+
81
+ for (const machine of initialMachines) {
82
+ machineMap.set(machine.machineId, machine);
83
+ }
84
+
85
+ renderWatchTable(machineMap, {
86
+ mode: 'snapshot',
87
+ machineId: opts.machine,
88
+ deploymentId: opts.deployment,
89
+ distressed: opts.distressed,
90
+ });
91
+
92
+ tui.info('Connecting to monitoring stream...');
93
+
94
+ let processingChain = Promise.resolve();
95
+ let resolveWait: (() => void) | null = null;
96
+
97
+ const monitorClient = new MonitorWebSocketClient({
98
+ baseUrl: getAPIBaseURL(config),
99
+ token: auth.apiKey,
100
+ orgId,
101
+ scope: toMonitorScope(opts.machine, opts.deployment),
102
+ onOpen: () => {
103
+ tui.success('Connected to monitoring stream');
104
+ },
105
+ onError: (error) => {
106
+ tui.error(`Monitoring stream error: ${error.message}`);
107
+ },
108
+ onClose: () => {
109
+ tui.info('Monitoring stream disconnected');
110
+ resolveWait?.();
111
+ },
112
+ onMessage: (message) => {
113
+ processingChain = processingChain.then(async () => {
114
+ await applyMonitorMessage(machineMap, message, apiClient, opts.deployment);
115
+ renderWatchTable(machineMap, {
116
+ mode: 'watch',
117
+ machineId: opts.machine,
118
+ deploymentId: opts.deployment,
119
+ distressed: opts.distressed,
120
+ lastMessage: message,
121
+ });
122
+ });
123
+ },
124
+ });
125
+
126
+ monitorClient.connect();
127
+
128
+ await new Promise<void>((resolve) => {
129
+ resolveWait = resolve;
130
+ const onSigInt = () => {
131
+ monitorClient.close();
132
+ process.off('SIGINT', onSigInt);
133
+ resolve();
134
+ };
135
+ process.on('SIGINT', onSigInt);
136
+ });
137
+
138
+ return Array.from(machineMap.values());
139
+ },
140
+ });
141
+
142
+ async function getSnapshotMachines(params: {
143
+ apiClient: Parameters<typeof listMonitorNodes>[0];
144
+ machineId?: string;
145
+ deploymentId?: string;
146
+ distressed?: boolean;
147
+ }): Promise<MachineMonitorState[]> {
148
+ const { apiClient, machineId, deploymentId, distressed } = params;
149
+
150
+ let machines: MachineMonitorState[];
151
+ if (distressed) {
152
+ machines = await listDistressedNodes(apiClient);
153
+ } else if (machineId) {
154
+ machines = [await getMonitorNode(apiClient, machineId)];
155
+ } else {
156
+ machines = await listMonitorNodes(apiClient);
157
+ }
158
+
159
+ if (!deploymentId) {
160
+ return machines;
161
+ }
162
+
163
+ const results = await Promise.all(
164
+ machines.map(async (machine) => {
165
+ const containers = await listMonitorNodeContainers(apiClient, machine.machineId);
166
+ const hasDeployment = containers.some(
167
+ (container) => container.deploymentId === deploymentId
168
+ );
169
+ return hasDeployment ? machine : null;
170
+ })
171
+ );
172
+ return results.filter((m): m is MachineMonitorState => m !== null);
173
+ }
174
+
175
+ function toMonitorScope(machineId?: string, deploymentId?: string): MonitorScope {
176
+ if (machineId) {
177
+ return { scope: 'machine', machineId };
178
+ }
179
+ if (deploymentId) {
180
+ return { scope: 'deployment', deploymentId };
181
+ }
182
+ return { scope: 'org' };
183
+ }
184
+
185
+ async function applyMonitorMessage(
186
+ machineMap: Map<string, MachineMonitorState>,
187
+ message: MonitorMessage,
188
+ apiClient: Parameters<typeof listMonitorNodes>[0],
189
+ deploymentId?: string
190
+ ) {
191
+ if (message.type === 'snapshot') {
192
+ machineMap.clear();
193
+ for (const machine of message.machines) {
194
+ machineMap.set(machine.machineId, machine);
195
+ }
196
+ if (deploymentId) {
197
+ await filterMapByDeployment(machineMap, apiClient, deploymentId);
198
+ }
199
+ return;
200
+ }
201
+
202
+ if (message.type === 'update') {
203
+ const existing = machineMap.get(message.machineId);
204
+ const next: MachineMonitorState = {
205
+ machineId: message.machineId,
206
+ orgId: existing?.orgId ?? '',
207
+ report: message.report,
208
+ compositeScore: message.report.capacity?.compositeScore ?? existing?.compositeScore ?? 0,
209
+ health: message.health,
210
+ reportedAt: usecToISO(message.report.reportedAtUs) ?? existing?.reportedAt ?? '',
211
+ updatedAt: new Date().toISOString(),
212
+ gravity: existing?.gravity ?? '',
213
+ };
214
+
215
+ if (deploymentId) {
216
+ const containers = await listMonitorNodeContainers(apiClient, message.machineId);
217
+ const include = containers.some((container) => container.deploymentId === deploymentId);
218
+ if (!include) {
219
+ machineMap.delete(message.machineId);
220
+ return;
221
+ }
222
+ }
223
+
224
+ machineMap.set(message.machineId, next);
225
+ return;
226
+ }
227
+
228
+ const existing = machineMap.get(message.machineId);
229
+ if (existing) {
230
+ existing.health = message.health;
231
+ existing.updatedAt = new Date().toISOString();
232
+ machineMap.set(message.machineId, existing);
233
+ }
234
+ }
235
+
236
+ async function filterMapByDeployment(
237
+ machineMap: Map<string, MachineMonitorState>,
238
+ apiClient: Parameters<typeof listMonitorNodes>[0],
239
+ deploymentId: string
240
+ ) {
241
+ const entries = Array.from(machineMap.keys());
242
+ const results = await Promise.all(
243
+ entries.map(async (machineId) => {
244
+ const containers = await listMonitorNodeContainers(apiClient, machineId);
245
+ const include = containers.some((container) => container.deploymentId === deploymentId);
246
+ return { machineId, include };
247
+ })
248
+ );
249
+ for (const { machineId, include } of results) {
250
+ if (!include) {
251
+ machineMap.delete(machineId);
252
+ }
253
+ }
254
+ }
255
+
256
+ function renderWatchTable(
257
+ machineMap: Map<string, MachineMonitorState>,
258
+ params: {
259
+ mode: 'snapshot' | 'watch';
260
+ machineId?: string;
261
+ deploymentId?: string;
262
+ distressed?: boolean;
263
+ lastMessage?: MonitorMessage;
264
+ }
265
+ ) {
266
+ console.clear();
267
+
268
+ const subtitle = [
269
+ params.mode === 'watch' ? 'Live mode' : 'Snapshot mode',
270
+ params.machineId ? `machine=${params.machineId}` : undefined,
271
+ params.deploymentId ? `deployment=${params.deploymentId}` : undefined,
272
+ params.distressed ? 'distressed=true' : undefined,
273
+ ]
274
+ .filter(Boolean)
275
+ .join(' • ');
276
+
277
+ tui.header('Cloud Monitor');
278
+ if (subtitle) {
279
+ tui.info(subtitle);
280
+ }
281
+ if (params.lastMessage && params.lastMessage.type === 'state_change') {
282
+ tui.warning(
283
+ `${params.lastMessage.machineId}: ${params.lastMessage.previousHealth} -> ${params.lastMessage.health}`
284
+ );
285
+ }
286
+
287
+ renderMachineTable(Array.from(machineMap.values()));
288
+ tui.info('Press Ctrl+C to stop watching.');
289
+ }
290
+
291
+ function renderMachineTable(machines: MachineMonitorState[]) {
292
+ if (machines.length === 0) {
293
+ tui.info('No machines found');
294
+ return;
295
+ }
296
+
297
+ const rows = machines
298
+ .slice()
299
+ .sort((a, b) => a.machineId.localeCompare(b.machineId))
300
+ .map((machine) => ({
301
+ Machine: machine.machineId,
302
+ Health: formatHealth(machine.health),
303
+ CPU: formatPercent(machine.report?.host?.cpu?.usagePercent),
304
+ Memory: formatPercent(machine.report?.host?.memory?.usagePercent),
305
+ Disk: formatPercent(maxDiskUsage(machine.report?.host?.disks)),
306
+ Pressure: formatScore(machine.compositeScore),
307
+ Containers: `${machine.report?.capacity?.runningContainers ?? 0}/${machine.report?.capacity?.totalContainers ?? 0}`,
308
+ 'Last Report': formatAge(machine.reportedAt),
309
+ }));
310
+
311
+ tui.table(rows, [
312
+ { name: 'Machine' },
313
+ { name: 'Health' },
314
+ { name: 'CPU', alignment: 'right' },
315
+ { name: 'Memory', alignment: 'right' },
316
+ { name: 'Disk', alignment: 'right' },
317
+ { name: 'Pressure', alignment: 'right' },
318
+ { name: 'Containers', alignment: 'right' },
319
+ { name: 'Last Report' },
320
+ ]);
321
+ }
322
+
323
+ function formatHealth(health: string): string {
324
+ if (health === 'CONNECTED') return '● CONNECTED';
325
+ if (health === 'STALE') return '◌ STALE';
326
+ if (health === 'DISCONNECTED') return '○ DISCONNECTED';
327
+ return health;
328
+ }
329
+
330
+ function formatPercent(value?: number): string {
331
+ if (value === undefined || value === null || Number.isNaN(value)) {
332
+ return '-';
333
+ }
334
+ return `${value.toFixed(1)}%`;
335
+ }
336
+
337
+ function formatScore(score?: number): string {
338
+ if (score === undefined || score === null || Number.isNaN(score)) {
339
+ return '-';
340
+ }
341
+ if (score >= 0.85) {
342
+ return `${score.toFixed(2)} ⚠`;
343
+ }
344
+ return score.toFixed(2);
345
+ }
346
+
347
+ function maxDiskUsage(disks?: Array<{ usagePercent: number }>): number | undefined {
348
+ if (!disks || disks.length === 0) {
349
+ return undefined;
350
+ }
351
+ return Math.max(...disks.map((d) => d.usagePercent));
352
+ }
353
+
354
+ function formatAge(timestamp: string): string {
355
+ const date = new Date(timestamp);
356
+ const time = date.getTime();
357
+ if (Number.isNaN(time)) {
358
+ return '-';
359
+ }
360
+
361
+ const diff = Date.now() - time;
362
+ if (diff < 60_000) return `${Math.max(0, Math.floor(diff / 1000))}s ago`;
363
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
364
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
365
+ return `${Math.floor(diff / 86_400_000)}d ago`;
366
+ }
367
+
368
+ function usecToISO(us?: number): string | undefined {
369
+ if (us === undefined || us <= 0 || Number.isNaN(us)) {
370
+ return undefined;
371
+ }
372
+ return new Date(Math.floor(us / 1000)).toISOString();
373
+ }
374
+
375
+ export default monitorSubcommand;
@@ -0,0 +1,64 @@
1
+ import { oauthClientActivity } from '@agentuity/core';
2
+ import { z } from 'zod';
3
+ import { getCommand } from '../../../command-prefix';
4
+ import * as tui from '../../../tui';
5
+ import { createSubcommand } from '../../../types';
6
+ import { createOAuthClient } from './util';
7
+
8
+ const OAuthClientActivityResponseSchema = z.array(
9
+ z.object({
10
+ activity_date: z.string(),
11
+ total_access: z.number(),
12
+ unique_users: z.number(),
13
+ })
14
+ );
15
+
16
+ export const activitySubcommand = createSubcommand({
17
+ name: 'activity',
18
+ description: 'Show activity for an OAuth application',
19
+ tags: ['read-only', 'requires-auth'],
20
+ examples: [
21
+ { command: getCommand('cloud oidc activity <id>'), description: 'Show OAuth activity' },
22
+ {
23
+ command: getCommand('cloud oidc activity <id> --days=30'),
24
+ description: 'Show OAuth activity for last 30 days',
25
+ },
26
+ ],
27
+ requires: { auth: true },
28
+ idempotent: true,
29
+ webUrl: (ctx) => `/settings/oauth-apps/${encodeURIComponent(ctx.args.id)}`,
30
+ schema: {
31
+ args: z.object({
32
+ id: z.string().describe('the OAuth client id'),
33
+ }),
34
+ options: z.object({
35
+ days: z.coerce.number().int().min(1).max(365).default(7).describe('Number of days'),
36
+ }),
37
+ response: OAuthClientActivityResponseSchema,
38
+ },
39
+
40
+ async handler(ctx) {
41
+ const { args, opts, options } = ctx;
42
+ const catalystClient = await createOAuthClient(ctx);
43
+
44
+ const activity = await tui.spinner('Fetching OAuth activity', () => {
45
+ return oauthClientActivity(catalystClient, args.id, opts.days);
46
+ });
47
+
48
+ if (!options.json) {
49
+ if (activity.length === 0) {
50
+ tui.info('No OAuth activity found');
51
+ } else {
52
+ const rows = activity.map((item) => ({
53
+ activity_date: item.activity_date,
54
+ total_access: item.total_access,
55
+ unique_users: item.unique_users,
56
+ }));
57
+
58
+ tui.table(rows);
59
+ }
60
+ }
61
+
62
+ return activity;
63
+ },
64
+ });