@agentplate/cli 1.0.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -0
  4. package/agents/architect.md +108 -0
  5. package/agents/builder.md +97 -0
  6. package/agents/coordinator.md +113 -0
  7. package/agents/deployer.md +117 -0
  8. package/agents/devops.md +114 -0
  9. package/agents/lead.md +107 -0
  10. package/agents/merger.md +103 -0
  11. package/agents/reviewer.md +90 -0
  12. package/agents/scout.md +95 -0
  13. package/agents/verifier.md +106 -0
  14. package/package.json +64 -0
  15. package/src/agents/guard-rules.ts +55 -0
  16. package/src/agents/identity.test.ts +161 -0
  17. package/src/agents/identity.ts +229 -0
  18. package/src/agents/manifest.test.ts +260 -0
  19. package/src/agents/manifest.ts +286 -0
  20. package/src/agents/overlay.test.ts +190 -0
  21. package/src/agents/overlay.ts +212 -0
  22. package/src/agents/system-prompt.test.ts +53 -0
  23. package/src/agents/system-prompt.ts +95 -0
  24. package/src/agents/turn-runner.ts +79 -0
  25. package/src/commands/coordinator.test.ts +75 -0
  26. package/src/commands/coordinator.ts +259 -0
  27. package/src/commands/deploy.test.ts +504 -0
  28. package/src/commands/deploy.ts +874 -0
  29. package/src/commands/doctor.test.ts +106 -0
  30. package/src/commands/doctor.ts +208 -0
  31. package/src/commands/init.ts +71 -0
  32. package/src/commands/log.ts +51 -0
  33. package/src/commands/mail.ts +197 -0
  34. package/src/commands/merge.ts +127 -0
  35. package/src/commands/model.ts +58 -0
  36. package/src/commands/prime.ts +61 -0
  37. package/src/commands/reap.ts +87 -0
  38. package/src/commands/serve.ts +61 -0
  39. package/src/commands/setup.ts +48 -0
  40. package/src/commands/ship.test.ts +106 -0
  41. package/src/commands/ship.ts +202 -0
  42. package/src/commands/skill.test.ts +458 -0
  43. package/src/commands/skill.ts +730 -0
  44. package/src/commands/sling.ts +365 -0
  45. package/src/commands/status.ts +60 -0
  46. package/src/commands/stop.ts +56 -0
  47. package/src/commands/tui.ts +199 -0
  48. package/src/commands/worktree.ts +77 -0
  49. package/src/config.test.ts +92 -0
  50. package/src/config.ts +202 -0
  51. package/src/db/sqlite.test.ts +77 -0
  52. package/src/db/sqlite.ts +102 -0
  53. package/src/deploy/audit.test.ts +233 -0
  54. package/src/deploy/audit.ts +245 -0
  55. package/src/deploy/context.test.ts +243 -0
  56. package/src/deploy/context.ts +72 -0
  57. package/src/deploy/registry.test.ts +101 -0
  58. package/src/deploy/registry.ts +86 -0
  59. package/src/deploy/secrets.test.ts +129 -0
  60. package/src/deploy/secrets.ts +69 -0
  61. package/src/deploy/targets/docker-gha.test.ts +323 -0
  62. package/src/deploy/targets/docker-gha.ts +841 -0
  63. package/src/deploy/types.ts +153 -0
  64. package/src/errors.test.ts +42 -0
  65. package/src/errors.ts +69 -0
  66. package/src/events/store.test.ts +183 -0
  67. package/src/events/store.ts +201 -0
  68. package/src/index.ts +137 -0
  69. package/src/insights/quality-gates.ts +73 -0
  70. package/src/json.test.ts +28 -0
  71. package/src/json.ts +50 -0
  72. package/src/logging/color.ts +62 -0
  73. package/src/logging/logger.ts +60 -0
  74. package/src/logging/sanitizer.test.ts +36 -0
  75. package/src/logging/sanitizer.ts +57 -0
  76. package/src/mail/client.test.ts +192 -0
  77. package/src/mail/client.ts +188 -0
  78. package/src/mail/store.test.ts +279 -0
  79. package/src/mail/store.ts +311 -0
  80. package/src/merge/lock.test.ts +88 -0
  81. package/src/merge/lock.ts +84 -0
  82. package/src/merge/queue.test.ts +136 -0
  83. package/src/merge/queue.ts +177 -0
  84. package/src/merge/resolver.test.ts +219 -0
  85. package/src/merge/resolver.ts +274 -0
  86. package/src/paths.ts +36 -0
  87. package/src/providers/apply.test.ts +90 -0
  88. package/src/providers/apply.ts +66 -0
  89. package/src/providers/registry.test.ts +74 -0
  90. package/src/providers/registry.ts +254 -0
  91. package/src/runtimes/claude.ts +313 -0
  92. package/src/runtimes/codex.ts +280 -0
  93. package/src/runtimes/cursor.ts +247 -0
  94. package/src/runtimes/gemini.ts +173 -0
  95. package/src/runtimes/mock.ts +71 -0
  96. package/src/runtimes/opencode.ts +259 -0
  97. package/src/runtimes/registry.test.ts +924 -0
  98. package/src/runtimes/registry.ts +63 -0
  99. package/src/runtimes/resolve.ts +45 -0
  100. package/src/runtimes/types.ts +97 -0
  101. package/src/scaffold.ts +68 -0
  102. package/src/secrets.test.ts +51 -0
  103. package/src/secrets.ts +78 -0
  104. package/src/serve/api.ts +667 -0
  105. package/src/serve/server.test.ts +433 -0
  106. package/src/serve/server.ts +271 -0
  107. package/src/serve/system.ts +90 -0
  108. package/src/serve/weather.ts +140 -0
  109. package/src/sessions/reaper.test.ts +162 -0
  110. package/src/sessions/reaper.ts +149 -0
  111. package/src/sessions/store.test.ts +351 -0
  112. package/src/sessions/store.ts +350 -0
  113. package/src/skills/distiller.test.ts +498 -0
  114. package/src/skills/distiller.ts +426 -0
  115. package/src/skills/feedback.test.ts +300 -0
  116. package/src/skills/feedback.ts +168 -0
  117. package/src/skills/lifecycle.ts +169 -0
  118. package/src/skills/retrieval.test.ts +421 -0
  119. package/src/skills/retrieval.ts +365 -0
  120. package/src/skills/safety.test.ts +335 -0
  121. package/src/skills/safety.ts +216 -0
  122. package/src/skills/store.test.ts +425 -0
  123. package/src/skills/store.ts +684 -0
  124. package/src/skills/types.ts +107 -0
  125. package/src/types.ts +442 -0
  126. package/src/utils/detect.test.ts +35 -0
  127. package/src/utils/detect.ts +82 -0
  128. package/src/version.test.ts +19 -0
  129. package/src/version.ts +7 -0
  130. package/src/wizard/setup.ts +254 -0
  131. package/src/worktree/manager.test.ts +181 -0
  132. package/src/worktree/manager.ts +229 -0
  133. package/templates/overlay.md.tmpl +102 -0
  134. package/ui/dist/assets/index-C7rXIMER.css +1 -0
  135. package/ui/dist/assets/index-W4kbr4by.js +4526 -0
  136. package/ui/dist/favicon.svg +21 -0
  137. package/ui/dist/index.html +16 -0
  138. package/ui/dist/logo-clay.svg +21 -0
  139. package/ui/dist/logo.svg +18 -0
package/src/paths.ts ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Canonical filesystem paths for Agentplate state, derived from a project root.
3
+ * Centralized so every command/store agrees on where things live.
4
+ */
5
+
6
+ import { join } from "node:path";
7
+ import { AGENTPLATE_DIR } from "./config.ts";
8
+
9
+ export const sessionsDbPath = (root: string): string => join(root, AGENTPLATE_DIR, "sessions.db");
10
+ export const eventsDbPath = (root: string): string => join(root, AGENTPLATE_DIR, "events.db");
11
+ export const mailDbPath = (root: string): string => join(root, AGENTPLATE_DIR, "mail.db");
12
+ export const mergeDbPath = (root: string): string => join(root, AGENTPLATE_DIR, "merge-queue.db");
13
+ export const deploysDbPath = (root: string): string => join(root, AGENTPLATE_DIR, "deploys.db");
14
+ export const manifestFilePath = (root: string): string =>
15
+ join(root, AGENTPLATE_DIR, "agent-manifest.json");
16
+ export const currentRunPath = (root: string): string =>
17
+ join(root, AGENTPLATE_DIR, "current-run.txt");
18
+ export const worktreesDir = (root: string): string => join(root, AGENTPLATE_DIR, "worktrees");
19
+ export const agentDefsDir = (root: string): string => join(root, AGENTPLATE_DIR, "agent-defs");
20
+ export const skillsDir = (root: string): string => join(root, AGENTPLATE_DIR, "skills");
21
+ export const agentStateDir = (root: string, agentName: string): string =>
22
+ join(root, AGENTPLATE_DIR, "agents", agentName);
23
+ export const appliedSkillsPath = (root: string, agentName: string): string =>
24
+ join(agentStateDir(root, agentName), "applied-skills.json");
25
+ export const specPath = (root: string, taskId: string): string =>
26
+ join(root, AGENTPLATE_DIR, "specs", `${taskId}.md`);
27
+
28
+ /**
29
+ * Path to a bundled base agent definition shipped with the package
30
+ * (`<package>/agents/<file>`). Resolved relative to this module (src/paths.ts).
31
+ */
32
+ export const packageAgentDefPath = (file: string): string =>
33
+ join(import.meta.dir, "..", "agents", file);
34
+
35
+ /** The package root (one level above src/). Used to locate bundled ui/dist. */
36
+ export const packageRootDir = (): string => join(import.meta.dir, "..");
@@ -0,0 +1,90 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { DEFAULT_CONFIG } from "../config.ts";
3
+ import { applyProviderSelection, buildProviderConfig } from "./apply.ts";
4
+ import { getProviderSpec } from "./registry.ts";
5
+
6
+ const anthropic = getProviderSpec("anthropic");
7
+ const openrouter = getProviderSpec("openrouter");
8
+ const custom = getProviderSpec("custom");
9
+
10
+ describe("buildProviderConfig", () => {
11
+ test("api-key auth stores the token env var", () => {
12
+ if (!anthropic) throw new Error("anthropic spec missing");
13
+ const cfg = buildProviderConfig(anthropic, "claude-sonnet-4-6", "api-key");
14
+ expect(cfg.type).toBe("native");
15
+ expect(cfg.authMode).toBe("api-key");
16
+ expect(cfg.authTokenEnv).toBe("ANTHROPIC_API_KEY");
17
+ expect(cfg.model).toBe("claude-sonnet-4-6");
18
+ expect(cfg.baseUrl).toBeUndefined();
19
+ });
20
+
21
+ test("subscription auth stores NO token env var (delegated to CLI login)", () => {
22
+ if (!anthropic) throw new Error("anthropic spec missing");
23
+ const cfg = buildProviderConfig(anthropic, "claude-opus-4-8", "subscription");
24
+ expect(cfg.authMode).toBe("subscription");
25
+ expect(cfg.authTokenEnv).toBeUndefined();
26
+ });
27
+
28
+ test("env auth keeps the token env var (read from environment at run time)", () => {
29
+ if (!anthropic) throw new Error("anthropic spec missing");
30
+ const cfg = buildProviderConfig(anthropic, "claude-opus-4-8", "env");
31
+ expect(cfg.authMode).toBe("env");
32
+ expect(cfg.authTokenEnv).toBe("ANTHROPIC_API_KEY");
33
+ });
34
+
35
+ test("gateway provider gets its default baseUrl", () => {
36
+ if (!openrouter) throw new Error("openrouter spec missing");
37
+ const cfg = buildProviderConfig(openrouter, "openai/gpt-4o", "api-key");
38
+ expect(cfg.type).toBe("gateway");
39
+ expect(cfg.baseUrl).toBe("https://openrouter.ai/api/v1");
40
+ });
41
+
42
+ test("custom baseUrl overrides the default", () => {
43
+ if (!custom) throw new Error("custom spec missing");
44
+ const cfg = buildProviderConfig(custom, "my-model", "api-key", "https://my.endpoint/v1");
45
+ expect(cfg.baseUrl).toBe("https://my.endpoint/v1");
46
+ });
47
+ });
48
+
49
+ describe("applyProviderSelection", () => {
50
+ test("registers the provider, marks it active, sets the runtime", () => {
51
+ if (!openrouter) throw new Error("openrouter spec missing");
52
+ const next = applyProviderSelection(DEFAULT_CONFIG, {
53
+ providerId: "openrouter",
54
+ spec: openrouter,
55
+ model: "anthropic/claude-sonnet-4-6",
56
+ authMode: "api-key",
57
+ runtime: "codex",
58
+ });
59
+ expect(next.activeProvider).toBe("openrouter");
60
+ expect(next.providers.openrouter?.model).toBe("anthropic/claude-sonnet-4-6");
61
+ expect(next.providers.openrouter?.authMode).toBe("api-key");
62
+ expect(next.runtime.default).toBe("codex");
63
+ });
64
+
65
+ test("subscription selection records authMode without a token env var", () => {
66
+ if (!anthropic) throw new Error("anthropic spec missing");
67
+ const next = applyProviderSelection(DEFAULT_CONFIG, {
68
+ providerId: "anthropic",
69
+ spec: anthropic,
70
+ model: "claude-opus-4-8",
71
+ authMode: "subscription",
72
+ runtime: "claude",
73
+ });
74
+ expect(next.providers.anthropic?.authMode).toBe("subscription");
75
+ expect(next.providers.anthropic?.authTokenEnv).toBeUndefined();
76
+ });
77
+
78
+ test("does not mutate the input config", () => {
79
+ if (!anthropic) throw new Error("anthropic spec missing");
80
+ const before = structuredClone(DEFAULT_CONFIG);
81
+ applyProviderSelection(DEFAULT_CONFIG, {
82
+ providerId: "anthropic",
83
+ spec: anthropic,
84
+ model: "claude-opus-4-8",
85
+ authMode: "api-key",
86
+ runtime: "claude",
87
+ });
88
+ expect(DEFAULT_CONFIG).toEqual(before);
89
+ });
90
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Pure helpers that turn a wizard selection into config — separated from the
3
+ * interactive I/O so they can be unit-tested without a TTY.
4
+ */
5
+
6
+ import type { AgentplateConfig, AuthMode, ProviderConfig } from "../types.ts";
7
+ import type { ProviderSpec } from "./registry.ts";
8
+
9
+ export interface ProviderSelection {
10
+ /** Provider id (catalog id, or a user-chosen id for custom endpoints). */
11
+ providerId: string;
12
+ /** The catalog spec the selection was based on. */
13
+ spec: ProviderSpec;
14
+ /** Chosen model id. */
15
+ model: string;
16
+ /** How credentials are obtained (subscription / api-key / env / none). */
17
+ authMode: AuthMode;
18
+ /** Base URL (gateway/custom providers). */
19
+ baseUrl?: string;
20
+ /** Coding-agent runtime to drive workers (e.g. "claude"). */
21
+ runtime: string;
22
+ }
23
+
24
+ /** Build a {@link ProviderConfig} from a catalog spec and the chosen values. */
25
+ export function buildProviderConfig(
26
+ spec: ProviderSpec,
27
+ model: string,
28
+ authMode: AuthMode,
29
+ baseUrl?: string,
30
+ ): ProviderConfig {
31
+ const config: ProviderConfig = {
32
+ type: spec.kind,
33
+ authMode,
34
+ model,
35
+ };
36
+ // Subscription/none auth delegates to the runtime login — no env var binding.
37
+ if (authMode === "api-key" || authMode === "env") {
38
+ config.authTokenEnv = spec.authEnvVar;
39
+ }
40
+ const resolvedBaseUrl = baseUrl ?? spec.defaultBaseUrl;
41
+ if (spec.kind === "gateway" && resolvedBaseUrl) {
42
+ config.baseUrl = resolvedBaseUrl;
43
+ }
44
+ return config;
45
+ }
46
+
47
+ /**
48
+ * Apply a provider selection to a config, returning a new config with the
49
+ * provider registered, marked active, and the runtime set. Does not mutate the
50
+ * input.
51
+ */
52
+ export function applyProviderSelection(
53
+ config: AgentplateConfig,
54
+ selection: ProviderSelection,
55
+ ): AgentplateConfig {
56
+ const next = structuredClone(config);
57
+ next.providers[selection.providerId] = buildProviderConfig(
58
+ selection.spec,
59
+ selection.model,
60
+ selection.authMode,
61
+ selection.baseUrl,
62
+ );
63
+ next.activeProvider = selection.providerId;
64
+ next.runtime.default = selection.runtime;
65
+ return next;
66
+ }
@@ -0,0 +1,74 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getProviderSpec,
4
+ listProviders,
5
+ MIN_CONTEXT_TOKENS,
6
+ meetsContextFloor,
7
+ PROVIDERS,
8
+ } from "./registry.ts";
9
+
10
+ describe("provider registry", () => {
11
+ test("getProviderSpec finds known providers", () => {
12
+ expect(getProviderSpec("anthropic")?.label).toBe("Anthropic");
13
+ expect(getProviderSpec("openrouter")?.kind).toBe("gateway");
14
+ });
15
+
16
+ test("getProviderSpec returns undefined for unknown", () => {
17
+ expect(getProviderSpec("nope")).toBeUndefined();
18
+ });
19
+
20
+ test("catalog includes the expected providers", () => {
21
+ const ids = listProviders().map((p) => p.id);
22
+ expect(ids).toContain("anthropic");
23
+ expect(ids).toContain("openai");
24
+ expect(ids).toContain("ollama");
25
+ expect(ids).toContain("custom");
26
+ expect(ids).toContain("opencode-zen");
27
+ });
28
+
29
+ test("CLI-login providers offer subscription via their runtime", () => {
30
+ // Each maps to a coding-agent CLI whose own login (OAuth/keys) is reused.
31
+ const cases: Array<[string, string]> = [
32
+ ["anthropic", "claude"],
33
+ ["openai", "codex"],
34
+ ["google", "gemini"],
35
+ ["opencode-zen", "opencode"],
36
+ ];
37
+ for (const [id, runtime] of cases) {
38
+ const spec = getProviderSpec(id);
39
+ expect(spec?.supportsSubscription).toBe(true);
40
+ expect(spec?.subscriptionRuntime).toBe(runtime);
41
+ }
42
+ });
43
+
44
+ test("every provider declares an auth env var", () => {
45
+ for (const spec of PROVIDERS) {
46
+ expect(spec.authEnvVar.length).toBeGreaterThan(0);
47
+ }
48
+ });
49
+
50
+ test("custom provider requires a base URL and has no preset models", () => {
51
+ const custom = getProviderSpec("custom");
52
+ expect(custom?.requiresBaseUrl).toBe(true);
53
+ expect(custom?.models.length).toBe(0);
54
+ });
55
+
56
+ test("ollama is keyless", () => {
57
+ expect(getProviderSpec("ollama")?.keyless).toBe(true);
58
+ });
59
+
60
+ test("meetsContextFloor enforces the minimum", () => {
61
+ expect(meetsContextFloor({ id: "a", label: "A", contextWindow: MIN_CONTEXT_TOKENS })).toBe(
62
+ true,
63
+ );
64
+ expect(meetsContextFloor({ id: "b", label: "B", contextWindow: 8000 })).toBe(false);
65
+ });
66
+
67
+ test("all preset models meet the context floor", () => {
68
+ for (const spec of PROVIDERS) {
69
+ for (const model of spec.models) {
70
+ expect(model.contextWindow).toBeGreaterThanOrEqual(MIN_CONTEXT_TOKENS);
71
+ }
72
+ }
73
+ });
74
+ });
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Provider catalog — the static knowledge Agentplate has about AI providers.
3
+ *
4
+ * This is the menu the setup wizard presents. It is intentionally code (not
5
+ * config): adding a provider here makes it instantly available in `agentplate
6
+ * setup`, mirroring how Hermes Agent's `select_provider_and_model()` works.
7
+ *
8
+ * A *provider* is an LLM backend with credentials. It is distinct from a
9
+ * *runtime* (the coding-agent CLI that drives workers — see src/runtimes/).
10
+ * `native` providers are reached through the runtime's own auth; `gateway`
11
+ * providers route through a base URL with a bearer token.
12
+ *
13
+ * Agentplate rejects models below `minContextTokens` of context, because
14
+ * multi-step tool-calling agents need room to work (the same 64k floor Hermes
15
+ * enforces).
16
+ */
17
+
18
+ /** Minimum context window (tokens) Agentplate will accept for an agent model. */
19
+ export const MIN_CONTEXT_TOKENS = 64_000;
20
+
21
+ export interface ProviderModel {
22
+ /** Model id passed to the provider/runtime (e.g. "claude-sonnet-4-6"). */
23
+ id: string;
24
+ /** Human label for the wizard. */
25
+ label: string;
26
+ /** Context window in tokens. */
27
+ contextWindow: number;
28
+ }
29
+
30
+ export interface ProviderSpec {
31
+ /** Unique provider id (key into config.providers). */
32
+ id: string;
33
+ /** Human label for the wizard. */
34
+ label: string;
35
+ /** One-line description. */
36
+ description: string;
37
+ /** `native` = runtime-native auth; `gateway` = base URL + bearer token. */
38
+ kind: "native" | "gateway";
39
+ /** Conventional env var name holding the API key. */
40
+ authEnvVar: string;
41
+ /** Default base URL for gateway providers. */
42
+ defaultBaseUrl?: string;
43
+ /** True if the user must supply a base URL (custom/self-hosted endpoints). */
44
+ requiresBaseUrl?: boolean;
45
+ /** True if this provider does not need an API key (e.g. local Ollama). */
46
+ keyless?: boolean;
47
+ /**
48
+ * True if this provider can be used via an existing CLI/subscription login
49
+ * (no API key stored) — e.g. a Claude Pro/Max OAuth session in Claude Code.
50
+ */
51
+ supportsSubscription?: boolean;
52
+ /** The runtime CLI that provides the subscription login (e.g. "claude"). */
53
+ subscriptionRuntime?: string;
54
+ /** Human label for the subscription option (e.g. "Claude Pro/Max subscription"). */
55
+ subscriptionLabel?: string;
56
+ /** Known models (the wizard offers these; custom ids are always allowed). */
57
+ models: ProviderModel[];
58
+ /** Where to get credentials / docs. */
59
+ docsUrl?: string;
60
+ }
61
+
62
+ /**
63
+ * The catalog. Model lists are a curated starting point, not exhaustive — the
64
+ * wizard always lets the user type a custom model id.
65
+ */
66
+ export const PROVIDERS: readonly ProviderSpec[] = [
67
+ {
68
+ id: "anthropic",
69
+ label: "Anthropic",
70
+ description: "Claude models (native Claude Code support)",
71
+ kind: "native",
72
+ authEnvVar: "ANTHROPIC_API_KEY",
73
+ supportsSubscription: true,
74
+ subscriptionRuntime: "claude",
75
+ subscriptionLabel: "Claude Pro/Max subscription (Claude Code login)",
76
+ docsUrl: "https://console.anthropic.com/settings/keys",
77
+ models: [
78
+ { id: "claude-opus-4-8", label: "Claude Opus 4.8", contextWindow: 200_000 },
79
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", contextWindow: 200_000 },
80
+ { id: "claude-haiku-4-5-20251001", label: "Claude Haiku 4.5", contextWindow: 200_000 },
81
+ ],
82
+ },
83
+ {
84
+ id: "openai",
85
+ label: "OpenAI",
86
+ description: "GPT-5 series — frontier and coding-optimized models (Codex)",
87
+ kind: "gateway",
88
+ authEnvVar: "OPENAI_API_KEY",
89
+ defaultBaseUrl: "https://api.openai.com/v1",
90
+ supportsSubscription: true,
91
+ subscriptionRuntime: "codex",
92
+ subscriptionLabel: "ChatGPT/Codex subscription (codex login)",
93
+ docsUrl: "https://platform.openai.com/api-keys",
94
+ // The development-focused slice of the Codex model catalog (codex-cli 0.128).
95
+ // gpt-5.5 leads (frontier coding); the codex/spark variants are the
96
+ // coding-optimized line. Spark is Codex-subscription only (not in the public
97
+ // API), so prefer it with the `codex` runtime. The wizard always allows a
98
+ // custom model id, so this is a curated starting point, not the full list.
99
+ models: [
100
+ { id: "gpt-5.5", label: "GPT-5.5 (frontier coding)", contextWindow: 272_000 },
101
+ { id: "gpt-5.4", label: "GPT-5.4 (everyday coding)", contextWindow: 272_000 },
102
+ { id: "gpt-5.4-mini", label: "GPT-5.4 Mini (fast, low-cost)", contextWindow: 272_000 },
103
+ { id: "gpt-5.3-codex", label: "GPT-5.3 Codex (coding-optimized)", contextWindow: 272_000 },
104
+ {
105
+ id: "gpt-5.3-codex-spark",
106
+ label: "GPT-5.3 Codex Spark (ultra-fast, Codex login only)",
107
+ contextWindow: 128_000,
108
+ },
109
+ ],
110
+ },
111
+ {
112
+ id: "openrouter",
113
+ label: "OpenRouter",
114
+ description: "Free development models behind one OpenAI-compatible gateway",
115
+ kind: "gateway",
116
+ authEnvVar: "OPENROUTER_API_KEY",
117
+ defaultBaseUrl: "https://openrouter.ai/api/v1",
118
+ docsUrl: "https://openrouter.ai/models?max_price=0",
119
+ // Curated free (`:free`) coding models verified against the OpenRouter API.
120
+ // Free tiers are rate-limited and may change; any paid model id can still be
121
+ // typed at the prompt. qwen3-coder leads (coding-specialized, 1M context).
122
+ models: [
123
+ { id: "qwen/qwen3-coder:free", label: "Qwen3 Coder (free)", contextWindow: 1_000_000 },
124
+ { id: "moonshotai/kimi-k2.6:free", label: "Kimi K2.6 (free)", contextWindow: 262_144 },
125
+ {
126
+ id: "qwen/qwen3-next-80b-a3b-instruct:free",
127
+ label: "Qwen3 Next 80B (free)",
128
+ contextWindow: 262_144,
129
+ },
130
+ { id: "z-ai/glm-4.5-air:free", label: "GLM 4.5 Air (free)", contextWindow: 131_072 },
131
+ { id: "openai/gpt-oss-120b:free", label: "GPT-OSS 120B (free)", contextWindow: 131_072 },
132
+ {
133
+ id: "meta-llama/llama-3.3-70b-instruct:free",
134
+ label: "Llama 3.3 70B (free)",
135
+ contextWindow: 131_072,
136
+ },
137
+ ],
138
+ },
139
+ {
140
+ id: "opencode-zen",
141
+ label: "OpenCode Zen",
142
+ description: "Free development models (MiniMax 2.5, GLM, Kimi, Qwen, DeepSeek…)",
143
+ kind: "gateway",
144
+ authEnvVar: "OPENCODE_API_KEY",
145
+ defaultBaseUrl: "https://opencode.ai/zen/v1",
146
+ // OpenCode manages its own login (`opencode auth login`), so prefer the
147
+ // subscription path — Agentplate stores no key and the `opencode` runtime
148
+ // uses its existing Zen login, mirroring Anthropic→claude.
149
+ supportsSubscription: true,
150
+ subscriptionRuntime: "opencode",
151
+ subscriptionLabel: "OpenCode Zen login (opencode auth)",
152
+ docsUrl: "https://opencode.ai/docs/zen",
153
+ // OpenCode Zen models, addressed in the `opencode` runtime's `provider/model`
154
+ // form (`opencode/<name>`); a bare id is "Invalid model format". The free
155
+ // (`*-free`) catalog ROTATES often, so this is a current snapshot, not a
156
+ // contract — the wizard always allows a custom id. Verified against
157
+ // `opencode models` (opencode 1.15.x).
158
+ models: [
159
+ { id: "opencode/minimax-m3-free", label: "MiniMax M3 (free)", contextWindow: 200_000 },
160
+ { id: "opencode/big-pickle", label: "Big Pickle", contextWindow: 200_000 },
161
+ {
162
+ id: "opencode/deepseek-v4-flash-free",
163
+ label: "DeepSeek V4 Flash (free)",
164
+ contextWindow: 200_000,
165
+ },
166
+ { id: "opencode/mimo-v2.5-free", label: "MiMo V2.5 (free)", contextWindow: 200_000 },
167
+ {
168
+ id: "opencode/nemotron-3-super-free",
169
+ label: "Nemotron 3 Super (free)",
170
+ contextWindow: 204_800,
171
+ },
172
+ ],
173
+ },
174
+ {
175
+ id: "deepseek",
176
+ label: "DeepSeek",
177
+ description: "DeepSeek chat and reasoner models",
178
+ kind: "gateway",
179
+ authEnvVar: "DEEPSEEK_API_KEY",
180
+ defaultBaseUrl: "https://api.deepseek.com/v1",
181
+ docsUrl: "https://platform.deepseek.com/api_keys",
182
+ models: [
183
+ { id: "deepseek-chat", label: "DeepSeek Chat", contextWindow: 128_000 },
184
+ { id: "deepseek-reasoner", label: "DeepSeek Reasoner", contextWindow: 128_000 },
185
+ ],
186
+ },
187
+ {
188
+ id: "google",
189
+ label: "Google Gemini",
190
+ description: "Latest Gemini 3.x models (1M context)",
191
+ kind: "gateway",
192
+ authEnvVar: "GEMINI_API_KEY",
193
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta/openai",
194
+ supportsSubscription: true,
195
+ subscriptionRuntime: "gemini",
196
+ subscriptionLabel: "Google account (Gemini CLI login)",
197
+ docsUrl: "https://aistudio.google.com/apikey",
198
+ // Latest Gemini lineup verified against models.dev (release dates). 3.1 Pro
199
+ // leads (frontier coding/reasoning); 3.5 Flash is the newest fast model;
200
+ // `gemini-flash-latest` is Google's always-newest Flash alias. 2.5 Pro is the
201
+ // current non-preview GA fallback. The wizard always allows a custom id.
202
+ models: [
203
+ { id: "gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (preview)", contextWindow: 1_048_576 },
204
+ { id: "gemini-3.5-flash", label: "Gemini 3.5 Flash", contextWindow: 1_048_576 },
205
+ { id: "gemini-3-pro-preview", label: "Gemini 3 Pro (preview)", contextWindow: 1_048_576 },
206
+ {
207
+ id: "gemini-3.1-flash-lite",
208
+ label: "Gemini 3.1 Flash Lite (low-cost)",
209
+ contextWindow: 1_048_576,
210
+ },
211
+ { id: "gemini-flash-latest", label: "Gemini Flash (latest alias)", contextWindow: 1_048_576 },
212
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro (GA)", contextWindow: 1_048_576 },
213
+ ],
214
+ },
215
+ {
216
+ id: "ollama",
217
+ label: "Ollama (local)",
218
+ description: "Run open models locally — no API key required",
219
+ kind: "gateway",
220
+ authEnvVar: "OLLAMA_API_KEY",
221
+ defaultBaseUrl: "http://localhost:11434/v1",
222
+ keyless: true,
223
+ docsUrl: "https://ollama.com/library",
224
+ models: [
225
+ { id: "qwen2.5-coder:32b", label: "Qwen2.5 Coder 32B", contextWindow: 128_000 },
226
+ { id: "llama3.3:70b", label: "Llama 3.3 70B", contextWindow: 128_000 },
227
+ ],
228
+ },
229
+ {
230
+ id: "custom",
231
+ label: "Custom endpoint",
232
+ description: "Any OpenAI-compatible endpoint (self-hosted, NIM, vLLM, …)",
233
+ kind: "gateway",
234
+ authEnvVar: "AGENTPLATE_CUSTOM_API_KEY",
235
+ requiresBaseUrl: true,
236
+ docsUrl: undefined,
237
+ models: [],
238
+ },
239
+ ];
240
+
241
+ /** Look up a provider spec by id. */
242
+ export function getProviderSpec(id: string): ProviderSpec | undefined {
243
+ return PROVIDERS.find((p) => p.id === id);
244
+ }
245
+
246
+ /** All provider specs, in catalog order. */
247
+ export function listProviders(): readonly ProviderSpec[] {
248
+ return PROVIDERS;
249
+ }
250
+
251
+ /** Does this model satisfy the minimum context requirement? */
252
+ export function meetsContextFloor(model: ProviderModel): boolean {
253
+ return model.contextWindow >= MIN_CONTEXT_TOKENS;
254
+ }