pennyrouter 0.2.7 → 0.2.8
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/LICENSE +21 -21
- package/README.md +225 -222
- package/bin/penny.js +9 -9
- package/bin/pennyrouter.js +7 -7
- package/package.json +30 -30
- package/src/cli.js +1727 -1733
- package/src/harnesses/aider.js +48 -48
- package/src/harnesses/claude-code.js +318 -318
- package/src/harnesses/claude-code.test.js +57 -57
- package/src/harnesses/cline.js +77 -77
- package/src/harnesses/codegpt.js +76 -76
- package/src/harnesses/codex.js +180 -180
- package/src/harnesses/codex.test.js +119 -119
- package/src/harnesses/opencode.js +151 -151
- package/src/harnesses/shared.js +51 -51
- package/src/harnesses/windsurf.js +57 -57
- package/src/harnesses/zed.js +61 -61
- package/src/launch.js +467 -393
- package/src/launch.test.js +153 -153
- package/src/session.js +157 -157
- package/src/session.test.js +44 -44
- package/src/state.js +103 -103
- package/src/statusline.js +202 -202
- package/src/statusline.test.js +72 -72
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
import {
|
|
2
|
-
atomicWrite,
|
|
3
|
-
backupFile,
|
|
4
|
-
compactHome,
|
|
5
|
-
exists,
|
|
6
|
-
expandHome,
|
|
7
|
-
readJsonFile,
|
|
8
|
-
readText,
|
|
9
|
-
writeJsonFile,
|
|
10
|
-
} from "../state.js";
|
|
11
|
-
import { PENNYROUTER_MODEL_CHOICES } from "./shared.js";
|
|
12
|
-
|
|
13
|
-
const JSONC_PATH = "~/.config/opencode/opencode.jsonc";
|
|
14
|
-
const JSON_PATH = "~/.config/opencode/opencode.json";
|
|
15
|
-
const MODEL_STATE_PATH = "~/.local/state/opencode/model.json";
|
|
16
|
-
|
|
17
|
-
export const opencodeHarness = {
|
|
18
|
-
id: "opencode",
|
|
19
|
-
name: "OpenCode",
|
|
20
|
-
|
|
21
|
-
async detect() {
|
|
22
|
-
return (await exists(JSONC_PATH)) || (await exists(JSON_PATH));
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
async planInstall() {
|
|
26
|
-
const configPath = (await exists(JSONC_PATH)) ? JSONC_PATH : JSON_PATH;
|
|
27
|
-
return {
|
|
28
|
-
configPath: expandHome(configPath),
|
|
29
|
-
summary: [
|
|
30
|
-
`write ${configPath}`,
|
|
31
|
-
"merge PennyRouter provider without replacing existing providers",
|
|
32
|
-
"publish the four Penny model choices",
|
|
33
|
-
`reset ${MODEL_STATE_PATH} so opencode rediscovers configured models`,
|
|
34
|
-
],
|
|
35
|
-
};
|
|
36
|
-
},
|
|
37
|
-
|
|
38
|
-
async install({ apiKey, gatewayBaseUrl, plan }) {
|
|
39
|
-
const configPath = compactHome(plan.configPath);
|
|
40
|
-
const backupPath = await backupFile(configPath, "opencode");
|
|
41
|
-
const current = await readConfig(configPath);
|
|
42
|
-
current.provider ||= {};
|
|
43
|
-
current.provider.pennyrouter = {
|
|
44
|
-
npm: "@ai-sdk/openai-compatible",
|
|
45
|
-
name: "PennyRouter",
|
|
46
|
-
options: {
|
|
47
|
-
baseURL: `${gatewayBaseUrl}/v1`,
|
|
48
|
-
apiKey,
|
|
49
|
-
},
|
|
50
|
-
models: Object.fromEntries(
|
|
51
|
-
PENNYROUTER_MODEL_CHOICES.map((model) => [model.id, { name: model.name }]),
|
|
52
|
-
),
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
await atomicWrite(configPath, `${JSON.stringify(current, null, 2)}\n`);
|
|
56
|
-
|
|
57
|
-
const modelBackupPath = await backupFile(MODEL_STATE_PATH, "opencode-model");
|
|
58
|
-
await writeJsonFile(MODEL_STATE_PATH, { recent: [], favorite: [], variant: {} });
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
harness: "opencode",
|
|
62
|
-
config_path: compactHome(plan.configPath),
|
|
63
|
-
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
64
|
-
installed_at: new Date().toISOString(),
|
|
65
|
-
restart: "type /exit, run opencode again, then pick a Penny model",
|
|
66
|
-
changes: {
|
|
67
|
-
provider_id: "pennyrouter",
|
|
68
|
-
model_state_path: MODEL_STATE_PATH,
|
|
69
|
-
model_state_backup_path: modelBackupPath ? compactHome(modelBackupPath) : null,
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
async uninstall(record) {
|
|
75
|
-
const current = await readConfig(record.config_path);
|
|
76
|
-
if (current.provider?.pennyrouter) delete current.provider.pennyrouter;
|
|
77
|
-
if (current.provider && Object.keys(current.provider).length === 0) delete current.provider;
|
|
78
|
-
await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
|
|
79
|
-
|
|
80
|
-
const model = (await readJsonFile(MODEL_STATE_PATH, null)) || {};
|
|
81
|
-
model.recent = removePennyRouterModels(model.recent);
|
|
82
|
-
model.favorite = removePennyRouterModels(model.favorite);
|
|
83
|
-
if (model.variant && typeof model.variant === "object") {
|
|
84
|
-
for (const [key, value] of Object.entries(model.variant)) {
|
|
85
|
-
if (isPennyRouterModel(key) || isPennyRouterModel(value)) delete model.variant[key];
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
await writeJsonFile(MODEL_STATE_PATH, model);
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
async function readConfig(path) {
|
|
93
|
-
const text = await readText(path, "");
|
|
94
|
-
if (!text.trim()) return {};
|
|
95
|
-
return JSON.parse(stripJsonComments(text));
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function stripJsonComments(text) {
|
|
99
|
-
let result = "";
|
|
100
|
-
let inString = false;
|
|
101
|
-
let quote = "";
|
|
102
|
-
let escaped = false;
|
|
103
|
-
|
|
104
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
105
|
-
const char = text[i];
|
|
106
|
-
const next = text[i + 1];
|
|
107
|
-
|
|
108
|
-
if (inString) {
|
|
109
|
-
result += char;
|
|
110
|
-
if (escaped) escaped = false;
|
|
111
|
-
else if (char === "\\") escaped = true;
|
|
112
|
-
else if (char === quote) inString = false;
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (char === '"' || char === "'") {
|
|
117
|
-
inString = true;
|
|
118
|
-
quote = char;
|
|
119
|
-
result += char;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (char === "/" && next === "/") {
|
|
124
|
-
while (i < text.length && text[i] !== "\n") i += 1;
|
|
125
|
-
result += "\n";
|
|
126
|
-
continue;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (char === "/" && next === "*") {
|
|
130
|
-
i += 2;
|
|
131
|
-
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
|
|
132
|
-
i += 1;
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
result += char;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return result;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function removePennyRouterModels(value) {
|
|
143
|
-
if (!Array.isArray(value)) return Array.isArray(value) ? [] : value;
|
|
144
|
-
return value.filter((item) => !isPennyRouterModel(item));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function isPennyRouterModel(value) {
|
|
148
|
-
if (typeof value === "string") return value.includes("pennyrouter");
|
|
149
|
-
if (value && typeof value === "object") return JSON.stringify(value).includes("pennyrouter");
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
atomicWrite,
|
|
3
|
+
backupFile,
|
|
4
|
+
compactHome,
|
|
5
|
+
exists,
|
|
6
|
+
expandHome,
|
|
7
|
+
readJsonFile,
|
|
8
|
+
readText,
|
|
9
|
+
writeJsonFile,
|
|
10
|
+
} from "../state.js";
|
|
11
|
+
import { PENNYROUTER_MODEL_CHOICES } from "./shared.js";
|
|
12
|
+
|
|
13
|
+
const JSONC_PATH = "~/.config/opencode/opencode.jsonc";
|
|
14
|
+
const JSON_PATH = "~/.config/opencode/opencode.json";
|
|
15
|
+
const MODEL_STATE_PATH = "~/.local/state/opencode/model.json";
|
|
16
|
+
|
|
17
|
+
export const opencodeHarness = {
|
|
18
|
+
id: "opencode",
|
|
19
|
+
name: "OpenCode",
|
|
20
|
+
|
|
21
|
+
async detect() {
|
|
22
|
+
return (await exists(JSONC_PATH)) || (await exists(JSON_PATH));
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
async planInstall() {
|
|
26
|
+
const configPath = (await exists(JSONC_PATH)) ? JSONC_PATH : JSON_PATH;
|
|
27
|
+
return {
|
|
28
|
+
configPath: expandHome(configPath),
|
|
29
|
+
summary: [
|
|
30
|
+
`write ${configPath}`,
|
|
31
|
+
"merge PennyRouter provider without replacing existing providers",
|
|
32
|
+
"publish the four Penny model choices",
|
|
33
|
+
`reset ${MODEL_STATE_PATH} so opencode rediscovers configured models`,
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async install({ apiKey, gatewayBaseUrl, plan }) {
|
|
39
|
+
const configPath = compactHome(plan.configPath);
|
|
40
|
+
const backupPath = await backupFile(configPath, "opencode");
|
|
41
|
+
const current = await readConfig(configPath);
|
|
42
|
+
current.provider ||= {};
|
|
43
|
+
current.provider.pennyrouter = {
|
|
44
|
+
npm: "@ai-sdk/openai-compatible",
|
|
45
|
+
name: "PennyRouter",
|
|
46
|
+
options: {
|
|
47
|
+
baseURL: `${gatewayBaseUrl}/v1`,
|
|
48
|
+
apiKey,
|
|
49
|
+
},
|
|
50
|
+
models: Object.fromEntries(
|
|
51
|
+
PENNYROUTER_MODEL_CHOICES.map((model) => [model.id, { name: model.name }]),
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
await atomicWrite(configPath, `${JSON.stringify(current, null, 2)}\n`);
|
|
56
|
+
|
|
57
|
+
const modelBackupPath = await backupFile(MODEL_STATE_PATH, "opencode-model");
|
|
58
|
+
await writeJsonFile(MODEL_STATE_PATH, { recent: [], favorite: [], variant: {} });
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
harness: "opencode",
|
|
62
|
+
config_path: compactHome(plan.configPath),
|
|
63
|
+
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
64
|
+
installed_at: new Date().toISOString(),
|
|
65
|
+
restart: "type /exit, run opencode again, then pick a Penny model",
|
|
66
|
+
changes: {
|
|
67
|
+
provider_id: "pennyrouter",
|
|
68
|
+
model_state_path: MODEL_STATE_PATH,
|
|
69
|
+
model_state_backup_path: modelBackupPath ? compactHome(modelBackupPath) : null,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async uninstall(record) {
|
|
75
|
+
const current = await readConfig(record.config_path);
|
|
76
|
+
if (current.provider?.pennyrouter) delete current.provider.pennyrouter;
|
|
77
|
+
if (current.provider && Object.keys(current.provider).length === 0) delete current.provider;
|
|
78
|
+
await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
|
|
79
|
+
|
|
80
|
+
const model = (await readJsonFile(MODEL_STATE_PATH, null)) || {};
|
|
81
|
+
model.recent = removePennyRouterModels(model.recent);
|
|
82
|
+
model.favorite = removePennyRouterModels(model.favorite);
|
|
83
|
+
if (model.variant && typeof model.variant === "object") {
|
|
84
|
+
for (const [key, value] of Object.entries(model.variant)) {
|
|
85
|
+
if (isPennyRouterModel(key) || isPennyRouterModel(value)) delete model.variant[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
await writeJsonFile(MODEL_STATE_PATH, model);
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
async function readConfig(path) {
|
|
93
|
+
const text = await readText(path, "");
|
|
94
|
+
if (!text.trim()) return {};
|
|
95
|
+
return JSON.parse(stripJsonComments(text));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function stripJsonComments(text) {
|
|
99
|
+
let result = "";
|
|
100
|
+
let inString = false;
|
|
101
|
+
let quote = "";
|
|
102
|
+
let escaped = false;
|
|
103
|
+
|
|
104
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
105
|
+
const char = text[i];
|
|
106
|
+
const next = text[i + 1];
|
|
107
|
+
|
|
108
|
+
if (inString) {
|
|
109
|
+
result += char;
|
|
110
|
+
if (escaped) escaped = false;
|
|
111
|
+
else if (char === "\\") escaped = true;
|
|
112
|
+
else if (char === quote) inString = false;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (char === '"' || char === "'") {
|
|
117
|
+
inString = true;
|
|
118
|
+
quote = char;
|
|
119
|
+
result += char;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (char === "/" && next === "/") {
|
|
124
|
+
while (i < text.length && text[i] !== "\n") i += 1;
|
|
125
|
+
result += "\n";
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (char === "/" && next === "*") {
|
|
130
|
+
i += 2;
|
|
131
|
+
while (i < text.length && !(text[i] === "*" && text[i + 1] === "/")) i += 1;
|
|
132
|
+
i += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
result += char;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removePennyRouterModels(value) {
|
|
143
|
+
if (!Array.isArray(value)) return Array.isArray(value) ? [] : value;
|
|
144
|
+
return value.filter((item) => !isPennyRouterModel(item));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isPennyRouterModel(value) {
|
|
148
|
+
if (typeof value === "string") return value.includes("pennyrouter");
|
|
149
|
+
if (value && typeof value === "object") return JSON.stringify(value).includes("pennyrouter");
|
|
150
|
+
return false;
|
|
151
|
+
}
|
package/src/harnesses/shared.js
CHANGED
|
@@ -1,51 +1,51 @@
|
|
|
1
|
-
export const PENNYROUTER_MODEL_CHOICES = [
|
|
2
|
-
{
|
|
3
|
-
id: "pennyrouter/penny-core:premium",
|
|
4
|
-
name: "Penny Core",
|
|
5
|
-
},
|
|
6
|
-
{
|
|
7
|
-
id: "pennyrouter/penny-speed:standard",
|
|
8
|
-
name: "Penny Speed",
|
|
9
|
-
},
|
|
10
|
-
{
|
|
11
|
-
id: "pennyrouter/penny-power:premium",
|
|
12
|
-
name: "Penny Power",
|
|
13
|
-
},
|
|
14
|
-
{
|
|
15
|
-
id: "pennyrouter/auto",
|
|
16
|
-
name: "Penny Custom",
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
export const PENNYROUTER_MODELS = PENNYROUTER_MODEL_CHOICES.map((model) => model.id);
|
|
21
|
-
|
|
22
|
-
export const PENNYROUTER_MODEL_NAMES = Object.fromEntries(
|
|
23
|
-
PENNYROUTER_MODEL_CHOICES.map((model) => [model.id, model.name]),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
export function removeJsonKeys(object, keys) {
|
|
27
|
-
if (!object || typeof object !== "object") return;
|
|
28
|
-
for (const key of keys) delete object[key];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function setYamlScalar(text, key, value) {
|
|
32
|
-
const escaped = String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
33
|
-
const line = `${key}: "${escaped}"`;
|
|
34
|
-
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:.*$`, "m");
|
|
35
|
-
if (pattern.test(text)) return text.replace(pattern, line);
|
|
36
|
-
const normalized = text.trimEnd();
|
|
37
|
-
return `${normalized}${normalized ? "\n" : ""}${line}\n`;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function removeYamlKeys(text, keys) {
|
|
41
|
-
const patterns = keys.map((key) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*:.*$`));
|
|
42
|
-
return text
|
|
43
|
-
.split("\n")
|
|
44
|
-
.filter((line) => !patterns.some((pattern) => pattern.test(line)))
|
|
45
|
-
.join("\n")
|
|
46
|
-
.replace(/\n{3,}/g, "\n\n");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function escapeRegExp(value) {
|
|
50
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
-
}
|
|
1
|
+
export const PENNYROUTER_MODEL_CHOICES = [
|
|
2
|
+
{
|
|
3
|
+
id: "pennyrouter/penny-core:premium",
|
|
4
|
+
name: "Penny Core",
|
|
5
|
+
},
|
|
6
|
+
{
|
|
7
|
+
id: "pennyrouter/penny-speed:standard",
|
|
8
|
+
name: "Penny Speed",
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: "pennyrouter/penny-power:premium",
|
|
12
|
+
name: "Penny Power",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: "pennyrouter/auto",
|
|
16
|
+
name: "Penny Custom",
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export const PENNYROUTER_MODELS = PENNYROUTER_MODEL_CHOICES.map((model) => model.id);
|
|
21
|
+
|
|
22
|
+
export const PENNYROUTER_MODEL_NAMES = Object.fromEntries(
|
|
23
|
+
PENNYROUTER_MODEL_CHOICES.map((model) => [model.id, model.name]),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export function removeJsonKeys(object, keys) {
|
|
27
|
+
if (!object || typeof object !== "object") return;
|
|
28
|
+
for (const key of keys) delete object[key];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setYamlScalar(text, key, value) {
|
|
32
|
+
const escaped = String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
33
|
+
const line = `${key}: "${escaped}"`;
|
|
34
|
+
const pattern = new RegExp(`^\\s*${escapeRegExp(key)}\\s*:.*$`, "m");
|
|
35
|
+
if (pattern.test(text)) return text.replace(pattern, line);
|
|
36
|
+
const normalized = text.trimEnd();
|
|
37
|
+
return `${normalized}${normalized ? "\n" : ""}${line}\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function removeYamlKeys(text, keys) {
|
|
41
|
+
const patterns = keys.map((key) => new RegExp(`^\\s*${escapeRegExp(key)}\\s*:.*$`));
|
|
42
|
+
return text
|
|
43
|
+
.split("\n")
|
|
44
|
+
.filter((line) => !patterns.some((pattern) => pattern.test(line)))
|
|
45
|
+
.join("\n")
|
|
46
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function escapeRegExp(value) {
|
|
50
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
51
|
+
}
|
|
@@ -1,57 +1,57 @@
|
|
|
1
|
-
import { atomicWrite, backupFile, compactHome, exists, expandHome, readJsonFile } from "../state.js";
|
|
2
|
-
import { PENNYROUTER_MODELS } from "./shared.js";
|
|
3
|
-
|
|
4
|
-
const CONFIG_PATH = process.platform === "win32"
|
|
5
|
-
? "~/\\.codeium\\windsurf\\config.json"
|
|
6
|
-
: "~/.codeium/windsurf/config.json";
|
|
7
|
-
|
|
8
|
-
export const windsurfHarness = {
|
|
9
|
-
id: "windsurf",
|
|
10
|
-
name: "Windsurf",
|
|
11
|
-
|
|
12
|
-
async detect() {
|
|
13
|
-
return exists(CONFIG_PATH);
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
async planInstall() {
|
|
17
|
-
return {
|
|
18
|
-
configPath: expandHome(CONFIG_PATH),
|
|
19
|
-
summary: [
|
|
20
|
-
`write ${CONFIG_PATH}`,
|
|
21
|
-
"set OpenAI-compatible base URL and API key",
|
|
22
|
-
"enable PennyRouter model aliases",
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
async install({ apiKey, gatewayBaseUrl, plan }) {
|
|
28
|
-
const backupPath = await backupFile(CONFIG_PATH, "windsurf");
|
|
29
|
-
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
30
|
-
current.api_base_url = `${gatewayBaseUrl}/v1`;
|
|
31
|
-
current.api_key = apiKey;
|
|
32
|
-
current.enabled_models = PENNYROUTER_MODELS;
|
|
33
|
-
await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
harness: "windsurf",
|
|
37
|
-
config_path: compactHome(plan.configPath),
|
|
38
|
-
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
39
|
-
installed_at: new Date().toISOString(),
|
|
40
|
-
restart: "restart Windsurf",
|
|
41
|
-
changes: { keys: ["api_base_url", "api_key", "enabled_models"] },
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
async uninstall(record) {
|
|
46
|
-
const current = (await readJsonFile(record.config_path, null)) || {};
|
|
47
|
-
if (typeof current.api_base_url === "string" && current.api_base_url.includes("pennyrouter")) {
|
|
48
|
-
delete current.api_base_url;
|
|
49
|
-
if (typeof current.api_key === "string" && current.api_key.startsWith("pr-")) delete current.api_key;
|
|
50
|
-
}
|
|
51
|
-
if (Array.isArray(current.enabled_models)) {
|
|
52
|
-
current.enabled_models = current.enabled_models.filter((model) => !String(model).includes("pennyrouter"));
|
|
53
|
-
if (current.enabled_models.length === 0) delete current.enabled_models;
|
|
54
|
-
}
|
|
55
|
-
await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
|
|
56
|
-
},
|
|
57
|
-
};
|
|
1
|
+
import { atomicWrite, backupFile, compactHome, exists, expandHome, readJsonFile } from "../state.js";
|
|
2
|
+
import { PENNYROUTER_MODELS } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
const CONFIG_PATH = process.platform === "win32"
|
|
5
|
+
? "~/\\.codeium\\windsurf\\config.json"
|
|
6
|
+
: "~/.codeium/windsurf/config.json";
|
|
7
|
+
|
|
8
|
+
export const windsurfHarness = {
|
|
9
|
+
id: "windsurf",
|
|
10
|
+
name: "Windsurf",
|
|
11
|
+
|
|
12
|
+
async detect() {
|
|
13
|
+
return exists(CONFIG_PATH);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
async planInstall() {
|
|
17
|
+
return {
|
|
18
|
+
configPath: expandHome(CONFIG_PATH),
|
|
19
|
+
summary: [
|
|
20
|
+
`write ${CONFIG_PATH}`,
|
|
21
|
+
"set OpenAI-compatible base URL and API key",
|
|
22
|
+
"enable PennyRouter model aliases",
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async install({ apiKey, gatewayBaseUrl, plan }) {
|
|
28
|
+
const backupPath = await backupFile(CONFIG_PATH, "windsurf");
|
|
29
|
+
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
30
|
+
current.api_base_url = `${gatewayBaseUrl}/v1`;
|
|
31
|
+
current.api_key = apiKey;
|
|
32
|
+
current.enabled_models = PENNYROUTER_MODELS;
|
|
33
|
+
await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
harness: "windsurf",
|
|
37
|
+
config_path: compactHome(plan.configPath),
|
|
38
|
+
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
39
|
+
installed_at: new Date().toISOString(),
|
|
40
|
+
restart: "restart Windsurf",
|
|
41
|
+
changes: { keys: ["api_base_url", "api_key", "enabled_models"] },
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async uninstall(record) {
|
|
46
|
+
const current = (await readJsonFile(record.config_path, null)) || {};
|
|
47
|
+
if (typeof current.api_base_url === "string" && current.api_base_url.includes("pennyrouter")) {
|
|
48
|
+
delete current.api_base_url;
|
|
49
|
+
if (typeof current.api_key === "string" && current.api_key.startsWith("pr-")) delete current.api_key;
|
|
50
|
+
}
|
|
51
|
+
if (Array.isArray(current.enabled_models)) {
|
|
52
|
+
current.enabled_models = current.enabled_models.filter((model) => !String(model).includes("pennyrouter"));
|
|
53
|
+
if (current.enabled_models.length === 0) delete current.enabled_models;
|
|
54
|
+
}
|
|
55
|
+
await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
|
|
56
|
+
},
|
|
57
|
+
};
|
package/src/harnesses/zed.js
CHANGED
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { atomicWrite, backupFile, compactHome, exists, expandHome, readJsonFile } from "../state.js";
|
|
2
|
-
import { PENNYROUTER_MODELS } from "./shared.js";
|
|
3
|
-
|
|
4
|
-
const CONFIG_PATH = process.platform === "win32"
|
|
5
|
-
? "~/AppData/Roaming/Zed/settings.json"
|
|
6
|
-
: "~/.config/zed/settings.json";
|
|
7
|
-
|
|
8
|
-
export const zedHarness = {
|
|
9
|
-
id: "zed",
|
|
10
|
-
name: "Zed",
|
|
11
|
-
|
|
12
|
-
async detect() {
|
|
13
|
-
return exists(CONFIG_PATH);
|
|
14
|
-
},
|
|
15
|
-
|
|
16
|
-
async planInstall() {
|
|
17
|
-
return {
|
|
18
|
-
configPath: expandHome(CONFIG_PATH),
|
|
19
|
-
summary: [
|
|
20
|
-
`write ${CONFIG_PATH}`,
|
|
21
|
-
"configure Zed custom language model provider for PennyRouter",
|
|
22
|
-
"publish PennyRouter model aliases",
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
},
|
|
26
|
-
|
|
27
|
-
async install({ apiKey, gatewayBaseUrl, plan }) {
|
|
28
|
-
const backupPath = await backupFile(CONFIG_PATH, "zed");
|
|
29
|
-
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
30
|
-
current.language_models ||= {};
|
|
31
|
-
current.language_models.providers ||= {};
|
|
32
|
-
current.language_models.providers.custom = {
|
|
33
|
-
api_url: `${gatewayBaseUrl}/v1`,
|
|
34
|
-
api_key: apiKey,
|
|
35
|
-
available_models: PENNYROUTER_MODELS,
|
|
36
|
-
};
|
|
37
|
-
await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
38
|
-
|
|
39
|
-
return {
|
|
40
|
-
harness: "zed",
|
|
41
|
-
config_path: compactHome(plan.configPath),
|
|
42
|
-
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
43
|
-
installed_at: new Date().toISOString(),
|
|
44
|
-
restart: "restart Zed",
|
|
45
|
-
changes: { provider: "language_models.providers.custom" },
|
|
46
|
-
};
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
async uninstall(record) {
|
|
50
|
-
const current = (await readJsonFile(record.config_path, null)) || {};
|
|
51
|
-
const provider = current.language_models?.providers?.custom;
|
|
52
|
-
if (provider && JSON.stringify(provider).includes("pennyrouter")) {
|
|
53
|
-
delete current.language_models.providers.custom;
|
|
54
|
-
}
|
|
55
|
-
if (current.language_models?.providers && Object.keys(current.language_models.providers).length === 0) {
|
|
56
|
-
delete current.language_models.providers;
|
|
57
|
-
}
|
|
58
|
-
if (current.language_models && Object.keys(current.language_models).length === 0) delete current.language_models;
|
|
59
|
-
await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
|
|
60
|
-
},
|
|
61
|
-
};
|
|
1
|
+
import { atomicWrite, backupFile, compactHome, exists, expandHome, readJsonFile } from "../state.js";
|
|
2
|
+
import { PENNYROUTER_MODELS } from "./shared.js";
|
|
3
|
+
|
|
4
|
+
const CONFIG_PATH = process.platform === "win32"
|
|
5
|
+
? "~/AppData/Roaming/Zed/settings.json"
|
|
6
|
+
: "~/.config/zed/settings.json";
|
|
7
|
+
|
|
8
|
+
export const zedHarness = {
|
|
9
|
+
id: "zed",
|
|
10
|
+
name: "Zed",
|
|
11
|
+
|
|
12
|
+
async detect() {
|
|
13
|
+
return exists(CONFIG_PATH);
|
|
14
|
+
},
|
|
15
|
+
|
|
16
|
+
async planInstall() {
|
|
17
|
+
return {
|
|
18
|
+
configPath: expandHome(CONFIG_PATH),
|
|
19
|
+
summary: [
|
|
20
|
+
`write ${CONFIG_PATH}`,
|
|
21
|
+
"configure Zed custom language model provider for PennyRouter",
|
|
22
|
+
"publish PennyRouter model aliases",
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async install({ apiKey, gatewayBaseUrl, plan }) {
|
|
28
|
+
const backupPath = await backupFile(CONFIG_PATH, "zed");
|
|
29
|
+
const current = (await readJsonFile(CONFIG_PATH, null)) || {};
|
|
30
|
+
current.language_models ||= {};
|
|
31
|
+
current.language_models.providers ||= {};
|
|
32
|
+
current.language_models.providers.custom = {
|
|
33
|
+
api_url: `${gatewayBaseUrl}/v1`,
|
|
34
|
+
api_key: apiKey,
|
|
35
|
+
available_models: PENNYROUTER_MODELS,
|
|
36
|
+
};
|
|
37
|
+
await atomicWrite(CONFIG_PATH, `${JSON.stringify(current, null, 2)}\n`);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
harness: "zed",
|
|
41
|
+
config_path: compactHome(plan.configPath),
|
|
42
|
+
backup_path: backupPath ? compactHome(backupPath) : null,
|
|
43
|
+
installed_at: new Date().toISOString(),
|
|
44
|
+
restart: "restart Zed",
|
|
45
|
+
changes: { provider: "language_models.providers.custom" },
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async uninstall(record) {
|
|
50
|
+
const current = (await readJsonFile(record.config_path, null)) || {};
|
|
51
|
+
const provider = current.language_models?.providers?.custom;
|
|
52
|
+
if (provider && JSON.stringify(provider).includes("pennyrouter")) {
|
|
53
|
+
delete current.language_models.providers.custom;
|
|
54
|
+
}
|
|
55
|
+
if (current.language_models?.providers && Object.keys(current.language_models.providers).length === 0) {
|
|
56
|
+
delete current.language_models.providers;
|
|
57
|
+
}
|
|
58
|
+
if (current.language_models && Object.keys(current.language_models).length === 0) delete current.language_models;
|
|
59
|
+
await atomicWrite(record.config_path, `${JSON.stringify(current, null, 2)}\n`);
|
|
60
|
+
},
|
|
61
|
+
};
|