@opencode/codemode 0.0.0-dev-20782 → 0.0.0-dev-20784

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.
@@ -1,4 +1,4 @@
1
- import { HttpClient } from "effect/unstable/http";
1
+ import { HttpClient } from "effect/http";
2
2
  import { make } from "../tool.js";
3
3
  import { invoke } from "./runtime.js";
4
4
  import { componentDefinitions, hasDirectionalSchemas, inputSchema, isRecord, methods, nonEmptyString, operationInput, operationOutput, operationPath, operationSecurityRequirements, securityRequirements, securitySchemes, specServerUrl, validateBaseUrl, } from "./spec.js";
@@ -1,4 +1,4 @@
1
1
  import { Effect } from "effect";
2
- import { HttpClient } from "effect/unstable/http";
2
+ import { HttpClient } from "effect/http";
3
3
  import type { Plan } from "./types.js";
4
4
  export declare const invoke: (plan: Plan, input: unknown) => Effect.Effect<unknown, unknown, HttpClient.HttpClient>;
@@ -1,5 +1,5 @@
1
1
  import { Effect, Option, Schema, Stream } from "effect";
2
- import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
2
+ import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
3
3
  import { ToolError, toolError } from "../tool-error.js";
4
4
  import { isRecord, own } from "./spec.js";
5
5
  const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Unknown));
@@ -1,5 +1,5 @@
1
1
  import { Effect } from "effect";
2
- import { HttpClient } from "effect/unstable/http";
2
+ import { HttpClient } from "effect/http";
3
3
  import type { Tool, JsonSchema } from "../tool.js";
4
4
  /** A parsed OpenAPI 3.x document. YAML must be parsed by the ctx. */
5
5
  export type Document = Record<string, unknown>;
@@ -1,2 +1,2 @@
1
1
  import { Effect } from "effect";
2
- import { HttpClient } from "effect/unstable/http";
2
+ import { HttpClient } from "effect/http";
@@ -3,10 +3,23 @@ const isEffectSchema = (schema) => Schema.isSchema(schema);
3
3
  const renderLiteral = (value) => JSON.stringify(value) ?? "unknown";
4
4
  export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
5
5
  const renderKey = (name) => (identifierSegment.test(name) ? name : JSON.stringify(name));
6
+ const definitionName = (ref) => {
7
+ const tokens = JsonPointer.parseUriFragment(ref);
8
+ return tokens?.length === 2 && (tokens[0] === "$defs" || tokens[0] === "definitions") ? tokens[1] : undefined;
9
+ };
10
+ // Effect encodes an unchecked `number` as `number | "Infinity" | "-Infinity" | "NaN"`, and older generators emit one
11
+ // singleton enum per value. Rendering them as plain `number` only narrows what the model is told to send.
6
12
  const effectNumberSentinel = (schema) => schema.type === "string" &&
7
13
  Array.isArray(schema.enum) &&
8
- schema.enum.length === 1 &&
9
- (schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity");
14
+ schema.enum.length > 0 &&
15
+ schema.enum.every((value) => value === "NaN" || value === "Infinity" || value === "-Infinity");
16
+ // Effect emits `{ not: { type: "null" } }` only for a struct with no properties or index signatures. With nothing but
17
+ // annotations beside it, it is TypeScript's `{}`. Mirrors `emptyInputJsonSchema` in `@opencode/ai`, which this
18
+ // standalone package cannot import.
19
+ const annotationKeywords = new Set(["title", "description", "default", "examples", "readOnly", "writeOnly"]);
20
+ const isEffectEmptyStruct = (schema) => schema.not?.type === "null" &&
21
+ Object.keys(schema.not).length === 1 &&
22
+ Object.keys(schema).every((key) => key === "not" || annotationKeywords.has(key));
10
23
  const intersection = (members) => {
11
24
  const concrete = members.filter((member) => member !== "unknown");
12
25
  if (concrete.length === 0)
@@ -21,8 +34,7 @@ const hasUnresolvedRef = (schema, definitions, seen = new Set(), visited = new S
21
34
  return false;
22
35
  const nextVisited = new Set([...visited, schema]);
23
36
  if (schema.$ref !== undefined) {
24
- const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1];
25
- const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment);
37
+ const name = definitionName(schema.$ref);
26
38
  if (name === undefined || definitions[name] === undefined || seen.has(name))
27
39
  return true;
28
40
  if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited))
@@ -124,8 +136,7 @@ const renderSchema = (schema, ctx, depth = 0, seen = new Set()) => {
124
136
  ? ctx
125
137
  : { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } };
126
138
  if (schema.$ref) {
127
- const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1];
128
- const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment);
139
+ const name = definitionName(schema.$ref);
129
140
  if (!name || !nested.definitions[name] || seen.has(name))
130
141
  return "unknown";
131
142
  return intersection([
@@ -142,6 +153,7 @@ const renderSchema = (schema, ctx, depth = 0, seen = new Set()) => {
142
153
  if (alternatives.some((item) => item.type === "number") &&
143
154
  alternatives.every((item) => item.type === "number" || effectNumberSentinel(item)))
144
155
  return "number";
156
+ // Older Effect releases emitted this pair for an empty struct, and raw schemas from their generators still can.
145
157
  if (alternatives.length === 2 &&
146
158
  alternatives[0]?.type === "object" &&
147
159
  alternatives[0].properties === undefined &&
@@ -196,13 +208,16 @@ const renderSchema = (schema, ctx, depth = 0, seen = new Set()) => {
196
208
  lines.push(`${pad}[key: string]: ${indexType},`);
197
209
  return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`;
198
210
  }
211
+ if (isEffectEmptyStruct(schema))
212
+ return "{}";
199
213
  return "unknown";
200
214
  };
201
215
  export const toTypeScript = (schema, decoded = false, pretty = false) => {
202
216
  try {
203
- const visible = decoded ? Schema.toType(schema) : schema;
204
- const document = Schema.toJsonSchemaDocument(visible);
205
- return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty });
217
+ const document = Schema.toJsonSchemaDocument(decoded ? Schema.toType(schema) : schema, {
218
+ onExcessProperty: "error",
219
+ });
220
+ return renderSchema(document.schema, { definitions: document.definitions, pretty });
206
221
  }
207
222
  catch {
208
223
  return "unknown";
@@ -219,17 +234,15 @@ export const jsonSchemaToTypeScript = (schema, pretty = false) => {
219
234
  export const inputProperties = (tool) => {
220
235
  try {
221
236
  const document = isEffectSchema(tool.input)
222
- ? Schema.toJsonSchemaDocument(tool.input)
237
+ ? Schema.toJsonSchemaDocument(tool.input, { onExcessProperty: "error" })
223
238
  : {
224
239
  schema: tool.input,
225
240
  definitions: { ...(tool.input.definitions ?? {}), ...(tool.input.$defs ?? {}) },
226
241
  };
227
- const definitions = document.definitions ?? {};
228
242
  let schema = document.schema;
229
243
  if (schema.$ref !== undefined) {
230
- const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1];
231
- const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment);
232
- const resolved = name === undefined ? undefined : definitions[name];
244
+ const name = definitionName(schema.$ref);
245
+ const resolved = name === undefined ? undefined : document.definitions[name];
233
246
  if (resolved === undefined)
234
247
  return [];
235
248
  schema = resolved;
@@ -246,10 +259,11 @@ export const inputProperties = (tool) => {
246
259
  }
247
260
  };
248
261
  export const inputTypeScript = (tool, pretty = false) => isEffectSchema(tool.input) ? toTypeScript(tool.input, false, pretty) : jsonSchemaToTypeScript(tool.input, pretty);
249
- // Empty object schemas render as `{}` in compact form; anything with properties,
250
- // an index signature, or union members renders differently, so equality is a
251
- // conservative emptiness test for both Effect and JSON Schema inputs.
252
- export const isEmptyInput = (tool) => inputTypeScript(tool) === "{}";
262
+ // Effect inputs are empty only when their encoded root is Effect's empty-struct marker, which records and checked
263
+ // structs never are. Raw JSON Schema inputs keep the `{}` rendering check, which treats `{ type: "object" }` as empty.
264
+ export const isEmptyInput = (tool) => isEffectSchema(tool.input)
265
+ ? isEffectEmptyStruct(Schema.toJsonSchemaDocument(tool.input, { referencePolicy: () => undefined }).schema)
266
+ : inputTypeScript(tool) === "{}";
253
267
  export const outputTypeScript = (tool, pretty = false) => tool.output === undefined
254
268
  ? "void"
255
269
  : isEffectSchema(tool.output)
package/dist/tool.d.ts CHANGED
@@ -12,6 +12,7 @@ export type JsonSchema = {
12
12
  readonly anyOf?: ReadonlyArray<JsonSchema>;
13
13
  readonly oneOf?: ReadonlyArray<JsonSchema>;
14
14
  readonly allOf?: ReadonlyArray<JsonSchema>;
15
+ readonly not?: JsonSchema;
15
16
  readonly properties?: Readonly<Record<string, JsonSchema>>;
16
17
  readonly required?: ReadonlyArray<string>;
17
18
  readonly items?: JsonSchema;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode/codemode",
4
- "version": "0.0.0-dev-20782",
4
+ "version": "0.0.0-dev-20784",
5
5
  "description": "Effect-native confined code execution over schema-described tools",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "acorn": "8.15.0",
32
- "effect": "4.0.0-rc.112"
32
+ "effect": "4.0.0-rc.118"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@tsconfig/bun": "1.0.9",