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
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,10 @@ export const ClaudePluginMetadata: AgentMetadata = {
env.DISABLE_AUTOUPDATER = '1';
}

// Disable experimental betas and telemetry for stability
// Allow experimental betas (prerequisite for the ENABLE_TOOL_SEARCH default),
// and disable telemetry for stability
if (!env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS) {
env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = '1';
env.CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS = '0';
}
if (!env.CLAUDE_CODE_ENABLE_TELEMETRY) {
env.CLAUDE_CODE_ENABLE_TELEMETRY = '0';
Expand Down
1,461 changes: 1,461 additions & 0 deletions docs/superpowers/plans/2026-09-15-bundle-claude-statusline.md

Large diffs are not rendered by default.

13 changes: 1 addition & 12 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@
],
"scripts": {
"postinstall": "node scripts/postinstall.mjs",
"build": "tsc && tsc-alias && npm run copy-plugin",
"build": "tsc && tsc-alias && npm run copy-plugin && npm run bundle-statusline",
"copy-plugin": "node scripts/copy-plugins.js",
"bundle-statusline": "node scripts/bundle-statusline.mjs",
"prepare:install-artifacts": "node scripts/prepare-install-artifacts.mjs",
"dev": "tsc --watch",
"test": "vitest run --project unit && vitest run --project cli && vitest run --project agent",
Expand Down Expand Up @@ -162,6 +163,7 @@
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vitest/ui": "^4.1.5",
"esbuild": "^0.28.1",
"eslint": "^9.38.0",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
Expand Down
37 changes: 37 additions & 0 deletions scripts/bundle-statusline.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env node

/**
* Bundles the Claude Code statusline into a single self-contained ESM artifact.
*
* statusline.ts runs standalone: Claude Code invokes it as `node <path>` from
* ~/.claude/settings.json as a detached process after the CLI itself has already exited, so it
* cannot resolve node_modules or import from the rest of the project at runtime. Bundling lets
* its source stay normal TypeScript with normal project imports (@/utils/...) while still
* deploying as one flat file with zero sibling dependencies. statusline-installer.ts reads this
* script's output and writes it into ~/.claude/ — see that file for the deploy path.
*
* Entry point is the TypeScript SOURCE (not tsc's dist/ output): esbuild transpiles TS itself
* and resolves the project's `@/*` path alias directly from tsconfig.json, so this step has no
* ordering dependency on `tsc`/`tsc-alias` — type-checking still happens separately via
* `npm run typecheck` / `tsc` in the build chain, this step only needs valid syntax.
*/

import * as esbuild from 'esbuild';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';

const __dirname = dirname(fileURLToPath(import.meta.url));
const rootDir = join(__dirname, '..');

await esbuild.build({
entryPoints: [join(rootDir, 'src/agents/plugins/claude/plugin/statusline.ts')],
outfile: join(rootDir, 'dist/agents/plugins/claude/plugin/statusline.bundle.mjs'),
bundle: true,
platform: 'node',
format: 'esm',
target: 'node20',
tsconfig: join(rootDir, 'tsconfig.json'),
logLevel: 'info',
});

console.log('Statusline bundled successfully!');
21 changes: 19 additions & 2 deletions scripts/copy-plugins.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ const copyConfigs = [
{
name: 'Claude plugin',
src: join(rootDir, 'src/agents/plugins/claude/plugin'),
dest: join(rootDir, 'dist/agents/plugins/claude/plugin')
dest: join(rootDir, 'dist/agents/plugins/claude/plugin'),
// statusline.ts lives in this tree as normal TS source; tsc compiles it and
// scripts/bundle-statusline.mjs bundles it separately, both under dist/. Exclude .ts here so
// this wholesale asset copy doesn't also duplicate the raw source (and its __tests__ file)
// into the published package.
filter: (src) => !src.endsWith('.ts')
},
{
name: 'Gemini extension',
Expand Down Expand Up @@ -63,6 +68,18 @@ const fileConfigs = [
name: 'Model pricing table',
src: join(rootDir, 'src/utils/pricing.json'),
dest: join(rootDir, 'dist/utils/pricing.json')
},
{
// Plain JS, zero project imports — deployed as-is beside any agent's statusline (see
// statusline-installer.ts) as well as imported normally by pricing.ts/usage-readers.ts.
name: 'Routing headers domain module',
src: join(rootDir, 'src/utils/routing-headers.mjs'),
dest: join(rootDir, 'dist/utils/routing-headers.mjs')
},
{
name: 'Bedrock pricing domain module',
src: join(rootDir, 'src/utils/bedrock-pricing.mjs'),
dest: join(rootDir, 'dist/utils/bedrock-pricing.mjs')
}
];

Expand All @@ -89,7 +106,7 @@ for (const config of copyConfigs) {

// Copy recursively
console.log(` - Copying from ${config.src}`);
cpSync(config.src, config.dest, { recursive: true });
cpSync(config.src, config.dest, { recursive: true, ...(config.filter ? { filter: config.filter } : {}) });

console.log(` ✓ ${config.name} copied successfully\n`);
}
Expand Down
10 changes: 10 additions & 0 deletions src/agents/core/AgentCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,16 @@ export class AgentCLI {
providerEnv.CODEMIE_PROFILE_NAME = config.name || 'default';
providerEnv.CODEMIE_CLI_VERSION = this.version;

// Record where the model came from. Plugin-side model resolution (e.g. the Claude
// plugin's live-catalog refresh) otherwise cannot tell a value the user typed seconds
// ago from a profile value that has gone stale, and silently replaces both. Mirrors
// the marker bin/codemie-copilot.js already sets for the Copilot plugin.
providerEnv.CODEMIE_MODEL_SOURCE = options.model
? 'cli'
: process.env.CODEMIE_MODEL
? 'env'
: 'default';

// Pass status flag to lifecycle hooks
if (options.status) {
providerEnv.CODEMIE_STATUS = '1';
Expand Down
45 changes: 36 additions & 9 deletions src/agents/core/BaseAgentAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,7 @@ export abstract class BaseAgentAdapter implements AgentAdapter {
branch: branch || undefined,
project: env.CODEMIE_PROJECT || undefined,
syncApiUrl: env.CODEMIE_SYNC_API_URL || undefined,
syncCodeMieUrl: env.CODEMIE_URL || undefined
syncCodeMieUrl: env.CODEMIE_URL || undefined,
};
}

Expand Down Expand Up @@ -1159,6 +1159,37 @@ export abstract class BaseAgentAdapter implements AgentAdapter {
}
}

// Pin the subagent default tier — but only when doing so cannot change what
// `model: inherit` resolves to.
//
// Upstream Claude Code reads CLAUDE_CODE_SUBAGENT_MODEL *before* both the agent's
// frontmatter `model` and the Agent tool's `model` parameter, and a value other than the
// literal "inherit" short-circuits every later branch. `inherit` is the default for
// subagents that declare no model at all, so an unconditional pin silently forces every
// subagent onto the pinned tier even when the session runs a completely different model
// (a router alias such as `claude-smart-router`, for example).
//
// Pinning is still needed on tenants with no distinct sonnet tier: an explicit
// `model: "sonnet"` would otherwise resolve to the upstream built-in sonnet ID, which
// such a tenant cannot serve. So pin when the fallback already *is* the session model —
// the common single-tier case, where the pin is a no-op for `inherit` — and skip it
// otherwise, letting `inherit` follow the session model as declared (EPMCDME-14355).
const pinSubagentDefault = (fallbackModel: string, tierLabel: string): void => {
if (!envMapping.subagentDefaultModel?.length) return;
if (env.CODEMIE_MODEL && env.CODEMIE_MODEL !== fallbackModel) {
logger.debug(
`[${this.metadata.name}] Session model differs from the only provisioned subagent tier ` +
`(${tierLabel}); leaving the subagent default unpinned so agents declaring ` +
`\`model: inherit\` follow the session model. Subagents that explicitly request an ` +
`unprovisioned tier may fail.`
);
return;
}
for (const envVar of envMapping.subagentDefaultModel) {
env[envVar] = fallbackModel;
}
};

// Transform model tiers (haiku/sonnet/opus)
// Note: All tier vars were already cleared in Step 1 above
if (env.CODEMIE_HAIKU_MODEL && envMapping.haikuModel) {
Expand All @@ -1175,21 +1206,17 @@ export abstract class BaseAgentAdapter implements AgentAdapter {
for (const envVar of envMapping.sonnetModel) {
env[envVar] = env.CODEMIE_SONNET_MODEL;
}
} else if ((!env.CODEMIE_SONNET_MODEL || env.CODEMIE_SONNET_MODEL === env.CODEMIE_HAIKU_MODEL) && env.CODEMIE_OPUS_MODEL && envMapping.subagentDefaultModel?.length) {
} else if ((!env.CODEMIE_SONNET_MODEL || env.CODEMIE_SONNET_MODEL === env.CODEMIE_HAIKU_MODEL) && env.CODEMIE_OPUS_MODEL) {
// No distinct sonnet tier, opus provisioned: route subagent default to opus so the
// upstream binary does not try an unavailable sonnet-tier model for subagent tasks.
// ANTHROPIC_DEFAULT_SONNET_MODEL is intentionally left unset to prevent duplicate-ID
// display in /model (EPMCDME-12779).
for (const envVar of envMapping.subagentDefaultModel) {
env[envVar] = env.CODEMIE_OPUS_MODEL;
}
} else if ((!env.CODEMIE_SONNET_MODEL || env.CODEMIE_SONNET_MODEL === env.CODEMIE_HAIKU_MODEL) && !env.CODEMIE_OPUS_MODEL && env.CODEMIE_HAIKU_MODEL && envMapping.subagentDefaultModel?.length) {
pinSubagentDefault(env.CODEMIE_OPUS_MODEL, 'opus');
} else if ((!env.CODEMIE_SONNET_MODEL || env.CODEMIE_SONNET_MODEL === env.CODEMIE_HAIKU_MODEL) && !env.CODEMIE_OPUS_MODEL && env.CODEMIE_HAIKU_MODEL) {
// Haiku-only tenant: route subagent default to haiku so the upstream binary does not
// try an unavailable sonnet-tier model. ANTHROPIC_DEFAULT_SONNET_MODEL is intentionally
// left unset to prevent duplicate-ID display in /model (EPMCDME-12779).
for (const envVar of envMapping.subagentDefaultModel) {
env[envVar] = env.CODEMIE_HAIKU_MODEL;
}
pinSubagentDefault(env.CODEMIE_HAIKU_MODEL, 'haiku');
}
if (env.CODEMIE_OPUS_MODEL && envMapping.opusModel) {
for (const envVar of envMapping.opusModel) {
Expand Down
7 changes: 5 additions & 2 deletions src/agents/core/__tests__/AgentCLI-model-source.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('AgentCLI.handleRun — CODEMIE_MODEL_SOURCE propagation', () => {
);
});

it('does not set CODEMIE_MODEL_SOURCE when --model is not passed (implicit/profile-sourced model)', async () => {
it('sets CODEMIE_MODEL_SOURCE=default when --model is not passed (implicit/profile-sourced model)', async () => {
mockHandleRunDependencies('claude-sonnet-5[1m]');
const run = vi.fn().mockResolvedValue(undefined);
const cli = new AgentCLI(createAdapter({ run })) as unknown as {
Expand All @@ -100,8 +100,11 @@ describe('AgentCLI.handleRun — CODEMIE_MODEL_SOURCE propagation', () => {
// No `model` in options — same as launching without --model, interactive or not.
await cli.handleRun([], {});

// Distinct from 'cli'/'env': codex-models.ts's isExplicitModelChoice() treats only those
// two as an explicit user choice, so 'default' must stay a real, distinguishable value
// rather than the field being merely present-or-absent.
const [, env] = run.mock.calls[0] as [string[], Record<string, unknown>, unknown];
expect(env.CODEMIE_MODEL_SOURCE).toBeUndefined();
expect(env.CODEMIE_MODEL_SOURCE).toBe('default');
});

it('also propagates CODEMIE_MODEL_SOURCE=cli in interactive mode (no --task) — no regression', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,14 @@ vi.mock('../../../../utils/security.js', () => ({
// missing native vars from the live catalog. Returning null means "no change" —
// the input env values pass through unmodified, which lets each test control the
// final tier landscape by seeding ANTHROPIC_DEFAULT_* directly in the env.
// listRouterModelIds/buildModelLabelMap are the router-id-list and label-map build beforeRun
// runs once after that loop (see CODEMIE_ROUTER_MODEL_IDS/CODEMIE_MODEL_LABELS) — irrelevant to
// this AC-6 tier-warning suite, so both are stubbed to their real functions' own "nothing to
// report" defaults.
vi.mock('../claude.models.js', () => ({
resolveClaudeModel: vi.fn(async () => null),
listRouterModelIds: vi.fn(async () => []),
buildModelLabelMap: vi.fn(async () => ({})),
}));

type HookEnv = NodeJS.ProcessEnv;
Expand Down
Loading
Loading