flavor-code 1.3.12 → 1.3.15
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/README.md +426 -425
- package/README.zh-CN.md +423 -422
- package/dist/{app-Z7D7XPVE.js → app-DDYOKGCX.js} +134 -30
- package/dist/{chunk-EZNIDHFZ.js → chunk-TQXNF337.js} +1 -1
- package/dist/{chunk-QC5OYQTB.js → chunk-WB4IJXFT.js} +403 -123
- package/dist/cli-main.js +1036 -0
- package/dist/cli.js +48 -1007
- package/dist/desktop/astgraph-service.d.ts +34 -0
- package/dist/desktop/main.js +502 -299
- package/dist/desktop-renderer/assets/{index-CE6mw-at.js → index-BAHTmEUI.js} +6 -6
- package/dist/desktop-renderer/assets/index-ZZsUx916.css +1 -0
- package/dist/desktop-renderer/assets/{interactive-terminal-DcaRc1vO.js → interactive-terminal-Bn8G5Pig.js} +1 -1
- package/dist/desktop-renderer/index.html +2 -2
- package/dist/explain/service.d.ts +114 -0
- package/dist/sdk/index.js +2 -2
- package/dist/ui/commands.d.ts +6 -2
- package/dist/ui/session.d.ts +1 -0
- package/package.json +1 -1
- package/dist/desktop-renderer/assets/index-DHTr_Acb.css +0 -1
|
@@ -66,12 +66,12 @@ function normalizeProviderError(error) {
|
|
|
66
66
|
|
|
67
67
|
// src/utils/async.ts
|
|
68
68
|
function withTimeout(promise, ms) {
|
|
69
|
-
return new Promise((
|
|
69
|
+
return new Promise((resolve29, reject) => {
|
|
70
70
|
let settled = false;
|
|
71
71
|
const timer = setTimeout(() => {
|
|
72
72
|
if (settled) return;
|
|
73
73
|
settled = true;
|
|
74
|
-
|
|
74
|
+
resolve29({ timedOut: true, value: void 0 });
|
|
75
75
|
}, ms);
|
|
76
76
|
timer.unref?.();
|
|
77
77
|
promise.then(
|
|
@@ -79,7 +79,7 @@ function withTimeout(promise, ms) {
|
|
|
79
79
|
if (!settled) {
|
|
80
80
|
settled = true;
|
|
81
81
|
clearTimeout(timer);
|
|
82
|
-
|
|
82
|
+
resolve29({ timedOut: false, value });
|
|
83
83
|
}
|
|
84
84
|
},
|
|
85
85
|
(error) => {
|
|
@@ -94,13 +94,13 @@ function withTimeout(promise, ms) {
|
|
|
94
94
|
}
|
|
95
95
|
function awaitWithSignal(promise, signal) {
|
|
96
96
|
signal.throwIfAborted();
|
|
97
|
-
return new Promise((
|
|
97
|
+
return new Promise((resolve29, reject) => {
|
|
98
98
|
const onAbort = () => reject(signal.reason);
|
|
99
99
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
100
100
|
promise.then(
|
|
101
101
|
(value) => {
|
|
102
102
|
signal.removeEventListener("abort", onAbort);
|
|
103
|
-
|
|
103
|
+
resolve29(value);
|
|
104
104
|
},
|
|
105
105
|
(error) => {
|
|
106
106
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -3224,6 +3224,7 @@ var MVP_COMMANDS = [
|
|
|
3224
3224
|
"mcp",
|
|
3225
3225
|
"commit",
|
|
3226
3226
|
"review",
|
|
3227
|
+
"explain",
|
|
3227
3228
|
"ide",
|
|
3228
3229
|
"memory",
|
|
3229
3230
|
"remember",
|
|
@@ -3262,6 +3263,7 @@ var COMMAND_DESCRIPTIONS = {
|
|
|
3262
3263
|
evolve: "Self-improvement loop: capture failures, suggest fixes, verify with tests",
|
|
3263
3264
|
commit: "Generate a commit message for staged changes and commit after confirmation",
|
|
3264
3265
|
review: "Review uncommitted changes for bugs and risks before committing",
|
|
3266
|
+
explain: "Explain a symbol for newcomers (AST graph + source + git history)",
|
|
3265
3267
|
mcp: "Manage MCP servers",
|
|
3266
3268
|
ide: "Show the connected IDE and editor context",
|
|
3267
3269
|
memory: "Show long-term project memory",
|
|
@@ -3338,6 +3340,12 @@ function parseSlashCommand(input, dynamicCommands = [], skillCommands = [], mana
|
|
|
3338
3340
|
const focus = args.join(" ").trim();
|
|
3339
3341
|
return focus.length === 0 ? { name } : { name, focus };
|
|
3340
3342
|
}
|
|
3343
|
+
if (name === "explain") {
|
|
3344
|
+
const [query, ...focusParts] = args;
|
|
3345
|
+
if (query === void 0) return { name };
|
|
3346
|
+
const focus = focusParts.join(" ").trim();
|
|
3347
|
+
return focus.length === 0 ? { name, query } : { name, query, focus };
|
|
3348
|
+
}
|
|
3341
3349
|
if (name === "loop") {
|
|
3342
3350
|
const goal = args.join(" ").trim();
|
|
3343
3351
|
if (!goal) return { name: "invalid", command: name, message: "Use /loop <goal>." };
|
|
@@ -3941,6 +3949,14 @@ var FlavorSession = class {
|
|
|
3941
3949
|
this.#notice(await required(this.#services.gitCommit, "commit")(command.hint, signal));
|
|
3942
3950
|
} else if (command.name === "review") {
|
|
3943
3951
|
this.#notice(await required(this.#services.gitReview, "review")(command.focus, signal));
|
|
3952
|
+
} else if (command.name === "explain") {
|
|
3953
|
+
const result = await required(this.#services.explain, "explain")(
|
|
3954
|
+
command.query,
|
|
3955
|
+
command.focus,
|
|
3956
|
+
signal,
|
|
3957
|
+
(chunk) => this.#services.output({ type: "text", text: chunk })
|
|
3958
|
+
);
|
|
3959
|
+
if (result !== "") this.#notice(result);
|
|
3944
3960
|
} else if (command.name === "mcp") {
|
|
3945
3961
|
this.#notice(await this.#services.mcp(command, signal));
|
|
3946
3962
|
} else if (command.name === "pals") {
|
|
@@ -4107,9 +4123,9 @@ var FlavorSession = class {
|
|
|
4107
4123
|
}
|
|
4108
4124
|
#waitForPlanningIdle(key) {
|
|
4109
4125
|
if (!this.#activePlanning.has(key)) return Promise.resolve();
|
|
4110
|
-
return new Promise((
|
|
4126
|
+
return new Promise((resolve29) => {
|
|
4111
4127
|
const check = () => {
|
|
4112
|
-
if (!this.#activePlanning.has(key))
|
|
4128
|
+
if (!this.#activePlanning.has(key)) resolve29();
|
|
4113
4129
|
else this.#planningIdleWaiters.add(check);
|
|
4114
4130
|
};
|
|
4115
4131
|
this.#planningIdleWaiters.add(check);
|
|
@@ -4457,12 +4473,12 @@ var BrokerStateError = class extends Error {
|
|
|
4457
4473
|
};
|
|
4458
4474
|
var WINDOWS_PIPE_PREFIX = "\\\\.\\pipe\\";
|
|
4459
4475
|
function canConnect(address) {
|
|
4460
|
-
return new Promise((
|
|
4476
|
+
return new Promise((resolve29) => {
|
|
4461
4477
|
const socket = new Socket();
|
|
4462
4478
|
const finish = (connected) => {
|
|
4463
4479
|
socket.removeAllListeners();
|
|
4464
4480
|
socket.destroy();
|
|
4465
|
-
|
|
4481
|
+
resolve29(connected);
|
|
4466
4482
|
};
|
|
4467
4483
|
socket.once("connect", () => finish(true));
|
|
4468
4484
|
socket.once("error", () => finish(false));
|
|
@@ -4511,7 +4527,7 @@ var PalBrokerServer = class {
|
|
|
4511
4527
|
this.authToken ??= await loadOrCreatePalAuthToken(this.authHome === void 0 ? {} : { home: this.authHome });
|
|
4512
4528
|
try {
|
|
4513
4529
|
await this.prepareAddress();
|
|
4514
|
-
await new Promise((
|
|
4530
|
+
await new Promise((resolve29, reject) => {
|
|
4515
4531
|
const onError = (error) => {
|
|
4516
4532
|
this.server.off("listening", onListening);
|
|
4517
4533
|
reject(error);
|
|
@@ -4520,7 +4536,7 @@ var PalBrokerServer = class {
|
|
|
4520
4536
|
this.server.off("error", onError);
|
|
4521
4537
|
this.started = true;
|
|
4522
4538
|
this.ownsAddress = true;
|
|
4523
|
-
|
|
4539
|
+
resolve29();
|
|
4524
4540
|
};
|
|
4525
4541
|
this.server.once("error", onError);
|
|
4526
4542
|
this.server.once("listening", onListening);
|
|
@@ -4548,7 +4564,7 @@ var PalBrokerServer = class {
|
|
|
4548
4564
|
const ownsAddress = this.ownsAddress;
|
|
4549
4565
|
for (const connection of this.connections) connection.socket.destroy();
|
|
4550
4566
|
if (this.started) {
|
|
4551
|
-
await new Promise((
|
|
4567
|
+
await new Promise((resolve29) => this.server.close(() => resolve29()));
|
|
4552
4568
|
this.started = false;
|
|
4553
4569
|
}
|
|
4554
4570
|
this.ownsAddress = false;
|
|
@@ -4567,8 +4583,8 @@ var PalBrokerServer = class {
|
|
|
4567
4583
|
});
|
|
4568
4584
|
if (this.endpointLock === void 0) throw new Error(`Pals endpoint '${this.address}' is active or owned`);
|
|
4569
4585
|
try {
|
|
4570
|
-
const
|
|
4571
|
-
if (!
|
|
4586
|
+
const stat10 = await lstat3(this.address);
|
|
4587
|
+
if (!stat10.isSocket()) throw new Error(`Refusing to unlink '${this.address}' because it is not a socket`);
|
|
4572
4588
|
await unlink3(this.address);
|
|
4573
4589
|
} catch (error) {
|
|
4574
4590
|
if (error.code !== "ENOENT") throw error;
|
|
@@ -4581,8 +4597,8 @@ var PalBrokerServer = class {
|
|
|
4581
4597
|
}
|
|
4582
4598
|
async unlinkKnownSocket() {
|
|
4583
4599
|
try {
|
|
4584
|
-
const
|
|
4585
|
-
if (
|
|
4600
|
+
const stat10 = await lstat3(this.address);
|
|
4601
|
+
if (stat10.isSocket()) await unlink3(this.address);
|
|
4586
4602
|
} catch (error) {
|
|
4587
4603
|
if (error.code !== "ENOENT") throw error;
|
|
4588
4604
|
}
|
|
@@ -5217,15 +5233,15 @@ var PalBrokerState = class {
|
|
|
5217
5233
|
// src/pals/broker-cli.ts
|
|
5218
5234
|
var startupAttempts = /* @__PURE__ */ new Map();
|
|
5219
5235
|
function delay(milliseconds) {
|
|
5220
|
-
return new Promise((
|
|
5236
|
+
return new Promise((resolve29) => setTimeout(resolve29, milliseconds));
|
|
5221
5237
|
}
|
|
5222
5238
|
function canConnect2(address) {
|
|
5223
|
-
return new Promise((
|
|
5239
|
+
return new Promise((resolve29) => {
|
|
5224
5240
|
const socket = new Socket2();
|
|
5225
5241
|
const finish = (connected) => {
|
|
5226
5242
|
socket.removeAllListeners();
|
|
5227
5243
|
socket.destroy();
|
|
5228
|
-
|
|
5244
|
+
resolve29(connected);
|
|
5229
5245
|
};
|
|
5230
5246
|
socket.once("connect", () => finish(true));
|
|
5231
5247
|
socket.once("error", () => finish(false));
|
|
@@ -5268,8 +5284,8 @@ async function runPalBroker(options) {
|
|
|
5268
5284
|
const sweepIntervalMs = options.sweepIntervalMs ?? 5e3;
|
|
5269
5285
|
const idleShutdownMs = options.idleShutdownMs ?? 3e4;
|
|
5270
5286
|
let resolveClosed;
|
|
5271
|
-
const closed = new Promise((
|
|
5272
|
-
resolveClosed =
|
|
5287
|
+
const closed = new Promise((resolve29) => {
|
|
5288
|
+
resolveClosed = resolve29;
|
|
5273
5289
|
});
|
|
5274
5290
|
let closing;
|
|
5275
5291
|
const timer = setInterval(() => {
|
|
@@ -5312,7 +5328,7 @@ async function ensureOnce(options) {
|
|
|
5312
5328
|
import { readFile as readFile22 } from "fs/promises";
|
|
5313
5329
|
import { createHash as createHash18, randomUUID as randomUUID20 } from "crypto";
|
|
5314
5330
|
import { homedir as homedir2, release as osRelease, tmpdir as tmpdir4, version as osVersion } from "os";
|
|
5315
|
-
import { basename as basename6, join as
|
|
5331
|
+
import { basename as basename6, join as join26, relative as relative20, resolve as resolve28, sep as sep20 } from "path";
|
|
5316
5332
|
|
|
5317
5333
|
// src/agent/subagents.ts
|
|
5318
5334
|
import { z as z5 } from "zod";
|
|
@@ -7164,10 +7180,10 @@ function sanitize(message2, secrets) {
|
|
|
7164
7180
|
}
|
|
7165
7181
|
function waitForRetry(delayMs, signal) {
|
|
7166
7182
|
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
7167
|
-
return new Promise((
|
|
7183
|
+
return new Promise((resolve29, reject) => {
|
|
7168
7184
|
const timer = setTimeout(() => {
|
|
7169
7185
|
signal?.removeEventListener("abort", onAbort);
|
|
7170
|
-
|
|
7186
|
+
resolve29();
|
|
7171
7187
|
}, delayMs);
|
|
7172
7188
|
const onAbort = () => {
|
|
7173
7189
|
clearTimeout(timer);
|
|
@@ -7778,10 +7794,10 @@ function serializeToolInput(input) {
|
|
|
7778
7794
|
}
|
|
7779
7795
|
function waitForRetry2(delayMs, signal) {
|
|
7780
7796
|
if (signal?.aborted) return Promise.reject(signal.reason);
|
|
7781
|
-
return new Promise((
|
|
7797
|
+
return new Promise((resolve29, reject) => {
|
|
7782
7798
|
const timer = setTimeout(() => {
|
|
7783
7799
|
signal?.removeEventListener("abort", onAbort);
|
|
7784
|
-
|
|
7800
|
+
resolve29();
|
|
7785
7801
|
}, delayMs);
|
|
7786
7802
|
const onAbort = () => {
|
|
7787
7803
|
clearTimeout(timer);
|
|
@@ -8987,7 +9003,7 @@ function decoratePayload(payload, envelope) {
|
|
|
8987
9003
|
return envelope === void 0 ? payload : { ...payload, ...envelope };
|
|
8988
9004
|
}
|
|
8989
9005
|
function runShellHandler(descriptor, event, signal) {
|
|
8990
|
-
return new Promise((
|
|
9006
|
+
return new Promise((resolve29, reject) => {
|
|
8991
9007
|
const child = spawn4(descriptor.command, [...descriptor.args ?? []], {
|
|
8992
9008
|
env: { ...process.env, ...descriptor.env },
|
|
8993
9009
|
shell: false,
|
|
@@ -9030,7 +9046,7 @@ function runShellHandler(descriptor, event, signal) {
|
|
|
9030
9046
|
return;
|
|
9031
9047
|
}
|
|
9032
9048
|
try {
|
|
9033
|
-
|
|
9049
|
+
resolve29(HookDecisionSchema.parse(JSON.parse(stdout)));
|
|
9034
9050
|
} catch (error) {
|
|
9035
9051
|
reject(error);
|
|
9036
9052
|
}
|
|
@@ -9227,14 +9243,14 @@ async function createIslandControlServer(options) {
|
|
|
9227
9243
|
void dispatch(line, socket, options, token);
|
|
9228
9244
|
});
|
|
9229
9245
|
});
|
|
9230
|
-
await new Promise((
|
|
9246
|
+
await new Promise((resolve29, reject) => {
|
|
9231
9247
|
const onError = (error) => {
|
|
9232
9248
|
server.removeListener("listening", onListening);
|
|
9233
9249
|
reject(error);
|
|
9234
9250
|
};
|
|
9235
9251
|
const onListening = () => {
|
|
9236
9252
|
server.removeListener("error", onError);
|
|
9237
|
-
|
|
9253
|
+
resolve29();
|
|
9238
9254
|
};
|
|
9239
9255
|
server.once("error", onError);
|
|
9240
9256
|
server.once("listening", onListening);
|
|
@@ -9305,11 +9321,11 @@ function write(socket, payload) {
|
|
|
9305
9321
|
}
|
|
9306
9322
|
}
|
|
9307
9323
|
function closeServer(server) {
|
|
9308
|
-
return new Promise((
|
|
9324
|
+
return new Promise((resolve29) => {
|
|
9309
9325
|
try {
|
|
9310
|
-
server.close(() =>
|
|
9326
|
+
server.close(() => resolve29());
|
|
9311
9327
|
} catch {
|
|
9312
|
-
|
|
9328
|
+
resolve29();
|
|
9313
9329
|
}
|
|
9314
9330
|
});
|
|
9315
9331
|
}
|
|
@@ -10494,7 +10510,7 @@ var USAGE = [
|
|
|
10494
10510
|
" clear reset signals, done markers, and verified markers"
|
|
10495
10511
|
].join("\n");
|
|
10496
10512
|
function runCommand(command, cwd, timeoutMs) {
|
|
10497
|
-
return new Promise((
|
|
10513
|
+
return new Promise((resolve29) => {
|
|
10498
10514
|
const isWindows = process.platform === "win32";
|
|
10499
10515
|
const child = spawn5(isWindows ? "cmd.exe" : "/bin/sh", isWindows ? ["/d", "/s", "/c", command] : ["-c", command], {
|
|
10500
10516
|
cwd,
|
|
@@ -10504,7 +10520,7 @@ function runCommand(command, cwd, timeoutMs) {
|
|
|
10504
10520
|
let stderr = "";
|
|
10505
10521
|
const timer = setTimeout(() => {
|
|
10506
10522
|
child.kill();
|
|
10507
|
-
|
|
10523
|
+
resolve29({ ok: false, stdout, stderr: `${stderr}
|
|
10508
10524
|
[killed: exceeded ${timeoutMs}ms]`, code: "timeout" });
|
|
10509
10525
|
}, timeoutMs);
|
|
10510
10526
|
child.stdout.on("data", (chunk) => {
|
|
@@ -10515,12 +10531,12 @@ function runCommand(command, cwd, timeoutMs) {
|
|
|
10515
10531
|
});
|
|
10516
10532
|
child.on("error", (error) => {
|
|
10517
10533
|
clearTimeout(timer);
|
|
10518
|
-
|
|
10534
|
+
resolve29({ ok: false, stdout, stderr: `${stderr}
|
|
10519
10535
|
${error.message}`, code: "spawn" });
|
|
10520
10536
|
});
|
|
10521
10537
|
child.on("close", (code) => {
|
|
10522
10538
|
clearTimeout(timer);
|
|
10523
|
-
|
|
10539
|
+
resolve29({ ok: code === 0, stdout, stderr, code: code ?? "unknown" });
|
|
10524
10540
|
});
|
|
10525
10541
|
});
|
|
10526
10542
|
}
|
|
@@ -14036,8 +14052,8 @@ var OAuthCallbackAuthProvider = class {
|
|
|
14036
14052
|
async #startCallbackServer(expectedState, signal) {
|
|
14037
14053
|
let serverResolve;
|
|
14038
14054
|
let serverReject;
|
|
14039
|
-
const codePromise = new Promise((
|
|
14040
|
-
serverResolve =
|
|
14055
|
+
const codePromise = new Promise((resolve29, reject) => {
|
|
14056
|
+
serverResolve = resolve29;
|
|
14041
14057
|
serverReject = reject;
|
|
14042
14058
|
});
|
|
14043
14059
|
const server = createServer3((req, res) => {
|
|
@@ -14071,7 +14087,7 @@ var OAuthCallbackAuthProvider = class {
|
|
|
14071
14087
|
);
|
|
14072
14088
|
serverResolve(code);
|
|
14073
14089
|
});
|
|
14074
|
-
return new Promise((
|
|
14090
|
+
return new Promise((resolve29, reject) => {
|
|
14075
14091
|
server.once("error", (error) => {
|
|
14076
14092
|
reject(new Error(`OAuth callback server failed: ${error.message}`));
|
|
14077
14093
|
});
|
|
@@ -14088,7 +14104,7 @@ var OAuthCallbackAuthProvider = class {
|
|
|
14088
14104
|
signal.addEventListener("abort", () => {
|
|
14089
14105
|
serverReject(new Error("OAuth flow aborted"));
|
|
14090
14106
|
}, { once: true });
|
|
14091
|
-
|
|
14107
|
+
resolve29({ redirectUri, promise: codePromise });
|
|
14092
14108
|
});
|
|
14093
14109
|
});
|
|
14094
14110
|
}
|
|
@@ -17227,8 +17243,8 @@ var LspConnection = class {
|
|
|
17227
17243
|
const id = this.#nextId++;
|
|
17228
17244
|
const request = { jsonrpc: "2.0", id, method, params };
|
|
17229
17245
|
const timeout = AbortSignal.timeout(LSP_REQUEST_TIMEOUT_MS);
|
|
17230
|
-
const promise = new Promise((
|
|
17231
|
-
this.#pending.set(id, { resolve:
|
|
17246
|
+
const promise = new Promise((resolve29, reject) => {
|
|
17247
|
+
this.#pending.set(id, { resolve: resolve29, reject });
|
|
17232
17248
|
timeout.addEventListener("abort", () => {
|
|
17233
17249
|
if (this.#pending.has(id)) {
|
|
17234
17250
|
this.#pending.delete(id);
|
|
@@ -17632,19 +17648,19 @@ async function changeSummary(workspace) {
|
|
|
17632
17648
|
};
|
|
17633
17649
|
}
|
|
17634
17650
|
async function stagedDiff(workspace, maxChars = 96e3) {
|
|
17635
|
-
const
|
|
17651
|
+
const stat10 = await git2(workspace, ["diff", "--staged", "--stat"]);
|
|
17636
17652
|
const diff = await git2(workspace, ["diff", "--staged", "--no-ext-diff", "--no-color"]);
|
|
17637
17653
|
if (!diff.ok) throw gitFailure(diff, "diff --staged");
|
|
17638
17654
|
const text = diff.stdout;
|
|
17639
17655
|
return {
|
|
17640
17656
|
diff: text.length > maxChars ? `${text.slice(0, maxChars)}
|
|
17641
17657
|
\u2026[diff truncated]` : text,
|
|
17642
|
-
stat:
|
|
17658
|
+
stat: stat10.stdout.trim(),
|
|
17643
17659
|
truncated: text.length > maxChars
|
|
17644
17660
|
};
|
|
17645
17661
|
}
|
|
17646
17662
|
async function uncommittedDiff(workspace, maxChars = 96e3) {
|
|
17647
|
-
const
|
|
17663
|
+
const stat10 = await git2(workspace, ["diff", "HEAD", "--stat"]);
|
|
17648
17664
|
const diff = await git2(workspace, ["diff", "HEAD", "--no-ext-diff", "--no-color"]);
|
|
17649
17665
|
if (!diff.ok && !diff.stderr.includes("unknown revision")) throw gitFailure(diff, "diff HEAD");
|
|
17650
17666
|
const summary2 = await changeSummary(workspace);
|
|
@@ -17652,7 +17668,7 @@ async function uncommittedDiff(workspace, maxChars = 96e3) {
|
|
|
17652
17668
|
return {
|
|
17653
17669
|
diff: text.length > maxChars ? `${text.slice(0, maxChars)}
|
|
17654
17670
|
\u2026[diff truncated]` : text,
|
|
17655
|
-
stat:
|
|
17671
|
+
stat: stat10.stdout.trim(),
|
|
17656
17672
|
untracked: summary2.untrackedFiles
|
|
17657
17673
|
};
|
|
17658
17674
|
}
|
|
@@ -17793,6 +17809,232 @@ function formatReviewReport(report) {
|
|
|
17793
17809
|
return lines.join("\n");
|
|
17794
17810
|
}
|
|
17795
17811
|
|
|
17812
|
+
// src/explain/service.ts
|
|
17813
|
+
async function resolveExplainTarget(graph, query) {
|
|
17814
|
+
const trimmed = query.trim();
|
|
17815
|
+
if (trimmed.includes("#")) {
|
|
17816
|
+
const hit = await graph.relations(trimmed).catch(() => void 0);
|
|
17817
|
+
if (hit?.origin !== void 0) return { kind: "resolved", node: hit.origin };
|
|
17818
|
+
}
|
|
17819
|
+
const hits = await graph.search(trimmed, 10);
|
|
17820
|
+
if (hits.length === 0) return { kind: "not-found", query: trimmed };
|
|
17821
|
+
if (hits.length === 1) return { kind: "resolved", node: hits[0] };
|
|
17822
|
+
return { kind: "ambiguous", candidates: hits };
|
|
17823
|
+
}
|
|
17824
|
+
function explainCandidateLabel(candidate) {
|
|
17825
|
+
return candidate.id;
|
|
17826
|
+
}
|
|
17827
|
+
async function selectExplainCandidate(candidates, questions, signal) {
|
|
17828
|
+
const shown = candidates.slice(0, 3);
|
|
17829
|
+
const answers = await questions.ask([{
|
|
17830
|
+
header: "Symbol",
|
|
17831
|
+
question: "Multiple symbols match. Which one should /explain cover? (Pick one, or type a more exact name.)",
|
|
17832
|
+
options: [
|
|
17833
|
+
...shown.map((candidate) => ({
|
|
17834
|
+
label: explainCandidateLabel(candidate),
|
|
17835
|
+
description: `${candidate.kind} \xB7 ${candidate.filePath}:${candidate.startLine}-${candidate.endLine}`
|
|
17836
|
+
})),
|
|
17837
|
+
{ label: "Cancel", description: "Abort /explain without choosing." }
|
|
17838
|
+
]
|
|
17839
|
+
}], signal);
|
|
17840
|
+
const answer = (answers[0] ?? "").trim();
|
|
17841
|
+
if (answer === "Cancel" || answer === "") return { kind: "cancelled" };
|
|
17842
|
+
const picked = shown.find((candidate) => explainCandidateLabel(candidate) === answer);
|
|
17843
|
+
if (picked !== void 0) return { kind: "picked", node: picked };
|
|
17844
|
+
return { kind: "typed", query: answer };
|
|
17845
|
+
}
|
|
17846
|
+
var MAX_SOURCE_CHARS = 2e4;
|
|
17847
|
+
function buildExplainPrompt(input) {
|
|
17848
|
+
const source = input.anchorSource.length > MAX_SOURCE_CHARS ? `${input.anchorSource.slice(0, MAX_SOURCE_CHARS)}
|
|
17849
|
+
\u2026 (truncated)` : input.anchorSource;
|
|
17850
|
+
const line = (n) => `- ${n.name} \u2014 ${n.filePath}:${n.startLine}`;
|
|
17851
|
+
return [
|
|
17852
|
+
"You are a senior engineer onboarding a new team member. Explain the following code symbol clearly and concretely; never invent behavior that is not in the shown evidence.",
|
|
17853
|
+
`Answer in ${input.language}.`,
|
|
17854
|
+
"Structure the answer with short markdown sections:",
|
|
17855
|
+
"1. \u5B83\u662F\u505A\u4EC0\u4E48\u7684 \u2014 one paragraph, plain language, beginner first.",
|
|
17856
|
+
"2. \u5173\u952E\u5B9E\u73B0\u70B9 \u2014 2-5 bullets tied to concrete lines/branches in the source.",
|
|
17857
|
+
"3. \u8C01\u8C03\u7528\u5B83 / \u5B83\u8C03\u7528\u8C01 \u2014 use the relation lists; explain the data flow in one or two sentences.",
|
|
17858
|
+
"4. \u4E3A\u4EC0\u4E48\u8FD9\u6837\u5199 \u2014 infer from the recent commit subjects when possible; say '\u4E0D\u786E\u5B9A' instead of guessing.",
|
|
17859
|
+
"5. \u65B0\u4EBA\u6CE8\u610F\u4E8B\u9879 \u2014 pitfalls, guards or invariants worth knowing.",
|
|
17860
|
+
...input.focus === void 0 || input.focus.trim() === "" ? [] : [`The user specifically asks about: ${input.focus.trim()}`],
|
|
17861
|
+
"",
|
|
17862
|
+
`Symbol: ${input.anchor.id} (${input.anchor.kind}${input.anchor.signature ? `, signature: ${input.anchor.signature}` : ""})`,
|
|
17863
|
+
"",
|
|
17864
|
+
`Source (${input.anchor.filePath}:${input.anchor.startLine}-${input.anchor.endLine}):`,
|
|
17865
|
+
source,
|
|
17866
|
+
"",
|
|
17867
|
+
`Callers:
|
|
17868
|
+
${input.callers.map(line).join("\n") || "- (none)"}`,
|
|
17869
|
+
`Callees:
|
|
17870
|
+
${input.callees.map(line).join("\n") || "- (none)"}`,
|
|
17871
|
+
"",
|
|
17872
|
+
`Recent commits touching the file:
|
|
17873
|
+
${input.history.map((h) => `- ${h.date} ${h.author}: ${h.subject}`).join("\n") || "- (no history)"}`
|
|
17874
|
+
].join("\n");
|
|
17875
|
+
}
|
|
17876
|
+
async function explainWithModel(options, prompt, signal, onText) {
|
|
17877
|
+
const { adapter, model } = options.registry.get(options.modelId());
|
|
17878
|
+
let text = "";
|
|
17879
|
+
for await (const event of adapter.stream({
|
|
17880
|
+
model,
|
|
17881
|
+
messages: [{ role: "user", content: prompt }],
|
|
17882
|
+
tools: [],
|
|
17883
|
+
signal
|
|
17884
|
+
})) {
|
|
17885
|
+
if (event.type === "text") {
|
|
17886
|
+
text += event.text;
|
|
17887
|
+
onText?.(event.text);
|
|
17888
|
+
} else if (event.type === "error") throw new Error(`explain generation failed: ${event.error.message}`);
|
|
17889
|
+
else if (event.type === "done") break;
|
|
17890
|
+
}
|
|
17891
|
+
const cleaned = text.trim();
|
|
17892
|
+
if (cleaned === "") throw new Error("explain generation returned no text");
|
|
17893
|
+
return cleaned;
|
|
17894
|
+
}
|
|
17895
|
+
var HISTORY_LIMIT = 8;
|
|
17896
|
+
var MAX_RESOLVE_ROUNDS = 2;
|
|
17897
|
+
async function runExplain(deps, query, focus, signal) {
|
|
17898
|
+
if (query === void 0) {
|
|
17899
|
+
return "Usage: /explain <symbol | file.ts#symbol> [what to focus on]. Example: /explain src/order.ts#cancelOrder error handling";
|
|
17900
|
+
}
|
|
17901
|
+
if (!(await deps.graph.status()).available) {
|
|
17902
|
+
return "The code graph is not built yet \u2014 run /ast init, then try /explain again.";
|
|
17903
|
+
}
|
|
17904
|
+
let target = await resolveExplainTarget(deps.graph, query);
|
|
17905
|
+
for (let round = 1; round <= MAX_RESOLVE_ROUNDS; round += 1) {
|
|
17906
|
+
if (target.kind !== "ambiguous") break;
|
|
17907
|
+
if (deps.questions === void 0) {
|
|
17908
|
+
return `"${query}" is ambiguous. Use the exact node id, e.g. /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}.`;
|
|
17909
|
+
}
|
|
17910
|
+
const selection = await selectExplainCandidate(target.candidates, deps.questions, signal);
|
|
17911
|
+
if (selection.kind === "cancelled") return "/explain cancelled \u2014 no symbol was selected.";
|
|
17912
|
+
if (selection.kind === "picked") {
|
|
17913
|
+
target = { kind: "resolved", node: selection.node };
|
|
17914
|
+
break;
|
|
17915
|
+
}
|
|
17916
|
+
target = await resolveExplainTarget(deps.graph, selection.query);
|
|
17917
|
+
if (round === MAX_RESOLVE_ROUNDS && target.kind === "ambiguous") {
|
|
17918
|
+
return `"${selection.query}" is still ambiguous. Pick one node id explicitly: /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}`;
|
|
17919
|
+
}
|
|
17920
|
+
}
|
|
17921
|
+
if (target.kind === "not-found") {
|
|
17922
|
+
return `No matching symbol for "${target.query}" \u2014 check the name, or refresh the graph with /ast sync.`;
|
|
17923
|
+
}
|
|
17924
|
+
if (target.kind !== "resolved") {
|
|
17925
|
+
return `"${query}" is ambiguous. Pick one node id explicitly: /explain ${target.candidates[0]?.id ?? "path/file.ts#name"}`;
|
|
17926
|
+
}
|
|
17927
|
+
const anchor = target.node;
|
|
17928
|
+
signal.throwIfAborted();
|
|
17929
|
+
deps.notify?.(`Explaining ${anchor.id} \u2026`);
|
|
17930
|
+
const { callers, callees } = await deps.graph.relations(anchor.id, 1);
|
|
17931
|
+
const raw = await deps.readFile(anchor.filePath).catch(() => "");
|
|
17932
|
+
const lines = raw.split("\n");
|
|
17933
|
+
const anchorSource = lines.slice(Math.max(0, anchor.startLine - 1), anchor.endLine).join("\n") || raw;
|
|
17934
|
+
const history = deps.history === void 0 ? [] : await deps.history(anchor.filePath, HISTORY_LIMIT).catch(() => []);
|
|
17935
|
+
try {
|
|
17936
|
+
if (deps.onText !== void 0) deps.onText(`Explain ${anchor.id}
|
|
17937
|
+
|
|
17938
|
+
`);
|
|
17939
|
+
const explanation = await explainWithModel(
|
|
17940
|
+
{ registry: deps.registry, modelId: deps.modelId },
|
|
17941
|
+
buildExplainPrompt({
|
|
17942
|
+
anchor,
|
|
17943
|
+
anchorSource,
|
|
17944
|
+
callers: callers.map((n) => ({ name: n.name, filePath: n.filePath, startLine: n.startLine })),
|
|
17945
|
+
callees: callees.map((n) => ({ name: n.name, filePath: n.filePath, startLine: n.startLine })),
|
|
17946
|
+
history,
|
|
17947
|
+
...focus === void 0 ? {} : { focus },
|
|
17948
|
+
language: deps.language
|
|
17949
|
+
}),
|
|
17950
|
+
signal,
|
|
17951
|
+
deps.onText
|
|
17952
|
+
);
|
|
17953
|
+
return deps.onText === void 0 ? `Explain ${anchor.id}
|
|
17954
|
+
|
|
17955
|
+
${explanation}` : "";
|
|
17956
|
+
} catch (error) {
|
|
17957
|
+
if (signal.aborted) throw error;
|
|
17958
|
+
return `/explain failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
17959
|
+
}
|
|
17960
|
+
}
|
|
17961
|
+
|
|
17962
|
+
// src/desktop/astgraph-service.ts
|
|
17963
|
+
import { stat as stat8 } from "fs/promises";
|
|
17964
|
+
import { createRequire as nodeCreateRequire } from "module";
|
|
17965
|
+
import { join as join22, resolve as resolve22 } from "path";
|
|
17966
|
+
var FIELDS = "id, kind, name, qualified_name, file_path, language, start_line, end_line, signature";
|
|
17967
|
+
var DesktopAstGraphService = class {
|
|
17968
|
+
#path;
|
|
17969
|
+
constructor(workspace) {
|
|
17970
|
+
this.#path = join22(resolve22(workspace), ".flavor", "astgraph", "index.db");
|
|
17971
|
+
}
|
|
17972
|
+
async status() {
|
|
17973
|
+
if ((await stat8(this.#path).catch(() => void 0))?.isFile() !== true) {
|
|
17974
|
+
return { available: false, path: this.#path, files: 0, nodes: 0, edges: 0 };
|
|
17975
|
+
}
|
|
17976
|
+
return this.#read((db) => {
|
|
17977
|
+
const count = (table) => Number(db.prepare(`SELECT COUNT(*) AS count FROM ${table}`).get().count);
|
|
17978
|
+
const metadata = db.prepare("SELECT value FROM project_metadata WHERE key = 'last_index'").get();
|
|
17979
|
+
let indexedAt;
|
|
17980
|
+
try {
|
|
17981
|
+
const at = Number(JSON.parse(metadata?.value ?? "{}").at);
|
|
17982
|
+
if (Number.isFinite(at)) indexedAt = new Date(at).toISOString();
|
|
17983
|
+
} catch {
|
|
17984
|
+
}
|
|
17985
|
+
return { available: true, path: this.#path, files: count("files"), nodes: count("nodes"), edges: count("edges"), ...indexedAt === void 0 ? {} : { indexedAt } };
|
|
17986
|
+
});
|
|
17987
|
+
}
|
|
17988
|
+
async search(query, limit = 40) {
|
|
17989
|
+
const value = query.trim().slice(0, 200);
|
|
17990
|
+
if (value.length === 0 || !(await this.status()).available) return [];
|
|
17991
|
+
return this.#read((db) => db.prepare(`SELECT ${FIELDS} FROM nodes WHERE lower(name) LIKE ? OR lower(qualified_name) LIKE ? ORDER BY CASE WHEN lower(name) = ? THEN 0 ELSE 1 END, name LIMIT ?`).all(`%${value.toLowerCase()}%`, `%${value.toLowerCase()}%`, value.toLowerCase(), Math.min(Math.max(limit, 1), 100)).map(mapNode));
|
|
17992
|
+
}
|
|
17993
|
+
async relations(id, hops = 2) {
|
|
17994
|
+
if (!/^[A-Za-z0-9._:/#@+~-]{1,512}$/.test(id)) throw new Error("Invalid AST node id");
|
|
17995
|
+
if (!(await this.status()).available) return { callers: [], callees: [], impact: [] };
|
|
17996
|
+
return this.#read((db) => {
|
|
17997
|
+
const originRow = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(id);
|
|
17998
|
+
const linked = (direction) => {
|
|
17999
|
+
const source = direction === "callers" ? "e.source" : "e.target";
|
|
18000
|
+
const predicate = direction === "callers" ? "e.target = ?" : "e.source = ?";
|
|
18001
|
+
return db.prepare(`SELECT DISTINCT n.${FIELDS.split(", ").join(", n.")} FROM edges e JOIN nodes n ON n.id = ${source} WHERE ${predicate} AND e.kind IN ('calls','imports') LIMIT 100`).all(id).map(mapNode);
|
|
18002
|
+
};
|
|
18003
|
+
const seen = /* @__PURE__ */ new Map();
|
|
18004
|
+
let frontier = [id];
|
|
18005
|
+
for (let hop = 1; hop <= Math.min(Math.max(hops, 1), 4) && frontier.length > 0 && seen.size < 200; hop += 1) {
|
|
18006
|
+
const next = [];
|
|
18007
|
+
for (const current of frontier) {
|
|
18008
|
+
const rows = db.prepare("SELECT source AS other FROM edges WHERE target = ? UNION SELECT target AS other FROM edges WHERE source = ? LIMIT 200").all(current, current);
|
|
18009
|
+
for (const row of rows) if (row.other !== id && !seen.has(row.other)) {
|
|
18010
|
+
seen.set(row.other, hop);
|
|
18011
|
+
next.push(row.other);
|
|
18012
|
+
}
|
|
18013
|
+
}
|
|
18014
|
+
frontier = next;
|
|
18015
|
+
}
|
|
18016
|
+
const impact = [...seen].flatMap(([nodeId, hop]) => {
|
|
18017
|
+
const row = db.prepare(`SELECT ${FIELDS} FROM nodes WHERE id = ?`).get(nodeId);
|
|
18018
|
+
return row === void 0 ? [] : [{ ...mapNode(row), hop }];
|
|
18019
|
+
});
|
|
18020
|
+
return { ...originRow === void 0 ? {} : { origin: mapNode(originRow) }, callers: linked("callers"), callees: linked("callees"), impact };
|
|
18021
|
+
});
|
|
18022
|
+
}
|
|
18023
|
+
#read(run) {
|
|
18024
|
+
const sqlite = nodeCreateRequire(import.meta.url)("node:sqlite");
|
|
18025
|
+
const db = new sqlite.DatabaseSync(this.#path, { readOnly: true });
|
|
18026
|
+
try {
|
|
18027
|
+
return run(db);
|
|
18028
|
+
} finally {
|
|
18029
|
+
db.close();
|
|
18030
|
+
}
|
|
18031
|
+
}
|
|
18032
|
+
};
|
|
18033
|
+
function mapNode(value) {
|
|
18034
|
+
const row = value;
|
|
18035
|
+
return { id: String(row.id), kind: String(row.kind), name: String(row.name), qualifiedName: String(row.qualified_name), filePath: String(row.file_path), language: String(row.language), startLine: Number(row.start_line), endLine: Number(row.end_line), ...row.signature == null ? {} : { signature: String(row.signature) } };
|
|
18036
|
+
}
|
|
18037
|
+
|
|
17796
18038
|
// src/git/tools.ts
|
|
17797
18039
|
import { z as z28 } from "zod";
|
|
17798
18040
|
var GitHistoryInput = z28.object({
|
|
@@ -17887,7 +18129,7 @@ var QuestionBridge = class {
|
|
|
17887
18129
|
}
|
|
17888
18130
|
}
|
|
17889
18131
|
#askLocally(questions, signal) {
|
|
17890
|
-
return new Promise((
|
|
18132
|
+
return new Promise((resolve29, reject) => {
|
|
17891
18133
|
const onAbort = () => {
|
|
17892
18134
|
this.#pending = void 0;
|
|
17893
18135
|
this.#active = false;
|
|
@@ -17901,7 +18143,7 @@ var QuestionBridge = class {
|
|
|
17901
18143
|
this.#pending = void 0;
|
|
17902
18144
|
this.#active = false;
|
|
17903
18145
|
this.#onChange?.();
|
|
17904
|
-
|
|
18146
|
+
resolve29(answers);
|
|
17905
18147
|
},
|
|
17906
18148
|
reject: (error) => {
|
|
17907
18149
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -17994,7 +18236,7 @@ function createTodoWriteTool() {
|
|
|
17994
18236
|
// src/tools/managed.ts
|
|
17995
18237
|
import { randomUUID as randomUUID16 } from "crypto";
|
|
17996
18238
|
import { link as link2, lstat as lstat11, mkdir as mkdir17, readFile as readFile19, readdir as readdir8, unlink as unlink5, writeFile as writeFile10 } from "fs/promises";
|
|
17997
|
-
import { dirname as dirname18, join as
|
|
18239
|
+
import { dirname as dirname18, join as join23, resolve as resolve23 } from "path";
|
|
17998
18240
|
import { z as z31 } from "zod";
|
|
17999
18241
|
var MANAGED_TOOL_VERSION = 1;
|
|
18000
18242
|
var MAX_RECORD_BYTES = 256 * 1024;
|
|
@@ -18036,9 +18278,9 @@ var ManagedToolStore = class {
|
|
|
18036
18278
|
#entries = /* @__PURE__ */ new Map();
|
|
18037
18279
|
#diagnostics = [];
|
|
18038
18280
|
constructor(options) {
|
|
18039
|
-
this.#workspace =
|
|
18040
|
-
this.#projectRoot =
|
|
18041
|
-
this.#globalRoot =
|
|
18281
|
+
this.#workspace = resolve23(options.workspace);
|
|
18282
|
+
this.#projectRoot = join23(this.#workspace, ".flavor", "tools");
|
|
18283
|
+
this.#globalRoot = join23(resolve23(options.home), ".flavor-code", "tools");
|
|
18042
18284
|
this.#now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
18043
18285
|
}
|
|
18044
18286
|
get diagnostics() {
|
|
@@ -18085,7 +18327,7 @@ var ManagedToolStore = class {
|
|
|
18085
18327
|
version: MANAGED_TOOL_VERSION,
|
|
18086
18328
|
createdAt: this.#now()
|
|
18087
18329
|
});
|
|
18088
|
-
const path =
|
|
18330
|
+
const path = join23(this.directory(input.scope), filename(input.name));
|
|
18089
18331
|
const definition = compileDefinition(record, input.scope, this.#workspace);
|
|
18090
18332
|
await writeExclusive(path, `${JSON.stringify(record, void 0, 2)}
|
|
18091
18333
|
`);
|
|
@@ -18110,7 +18352,7 @@ var ManagedToolStore = class {
|
|
|
18110
18352
|
if (!parsed.success) return [];
|
|
18111
18353
|
const matches = this.#matching(parsed.data.name).filter((entry2) => parsed.data.scope === void 0 || entry2.scope === parsed.data.scope);
|
|
18112
18354
|
if (matches.length > 0) return matches.map((entry2) => entry2.path);
|
|
18113
|
-
return [
|
|
18355
|
+
return [join23(this.directory(parsed.data.scope ?? "project"), filename(parsed.data.name))];
|
|
18114
18356
|
}
|
|
18115
18357
|
async #loadDirectory(scope2) {
|
|
18116
18358
|
const root = this.directory(scope2);
|
|
@@ -18123,7 +18365,7 @@ var ManagedToolStore = class {
|
|
|
18123
18365
|
return;
|
|
18124
18366
|
}
|
|
18125
18367
|
for (const name of names) {
|
|
18126
|
-
const path =
|
|
18368
|
+
const path = join23(root, name);
|
|
18127
18369
|
try {
|
|
18128
18370
|
const info = await lstat11(path);
|
|
18129
18371
|
if (!info.isFile() || info.isSymbolicLink()) throw new Error("Managed tool record must be a regular file");
|
|
@@ -19042,7 +19284,7 @@ ${total}`;
|
|
|
19042
19284
|
// src/memory/store.ts
|
|
19043
19285
|
import { createHash as createHash16 } from "crypto";
|
|
19044
19286
|
import { readFile as readFile20, rm as rm9 } from "fs/promises";
|
|
19045
|
-
import { resolve as
|
|
19287
|
+
import { resolve as resolve24, join as join24, relative as relative17, sep as sep17 } from "path";
|
|
19046
19288
|
|
|
19047
19289
|
// src/memory/similarity.ts
|
|
19048
19290
|
function normalizeForSimilarity(value) {
|
|
@@ -19144,10 +19386,10 @@ var MemoryStore = class {
|
|
|
19144
19386
|
constructor(options) {
|
|
19145
19387
|
if (!Number.isSafeInteger(options.maxEntries) || options.maxEntries < 1) throw new Error("maxEntries must be an integer of at least 1");
|
|
19146
19388
|
if (!Number.isSafeInteger(options.maxEntryChars) || options.maxEntryChars < 1) throw new Error("maxEntryChars must be an integer of at least 1");
|
|
19147
|
-
this.workspace =
|
|
19148
|
-
this.#memoryRoot =
|
|
19149
|
-
this.path =
|
|
19150
|
-
this.#behaviorPath =
|
|
19389
|
+
this.workspace = resolve24(options.workspace);
|
|
19390
|
+
this.#memoryRoot = join24(this.workspace, ".flavor", "memory");
|
|
19391
|
+
this.path = join24(this.#memoryRoot, "MEMORY.md");
|
|
19392
|
+
this.#behaviorPath = join24(this.#memoryRoot, "behavior.json");
|
|
19151
19393
|
this.#maxEntries = options.maxEntries;
|
|
19152
19394
|
this.#maxEntryChars = options.maxEntryChars;
|
|
19153
19395
|
}
|
|
@@ -19370,7 +19612,7 @@ var MemoryStore = class {
|
|
|
19370
19612
|
}
|
|
19371
19613
|
#resolveContentPath(contentPath) {
|
|
19372
19614
|
if (!/^tasks\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(contentPath)) throw new Error("Invalid memory content path");
|
|
19373
|
-
const path =
|
|
19615
|
+
const path = resolve24(this.#memoryRoot, ...contentPath.split("/"));
|
|
19374
19616
|
const rel = relative17(this.#memoryRoot, path);
|
|
19375
19617
|
if (rel === ".." || rel.startsWith(`..${sep17}`)) throw new Error("Memory content path escapes the memory directory");
|
|
19376
19618
|
return path;
|
|
@@ -19841,8 +20083,8 @@ function createExecutionEnvironment(workspace, config) {
|
|
|
19841
20083
|
}
|
|
19842
20084
|
|
|
19843
20085
|
// src/ide/client.ts
|
|
19844
|
-
import { readFile as readFile21, readdir as readdir9, stat as
|
|
19845
|
-
import { basename as basename5, join as
|
|
20086
|
+
import { readFile as readFile21, readdir as readdir9, stat as stat9 } from "fs/promises";
|
|
20087
|
+
import { basename as basename5, join as join25, relative as relative18, resolve as resolve25, sep as sep18 } from "path";
|
|
19846
20088
|
var FlavorIdeClient = class {
|
|
19847
20089
|
#workspace;
|
|
19848
20090
|
#home;
|
|
@@ -19853,8 +20095,8 @@ var FlavorIdeClient = class {
|
|
|
19853
20095
|
#sessionId;
|
|
19854
20096
|
#eventTail = Promise.resolve();
|
|
19855
20097
|
constructor(options) {
|
|
19856
|
-
this.#workspace =
|
|
19857
|
-
this.#home =
|
|
20098
|
+
this.#workspace = resolve25(options.workspace);
|
|
20099
|
+
this.#home = resolve25(options.home);
|
|
19858
20100
|
this.#environment = options.environment ?? process.env;
|
|
19859
20101
|
this.#fetch = options.fetch ?? fetch;
|
|
19860
20102
|
}
|
|
@@ -19938,7 +20180,7 @@ var FlavorIdeClient = class {
|
|
|
19938
20180
|
};
|
|
19939
20181
|
if (await this.#healthy(configured)) return configured;
|
|
19940
20182
|
}
|
|
19941
|
-
const directory =
|
|
20183
|
+
const directory = join25(this.#home, ".flavor-code", "ide");
|
|
19942
20184
|
let entries;
|
|
19943
20185
|
try {
|
|
19944
20186
|
entries = (await readdir9(directory)).filter((name) => name.endsWith(".lock"));
|
|
@@ -19946,9 +20188,9 @@ var FlavorIdeClient = class {
|
|
|
19946
20188
|
return void 0;
|
|
19947
20189
|
}
|
|
19948
20190
|
const candidates = (await Promise.all(entries.map(async (name) => {
|
|
19949
|
-
const path =
|
|
20191
|
+
const path = join25(directory, name);
|
|
19950
20192
|
try {
|
|
19951
|
-
const [contents, info] = await Promise.all([readFile21(path, "utf8"),
|
|
20193
|
+
const [contents, info] = await Promise.all([readFile21(path, "utf8"), stat9(path)]);
|
|
19952
20194
|
const lock = parseLockfile(contents);
|
|
19953
20195
|
return lock === void 0 ? void 0 : { lock, modified: info.mtimeMs };
|
|
19954
20196
|
} catch {
|
|
@@ -20052,8 +20294,8 @@ function parsePort(value) {
|
|
|
20052
20294
|
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : void 0;
|
|
20053
20295
|
}
|
|
20054
20296
|
function isWithin8(parent, child) {
|
|
20055
|
-
const base = normalize(
|
|
20056
|
-
const target = normalize(
|
|
20297
|
+
const base = normalize(resolve25(parent));
|
|
20298
|
+
const target = normalize(resolve25(child));
|
|
20057
20299
|
return target === base || target.startsWith(`${base}${sep18}`);
|
|
20058
20300
|
}
|
|
20059
20301
|
function normalize(path) {
|
|
@@ -20083,8 +20325,8 @@ var JobRegistry = class {
|
|
|
20083
20325
|
const id = `job-${randomUUID17().slice(0, 12)}`;
|
|
20084
20326
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
20085
20327
|
let settle;
|
|
20086
|
-
const done = new Promise((
|
|
20087
|
-
settle =
|
|
20328
|
+
const done = new Promise((resolve29) => {
|
|
20329
|
+
settle = resolve29;
|
|
20088
20330
|
});
|
|
20089
20331
|
const record = {
|
|
20090
20332
|
id,
|
|
@@ -20207,8 +20449,8 @@ function snapshot2(job) {
|
|
|
20207
20449
|
|
|
20208
20450
|
// src/terminal/service.ts
|
|
20209
20451
|
import { randomUUID as randomUUID18 } from "crypto";
|
|
20210
|
-
import { createRequire as
|
|
20211
|
-
import { isAbsolute as isAbsolute11, relative as relative19, resolve as
|
|
20452
|
+
import { createRequire as nodeCreateRequire2 } from "module";
|
|
20453
|
+
import { isAbsolute as isAbsolute11, relative as relative19, resolve as resolve26, sep as sep19 } from "path";
|
|
20212
20454
|
var TerminalService = class {
|
|
20213
20455
|
#root;
|
|
20214
20456
|
#factory;
|
|
@@ -20216,7 +20458,7 @@ var TerminalService = class {
|
|
|
20216
20458
|
#maxOutputChars;
|
|
20217
20459
|
#sessions = /* @__PURE__ */ new Map();
|
|
20218
20460
|
constructor(workspace, options = {}) {
|
|
20219
|
-
this.#root =
|
|
20461
|
+
this.#root = resolve26(workspace);
|
|
20220
20462
|
this.#factory = options.factory ?? defaultFactory;
|
|
20221
20463
|
this.#jobs = options.jobs;
|
|
20222
20464
|
this.#maxOutputChars = options.maxOutputChars ?? 2e5;
|
|
@@ -20321,7 +20563,7 @@ function snapshot3(record) {
|
|
|
20321
20563
|
}
|
|
20322
20564
|
function defaultFactory(shell, args, options) {
|
|
20323
20565
|
try {
|
|
20324
|
-
const pty =
|
|
20566
|
+
const pty = nodeCreateRequire2(import.meta.url)("node-pty");
|
|
20325
20567
|
return pty.spawn(shell, args, { ...options, name: "xterm-256color" });
|
|
20326
20568
|
} catch (error) {
|
|
20327
20569
|
throw new Error(`Pseudo-terminal backend is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -20334,7 +20576,7 @@ function cleanEnvironment(environment) {
|
|
|
20334
20576
|
return Object.fromEntries(Object.entries(environment).filter((entry2) => entry2[1] !== void 0));
|
|
20335
20577
|
}
|
|
20336
20578
|
function workspacePath(root, input) {
|
|
20337
|
-
const path =
|
|
20579
|
+
const path = resolve26(root, input);
|
|
20338
20580
|
const delta = relative19(root, path);
|
|
20339
20581
|
if (delta === ".." || delta.startsWith(`..${sep19}`) || isAbsolute11(delta)) throw new Error("Terminal cwd is outside the workspace");
|
|
20340
20582
|
return path;
|
|
@@ -20342,7 +20584,7 @@ function workspacePath(root, input) {
|
|
|
20342
20584
|
|
|
20343
20585
|
// src/tools/terminal.ts
|
|
20344
20586
|
import { z as z33 } from "zod";
|
|
20345
|
-
import { resolve as
|
|
20587
|
+
import { resolve as resolve27 } from "path";
|
|
20346
20588
|
var Id = z33.string().regex(/^term-[a-f0-9-]+$/);
|
|
20347
20589
|
var OpenInput = z33.object({ cwd: z33.string().optional(), shell: z33.string().optional(), args: z33.array(z33.string()).optional(), columns: z33.coerce.number().int().min(10).max(500).optional(), rows: z33.coerce.number().int().min(2).max(300).optional() });
|
|
20348
20590
|
var OptionalBoolean = z33.union([z33.boolean(), z33.string().refine((value) => value === "true" || value === "false")]);
|
|
@@ -20356,8 +20598,8 @@ function createTerminalTools(terminals, workspace = process.cwd()) {
|
|
|
20356
20598
|
name: "TerminalOpen",
|
|
20357
20599
|
description: "Open a persistent interactive pseudo-terminal in the workspace",
|
|
20358
20600
|
inputSchema: OpenInput,
|
|
20359
|
-
paths: (input) => [
|
|
20360
|
-
permissions: (input) => ({ paths: [
|
|
20601
|
+
paths: (input) => [resolve27(workspace, input.cwd ?? ".")],
|
|
20602
|
+
permissions: (input) => ({ paths: [resolve27(workspace, input.cwd ?? ".")], command: input.shell ?? "system-shell", args: input.args ?? [], cwd: resolve27(workspace, input.cwd ?? ".") }),
|
|
20361
20603
|
execute: async (input, _signal, context) => terminals.open({
|
|
20362
20604
|
owner: owner(context),
|
|
20363
20605
|
...input.cwd === void 0 ? {} : { cwd: input.cwd },
|
|
@@ -20583,8 +20825,8 @@ var PalClient = class {
|
|
|
20583
20825
|
let disconnectTimer;
|
|
20584
20826
|
await Promise.race([
|
|
20585
20827
|
this.request({ version: 1, type: "disconnect", requestId: randomUUID19() }).catch(() => void 0),
|
|
20586
|
-
new Promise((
|
|
20587
|
-
disconnectTimer = setTimeout(
|
|
20828
|
+
new Promise((resolve29) => {
|
|
20829
|
+
disconnectTimer = setTimeout(resolve29, Math.min(this.requestTimeoutMs, 250));
|
|
20588
20830
|
})
|
|
20589
20831
|
]);
|
|
20590
20832
|
if (disconnectTimer !== void 0) clearTimeout(disconnectTimer);
|
|
@@ -20630,7 +20872,7 @@ var PalClient = class {
|
|
|
20630
20872
|
return presence;
|
|
20631
20873
|
}
|
|
20632
20874
|
openSocket() {
|
|
20633
|
-
return new Promise((
|
|
20875
|
+
return new Promise((resolve29, reject) => {
|
|
20634
20876
|
let settled = false;
|
|
20635
20877
|
const rejectConnection = (error) => {
|
|
20636
20878
|
if (settled) return;
|
|
@@ -20653,7 +20895,7 @@ var PalClient = class {
|
|
|
20653
20895
|
settled = true;
|
|
20654
20896
|
if (this.connectingReject === rejectConnection) this.connectingReject = void 0;
|
|
20655
20897
|
socket.off("error", onInitialError);
|
|
20656
|
-
|
|
20898
|
+
resolve29();
|
|
20657
20899
|
};
|
|
20658
20900
|
const onInitialError = (error) => {
|
|
20659
20901
|
socket.off("connect", onConnect);
|
|
@@ -20684,12 +20926,12 @@ var PalClient = class {
|
|
|
20684
20926
|
if (this.closed || socket === void 0 || socket.destroyed || !socket.writable) {
|
|
20685
20927
|
return Promise.reject(new Error(this.closed ? "Pal client is closed" : "Pal client is not connected"));
|
|
20686
20928
|
}
|
|
20687
|
-
return new Promise((
|
|
20929
|
+
return new Promise((resolve29, reject) => {
|
|
20688
20930
|
const timer = setTimeout(() => {
|
|
20689
20931
|
this.pending.delete(request.requestId);
|
|
20690
20932
|
reject(new Error(`Pal request '${request.type}' timed out`));
|
|
20691
20933
|
}, this.requestTimeoutMs);
|
|
20692
|
-
this.pending.set(request.requestId, { resolve:
|
|
20934
|
+
this.pending.set(request.requestId, { resolve: resolve29, reject, timer });
|
|
20693
20935
|
let frame;
|
|
20694
20936
|
try {
|
|
20695
20937
|
frame = encodeControlFrame(BrokerRequestSchema.parse(request));
|
|
@@ -21093,8 +21335,8 @@ var ApprovalBridge = class {
|
|
|
21093
21335
|
}
|
|
21094
21336
|
};
|
|
21095
21337
|
async function createProductionRuntime(options) {
|
|
21096
|
-
const workspace =
|
|
21097
|
-
const home =
|
|
21338
|
+
const workspace = resolve28(options.workspace ?? process.cwd());
|
|
21339
|
+
const home = resolve28(options.home ?? homedir2());
|
|
21098
21340
|
const environment = options.environment ?? process.env;
|
|
21099
21341
|
const ide = new FlavorIdeClient({ workspace, home, environment });
|
|
21100
21342
|
const ideReady = ide.initialize();
|
|
@@ -21179,9 +21421,28 @@ async function createProductionRuntime(options) {
|
|
|
21179
21421
|
const recovered = options.resumeSession === void 0 ? void 0 : await sessionStore.load(options.resumeSession === true ? void 0 : options.resumeSession);
|
|
21180
21422
|
let timelineState = recovered === void 0 ? createTranscriptState() : restoreTranscriptState(recovered.timeline.state);
|
|
21181
21423
|
const restoredTranscript = restoreTranscriptState(timelineState);
|
|
21424
|
+
const pendingTimelineStreams = [];
|
|
21425
|
+
const flushTimelineStreams = () => {
|
|
21426
|
+
for (const pending of pendingTimelineStreams.splice(0)) {
|
|
21427
|
+
timelineState = transcriptReducer(timelineState, {
|
|
21428
|
+
type: "session",
|
|
21429
|
+
event: { type: pending.type, text: pending.chunks.join("") }
|
|
21430
|
+
});
|
|
21431
|
+
}
|
|
21432
|
+
};
|
|
21433
|
+
const recordTimelineEvent = (event) => {
|
|
21434
|
+
if (event.type === "text" || event.type === "thinking") {
|
|
21435
|
+
const last = pendingTimelineStreams.at(-1);
|
|
21436
|
+
if (last?.type === event.type) last.chunks.push(event.text);
|
|
21437
|
+
else pendingTimelineStreams.push({ type: event.type, chunks: [event.text] });
|
|
21438
|
+
return;
|
|
21439
|
+
}
|
|
21440
|
+
flushTimelineStreams();
|
|
21441
|
+
timelineState = transcriptReducer(timelineState, { type: "session", event });
|
|
21442
|
+
};
|
|
21182
21443
|
let ideSessionId;
|
|
21183
21444
|
const emitOutput = (event) => {
|
|
21184
|
-
|
|
21445
|
+
recordTimelineEvent(event);
|
|
21185
21446
|
options.output(event);
|
|
21186
21447
|
if (ideSessionId !== void 0) ide.publishEvent(ideSessionId, event);
|
|
21187
21448
|
};
|
|
@@ -21318,8 +21579,8 @@ async function createProductionRuntime(options) {
|
|
|
21318
21579
|
const registeredProviders = registration.registered;
|
|
21319
21580
|
let effectiveLlm = registration.effectiveLlm;
|
|
21320
21581
|
const pluginHost = new PluginHost({
|
|
21321
|
-
globalPluginDirs: [
|
|
21322
|
-
projectPluginDirs: [
|
|
21582
|
+
globalPluginDirs: [join26(home, ".flavor-code", "plugins")],
|
|
21583
|
+
projectPluginDirs: [join26(workspace, ".flavor", "plugins")],
|
|
21323
21584
|
config,
|
|
21324
21585
|
// Existing project plugins (including astgraph and superharness) use Node.js
|
|
21325
21586
|
// built-ins. The Worker/vm sandbox deliberately blocks those imports, so it
|
|
@@ -21427,7 +21688,7 @@ async function createProductionRuntime(options) {
|
|
|
21427
21688
|
const address = palSocketAddress({
|
|
21428
21689
|
platform: process.platform,
|
|
21429
21690
|
userScope: home,
|
|
21430
|
-
runtimeDir:
|
|
21691
|
+
runtimeDir: join26(tmpdir4(), `flavor-code-pals-${createHash18("sha256").update(home).digest("hex").slice(0, 16)}`)
|
|
21431
21692
|
});
|
|
21432
21693
|
const baseCollaborationClient = collaboration.client ?? collaboration.createClient?.({
|
|
21433
21694
|
instanceId: collaboration.instanceId,
|
|
@@ -21538,8 +21799,8 @@ async function createProductionRuntime(options) {
|
|
|
21538
21799
|
diagnostics.push(`MCP servers could not start: ${message(error)}`);
|
|
21539
21800
|
});
|
|
21540
21801
|
const skills = new SkillRegistry({
|
|
21541
|
-
globalRoots: [
|
|
21542
|
-
projectRoots: [
|
|
21802
|
+
globalRoots: [join26(home, ".flavor-code", "skills")],
|
|
21803
|
+
projectRoots: [join26(workspace, ".flavor", "skills"), ...pluginSkillRoots],
|
|
21543
21804
|
authorizeResource: async () => true,
|
|
21544
21805
|
disabledNames: config.skills.disabled
|
|
21545
21806
|
});
|
|
@@ -21547,7 +21808,7 @@ async function createProductionRuntime(options) {
|
|
|
21547
21808
|
skillsReady.catch(() => void 0);
|
|
21548
21809
|
tools.push(createSkillTool(skills), createSkillResourceTool(skills));
|
|
21549
21810
|
if (options.extraTools !== void 0) tools.push(...options.extraTools);
|
|
21550
|
-
const flavor = await optionalText2(
|
|
21811
|
+
const flavor = await optionalText2(join26(workspace, "FLAVOR.md"));
|
|
21551
21812
|
const instructionBaseline = await workspaceInstructions.baseline();
|
|
21552
21813
|
let memoryContext;
|
|
21553
21814
|
if (memoryStore !== void 0) {
|
|
@@ -21598,27 +21859,30 @@ async function createProductionRuntime(options) {
|
|
|
21598
21859
|
};
|
|
21599
21860
|
}
|
|
21600
21861
|
let persistTail = Promise.resolve();
|
|
21601
|
-
const sessionDocument = () =>
|
|
21602
|
-
|
|
21603
|
-
|
|
21604
|
-
|
|
21605
|
-
|
|
21606
|
-
|
|
21607
|
-
|
|
21608
|
-
|
|
21609
|
-
|
|
21610
|
-
|
|
21611
|
-
|
|
21612
|
-
|
|
21613
|
-
|
|
21614
|
-
|
|
21615
|
-
|
|
21616
|
-
|
|
21617
|
-
|
|
21618
|
-
|
|
21619
|
-
|
|
21620
|
-
|
|
21621
|
-
|
|
21862
|
+
const sessionDocument = () => {
|
|
21863
|
+
flushTimelineStreams();
|
|
21864
|
+
return {
|
|
21865
|
+
version: SESSION_VERSION,
|
|
21866
|
+
sessionId,
|
|
21867
|
+
createdAt,
|
|
21868
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
21869
|
+
workspace: { path: workspace },
|
|
21870
|
+
conversation: storedConversation(harness.main.context.snapshot()),
|
|
21871
|
+
tasks: {
|
|
21872
|
+
...taskPlan === void 0 ? {} : { plan: taskPlan },
|
|
21873
|
+
...taskGraph === void 0 ? {} : { graph: taskGraph },
|
|
21874
|
+
states: { ...taskStates },
|
|
21875
|
+
results: { ...taskResults }
|
|
21876
|
+
},
|
|
21877
|
+
// Persist the resolved decision, not the harness snapshot: after logout the
|
|
21878
|
+
// harness may still hold the old login model, and resuming from that stale
|
|
21879
|
+
// value is what made the welcome card show a previous service.
|
|
21880
|
+
models: { main: mainModel, subagent: childModel },
|
|
21881
|
+
permissionMode: harness.permissionMode,
|
|
21882
|
+
memory: memoryLifecycle,
|
|
21883
|
+
timeline: { version: 1, state: timelineState }
|
|
21884
|
+
};
|
|
21885
|
+
};
|
|
21622
21886
|
let persistFailed = false;
|
|
21623
21887
|
const persist = () => {
|
|
21624
21888
|
persistTail = persistTail.catch(() => void 0).then(
|
|
@@ -22125,7 +22389,7 @@ async function createProductionRuntime(options) {
|
|
|
22125
22389
|
evolveService.toolDefinition(),
|
|
22126
22390
|
...mcpTools
|
|
22127
22391
|
];
|
|
22128
|
-
const loopFlavor = await optionalText2(
|
|
22392
|
+
const loopFlavor = await optionalText2(join26(input.workspace, "FLAVOR.md"));
|
|
22129
22393
|
const loopInstructionBaseline = await loopInstructions.baseline();
|
|
22130
22394
|
const loopEnvironment = createPromptEnvironment({
|
|
22131
22395
|
now: /* @__PURE__ */ new Date(),
|
|
@@ -22485,6 +22749,22 @@ async function createProductionRuntime(options) {
|
|
|
22485
22749
|
modelId: () => harness.subagentModelId,
|
|
22486
22750
|
notify: (message2) => emitOutput({ type: "notice", message: message2 })
|
|
22487
22751
|
}, focus, signal),
|
|
22752
|
+
explain: (query, focus, signal, onText) => runExplain({
|
|
22753
|
+
graph: new DesktopAstGraphService(workspace),
|
|
22754
|
+
readFile: async (path) => {
|
|
22755
|
+
const root = resolve28(workspace);
|
|
22756
|
+
const abs = resolve28(root, path);
|
|
22757
|
+
if (abs !== root && !abs.startsWith(root + sep20)) throw new Error(`path outside workspace: ${path}`);
|
|
22758
|
+
return readFile22(abs, "utf8");
|
|
22759
|
+
},
|
|
22760
|
+
history: async (path, limit) => fileHistory(workspace, path, limit),
|
|
22761
|
+
questions,
|
|
22762
|
+
registry,
|
|
22763
|
+
modelId: () => harness.subagentModelId,
|
|
22764
|
+
language,
|
|
22765
|
+
notify: (message2) => emitOutput({ type: "notice", message: message2 }),
|
|
22766
|
+
...onText === void 0 ? {} : { onText }
|
|
22767
|
+
}, query, focus, signal),
|
|
22488
22768
|
audit: async (toolFilter) => {
|
|
22489
22769
|
try {
|
|
22490
22770
|
const raw = await readFile22(auditLogger.path, "utf8");
|
|
@@ -22674,7 +22954,7 @@ ${renderMemoryDocument(entries)}`;
|
|
|
22674
22954
|
return `Provider "${providerName}" is missing authorizationUrl, tokenUrl, or clientId.`;
|
|
22675
22955
|
}
|
|
22676
22956
|
try {
|
|
22677
|
-
const tokenStore = createFileTokenStore(
|
|
22957
|
+
const tokenStore = createFileTokenStore(join26(home, ".flavor-code", "auth.json"));
|
|
22678
22958
|
const credentialId = oauthCredentialId(oauthConfig.tokenUrl, oauthConfig.clientId);
|
|
22679
22959
|
await migrateLegacyOAuthToken(tokenStore, providerName, credentialId);
|
|
22680
22960
|
const oauth = new OAuthCallbackAuthProvider({
|
|
@@ -22721,7 +23001,7 @@ ${renderMemoryDocument(entries)}`;
|
|
|
22721
23001
|
}
|
|
22722
23002
|
},
|
|
22723
23003
|
async logout() {
|
|
22724
|
-
const tokenStore = createFileTokenStore(
|
|
23004
|
+
const tokenStore = createFileTokenStore(join26(home, ".flavor-code", "auth.json"));
|
|
22725
23005
|
const tokens = await tokenStore.load();
|
|
22726
23006
|
const hadCredentials = Object.keys(tokens).length > 0 || effectiveLlm !== void 0;
|
|
22727
23007
|
if (!hadCredentials) {
|
|
@@ -22997,8 +23277,8 @@ async function workspaceFingerprint(workspace) {
|
|
|
22997
23277
|
let remaining = 5 * 1024 * 1024;
|
|
22998
23278
|
for (const name of untracked.stdout.split("\0").filter(Boolean).sort()) {
|
|
22999
23279
|
if (name === ".flavor/loops" || name.startsWith(".flavor/loops/")) continue;
|
|
23000
|
-
const path =
|
|
23001
|
-
const relativePath = relative20(
|
|
23280
|
+
const path = resolve28(workspace, name);
|
|
23281
|
+
const relativePath = relative20(resolve28(workspace), path);
|
|
23002
23282
|
if (relativePath === ".." || relativePath.startsWith(`..${sep20}`)) continue;
|
|
23003
23283
|
hash.update(name);
|
|
23004
23284
|
if (remaining <= 0) continue;
|
|
@@ -23216,7 +23496,7 @@ async function registerConfiguredAdapters(providers, registry, environment, diag
|
|
|
23216
23496
|
const configured = { ...providers };
|
|
23217
23497
|
if (configured.openai === void 0 && environment.OPENAI_API_KEY) configured.openai = { type: "openai", apiKey: environment.OPENAI_API_KEY };
|
|
23218
23498
|
if (configured.anthropic === void 0 && environment.ANTHROPIC_API_KEY) configured.anthropic = { type: "anthropic", apiKey: environment.ANTHROPIC_API_KEY };
|
|
23219
|
-
const oauthTokenStore = createFileTokenStore(
|
|
23499
|
+
const oauthTokenStore = createFileTokenStore(join26(home, ".flavor-code", "auth.json"));
|
|
23220
23500
|
const registered = [];
|
|
23221
23501
|
let effectiveLlm;
|
|
23222
23502
|
for (const [name, provider] of Object.entries(configured)) {
|