@sailresearch/code 0.1.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 ADDED
@@ -0,0 +1,87 @@
1
+ # @sailresearch/code
2
+
3
+ Run [Claude Code](https://code.claude.com) on **GLM-5.2** via
4
+ [Sail](https://sailresearch.com) — one command, no Anthropic account.
5
+
6
+ ```sh
7
+ npx @sailresearch/code
8
+ ```
9
+
10
+ That's it. On first run it opens a browser to mint a Sail API key (or reuses
11
+ `SAIL_API_KEY` / an existing `sail auth login` credential from `~/.sail`),
12
+ sets the routing environment, and launches `claude`. A freshly minted key is
13
+ saved to `~/.sail` (like `sail auth login`); your Claude Code setup is left
14
+ untouched. Set `SAIL_API_KEY` in your environment to skip the browser flow and
15
+ persist nothing.
16
+
17
+ Claude Code itself must be installed (`npm install -g @anthropic-ai/claude-code`).
18
+
19
+ ## Persistent setup / VS Code
20
+
21
+ To make `claude` route to Sail without the launcher (for VS Code, or a bare
22
+ `claude`), persist the env block:
23
+
24
+ ```sh
25
+ npx @sailresearch/code setup # writes ~/.claude/settings.json (backed up)
26
+ npx @sailresearch/code setup --revert # restores the backup
27
+ ```
28
+
29
+ Notes:
30
+
31
+ - After `setup`, `sail-code` runs still win over the persisted values (the
32
+ launcher passes its env as command-line settings, which outrank settings
33
+ files) — so `--mode`/`--model` flags keep working.
34
+ - `setup` (default `--user`) writes the **lowest-precedence** settings file, so
35
+ a repo that ships its own `.claude/settings.json` with a conflicting
36
+ `ANTHROPIC_BASE_URL` / provider selector still outranks it inside that repo —
37
+ run `setup --project` there. Elsewhere, bare `claude` routes to Sail.
38
+ - **VS Code:** extension sessions read the persisted env block, but the
39
+ extension's own pre-launch login check does not. If you have no saved
40
+ Anthropic login, also set `claudeCode.disableLoginPrompt: true` and
41
+ `claudeCode.environmentVariables` in VS Code's user settings — `setup`
42
+ prints the exact snippet.
43
+ - `setup --project` writes the repo-local `.claude/settings.local.json`
44
+ instead — Claude Code's personal, not-shared project settings. It holds your
45
+ API key, so `setup` also adds it to `.claude/.gitignore`; keep it out of
46
+ commits. Claude Code applies project settings only after you've trusted the
47
+ folder in its first-run prompt.
48
+
49
+ ## Choosing models
50
+
51
+ ```sh
52
+ npx @sailresearch/code --model zai-org/GLM-5.2-FP8 --background-model openai/gpt-oss-120b
53
+ ```
54
+
55
+ Both tiers default to GLM-5.2. `--background-model` moves Claude Code's
56
+ haiku/background tier (titles, summaries) to a cheaper model from the
57
+ [Sail catalog](https://docs.sailresearch.com/models).
58
+
59
+ ## Passing flags to claude
60
+
61
+ Everything after `--` goes to `claude` untouched:
62
+
63
+ ```sh
64
+ npx @sailresearch/code -- --resume
65
+ ```
66
+
67
+ ## How it works
68
+
69
+ Claude Code speaks the Anthropic Messages API to whatever
70
+ `ANTHROPIC_BASE_URL` points at. Sail serves that API for GLM-5.2 (system
71
+ prompts, tools, streaming included), so the launcher only sets environment
72
+ variables — see them with `npx @sailresearch/code env`. Your Anthropic
73
+ credentials are never involved or touched.
74
+
75
+ Claude Code's own non-model traffic (update checks, error reports, usage
76
+ telemetry) still goes to Anthropic endpoints; set
77
+ `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1` before launching if you need
78
+ that off (it also disables auto-updates).
79
+
80
+ The native Claude Code desktop app reads routing only from managed (MDM)
81
+ configuration and is not supported; use the terminal or VS Code.
82
+
83
+ If you're signed into an enterprise **Claude apps gateway**, that session
84
+ outranks `ANTHROPIC_AUTH_TOKEN`, so `claude` keeps using the gateway instead
85
+ of Sail — run `/logout` first (ordinary Pro/Max users aren't affected).
86
+
87
+ Docs: https://docs.sailresearch.com/claude-code
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { main } from "../src/cli.js";
3
+
4
+ main().catch((err) => {
5
+ console.error(`sail-code: ${err?.message ?? err}`);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@sailresearch/code",
3
+ "version": "0.1.0",
4
+ "description": "Run Claude Code on GLM-5.2 via Sail — one command, no Anthropic account.",
5
+ "license": "MIT",
6
+ "author": "Sail Research",
7
+ "homepage": "https://docs.sailresearch.com/claude-code",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/sailresearchco/sail-code.git"
11
+ },
12
+ "type": "module",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "bin": {
17
+ "sail-code": "bin/sail-code.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "src/",
22
+ "README.md"
23
+ ],
24
+ "engines": {
25
+ "node": ">=18.17"
26
+ },
27
+ "scripts": {
28
+ "test": "node --test"
29
+ }
30
+ }
package/src/cli.js ADDED
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Argument parsing and dispatch for `sail-code`.
3
+ *
4
+ * Everything after a literal `--` is passed to `claude` untouched. Flags
5
+ * before it belong to sail-code; unknown ones are an error (not silently
6
+ * forwarded) so a typo can't change which tool interprets a flag.
7
+ */
8
+
9
+ import { createRequire } from "node:module";
10
+
11
+ import { DEFAULT_MODEL, MODE_VALUES, buildClaudeEnv } from "./constants.js";
12
+ import { maskSecret, resolveTarget } from "./credentials.js";
13
+ import { browserLogin } from "./login.js";
14
+ import { resolveCredentials, run } from "./run.js";
15
+ import { revert, setup } from "./setup.js";
16
+
17
+ const require = createRequire(import.meta.url);
18
+
19
+ const HELP = `sail-code — run Claude Code on GLM-5.2 via Sail (no Anthropic account)
20
+
21
+ Usage:
22
+ sail-code [flags] [-- <claude args...>] Launch claude routed to Sail
23
+ sail-code setup [--user|--project] Persist routing into settings
24
+ [--revert] (--project → gitignored local file)
25
+ sail-code login Log in and store a Sail API key
26
+ sail-code env Print the env block (key masked)
27
+
28
+ Flags:
29
+ --model <id> Main model (default ${DEFAULT_MODEL})
30
+ --background-model <id> Background/haiku-tier model (default: same as --model)
31
+ --mode <name> Sail environment: ${MODE_VALUES.join(" | ")}
32
+ -h, --help Show this help
33
+ -v, --version Show the version
34
+
35
+ Credentials resolve from SAIL_API_KEY, then ~/.sail (shared with the Sail
36
+ CLI), then a browser login. Docs: https://docs.sailresearch.com/claude-code`;
37
+
38
+ /** Parse argv (post-node, post-script). Exported for tests. */
39
+ export function parseArgs(argv) {
40
+ const parsed = {
41
+ command: "run",
42
+ model: undefined,
43
+ backgroundModel: undefined,
44
+ modeFlag: undefined,
45
+ scope: undefined,
46
+ revert: false,
47
+ help: false,
48
+ version: false,
49
+ claudeArgs: [],
50
+ };
51
+
52
+ const sep = argv.indexOf("--");
53
+ const own = sep === -1 ? argv : argv.slice(0, sep);
54
+ parsed.claudeArgs = sep === -1 ? [] : argv.slice(sep + 1);
55
+
56
+ const takesValue = {
57
+ "--model": "model",
58
+ "--background-model": "backgroundModel",
59
+ "--mode": "modeFlag",
60
+ };
61
+ let commandSeen = false;
62
+
63
+ for (let i = 0; i < own.length; i++) {
64
+ const arg = own[i];
65
+ if (!arg.startsWith("-")) {
66
+ if (commandSeen)
67
+ throw new Error(
68
+ `unexpected argument "${arg}" (use -- to pass args to claude)`,
69
+ );
70
+ if (!["setup", "login", "env", "run"].includes(arg)) {
71
+ throw new Error(
72
+ `unknown command "${arg}" (expected setup, login, or env)`,
73
+ );
74
+ }
75
+ parsed.command = arg;
76
+ commandSeen = true;
77
+ continue;
78
+ }
79
+ if (arg in takesValue) {
80
+ const value = own[++i];
81
+ if (value === undefined || value.startsWith("-")) {
82
+ throw new Error(`${arg} requires a value`);
83
+ }
84
+ parsed[takesValue[arg]] = value;
85
+ } else if (arg === "--user") parsed.scope = "user";
86
+ else if (arg === "--project") parsed.scope = "project";
87
+ else if (arg === "--revert") parsed.revert = true;
88
+ else if (arg === "-h" || arg === "--help") parsed.help = true;
89
+ else if (arg === "-v" || arg === "--version") parsed.version = true;
90
+ else
91
+ throw new Error(`unknown flag "${arg}" (flags for claude go after --)`);
92
+ }
93
+
94
+ if (parsed.modeFlag && !MODE_VALUES.includes(parsed.modeFlag)) {
95
+ throw new Error(
96
+ `unknown --mode "${parsed.modeFlag}" (expected one of: ${MODE_VALUES.join(", ")})`,
97
+ );
98
+ }
99
+ if (parsed.scope && parsed.command !== "setup") {
100
+ throw new Error(`--${parsed.scope} only applies to setup`);
101
+ }
102
+ return parsed;
103
+ }
104
+
105
+ export async function main() {
106
+ const args = parseArgs(process.argv.slice(2));
107
+
108
+ if (args.help) {
109
+ console.log(HELP);
110
+ return;
111
+ }
112
+ if (args.version) {
113
+ console.log(require("../package.json").version);
114
+ return;
115
+ }
116
+
117
+ let exitCode = 0;
118
+ switch (args.command) {
119
+ case "login": {
120
+ const target = resolveTarget({ modeFlag: args.modeFlag });
121
+ await browserLogin({
122
+ expectedTarget: target.apiUrl,
123
+ expectedSource: target.source,
124
+ });
125
+ break;
126
+ }
127
+ case "setup":
128
+ exitCode = args.revert
129
+ ? revert({ scope: args.scope ?? "user" })
130
+ : await setup({
131
+ scope: args.scope ?? "user",
132
+ modeFlag: args.modeFlag,
133
+ model: args.model,
134
+ backgroundModel: args.backgroundModel,
135
+ });
136
+ break;
137
+ case "env": {
138
+ const { apiKey, apiUrl } = await resolveCredentials({
139
+ modeFlag: args.modeFlag,
140
+ });
141
+ const env = buildClaudeEnv({
142
+ apiUrl,
143
+ apiKey,
144
+ model: args.model,
145
+ backgroundModel: args.backgroundModel,
146
+ });
147
+ for (const [key, value] of Object.entries(env)) {
148
+ const shown =
149
+ key === "ANTHROPIC_AUTH_TOKEN" ? maskSecret(value) : value;
150
+ console.log(`${key}=${shown}`);
151
+ }
152
+ console.error(
153
+ "\n(ANTHROPIC_AUTH_TOKEN is masked; it is your SAIL_API_KEY value)",
154
+ );
155
+ break;
156
+ }
157
+ default:
158
+ exitCode = await run({
159
+ modeFlag: args.modeFlag,
160
+ model: args.model,
161
+ backgroundModel: args.backgroundModel,
162
+ claudeArgs: args.claudeArgs,
163
+ });
164
+ }
165
+ if (exitCode !== 0) process.exit(exitCode);
166
+ }
@@ -0,0 +1,177 @@
1
+ /**
2
+ * Shared constants: the GLM model default, Sail environment targets, and the
3
+ * single source of truth for the Claude Code environment block.
4
+ *
5
+ * Every surface that configures Claude Code (the `run` exec path, the `setup`
6
+ * settings.json path, and the `env` printout) must build its variables through
7
+ * `buildClaudeEnv` so the three can never drift.
8
+ */
9
+
10
+ export const DEFAULT_MODEL = "zai-org/GLM-5.2-FP8";
11
+
12
+ /** Public API targets by mode name, mirroring the Sail CLI's mode map. */
13
+ export const MODE_API_URLS = {
14
+ prod: "https://api.sailresearch.com",
15
+ dev: "https://dev.sailresearch.com",
16
+ staging: "https://staging.sailresearch.com",
17
+ local: "http://localhost:8080",
18
+ };
19
+
20
+ export const MODE_VALUES = Object.keys(MODE_API_URLS);
21
+
22
+ /** Web app that serves the browser login handoff (`/cli-auth`). */
23
+ export const DEFAULT_APP_URL = "https://app.sailresearch.com";
24
+
25
+ /**
26
+ * Claude Code reads the Anthropic Messages endpoint as `{base}/v1/messages`,
27
+ * so the base URL must be the bare API host (no `/v1` suffix).
28
+ *
29
+ * `ANTHROPIC_AUTH_TOKEN` (not `ANTHROPIC_API_KEY`) is required: Sail
30
+ * authenticates with `Authorization: Bearer` and rejects the Anthropic
31
+ * `x-api-key` header that `ANTHROPIC_API_KEY` would select.
32
+ */
33
+ /**
34
+ * Capabilities declared for aliases pinned to the DEFAULT (GLM) model.
35
+ * Claude Code enables effort/thinking by matching the model ID against known
36
+ * patterns; a custom ID like `zai-org/GLM-5.2-FP8` matches none, so those
37
+ * features are silently DISABLED — `/effort` selections and
38
+ * `CLAUDE_CODE_EFFORT_LEVEL` get dropped before the request. Declaring
39
+ * `_SUPPORTED_CAPABILITIES` is the documented primary fix for pinned custom
40
+ * models. GLM-5.2 is a reasoning model and Sail maps Anthropic `thinking` →
41
+ * reasoning and `output_config.effort` → reasoning effort, so both are real.
42
+ * Deliberately conservative: no xhigh/max effort or adaptive/interleaved
43
+ * thinking claims until validated end-to-end.
44
+ */
45
+ export const DEFAULT_MODEL_CAPABILITIES = "effort,thinking";
46
+
47
+ export function buildClaudeEnv({ apiUrl, apiKey, model, backgroundModel }) {
48
+ const base = String(apiUrl ?? "").replace(/\/+$/, "");
49
+ const main = model || DEFAULT_MODEL;
50
+ const background = backgroundModel || main;
51
+ // Only aliases pinned to the KNOWN default model get a capabilities
52
+ // declaration — for a user-overridden --model/--background-model we can't
53
+ // know what the model supports, and `_SUPPORTED_CAPABILITIES` disables
54
+ // everything unlisted, so guessing wrong would be worse than Claude Code's
55
+ // built-in detection.
56
+ const capabilities = {};
57
+ for (const [alias, m] of [
58
+ ["OPUS", main],
59
+ ["SONNET", main],
60
+ ["HAIKU", background],
61
+ ["FABLE", main],
62
+ ]) {
63
+ if (m === DEFAULT_MODEL) {
64
+ capabilities[`ANTHROPIC_DEFAULT_${alias}_MODEL_SUPPORTED_CAPABILITIES`] =
65
+ DEFAULT_MODEL_CAPABILITIES;
66
+ }
67
+ }
68
+ return {
69
+ ANTHROPIC_BASE_URL: base,
70
+ ANTHROPIC_AUTH_TOKEN: apiKey,
71
+ // Pin the startup model. Without this, a saved /model choice (persisted
72
+ // into settings.json) or the model restored by --resume wins startup
73
+ // resolution, and a `fable` alias or full claude-* ID would reach Sail
74
+ // unmapped. Precedence keeps mid-session /model switching working:
75
+ // /model (session) > --model > ANTHROPIC_MODEL > settings model field.
76
+ ANTHROPIC_MODEL: main,
77
+ ANTHROPIC_DEFAULT_OPUS_MODEL: main,
78
+ ANTHROPIC_DEFAULT_SONNET_MODEL: main,
79
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: background,
80
+ ANTHROPIC_DEFAULT_FABLE_MODEL: main,
81
+ // Declare effort/thinking for aliases pinned to the default GLM model
82
+ // (see DEFAULT_MODEL_CAPABILITIES). Custom overrides get no declaration.
83
+ ...capabilities,
84
+ // Pin subagents to the main Sail model. CLAUDE_CODE_SUBAGENT_MODEL is the
85
+ // highest-priority subagent model source — it outranks a subagent's
86
+ // frontmatter `model:` and the alias remaps above — so a value inherited
87
+ // from the user's shell or a settings file (e.g. a stale `claude-sonnet-*`
88
+ // from a prior Anthropic/Bedrock setup) would otherwise make every
89
+ // delegated subagent request an unmapped model from Sail and fail. Setting
90
+ // it to the resolved main model neutralizes that inherited value on every
91
+ // surface (this rides the exec env and the highest-precedence --settings
92
+ // layer, and setup persists it) and also routes custom subagents whose
93
+ // frontmatter pins a full claude-* ID onto GLM instead of failing.
94
+ // Trade-off: per-subagent frontmatter tiers and --background-model don't
95
+ // apply to subagents under sail-code; they all run on the main model.
96
+ CLAUDE_CODE_SUBAGENT_MODEL: main,
97
+ // Sail turn times can exceed Claude Code's default request timeout.
98
+ API_TIMEOUT_MS: "3000000",
99
+ // Disable the separate 5-minute stream-idle watchdog. It's independent of
100
+ // API_TIMEOUT_MS (that's the whole-request cap; this fires when no bytes
101
+ // arrive for 5 min) and is ACTIVE by default on custom `ANTHROPIC_BASE_URL`
102
+ // connections like Sail. A throughput-optimized Sail turn can queue longer
103
+ // than 5 min before streaming (standard/flex completion windows), so
104
+ // without this it would be aborted mid-turn despite the raised request
105
+ // timeout. "0" disables it (documented for slow gateways).
106
+ API_FORCE_IDLE_TIMEOUT: "0",
107
+ // MCP tool search needs server-side tool_reference expansion, which Sail
108
+ // does not implement; force it off so Claude Code loads MCP tool schemas
109
+ // upfront (those Sail passes through). Explicit "false" (vs unset) also
110
+ // lets setup/revert clean up any previously persisted value.
111
+ ENABLE_TOOL_SEARCH: "false",
112
+ };
113
+ }
114
+
115
+ /** The env keys this tool owns in settings.json — used by setup/revert. */
116
+ export const OWNED_ENV_KEYS = Object.keys(
117
+ buildClaudeEnv({ apiUrl: "", apiKey: "" }),
118
+ );
119
+
120
+ /**
121
+ * Provider-selection env vars that outrank ANTHROPIC_AUTH_TOKEN in Claude
122
+ * Code's auth precedence. If the user's shell has any of these from another
123
+ * Claude Code setup, an unscrubbed child would route to Bedrock / Vertex /
124
+ * Foundry / Mantle / Claude-Platform-on-AWS instead of Sail. `run` strips them
125
+ * from the spawned env. We strip only the selector, not the ancillary provider
126
+ * vars (`ANTHROPIC_AWS_*`, `AWS_REGION`, cloud creds): those are inert once the
127
+ * selector is gone, and some are too broad to safely drop from the child.
128
+ */
129
+ export const CONFLICTING_PROVIDER_VARS = [
130
+ "CLAUDE_CODE_USE_BEDROCK",
131
+ "CLAUDE_CODE_USE_VERTEX",
132
+ "CLAUDE_CODE_USE_FOUNDRY",
133
+ "CLAUDE_CODE_USE_MANTLE",
134
+ "CLAUDE_CODE_USE_ANTHROPIC_AWS",
135
+ ];
136
+
137
+ /**
138
+ * Env vars that inject extra HTTP headers into every Anthropic request.
139
+ * Claude Code merges `ANTHROPIC_CUSTOM_HEADERS` onto outgoing requests, so a
140
+ * value left over from another gateway setup (e.g. an `x-portkey-api-key`)
141
+ * would be sent to Sail once we overlay `ANTHROPIC_BASE_URL` — leaking a
142
+ * stale third-party secret and possibly mutating the Sail request. We only
143
+ * own the base URL and token, not arbitrary headers, so strip these from the
144
+ * spawned env alongside the provider selectors.
145
+ */
146
+ export const HEADER_INJECTION_VARS = ["ANTHROPIC_CUSTOM_HEADERS"];
147
+
148
+ /**
149
+ * Companion vars that customize a pinned alias model's display and
150
+ * capabilities. They are keyed to the ALIAS, not the concrete model id, so
151
+ * once we repoint `ANTHROPIC_DEFAULT_<ALIAS>_MODEL` to GLM a value inherited
152
+ * from another setup applies to GLM. `_SUPPORTED_CAPABILITIES` is the
153
+ * dangerous one: when set it enables only the listed capabilities and
154
+ * *disables the rest* for that pinned model, so a stale value can hide
155
+ * behavior GLM supports (or advertise ones it doesn't); `_NAME`/`_DESCRIPTION`
156
+ * just mislabel it in the picker. We scrub INHERITED values of these from the
157
+ * spawned child env (`run`) and the persisted settings (`setup`) — our own
158
+ * env block then declares `_SUPPORTED_CAPABILITIES` for aliases pinned to the
159
+ * default GLM model (see DEFAULT_MODEL_CAPABILITIES), and that declaration
160
+ * also rides the temp `--settings` layer, which outranks a companion var
161
+ * living in a settings file. For user-overridden models nothing is declared
162
+ * (built-in detection), and NAME/DESCRIPTION are never set. Empty string is
163
+ * NOT a documented "unset" for these — for `_SUPPORTED_CAPABILITIES` it could
164
+ * read as "no capabilities", worse than leaving it — so inherited values are
165
+ * deleted, never emptied. Residual gap: with a custom --model, a companion
166
+ * var in a *settings file* during `run` can't be cleared by our layers.
167
+ */
168
+ export const MODEL_ALIAS_COMPANION_VARS = [
169
+ "OPUS",
170
+ "SONNET",
171
+ "HAIKU",
172
+ "FABLE",
173
+ ].flatMap((alias) => [
174
+ `ANTHROPIC_DEFAULT_${alias}_MODEL_NAME`,
175
+ `ANTHROPIC_DEFAULT_${alias}_MODEL_DESCRIPTION`,
176
+ `ANTHROPIC_DEFAULT_${alias}_MODEL_SUPPORTED_CAPABILITIES`,
177
+ ]);