Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions packages/codework/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,99 @@
# @codeworksh/cli

Command-line interface for the CodeWork coding agent.

## Installation

```bash
pnpm add -g @codeworksh/cli
# or run directly
pnpm dlx @codeworksh/cli <command>
```

---

## Commands

### `codework run`

Run an agent session with a prompt.

```bash
# Start a new session
codework run "Inspect and fix the failing tests"

# Create a session with explicit provider and model
codework run --provider openai --model gpt-5.5 --thinking high "Plan database migration"

# Continue an existing session
codework run --session ses_01a070f0 "Now implement the migration script"

# Run in an isolated sandbox (local, daytona, vercel, memory, sqldb)
codework run --sandbox daytona "Inspect repository"
codework run --sandbox daytona --sandbox-provider-id <remote-id> "Continue in sandbox"
```

#### Flags:

- `-s, --session <id>` — Continue an existing session ID.
- `-C, --cwd <path>` — Working directory for a new session.
- `--sandbox <driver>` — Sandbox driver (`local`, `daytona`, `vercel`, `memory`, `sqldb`). Default: `local`.
- `--sandbox-provider-id <id>` — Provider ID of an existing remote sandbox.
- `--provider <id>` — Model catalog provider ID.
- `--model <id>` — Model ID.
- `--thinking <level>` — Thinking/reasoning level (`off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`).

Global flags: `--user-config-dir`, `--home`, `--database`.

---

### `codework models`

List catalog models as `provider/model`, one per line.

```bash
# List all models across all providers
codework models

# List models for a specific provider
codework models --provider openai

# List all supported provider IDs
codework models providers
```

---

### `codework models generate`

Generate or update the `models.gen.json` catalog from model registries.

```bash
# Generate to CODEWORK_MODELS_FILE or ./models.gen.json
codework models generate

# Generate into a specific directory (resolves to ./models.gen.json)
codework models generate .

# Generate to an explicit file path
codework models generate ./path/to/custom-models.json
```

---

## Environment Variables

| Variable | Description |
| :----------------------------- | :--------------------------------------------------- |
| `CODEWORK_MODELS_FILE` | Path to the generated `models.gen.json` catalog file |
| `OPENAI_API_KEY` | API key for OpenAI models |
| `ANTHROPIC_API_KEY` | API key for Anthropic models |
| `OPENROUTER_API_KEY` | API key for OpenRouter models |
| `GOOGLE_GENERATIVE_AI_API_KEY` | API key for Google models |
| `XAI_API_KEY` | API key for xAI models |

---

## License

MIT
51 changes: 44 additions & 7 deletions packages/codework/src/cli/cmd/cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,52 @@ export const Cmd = Spec.make("codework", {
},
],
}),
Spec.make("modelgen", {
description: "Generate the model catalog",
Spec.make("models", {
description: "Model catalog management and inspection",
params: {
path: Argument.string("path").pipe(
Argument.withDescription("Output path; defaults to CODEWORK_MODELS_FILE or ./models.gen.json"),
Argument.optional,
),
provider: Flag.string("provider").pipe(Flag.withDescription("Model catalog provider ID"), Flag.optional),
},
examples: [{ command: "codework modelgen", description: "Generate models.gen.json" }],
examples: [
{ command: "codework models", description: "List all models from all providers" },
{
command: "codework models --provider openai",
description: "List models for a specific provider",
},
{ command: "codework models providers", description: "List all available provider IDs" },
{ command: "codework models generate", description: "Generate models.gen.json" },
{ command: "codework models generate .", description: "Generate models.gen.json in current directory" },
{
command: "codework models generate /path/to/models.gen.json",
description: "Generate catalog at explicit path",
},
],
commands: [
Spec.make("providers", {
description: "List available model providers in the catalog",
}),
Spec.make("generate", {
description: "Generate or update the model catalog",
params: {
path: Argument.string("path").pipe(
Argument.withDescription(
"Output file or directory; defaults to CODEWORK_MODELS_FILE or ./models.gen.json",
),
Argument.optional,
),
},
examples: [
{ command: "codework models generate", description: "Generate models.gen.json" },
{
command: "codework models generate .",
description: "Generate models.gen.json in current directory",
},
{
command: "codework models generate /path/to/models.gen.json",
description: "Generate catalog at explicit path",
},
],
}),
],
}),
],
});
17 changes: 0 additions & 17 deletions packages/codework/src/cli/cmd/handlers/modelgen.ts

This file was deleted.

49 changes: 49 additions & 0 deletions packages/codework/src/cli/cmd/handlers/models/catalog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Model } from "@codeworksh/aikit";
import { Runner } from "@codeworksh/harness/effect";
import { Effect } from "effect";

const catalogError = (cause: unknown): Runner.ModelCatalogError => {
if (
typeof cause === "object" &&
cause !== null &&
"name" in cause &&
cause.name === "ModelCatalogLoadError" &&
"data" in cause &&
typeof cause.data === "object" &&
cause.data !== null
) {
const data = cause.data;
if (
"path" in data &&
typeof data.path === "string" &&
"message" in data &&
typeof data.message === "string" &&
"reason" in data &&
(data.reason === "missing" ||
data.reason === "unreadable" ||
data.reason === "empty" ||
data.reason === "invalid")
) {
return new Runner.ModelCatalogError({
path: data.path,
reason: data.reason,
detail: data.message,
});
}
}
return new Runner.ModelCatalogError({
path: "",
reason: "unreadable",
detail: cause instanceof Error ? cause.message : "failed to load the model catalog",
});
};

export const loadCatalog = Effect.tryPromise({
try: () => Model.getBuiltInModels(),
catch: catalogError,
});

export const loadProviders = Effect.tryPromise({
try: () => Model.getProviders(),
catch: catalogError,
});
43 changes: 43 additions & 0 deletions packages/codework/src/cli/cmd/handlers/models/generate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { generateModels } from "@codeworksh/aikit/modelgen";
import { Effect, FileSystem, Option, Path } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { ModelgenError, reportFailure } from "../../../error.ts";
import { writeError, writeOut } from "../../../output.ts";
import { Cmd } from "../../cmd.ts";

const DEFAULT_FILENAME = "models.gen.json";

const resolveTarget = Effect.fn("CLI.models.resolveTarget")(function* (inputPath: Option.Option<string>) {
if (Option.isNone(inputPath)) return Option.none<string>();

const path = yield* Path.Path;
const fs = yield* FileSystem.FileSystem;
const raw = inputPath.value.trim();
const asDirectory = raw === "." || raw === ".." || raw.endsWith("/") || raw.endsWith("\\");
const resolved = path.resolve(raw);
if (asDirectory) return Option.some(path.join(resolved, DEFAULT_FILENAME));

const directory = yield* fs.stat(resolved).pipe(
Effect.map((stat) => stat.type === "Directory"),
Effect.orElseSucceed(() => false),
);
return Option.some(directory ? path.join(resolved, DEFAULT_FILENAME) : resolved);
});

export default Runtime.handler(
Cmd.commands.models.commands.generate,
Effect.fn("CLI.models.generate")(function* ({ path }) {
const program = Effect.gen(function* () {
const targetPath = yield* resolveTarget(path);
const generated = yield* Effect.tryPromise({
try: () =>
generateModels(Option.match(targetPath, { onNone: () => ({}), onSome: (value) => ({ path: value }) })),
catch: (cause) => new ModelgenError({ cause }),
});
yield* writeError(`Generated model catalog at ${generated}\n`);
yield* writeOut(`${generated}\n`);
});

return yield* program.pipe(Effect.catch(reportFailure));
}),
);
38 changes: 38 additions & 0 deletions packages/codework/src/cli/cmd/handlers/models/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Effect, Option } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { InvalidInputError, reportFailure } from "../../../error.ts";
import { writeOut } from "../../../output.ts";
import { Cmd } from "../../cmd.ts";
import { loadCatalog } from "./catalog.ts";

export default Runtime.handler(
Cmd.commands.models,
Effect.fn("CLI.models.list")(function* ({ provider }) {
const program = Effect.gen(function* () {
const catalog = yield* loadCatalog;
if (Option.isSome(provider)) {
const providerId = provider.value;
if (!Object.hasOwn(catalog, providerId)) {
return yield* new InvalidInputError({
message: `provider "${providerId}" is not in the catalog\nhint: run \`codework models providers\``,
});
}
const sorted = Object.keys(catalog[providerId] ?? {}).sort((a, b) => a.localeCompare(b));
yield* writeOut(sorted.map((modelId) => `${providerId}/${modelId}\n`).join(""));
return;
}

const lines: string[] = [];
const providers = Object.keys(catalog).sort((a, b) => a.localeCompare(b));
for (const providerId of providers) {
const providerModels = Object.keys(catalog[providerId] ?? {}).sort((a, b) => a.localeCompare(b));
for (const modelId of providerModels) {
lines.push(`${providerId}/${modelId}\n`);
}
}
yield* writeOut(lines.join(""));
});

return yield* program.pipe(Effect.catch(reportFailure));
}),
);
19 changes: 19 additions & 0 deletions packages/codework/src/cli/cmd/handlers/models/providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Effect } from "effect";
import { Runtime } from "../../../../framework/runtime.ts";
import { reportFailure } from "../../../error.ts";
import { writeOut } from "../../../output.ts";
import { Cmd } from "../../cmd.ts";
import { loadProviders } from "./catalog.ts";

export default Runtime.handler(
Cmd.commands.models.commands.providers,
Effect.fn("CLI.models.providers")(function* () {
const program = Effect.gen(function* () {
const providers = yield* loadProviders;
const sorted = [...providers].sort((a, b) => a.localeCompare(b));
yield* writeOut(sorted.map((providerId) => `${providerId}\n`).join(""));
});

return yield* program.pipe(Effect.catch(reportFailure));
}),
);
26 changes: 24 additions & 2 deletions packages/codework/src/cli/error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Runner } from "@codeworksh/harness/effect";
import { SandboxProvider } from "@codeworksh/harness/sandbox";
import { Duration, Schema } from "effect";
import { Duration, Effect, Schema } from "effect";
import { writeError } from "./output.ts";

/** Bad flag or argument combinations the parser cannot express on its own. */
export class InvalidInputError extends Schema.TaggedError<InvalidInputError>()("CLI.InvalidInputError", {
Expand All @@ -18,6 +19,8 @@ export class ModelgenError extends Schema.TaggedError<ModelgenError>()("CLI.Mode
*/
export type CommandError = InvalidInputError | ModelgenError;

const isInvalidInputError = Schema.is(InvalidInputError);
const isModelgenError = Schema.is(ModelgenError);
const isProviderError = Schema.is(Runner.ProviderError);
const isModelCatalogError = Schema.is(Runner.ModelCatalogError);
const isModelNotFoundError = Schema.is(Runner.ModelNotFoundError);
Expand Down Expand Up @@ -106,6 +109,16 @@ const unknownMessage = (error: unknown): string => {

/** Render typed SDK errors for humans without exposing Effect causes or provider payloads. */
export const renderError = (error: unknown): string => {
if (isInvalidInputError(error)) {
return `error: ${error.message}\n`;
}
if (isModelgenError(error)) {
return (
["error[model_catalog]: failed to generate the model catalog", "hint: check the output path and retry"].join(
"\n",
) + "\n"
);
}
if (isSandboxProviderError(error)) {
return (
[
Expand Down Expand Up @@ -133,7 +146,7 @@ export const renderError = (error: unknown): string => {
return (
[
`error[model_catalog]: ${error.message}`,
"hint: run `codework modelgen` or set CODEWORK_MODELS_FILE to a generated catalog",
"hint: run `codework models generate` or set CODEWORK_MODELS_FILE to a generated catalog",
].join("\n") + "\n"
);
}
Expand All @@ -150,3 +163,12 @@ export const renderError = (error: unknown): string => {
}
return `error: ${unknownMessage(error)}\n`;
};

export const reportFailure = (error: unknown) =>
writeError(renderError(error)).pipe(
Effect.andThen(
Effect.sync(() => {
process.exitCode = 1;
}),
),
);
Loading