codiedev 0.9.56 → 0.9.58
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/assets/tui-brand-plugin.tsx +61 -0
- package/dist/codie.js +47 -27
- package/dist/codieBrandInstall.d.ts +11 -0
- package/dist/codieBrandInstall.js +98 -0
- package/dist/codieLauncher.d.ts +33 -0
- package/dist/codieLauncher.js +77 -9
- package/dist/codieRuntime.d.ts +17 -1
- package/dist/codieRuntime.js +22 -4
- package/dist/codieRuntimeInstall.d.ts +39 -10
- package/dist/codieRuntimeInstall.js +124 -5
- package/dist/codieUsage.d.ts +57 -0
- package/dist/codieUsage.js +184 -0
- package/package.json +1 -1
|
@@ -4,11 +4,16 @@
|
|
|
4
4
|
// over the product name, with a "powered by opencode" credit line beneath.
|
|
5
5
|
// Installed and kept current by `codie`; deleting this file and its tui.json
|
|
6
6
|
// entry restores the stock logo.
|
|
7
|
+
// Also paints the governed status line beside the prompt: who is spending,
|
|
8
|
+
// how much of which budget, and what is in flight. The numbers and the
|
|
9
|
+
// wording come from the codiedev package's tested usage module, whose
|
|
10
|
+
// absolute path `codie` bakes in when it stages this file.
|
|
7
11
|
// Mark ported from the retired Grok-fork TUI (assets/logo/logo07.txt), itself
|
|
8
12
|
// derived from vm-demo/public/codiedev-icon.png — keep aligned to that
|
|
9
13
|
// intertwined c/d silhouette when refreshing brand assets.
|
|
10
14
|
import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
|
|
11
15
|
import { TextAttributes } from "@opentui/core"
|
|
16
|
+
import { createSignal } from "solid-js"
|
|
12
17
|
|
|
13
18
|
const MARK = [
|
|
14
19
|
"⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⣿⣿",
|
|
@@ -20,6 +25,56 @@ const MARK = [
|
|
|
20
25
|
"⠀⠈⠻⢿⣿⣿⣿⠿⠋⠀⠀⠙⠿⣿⣿⣿⡿⠟⠁",
|
|
21
26
|
]
|
|
22
27
|
|
|
28
|
+
// One poller per TUI process, shared by every slot that shows the line. A
|
|
29
|
+
// failure anywhere here costs the status line and nothing else: the home
|
|
30
|
+
// screen still brands, and the developer still codes.
|
|
31
|
+
const REFRESH_MS = 30_000
|
|
32
|
+
|
|
33
|
+
const [usageLine, setUsageLine] = createSignal("")
|
|
34
|
+
|
|
35
|
+
async function startUsagePolling() {
|
|
36
|
+
let usage: any
|
|
37
|
+
try {
|
|
38
|
+
// Imported here, not at the top of the file. This plugin is copied
|
|
39
|
+
// outside the package, so its resolution is the engine loader's business:
|
|
40
|
+
// a top-level import that fails takes the whole module with it, and the
|
|
41
|
+
// developer silently loses the brand mark that worked before. solid-js
|
|
42
|
+
// stays a static import because @opentui/solid is already this file's JSX
|
|
43
|
+
// runtime — if Solid were missing the logo would be gone regardless.
|
|
44
|
+
const { createRequire } = await import("module")
|
|
45
|
+
usage = createRequire(import.meta.url)(__CODIEDEV_USAGE_MODULE__)
|
|
46
|
+
} catch {
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
const key = process.env.CODIEDEV_GATEWAY_KEY
|
|
50
|
+
if (!key || typeof usage?.fetchCodieUsage !== "function") return
|
|
51
|
+
const poll = async () => {
|
|
52
|
+
try {
|
|
53
|
+
const snapshot = await usage.fetchCodieUsage({
|
|
54
|
+
baseUrl: process.env.CODIEDEV_GATEWAY_URL,
|
|
55
|
+
key,
|
|
56
|
+
})
|
|
57
|
+
// A failed poll leaves the last good line up rather than blanking it:
|
|
58
|
+
// a number that stopped updating beats a number that vanished.
|
|
59
|
+
if (snapshot) setUsageLine(usage.formatUsageLine(snapshot))
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
void poll()
|
|
63
|
+
const timer = setInterval(poll, REFRESH_MS)
|
|
64
|
+
// Never hold the process open for a status poll.
|
|
65
|
+
;(timer as any).unref?.()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
void startUsagePolling().catch(() => {})
|
|
69
|
+
|
|
70
|
+
function StatusLine(props: { theme: any }) {
|
|
71
|
+
return (
|
|
72
|
+
<text fg={props.theme.textMuted} selectable={false}>
|
|
73
|
+
{usageLine()}
|
|
74
|
+
</text>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
const tui: TuiPlugin = async (api) => {
|
|
24
79
|
api.slots.register({
|
|
25
80
|
slots: {
|
|
@@ -51,6 +106,12 @@ const tui: TuiPlugin = async (api) => {
|
|
|
51
106
|
</box>
|
|
52
107
|
)
|
|
53
108
|
},
|
|
109
|
+
home_prompt_right(ctx) {
|
|
110
|
+
return <StatusLine theme={ctx.theme.current} />
|
|
111
|
+
},
|
|
112
|
+
session_prompt_right(ctx) {
|
|
113
|
+
return <StatusLine theme={ctx.theme.current} />
|
|
114
|
+
},
|
|
54
115
|
},
|
|
55
116
|
})
|
|
56
117
|
}
|
package/dist/codie.js
CHANGED
|
@@ -51,9 +51,14 @@ const child_process_1 = require("child_process");
|
|
|
51
51
|
const url_1 = require("url");
|
|
52
52
|
const configDir_1 = require("./configDir");
|
|
53
53
|
const codieLauncher_1 = require("./codieLauncher");
|
|
54
|
+
const codieBrandInstall_1 = require("./codieBrandInstall");
|
|
55
|
+
const codieUsage_1 = require("./codieUsage");
|
|
54
56
|
const codieRuntime_1 = require("./codieRuntime");
|
|
55
57
|
const codieRuntimeInstall_1 = require("./codieRuntimeInstall");
|
|
56
58
|
const upgradeNudge_1 = require("./upgradeNudge");
|
|
59
|
+
// How long to let the gateway finish booking the last request before asking
|
|
60
|
+
// what the session cost.
|
|
61
|
+
const USAGE_SETTLE_MS = 1200;
|
|
57
62
|
function git(args) {
|
|
58
63
|
const result = (0, child_process_1.spawnSync)("git", args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
59
64
|
return result.status === 0 ? result.stdout.trim() : "";
|
|
@@ -96,32 +101,6 @@ function assetPath(name) {
|
|
|
96
101
|
// dist/codie.js → ../assets/<name>; src/codie.ts → ../assets/<name>.
|
|
97
102
|
return path.join(__dirname, "..", "assets", name);
|
|
98
103
|
}
|
|
99
|
-
// Stage the home-screen branding plugin. TUI plugins only load from tui.json
|
|
100
|
-
// in the OpenCode config dir, so the packaged file is copied next to the
|
|
101
|
-
// capture plugin and referenced with a relative spec. Best-effort: any
|
|
102
|
-
// failure (e.g. a hand-edited tui.json with comments) leaves the stock logo.
|
|
103
|
-
function installBrandPlugin() {
|
|
104
|
-
try {
|
|
105
|
-
// OpenCode resolves its config dir per the XDG spec, so a customized
|
|
106
|
-
// XDG_CONFIG_HOME must be honored or tui.json lands where it never reads.
|
|
107
|
-
const configDir = path.join(process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config"), "opencode");
|
|
108
|
-
const pluginDir = path.join(configDir, "plugins");
|
|
109
|
-
fs.mkdirSync(pluginDir, { recursive: true });
|
|
110
|
-
const source = fs.readFileSync(assetPath("tui-brand-plugin.tsx"), "utf8");
|
|
111
|
-
const destination = path.join(pluginDir, "codiedev-brand.tsx");
|
|
112
|
-
if (!fs.existsSync(destination) || fs.readFileSync(destination, "utf8") !== source) {
|
|
113
|
-
fs.writeFileSync(destination, source);
|
|
114
|
-
}
|
|
115
|
-
const tuiConfigPath = path.join(configDir, "tui.json");
|
|
116
|
-
const existing = fs.existsSync(tuiConfigPath) ? JSON.parse(fs.readFileSync(tuiConfigPath, "utf8")) : {};
|
|
117
|
-
const { config, changed } = (0, codieLauncher_1.withBrandPlugin)(existing);
|
|
118
|
-
if (changed)
|
|
119
|
-
fs.writeFileSync(tuiConfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
console.error(`codie: continuing without Codiedev branding (${error instanceof Error ? error.message : error})`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
104
|
// Remove the engine's boot/exit banner wordmark in place (see
|
|
126
105
|
// patchBrandWordmark). Best-effort like the TUI plugin: any failure rolls the
|
|
127
106
|
// bytes back and the stock banner keeps working. Runs once per install; an
|
|
@@ -209,12 +188,26 @@ async function main() {
|
|
|
209
188
|
return null;
|
|
210
189
|
}
|
|
211
190
|
},
|
|
191
|
+
isEngine: codieRuntimeInstall_1.isEngineOnDisk,
|
|
212
192
|
}),
|
|
213
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
|
+
},
|
|
214
207
|
});
|
|
215
208
|
const metadataPlugin = (0, url_1.pathToFileURL)(assetPath("opencode-plugin.mjs")).href;
|
|
216
209
|
const capturePlugin = (0, codieLauncher_1.resolveCapturePlugin)({ homeDir: os.homedir(), existsSync: fs.existsSync });
|
|
217
|
-
|
|
210
|
+
(0, codieBrandInstall_1.installTuiPlugin)();
|
|
218
211
|
installBrandWordmark(runtime);
|
|
219
212
|
const config = (0, codieLauncher_1.buildCodieConfig)({
|
|
220
213
|
existing: existingInlineConfig(),
|
|
@@ -232,6 +225,33 @@ async function main() {
|
|
|
232
225
|
OPENCODE_CONFIG_CONTENT: JSON.stringify(config),
|
|
233
226
|
};
|
|
234
227
|
const runtimeArgs = (0, codieLauncher_1.withCodieWorkingDirectory)(parsed.args, process.cwd());
|
|
228
|
+
const nonInteractiveRun = runtimeArgs[0] === "run";
|
|
229
|
+
if (nonInteractiveRun) {
|
|
230
|
+
// The TUI paints its own live status line from the same endpoint. A
|
|
231
|
+
// non-interactive run has no TUI, so the budget arrives as two printed
|
|
232
|
+
// lines instead — and only for a human at a terminal, so a scripted run
|
|
233
|
+
// is neither slowed nor polluted.
|
|
234
|
+
const reportCost = Boolean(process.stderr.isTTY);
|
|
235
|
+
const startedAt = Date.now();
|
|
236
|
+
if (reportCost) {
|
|
237
|
+
const line = (0, codieUsage_1.formatUsageLine)(await (0, codieUsage_1.fetchCodieUsage)({ baseUrl: gatewayUrl, key }));
|
|
238
|
+
if (line)
|
|
239
|
+
console.error(line);
|
|
240
|
+
}
|
|
241
|
+
const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, { stdio: "inherit", env });
|
|
242
|
+
if (result.error)
|
|
243
|
+
throw result.error;
|
|
244
|
+
process.exitCode = result.status === null ? 1 : result.status;
|
|
245
|
+
if (reportCost) {
|
|
246
|
+
// The gateway books a request's cost after it has answered, so a poll
|
|
247
|
+
// fired the instant the run closes can miss the last one.
|
|
248
|
+
await new Promise((resolve) => setTimeout(resolve, USAGE_SETTLE_MS));
|
|
249
|
+
const cost = (0, codieUsage_1.formatSessionCostLine)(await (0, codieUsage_1.fetchCodieUsage)({ baseUrl: gatewayUrl, key, sinceMs: startedAt }));
|
|
250
|
+
if (cost)
|
|
251
|
+
console.error(cost);
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
235
255
|
const result = (0, child_process_1.spawnSync)(runtime, runtimeArgs, { stdio: "inherit", env });
|
|
236
256
|
if (result.error)
|
|
237
257
|
throw result.error;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage module this install should load. The shipped package has the
|
|
3
|
+
* compiled file; a source checkout (and the test run) has the TypeScript one.
|
|
4
|
+
* Naming a path that does not exist would cost the status line silently, so a
|
|
5
|
+
* miss raises instead and the caller falls back to the plain plugin.
|
|
6
|
+
*/
|
|
7
|
+
export declare function usageModulePath(): string;
|
|
8
|
+
/** The engine resolves its config dir per the XDG spec, so a customized
|
|
9
|
+
* XDG_CONFIG_HOME must be honored or tui.json lands where it never reads. */
|
|
10
|
+
export declare function openCodeConfigDir(): string;
|
|
11
|
+
export declare function installTuiPlugin(): void;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Stages the Codiedev TUI plugin into the engine's config directory.
|
|
3
|
+
//
|
|
4
|
+
// TUI plugins load only from tui.json in the OpenCode config dir, so the
|
|
5
|
+
// packaged template is copied there and referenced by a relative spec. The
|
|
6
|
+
// copy also carries the status line, which needs an absolute path back into
|
|
7
|
+
// this package — the plugin sits outside it and cannot reach it any other way.
|
|
8
|
+
//
|
|
9
|
+
// Every step is best-effort: a developer who cannot write this directory still
|
|
10
|
+
// gets a working harness, just the stock logo and no status line.
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.usageModulePath = usageModulePath;
|
|
46
|
+
exports.openCodeConfigDir = openCodeConfigDir;
|
|
47
|
+
exports.installTuiPlugin = installTuiPlugin;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const os = __importStar(require("os"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
const codieLauncher_1 = require("./codieLauncher");
|
|
52
|
+
function assetPath(name) {
|
|
53
|
+
// dist/codieBrandInstall.js → ../assets/<name>; src/… → ../assets/<name>.
|
|
54
|
+
return path.join(__dirname, "..", "assets", name);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The usage module this install should load. The shipped package has the
|
|
58
|
+
* compiled file; a source checkout (and the test run) has the TypeScript one.
|
|
59
|
+
* Naming a path that does not exist would cost the status line silently, so a
|
|
60
|
+
* miss raises instead and the caller falls back to the plain plugin.
|
|
61
|
+
*/
|
|
62
|
+
function usageModulePath() {
|
|
63
|
+
const candidates = [
|
|
64
|
+
path.join(__dirname, "codieUsage.js"),
|
|
65
|
+
path.join(__dirname, "codieUsage.ts"),
|
|
66
|
+
];
|
|
67
|
+
const found = candidates.find((candidate) => fs.existsSync(candidate));
|
|
68
|
+
if (!found)
|
|
69
|
+
throw new Error("this install has no Codiedev usage module");
|
|
70
|
+
return found;
|
|
71
|
+
}
|
|
72
|
+
/** The engine resolves its config dir per the XDG spec, so a customized
|
|
73
|
+
* XDG_CONFIG_HOME must be honored or tui.json lands where it never reads. */
|
|
74
|
+
function openCodeConfigDir() {
|
|
75
|
+
return path.join(process.env.XDG_CONFIG_HOME?.trim() || path.join(os.homedir(), ".config"), "opencode");
|
|
76
|
+
}
|
|
77
|
+
function installTuiPlugin() {
|
|
78
|
+
try {
|
|
79
|
+
const configDir = openCodeConfigDir();
|
|
80
|
+
const pluginDir = path.join(configDir, "plugins");
|
|
81
|
+
fs.mkdirSync(pluginDir, { recursive: true });
|
|
82
|
+
const source = (0, codieLauncher_1.stageUsageModule)(fs.readFileSync(assetPath("tui-brand-plugin.tsx"), "utf8"), usageModulePath());
|
|
83
|
+
const destination = path.join(pluginDir, path.basename(codieLauncher_1.BRAND_PLUGIN_SPEC));
|
|
84
|
+
if (!fs.existsSync(destination) || fs.readFileSync(destination, "utf8") !== source) {
|
|
85
|
+
fs.writeFileSync(destination, source);
|
|
86
|
+
}
|
|
87
|
+
const tuiConfigPath = path.join(configDir, "tui.json");
|
|
88
|
+
const existing = fs.existsSync(tuiConfigPath)
|
|
89
|
+
? JSON.parse(fs.readFileSync(tuiConfigPath, "utf8"))
|
|
90
|
+
: {};
|
|
91
|
+
const { config, changed } = (0, codieLauncher_1.withBrandPlugin)(existing);
|
|
92
|
+
if (changed)
|
|
93
|
+
fs.writeFileSync(tuiConfigPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(`codie: continuing without Codiedev branding (${error instanceof Error ? error.message : error})`);
|
|
97
|
+
}
|
|
98
|
+
}
|
package/dist/codieLauncher.d.ts
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
|
+
export interface ModelCost {
|
|
2
|
+
/** Dollars per million prompt tokens. */
|
|
3
|
+
input: number;
|
|
4
|
+
/** Dollars per million completion tokens. */
|
|
5
|
+
output: number;
|
|
6
|
+
}
|
|
7
|
+
export interface ModelLimit {
|
|
8
|
+
/** Maximum prompt tokens the model accepts. */
|
|
9
|
+
context: number;
|
|
10
|
+
/** Maximum completion tokens the model emits. */
|
|
11
|
+
output: number;
|
|
12
|
+
}
|
|
1
13
|
export interface CatalogModel {
|
|
2
14
|
id: string;
|
|
3
15
|
name?: string;
|
|
16
|
+
cost?: ModelCost;
|
|
17
|
+
limit?: ModelLimit;
|
|
4
18
|
}
|
|
19
|
+
/** A rate card is usable only when both sides are real, finite and not
|
|
20
|
+
* negative. Half a card prices a session wrong in a way nobody can see, so a
|
|
21
|
+
* partial one is dropped and the model stays unpriced. */
|
|
22
|
+
export declare function usableModelCost(value: unknown): ModelCost | undefined;
|
|
23
|
+
/** A context/output window is usable only when both sides are real, finite and
|
|
24
|
+
* positive. A partial window makes the engine's "% of context used" read wrong
|
|
25
|
+
* in a way nobody can see, so a partial one is dropped and the model keeps the
|
|
26
|
+
* engine's default. */
|
|
27
|
+
export declare function usableModelLimit(value: unknown): ModelLimit | undefined;
|
|
5
28
|
/**
|
|
6
29
|
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
7
30
|
* OpenCode can reuse a previously-known project instance; without an explicit
|
|
@@ -17,6 +40,16 @@ export declare function buildCodieConfig({ existing, catalog, env, plugin, plugi
|
|
|
17
40
|
plugin?: string;
|
|
18
41
|
plugins?: string[];
|
|
19
42
|
}): Record<string, any>;
|
|
43
|
+
export declare const USAGE_MODULE_PLACEHOLDER = "__CODIEDEV_USAGE_MODULE__";
|
|
44
|
+
/**
|
|
45
|
+
* Point the staged TUI plugin at this install's usage module.
|
|
46
|
+
*
|
|
47
|
+
* The plugin is copied into the engine's own config directory, so it cannot
|
|
48
|
+
* reach the package by a relative path — the absolute path is baked in at
|
|
49
|
+
* stage time. It goes in as a JSON literal so a Windows path keeps its
|
|
50
|
+
* backslashes instead of turning into escape sequences.
|
|
51
|
+
*/
|
|
52
|
+
export declare function stageUsageModule(source: string, modulePath: string): string;
|
|
20
53
|
export declare const BRAND_PLUGIN_SPEC = "./plugins/codiedev-brand.tsx";
|
|
21
54
|
export declare function withBrandPlugin(existing: Record<string, any> | undefined): {
|
|
22
55
|
config: Record<string, any>;
|
package/dist/codieLauncher.js
CHANGED
|
@@ -42,10 +42,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
42
42
|
};
|
|
43
43
|
})();
|
|
44
44
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
-
exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = void 0;
|
|
45
|
+
exports.HIDDEN_WORDMARK = exports.CODIEDEV_WORDMARK = exports.LEGACY_CODIEDEV_WORDMARK = exports.STOCK_WORDMARK = exports.BRAND_PLUGIN_SPEC = exports.USAGE_MODULE_PLACEHOLDER = void 0;
|
|
46
|
+
exports.usableModelCost = usableModelCost;
|
|
47
|
+
exports.usableModelLimit = usableModelLimit;
|
|
46
48
|
exports.withCodieWorkingDirectory = withCodieWorkingDirectory;
|
|
47
49
|
exports.bareModel = bareModel;
|
|
48
50
|
exports.buildCodieConfig = buildCodieConfig;
|
|
51
|
+
exports.stageUsageModule = stageUsageModule;
|
|
49
52
|
exports.withBrandPlugin = withBrandPlugin;
|
|
50
53
|
exports.patchBrandWordmark = patchBrandWordmark;
|
|
51
54
|
exports.resolveCapturePlugin = resolveCapturePlugin;
|
|
@@ -62,6 +65,37 @@ const DEFAULT_NAMES = {
|
|
|
62
65
|
"z-ai/glm-5.2": "GLM 5.2 · Fireworks ZDR",
|
|
63
66
|
"moonshotai/kimi-k2.7-code": "Kimi K2.7 Code · Fireworks ZDR",
|
|
64
67
|
};
|
|
68
|
+
/** A rate card is usable only when both sides are real, finite and not
|
|
69
|
+
* negative. Half a card prices a session wrong in a way nobody can see, so a
|
|
70
|
+
* partial one is dropped and the model stays unpriced. */
|
|
71
|
+
function usableModelCost(value) {
|
|
72
|
+
if (!value || typeof value !== "object")
|
|
73
|
+
return undefined;
|
|
74
|
+
const { input, output } = value;
|
|
75
|
+
if (typeof input !== "number" || typeof output !== "number")
|
|
76
|
+
return undefined;
|
|
77
|
+
if (!Number.isFinite(input) || !Number.isFinite(output))
|
|
78
|
+
return undefined;
|
|
79
|
+
if (input < 0 || output < 0)
|
|
80
|
+
return undefined;
|
|
81
|
+
return { input, output };
|
|
82
|
+
}
|
|
83
|
+
/** A context/output window is usable only when both sides are real, finite and
|
|
84
|
+
* positive. A partial window makes the engine's "% of context used" read wrong
|
|
85
|
+
* in a way nobody can see, so a partial one is dropped and the model keeps the
|
|
86
|
+
* engine's default. */
|
|
87
|
+
function usableModelLimit(value) {
|
|
88
|
+
if (!value || typeof value !== "object")
|
|
89
|
+
return undefined;
|
|
90
|
+
const { context, output } = value;
|
|
91
|
+
if (typeof context !== "number" || typeof output !== "number")
|
|
92
|
+
return undefined;
|
|
93
|
+
if (!Number.isFinite(context) || !Number.isFinite(output))
|
|
94
|
+
return undefined;
|
|
95
|
+
if (context <= 0 || output <= 0)
|
|
96
|
+
return undefined;
|
|
97
|
+
return { context, output };
|
|
98
|
+
}
|
|
65
99
|
/**
|
|
66
100
|
* Pin non-interactive runs to the directory where `codie` was launched.
|
|
67
101
|
* OpenCode can reuse a previously-known project instance; without an explicit
|
|
@@ -121,10 +155,23 @@ function buildCodieConfig({ existing = {}, catalog, env = {}, plugin, plugins =
|
|
|
121
155
|
const planModel = governedModel(requestedPlan, approved, fast, "Plan agent");
|
|
122
156
|
const existingPlugins = Array.isArray(existing.plugin) ? existing.plugin : [];
|
|
123
157
|
const requiredPlugins = [...plugins, plugin].filter(Boolean);
|
|
124
|
-
const models = Object.fromEntries(catalog.map((model) =>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
const models = Object.fromEntries(catalog.map((model) => {
|
|
159
|
+
// The engine prices a session from this rate card. Without it every
|
|
160
|
+
// model reads $0.00, which is how the harness came to show no spend at
|
|
161
|
+
// all. The gateway publishes prompt and completion prices only, so
|
|
162
|
+
// cached tokens are priced at zero here and the engine's session figure
|
|
163
|
+
// stays an estimate — the budget line from /me/usage is the true spend.
|
|
164
|
+
const cost = usableModelCost(model.cost);
|
|
165
|
+
const limit = usableModelLimit(model.limit);
|
|
166
|
+
return [
|
|
167
|
+
bareModel(model.id),
|
|
168
|
+
{
|
|
169
|
+
name: model.name || DEFAULT_NAMES[bareModel(model.id)] || bareModel(model.id),
|
|
170
|
+
...(cost ? { cost: { ...cost, cache: { read: 0, write: 0 } } } : {}),
|
|
171
|
+
...(limit ? { limit } : {}),
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}));
|
|
128
175
|
return {
|
|
129
176
|
...existing,
|
|
130
177
|
model: governedModel(primary, approved, first, "Primary agent"),
|
|
@@ -152,6 +199,21 @@ function buildCodieConfig({ existing = {}, catalog, env = {}, plugin, plugins =
|
|
|
152
199
|
plugin: [...existingPlugins.filter((item) => !requiredPlugins.includes(item)), ...requiredPlugins],
|
|
153
200
|
};
|
|
154
201
|
}
|
|
202
|
+
exports.USAGE_MODULE_PLACEHOLDER = "__CODIEDEV_USAGE_MODULE__";
|
|
203
|
+
/**
|
|
204
|
+
* Point the staged TUI plugin at this install's usage module.
|
|
205
|
+
*
|
|
206
|
+
* The plugin is copied into the engine's own config directory, so it cannot
|
|
207
|
+
* reach the package by a relative path — the absolute path is baked in at
|
|
208
|
+
* stage time. It goes in as a JSON literal so a Windows path keeps its
|
|
209
|
+
* backslashes instead of turning into escape sequences.
|
|
210
|
+
*/
|
|
211
|
+
function stageUsageModule(source, modulePath) {
|
|
212
|
+
if (!source.includes(exports.USAGE_MODULE_PLACEHOLDER)) {
|
|
213
|
+
throw new Error(`plugin template is missing the ${exports.USAGE_MODULE_PLACEHOLDER} placeholder`);
|
|
214
|
+
}
|
|
215
|
+
return source.split(exports.USAGE_MODULE_PLACEHOLDER).join(JSON.stringify(modulePath));
|
|
216
|
+
}
|
|
155
217
|
exports.BRAND_PLUGIN_SPEC = "./plugins/codiedev-brand.tsx";
|
|
156
218
|
// Pure merge for ~/.config/opencode/tui.json: TUI plugins load from tui.json,
|
|
157
219
|
// not the opencode.json we inject via OPENCODE_CONFIG_CONTENT.
|
|
@@ -310,10 +372,16 @@ async function fetchApprovedCatalog({ baseUrl, key, fetchImpl = fetch, }) {
|
|
|
310
372
|
if (!Array.isArray(payload?.data) || payload.data.length === 0) {
|
|
311
373
|
throw new Error("model catalog is empty");
|
|
312
374
|
}
|
|
313
|
-
return payload.data.map((model) =>
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
375
|
+
return payload.data.map((model) => {
|
|
376
|
+
const cost = usableModelCost(model.cost);
|
|
377
|
+
const limit = usableModelLimit(model.limit);
|
|
378
|
+
return {
|
|
379
|
+
id: bareModel(model.id),
|
|
380
|
+
name: typeof model.name === "string" && model.name ? model.name : DEFAULT_NAMES[bareModel(model.id)],
|
|
381
|
+
...(cost ? { cost } : {}),
|
|
382
|
+
...(limit ? { limit } : {}),
|
|
383
|
+
};
|
|
384
|
+
});
|
|
317
385
|
}
|
|
318
386
|
finally {
|
|
319
387
|
clearTimeout(timeout);
|
package/dist/codieRuntime.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/codieRuntime.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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() {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface CodieUsageBudget {
|
|
2
|
+
companyBudgetUsd: number | null;
|
|
3
|
+
companySpentUsd: number;
|
|
4
|
+
companyHeldUsd: number;
|
|
5
|
+
userBudgetUsd: number | null;
|
|
6
|
+
userSpentUsd: number;
|
|
7
|
+
userHeldUsd: number;
|
|
8
|
+
keyBudgetUsd: number | null;
|
|
9
|
+
keySpentUsd: number;
|
|
10
|
+
keyHeldUsd: number | null;
|
|
11
|
+
}
|
|
12
|
+
export interface CodieUsageInFlight {
|
|
13
|
+
/** Reservations open inside the gateway's hold TTL: real work running now. */
|
|
14
|
+
requests: number;
|
|
15
|
+
heldUsd: number;
|
|
16
|
+
/** Reservations past the TTL, waiting for the sweep. They hold budget
|
|
17
|
+
* without doing work, so they are never counted as live work. */
|
|
18
|
+
staleRequests: number;
|
|
19
|
+
staleHeldUsd: number;
|
|
20
|
+
}
|
|
21
|
+
export interface CodieUsage {
|
|
22
|
+
enabled: boolean;
|
|
23
|
+
user: {
|
|
24
|
+
name: string;
|
|
25
|
+
email: string;
|
|
26
|
+
role: string;
|
|
27
|
+
} | null;
|
|
28
|
+
budget: CodieUsageBudget;
|
|
29
|
+
inFlight: CodieUsageInFlight;
|
|
30
|
+
since?: {
|
|
31
|
+
costUsd: number;
|
|
32
|
+
promptTokens: number;
|
|
33
|
+
completionTokens: number;
|
|
34
|
+
requests: number;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* One line: who is spending, how much, against what, and what is running.
|
|
39
|
+
* Returns "" when there is nothing honest to say.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatUsageLine(usage: CodieUsage | null | undefined): string;
|
|
42
|
+
/**
|
|
43
|
+
* What this session just cost. Returns "" when the session billed nothing,
|
|
44
|
+
* because a developer who spent nothing does not need to be told a number.
|
|
45
|
+
*/
|
|
46
|
+
export declare function formatSessionCostLine(usage: CodieUsage | null | undefined): string;
|
|
47
|
+
/**
|
|
48
|
+
* Read the developer's budget snapshot. Returns null on any refusal, network
|
|
49
|
+
* failure, timeout, or malformed answer — a status poll is never worth an
|
|
50
|
+
* error in front of a developer who asked for none.
|
|
51
|
+
*/
|
|
52
|
+
export declare function fetchCodieUsage({ baseUrl, key, sinceMs, fetchImpl, }: {
|
|
53
|
+
baseUrl?: string;
|
|
54
|
+
key: string;
|
|
55
|
+
sinceMs?: number;
|
|
56
|
+
fetchImpl?: typeof fetch;
|
|
57
|
+
}): Promise<CodieUsage | null>;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The Codie harness status line.
|
|
3
|
+
//
|
|
4
|
+
// The gateway already knows what a developer has spent, what caps them, and
|
|
5
|
+
// what they have in flight. Nothing ever asked it. This module asks, and turns
|
|
6
|
+
// the answer into one line the harness can paint next to the prompt.
|
|
7
|
+
//
|
|
8
|
+
// Every failure here is silent by design: a status poll must never interrupt,
|
|
9
|
+
// delay, or break a coding session. No snapshot means no line.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.formatUsageLine = formatUsageLine;
|
|
12
|
+
exports.formatSessionCostLine = formatSessionCostLine;
|
|
13
|
+
exports.fetchCodieUsage = fetchCodieUsage;
|
|
14
|
+
const USAGE_TIMEOUT_MS = 4000;
|
|
15
|
+
function cleanBaseUrl(value) {
|
|
16
|
+
return String(value || "https://gateway.codiedev.com/api/v1").replace(/\/+$/, "");
|
|
17
|
+
}
|
|
18
|
+
function money(value) {
|
|
19
|
+
return `$${value.toFixed(2)}`;
|
|
20
|
+
}
|
|
21
|
+
/** Budgets are set in whole dollars far more often than not, so $150 reads
|
|
22
|
+
* better than $150.00 next to a spend figure that does need its cents. */
|
|
23
|
+
function budgetMoney(value) {
|
|
24
|
+
return Number.isInteger(value) ? `$${value}` : money(value);
|
|
25
|
+
}
|
|
26
|
+
function percent(spent, budget) {
|
|
27
|
+
if (!(budget > 0))
|
|
28
|
+
return null;
|
|
29
|
+
const share = (spent / budget) * 100;
|
|
30
|
+
// A first request against a large budget rounds to 0%, which reads as
|
|
31
|
+
// "nothing spent". Say "under one percent" instead.
|
|
32
|
+
if (share > 0 && share < 1)
|
|
33
|
+
return "<1%";
|
|
34
|
+
return `${Math.round(share)}%`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* One line: who is spending, how much, against what, and what is running.
|
|
38
|
+
* Returns "" when there is nothing honest to say.
|
|
39
|
+
*/
|
|
40
|
+
function formatUsageLine(usage) {
|
|
41
|
+
if (!usage || !usage.enabled)
|
|
42
|
+
return "";
|
|
43
|
+
const { budget, inFlight } = usage;
|
|
44
|
+
const segments = [];
|
|
45
|
+
if (usage.user?.name)
|
|
46
|
+
segments.push(usage.user.name);
|
|
47
|
+
// The developer's own month-to-date spend is always the headline. Only the
|
|
48
|
+
// budget that actually caps them earns a percentage next to it.
|
|
49
|
+
if (typeof budget.userBudgetUsd === "number") {
|
|
50
|
+
segments.push(`${money(budget.userSpentUsd)} / ${budgetMoney(budget.userBudgetUsd)}`);
|
|
51
|
+
const share = percent(budget.userSpentUsd, budget.userBudgetUsd);
|
|
52
|
+
if (share)
|
|
53
|
+
segments.push(share);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
segments.push(money(budget.userSpentUsd));
|
|
57
|
+
if (typeof budget.companyBudgetUsd === "number") {
|
|
58
|
+
const share = percent(budget.companySpentUsd, budget.companyBudgetUsd);
|
|
59
|
+
// Labelled, because a team percentage beside a personal dollar figure
|
|
60
|
+
// otherwise reads as the developer's own share.
|
|
61
|
+
if (share)
|
|
62
|
+
segments.push(`team ${share}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (inFlight.requests > 0)
|
|
66
|
+
segments.push(`${inFlight.requests} in flight`);
|
|
67
|
+
// Stale holds cap a budget while doing no work. Naming them is the
|
|
68
|
+
// difference between "I am blocked" and "I am blocked by nothing".
|
|
69
|
+
if (inFlight.staleHeldUsd > 0)
|
|
70
|
+
segments.push(`${money(inFlight.staleHeldUsd)} held stale`);
|
|
71
|
+
return segments.join(" · ");
|
|
72
|
+
}
|
|
73
|
+
function tokens(total) {
|
|
74
|
+
// Rounded by arithmetic, not toFixed: 2.55 is not exactly representable, so
|
|
75
|
+
// toFixed(1) rounds it down and the same count can read two ways.
|
|
76
|
+
if (total >= 1_000_000)
|
|
77
|
+
return `${Math.round(total / 100_000) / 10}M`;
|
|
78
|
+
if (total >= 1_000)
|
|
79
|
+
return `${Math.round(total / 1_000)}k`;
|
|
80
|
+
return String(total);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* What this session just cost. Returns "" when the session billed nothing,
|
|
84
|
+
* because a developer who spent nothing does not need to be told a number.
|
|
85
|
+
*/
|
|
86
|
+
function formatSessionCostLine(usage) {
|
|
87
|
+
const since = readSince(usage?.since);
|
|
88
|
+
if (!since || since.requests <= 0)
|
|
89
|
+
return "";
|
|
90
|
+
// A real charge that rounds to $0.00 reads as free. Say it is under a cent
|
|
91
|
+
// instead — the month-to-date figure is where it really lands.
|
|
92
|
+
const cost = since.costUsd > 0 && since.costUsd < 0.005 ? "<$0.01" : money(since.costUsd);
|
|
93
|
+
const requests = `${since.requests} ${since.requests === 1 ? "request" : "requests"}`;
|
|
94
|
+
return `This session: ${cost} · ${requests} · ${tokens(since.promptTokens + since.completionTokens)} tokens`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A usage window is reported only when every field of it is a real number.
|
|
98
|
+
* A partial block is worse than none: it reads as a cheap session, and the
|
|
99
|
+
* arithmetic on a missing field throws out of a call site whose whole
|
|
100
|
+
* contract is that it cannot break the run that just finished.
|
|
101
|
+
*/
|
|
102
|
+
function readSince(value) {
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
104
|
+
return undefined;
|
|
105
|
+
const { costUsd, promptTokens, completionTokens, requests } = value;
|
|
106
|
+
const finite = (candidate) => typeof candidate === "number" && Number.isFinite(candidate);
|
|
107
|
+
if (!finite(costUsd) || !finite(promptTokens) || !finite(completionTokens) || !finite(requests)) {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return { costUsd, promptTokens, completionTokens, requests };
|
|
111
|
+
}
|
|
112
|
+
function readNumber(value, fallback = 0) {
|
|
113
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
114
|
+
}
|
|
115
|
+
function readBudgetCap(value) {
|
|
116
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
117
|
+
}
|
|
118
|
+
function readUser(value) {
|
|
119
|
+
if (!value || typeof value !== "object")
|
|
120
|
+
return null;
|
|
121
|
+
const { name, email, role } = value;
|
|
122
|
+
if (typeof name !== "string" || !name)
|
|
123
|
+
return null;
|
|
124
|
+
return {
|
|
125
|
+
name,
|
|
126
|
+
email: typeof email === "string" ? email : "",
|
|
127
|
+
role: typeof role === "string" ? role : "",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Read the developer's budget snapshot. Returns null on any refusal, network
|
|
132
|
+
* failure, timeout, or malformed answer — a status poll is never worth an
|
|
133
|
+
* error in front of a developer who asked for none.
|
|
134
|
+
*/
|
|
135
|
+
async function fetchCodieUsage({ baseUrl, key, sinceMs, fetchImpl = fetch, }) {
|
|
136
|
+
const controller = new AbortController();
|
|
137
|
+
const timeout = setTimeout(() => controller.abort(), USAGE_TIMEOUT_MS);
|
|
138
|
+
try {
|
|
139
|
+
const query = typeof sinceMs === "number" && sinceMs > 0 ? `?since=${sinceMs}` : "";
|
|
140
|
+
const response = await fetchImpl(`${cleanBaseUrl(baseUrl)}/me/usage${query}`, {
|
|
141
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
142
|
+
signal: controller.signal,
|
|
143
|
+
});
|
|
144
|
+
if (!response.ok)
|
|
145
|
+
return null;
|
|
146
|
+
const payload = (await response.json());
|
|
147
|
+
const budget = payload?.budget;
|
|
148
|
+
if (!budget || typeof budget !== "object")
|
|
149
|
+
return null;
|
|
150
|
+
const raw = budget;
|
|
151
|
+
const flight = (payload?.inFlight ?? {});
|
|
152
|
+
return {
|
|
153
|
+
enabled: payload?.enabled === true,
|
|
154
|
+
user: readUser(payload?.user),
|
|
155
|
+
budget: {
|
|
156
|
+
companyBudgetUsd: readBudgetCap(raw.companyBudgetUsd),
|
|
157
|
+
companySpentUsd: readNumber(raw.companySpentUsd),
|
|
158
|
+
companyHeldUsd: readNumber(raw.companyHeldUsd),
|
|
159
|
+
userBudgetUsd: readBudgetCap(raw.userBudgetUsd),
|
|
160
|
+
userSpentUsd: readNumber(raw.userSpentUsd),
|
|
161
|
+
userHeldUsd: readNumber(raw.userHeldUsd),
|
|
162
|
+
keyBudgetUsd: readBudgetCap(raw.keyBudgetUsd),
|
|
163
|
+
keySpentUsd: readNumber(raw.keySpentUsd),
|
|
164
|
+
keyHeldUsd: typeof raw.keyHeldUsd === "number" ? raw.keyHeldUsd : null,
|
|
165
|
+
},
|
|
166
|
+
inFlight: {
|
|
167
|
+
requests: readNumber(flight.requests),
|
|
168
|
+
heldUsd: readNumber(flight.heldUsd),
|
|
169
|
+
staleRequests: readNumber(flight.staleRequests),
|
|
170
|
+
staleHeldUsd: readNumber(flight.staleHeldUsd),
|
|
171
|
+
},
|
|
172
|
+
...(() => {
|
|
173
|
+
const since = readSince(payload?.since);
|
|
174
|
+
return since ? { since } : {};
|
|
175
|
+
})(),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
clearTimeout(timeout);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codiedev",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.58",
|
|
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",
|