@yhong91/cpac 0.1.25 → 0.1.27

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.
@@ -0,0 +1,59 @@
1
+ import { spawn } from "node:child_process";
2
+ import { apiBase, catalogModelId, fetchCatalog, } from "../config.js";
3
+ import { CPACError } from "../util.js";
4
+ const OPENCODE_OUTPUT_BUDGET = 32_000;
5
+ // Ephemeral takeover: inline a CPA provider via OPENCODE_CONFIG_CONTENT so no
6
+ // opencode config file is touched; the session ends with zero residue.
7
+ export async function runOpencode(config, args, executable = "opencode") {
8
+ const apiKey = process.env[config.api_key_env]?.trim();
9
+ if (!apiKey)
10
+ throw new CPACError(`environment variable ${config.api_key_env} is not set`);
11
+ const catalog = await fetchCatalog(config.cpa_url, apiKey);
12
+ let document;
13
+ try {
14
+ document = JSON.parse(new TextDecoder().decode(catalog.bytes));
15
+ }
16
+ catch {
17
+ throw new CPACError("invalid CPA catalog");
18
+ }
19
+ const models = {};
20
+ for (const row of document.models) {
21
+ const id = catalogModelId(row);
22
+ if (!id)
23
+ continue;
24
+ const window = typeof row.context_window === "number" && row.context_window > 0
25
+ ? Math.floor(row.context_window)
26
+ : 0;
27
+ models[id] =
28
+ window > 0
29
+ ? {
30
+ limit: {
31
+ context: window,
32
+ output: Math.min(OPENCODE_OUTPUT_BUDGET, window),
33
+ },
34
+ }
35
+ : {};
36
+ }
37
+ const content = JSON.stringify({
38
+ provider: {
39
+ cpac: {
40
+ npm: "@ai-sdk/openai-compatible",
41
+ options: { baseURL: apiBase(config.cpa_url), apiKey },
42
+ models,
43
+ },
44
+ },
45
+ });
46
+ const env = {
47
+ ...process.env,
48
+ OPENCODE_CONFIG_CONTENT: content,
49
+ };
50
+ return await new Promise((resolve, reject) => {
51
+ const child = spawn(executable, args, { env, stdio: "inherit" });
52
+ child.once("error", (error) => {
53
+ reject(new CPACError(error instanceof Error && "code" in error && error.code === "ENOENT"
54
+ ? `${executable} not found`
55
+ : `cannot start ${executable}: ${error.message}`));
56
+ });
57
+ child.once("close", (code) => resolve(code ?? 1));
58
+ });
59
+ }
@@ -0,0 +1,32 @@
1
+ import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { CPACError, atomicWrite, ensureCpacBackup, expandUserPath, } from "../util.js";
6
+ export function piExtensionsDir() {
7
+ return join(expandUserPath(process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent")), "extensions");
8
+ }
9
+ export function isPiExtensionInstalled() {
10
+ return existsSync(join(piExtensionsDir(), "cpac.ts"));
11
+ }
12
+ function piTemplatePath() {
13
+ return join(dirname(fileURLToPath(import.meta.url)), "..", "pi-extension.template");
14
+ }
15
+ function piExtensionContent(cpaUrl) {
16
+ return readFileSync(piTemplatePath(), "utf8").replace("__CPA_URL__", cpaUrl);
17
+ }
18
+ export async function installPiExtension(config) {
19
+ const dir = piExtensionsDir();
20
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
21
+ const target = join(dir, "cpac.ts");
22
+ ensureCpacBackup(target);
23
+ atomicWrite(target, Buffer.from(piExtensionContent(config.cpa_url)), 0o644);
24
+ console.log(`Installed Pi extension: ${target}`);
25
+ }
26
+ export async function uninstallPiExtension() {
27
+ const target = join(piExtensionsDir(), "cpac.ts");
28
+ if (!existsSync(target))
29
+ throw new CPACError("Pi extension is not installed");
30
+ unlinkSync(target);
31
+ console.log(`Removed Pi extension: ${target}`);
32
+ }
package/dist/util.js ADDED
@@ -0,0 +1,200 @@
1
+ import { chmodSync, closeSync, copyFileSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, parse } from "node:path";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { Writable } from "node:stream";
6
+ let atomicSequence = 0;
7
+ export class CPACError extends Error {
8
+ }
9
+ export function objectValue(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ export function ensureCpacBackup(target) {
13
+ if (!existsSync(target))
14
+ return;
15
+ const backup = `${target}.cpac-backup`;
16
+ if (existsSync(backup))
17
+ return;
18
+ copyFileSync(target, backup);
19
+ }
20
+ export function expandUserPath(value) {
21
+ if (value === "~")
22
+ return homedir();
23
+ if (value.startsWith("~/") || value.startsWith("~\\"))
24
+ return join(homedir(), value.slice(2));
25
+ return value;
26
+ }
27
+ export async function checkboxPicker(title, items, max) {
28
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
29
+ throw new CPACError("model selection requires an interactive terminal");
30
+ }
31
+ const stdin = process.stdin;
32
+ const out = process.stderr;
33
+ const selected = new Set();
34
+ let cursor = 0;
35
+ const lines = items.length + 1;
36
+ const render = () => {
37
+ out.write(`${title} [space: toggle, up/down: move, enter: confirm, q: cancel, max ${max}]\n`);
38
+ items.forEach((item, index) => {
39
+ const box = selected.has(index) ? "[x]" : "[ ]";
40
+ out.write(`${index === cursor ? ">" : " "} ${box} ${item}\n`);
41
+ });
42
+ };
43
+ const redraw = () => {
44
+ out.write(`\x1b[${lines}F`);
45
+ for (let line = 0; line < lines; line += 1)
46
+ out.write("\x1b[2K\x1b[1E");
47
+ out.write(`\x1b[${lines}F`);
48
+ render();
49
+ };
50
+ return await new Promise((resolvePromise, rejectPromise) => {
51
+ const finish = (error) => {
52
+ stdin.removeListener("data", onData);
53
+ try {
54
+ stdin.setRawMode(false);
55
+ }
56
+ catch {
57
+ // Terminal already gone; nothing to restore.
58
+ }
59
+ stdin.pause();
60
+ out.write("\n");
61
+ if (error)
62
+ rejectPromise(error);
63
+ else
64
+ resolvePromise([...selected].map((index) => items[index]));
65
+ };
66
+ const onData = (chunk) => {
67
+ const key = chunk.toString("utf8");
68
+ if (key === "\x03" || key === "q" || key === "\x1b") {
69
+ finish(new CPACError("model selection cancelled"));
70
+ return;
71
+ }
72
+ if (key === "\r" || key === "\n") {
73
+ if (max === 1 && selected.size === 0) {
74
+ selected.add(cursor);
75
+ }
76
+ finish();
77
+ return;
78
+ }
79
+ if (key === "\x1b[A")
80
+ cursor = (cursor + items.length - 1) % items.length;
81
+ else if (key === "\x1b[B")
82
+ cursor = (cursor + 1) % items.length;
83
+ else if (key === " ") {
84
+ if (max === 1) {
85
+ selected.clear();
86
+ selected.add(cursor);
87
+ finish();
88
+ return;
89
+ }
90
+ if (selected.has(cursor))
91
+ selected.delete(cursor);
92
+ else if (selected.size < max)
93
+ selected.add(cursor);
94
+ }
95
+ redraw();
96
+ };
97
+ stdin.setRawMode(true);
98
+ stdin.resume();
99
+ stdin.on("data", onData);
100
+ render();
101
+ });
102
+ }
103
+ export function tomlString(value) {
104
+ return JSON.stringify(value);
105
+ }
106
+ export function tomlStringArray(values) {
107
+ return `[ ${values.map(tomlString).join(", ")} ]`;
108
+ }
109
+ export function dominantEol(content) {
110
+ const crlf = (content.match(/\r\n/g) ?? []).length;
111
+ if (crlf === 0)
112
+ return "\n";
113
+ const bareLf = (content.match(/\n/g) ?? []).length - crlf;
114
+ return crlf >= bareLf ? "\r\n" : "\n";
115
+ }
116
+ export function atomicWrite(path, data, mode = 0o600) {
117
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
118
+ const temporary = `${path}.cpac.${process.pid}.${Date.now()}.${++atomicSequence}.tmp`;
119
+ let descriptor;
120
+ try {
121
+ descriptor = openSync(temporary, "wx", 0o600);
122
+ writeFileSync(descriptor, data);
123
+ fsyncSync(descriptor);
124
+ closeSync(descriptor);
125
+ descriptor = undefined;
126
+ chmodSync(temporary, mode);
127
+ renameSync(temporary, path);
128
+ }
129
+ catch (error) {
130
+ if (descriptor !== undefined) {
131
+ try {
132
+ closeSync(descriptor);
133
+ }
134
+ catch (closeError) {
135
+ void closeError;
136
+ }
137
+ }
138
+ try {
139
+ unlinkSync(temporary);
140
+ }
141
+ catch (unlinkError) {
142
+ void unlinkError;
143
+ }
144
+ throw error;
145
+ }
146
+ }
147
+ export async function promptSecret(name) {
148
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
149
+ throw new CPACError(`environment variable ${name} is not set; run: export ${name}="..."`);
150
+ }
151
+ process.stderr.write(`Enter ${name}: `);
152
+ const silent = new Writable({
153
+ write(_chunk, _encoding, callback) {
154
+ callback();
155
+ },
156
+ });
157
+ const prompt = createInterface({
158
+ input: process.stdin,
159
+ output: silent,
160
+ terminal: true,
161
+ });
162
+ try {
163
+ const value = (await prompt.question("")).trim();
164
+ process.stderr.write("\n");
165
+ if (!value)
166
+ throw new CPACError(`${name} must not be empty`);
167
+ return value;
168
+ }
169
+ finally {
170
+ prompt.close();
171
+ }
172
+ }
173
+ export async function resolveApiKey(name) {
174
+ return process.env[name]?.trim() || (await promptSecret(name));
175
+ }
176
+ export function shellProfile() {
177
+ const shell = parse(process.env.SHELL || "").base;
178
+ if (shell === "zsh")
179
+ return join(homedir(), ".zshrc");
180
+ if (shell === "bash")
181
+ return join(homedir(), process.platform === "darwin" ? ".bash_profile" : ".bashrc");
182
+ if (["sh", "dash", "ksh"].includes(shell))
183
+ return join(homedir(), ".profile");
184
+ throw new CPACError(`unsupported shell ${shell || "unknown"}; run: export CPA_API_KEY="..."`);
185
+ }
186
+ function shellQuote(value) {
187
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
188
+ }
189
+ export function saveApiKeyExport(profile, name, apiKey) {
190
+ const start = `# >>> CPAC ${name} >>>`;
191
+ const end = `# <<< CPAC ${name} <<<`;
192
+ const block = `${start}\nexport ${name}=${shellQuote(apiKey)}\n${end}`;
193
+ let content = existsSync(profile) ? readFileSync(profile, "utf8") : "";
194
+ const managed = new RegExp(`${start.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${end.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`);
195
+ content = managed.test(content)
196
+ ? content.replace(managed, block)
197
+ : `${content}${content && !content.endsWith("\n") ? "\n" : ""}${block}\n`;
198
+ const mode = existsSync(profile) ? statSync(profile).mode & 0o7777 : 0o600;
199
+ atomicWrite(profile, Buffer.from(content), mode);
200
+ }
package/package.json CHANGED
@@ -1,14 +1,13 @@
1
1
  {
2
2
  "name": "@yhong91/cpac",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Connect Codex and Claude Code to a remote CLIProxyAPI gateway",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cpac": "dist/cpac.js"
8
8
  },
9
9
  "files": [
10
- "dist/cpac.js",
11
- "dist/pi-extension.template",
10
+ "dist",
12
11
  "cpac.example.json",
13
12
  "README.md"
14
13
  ],