@agentuity/cli 0.1.36 → 0.1.38

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 (104) hide show
  1. package/bin/cli.ts +20 -0
  2. package/dist/agent-detection.d.ts +71 -0
  3. package/dist/agent-detection.d.ts.map +1 -0
  4. package/dist/agent-detection.js +232 -0
  5. package/dist/agent-detection.js.map +1 -0
  6. package/dist/cache/index.d.ts +1 -0
  7. package/dist/cache/index.d.ts.map +1 -1
  8. package/dist/cache/index.js +1 -0
  9. package/dist/cache/index.js.map +1 -1
  10. package/dist/cache/project-cache.d.ts +16 -0
  11. package/dist/cache/project-cache.d.ts.map +1 -0
  12. package/dist/cache/project-cache.js +36 -0
  13. package/dist/cache/project-cache.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +10 -2
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cmd/auth/index.d.ts.map +1 -1
  18. package/dist/cmd/auth/index.js +0 -2
  19. package/dist/cmd/auth/index.js.map +1 -1
  20. package/dist/cmd/auth/org/enroll.d.ts +2 -0
  21. package/dist/cmd/auth/org/enroll.d.ts.map +1 -0
  22. package/dist/cmd/auth/{machine/setup.js → org/enroll.js} +14 -14
  23. package/dist/cmd/auth/org/enroll.js.map +1 -0
  24. package/dist/cmd/auth/org/index.d.ts.map +1 -1
  25. package/dist/cmd/auth/org/index.js +53 -10
  26. package/dist/cmd/auth/org/index.js.map +1 -1
  27. package/dist/cmd/auth/org/status.d.ts +2 -0
  28. package/dist/cmd/auth/org/status.d.ts.map +1 -0
  29. package/dist/cmd/auth/org/status.js +60 -0
  30. package/dist/cmd/auth/org/status.js.map +1 -0
  31. package/dist/cmd/auth/org/unenroll.d.ts +2 -0
  32. package/dist/cmd/auth/org/unenroll.d.ts.map +1 -0
  33. package/dist/cmd/auth/org/unenroll.js +68 -0
  34. package/dist/cmd/auth/org/unenroll.js.map +1 -0
  35. package/dist/cmd/build/ast.d.ts.map +1 -1
  36. package/dist/cmd/build/ast.js +6 -0
  37. package/dist/cmd/build/ast.js.map +1 -1
  38. package/dist/cmd/cloud/deploy.d.ts.map +1 -1
  39. package/dist/cmd/cloud/deploy.js +11 -4
  40. package/dist/cmd/cloud/deploy.js.map +1 -1
  41. package/dist/cmd/cloud/keyvalue/get.d.ts.map +1 -1
  42. package/dist/cmd/cloud/keyvalue/get.js +6 -1
  43. package/dist/cmd/cloud/keyvalue/get.js.map +1 -1
  44. package/dist/cmd/cloud/keyvalue/repl.d.ts.map +1 -1
  45. package/dist/cmd/cloud/keyvalue/repl.js +6 -1
  46. package/dist/cmd/cloud/keyvalue/repl.js.map +1 -1
  47. package/dist/cmd/cloud/keyvalue/set.d.ts.map +1 -1
  48. package/dist/cmd/cloud/keyvalue/set.js +7 -1
  49. package/dist/cmd/cloud/keyvalue/set.js.map +1 -1
  50. package/dist/cmd/cloud/vector/get.d.ts.map +1 -1
  51. package/dist/cmd/cloud/vector/get.js +5 -0
  52. package/dist/cmd/cloud/vector/get.js.map +1 -1
  53. package/dist/cmd/cloud/vector/search.d.ts.map +1 -1
  54. package/dist/cmd/cloud/vector/search.js +2 -0
  55. package/dist/cmd/cloud/vector/search.js.map +1 -1
  56. package/dist/cmd/cloud/vector/upsert.d.ts.map +1 -1
  57. package/dist/cmd/cloud/vector/upsert.js +24 -1
  58. package/dist/cmd/cloud/vector/upsert.js.map +1 -1
  59. package/dist/cmd/project/reconcile.d.ts.map +1 -1
  60. package/dist/cmd/project/reconcile.js +15 -2
  61. package/dist/cmd/project/reconcile.js.map +1 -1
  62. package/dist/cmd/support/report.d.ts.map +1 -1
  63. package/dist/cmd/support/report.js.map +1 -1
  64. package/dist/cmd/support/system.d.ts.map +1 -1
  65. package/dist/cmd/support/system.js +5 -0
  66. package/dist/cmd/support/system.js.map +1 -1
  67. package/dist/internal-logger.d.ts +4 -0
  68. package/dist/internal-logger.d.ts.map +1 -1
  69. package/dist/internal-logger.js +18 -0
  70. package/dist/internal-logger.js.map +1 -1
  71. package/dist/types.d.ts +13 -0
  72. package/dist/types.d.ts.map +1 -1
  73. package/dist/types.js.map +1 -1
  74. package/package.json +6 -6
  75. package/src/agent-detection.ts +262 -0
  76. package/src/cache/index.ts +2 -0
  77. package/src/cache/project-cache.ts +41 -0
  78. package/src/cli.ts +10 -2
  79. package/src/cmd/auth/index.ts +0 -2
  80. package/src/cmd/auth/{machine/setup.ts → org/enroll.ts} +13 -13
  81. package/src/cmd/auth/org/index.ts +54 -10
  82. package/src/cmd/auth/org/status.ts +64 -0
  83. package/src/cmd/auth/org/unenroll.ts +80 -0
  84. package/src/cmd/build/ast.ts +6 -0
  85. package/src/cmd/cloud/deploy.ts +11 -4
  86. package/src/cmd/cloud/keyvalue/get.ts +6 -1
  87. package/src/cmd/cloud/keyvalue/repl.ts +6 -1
  88. package/src/cmd/cloud/keyvalue/set.ts +7 -1
  89. package/src/cmd/cloud/vector/get.ts +6 -0
  90. package/src/cmd/cloud/vector/search.ts +2 -0
  91. package/src/cmd/cloud/vector/upsert.ts +28 -1
  92. package/src/cmd/project/reconcile.ts +15 -2
  93. package/src/cmd/support/report.ts +1 -3
  94. package/src/cmd/support/system.ts +6 -0
  95. package/src/internal-logger.ts +18 -0
  96. package/src/types.ts +13 -0
  97. package/dist/cmd/auth/machine/index.d.ts +0 -2
  98. package/dist/cmd/auth/machine/index.d.ts.map +0 -1
  99. package/dist/cmd/auth/machine/index.js +0 -16
  100. package/dist/cmd/auth/machine/index.js.map +0 -1
  101. package/dist/cmd/auth/machine/setup.d.ts +0 -2
  102. package/dist/cmd/auth/machine/setup.d.ts.map +0 -1
  103. package/dist/cmd/auth/machine/setup.js.map +0 -1
  104. package/src/cmd/auth/machine/index.ts +0 -16
@@ -0,0 +1,262 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ /**
4
+ * Map of process names to internal agent short names.
5
+ * The key is the process name (or substring) that appears in the parent process command line.
6
+ * The value is the internal short name used to identify the agent.
7
+ *
8
+ * Process names verified via `agentuity cloud sandbox run --runtime <agent>:latest`:
9
+ * - opencode: binary 'opencode' (from bun install -g opencode-ai)
10
+ * - codex: binary 'codex' (from npm install -g @openai/codex)
11
+ * - cursor: binary 'cursor-agent' (from curl installer)
12
+ * - claude-code: binary 'claude', shows as 'node /usr/local/bin/claude'
13
+ * - copilot: binary 'copilot', shows as 'node /usr/local/bin/copilot' and spawns native binary
14
+ * - gemini: binary 'gemini', shows as 'node /usr/local/bin/gemini'
15
+ * - amp: binary 'amp', shows as 'node --no-warnings /usr/local/bin/amp'
16
+ *
17
+ * IMPORTANT: Order matters! More specific patterns should come before less specific ones.
18
+ * For example, 'opencode' must be checked before 'code' to avoid false matches.
19
+ */
20
+ export const KNOWN_AGENTS: [string, string][] = [
21
+ // Verified via cloud sandbox runtime - most specific patterns first
22
+ ['opencode', 'opencode'],
23
+ ['codex', 'codex'],
24
+ ['cursor-agent', 'cursor'],
25
+ ['claude', 'claude-code'],
26
+ ['copilot', 'copilot'],
27
+ ['gemini', 'gemini'],
28
+ ['cline', 'cline'],
29
+ ['roo-code', 'roo'],
30
+ ['windsurf', 'windsurf'],
31
+ ['zed', 'zed'],
32
+ ['amp', 'amp'],
33
+ // TODO: VSCode Agent Mode detection - need to find a reliable way to detect
34
+ // when VSCode's built-in agent (Copilot Chat) is running commands vs just
35
+ // running in VSCode's integrated terminal. May need env var detection.
36
+ ];
37
+
38
+ export type KnownAgent = (typeof KNOWN_AGENTS)[number][1];
39
+
40
+ /**
41
+ * Promise for the detection result (set when detection starts)
42
+ */
43
+ let detectionPromise: Promise<string | undefined> | null = null;
44
+
45
+ /**
46
+ * Cached result after detection completes (null = not yet resolved)
47
+ */
48
+ let cachedResult: string | undefined | null = null;
49
+
50
+ /**
51
+ * Callbacks to invoke when agent detection completes
52
+ */
53
+ const detectionCallbacks: Array<(agent: string | undefined) => void> = [];
54
+
55
+ /**
56
+ * Get the command line and parent PID for a given PID in a single ps call
57
+ */
58
+ function getProcessInfo(pid: number): Promise<{ command: string; ppid: number } | undefined> {
59
+ return new Promise((resolve) => {
60
+ const ps = spawn('ps', ['-p', String(pid), '-o', 'ppid=,command='], {
61
+ stdio: ['ignore', 'pipe', 'ignore'],
62
+ });
63
+
64
+ let output = '';
65
+
66
+ ps.stdout.on('data', (data: Buffer) => {
67
+ output += data.toString();
68
+ });
69
+
70
+ ps.on('close', () => {
71
+ const trimmed = output.trim();
72
+ if (!trimmed) {
73
+ resolve(undefined);
74
+ return;
75
+ }
76
+
77
+ // Output format: " PPID COMMAND" (ppid is right-aligned, then space, then command)
78
+ const match = trimmed.match(/^\s*(\d+)\s+(.+)$/);
79
+ if (!match) {
80
+ resolve(undefined);
81
+ return;
82
+ }
83
+
84
+ const ppid = parseInt(match[1], 10);
85
+ const command = match[2].toLowerCase();
86
+
87
+ if (isNaN(ppid) || ppid <= 1 || !command) {
88
+ resolve(undefined);
89
+ return;
90
+ }
91
+
92
+ resolve({ command, ppid });
93
+ });
94
+
95
+ ps.on('error', () => {
96
+ resolve(undefined);
97
+ });
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Check if a command matches any known agent
103
+ */
104
+ function matchAgent(command: string): string | undefined {
105
+ for (const [processName, agentName] of KNOWN_AGENTS) {
106
+ if (command.includes(processName)) {
107
+ return agentName;
108
+ }
109
+ }
110
+ return undefined;
111
+ }
112
+
113
+ /**
114
+ * Detect the parent process command and check if it matches a known agent.
115
+ * Walks up the process tree to find the first matching agent.
116
+ */
117
+ function detectParentAgent(): Promise<string | undefined> {
118
+ return new Promise((resolve) => {
119
+ // TODO: Implement Windows support using wmic or PowerShell
120
+ if (process.platform === 'win32') {
121
+ resolve(undefined);
122
+ return;
123
+ }
124
+
125
+ const maxDepth = 10; // Limit how far up we walk the tree
126
+
127
+ async function walkTree(pid: number, depth: number): Promise<string | undefined> {
128
+ if (depth >= maxDepth || pid <= 1) {
129
+ return undefined;
130
+ }
131
+
132
+ // Get both command and parent PID in a single ps call
133
+ const info = await getProcessInfo(pid);
134
+ if (!info) {
135
+ return undefined;
136
+ }
137
+
138
+ // Check if this process matches a known agent
139
+ const agent = matchAgent(info.command);
140
+ if (agent) {
141
+ return agent;
142
+ }
143
+
144
+ // Walk up to parent
145
+ return walkTree(info.ppid, depth + 1);
146
+ }
147
+
148
+ const ppid = process.ppid;
149
+ if (!ppid) {
150
+ resolve(undefined);
151
+ return;
152
+ }
153
+
154
+ walkTree(ppid, 0).then(resolve).catch(() => resolve(undefined));
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Start agent detection immediately (non-blocking).
160
+ * Call this early in CLI startup to begin detection in the background.
161
+ */
162
+ export function startAgentDetection(): void {
163
+ if (detectionPromise !== null) {
164
+ // Already started
165
+ return;
166
+ }
167
+
168
+ detectionPromise = detectParentAgent().then((result) => {
169
+ cachedResult = result;
170
+ // Invoke all registered callbacks
171
+ for (const callback of detectionCallbacks) {
172
+ try {
173
+ callback(result);
174
+ } catch {
175
+ // Ignore callback errors
176
+ }
177
+ }
178
+ return result;
179
+ });
180
+ }
181
+
182
+ /**
183
+ * Register a callback to be invoked when agent detection completes.
184
+ * If detection has already completed, the callback is invoked immediately.
185
+ * This is non-blocking and does not return a promise.
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * onAgentDetected((agent) => {
190
+ * if (agent) {
191
+ * console.log(`Detected agent: ${agent}`);
192
+ * }
193
+ * });
194
+ * ```
195
+ */
196
+ export function onAgentDetected(callback: (agent: string | undefined) => void): void {
197
+ // If detection already completed, invoke immediately
198
+ if (cachedResult !== null) {
199
+ try {
200
+ callback(cachedResult);
201
+ } catch {
202
+ // Ignore callback errors
203
+ }
204
+ return;
205
+ }
206
+
207
+ // Otherwise, register for later invocation
208
+ detectionCallbacks.push(callback);
209
+ }
210
+
211
+ /**
212
+ * Get the cached detection result synchronously.
213
+ * Returns undefined if detection hasn't completed yet or no agent was detected.
214
+ * Returns null if detection hasn't started or completed yet.
215
+ *
216
+ * Use this for synchronous access when you don't want to wait for detection.
217
+ */
218
+ export function getDetectedAgent(): string | undefined | null {
219
+ return cachedResult;
220
+ }
221
+
222
+ /**
223
+ * Wait for agent detection to complete and ensure all callbacks have been invoked.
224
+ * Call this before CLI exit to ensure the detected agent is written to session logs.
225
+ *
226
+ * This is a no-op if detection hasn't started or has already completed.
227
+ */
228
+ export async function flushAgentDetection(): Promise<void> {
229
+ if (detectionPromise !== null) {
230
+ await detectionPromise;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Check if the CLI is being executed from a known coding agent.
236
+ * Returns the agent name if detected, undefined otherwise.
237
+ *
238
+ * This function returns immediately if detection has already completed,
239
+ * otherwise it awaits the detection promise started by startAgentDetection().
240
+ *
241
+ * @example
242
+ * ```typescript
243
+ * const agent = await isExecutingFromAgent();
244
+ * if (agent) {
245
+ * logger.debug(`Running from agent: ${agent}`);
246
+ * }
247
+ * ```
248
+ */
249
+ export async function isExecutingFromAgent(): Promise<string | undefined> {
250
+ // Return cached result if detection has completed
251
+ if (cachedResult !== null) {
252
+ return cachedResult;
253
+ }
254
+
255
+ // If detection hasn't started yet, start it now
256
+ if (detectionPromise === null) {
257
+ startAgentDetection();
258
+ }
259
+
260
+ // Wait for detection to complete
261
+ return detectionPromise!;
262
+ }
@@ -9,3 +9,5 @@ export {
9
9
  type ResourceType,
10
10
  type ResourceInfo,
11
11
  } from './resource-region';
12
+
13
+ export { getCachedProject, setCachedProject, clearProjectCache } from './project-cache';
@@ -0,0 +1,41 @@
1
+ import type { Project } from '@agentuity/server';
2
+
3
+ /**
4
+ * In-memory cache for project data to avoid duplicate API calls within a single CLI command execution.
5
+ * This cache is NOT persisted to disk - it only lives for the duration of the CLI process.
6
+ *
7
+ * The cache key is `{profile}:{projectId}` to ensure proper isolation between profiles.
8
+ */
9
+ const projectCache = new Map<string, Project>();
10
+
11
+ /**
12
+ * Generate a cache key from profile and project ID
13
+ */
14
+ function getCacheKey(profile: string, projectId: string): string {
15
+ return `${profile}:${projectId}`;
16
+ }
17
+
18
+ /**
19
+ * Get a cached project by profile and project ID.
20
+ * Returns null if not found in cache.
21
+ */
22
+ export function getCachedProject(profile: string, projectId: string): Project | null {
23
+ const key = getCacheKey(profile, projectId);
24
+ return projectCache.get(key) ?? null;
25
+ }
26
+
27
+ /**
28
+ * Store a project in the cache.
29
+ */
30
+ export function setCachedProject(profile: string, projectId: string, project: Project): void {
31
+ const key = getCacheKey(profile, projectId);
32
+ projectCache.set(key, project);
33
+ }
34
+
35
+ /**
36
+ * Clear all cached projects.
37
+ * Useful for testing or when switching contexts.
38
+ */
39
+ export function clearProjectCache(): void {
40
+ projectCache.clear();
41
+ }
package/src/cli.ts CHANGED
@@ -27,6 +27,7 @@ import { getCommand } from './command-prefix';
27
27
  import { isValidateMode, outputValidation, type ValidationResult } from './output';
28
28
  import { StructuredError } from '@agentuity/core';
29
29
  import { setProgram } from './program-ref';
30
+ import { getCachedProject, setCachedProject } from './cache';
30
31
 
31
32
  /**
32
33
  * Check if an error is a CLI input validation error (Zod error from schema parsing),
@@ -1052,8 +1053,15 @@ async function registerSubcommand(
1052
1053
  },
1053
1054
  };
1054
1055
  const apiClient = createAPIClient(baseCtx, configWithAuth as Config);
1055
- const { projectGet } = await import('@agentuity/server');
1056
- const projectDetails = await projectGet(apiClient, { id: projectId, mask: true });
1056
+ // Check cache first to avoid duplicate API calls
1057
+ const profile = baseCtx.config?.name ?? 'default';
1058
+ let projectDetails = getCachedProject(profile, projectId);
1059
+ if (!projectDetails) {
1060
+ const { projectGet } = await import('@agentuity/server');
1061
+ // Use keys: false to match other callers and ensure cache consistency
1062
+ projectDetails = await projectGet(apiClient, { id: projectId, keys: false });
1063
+ setCachedProject(profile, projectId, projectDetails);
1064
+ }
1057
1065
  project = {
1058
1066
  projectId: projectDetails.id,
1059
1067
  orgId: projectDetails.orgId,
@@ -6,7 +6,6 @@ import { signupCommand } from './signup';
6
6
  import { whoamiCommand } from './whoami';
7
7
  import { sshSubcommand } from './ssh';
8
8
  import { orgSubcommand } from './org';
9
- import { machineSubcommand } from './machine';
10
9
  import { getCommand } from '../../command-prefix';
11
10
 
12
11
  export const command = createCommand({
@@ -26,6 +25,5 @@ export const command = createCommand({
26
25
  whoamiCommand,
27
26
  sshSubcommand,
28
27
  orgSubcommand,
29
- machineSubcommand,
30
28
  ],
31
29
  });
@@ -1,28 +1,28 @@
1
1
  import { z } from 'zod';
2
2
  import { createSubcommand } from '../../../types';
3
3
  import * as tui from '../../../tui';
4
- import { machineAuthSetup } from '@agentuity/server';
4
+ import { orgAuthEnroll } from '@agentuity/server';
5
5
  import { getCommand } from '../../../command-prefix';
6
6
  import { ErrorCode } from '../../../errors';
7
7
  import { readFileSync } from 'fs';
8
8
 
9
- const MachineSetupResponseSchema = z.object({
10
- success: z.boolean().describe('Whether the setup succeeded'),
9
+ const EnrollResponseSchema = z.object({
10
+ success: z.boolean().describe('Whether the enrollment succeeded'),
11
11
  orgId: z.string().describe('The organization ID'),
12
12
  });
13
13
 
14
- export const setupSubcommand = createSubcommand({
15
- name: 'setup',
14
+ export const enrollSubcommand = createSubcommand({
15
+ name: 'enroll',
16
16
  description:
17
- 'Set up machine authentication by uploading a public key for self-hosted infrastructure',
17
+ 'Configure your organization for self-hosted infrastructure by uploading a public key',
18
18
  tags: ['mutating', 'slow', 'requires-auth', 'uses-stdin'],
19
19
  examples: [
20
20
  {
21
- command: `${getCommand('auth machine setup')} --file ./public-key.pem`,
21
+ command: `${getCommand('auth org enroll')} --file ./public-key.pem`,
22
22
  description: 'Upload ECDSA P-256 public key from file',
23
23
  },
24
24
  {
25
- command: `cat public-key.pem | ${getCommand('auth machine setup')}`,
25
+ command: `cat public-key.pem | ${getCommand('auth org enroll')}`,
26
26
  description: 'Upload public key from stdin',
27
27
  },
28
28
  ],
@@ -32,7 +32,7 @@ export const setupSubcommand = createSubcommand({
32
32
  options: z.object({
33
33
  file: z.string().optional().describe('Path to the public key file (PEM format)'),
34
34
  }),
35
- response: MachineSetupResponseSchema,
35
+ response: EnrollResponseSchema,
36
36
  },
37
37
  async handler(ctx) {
38
38
  const { apiClient, opts, options, logger, orgId } = ctx;
@@ -83,13 +83,13 @@ export const setupSubcommand = createSubcommand({
83
83
 
84
84
  const result = await tui.spinner({
85
85
  type: 'simple',
86
- message: 'Setting up machine authentication...',
87
- callback: () => machineAuthSetup(apiClient, orgId, publicKey),
86
+ message: 'Enrolling organization...',
87
+ callback: () => orgAuthEnroll(apiClient, orgId, publicKey),
88
88
  clearOnSuccess: true,
89
89
  });
90
90
 
91
91
  if (!options.json) {
92
- tui.success(`Machine authentication configured for organization ${result.orgId}`);
92
+ tui.success(`Organization ${result.orgId} enrolled for self-hosted infrastructure.`);
93
93
  tui.newline();
94
94
  tui.info(
95
95
  'Your self-hosted machines can now authenticate using the corresponding private key.'
@@ -98,7 +98,7 @@ export const setupSubcommand = createSubcommand({
98
98
 
99
99
  return { success: true, orgId: result.orgId };
100
100
  } catch (ex) {
101
- tui.fatal(`Failed to set up machine authentication: ${ex}`, ErrorCode.API_ERROR);
101
+ tui.fatal(`Failed to enroll organization: ${ex}`, ErrorCode.API_ERROR);
102
102
  }
103
103
  },
104
104
  });
@@ -4,6 +4,9 @@ import { getCommand } from '../../../command-prefix';
4
4
  import { saveOrgId, clearOrgId } from '../../../config';
5
5
  import * as tui from '../../../tui';
6
6
  import { listOrganizations } from '@agentuity/server';
7
+ import { enrollSubcommand } from './enroll';
8
+ import { unenrollSubcommand } from './unenroll';
9
+ import { statusSubcommand } from './status';
7
10
 
8
11
  const selectCommand = createSubcommand({
9
12
  name: 'select',
@@ -75,7 +78,8 @@ const selectCommand = createSubcommand({
75
78
  const unselectCommand = createSubcommand({
76
79
  name: 'unselect',
77
80
  description: 'Clear the default organization preference',
78
- tags: ['fast'],
81
+ tags: ['fast', 'requires-auth'],
82
+ requires: { auth: true, apiClient: true },
79
83
  examples: [
80
84
  { command: getCommand('auth org unselect'), description: 'Clear default organization' },
81
85
  ],
@@ -106,37 +110,77 @@ const unselectCommand = createSubcommand({
106
110
  const currentCommand = createSubcommand({
107
111
  name: 'current',
108
112
  description: 'Show the current default organization',
109
- tags: ['read-only', 'fast'],
113
+ tags: ['read-only', 'fast', 'requires-auth'],
110
114
  idempotent: true,
115
+ requires: { auth: true, apiClient: true },
111
116
  examples: [
112
- { command: getCommand('auth org current'), description: 'Show default organization' },
117
+ { command: getCommand('auth org current'), description: 'Show default organization ID' },
118
+ { command: getCommand('auth org current --name'), description: 'Show default organization name' },
113
119
  { command: getCommand('auth org current --json'), description: 'Show output in JSON format' },
114
120
  ],
115
121
  schema: {
116
- response: z.string().nullable().describe('The current organization ID or null if not set'),
122
+ options: z.object({
123
+ name: z.boolean().optional().describe('Show organization name instead of ID'),
124
+ }),
125
+ response: z
126
+ .object({
127
+ id: z.string().nullable().describe('The current organization ID or null if not set'),
128
+ name: z.string().nullable().describe('The current organization name or null if not set or not found'),
129
+ })
130
+ .describe('The current organization details'),
117
131
  },
118
132
 
119
133
  async handler(ctx) {
120
- const { options, config } = ctx;
134
+ const { options, config, apiClient, opts } = ctx;
121
135
  const orgId = config?.preferences?.orgId || null;
122
136
 
137
+ let orgName: string | null = null;
138
+
139
+ // Fetch org name if we have an orgId and either --name or --json is requested
140
+ if (orgId && (opts.name || options.json)) {
141
+ const orgs = await listOrganizations(apiClient);
142
+ const org = orgs.find((o) => o.id === orgId);
143
+ orgName = org?.name ?? null;
144
+ }
145
+
123
146
  if (!options.json) {
124
- if (orgId) {
125
- console.log(orgId);
147
+ if (opts.name) {
148
+ // --name flag: print only the org name
149
+ if (orgName) {
150
+ console.log(orgName);
151
+ }
152
+ } else {
153
+ // Default behavior: print only the org ID
154
+ if (orgId) {
155
+ console.log(orgId);
156
+ }
126
157
  }
127
158
  }
128
159
 
129
- return orgId;
160
+ return { id: orgId, name: orgName };
130
161
  },
131
162
  });
132
163
 
133
164
  export const orgSubcommand = createCommand({
134
165
  name: 'org',
135
- description: 'Manage default organization preference',
166
+ aliases: ['machine', 'organization'],
167
+ description: 'Manage organization preferences and machine authentication',
136
168
  tags: ['fast'],
137
169
  examples: [
138
170
  { command: getCommand('auth org select'), description: 'Set default organization' },
139
171
  { command: getCommand('auth org current'), description: 'Show current default' },
172
+ {
173
+ command: getCommand('auth org enroll --file ./public-key.pem'),
174
+ description: 'Enroll an organization',
175
+ },
176
+ { command: getCommand('auth org status'), description: 'Show org auth status' },
177
+ ],
178
+ subcommands: [
179
+ selectCommand,
180
+ unselectCommand,
181
+ currentCommand,
182
+ enrollSubcommand,
183
+ unenrollSubcommand,
184
+ statusSubcommand,
140
185
  ],
141
- subcommands: [selectCommand, unselectCommand, currentCommand],
142
186
  });
@@ -0,0 +1,64 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand } from '../../../types';
3
+ import * as tui from '../../../tui';
4
+ import { orgAuthStatus } from '@agentuity/server';
5
+ import { getCommand } from '../../../command-prefix';
6
+ import { ErrorCode } from '../../../errors';
7
+
8
+ const StatusResponseSchema = z.object({
9
+ publicKey: z.string().nullable().describe('The public key or null if not set'),
10
+ });
11
+
12
+ export const statusSubcommand = createSubcommand({
13
+ name: 'status',
14
+ aliases: ['show'],
15
+ description: 'Get the current public key status for an organization',
16
+ tags: ['read-only', 'fast', 'requires-auth'],
17
+ examples: [
18
+ {
19
+ command: getCommand('auth org status'),
20
+ description: 'Show the current public key for the organization',
21
+ },
22
+ {
23
+ command: getCommand('auth org show'),
24
+ description: 'Show the current public key (alias)',
25
+ },
26
+ ],
27
+ requires: { auth: true, apiClient: true, org: true },
28
+ idempotent: true,
29
+ schema: {
30
+ response: StatusResponseSchema,
31
+ },
32
+ async handler(ctx) {
33
+ const { apiClient, options, orgId } = ctx;
34
+
35
+ try {
36
+ const result = await tui.spinner({
37
+ type: 'simple',
38
+ message: 'Fetching organization authentication status...',
39
+ callback: () => orgAuthStatus(apiClient, orgId),
40
+ clearOnSuccess: true,
41
+ });
42
+
43
+ if (!options.json) {
44
+ if (result.publicKey) {
45
+ tui.success('Organization authentication is configured');
46
+ tui.newline();
47
+ console.log(tui.bold('Public Key:'));
48
+ tui.newline();
49
+ console.log(result.publicKey);
50
+ } else {
51
+ tui.info('No public key is configured for this organization');
52
+ tui.newline();
53
+ tui.info(
54
+ `Use '${getCommand('auth org enroll --file ./public-key.pem')}' to set up machine authentication.`
55
+ );
56
+ }
57
+ }
58
+
59
+ return { publicKey: result.publicKey };
60
+ } catch (ex) {
61
+ tui.fatal(`Failed to get organization authentication status: ${ex}`, ErrorCode.API_ERROR);
62
+ }
63
+ },
64
+ });
@@ -0,0 +1,80 @@
1
+ import { z } from 'zod';
2
+ import { createSubcommand } from '../../../types';
3
+ import * as tui from '../../../tui';
4
+ import { orgAuthUnenroll } from '@agentuity/server';
5
+ import { getCommand } from '../../../command-prefix';
6
+ import { ErrorCode } from '../../../errors';
7
+
8
+ const UnenrollResponseSchema = z.object({
9
+ success: z.boolean().describe('Whether the unenrollment succeeded'),
10
+ });
11
+
12
+ export const unenrollSubcommand = createSubcommand({
13
+ name: 'unenroll',
14
+ description: 'Remove the public key from the organization, disabling self-hosted infrastructure',
15
+ tags: ['mutating', 'destructive', 'slow', 'requires-auth'],
16
+ examples: [
17
+ {
18
+ command: getCommand('auth org unenroll'),
19
+ description: 'Remove the public key from the current organization (with confirmation)',
20
+ },
21
+ {
22
+ command: getCommand('auth org unenroll --confirm'),
23
+ description: 'Remove the public key without confirmation prompt',
24
+ },
25
+ ],
26
+ requires: { auth: true, apiClient: true, org: true },
27
+ idempotent: true,
28
+ schema: {
29
+ options: z.object({
30
+ confirm: z.boolean().optional().default(false).describe('Skip confirmation prompt'),
31
+ }),
32
+ response: UnenrollResponseSchema,
33
+ },
34
+ async handler(ctx) {
35
+ const { apiClient, options, opts, orgId } = ctx;
36
+
37
+ if (!opts.confirm) {
38
+ if (process.stdin.isTTY) {
39
+ tui.newline();
40
+ tui.warning(
41
+ 'Removing your public key will immediately stop all network traffic and routing to your machines and all further traffic will immediately fail.'
42
+ );
43
+ tui.newline();
44
+
45
+ const confirmed = await tui.confirm(
46
+ 'Are you sure you want to remove organization authentication?',
47
+ false
48
+ );
49
+
50
+ if (!confirmed) {
51
+ tui.info('Unenrollment cancelled.');
52
+ return { success: false };
53
+ }
54
+ } else {
55
+ tui.error(
56
+ 'Non-interactive sessions require --confirm flag to unenroll. Use: ' +
57
+ getCommand('auth org unenroll --confirm')
58
+ );
59
+ return { success: false };
60
+ }
61
+ }
62
+
63
+ try {
64
+ await tui.spinner({
65
+ type: 'simple',
66
+ message: 'Removing organization authentication...',
67
+ callback: () => orgAuthUnenroll(apiClient, orgId),
68
+ clearOnSuccess: true,
69
+ });
70
+
71
+ if (!options.json) {
72
+ tui.success('Organization authentication removed');
73
+ }
74
+
75
+ return { success: true };
76
+ } catch (ex) {
77
+ tui.fatal(`Failed to unenroll organization: ${ex}`, ErrorCode.API_ERROR);
78
+ }
79
+ },
80
+ });