@opengeni/ogtool 1.2.1-canary.37222965366001 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,7 +44,7 @@ Commands:
44
44
  - `ogtool list [--json] [--query <substring>] [--limit <1..100>] [--offset <integer>]`
45
45
  - `ogtool list --full` — the previous full catalog JSON, including identity and schemas
46
46
  - `ogtool show <tool-path-or-model-name>` — one tool's details and schemas as JSON
47
- - `ogtool call <tool-path-or-model-name> [json-object]`
47
+ - `ogtool call <tool-path-or-model-name> [json-object] [--full]` — the tool result as JSON. A text block that only repeats `structuredContent` as JSON is omitted, so the payload prints once; prose, differing text, annotated text, and non-text blocks stay. `--full` prints the exact result.
48
48
  - `ogtool declarations [output-file]`
49
49
  - `ogtool doctor`
50
50
  - `ogtool --version`
@@ -40,6 +40,7 @@ var __export = (target, all) => {
40
40
  // src/cli.ts
41
41
  var exports_cli = {};
42
42
  __export(exports_cli, {
43
+ callOutput: () => callOutput,
43
44
  listProjection: () => listProjection,
44
45
  parseToolArguments: () => parseToolArguments,
45
46
  resolveTool: () => resolveTool,
@@ -14619,6 +14620,7 @@ function requireCompleteSiteToolContext(value, context) {
14619
14620
  }
14620
14621
  var CodemodeOperationState = exports_external.enum([
14621
14622
  "queued",
14623
+ "waiting_for_approval",
14622
14624
  "running",
14623
14625
  "completed",
14624
14626
  "failed",
@@ -14642,6 +14644,8 @@ var CodemodeOperation = exports_external.object({
14642
14644
  message: "Codemode operation caller must be codemode"
14643
14645
  }),
14644
14646
  state: CodemodeOperationState,
14647
+ durableApproval: exports_external.literal(true).optional(),
14648
+ approvalRequestId: exports_external.string().uuid().optional(),
14645
14649
  result: AttemptToolResult.nullable(),
14646
14650
  errorCode: exports_external.string().min(1).max(128).nullable(),
14647
14651
  errorMessage: exports_external.string().min(1).max(4096).nullable(),
@@ -14652,6 +14656,7 @@ var CodemodeOperation = exports_external.object({
14652
14656
  updatedAt: exports_external.string().datetime({ offset: true })
14653
14657
  }).strict();
14654
14658
  var CodemodeCallRequest = exports_external.object({
14659
+ durableApproval: exports_external.literal(true).optional(),
14655
14660
  operationId: exports_external.string().uuid(),
14656
14661
  catalogDigest: sha256,
14657
14662
  identity: AttemptToolIdentity,
@@ -14672,6 +14677,65 @@ var CodemodeDispatchAck = exports_external.object({
14672
14677
  status: exports_external.enum(["accepted", "already_running", "terminal", "unavailable", "rejected"])
14673
14678
  }).strict();
14674
14679
 
14680
+ // ../contracts/src/mcp-structured-content.ts
14681
+ function omitStructuredContentTextDuplicates(result) {
14682
+ const structured = result.structuredContent;
14683
+ if (structured === undefined || structured === null)
14684
+ return result;
14685
+ const content = result.content.filter((entry) => !isPlainTextBlock(entry) || !textDuplicatesStructuredContent(entry.text, structured));
14686
+ if (content.length === result.content.length)
14687
+ return result;
14688
+ return { ...result, content };
14689
+ }
14690
+ function isPlainTextBlock(entry) {
14691
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
14692
+ return false;
14693
+ const record2 = entry;
14694
+ if (record2.type !== "text" || typeof record2.text !== "string")
14695
+ return false;
14696
+ return Object.keys(record2).every((key) => key === "type" || key === "text");
14697
+ }
14698
+ function textDuplicatesStructuredContent(text, structured) {
14699
+ const trimmed = text.replace(/^[ \t\n\r]+/u, "");
14700
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("["))
14701
+ return false;
14702
+ let parsed;
14703
+ try {
14704
+ parsed = JSON.parse(text);
14705
+ } catch {
14706
+ return false;
14707
+ }
14708
+ return !containsUnsafeInteger(parsed) && jsonValuesEqual(parsed, structured);
14709
+ }
14710
+ function containsUnsafeInteger(value) {
14711
+ if (typeof value === "number")
14712
+ return Number.isInteger(value) && !Number.isSafeInteger(value);
14713
+ if (Array.isArray(value))
14714
+ return value.some(containsUnsafeInteger);
14715
+ if (value && typeof value === "object")
14716
+ return Object.values(value).some(containsUnsafeInteger);
14717
+ return false;
14718
+ }
14719
+ function jsonValuesEqual(left, right) {
14720
+ if (left === right)
14721
+ return true;
14722
+ if (typeof left !== "object" || typeof right !== "object" || left === null || right === null) {
14723
+ return false;
14724
+ }
14725
+ if (Array.isArray(left) || Array.isArray(right)) {
14726
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
14727
+ return false;
14728
+ }
14729
+ return left.every((entry, index) => jsonValuesEqual(entry, right[index]));
14730
+ }
14731
+ const leftRecord = left;
14732
+ const rightRecord = right;
14733
+ const leftKeys = Object.keys(leftRecord);
14734
+ if (leftKeys.length !== Object.keys(rightRecord).length)
14735
+ return false;
14736
+ return leftKeys.every((key) => Object.hasOwn(rightRecord, key) && jsonValuesEqual(leftRecord[key], rightRecord[key]));
14737
+ }
14738
+
14675
14739
  // ../tool-gateway/src/catalog.ts
14676
14740
  var import_node_crypto = require("node:crypto");
14677
14741
 
@@ -15089,6 +15153,20 @@ class CodemodeOperationError extends Error {
15089
15153
  this.operation = operation;
15090
15154
  this.code = code;
15091
15155
  this.name = "CodemodeOperationError";
15156
+ Object.defineProperty(this, "operation", { value: operation, enumerable: false });
15157
+ }
15158
+ }
15159
+
15160
+ class CodemodeApprovalPendingError extends Error {
15161
+ operationId;
15162
+ approvalRequestId;
15163
+ code = "codemode_approval_pending";
15164
+ state = "waiting_for_approval";
15165
+ constructor(operationId, approvalRequestId) {
15166
+ super(`Operation ${operationId} is waiting for approval. After review, read or resume this handle; do not submit its arguments again.`);
15167
+ this.operationId = operationId;
15168
+ this.approvalRequestId = approvalRequestId;
15169
+ this.name = "CodemodeApprovalPendingError";
15092
15170
  }
15093
15171
  }
15094
15172
 
@@ -15212,6 +15290,9 @@ class CodemodeClient {
15212
15290
  } else {
15213
15291
  operation = await this.read(operationId, options.signal);
15214
15292
  }
15293
+ if (operation.state === "waiting_for_approval") {
15294
+ throw new CodemodeApprovalPendingError(operation.operationId, operation.approvalRequestId);
15295
+ }
15215
15296
  if (operation.state === "completed") {
15216
15297
  return { result: AttemptToolResult.parse(operation.result), entry };
15217
15298
  }
@@ -15258,11 +15339,39 @@ class CodemodeClient {
15258
15339
  operationId,
15259
15340
  catalogDigest,
15260
15341
  identity,
15261
- arguments: argumentsValue
15342
+ arguments: argumentsValue,
15343
+ durableApproval: true
15262
15344
  })
15263
15345
  });
15264
15346
  return CodemodeCallSubmission.parse(await response.json()).operation;
15265
15347
  }
15348
+ async status(operationId, signal) {
15349
+ const operation = await this.read(operationId, signal);
15350
+ return {
15351
+ operationId: operation.operationId,
15352
+ state: operation.state,
15353
+ approvalRequestId: operation.approvalRequestId,
15354
+ result: operation.result,
15355
+ errorCode: operation.errorCode,
15356
+ errorMessage: operation.errorMessage
15357
+ };
15358
+ }
15359
+ async resume(operationId, options = {}) {
15360
+ const deadline = Date.now() + boundedPositiveInteger(options.timeoutMs ?? this.timeoutMs, 1000, 60 * 60000);
15361
+ while (true) {
15362
+ throwIfAborted(options.signal);
15363
+ const operation = await this.read(operationId, options.signal);
15364
+ if (operation.state === "waiting_for_approval")
15365
+ throw new CodemodeApprovalPendingError(operation.operationId, operation.approvalRequestId);
15366
+ if (operation.state === "completed")
15367
+ return AttemptToolResult.parse(operation.result);
15368
+ if (["failed", "outcome_unknown", "cancelled"].includes(operation.state))
15369
+ throw new CodemodeOperationError(operation, operation.errorCode ?? `codemode_${operation.state}`);
15370
+ if (Date.now() >= deadline)
15371
+ throw new CodemodeTransportError(`Codemode operation ${operationId} did not settle before the client deadline`);
15372
+ await abortableDelay(this.pollIntervalMs, options.signal);
15373
+ }
15374
+ }
15266
15375
  async read(operationId, signal) {
15267
15376
  const response = await this.request(`/calls/${operationId}`, {
15268
15377
  method: "GET",
@@ -15280,7 +15389,8 @@ class CodemodeClient {
15280
15389
  ...init,
15281
15390
  headers: {
15282
15391
  ...Object.fromEntries(new Headers(init.headers).entries()),
15283
- authorization: `Bearer ${token}`
15392
+ authorization: `Bearer ${token}`,
15393
+ "x-opengeni-codemode-capabilities": "durable-approval-v1"
15284
15394
  }
15285
15395
  });
15286
15396
  if (!response.ok && throwOnError) {
@@ -15419,7 +15529,7 @@ async function abortableDelay(delayMs, signal) {
15419
15529
  // package.json
15420
15530
  var package_default = {
15421
15531
  name: "@opengeni/ogtool",
15422
- version: "1.2.1-canary.37222965366001",
15532
+ version: "1.4.0",
15423
15533
  description: "Codemode CLI and typed client entrypoint for an OpenGeni execution attempt.",
15424
15534
  license: "Apache-2.0",
15425
15535
  repository: {
@@ -15456,7 +15566,8 @@ var package_default = {
15456
15566
  prepublishOnly: "bash ../../scripts/prepublish-guard"
15457
15567
  },
15458
15568
  dependencies: {
15459
- "@opengeni/codemode": "workspace:*"
15569
+ "@opengeni/codemode": "workspace:*",
15570
+ "@opengeni/contracts": "workspace:*"
15460
15571
  },
15461
15572
  devDependencies: {
15462
15573
  tsup: "^8.5.0",
@@ -15548,12 +15659,15 @@ function usage(exitCode = 1) {
15548
15659
  " ogtool list [--full | --json]",
15549
15660
  " [--query <substring>] [--limit <1..100>] [--offset <nonnegative integer>]",
15550
15661
  " ogtool show <tool-path-or-model-name>",
15551
- " ogtool call <tool-path-or-model-name> [json-object]",
15662
+ " ogtool call <tool-path-or-model-name> [json-object] [--full]",
15663
+ " ogtool read <operation-id>",
15664
+ " ogtool resume <operation-id>",
15552
15665
  " ogtool declarations [output-file]",
15553
15666
  " ogtool doctor",
15554
15667
  " ogtool --version",
15555
15668
  "",
15556
15669
  "list returns all authorized tools by default; --limit/--offset are opt-in slices",
15670
+ "call omits a text block that only repeats structuredContent; --full prints the exact result",
15557
15671
  "",
15558
15672
  "requires OPENGENI_CODEMODE_URL and OPENGENI_CODEMODE_TOKEN or OPENGENI_CODEMODE_TOKEN_FILE"
15559
15673
  ].join(`
@@ -15674,6 +15788,11 @@ function showOutput(catalog, name) {
15674
15788
  return `${output}
15675
15789
  `;
15676
15790
  }
15791
+ function callOutput(result, full = false) {
15792
+ const printed = full ? result : omitStructuredContentTextDuplicates(result);
15793
+ return `${JSON.stringify(printed, null, 2)}
15794
+ `;
15795
+ }
15677
15796
  function parseToolArguments(raw) {
15678
15797
  if (raw === undefined)
15679
15798
  return {};
@@ -15709,6 +15828,14 @@ async function main() {
15709
15828
  throw new Error("usage: ogtool show <tool-path-or-model-name>");
15710
15829
  }
15711
15830
  const client = configuredClient();
15831
+ if (command === "read" || command === "resume") {
15832
+ if (args.length !== 1 || !/^[0-9a-f-]{36}$/iu.test(args[0]))
15833
+ throw new Error("A valid operation ID is required");
15834
+ const result = command === "read" ? await client.status(args[0]) : await client.resume(args[0]);
15835
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
15836
+ `);
15837
+ return;
15838
+ }
15712
15839
  if (command === "list") {
15713
15840
  const catalog = await client.catalog();
15714
15841
  if (listOptions.full) {
@@ -15724,17 +15851,17 @@ async function main() {
15724
15851
  return;
15725
15852
  }
15726
15853
  if (command === "call") {
15727
- const name = process.argv[3];
15854
+ const full = args.includes("--full");
15855
+ const [name, rawArguments] = args.filter((arg) => arg !== "--full");
15728
15856
  if (!name)
15729
15857
  throw new Error("tool path or model name is required");
15730
- const argumentsValue = parseToolArguments(process.argv[4]);
15858
+ const argumentsValue = parseToolArguments(rawArguments);
15731
15859
  const catalog = await client.catalog();
15732
15860
  const entry = resolveTool(catalog, name);
15733
15861
  const result = await client.call(entry.identity, argumentsValue, {
15734
15862
  operationId: import_node_crypto3.randomUUID()
15735
15863
  });
15736
- process.stdout.write(`${JSON.stringify(result, null, 2)}
15737
- `);
15864
+ process.stdout.write(callOutput(result, full));
15738
15865
  return;
15739
15866
  }
15740
15867
  if (command === "declarations") {
@@ -15753,6 +15880,18 @@ async function main() {
15753
15880
  return usage();
15754
15881
  }
15755
15882
  main().catch((error51) => {
15883
+ if (error51 instanceof CodemodeApprovalPendingError) {
15884
+ process.stdout.write(`${JSON.stringify({ operationId: error51.operationId, state: error51.state, code: error51.code, approvalRequestId: error51.approvalRequestId, message: error51.message })}
15885
+ `);
15886
+ process.exitCode = 2;
15887
+ return;
15888
+ }
15889
+ if (error51 instanceof CodemodeOperationError) {
15890
+ process.stdout.write(`${JSON.stringify({ operationId: error51.operation.operationId, state: error51.operation.state, code: error51.code, message: error51.message })}
15891
+ `);
15892
+ process.exitCode = error51.operation.state === "outcome_unknown" ? 3 : 1;
15893
+ return;
15894
+ }
15756
15895
  const code = error51 && typeof error51 === "object" && "code" in error51 ? String(error51.code) : null;
15757
15896
  process.stderr.write(`${error51 instanceof Error ? error51.message : String(error51)}
15758
15897
  `);
package/dist/cli.d.ts CHANGED
@@ -1,5 +1,12 @@
1
1
  import { type AttemptToolCatalog, type AttemptToolCatalogEntry } from "@opengeni/codemode";
2
+ import { type AttemptToolResult } from "@opengeni/contracts";
2
3
  export declare function resolveTool(catalog: AttemptToolCatalog, name: string): AttemptToolCatalogEntry;
3
4
  export declare function listProjection(catalog: AttemptToolCatalog): Record<string, unknown>;
4
5
  export declare function showOutput(catalog: AttemptToolCatalog, name: string): string;
6
+ /**
7
+ * `call` prints the payload once by default: a text block that only repeats
8
+ * `structuredContent` as JSON is omitted. `--full` prints the exact result.
9
+ * The native client (`codemode call`) applies the same rule.
10
+ */
11
+ export declare function callOutput(result: AttemptToolResult, full?: boolean): string;
5
12
  export declare function parseToolArguments(raw: string | undefined): Record<string, unknown>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/ogtool",
3
- "version": "1.2.1-canary.37222965366001",
3
+ "version": "1.4.0",
4
4
  "description": "Codemode CLI and typed client entrypoint for an OpenGeni execution attempt.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -37,7 +37,8 @@
37
37
  "prepublishOnly": "bash ../../scripts/prepublish-guard"
38
38
  },
39
39
  "dependencies": {
40
- "@opengeni/codemode": "1.2.1-canary.37222965366001"
40
+ "@opengeni/codemode": "1.4.0",
41
+ "@opengeni/contracts": "1.4.0"
41
42
  },
42
43
  "engines": {
43
44
  "node": ">=18"
package/src/cli.ts CHANGED
@@ -2,10 +2,13 @@ import { randomUUID } from "node:crypto";
2
2
  import { readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import {
4
4
  CodemodeClient,
5
+ CodemodeApprovalPendingError,
6
+ CodemodeOperationError,
5
7
  generateCodemodeDeclarations,
6
8
  type AttemptToolCatalog,
7
9
  type AttemptToolCatalogEntry,
8
10
  } from "@opengeni/codemode";
11
+ import { omitStructuredContentTextDuplicates, type AttemptToolResult } from "@opengeni/contracts";
9
12
  import packageManifest from "../package.json" with { type: "json" };
10
13
  import { compactOutput, parseListOptions } from "./catalog-discovery";
11
14
 
@@ -18,12 +21,15 @@ function usage(exitCode = 1): void {
18
21
  " ogtool list [--full | --json]",
19
22
  " [--query <substring>] [--limit <1..100>] [--offset <nonnegative integer>]",
20
23
  " ogtool show <tool-path-or-model-name>",
21
- " ogtool call <tool-path-or-model-name> [json-object]",
24
+ " ogtool call <tool-path-or-model-name> [json-object] [--full]",
25
+ " ogtool read <operation-id>",
26
+ " ogtool resume <operation-id>",
22
27
  " ogtool declarations [output-file]",
23
28
  " ogtool doctor",
24
29
  " ogtool --version",
25
30
  "",
26
31
  "list returns all authorized tools by default; --limit/--offset are opt-in slices",
32
+ "call omits a text block that only repeats structuredContent; --full prints the exact result",
27
33
  "",
28
34
  "requires OPENGENI_CODEMODE_URL and OPENGENI_CODEMODE_TOKEN or OPENGENI_CODEMODE_TOKEN_FILE",
29
35
  ].join("\n");
@@ -160,6 +166,16 @@ export function showOutput(catalog: AttemptToolCatalog, name: string): string {
160
166
  return `${output}\n`;
161
167
  }
162
168
 
169
+ /**
170
+ * `call` prints the payload once by default: a text block that only repeats
171
+ * `structuredContent` as JSON is omitted. `--full` prints the exact result.
172
+ * The native client (`codemode call`) applies the same rule.
173
+ */
174
+ export function callOutput(result: AttemptToolResult, full = false): string {
175
+ const printed = full ? result : omitStructuredContentTextDuplicates(result);
176
+ return `${JSON.stringify(printed, null, 2)}\n`;
177
+ }
178
+
163
179
  export function parseToolArguments(raw: string | undefined): Record<string, unknown> {
164
180
  if (raw === undefined) return {};
165
181
  let value: unknown;
@@ -197,6 +213,14 @@ async function main(): Promise<void> {
197
213
  }
198
214
 
199
215
  const client = configuredClient();
216
+ if (command === "read" || command === "resume") {
217
+ if (args.length !== 1 || !/^[0-9a-f-]{36}$/iu.test(args[0]!))
218
+ throw new Error("A valid operation ID is required");
219
+ const result =
220
+ command === "read" ? await client.status(args[0]!) : await client.resume(args[0]!);
221
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
222
+ return;
223
+ }
200
224
  if (command === "list") {
201
225
  const catalog = await client.catalog();
202
226
  if (listOptions!.full) {
@@ -211,15 +235,16 @@ async function main(): Promise<void> {
211
235
  return;
212
236
  }
213
237
  if (command === "call") {
214
- const name = process.argv[3];
238
+ const full = args.includes("--full");
239
+ const [name, rawArguments] = args.filter((arg) => arg !== "--full");
215
240
  if (!name) throw new Error("tool path or model name is required");
216
- const argumentsValue = parseToolArguments(process.argv[4]);
241
+ const argumentsValue = parseToolArguments(rawArguments);
217
242
  const catalog = await client.catalog();
218
243
  const entry = resolveTool(catalog, name);
219
244
  const result = await client.call(entry.identity, argumentsValue, {
220
245
  operationId: randomUUID(),
221
246
  });
222
- process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
247
+ process.stdout.write(callOutput(result, full));
223
248
  return;
224
249
  }
225
250
  if (command === "declarations") {
@@ -240,6 +265,20 @@ async function main(): Promise<void> {
240
265
  }
241
266
 
242
267
  void main().catch((error: unknown) => {
268
+ if (error instanceof CodemodeApprovalPendingError) {
269
+ process.stdout.write(
270
+ `${JSON.stringify({ operationId: error.operationId, state: error.state, code: error.code, approvalRequestId: error.approvalRequestId, message: error.message })}\n`,
271
+ );
272
+ process.exitCode = 2;
273
+ return;
274
+ }
275
+ if (error instanceof CodemodeOperationError) {
276
+ process.stdout.write(
277
+ `${JSON.stringify({ operationId: error.operation.operationId, state: error.operation.state, code: error.code, message: error.message })}\n`,
278
+ );
279
+ process.exitCode = error.operation.state === "outcome_unknown" ? 3 : 1;
280
+ return;
281
+ }
243
282
  const code =
244
283
  error && typeof error === "object" && "code" in error
245
284
  ? String((error as { code: unknown }).code)