@ogment-ai/cli 0.3.5 → 0.4.1

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 (72) hide show
  1. package/README.md +72 -76
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +521 -247
  5. package/dist/commands/auth.d.ts +22 -0
  6. package/dist/commands/auth.d.ts.map +1 -0
  7. package/dist/commands/auth.js +29 -0
  8. package/dist/commands/catalog.d.ts +29 -0
  9. package/dist/commands/catalog.d.ts.map +1 -0
  10. package/dist/commands/catalog.js +151 -0
  11. package/dist/commands/invoke.d.ts +17 -0
  12. package/dist/commands/invoke.d.ts.map +1 -0
  13. package/dist/commands/invoke.js +123 -0
  14. package/dist/commands/{info.d.ts → status.d.ts} +4 -4
  15. package/dist/commands/status.d.ts.map +1 -0
  16. package/dist/commands/{info.js → status.js} +1 -1
  17. package/dist/output/envelope.d.ts +19 -0
  18. package/dist/output/envelope.d.ts.map +1 -0
  19. package/dist/output/envelope.js +51 -0
  20. package/dist/output/manager.d.ts +16 -3
  21. package/dist/output/manager.d.ts.map +1 -1
  22. package/dist/output/manager.js +27 -29
  23. package/dist/services/account.d.ts.map +1 -1
  24. package/dist/services/account.js +18 -2
  25. package/dist/services/auth.d.ts +14 -3
  26. package/dist/services/auth.d.ts.map +1 -1
  27. package/dist/services/auth.js +119 -15
  28. package/dist/services/info.d.ts.map +1 -1
  29. package/dist/services/info.js +11 -10
  30. package/dist/services/mcp.d.ts.map +1 -1
  31. package/dist/services/mcp.js +24 -23
  32. package/dist/shared/constants.d.ts +0 -1
  33. package/dist/shared/constants.d.ts.map +1 -1
  34. package/dist/shared/constants.js +0 -1
  35. package/dist/shared/error-codes.d.ts +28 -0
  36. package/dist/shared/error-codes.d.ts.map +1 -0
  37. package/dist/shared/error-codes.js +25 -0
  38. package/dist/shared/errors.d.ts +100 -9
  39. package/dist/shared/errors.d.ts.map +1 -1
  40. package/dist/shared/errors.js +101 -0
  41. package/dist/shared/exit-codes.d.ts +8 -5
  42. package/dist/shared/exit-codes.d.ts.map +1 -1
  43. package/dist/shared/exit-codes.js +31 -14
  44. package/dist/shared/guards.d.ts +5 -1
  45. package/dist/shared/guards.d.ts.map +1 -1
  46. package/dist/shared/guards.js +6 -1
  47. package/dist/shared/retry.d.ts +17 -0
  48. package/dist/shared/retry.d.ts.map +1 -0
  49. package/dist/shared/retry.js +27 -0
  50. package/dist/shared/schema-example.d.ts +2 -0
  51. package/dist/shared/schema-example.d.ts.map +1 -0
  52. package/dist/shared/schema-example.js +128 -0
  53. package/dist/shared/schemas.d.ts +0 -42
  54. package/dist/shared/schemas.d.ts.map +1 -1
  55. package/dist/shared/schemas.js +0 -21
  56. package/dist/shared/types.d.ts +84 -12
  57. package/dist/shared/types.d.ts.map +1 -1
  58. package/dist/shared/types.js +1 -1
  59. package/package.json +5 -7
  60. package/dist/commands/call.d.ts +0 -14
  61. package/dist/commands/call.d.ts.map +0 -1
  62. package/dist/commands/call.js +0 -51
  63. package/dist/commands/describe.d.ts +0 -4
  64. package/dist/commands/describe.d.ts.map +0 -1
  65. package/dist/commands/describe.js +0 -109
  66. package/dist/commands/info.d.ts.map +0 -1
  67. package/dist/commands/servers.d.ts +0 -13
  68. package/dist/commands/servers.d.ts.map +0 -1
  69. package/dist/commands/servers.js +0 -29
  70. package/dist/postinstall.d.ts +0 -2
  71. package/dist/postinstall.d.ts.map +0 -1
  72. package/dist/postinstall.js +0 -12
package/dist/cli.js CHANGED
@@ -2,10 +2,10 @@
2
2
  import { realpathSync } from "node:fs";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { Command, CommanderError } from "commander";
5
- import { runCallCommand } from "./commands/call.js";
6
- import { runDescribeCommand } from "./commands/describe.js";
7
- import { runInfoCommand } from "./commands/info.js";
8
- import { runServersCommand } from "./commands/servers.js";
5
+ import { runAuthLoginCommand, runAuthLogoutCommand, runAuthStatusCommand, } from "./commands/auth.js";
6
+ import { runCatalogCommand, runCatalogToolDetailsCommand, runCatalogToolsCommand, } from "./commands/catalog.js";
7
+ import { runInvokeCommand } from "./commands/invoke.js";
8
+ import { runStatusCommand } from "./commands/status.js";
9
9
  import { createBrowserOpener } from "./infra/browser.js";
10
10
  import { createFileCredentialsStore } from "./infra/credentials.js";
11
11
  import { createRuntimeConfig } from "./infra/env.js";
@@ -15,9 +15,11 @@ import { createAccountService } from "./services/account.js";
15
15
  import { createAuthService } from "./services/auth.js";
16
16
  import { createInfoService } from "./services/info.js";
17
17
  import { createMcpService } from "./services/mcp.js";
18
- import { UnexpectedError, ValidationError } from "./shared/errors.js";
18
+ import { buildJsonSchemaExample } from "./shared/schema-example.js";
19
+ import { ERROR_CODE } from "./shared/error-codes.js";
20
+ import { AuthError, ContractMismatchError, NotFoundError, RemoteRequestError, UnexpectedError, ValidationError, } from "./shared/errors.js";
19
21
  import { EXIT_CODE, exitCodeForError } from "./shared/exit-codes.js";
20
- import { APP_DESCRIPTION, APP_NAME, AGENT_SUCCESS_HINT, VERSION } from "./shared/constants.js";
22
+ import { APP_DESCRIPTION, APP_NAME, VERSION } from "./shared/constants.js";
21
23
  class CliExitError extends Error {
22
24
  exitCode;
23
25
  constructor(exitCode) {
@@ -76,7 +78,7 @@ const asGlobalOptions = (command) => {
76
78
  const options = command.optsWithGlobals();
77
79
  return {
78
80
  apiKey: options.apiKey,
79
- json: options.json,
81
+ human: options.human,
80
82
  nonInteractive: options.nonInteractive,
81
83
  quiet: options.quiet,
82
84
  yes: options.yes,
@@ -84,7 +86,7 @@ const asGlobalOptions = (command) => {
84
86
  };
85
87
  const mapGlobalOutputOptions = (options) => {
86
88
  return {
87
- json: options.json,
89
+ human: options.human,
88
90
  nonInteractive: options.nonInteractive,
89
91
  quiet: options.quiet,
90
92
  yes: options.yes,
@@ -93,177 +95,166 @@ const mapGlobalOutputOptions = (options) => {
93
95
  const throwCommandError = (error) => {
94
96
  throw new CliExitError(exitCodeForError(error));
95
97
  };
96
- const isRecord = (value) => {
97
- return typeof value === "object" && value !== null;
98
+ const nextAction = (id, title, command, reason, when = null) => {
99
+ return {
100
+ command,
101
+ id,
102
+ reason,
103
+ title,
104
+ when,
105
+ };
106
+ };
107
+ const hasPlaceholderTokens = (command) => {
108
+ return command.includes("<") || command.includes(">");
109
+ };
110
+ const nextActionsForError = (error) => {
111
+ const fallbackByCode = {
112
+ [ERROR_CODE.authDeviceExpired]: {
113
+ command: "ogment auth login",
114
+ reason: "Device code expired; restart login to continue.",
115
+ title: "Restart login",
116
+ },
117
+ [ERROR_CODE.authDevicePending]: {
118
+ command: "ogment auth status",
119
+ reason: "Authorization is pending; check whether credentials are now available.",
120
+ title: "Check auth status",
121
+ },
122
+ [ERROR_CODE.authInvalidCredentials]: {
123
+ command: "ogment auth logout",
124
+ reason: "Credentials are invalid; clear local state before re-authenticating.",
125
+ title: "Reset auth state",
126
+ },
127
+ [ERROR_CODE.authRequired]: {
128
+ command: "ogment auth login",
129
+ reason: "Authentication is required before catalog or invoke commands can run.",
130
+ title: "Authenticate",
131
+ },
132
+ [ERROR_CODE.remoteRateLimited]: {
133
+ command: "ogment status",
134
+ reason: "Remote rate limit detected; check diagnostics before retrying.",
135
+ title: "Inspect diagnostics",
136
+ },
137
+ [ERROR_CODE.remoteUnavailable]: {
138
+ command: "ogment status",
139
+ reason: "Remote service is unavailable; check connectivity and endpoint health.",
140
+ title: "Inspect diagnostics",
141
+ },
142
+ [ERROR_CODE.toolInputSchemaViolation]: {
143
+ command: "ogment catalog",
144
+ reason: "Tool contract did not validate; inspect available servers and tool metadata.",
145
+ title: "Inspect catalog",
146
+ },
147
+ [ERROR_CODE.toolNotFound]: {
148
+ command: "ogment catalog",
149
+ reason: "Requested tool could not be found; rediscover servers and tools.",
150
+ title: "Rediscover tools",
151
+ },
152
+ [ERROR_CODE.transportRequestFailed]: {
153
+ command: "ogment status",
154
+ reason: "Transport request failed; inspect connectivity diagnostics.",
155
+ title: "Inspect diagnostics",
156
+ },
157
+ [ERROR_CODE.contractVersionUnsupported]: {
158
+ command: "ogment --version",
159
+ reason: "Contract mismatch detected; verify CLI version and upgrade before retrying.",
160
+ title: "Inspect CLI version",
161
+ },
162
+ [ERROR_CODE.internalUnexpected]: {
163
+ command: "ogment status",
164
+ reason: "Unexpected failure detected; run diagnostics before retrying the workflow.",
165
+ title: "Inspect diagnostics",
166
+ },
167
+ [ERROR_CODE.validationInvalidInput]: {
168
+ command: "ogment --help",
169
+ reason: "Input validation failed; inspect canonical command syntax.",
170
+ title: "Show command help",
171
+ },
172
+ };
173
+ const fallback = fallbackByCode[error.code];
174
+ const suggestedCommand = typeof error.suggestedCommand === "string" && !hasPlaceholderTokens(error.suggestedCommand)
175
+ ? error.suggestedCommand
176
+ : undefined;
177
+ const command = suggestedCommand ?? fallback?.command;
178
+ if (command === undefined) {
179
+ return [];
180
+ }
181
+ return [
182
+ nextAction(`recover_${error.code.toLowerCase()}`, fallback?.title ?? "Run suggested command", command, fallback?.reason ?? `Suggested recovery for error ${error.code}.`, "immediate"),
183
+ ];
98
184
  };
99
- const ensureSuccess = (result, output) => {
185
+ const ensureSuccess = (result, output, context) => {
100
186
  if (result.status === "error") {
101
- output.error(result.error);
187
+ output.error(result.error, {
188
+ command: context.command,
189
+ entity: context.entity ?? null,
190
+ nextActions: nextActionsForError(result.error),
191
+ });
102
192
  throwCommandError(result.error);
103
193
  }
104
194
  return result.value;
105
195
  };
106
- const formatSchemaType = (property) => {
107
- if (Array.isArray(property.enum) && property.enum.length > 0) {
108
- return property.enum.map(String).join(" | ");
109
- }
110
- if (property.type === "array" && typeof property.items?.type === "string") {
111
- return `${property.items.type}[]`;
112
- }
113
- if (typeof property.type === "string" && property.type.length > 0) {
114
- return property.type;
115
- }
116
- return "unknown";
196
+ const nextActionsForLogin = (payload, mode) => {
197
+ return [
198
+ nextAction("check_auth_status", "Check auth status", "ogment auth status", `Verify persisted credentials after ${mode} login for ${payload.agentName}.`, "immediate"),
199
+ nextAction("discover_servers", "Discover servers", "ogment catalog", `Logged in as ${payload.agentName} via ${mode}; discover available servers.`, "after_auth"),
200
+ ];
117
201
  };
118
- const renderSchemaParameters = (output, schema) => {
119
- const required = new Set();
120
- const requiredValue = schema["required"];
121
- if (Array.isArray(requiredValue)) {
122
- for (const item of requiredValue) {
123
- if (typeof item === "string") {
124
- required.add(item);
125
- }
126
- }
127
- }
128
- const propertiesValue = schema["properties"];
129
- if (!isRecord(propertiesValue)) {
130
- output.info(" Parameters: none");
131
- return;
132
- }
133
- const entries = Object.entries(propertiesValue);
134
- if (entries.length === 0) {
135
- output.info(" Parameters: none");
136
- return;
137
- }
138
- output.info(" Parameters:");
139
- for (const [name, propertyValue] of entries) {
140
- if (!isRecord(propertyValue)) {
141
- continue;
142
- }
143
- const property = propertyValue;
144
- const type = formatSchemaType(property);
145
- const requirement = required.has(name) ? "required" : "optional";
146
- const description = typeof property.description === "string" && property.description.length > 0
147
- ? ` - ${property.description}`
148
- : "";
149
- output.info(` ${name} (${type}, ${requirement})${description}`);
150
- }
202
+ const nextActionsForAuthStatus = (payload) => {
203
+ if (!payload.loggedIn) {
204
+ return [
205
+ nextAction("login", "Authenticate", "ogment auth login", "No API key is configured locally; login is required.", "immediate"),
206
+ ];
207
+ }
208
+ return [
209
+ nextAction("discover_servers", "Discover servers", "ogment catalog", `Authenticated via ${payload.apiKeySource}; discover servers next.`, "after_auth"),
210
+ ];
151
211
  };
152
- const renderSchemaBlock = (output, label, schema) => {
153
- if (schema === undefined) {
154
- output.info(` ${label}: unavailable`);
155
- return;
212
+ const nextActionsForCatalogSummary = (payload, context) => {
213
+ const actions = [];
214
+ const targetServer = payload.servers.find((server) => server.toolCount > 0);
215
+ if (targetServer !== undefined) {
216
+ actions.push(nextAction("inspect_tools", "Inspect tools", `ogment catalog ${targetServer.serverId}`, `${targetServer.serverId} has ${targetServer.toolCount} available tools.`, "if_tool_count_gt_0"));
156
217
  }
157
- output.info(` ${label}:`);
158
- const lines = JSON.stringify(schema, null, 2).split("\n");
159
- for (const line of lines) {
160
- output.info(` ${line}`);
218
+ if (context.nextCursor !== null) {
219
+ const limitFlag = context.limit === null ? "" : ` --limit ${context.limit}`;
220
+ actions.push(nextAction("next_catalog_page", "Load next page", `ogment catalog --cursor ${context.nextCursor}${limitFlag}`, `More servers are available after cursor ${context.nextCursor}.`, "if_more_servers"));
161
221
  }
222
+ return actions;
162
223
  };
163
- const renderServersListHuman = (output, servers) => {
164
- if (servers.length === 0) {
165
- output.info("No servers available.");
166
- return;
167
- }
168
- for (const server of servers) {
169
- const description = typeof server.description === "string" && server.description.length > 0
170
- ? ` - ${server.description}`
171
- : "";
172
- output.info(`${server.path} (${server.orgSlug}) ${server.name}${description}`);
173
- }
174
- output.info("Inspect tools with: ogment servers <path>");
224
+ const nextActionsForCatalogTools = (payload) => {
225
+ const firstTool = payload.tools[0];
226
+ if (firstTool === undefined) {
227
+ return [];
228
+ }
229
+ return [
230
+ nextAction("inspect_tool", "Inspect tool details", `ogment catalog ${payload.server.serverId} ${firstTool.name}`, `Inspect schema for ${payload.server.serverId}/${firstTool.name} before invoking.`, "if_tools_available"),
231
+ ];
175
232
  };
176
- const renderServerDetailsHuman = (output, payload) => {
177
- output.info(`Server ${payload.server.name} (${payload.server.path}, ${payload.server.orgSlug})`);
178
- if (payload.tools.length === 0) {
179
- output.info("No tools available.");
180
- return;
181
- }
182
- for (const tool of payload.tools) {
183
- const description = typeof tool.description === "string" && tool.description.length > 0
184
- ? ` - ${tool.description}`
185
- : "";
186
- output.info(` ${tool.name}${description}`);
187
- renderSchemaParameters(output, tool.inputSchema);
188
- renderSchemaBlock(output, "Input schema", tool.inputSchema);
189
- renderSchemaBlock(output, "Output schema", tool.outputSchema);
190
- }
233
+ const toInlineJsonArgument = (value) => {
234
+ const serialized = JSON.stringify(value);
235
+ const escaped = serialized.replaceAll("'", String.raw `'\''`);
236
+ return `'${escaped}'`;
191
237
  };
192
- const formatLatency = (latencyMs) => {
193
- if (latencyMs === null) {
194
- return "n/a";
195
- }
196
- return `${latencyMs}ms`;
238
+ const nextActionsForCatalogToolDetails = (payload, exampleInput) => {
239
+ const inputArgument = toInlineJsonArgument(exampleInput);
240
+ return [
241
+ nextAction("invoke_tool", "Invoke this tool", `ogment invoke ${payload.server.serverId}/${payload.name} --input ${inputArgument}`, `Invoke ${payload.server.serverId}/${payload.name} with schema-shaped input.`, "after_tool_inspection"),
242
+ ];
197
243
  };
198
- const renderInfoHuman = (output, payload) => {
199
- output.info(`Diagnostics status: ${payload.summary.status.toUpperCase()}`);
200
- output.info(`Generated at: ${payload.generatedAt}`);
201
- output.info("");
202
- output.info("Runtime:");
203
- output.info(` CLI version: ${payload.runtime.cliVersion}`);
204
- output.info(` Node version: ${payload.runtime.nodeVersion}`);
205
- output.info(` Platform: ${payload.runtime.platform} (${payload.runtime.processArch})`);
206
- output.info(` Environment: ${payload.runtime.executionEnvironment}`);
207
- output.info("");
208
- output.info("Config:");
209
- output.info(` Base URL: ${payload.config.baseUrl} (${payload.config.baseUrlSource})`);
210
- output.info(` Config dir: ${payload.config.configDir}`);
211
- output.info(` Credentials path: ${payload.config.credentialsPath}`);
212
- output.info("");
213
- output.info("Auth:");
214
- output.info(` API key source: ${payload.auth.apiKeySource}`);
215
- output.info(` API key present: ${payload.auth.apiKeyPresent ? "yes" : "no"}`);
216
- output.info(` API key fingerprint: ${payload.auth.apiKeyPreview ?? "none"}`);
217
- output.info(` Credentials file exists: ${payload.auth.credentialsFileExists ? "yes" : "no"}`);
218
- if (payload.auth.credentialsFileLoadError !== null) {
219
- output.info(` Credentials load error: ${payload.auth.credentialsFileLoadError}`);
220
- }
221
- output.info("");
222
- output.info("Remote:");
223
- output.info(` Ping endpoint: ${payload.remote.ping.endpoint}`);
224
- output.info(` Ping reachable: ${payload.remote.ping.reachable ? "yes" : "no"}`);
225
- output.info(` Ping latency: ${formatLatency(payload.remote.ping.latencyMs)}`);
226
- if (payload.remote.ping.status !== null) {
227
- output.info(` Ping status: ${payload.remote.ping.status} ${payload.remote.ping.statusText ?? ""}`.trimEnd());
228
- }
229
- if (payload.remote.ping.error !== null) {
230
- output.info(` Ping error: ${payload.remote.ping.error}`);
231
- }
232
- output.info(` Account check: ${payload.remote.account.status}`);
233
- output.info(` Account latency: ${formatLatency(payload.remote.account.latencyMs)}`);
234
- if (payload.remote.account.serverCount !== null) {
235
- output.info(` Server count: ${payload.remote.account.serverCount}`);
236
- }
237
- if (payload.remote.account.orgCount !== null) {
238
- output.info(` Organization count: ${payload.remote.account.orgCount}`);
239
- }
240
- if (payload.remote.account.serverPaths.length > 0) {
241
- output.info(` Server paths: ${payload.remote.account.serverPaths.join(", ")}`);
242
- }
243
- if (payload.remote.account.message !== null) {
244
- output.info(` Account message: ${payload.remote.account.message}`);
245
- }
246
- output.info("");
247
- output.info("Documentation:");
248
- output.info(` Config precedence: ${payload.documentation.configPrecedence.join(" -> ")}`);
249
- output.info(" Quick commands:");
250
- for (const command of payload.documentation.quickCommands) {
251
- output.info(` ${command}`);
252
- }
253
- if (payload.summary.issues.length > 0) {
254
- output.info("");
255
- output.info("Issues:");
256
- for (const issue of payload.summary.issues) {
257
- output.info(` - ${issue}`);
258
- }
259
- }
260
- if (payload.summary.nextActions.length > 0) {
261
- output.info("");
262
- output.info("Next actions:");
263
- for (const action of payload.summary.nextActions) {
264
- output.info(` - ${action}`);
265
- }
266
- }
244
+ const nextActionsForInvoke = (payload) => {
245
+ return [
246
+ nextAction("inspect_tool", "Inspect tool schema", `ogment catalog ${payload.serverId} ${payload.toolName}`, `Review ${payload.serverId}/${payload.toolName} schema for the next invocation.`, "after_invoke"),
247
+ ];
248
+ };
249
+ const nextActionsForStatus = (payload) => {
250
+ if (!payload.auth.apiKeyPresent) {
251
+ return [
252
+ nextAction("login", "Authenticate", "ogment auth login", "Status detected no API key; authenticate first.", "immediate"),
253
+ ];
254
+ }
255
+ return [
256
+ nextAction("discover_servers", "Discover servers", "ogment catalog", `Connectivity is ${payload.summary.status}; discover available servers.`, "after_status"),
257
+ ];
267
258
  };
268
259
  const createProgram = (runtime) => {
269
260
  const program = new Command();
@@ -275,8 +266,8 @@ const createProgram = (runtime) => {
275
266
  .name(APP_NAME)
276
267
  .description(APP_DESCRIPTION)
277
268
  .version(VERSION)
278
- .option("--apiKey <key>", "API key override")
279
- .option("--json", "Output machine-readable JSON")
269
+ .option("--api-key <key>", "API key override")
270
+ .option("--human", "Render output for humans")
280
271
  .option("--quiet", "Suppress non-essential output")
281
272
  .option("--non-interactive", "Disable interactive behavior")
282
273
  .option("--yes", "Assume yes for any confirmation")
@@ -285,121 +276,393 @@ const createProgram = (runtime) => {
285
276
  runtime.output.configure(mapGlobalOutputOptions(options));
286
277
  runtime.context.apiKeyOverride = options.apiKey;
287
278
  });
288
- program
279
+ const authCommand = program.command("auth").description("Authentication workflows");
280
+ const runLoginFlow = async (mode, apiKey) => {
281
+ const loginInvocation = (() => {
282
+ if (mode === "api_key") {
283
+ return {
284
+ commandOptions: {
285
+ apiKey: apiKey ?? "",
286
+ mode: "apiKey",
287
+ },
288
+ invokedCommand: "ogment auth login --api-key <redacted>",
289
+ };
290
+ }
291
+ if (mode === "browser") {
292
+ return {
293
+ commandOptions: {
294
+ mode: "browser",
295
+ },
296
+ invokedCommand: "ogment auth login --browser",
297
+ };
298
+ }
299
+ const deviceCommand = "ogment auth login";
300
+ return {
301
+ commandOptions: {
302
+ mode: "device",
303
+ onPending: ({ userCode, verificationUri }) => {
304
+ const pendingNextActions = [
305
+ nextAction("check_login_status", "Check login status", "ogment auth status", `Approve ${userCode} at ${verificationUri}, then verify local auth state.`, "immediate"),
306
+ nextAction("restart_device_login", "Restart login", "ogment auth login", `Restart login if code ${userCode} expires before approval.`, "if_expired"),
307
+ ];
308
+ runtime.output.success({
309
+ state: "pending",
310
+ userCode,
311
+ verificationUri,
312
+ }, {
313
+ command: deviceCommand,
314
+ entity: {
315
+ mode,
316
+ state: "pending",
317
+ userCode,
318
+ verificationUri,
319
+ },
320
+ humanMessage: `Open ${verificationUri} and enter code ${userCode}`,
321
+ nextActions: pendingNextActions,
322
+ });
323
+ },
324
+ },
325
+ invokedCommand: deviceCommand,
326
+ };
327
+ })();
328
+ const { commandOptions, invokedCommand } = loginInvocation;
329
+ const result = await runAuthLoginCommand(runtime.context, commandOptions);
330
+ const data = ensureSuccess(result, runtime.output, {
331
+ command: invokedCommand,
332
+ entity: {
333
+ mode,
334
+ },
335
+ });
336
+ let humanMessage = `Logged in as ${data.agentName}.`;
337
+ if (data.alreadyLoggedIn) {
338
+ humanMessage = `Already logged in as ${data.agentName}.`;
339
+ }
340
+ else if (mode === "api_key") {
341
+ humanMessage = `Imported API key for ${data.agentName}.`;
342
+ }
343
+ runtime.output.success(data, {
344
+ command: invokedCommand,
345
+ entity: {
346
+ agentName: data.agentName,
347
+ alreadyLoggedIn: data.alreadyLoggedIn,
348
+ mode,
349
+ },
350
+ humanMessage,
351
+ nextActions: nextActionsForLogin(data, mode),
352
+ });
353
+ };
354
+ const loginWorkflowCommand = authCommand
289
355
  .command("login")
290
- .description("Authenticate with Ogment")
291
- .option("--device", "Use device flow explicitly")
292
- .action(async (options) => {
293
- const result = await runtime.context.services.auth.login({
294
- device: options.device === true,
295
- nonInteractive: runtime.output.nonInteractive,
296
- onPending: ({ userCode, verificationUri }) => {
297
- runtime.output.info(`Open ${verificationUri}`);
298
- runtime.output.info(`Enter code: ${userCode}`);
356
+ .description("Authenticate with Ogment using device flow (recommended)")
357
+ .option("--browser", "Fallback: use browser callback flow");
358
+ loginWorkflowCommand.action(async (options, command) => {
359
+ const { apiKey } = asGlobalOptions(command);
360
+ if (options.browser === true && apiKey !== undefined) {
361
+ throw new ValidationError({
362
+ details: "--browser with --api-key",
363
+ message: "Use only one login mode: default device, --browser, or --api-key.",
364
+ suggestedCommand: "ogment auth login",
365
+ });
366
+ }
367
+ if (apiKey !== undefined) {
368
+ await runLoginFlow("api_key", apiKey);
369
+ return;
370
+ }
371
+ if (options.browser === true) {
372
+ await runLoginFlow("browser");
373
+ return;
374
+ }
375
+ await runLoginFlow("device");
376
+ });
377
+ authCommand
378
+ .command("status")
379
+ .description("Show local authentication status")
380
+ .action(async () => {
381
+ const command = "ogment auth status";
382
+ const result = await runAuthStatusCommand(runtime.context);
383
+ const data = ensureSuccess(result, runtime.output, {
384
+ command,
385
+ });
386
+ runtime.output.success(data, {
387
+ command,
388
+ entity: {
389
+ apiKeySource: data.apiKeySource,
390
+ loggedIn: data.loggedIn,
299
391
  },
392
+ humanMessage: data.loggedIn
393
+ ? `Authenticated (${data.apiKeySource}).`
394
+ : "Not authenticated.",
395
+ nextActions: nextActionsForAuthStatus(data),
300
396
  });
301
- const data = ensureSuccess(result, runtime.output);
302
- const humanMessage = data.alreadyLoggedIn
303
- ? `Already logged in as ${data.agentName}.`
304
- : `Logged in as ${data.agentName}. ${AGENT_SUCCESS_HINT}`;
305
- runtime.output.success(data, humanMessage);
306
397
  });
307
- program
398
+ authCommand
308
399
  .command("logout")
309
400
  .description("Revoke token and delete local credentials")
310
401
  .action(async () => {
311
- const result = await runtime.context.services.auth.logout();
312
- const data = ensureSuccess(result, runtime.output);
313
- let humanMessage = "Not logged in.";
402
+ const command = "ogment auth logout";
403
+ const result = await runAuthLogoutCommand(runtime.context);
404
+ const data = ensureSuccess(result, runtime.output, {
405
+ command,
406
+ });
407
+ let message = "Not logged in.";
314
408
  if (data.localCredentialsDeleted) {
315
- humanMessage = data.revoked
409
+ message = data.revoked
316
410
  ? "Logged out and revoked API key."
317
- : "Local credentials deleted. Server revocation not confirmed.";
411
+ : "Logged out locally. Server revocation not confirmed.";
318
412
  }
319
- runtime.output.success(data, humanMessage);
413
+ runtime.output.success(data, {
414
+ command,
415
+ entity: {
416
+ localCredentialsDeleted: data.localCredentialsDeleted,
417
+ revoked: data.revoked,
418
+ },
419
+ humanMessage: message,
420
+ nextActions: [
421
+ nextAction("login", "Authenticate again", "ogment auth login", "Logout completed; authenticate again before further tool calls.", "after_logout"),
422
+ ],
423
+ });
320
424
  });
321
- program
322
- .command("servers [path]")
323
- .description("List servers or inspect tools for one server")
324
- .action(async (path) => {
325
- const result = await runServersCommand(runtime.context, { path });
326
- const data = ensureSuccess(result, runtime.output);
327
- if (runtime.output.mode === "human") {
328
- if ("servers" in data) {
329
- renderServersListHuman(runtime.output, data.servers);
330
- return;
425
+ const catalogCommand = program
426
+ .command("catalog [serverId] [toolName]")
427
+ .description("Discover servers and tools with progressive disclosure")
428
+ .option("--cursor <cursor>", "Pagination cursor (catalog summary only)")
429
+ .option("--limit <limit>", "Maximum servers to return (catalog summary only)")
430
+ .option("--example", "Include a generated example input payload (tool details only)");
431
+ catalogCommand.action(async (serverId, toolName, options) => {
432
+ const parsedLimit = typeof options.limit === "string" ? Number.parseInt(options.limit, 10) : undefined;
433
+ if (options.limit !== undefined && (parsedLimit === undefined || Number.isNaN(parsedLimit))) {
434
+ throw new ValidationError({
435
+ details: options.limit,
436
+ message: "Invalid --limit value. Expected an integer.",
437
+ suggestedCommand: "ogment catalog --limit 20",
438
+ });
439
+ }
440
+ if (parsedLimit !== undefined && parsedLimit < 1) {
441
+ throw new ValidationError({
442
+ details: options.limit ?? String(parsedLimit),
443
+ message: "Invalid --limit value. Expected a positive integer.",
444
+ suggestedCommand: "ogment catalog --limit 20",
445
+ });
446
+ }
447
+ if (serverId === undefined) {
448
+ if (options.example === true) {
449
+ throw new ValidationError({
450
+ details: "--example without <server-id> <tool-name>",
451
+ message: "Invalid catalog options. --example requires <server-id> <tool-name>.",
452
+ suggestedCommand: "ogment catalog",
453
+ });
331
454
  }
332
- renderServerDetailsHuman(runtime.output, data);
455
+ const command = [
456
+ "ogment catalog",
457
+ ...(options.cursor === undefined ? [] : [`--cursor ${options.cursor}`]),
458
+ ...(parsedLimit === undefined || Number.isNaN(parsedLimit)
459
+ ? []
460
+ : [`--limit ${parsedLimit}`]),
461
+ ].join(" ");
462
+ const result = await runCatalogCommand(runtime.context, {
463
+ cursor: options.cursor,
464
+ limit: parsedLimit,
465
+ });
466
+ const data = ensureSuccess(result, runtime.output, {
467
+ command,
468
+ entity: {
469
+ cursor: options.cursor ?? null,
470
+ limit: parsedLimit ?? null,
471
+ },
472
+ });
473
+ const outputData = {
474
+ servers: data.servers,
475
+ };
476
+ runtime.output.success(outputData, {
477
+ command,
478
+ entity: {
479
+ cursor: options.cursor ?? null,
480
+ limit: parsedLimit ?? null,
481
+ serverCount: data.servers.length,
482
+ },
483
+ humanMessage: data.nextCursor === null
484
+ ? `${data.servers.length} server(s) available.`
485
+ : `${data.servers.length} server(s) shown. More available with --cursor ${data.nextCursor}.`,
486
+ nextActions: nextActionsForCatalogSummary(outputData, {
487
+ limit: parsedLimit ?? null,
488
+ nextCursor: data.nextCursor,
489
+ }),
490
+ pagination: {
491
+ nextCursor: data.nextCursor,
492
+ },
493
+ });
333
494
  return;
334
495
  }
335
- runtime.output.success(data);
336
- });
337
- program
338
- .command("call <serverPath> <toolName> [argsJson]")
339
- .description("Call a tool on an Ogment server")
340
- .action(async (serverPath, toolName, argsJson) => {
341
- const result = await runCallCommand(runtime.context, {
342
- argsJson,
343
- serverPath,
496
+ if (toolName === undefined) {
497
+ if (options.cursor !== undefined || parsedLimit !== undefined) {
498
+ throw new ValidationError({
499
+ details: `${options.cursor === undefined ? "" : `--cursor ${options.cursor} `}${parsedLimit === undefined ? "" : `--limit ${parsedLimit}`}`.trim(),
500
+ message: "Invalid catalog options. --cursor and --limit are only valid for `ogment catalog`.",
501
+ suggestedCommand: "ogment catalog",
502
+ });
503
+ }
504
+ if (options.example === true) {
505
+ throw new ValidationError({
506
+ details: `--example without tool name for ${serverId}`,
507
+ message: "Invalid catalog options. --example requires <tool-name>.",
508
+ suggestedCommand: `ogment catalog ${serverId}`,
509
+ });
510
+ }
511
+ const command = `ogment catalog ${serverId}`;
512
+ const result = await runCatalogToolsCommand(runtime.context, { serverId });
513
+ const data = ensureSuccess(result, runtime.output, {
514
+ command,
515
+ entity: {
516
+ serverId,
517
+ },
518
+ });
519
+ runtime.output.success(data, {
520
+ command,
521
+ entity: {
522
+ serverId: data.server.serverId,
523
+ toolCount: data.tools.length,
524
+ },
525
+ humanMessage: `${data.tools.length} tool(s) available on ${data.server.serverId}.`,
526
+ nextActions: nextActionsForCatalogTools(data),
527
+ });
528
+ return;
529
+ }
530
+ if (options.cursor !== undefined || parsedLimit !== undefined) {
531
+ throw new ValidationError({
532
+ details: `${options.cursor === undefined ? "" : `--cursor ${options.cursor} `}${parsedLimit === undefined ? "" : `--limit ${parsedLimit}`}`.trim(),
533
+ message: "Invalid catalog options. --cursor and --limit are only valid for `ogment catalog`.",
534
+ suggestedCommand: `ogment catalog ${serverId} ${toolName}${options.example === true ? " --example" : ""}`,
535
+ });
536
+ }
537
+ const command = [
538
+ "ogment catalog",
539
+ serverId,
344
540
  toolName,
541
+ ...(options.example === true ? ["--example"] : []),
542
+ ].join(" ");
543
+ const result = await runCatalogToolDetailsCommand(runtime.context, { serverId, toolName });
544
+ const data = ensureSuccess(result, runtime.output, {
545
+ command,
546
+ entity: {
547
+ serverId,
548
+ toolName,
549
+ },
345
550
  });
346
- const data = ensureSuccess(result, runtime.output);
347
- if (runtime.output.mode === "human") {
348
- runtime.output.json(data.result);
349
- return;
551
+ const exampleInput = buildJsonSchemaExample(data.inputSchema);
552
+ const outputData = options.example === true ? { ...data, exampleInput } : data;
553
+ runtime.output.success(outputData, {
554
+ command,
555
+ entity: {
556
+ serverId: data.server.serverId,
557
+ toolName: data.name,
558
+ },
559
+ humanMessage: `Loaded schema for ${data.server.serverId}/${data.name}.`,
560
+ nextActions: nextActionsForCatalogToolDetails(data, exampleInput),
561
+ });
562
+ });
563
+ program
564
+ .command("invoke <target>")
565
+ .description("Invoke a tool using <server-id>/<tool-name>")
566
+ .option("--input <value>", "Input payload: inline JSON object, @path, or - for stdin")
567
+ .action(async (target, options) => {
568
+ let inputSource = "none";
569
+ let command = `ogment invoke ${target}`;
570
+ if (options.input !== undefined) {
571
+ if (options.input === "-") {
572
+ inputSource = "stdin";
573
+ command = `ogment invoke ${target} --input -`;
574
+ }
575
+ else if (options.input.startsWith("@")) {
576
+ inputSource = "file";
577
+ command = `ogment invoke ${target} --input ${options.input}`;
578
+ }
579
+ else {
580
+ inputSource = "inline_json";
581
+ command = `ogment invoke ${target} --input <json>`;
582
+ }
350
583
  }
351
- runtime.output.success(data);
584
+ const result = await runInvokeCommand(runtime.context, {
585
+ input: options.input,
586
+ target,
587
+ });
588
+ const data = ensureSuccess(result, runtime.output, {
589
+ command,
590
+ entity: {
591
+ inputSource,
592
+ target,
593
+ },
594
+ });
595
+ runtime.output.success(data, {
596
+ command,
597
+ entity: {
598
+ inputSource,
599
+ serverId: data.serverId,
600
+ toolName: data.toolName,
601
+ },
602
+ humanMessage: `Invoked ${data.serverId}/${data.toolName}.`,
603
+ nextActions: nextActionsForInvoke(data),
604
+ });
352
605
  });
353
606
  program
354
- .command("info")
607
+ .command("status")
355
608
  .description("Show runtime configuration and connectivity diagnostics")
356
609
  .action(async () => {
610
+ const command = "ogment status";
357
611
  if (runtime.infoService === undefined) {
358
612
  throw new UnexpectedError({
359
613
  message: "Info service is not configured",
360
614
  });
361
615
  }
362
- const result = await runInfoCommand({
616
+ const result = await runStatusCommand({
363
617
  apiKeyOverride: runtime.context.apiKeyOverride,
364
618
  }, {
365
619
  infoService: runtime.infoService,
366
620
  });
367
- const data = ensureSuccess(result, runtime.output);
368
- if (runtime.output.mode === "human") {
369
- renderInfoHuman(runtime.output, data);
370
- return;
371
- }
372
- runtime.output.success(data);
373
- });
374
- program
375
- .command("describe")
376
- .description("Describe this CLI as a tool set for agent registration")
377
- .action(() => {
378
- const result = runDescribeCommand();
379
- const data = ensureSuccess(result, runtime.output);
380
- runtime.output.json(data);
621
+ const data = ensureSuccess(result, runtime.output, {
622
+ command,
623
+ });
624
+ runtime.output.success(data, {
625
+ command,
626
+ entity: {
627
+ summaryStatus: data.summary.status,
628
+ },
629
+ humanMessage: `Diagnostics status: ${data.summary.status.toUpperCase()}`,
630
+ nextActions: nextActionsForStatus(data),
631
+ });
381
632
  });
382
633
  program.action(() => {
383
- if (runtime.output.mode === "json") {
384
- runtime.output.success({
385
- commands: ["login", "logout", "servers", "call", "info", "describe"],
386
- });
387
- return;
388
- }
389
- runtime.output.info(`${APP_NAME} v${VERSION}`);
390
- runtime.output.info(APP_DESCRIPTION);
391
- runtime.output.info("");
392
- runtime.output.info("Commands:");
393
- runtime.output.info(" login [--device]");
394
- runtime.output.info(" servers [path]");
395
- runtime.output.info(" call <serverPath> <toolName> [argsJson]");
396
- runtime.output.info(" info");
397
- runtime.output.info(" logout");
398
- runtime.output.info(" describe");
634
+ runtime.output.success({
635
+ commands: [
636
+ "auth login",
637
+ "auth status",
638
+ "auth logout",
639
+ "catalog",
640
+ "catalog <server-id>",
641
+ "catalog <server-id> <tool-name>",
642
+ "invoke <server-id>/<tool-name>",
643
+ "status",
644
+ ],
645
+ }, {
646
+ command: "ogment",
647
+ entity: null,
648
+ humanMessage: "Select a command to continue.",
649
+ nextActions: [
650
+ nextAction("login", "Authenticate", "ogment auth login", "Authenticate first so catalog and invoke commands can run.", "immediate"),
651
+ nextAction("catalog", "Discover servers", "ogment catalog", "List accessible servers and tool counts.", "after_auth"),
652
+ ],
653
+ });
399
654
  });
400
655
  return program;
401
656
  };
402
657
  const normalizeCommanderError = (error) => {
658
+ if (error instanceof AuthError ||
659
+ error instanceof ContractMismatchError ||
660
+ error instanceof NotFoundError ||
661
+ error instanceof RemoteRequestError ||
662
+ error instanceof UnexpectedError ||
663
+ error instanceof ValidationError) {
664
+ return error;
665
+ }
403
666
  if (error instanceof CommanderError) {
404
667
  return new ValidationError({
405
668
  details: error.message,
@@ -423,6 +686,13 @@ const normalizeCliArgv = (argv) => {
423
686
  }
424
687
  return argv;
425
688
  };
689
+ const commandFromArgv = (argv) => {
690
+ const normalized = normalizeCliArgv(argv);
691
+ if (normalized.length === 0) {
692
+ return "ogment";
693
+ }
694
+ return `ogment ${normalized.join(" ")}`;
695
+ };
426
696
  export const runCli = async (argv = process.argv.slice(2), runtime = createRuntime()) => {
427
697
  const program = createProgram(runtime);
428
698
  try {
@@ -437,7 +707,11 @@ export const runCli = async (argv = process.argv.slice(2), runtime = createRunti
437
707
  return EXIT_CODE.success;
438
708
  }
439
709
  const normalized = normalizeCommanderError(error);
440
- runtime.output.error(normalized);
710
+ runtime.output.error(normalized, {
711
+ command: commandFromArgv(argv),
712
+ entity: null,
713
+ nextActions: nextActionsForError(normalized),
714
+ });
441
715
  return exitCodeForError(normalized);
442
716
  }
443
717
  };