codiedev 0.9.57 → 0.9.59

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.
@@ -31,6 +31,7 @@ const MARK = [
31
31
  const REFRESH_MS = 30_000
32
32
 
33
33
  const [usageLine, setUsageLine] = createSignal("")
34
+ const [usageTone, setUsageTone] = createSignal("normal")
34
35
 
35
36
  async function startUsagePolling() {
36
37
  let usage: any
@@ -56,7 +57,11 @@ async function startUsagePolling() {
56
57
  })
57
58
  // A failed poll leaves the last good line up rather than blanking it:
58
59
  // a number that stopped updating beats a number that vanished.
59
- if (snapshot) setUsageLine(usage.formatUsageLine(snapshot))
60
+ if (snapshot) {
61
+ setUsageLine(usage.formatUsageLine(snapshot))
62
+ // Older installs of the usage module have no tone; stay muted there.
63
+ if (typeof usage.usageTone === "function") setUsageTone(usage.usageTone(snapshot))
64
+ }
60
65
  } catch {}
61
66
  }
62
67
  void poll()
@@ -69,7 +74,14 @@ void startUsagePolling().catch(() => {})
69
74
 
70
75
  function StatusLine(props: { theme: any }) {
71
76
  return (
72
- <text fg={props.theme.textMuted} selectable={false}>
77
+ <text
78
+ fg={usageTone() === "critical"
79
+ ? (props.theme.error ?? props.theme.textMuted)
80
+ : usageTone() === "warn"
81
+ ? (props.theme.warning ?? props.theme.textMuted)
82
+ : props.theme.textMuted}
83
+ selectable={false}
84
+ >
73
85
  {usageLine()}
74
86
  </text>
75
87
  )
package/dist/codie.js CHANGED
@@ -188,8 +188,22 @@ async function main() {
188
188
  return null;
189
189
  }
190
190
  },
191
+ isEngine: codieRuntimeInstall_1.isEngineOnDisk,
191
192
  }),
192
193
  install: (missing) => (0, codieRuntimeInstall_1.installCodieRuntimeOnDisk)(missing, stateDir),
194
+ repair: (unlinked) => {
195
+ // npm 12 skips dependency install scripts; codie runs opencode-ai's own
196
+ // link step instead (not when npm is explicitly set to ignore scripts).
197
+ console.error("codie: finishing the engine setup npm skipped (opencode-ai's postinstall)…");
198
+ const linked = (0, codieRuntimeInstall_1.linkEngineOnDisk)(unlinked.packageDir);
199
+ // npm set to ignore scripts: a reinstall would hit the same wall, so stop
200
+ // with the one clear step instead of downloading again every run.
201
+ if (!linked.ok && linked.detail === codieRuntimeInstall_1.IGNORE_SCRIPTS)
202
+ throw new Error((0, codieRuntimeInstall_1.linkFailure)(linked, unlinked.packageDir));
203
+ if (!linked.ok)
204
+ console.error(`codie: ${(0, codieRuntimeInstall_1.linkFailure)(linked, unlinked.packageDir)} Reinstalling the engine.`);
205
+ return linked.ok;
206
+ },
193
207
  });
194
208
  const metadataPlugin = (0, url_1.pathToFileURL)(assetPath("opencode-plugin.mjs")).href;
195
209
  const capturePlugin = (0, codieLauncher_1.resolveCapturePlugin)({ homeDir: os.homedir(), existsSync: fs.existsSync });
@@ -8,12 +8,28 @@ export type CodieRuntime = {
8
8
  installDir: string;
9
9
  binary: string;
10
10
  spec: string;
11
+ } | {
12
+ kind: "unlinked";
13
+ installDir: string;
14
+ binary: string;
15
+ spec: string;
16
+ packageDir: string;
11
17
  };
12
18
  export declare function codieRuntimeDir(stateDir: string): string;
13
- export declare function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }: {
19
+ /**
20
+ * The real engine is a ~132MB native executable. opencode-ai ships
21
+ * bin/opencode.exe as a ~500-byte shell script that only prints "postinstall
22
+ * was not run" until its postinstall swaps the binary in; anything this small
23
+ * is that placeholder, not an engine. Pure.
24
+ */
25
+ export declare const ENGINE_MIN_BYTES = 1000000;
26
+ export declare function isEngineSize(bytes: number): boolean;
27
+ export declare function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, isEngine, }: {
14
28
  env: Record<string, string | undefined>;
15
29
  stateDir: string;
16
30
  existsSync: (candidate: string) => boolean;
17
31
  resolveBundled: () => string | null;
32
+ /** Whether a candidate is a real engine, not opencode-ai's placeholder. */
33
+ isEngine?: (candidate: string) => boolean;
18
34
  }): CodieRuntime;
19
35
  export declare function assertCodieNodeVersion(version: string): void;
@@ -43,8 +43,9 @@ var __importStar = (this && this.__importStar) || (function () {
43
43
  };
44
44
  })();
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
- exports.CODIE_MIN_NODE_MAJOR = exports.OPENCODE_RUNTIME_SPEC = void 0;
46
+ exports.ENGINE_MIN_BYTES = exports.CODIE_MIN_NODE_MAJOR = exports.OPENCODE_RUNTIME_SPEC = void 0;
47
47
  exports.codieRuntimeDir = codieRuntimeDir;
48
+ exports.isEngineSize = isEngineSize;
48
49
  exports.resolveCodieRuntime = resolveCodieRuntime;
49
50
  exports.assertCodieNodeVersion = assertCodieNodeVersion;
50
51
  const path = __importStar(require("path"));
@@ -62,7 +63,20 @@ function codieRuntimeDir(stateDir) {
62
63
  function installedBinary(stateDir) {
63
64
  return path.join(codieRuntimeDir(stateDir), "node_modules", "opencode-ai", "bin", "opencode.exe");
64
65
  }
65
- function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }) {
66
+ /**
67
+ * The real engine is a ~132MB native executable. opencode-ai ships
68
+ * bin/opencode.exe as a ~500-byte shell script that only prints "postinstall
69
+ * was not run" until its postinstall swaps the binary in; anything this small
70
+ * is that placeholder, not an engine. Pure.
71
+ */
72
+ exports.ENGINE_MIN_BYTES = 1_000_000;
73
+ function isEngineSize(bytes) {
74
+ return bytes >= exports.ENGINE_MIN_BYTES;
75
+ }
76
+ function installedPackageDir(stateDir) {
77
+ return path.join(codieRuntimeDir(stateDir), "node_modules", "opencode-ai");
78
+ }
79
+ function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, isEngine = () => true, }) {
66
80
  // 1. Explicit override — the air-gapped / pre-provisioned escape hatch.
67
81
  const override = env.CODIEDEV_OPENCODE_BINARY?.trim();
68
82
  if (override) {
@@ -74,12 +88,16 @@ function resolveCodieRuntime({ env, stateDir, existsSync, resolveBundled, }) {
74
88
  // 2. A bundled engine, if this package was installed with one (dev checkout,
75
89
  // or a future vendored distribution).
76
90
  const bundled = resolveBundled();
77
- if (bundled)
91
+ if (bundled && isEngine(bundled))
78
92
  return { kind: "ready", binary: bundled };
79
93
  // 3. The lazily installed engine.
80
94
  const binary = installedBinary(stateDir);
81
- if (existsSync(binary))
95
+ if (existsSync(binary)) {
96
+ if (!isEngine(binary)) {
97
+ return { kind: "unlinked", installDir: codieRuntimeDir(stateDir), binary, spec: exports.OPENCODE_RUNTIME_SPEC, packageDir: installedPackageDir(stateDir) };
98
+ }
82
99
  return { kind: "ready", binary };
100
+ }
83
101
  return { kind: "missing", installDir: codieRuntimeDir(stateDir), binary, spec: exports.OPENCODE_RUNTIME_SPEC };
84
102
  }
85
103
  function assertCodieNodeVersion(version) {
@@ -1,9 +1,17 @@
1
1
  import { CodieRuntime } from "./codieRuntime";
2
+ type InstallableCodieRuntime = Exclude<CodieRuntime, {
3
+ kind: "ready";
4
+ }>;
5
+ /** Outcome of running opencode-ai's own link step. `detail` is its error. */
6
+ export type LinkResult = {
7
+ ok: boolean;
8
+ detail?: string;
9
+ };
10
+ /** linkEngine's detail when npm is explicitly set not to run install scripts. */
11
+ export declare const IGNORE_SCRIPTS = "ignore-scripts";
2
12
  export declare function buildRuntimeInstallArgs(stagingDir: string, spec: string): string[];
3
13
  export interface InstallDeps {
4
- runtime: Extract<CodieRuntime, {
5
- kind: "missing";
6
- }>;
14
+ runtime: InstallableCodieRuntime;
7
15
  makeStagingDir: () => string;
8
16
  run: (args: string[]) => {
9
17
  status: number | null;
@@ -13,15 +21,36 @@ export interface InstallDeps {
13
21
  rename: (from: string, to: string) => void;
14
22
  remove: (target: string) => void;
15
23
  log: (message: string) => void;
24
+ /** Whether a file is a real engine, not opencode-ai's placeholder script. */
25
+ isEngine?: (candidate: string) => boolean;
26
+ /** Run opencode-ai's postinstall in a package dir. */
27
+ linkEngine?: (packageDir: string) => LinkResult;
16
28
  }
17
29
  export declare function installCodieRuntime(deps: InstallDeps): void;
18
- export declare function ensureCodieRuntimeBinary({ resolve, install, }: {
30
+ export declare function ensureCodieRuntimeBinary({ resolve, install, repair, }: {
19
31
  resolve: () => CodieRuntime;
20
- install: (runtime: Extract<CodieRuntime, {
21
- kind: "missing";
22
- }>) => void;
32
+ install: (runtime: InstallableCodieRuntime) => void;
33
+ /** Link an unlinked install in place (runs its postinstall); true if it's now an engine. */
34
+ repair?: (runtime: Extract<CodieRuntime, {
35
+ kind: "unlinked";
36
+ }>) => boolean;
23
37
  }): string;
24
- export declare function installCodieRuntimeOnDisk(runtime: Extract<CodieRuntime, {
25
- kind: "missing";
26
- }>, stateDir: string): void;
38
+ /** Why an installed engine still isn't runnable, with opencode-ai's own error. Pure. */
39
+ export declare function linkFailure(linked: LinkResult, packageDir?: string): string;
40
+ /** A cmd.exe command line: each argument double-quoted when it has spaces or
41
+ * shell metacharacters (paths like C:\Users\First Last\…). Pure. */
42
+ export declare function windowsCommand(command: string, args: string[]): string;
43
+ /** A real engine on disk (not opencode-ai's placeholder script). */
44
+ export declare function isEngineOnDisk(candidate: string): boolean;
45
+ /** Whether npm is explicitly configured not to run install scripts (an org
46
+ * policy codie shouldn't bypass). npm 12's default script approval isn't this. */
47
+ export declare function npmIgnoresScripts(env?: Record<string, string | undefined>): boolean;
48
+ /** Run opencode-ai's own postinstall, which links the platform binary into
49
+ * bin/opencode.exe (and picks the right build for this CPU/libc). The same
50
+ * step npm would have run; skipped when npm is set to ignore scripts. */
51
+ export declare function linkEngineOnDisk(packageDir: string, { ignoreScripts }?: {
52
+ ignoreScripts?: () => boolean;
53
+ }): LinkResult;
54
+ export declare function installCodieRuntimeOnDisk(runtime: InstallableCodieRuntime, stateDir: string): void;
27
55
  export declare function defaultStagingRoot(): string;
56
+ export {};
@@ -39,9 +39,15 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.IGNORE_SCRIPTS = void 0;
42
43
  exports.buildRuntimeInstallArgs = buildRuntimeInstallArgs;
43
44
  exports.installCodieRuntime = installCodieRuntime;
44
45
  exports.ensureCodieRuntimeBinary = ensureCodieRuntimeBinary;
46
+ exports.linkFailure = linkFailure;
47
+ exports.windowsCommand = windowsCommand;
48
+ exports.isEngineOnDisk = isEngineOnDisk;
49
+ exports.npmIgnoresScripts = npmIgnoresScripts;
50
+ exports.linkEngineOnDisk = linkEngineOnDisk;
45
51
  exports.installCodieRuntimeOnDisk = installCodieRuntimeOnDisk;
46
52
  exports.defaultStagingRoot = defaultStagingRoot;
47
53
  const fs = __importStar(require("fs"));
@@ -49,6 +55,8 @@ const os = __importStar(require("os"));
49
55
  const path = __importStar(require("path"));
50
56
  const child_process_1 = require("child_process");
51
57
  const codieRuntime_1 = require("./codieRuntime");
58
+ /** linkEngine's detail when npm is explicitly set not to run install scripts. */
59
+ exports.IGNORE_SCRIPTS = "ignore-scripts";
52
60
  function buildRuntimeInstallArgs(stagingDir, spec) {
53
61
  // No --registry / --proxy: inherit whatever npm config the developer's
54
62
  // machine already has so an internal mirror keeps working.
@@ -64,7 +72,7 @@ function buildRuntimeInstallArgs(stagingDir, spec) {
64
72
  ];
65
73
  }
66
74
  function installCodieRuntime(deps) {
67
- const { runtime, makeStagingDir, run, exists, rename, remove, log } = deps;
75
+ const { runtime, makeStagingDir, run, exists, rename, remove, log, isEngine = () => true, linkEngine } = deps;
68
76
  const staging = makeStagingDir();
69
77
  log(`Installing the Codie engine (${runtime.spec}). This runs once, and only for \`codie\`.`);
70
78
  let result;
@@ -87,6 +95,35 @@ function installCodieRuntime(deps) {
87
95
  throw new Error(`The Codie engine install did not produce a runtime binary. ` +
88
96
  `Check that ${runtime.spec} is available from your npm registry.`);
89
97
  }
98
+ // The placeholder exists even when postinstall never ran (npm 12 skips
99
+ // dependency install scripts); link it here — what opencode-ai's own error
100
+ // tells you to do — before promoting.
101
+ if (!isEngine(stagedBinary)) {
102
+ let linked = { ok: false };
103
+ try {
104
+ linked = linkEngine?.(path.dirname(path.dirname(stagedBinary))) ?? { ok: false };
105
+ }
106
+ catch (error) {
107
+ linked = { ok: false, detail: error instanceof Error ? error.message : String(error) };
108
+ }
109
+ if (!linked.ok || !isEngine(stagedBinary)) {
110
+ if (linked.detail === exports.IGNORE_SCRIPTS) {
111
+ // npm is set to ignore scripts: keep the install (promote it) so the
112
+ // printed postinstall path exists and the next run is a no-download
113
+ // "unlinked" that stops with the same step, not another download.
114
+ try {
115
+ rename(staging, runtime.installDir);
116
+ }
117
+ finally {
118
+ remove(staging);
119
+ }
120
+ }
121
+ else {
122
+ remove(staging);
123
+ }
124
+ throw new Error(linkFailure(linked, path.join(runtime.installDir, "node_modules", "opencode-ai")));
125
+ }
126
+ }
90
127
  try {
91
128
  rename(staging, runtime.installDir);
92
129
  }
@@ -98,13 +135,33 @@ function installCodieRuntime(deps) {
98
135
  }
99
136
  }
100
137
  // Resolve the engine, downloading it once if this is the first `codie` run.
101
- function ensureCodieRuntimeBinary({ resolve, install, }) {
138
+ function ensureCodieRuntimeBinary({ resolve, install, repair, }) {
102
139
  const runtime = resolve();
103
140
  if (runtime.kind === "ready")
104
141
  return runtime.binary;
142
+ // An install whose postinstall never ran: link it in place (no download).
143
+ // Re-resolve after, so a concurrent repair or a partial link falls through
144
+ // to a reinstall rather than launching a missing file.
145
+ if (runtime.kind === "unlinked" && repair?.(runtime)) {
146
+ const after = resolve();
147
+ if (after.kind === "ready")
148
+ return after.binary;
149
+ }
105
150
  install(runtime);
106
151
  return runtime.binary;
107
152
  }
153
+ /** Why an installed engine still isn't runnable, with opencode-ai's own error. Pure. */
154
+ function linkFailure(linked, packageDir) {
155
+ if (linked.detail === exports.IGNORE_SCRIPTS) {
156
+ const script = packageDir ? `"${path.join(packageDir, "postinstall.mjs")}"` : "postinstall.mjs in the opencode-ai package";
157
+ return (`The Codie engine installed, but npm is set to ignore install scripts (ignore-scripts=true), ` +
158
+ `so codie won't run opencode-ai's setup step. Run \`node ${script}\` yourself, ` +
159
+ `or set CODIEDEV_OPENCODE_BINARY to an opencode binary.`);
160
+ }
161
+ return (`The Codie engine installed, but its setup step (opencode-ai's postinstall) failed, so there is no runnable engine` +
162
+ (linked.detail ? `: ${linked.detail}` : ".") +
163
+ ` Set CODIEDEV_OPENCODE_BINARY to an opencode binary to use a pre-provisioned engine.`);
164
+ }
108
165
  function installFailure(detail) {
109
166
  return new Error(`Could not download the Codie engine: ${detail}\n` +
110
167
  `If you are behind a proxy or an internal registry, make sure npm can reach it. ` +
@@ -112,6 +169,62 @@ function installFailure(detail) {
112
169
  }
113
170
  // ── Real-world wiring ───────────────────────────────────────────────────
114
171
  // Kept separate from the pure logic above so tests never shell out to npm.
172
+ /** A cmd.exe command line: each argument double-quoted when it has spaces or
173
+ * shell metacharacters (paths like C:\Users\First Last\…). Pure. */
174
+ function windowsCommand(command, args) {
175
+ const quote = (a) => (/[\s"&|<>^()%!]/.test(a) ? `"${a.replace(/"/g, '""')}"` : a);
176
+ return [command, ...args.map(quote)].join(" ");
177
+ }
178
+ /** A real engine on disk (not opencode-ai's placeholder script). */
179
+ function isEngineOnDisk(candidate) {
180
+ try {
181
+ return (0, codieRuntime_1.isEngineSize)(fs.statSync(candidate).size);
182
+ }
183
+ catch {
184
+ return false;
185
+ }
186
+ }
187
+ /** Whether npm is explicitly configured not to run install scripts (an org
188
+ * policy codie shouldn't bypass). npm 12's default script approval isn't this. */
189
+ function npmIgnoresScripts(env = process.env) {
190
+ // npm reads either case and treats anything but a false-ish value as true.
191
+ const fromEnv = env.npm_config_ignore_scripts ?? env.NPM_CONFIG_IGNORE_SCRIPTS;
192
+ if (fromEnv !== undefined)
193
+ return !["", "false", "0", "null", "no"].includes(fromEnv.trim().toLowerCase());
194
+ const opts = { encoding: "utf8", timeout: 10_000, windowsHide: true };
195
+ // npm.cmd needs a shell on Windows (Node 20+ refuses to spawn .cmd without
196
+ // one). One constant command string — args plus shell:true is deprecated.
197
+ const result = process.platform === "win32"
198
+ ? (0, child_process_1.spawnSync)("npm.cmd config get ignore-scripts", { ...opts, shell: true })
199
+ : (0, child_process_1.spawnSync)("npm", ["config", "get", "ignore-scripts"], opts);
200
+ return result.status === 0 && (result.stdout || "").trim() === "true";
201
+ }
202
+ const LINK_TIMEOUT_MS = 120_000;
203
+ /** Run opencode-ai's own postinstall, which links the platform binary into
204
+ * bin/opencode.exe (and picks the right build for this CPU/libc). The same
205
+ * step npm would have run; skipped when npm is set to ignore scripts. */
206
+ function linkEngineOnDisk(packageDir, { ignoreScripts = npmIgnoresScripts } = {}) {
207
+ const script = path.join(packageDir, "postinstall.mjs");
208
+ if (!fs.existsSync(script))
209
+ return { ok: false, detail: "opencode-ai has no postinstall.mjs" };
210
+ if (ignoreScripts())
211
+ return { ok: false, detail: exports.IGNORE_SCRIPTS };
212
+ const result = (0, child_process_1.spawnSync)(process.execPath, [script], {
213
+ cwd: packageDir,
214
+ encoding: "utf8",
215
+ stdio: ["ignore", "ignore", "pipe"],
216
+ timeout: LINK_TIMEOUT_MS,
217
+ windowsHide: true,
218
+ });
219
+ if (result.error || result.signal) {
220
+ return { ok: false, detail: result.error ? result.error.message : `timed out after ${LINK_TIMEOUT_MS / 1000}s` };
221
+ }
222
+ if (result.status !== 0)
223
+ return { ok: false, detail: (result.stderr || "").trim().split("\n").slice(-3).join(" ") || `exit ${result.status}` };
224
+ return isEngineOnDisk(path.join(packageDir, "bin", "opencode.exe"))
225
+ ? { ok: true }
226
+ : { ok: false, detail: "postinstall ran but left no engine" };
227
+ }
115
228
  function installCodieRuntimeOnDisk(runtime, stateDir) {
116
229
  installCodieRuntime({
117
230
  runtime,
@@ -121,12 +234,16 @@ function installCodieRuntimeOnDisk(runtime, stateDir) {
121
234
  return fs.mkdtempSync(path.join(path.dirname((0, codieRuntime_1.codieRuntimeDir)(stateDir)), ".runtime-staging-"));
122
235
  },
123
236
  run: (args) => {
124
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
125
- const result = (0, child_process_1.spawnSync)(npm, args, {
237
+ const opts = {
126
238
  encoding: "utf8",
127
239
  stdio: ["ignore", "inherit", "pipe"],
128
240
  env: { ...process.env, npm_config_yes: "true" },
129
- });
241
+ };
242
+ // Windows: Node 20+ refuses to spawn npm.cmd without a shell (EINVAL),
243
+ // which made the engine install fail outright there. One quoted string.
244
+ const result = process.platform === "win32"
245
+ ? (0, child_process_1.spawnSync)(windowsCommand("npm.cmd", args), { ...opts, shell: true, windowsHide: true })
246
+ : (0, child_process_1.spawnSync)("npm", args, opts);
130
247
  if (result.error)
131
248
  throw result.error;
132
249
  return { status: result.status, stderr: result.stderr || "" };
@@ -138,6 +255,8 @@ function installCodieRuntimeOnDisk(runtime, stateDir) {
138
255
  },
139
256
  remove: (target) => fs.rmSync(target, { recursive: true, force: true }),
140
257
  log: (message) => console.error(message),
258
+ isEngine: isEngineOnDisk,
259
+ linkEngine: linkEngineOnDisk,
141
260
  });
142
261
  }
143
262
  function defaultStagingRoot() {
@@ -39,6 +39,13 @@ export interface CodieUsage {
39
39
  * Returns "" when there is nothing honest to say.
40
40
  */
41
41
  export declare function formatUsageLine(usage: CodieUsage | null | undefined): string;
42
+ export type UsageTone = "normal" | "warn" | "critical";
43
+ /**
44
+ * How urgently the status line should read, from the limit that will stop
45
+ * this developer first: their own cap, or the shared pool when they have no
46
+ * cap or the pool runs out sooner. 75% warns, 90% is critical.
47
+ */
48
+ export declare function usageTone(usage: CodieUsage | null | undefined): UsageTone;
42
49
  /**
43
50
  * What this session just cost. Returns "" when the session billed nothing,
44
51
  * because a developer who spent nothing does not need to be told a number.
@@ -9,6 +9,7 @@
9
9
  // delay, or break a coding session. No snapshot means no line.
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
11
  exports.formatUsageLine = formatUsageLine;
12
+ exports.usageTone = usageTone;
12
13
  exports.formatSessionCostLine = formatSessionCostLine;
13
14
  exports.fetchCodieUsage = fetchCodieUsage;
14
15
  const USAGE_TIMEOUT_MS = 4000;
@@ -51,6 +52,16 @@ function formatUsageLine(usage) {
51
52
  const share = percent(budget.userSpentUsd, budget.userBudgetUsd);
52
53
  if (share)
53
54
  segments.push(share);
55
+ // A personal cap does not help once the shared pool is nearly gone: say
56
+ // so when the pool is running low and will stop this developer before
57
+ // their own cap does.
58
+ if (poolBindsFirst(budget) && typeof budget.companyBudgetUsd === "number"
59
+ && budget.companyBudgetUsd > 0
60
+ && budget.companySpentUsd / budget.companyBudgetUsd >= 0.75) {
61
+ const share = percent(budget.companySpentUsd, budget.companyBudgetUsd);
62
+ if (share)
63
+ segments.push(`team pool ${share}`);
64
+ }
54
65
  }
55
66
  else {
56
67
  segments.push(money(budget.userSpentUsd));
@@ -62,6 +73,10 @@ function formatUsageLine(usage) {
62
73
  segments.push(`team ${share}`);
63
74
  }
64
75
  }
76
+ if (typeof budget.companyBudgetUsd === "number" && budget.companyBudgetUsd > 0
77
+ && budget.companySpentUsd >= budget.companyBudgetUsd) {
78
+ segments.push("team pool used up");
79
+ }
65
80
  if (inFlight.requests > 0)
66
81
  segments.push(`${inFlight.requests} in flight`);
67
82
  // Stale holds cap a budget while doing no work. Naming them is the
@@ -70,6 +85,37 @@ function formatUsageLine(usage) {
70
85
  segments.push(`${money(inFlight.staleHeldUsd)} held stale`);
71
86
  return segments.join(" · ");
72
87
  }
88
+ function poolBindsFirst(budget) {
89
+ if (typeof budget.companyBudgetUsd !== "number" || typeof budget.userBudgetUsd !== "number")
90
+ return false;
91
+ const poolLeft = Math.max(0, budget.companyBudgetUsd - budget.companySpentUsd);
92
+ const userLeft = Math.max(0, budget.userBudgetUsd - budget.userSpentUsd);
93
+ return budget.userBudgetUsd > 0 && poolLeft < userLeft;
94
+ }
95
+ /**
96
+ * How urgently the status line should read, from the limit that will stop
97
+ * this developer first: their own cap, or the shared pool when they have no
98
+ * cap or the pool runs out sooner. 75% warns, 90% is critical.
99
+ */
100
+ function usageTone(usage) {
101
+ if (!usage || !usage.enabled)
102
+ return "normal";
103
+ const { budget } = usage;
104
+ const ratios = [];
105
+ if (typeof budget.userBudgetUsd === "number" && budget.userBudgetUsd > 0) {
106
+ ratios.push(budget.userSpentUsd / budget.userBudgetUsd);
107
+ }
108
+ if (typeof budget.companyBudgetUsd === "number" && budget.companyBudgetUsd > 0
109
+ && (typeof budget.userBudgetUsd !== "number" || poolBindsFirst(budget))) {
110
+ ratios.push(budget.companySpentUsd / budget.companyBudgetUsd);
111
+ }
112
+ const worst = ratios.length ? Math.max(...ratios) : 0;
113
+ if (worst >= 0.9)
114
+ return "critical";
115
+ if (worst >= 0.75)
116
+ return "warn";
117
+ return "normal";
118
+ }
73
119
  function tokens(total) {
74
120
  // Rounded by arithmetic, not toFixed: 2.55 is not exactly representable, so
75
121
  // toFixed(1) rounds it down and the same count can read two ways.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codiedev",
3
- "version": "0.9.57",
3
+ "version": "0.9.59",
4
4
  "description": "Connect Claude Code, Codex, Cursor, OpenCode, or VS Code Copilot to Codiedev for team session capture and artifact collaboration — and run the governed Codie harness",
5
5
  "bin": {
6
6
  "codiedev": "dist/cli.js",