Skip to content

Commit 462fc0c

Browse files
committed
chore: ship compiled dist in the repository
Git/package installs of OpenClaw plugins require the built runtime (./dist/index.js); the TypeScript-source fallback only applies to local dev paths (plugins.load.paths / --link). Commit the build output so the plugin installs from the GitHub repo, and document it in README/AGENTS.
1 parent 4e4a57d commit 462fc0c

6 files changed

Lines changed: 493 additions & 9 deletions

File tree

.gitignore

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
# Dependencies
22
node_modules/
33

4-
# Build output
5-
dist/
6-
74
# Environment / secrets (never commit)
85
.env
96
.env.*

AGENTS.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ Command Code (commandcode.ai) model provider plugin for OpenClaw, with three-tie
1717
- `test/` — Vitest unit tests (transport + projection + dynamic resolution)
1818
- `scripts/` — Node `.mjs` utilities (`generate-baseline.mjs`, `smoke.discovery.mjs`, `live-test.mjs`)
1919
- `.github/workflows/` — CI (typecheck + build + tests)
20-
- `dist/` — build output, git-ignored
20+
- `dist/` — build output, **committed**: OpenClaw git/package installs require the compiled runtime (`./dist/index.js`); the TypeScript-source fallback only applies to local dev paths (`plugins.load.paths`, `--link`). Regenerate with `npm run build` before committing.
21+
- `openclaw.plugin.json` — manifest (provider id, auth env var, onboarding choice, modelCatalog)
22+
- `package.json` — package + openclaw extension metadata (`prepack` builds dist for npm publish)
2123

2224
## Code style
2325

README.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ node scripts/generate-baseline.mjs
4747
(GOAT / Pro / Max / Team / Provider). The **Go** plan has no API access and
4848
any request returns HTTP `403 upgrade_required`.
4949

50-
## Install (local dev)
50+
## Install
5151

52-
From this directory:
52+
From GitHub (requires the committed `dist/` runtime in the repo):
53+
54+
```bash
55+
openclaw plugins install git:github.com/TheStreamCode/openclaw-command-code
56+
```
57+
58+
From a local checkout:
5359

5460
```bash
5561
# with a local path (developing/testing)
@@ -134,8 +140,11 @@ node scripts/live-test.mjs # live inference (reads the key from ~/.ope
134140

135141
The repo intentionally ships **no TypeScript `.d.ts`**: the plugin is consumed
136142
as a runtime entry (`dist/index.js`), not as a typed library, and an exported
137-
definition type would not be portable. `dist/` itself is git-ignored and built
138-
before install/publish.
143+
definition type would not be portable. `dist/` is **committed** because
144+
OpenClaw git/package installs require the compiled runtime — the
145+
TypeScript-source fallback only applies to local dev paths (`plugins.load.paths`,
146+
`--link`). Regenerate it with `npm run build` after any source change (the
147+
`prepack` script also builds it for npm publish).
139148

140149
A GitHub Actions workflow (`.github/workflows/ci.yml`) runs type-check, build,
141150
and unit tests on every push to `main` and on pull requests. Updates to this

dist/index.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
/**
2+
* Command Code model provider plugin for OpenClaw.
3+
*
4+
* Registers an OpenAI/Anthropic-compatible provider backed by the Command Code
5+
* Provider API (https://commandcode.ai). The model catalog uses a two-tier
6+
* strategy:
7+
*
8+
* - `buildStaticProvider`: a bundled baseline snapshot (generated, never
9+
* hand-edited) so models are discoverable before credentials resolve.
10+
* - `buildProvider`: the live catalog fetched at runtime from
11+
* `GET https://api.commandcode.ai/provider/v1/models` with a short TTL,
12+
* keeping models fresh when the gateway is running.
13+
* - `resolveCommandCodeDynamicModel`: a runtime model resolver for ids
14+
* missing from the per-agent registry (models.json). OpenClaw only
15+
* materializes a provider into that registry when its auth can be proven
16+
* at planning time (env var, auth profile, or explicit config); without
17+
* that proof, resolution falls through to this hook instead of failing
18+
* with "Unknown model".
19+
*
20+
* Auth: `COMMAND_CODE_API_KEY` (Studio > API Keys). Requires a plan with API
21+
* access (GOAT / Pro / Max / Team / Provider); the Go plan returns 403
22+
* `upgrade_required`.
23+
*/
24+
import { defineSingleProviderPluginEntry, } from "openclaw/plugin-sdk/provider-entry";
25+
import { getCachedLiveProviderModelRows, } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
26+
import { commandCodeBaselineModels } from "./src/baseline.models.js";
27+
/** Endpoint that lists available models. Public (no auth required for the list). */
28+
const MODELS_ENDPOINT = "https://api.commandcode.ai/provider/v1/models";
29+
/** Base URL for OpenAI-compatible (chat/completions) traffic. */
30+
const OPENAI_BASE_URL = "https://api.commandcode.ai/provider/v1";
31+
/** Base URL for Anthropic-compatible (messages) traffic. */
32+
const ANTHROPIC_BASE_URL = "https://api.commandcode.ai/provider/v1";
33+
/** Cache TTL for the live model catalog (ms). */
34+
const CATALOG_TTL_MS = 60_000;
35+
/** Derives the Claude (Anthropic-messages) models by id convention. */
36+
export function isClaudeModel(modelId) {
37+
return modelId.startsWith("claude-");
38+
}
39+
/**
40+
* Maps a live Command Code model row to an OpenClaw model definition.
41+
*
42+
* The `/models` endpoint returns `id`, `name`, and `context_length` only. It
43+
* does NOT expose per-token pricing, max output tokens, or input modalities.
44+
* Those are therefore set to conservative provider-neutral defaults here and
45+
* documented as such; they can be enriched later without hardcoding the model
46+
* list itself.
47+
*/
48+
export function projectModel(row) {
49+
const id = typeof row.id === "string" && row.id.length > 0 ? row.id : null;
50+
if (!id)
51+
return null;
52+
const rawName = row.name;
53+
const name = typeof rawName === "string" && rawName.length > 0 ? rawName : id;
54+
const rawCtx = row.context_length;
55+
const contextWindow = typeof rawCtx === "number" && rawCtx > 0 ? rawCtx : 200_000;
56+
const claude = isClaudeModel(id);
57+
const api = claude ? "anthropic-messages" : "openai-completions";
58+
return {
59+
id,
60+
name,
61+
api,
62+
// Base URL per model so Claude models hit /messages and everything else
63+
// hits /chat/completions. Both paths live under the same /provider/v1 host.
64+
baseUrl: claude ? ANTHROPIC_BASE_URL : OPENAI_BASE_URL,
65+
reasoning: true,
66+
// Input modalities are not reported by /models. Stay conservative (text)
67+
// so models without vision support are never sent image input.
68+
input: ["text"],
69+
// Pricing is not returned by /models; set to zero to avoid fabricating
70+
// prices. See README for the enrichment note.
71+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
72+
contextWindow,
73+
// Output budget is not returned by /models. Use a conservative per-model
74+
// cap that stays within a sane ceiling for large-context models.
75+
maxTokens: Math.min(contextWindow, 131_072),
76+
};
77+
}
78+
/**
79+
* Builds the provider config from a set of Command Code model rows, mapping each
80+
* row through the shared projection. Used by both the live catalog (fetched at
81+
* runtime) and the static baseline (bundled snapshot for pre-credential
82+
* discovery), so the two never drift in shape.
83+
*/
84+
export function providerFromRows(rows) {
85+
const models = rows
86+
.map((row) => projectModel(row))
87+
.filter((m) => m !== null);
88+
return {
89+
baseUrl: OPENAI_BASE_URL,
90+
api: "openai-completions",
91+
models,
92+
};
93+
}
94+
/**
95+
* Builds the provider config with a fully live-discovered catalog fetched from
96+
* the Command Code Provider API. No model list is hardcoded here.
97+
*/
98+
async function buildProvider() {
99+
const rows = await getCachedLiveProviderModelRows({
100+
providerId: "commandcode",
101+
endpoint: MODELS_ENDPOINT,
102+
ttlMs: CATALOG_TTL_MS,
103+
auditContext: "commandcode-model-discovery",
104+
// Endpoint already returns the OpenAI `{ data: [{ id, object, ... }] }`
105+
// shape, so the default row/readModel handling covers it. No custom readRows
106+
// or readModelId needed.
107+
});
108+
return providerFromRows(rows);
109+
}
110+
/**
111+
* Builds the provider config from the bundled static baseline. This exposes
112+
* models for cheap pre-credential discovery (models list without a resolved
113+
* key / gateway) and is refreshed at runtime by the live catalog above.
114+
*/
115+
async function buildStaticProvider() {
116+
return providerFromRows(commandCodeBaselineModels);
117+
}
118+
/** Strips a leading `<provider>/` prefix from a runtime model id when present. */
119+
function stripProviderModelPrefix(provider, modelId) {
120+
const prefix = `${provider}/`;
121+
return modelId.startsWith(prefix) ? modelId.slice(prefix.length) : modelId;
122+
}
123+
/**
124+
* Resolves commandcode models missing from the local per-agent registry.
125+
*
126+
* OpenClaw only materializes a provider's models into the agent registry
127+
* (models.json) when its auth can be proven at planning time (env var, auth
128+
* profile, or explicit config). When that proof is absent, model resolution
129+
* falls through to this hook instead of failing with "Unknown model".
130+
*
131+
* The baseline is consulted synchronously; ids not in the snapshot receive a
132+
* conservative provider-neutral definition so newly published models keep
133+
* working without a baseline refresh.
134+
*/
135+
export function resolveCommandCodeDynamicModel(ctx) {
136+
const provider = ctx.provider ?? "commandcode";
137+
const modelId = stripProviderModelPrefix(provider, ctx.modelId);
138+
const row = commandCodeBaselineModels.find((entry) => entry.id === modelId);
139+
const projected = row ? projectModel(row) : null;
140+
const claude = isClaudeModel(modelId);
141+
const api = projected?.api ?? (claude ? "anthropic-messages" : "openai-completions");
142+
const contextWindow = projected?.contextWindow ?? 200_000;
143+
return {
144+
id: modelId,
145+
name: projected?.name ?? modelId,
146+
api,
147+
provider,
148+
baseUrl: claude ? ANTHROPIC_BASE_URL : OPENAI_BASE_URL,
149+
reasoning: true,
150+
input: ["text"],
151+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
152+
contextWindow,
153+
maxTokens: Math.min(contextWindow, 131_072),
154+
};
155+
}
156+
export default defineSingleProviderPluginEntry({
157+
id: "commandcode",
158+
name: "Command Code",
159+
description: "Command Code (commandcode.ai) model provider with live model discovery.",
160+
provider: {
161+
label: "Command Code",
162+
docsPath: "/providers/commandcode",
163+
auth: [
164+
{
165+
methodId: "api-key",
166+
label: "Command Code API key",
167+
hint: "API key from commandcode.ai Studio > API Keys",
168+
optionKey: "commandcodeApiKey",
169+
flagName: "--commandcode-api-key",
170+
envVar: "COMMAND_CODE_API_KEY",
171+
promptMessage: "Enter your Command Code API key",
172+
defaultModel: "commandcode/deepseek/deepseek-v4-flash",
173+
},
174+
],
175+
catalog: {
176+
// Live-discovered catalog. The /models endpoint is public, so discovery
177+
// works before the user configures a key; inference still requires it.
178+
buildProvider,
179+
// Static baseline for discovery before credentials are resolved. The
180+
// baseline is generated from the live endpoint (scripts/generate-baseline.mjs)
181+
// and is kept fresh at runtime by the live catalog above.
182+
buildStaticProvider,
183+
},
184+
// Resolves commandcode models that are missing from the per-agent registry
185+
// (see resolveCommandCodeDynamicModel) so inference never fails with
186+
// "Unknown model" when the planner could not prove auth.
187+
resolveDynamicModel: resolveCommandCodeDynamicModel,
188+
},
189+
});

0 commit comments

Comments
 (0)