pi-firecode 1.0.0 → 1.0.2

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.
Files changed (93) hide show
  1. package/README.md +59 -41
  2. package/README.zh-CN.md +78 -0
  3. package/config.example.jsonc +21 -22
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11443 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/{master → dist/master}/prompts/master.zh.md +3 -3
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/watcher/prompts/watch.en.md +43 -0
  10. package/package.json +10 -27
  11. package/activity.ts +0 -92
  12. package/busy.ts +0 -188
  13. package/config.ts +0 -500
  14. package/deliver.ts +0 -119
  15. package/flame.ts +0 -149
  16. package/format.ts +0 -133
  17. package/header.ts +0 -218
  18. package/herdr-client.ts +0 -60
  19. package/index.ts +0 -68
  20. package/jsonc.ts +0 -34
  21. package/master/actions.ts +0 -293
  22. package/master/activity-list.ts +0 -281
  23. package/master/event-card.ts +0 -87
  24. package/master/event-format.ts +0 -83
  25. package/master/guard.ts +0 -46
  26. package/master/index.ts +0 -252
  27. package/master/list-view.ts +0 -129
  28. package/master/outbox.ts +0 -188
  29. package/master/prompt.ts +0 -26
  30. package/master/role.ts +0 -18
  31. package/master/run.ts +0 -315
  32. package/master/runtime.ts +0 -259
  33. package/master/spawn.ts +0 -234
  34. package/master/state.ts +0 -225
  35. package/master/worker-view.ts +0 -517
  36. package/provider/claude-sub.ts +0 -151
  37. package/provider/openai-native/index.ts +0 -8
  38. package/provider/openai-native/src/compact-client.ts +0 -350
  39. package/provider/openai-native/src/config.ts +0 -297
  40. package/provider/openai-native/src/extension.ts +0 -93
  41. package/provider/openai-native/src/native-compaction.ts +0 -151
  42. package/provider/openai-native/src/native-details.ts +0 -157
  43. package/provider/openai-native/src/native-replay.ts +0 -253
  44. package/provider/openai-native/src/native-runtime.ts +0 -144
  45. package/provider/openai-native/src/options.ts +0 -87
  46. package/provider/openai-native/src/request-pipeline.ts +0 -21
  47. package/provider/openai-native/src/responses-input.ts +0 -433
  48. package/review/advisor.ts +0 -110
  49. package/review/card.ts +0 -398
  50. package/review/checkpoint.ts +0 -378
  51. package/review/evidence.ts +0 -230
  52. package/review/index.ts +0 -992
  53. package/review/occupancy.ts +0 -25
  54. package/review/outcome.ts +0 -98
  55. package/review/prompt.ts +0 -209
  56. package/review/reviewer.ts +0 -353
  57. package/review/session.ts +0 -110
  58. package/review/state.ts +0 -877
  59. package/review/ui.ts +0 -89
  60. package/round-recorder.ts +0 -18
  61. package/session/herdr-display.ts +0 -87
  62. package/session/presets.ts +0 -300
  63. package/session/quota.ts +0 -123
  64. package/session/rename.ts +0 -46
  65. package/session/stats.ts +0 -313
  66. package/statusbar/index.ts +0 -256
  67. package/statusbar/render.ts +0 -113
  68. package/theme.ts +0 -45
  69. package/tools/actions.ts +0 -53
  70. package/tools/assistant-view.ts +0 -64
  71. package/tools/click-anchor.ts +0 -56
  72. package/tools/group-view.ts +0 -420
  73. package/tools/grouping.ts +0 -158
  74. package/tools/host.ts +0 -369
  75. package/tools/index.ts +0 -186
  76. package/tools/line.ts +0 -231
  77. package/tools/machine.ts +0 -78
  78. package/tools/parts.ts +0 -143
  79. package/tools/round.ts +0 -95
  80. package/tools/timing.ts +0 -22
  81. package/tools/turn-clock.ts +0 -41
  82. package/tools/turn-summary.ts +0 -109
  83. package/truncated-write.ts +0 -18
  84. package/watcher/card.ts +0 -91
  85. package/watcher/index.ts +0 -176
  86. package/watcher/observer.ts +0 -75
  87. package/watcher/transcript.ts +0 -56
  88. /package/{master → dist/master}/prompts/worker.zh.md +0 -0
  89. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  90. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
  91. /package/{review → dist/review}/prompts/review.en.md +0 -0
  92. /package/{review → dist/review}/prompts/review.zh.md +0 -0
  93. /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
@@ -1,350 +0,0 @@
1
- import type { NativeCompactionRuntime } from "./native-runtime";
2
- import type { NativeCompactionRequest } from "./responses-input";
3
-
4
- const JSON_CONTENT_TYPE = "application/json";
5
- const COMPACTION_TRIGGER = { type: "compaction_trigger" } as const;
6
- const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
7
-
8
- type ParsedOutput = {
9
- output: Record<string, unknown>[];
10
- createdAt?: unknown;
11
- };
12
-
13
- type ParsedFailure = {
14
- reason: "input-too-large" | "response-failed";
15
- detail?: string;
16
- };
17
-
18
- export type NativeCompactionFailureReason =
19
- | "aborted"
20
- | "network-error"
21
- | "non-2xx"
22
- | "empty-body"
23
- | "invalid-json"
24
- | "malformed-response"
25
- | "missing-compaction"
26
- | "input-too-large"
27
- | "response-failed";
28
-
29
- export type NativeCompactionResult =
30
- | {
31
- ok: true;
32
- compactedWindow: unknown[];
33
- createdAt?: string;
34
- }
35
- | {
36
- ok: false;
37
- reason: NativeCompactionFailureReason;
38
- status?: number;
39
- detail?: string;
40
- };
41
-
42
- function isRecord(value: unknown): value is Record<string, unknown> {
43
- return value !== null && typeof value === "object" && !Array.isArray(value);
44
- }
45
-
46
- function isAbortError(error: unknown): boolean {
47
- return (
48
- (error instanceof DOMException && error.name === "AbortError") ||
49
- (error instanceof Error && (error.name === "AbortError" || error.name === "ABORT_ERR"))
50
- );
51
- }
52
-
53
- function normalizeResponseTimestamp(value: unknown): string | undefined {
54
- if (typeof value === "number" && Number.isFinite(value)) {
55
- const milliseconds = value > 1_000_000_000_000 ? value : value * 1000;
56
- return new Date(milliseconds).toISOString();
57
- }
58
- if (typeof value !== "string" || !value.trim()) {
59
- return undefined;
60
- }
61
- const parsed = Date.parse(value);
62
- return Number.isNaN(parsed) ? value.trim() : new Date(parsed).toISOString();
63
- }
64
-
65
- function errorMessage(value: unknown): string | undefined {
66
- if (!isRecord(value)) {
67
- return undefined;
68
- }
69
- if (typeof value.message === "string" && value.message.trim()) {
70
- return value.message.trim();
71
- }
72
- if (isRecord(value.error)) {
73
- return errorMessage(value.error);
74
- }
75
- if (isRecord(value.response)) {
76
- return errorMessage(value.response);
77
- }
78
- return undefined;
79
- }
80
-
81
- function providerError(value: unknown): Record<string, unknown> | undefined {
82
- if (!isRecord(value)) {
83
- return undefined;
84
- }
85
- if (isRecord(value.error)) {
86
- return value.error;
87
- }
88
- return providerError(value.response);
89
- }
90
-
91
- function parseFailedResponse(value: unknown): ParsedFailure | undefined {
92
- if (!isRecord(value)) {
93
- return undefined;
94
- }
95
- const response = isRecord(value.response) ? value.response : value;
96
- if (value.type !== "response.failed" && value.type !== "error" && response.status !== "failed") {
97
- return undefined;
98
- }
99
- const error = providerError(value);
100
- const code = typeof error?.code === "string" ? error.code : undefined;
101
- const detail = error ? JSON.stringify(error) : errorMessage(value);
102
- return {
103
- reason: code === "context_length_exceeded" ? "input-too-large" : "response-failed",
104
- ...(detail ? { detail } : {}),
105
- };
106
- }
107
-
108
- function parseErrorDetail(responseText: string): string | undefined {
109
- try {
110
- return errorMessage(JSON.parse(responseText));
111
- } catch {
112
- return undefined;
113
- }
114
- }
115
-
116
- function isOutputArray(value: unknown): value is Record<string, unknown>[] {
117
- return Array.isArray(value) && value.every(isRecord);
118
- }
119
-
120
- function parseJsonOutput(value: unknown): ParsedOutput | undefined {
121
- if (!isRecord(value) || !isOutputArray(value.output)) {
122
- return undefined;
123
- }
124
- return { output: value.output, createdAt: value.created_at };
125
- }
126
-
127
- function parseSseOutput(responseText: string): ParsedOutput | ParsedFailure | undefined {
128
- let completed: ParsedOutput | undefined;
129
- const streamedItems: Record<string, unknown>[] = [];
130
- let failure: ParsedFailure | undefined;
131
- for (const block of responseText.split(/\r?\n\r?\n/)) {
132
- const data = block
133
- .split(/\r?\n/)
134
- .filter((line) => line.startsWith("data:"))
135
- .map((line) => line.slice(5).trimStart())
136
- .join("\n");
137
- if (!data || data === "[DONE]") {
138
- continue;
139
- }
140
- let event: unknown;
141
- try {
142
- event = JSON.parse(data);
143
- } catch {
144
- continue;
145
- }
146
- if (!isRecord(event)) {
147
- continue;
148
- }
149
- if (event.type === "response.output_item.done" && isRecord(event.item)) {
150
- streamedItems.push(event.item);
151
- continue;
152
- }
153
- if (event.type === "response.completed") {
154
- completed = parseJsonOutput(event.response) ?? {
155
- output: [],
156
- createdAt: isRecord(event.response) ? event.response.created_at : undefined,
157
- };
158
- continue;
159
- }
160
- failure = parseFailedResponse(event) ?? failure;
161
- }
162
- if (completed?.output.length) {
163
- return completed;
164
- }
165
- if (completed && streamedItems.length > 0) {
166
- return { output: streamedItems, createdAt: completed.createdAt };
167
- }
168
- return failure;
169
- }
170
-
171
- function parseResponseOutput(responseText: string):
172
- | { ok: true; parsed: ParsedOutput }
173
- | { ok: false; reason: "invalid-json" | "malformed-response" | ParsedFailure["reason"]; detail?: string } {
174
- const trimmed = responseText.trim();
175
- if (trimmed.startsWith("{")) {
176
- try {
177
- const value = JSON.parse(trimmed);
178
- const failure = parseFailedResponse(value);
179
- if (failure) {
180
- return { ok: false, ...failure };
181
- }
182
- const parsed = parseJsonOutput(value);
183
- if (parsed) {
184
- return { ok: true, parsed };
185
- }
186
- const detail = errorMessage(value);
187
- return { ok: false, reason: "malformed-response", ...(detail ? { detail } : {}) };
188
- } catch {
189
- return { ok: false, reason: "invalid-json" };
190
- }
191
- }
192
-
193
- const sse = parseSseOutput(trimmed);
194
- if (sse && "output" in sse) {
195
- return { ok: true, parsed: sse };
196
- }
197
- return sse
198
- ? { ok: false, ...sse }
199
- : { ok: false, reason: "malformed-response" };
200
- }
201
-
202
- function isRetainedInputItem(value: unknown): value is Record<string, unknown> {
203
- return isRecord(value) && (value.role === "user" || value.role === "developer" || value.role === "system");
204
- }
205
-
206
- function itemText(item: Record<string, unknown>): string {
207
- if (typeof item.content === "string") {
208
- return item.content;
209
- }
210
- if (!Array.isArray(item.content)) {
211
- return "";
212
- }
213
- return item.content
214
- .filter(isRecord)
215
- .map((part) => (typeof part.text === "string" ? part.text : ""))
216
- .join("");
217
- }
218
-
219
- function estimateTokens(item: Record<string, unknown>): number {
220
- return Math.max(1, Math.ceil(itemText(item).length / 4));
221
- }
222
-
223
- function buildCompactedWindow(input: unknown[], compactionItem: Record<string, unknown>): unknown[] {
224
- const retained: Record<string, unknown>[] = [];
225
- let remaining = RETAINED_MESSAGE_TOKEN_BUDGET;
226
- for (let index = input.length - 1; index >= 0 && remaining > 0; index--) {
227
- const item = input[index];
228
- if (!isRetainedInputItem(item)) {
229
- continue;
230
- }
231
- const tokens = estimateTokens(item);
232
- if (tokens > remaining) {
233
- continue;
234
- }
235
- retained.push(structuredClone(item));
236
- remaining -= tokens;
237
- }
238
- retained.reverse();
239
- return [...retained, structuredClone(compactionItem)];
240
- }
241
-
242
- function decodeJwtPayload(token: string): Record<string, unknown> | undefined {
243
- const parts = token.split(".");
244
- if (parts.length !== 3) {
245
- return undefined;
246
- }
247
- try {
248
- const payload = JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf8"));
249
- return isRecord(payload) ? payload : undefined;
250
- } catch {
251
- return undefined;
252
- }
253
- }
254
-
255
- function codexAccountId(token: string): string | undefined {
256
- const authClaims = decodeJwtPayload(token)?.["https://api.openai.com/auth"];
257
- if (!isRecord(authClaims) || typeof authClaims.chatgpt_account_id !== "string") {
258
- return undefined;
259
- }
260
- return authClaims.chatgpt_account_id.trim() || undefined;
261
- }
262
-
263
- function buildHeaders(runtime: NativeCompactionRuntime): Record<string, string> {
264
- const headers = new Headers(runtime.currentModel.headers ?? {});
265
- for (const [key, value] of Object.entries(runtime.headers ?? {})) {
266
- if (value === null) headers.delete(key);
267
- else headers.set(key, value);
268
- }
269
- headers.set("accept", "text/event-stream, application/json");
270
- headers.set("content-type", JSON_CONTENT_TYPE);
271
- if (!headers.has("authorization")) {
272
- headers.set("authorization", `Bearer ${runtime.apiKey}`);
273
- }
274
- if (runtime.provider === "openai-codex") {
275
- const accountId = codexAccountId(runtime.apiKey);
276
- if (accountId) {
277
- headers.set("chatgpt-account-id", accountId);
278
- }
279
- headers.set("originator", "pi");
280
- headers.set("user-agent", `pi (${process.platform}; ${process.arch})`);
281
- headers.set("openai-beta", "responses=experimental");
282
- }
283
- return Object.fromEntries(headers.entries());
284
- }
285
-
286
- export async function executeNativeCompaction(args: {
287
- runtime: NativeCompactionRuntime;
288
- request: NativeCompactionRequest;
289
- signal?: AbortSignal;
290
- }): Promise<NativeCompactionResult> {
291
- const { runtime, request, signal } = args;
292
- if (signal?.aborted) {
293
- return { ok: false, reason: "aborted" };
294
- }
295
-
296
- try {
297
- // 输入是否超窗由服务端裁决(context_length_exceeded → input-too-large):
298
- // 本地字符估算既不准也是第二事实源,历史上曾把正常触发线(窗口 − 预留)上的压缩全部误拒。
299
- const input = [...request.input, COMPACTION_TRIGGER];
300
- const response = await fetch(runtime.responsesUrl, {
301
- method: "POST",
302
- headers: buildHeaders(runtime),
303
- body: JSON.stringify({
304
- model: request.model,
305
- instructions: request.instructions,
306
- input,
307
- store: false,
308
- stream: true,
309
- }),
310
- signal,
311
- });
312
- const responseText = await response.text();
313
- if (!response.ok) {
314
- const detail = parseErrorDetail(responseText);
315
- return {
316
- ok: false,
317
- reason: "non-2xx",
318
- status: response.status,
319
- ...(detail ? { detail } : {}),
320
- };
321
- }
322
- if (!responseText.trim()) {
323
- return { ok: false, reason: "empty-body", status: response.status };
324
- }
325
-
326
- const parsed = parseResponseOutput(responseText);
327
- if (!parsed.ok) {
328
- return { ...parsed, status: response.status };
329
- }
330
-
331
- const compactionItems = parsed.parsed.output.filter((item) => item.type === "compaction");
332
- if (compactionItems.length !== 1) {
333
- return { ok: false, reason: "missing-compaction", status: response.status };
334
- }
335
-
336
- return {
337
- ok: true,
338
- compactedWindow: buildCompactedWindow(request.input, compactionItems[0]!),
339
- createdAt: normalizeResponseTimestamp(parsed.parsed.createdAt),
340
- };
341
- } catch (error) {
342
- return isAbortError(error)
343
- ? { ok: false, reason: "aborted" }
344
- : {
345
- ok: false,
346
- reason: "network-error",
347
- detail: error instanceof Error ? error.message : String(error),
348
- };
349
- }
350
- }
@@ -1,297 +0,0 @@
1
- import { closeSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { parseJsonc } from "../../../jsonc.js";
3
-
4
- const TEXT_VERBOSITIES = ["low", "medium", "high"] as const;
5
-
6
- export type TextVerbosity = (typeof TEXT_VERBOSITIES)[number];
7
-
8
- export type OpenAIProviderSettings = {
9
- textVerbosity?: TextVerbosity;
10
- priority?: true;
11
- };
12
-
13
- export type OpenAINativeSettings = {
14
- nativeCompaction: boolean;
15
- providers: Record<string, OpenAIProviderSettings>;
16
- };
17
-
18
- export type LoadedOpenAINativeSettings = {
19
- settings: OpenAINativeSettings;
20
- warnings: string[];
21
- };
22
-
23
- function isRecord(value: unknown): value is Record<string, unknown> {
24
- return value !== null && typeof value === "object" && !Array.isArray(value);
25
- }
26
-
27
- function isTextVerbosity(value: unknown): value is TextVerbosity {
28
- return typeof value === "string" && TEXT_VERBOSITIES.includes(value as TextVerbosity);
29
- }
30
-
31
- function createDefaultSettings(): OpenAINativeSettings {
32
- return {
33
- nativeCompaction: false,
34
- providers: {},
35
- };
36
- }
37
-
38
- function readConfig(configPath: string): Record<string, unknown> {
39
- const parsed = parseJsonc(readFileSync(configPath, "utf8"));
40
- if (!isRecord(parsed)) {
41
- throw new Error("expected a JSON object");
42
- }
43
- return parsed;
44
- }
45
-
46
- /** 只替换名为 key 的对象,文件其余部分(含注释)原样保留。 */
47
- function replaceKeyedObject(text: string, key: string, value: unknown): string | undefined {
48
- const marker = new RegExp(`"${key}"\\s*:\\s*\\{`);
49
- const match = marker.exec(text);
50
- if (!match) return undefined;
51
- const brace = text.indexOf("{", match.index + match[0].length - 1);
52
- if (brace < 0) return undefined;
53
-
54
- let depth = 0;
55
- let inString = false;
56
- let escape = false;
57
- for (let index = brace; index < text.length; index++) {
58
- const char = text[index];
59
- if (inString) {
60
- if (escape) escape = false;
61
- else if (char === "\\") escape = true;
62
- else if (char === '"') inString = false;
63
- continue;
64
- }
65
- if (char === '"') {
66
- inString = true;
67
- continue;
68
- }
69
- if (char === "{") depth += 1;
70
- else if (char === "}") {
71
- depth -= 1;
72
- if (depth !== 0) continue;
73
- const lineStart = text.lastIndexOf("\n", match.index) + 1;
74
- const indent = text.slice(lineStart, match.index);
75
- const pretty = JSON.stringify(value, null, "\t")
76
- .split("\n")
77
- .map((line, lineIndex) => (lineIndex === 0 ? line : indent + line))
78
- .join("\n");
79
- return text.slice(0, brace) + pretty + text.slice(index + 1);
80
- }
81
- }
82
- return undefined;
83
- }
84
-
85
- /** firecode/config.jsonc 用 `openai` 节;单测仍可用独立的 native 文件。 */
86
- function openaiRoot(root: Record<string, unknown>): Record<string, unknown> {
87
- return isRecord(root.openai) ? root.openai : root;
88
- }
89
-
90
- function isFirecodeConfig(root: Record<string, unknown>): boolean {
91
- return "openai" in root || "presets" in root || "keys" in root || "features" in root;
92
- }
93
-
94
- function parseProviderSettings(
95
- provider: string,
96
- value: unknown,
97
- warnings: string[],
98
- ): OpenAIProviderSettings | undefined {
99
- if (!isRecord(value)) {
100
- warnings.push(`providers.${provider}: expected an object.`);
101
- return undefined;
102
- }
103
-
104
- const settings: OpenAIProviderSettings = {};
105
- if (value.textVerbosity !== undefined) {
106
- if (isTextVerbosity(value.textVerbosity)) {
107
- settings.textVerbosity = value.textVerbosity;
108
- } else {
109
- warnings.push(`providers.${provider}.textVerbosity: expected low, medium, or high.`);
110
- }
111
- }
112
- if (value.priority !== undefined) {
113
- if (typeof value.priority === "boolean") {
114
- if (value.priority) {
115
- settings.priority = true;
116
- }
117
- } else {
118
- warnings.push(`providers.${provider}.priority: expected a boolean.`);
119
- }
120
- }
121
-
122
- return Object.keys(settings).length > 0 ? settings : undefined;
123
- }
124
-
125
- function parseSettings(root: Record<string, unknown>): LoadedOpenAINativeSettings {
126
- const warnings: string[] = [];
127
- const settings = createDefaultSettings();
128
- if (root.nativeCompaction !== undefined) {
129
- if (typeof root.nativeCompaction === "boolean") {
130
- settings.nativeCompaction = root.nativeCompaction;
131
- } else {
132
- warnings.push("nativeCompaction: expected a boolean.");
133
- }
134
- }
135
- if (root.providers === undefined) {
136
- return { settings, warnings };
137
- }
138
- if (!isRecord(root.providers)) {
139
- warnings.push("providers: expected an object.");
140
- return { settings, warnings };
141
- }
142
-
143
- for (const [provider, value] of Object.entries(root.providers)) {
144
- if (!provider.trim()) {
145
- warnings.push("providers: provider name cannot be empty.");
146
- continue;
147
- }
148
- const providerSettings = parseProviderSettings(provider, value, warnings);
149
- if (providerSettings) {
150
- settings.providers[provider] = providerSettings;
151
- }
152
- }
153
-
154
- return { settings, warnings };
155
- }
156
-
157
- function serializeSettings(settings: OpenAINativeSettings): Record<string, unknown> {
158
- const providers: Record<string, OpenAIProviderSettings> = {};
159
- for (const [provider, providerSettings] of Object.entries(settings.providers)) {
160
- providers[provider] = {
161
- ...(providerSettings.textVerbosity ? { textVerbosity: providerSettings.textVerbosity } : {}),
162
- ...(providerSettings.priority ? { priority: true } : {}),
163
- };
164
- }
165
-
166
- return {
167
- nativeCompaction: settings.nativeCompaction,
168
- providers,
169
- };
170
- }
171
-
172
- function isProcessRunning(processId: number): boolean {
173
- if (!Number.isSafeInteger(processId) || processId <= 0) {
174
- return false;
175
- }
176
-
177
- try {
178
- process.kill(processId, 0);
179
- return true;
180
- } catch (error) {
181
- return (error as NodeJS.ErrnoException).code === "EPERM";
182
- }
183
- }
184
-
185
- function acquireConfigLock(lockPath: string): number {
186
- try {
187
- return openSync(lockPath, "wx", 0o600);
188
- } catch (error) {
189
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") {
190
- throw error;
191
- }
192
- }
193
-
194
- const processId = Number.parseInt(readFileSync(lockPath, "utf8"), 10);
195
- if (!Number.isSafeInteger(processId) || isProcessRunning(processId)) {
196
- throw new Error("config is being updated by another Pi process");
197
- }
198
-
199
- rmSync(lockPath, { force: true });
200
- return openSync(lockPath, "wx", 0o600);
201
- }
202
-
203
- function writeConfig(
204
- configPath: string,
205
- settings: OpenAINativeSettings,
206
- firecodeRoot?: Record<string, unknown>,
207
- ): void {
208
- const serialized = serializeSettings(settings);
209
- let text: string;
210
- if (firecodeRoot) {
211
- const original = readFileSync(configPath, "utf8");
212
- text = replaceKeyedObject(original, "openai", serialized)
213
- ?? `${JSON.stringify({ ...firecodeRoot, openai: serialized }, null, "\t")}\n`;
214
- } else {
215
- text = `${JSON.stringify(serialized, null, 2)}\n`;
216
- }
217
- const temporaryPath = `${configPath}.${process.pid}.${crypto.randomUUID()}.tmp`;
218
- try {
219
- writeFileSync(temporaryPath, text, {
220
- encoding: "utf8",
221
- mode: 0o600,
222
- });
223
- renameSync(temporaryPath, configPath);
224
- } finally {
225
- rmSync(temporaryPath, { force: true });
226
- }
227
- }
228
-
229
- function withConfigLock<T>(configPath: string, action: () => T): T {
230
- const lockPath = `${configPath}.lock`;
231
- const lockDescriptor = acquireConfigLock(lockPath);
232
- writeFileSync(lockDescriptor, `${process.pid}\n`, "utf8");
233
- try {
234
- return action();
235
- } finally {
236
- closeSync(lockDescriptor);
237
- rmSync(lockPath, { force: true });
238
- }
239
- }
240
-
241
- export function loadOpenAINativeSettings(configPath: string): LoadedOpenAINativeSettings {
242
- try {
243
- return parseSettings(openaiRoot(readConfig(configPath)));
244
- } catch (error) {
245
- return {
246
- settings: createDefaultSettings(),
247
- warnings: [`config.json: ${error instanceof Error ? error.message : String(error)}`],
248
- };
249
- }
250
- }
251
-
252
- export function togglePriority(
253
- provider: string,
254
- configPath: string,
255
- ): { enabled: boolean; loaded: LoadedOpenAINativeSettings } {
256
- if (!provider.trim()) {
257
- throw new Error("provider name cannot be empty");
258
- }
259
-
260
- return withConfigLock(configPath, () => {
261
- const root = readConfig(configPath);
262
- const loaded = parseSettings(openaiRoot(root));
263
- const providerSettings = loaded.settings.providers[provider] ?? {};
264
- const enabled = providerSettings.priority !== true;
265
- const providers = {
266
- ...loaded.settings.providers,
267
- [provider]: {
268
- ...providerSettings,
269
- ...(enabled ? { priority: true as const } : {}),
270
- },
271
- };
272
-
273
- if (!enabled) {
274
- delete providers[provider].priority;
275
- if (Object.keys(providers[provider]).length === 0) {
276
- delete providers[provider];
277
- }
278
- }
279
-
280
- const settings: OpenAINativeSettings = {
281
- nativeCompaction: loaded.settings.nativeCompaction,
282
- providers,
283
- };
284
- writeConfig(configPath, settings, isFirecodeConfig(root) ? root : undefined);
285
- return {
286
- enabled,
287
- loaded: { settings, warnings: loaded.warnings },
288
- };
289
- });
290
- }
291
-
292
- export function providerSettings(
293
- settings: OpenAINativeSettings,
294
- provider: string | undefined,
295
- ): OpenAIProviderSettings | undefined {
296
- return provider ? settings.providers[provider] : undefined;
297
- }