@tiens.nguyen/gu-cli 1.0.716 → 1.0.717
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/gu-repl.mjs +111 -6
- package/package.json +1 -1
package/gu-repl.mjs
CHANGED
|
@@ -4473,6 +4473,26 @@ async function loadSessionMaxCodeTokens(cwd) {
|
|
|
4473
4473
|
* and the URL is what the global output-token total is keyed to (task #125) — restoring a
|
|
4474
4474
|
* subset would leave the footer describing one backend while the turns went to another.
|
|
4475
4475
|
*/
|
|
4476
|
+
/**
|
|
4477
|
+
* Mirror this folder's coding choice to the SAME database that stores the key
|
|
4478
|
+
* material (task #225) — cloud Mongo in hosted mode, localhost Mongo in Client
|
|
4479
|
+
* Expert — so the choice and the credentials always live in one consistent store.
|
|
4480
|
+
* The local session file stays as a fast offline cache. A failure is reported,
|
|
4481
|
+
* never silent, so a lost connection is visible rather than a stale choice later.
|
|
4482
|
+
*/
|
|
4483
|
+
async function persistCodingChoiceToServer(cwd) {
|
|
4484
|
+
try {
|
|
4485
|
+
await saveCodingChoice(folderHashFor(cwd), {
|
|
4486
|
+
codingKeyId: sessionCodingKeyId,
|
|
4487
|
+
codingModel: sessionCodingModel,
|
|
4488
|
+
codingLabel: sessionCodingLabel,
|
|
4489
|
+
codingUrl: sessionCodingUrl,
|
|
4490
|
+
});
|
|
4491
|
+
} catch (err) {
|
|
4492
|
+
console.error(dim(`(coding choice could not reach the database: ${err.message})`));
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4476
4496
|
/**
|
|
4477
4497
|
* Persist the /model pick THE MOMENT IT IS MADE.
|
|
4478
4498
|
*
|
|
@@ -4497,6 +4517,8 @@ async function saveSessionCoding(cwd) {
|
|
|
4497
4517
|
// carry on with the pick applied for this session.
|
|
4498
4518
|
console.error(dim(`(coding model save failed: ${err.message})`));
|
|
4499
4519
|
}
|
|
4520
|
+
// ALSO mirrored to the database store (task #225), same place the credentials live.
|
|
4521
|
+
await persistCodingChoiceToServer(cwd);
|
|
4500
4522
|
}
|
|
4501
4523
|
|
|
4502
4524
|
/**
|
|
@@ -4516,6 +4538,8 @@ async function saveSessionCodingKeyId(cwd) {
|
|
|
4516
4538
|
} catch (err) {
|
|
4517
4539
|
console.error(dim(`(coding key choice save failed: ${err.message})`));
|
|
4518
4540
|
}
|
|
4541
|
+
// ALSO mirrored to the same database store that holds the credentials (task #225).
|
|
4542
|
+
await persistCodingChoiceToServer(cwd);
|
|
4519
4543
|
}
|
|
4520
4544
|
|
|
4521
4545
|
async function loadSessionCodingKeyId(cwd) {
|
|
@@ -7500,6 +7524,46 @@ async function deleteCodingKey(id) {
|
|
|
7500
7524
|
}
|
|
7501
7525
|
}
|
|
7502
7526
|
|
|
7527
|
+
/**
|
|
7528
|
+
* This folder's stored coding choice, from the SAME Mongo that holds the key secrets
|
|
7529
|
+
* (task #225). Returns {codingKeyId, codingModel, codingLabel, codingUrl} or null when none
|
|
7530
|
+
* is stored for this folder. THROWS on a network/HTTP failure so the caller can say WHY —
|
|
7531
|
+
* the choice and the secret now live in one store, so "cannot reach it" is a real error,
|
|
7532
|
+
* not a silent fall-back.
|
|
7533
|
+
*/
|
|
7534
|
+
async function fetchCodingChoice(folderHash) {
|
|
7535
|
+
const r = await fetch(`${apiBase}/api/worker/coding-choice?folder=${encodeURIComponent(folderHash)}`, {
|
|
7536
|
+
headers: { "X-Worker-Key": workerKey },
|
|
7537
|
+
signal: AbortSignal.timeout(API_CALL_TIMEOUT_MS),
|
|
7538
|
+
});
|
|
7539
|
+
if (!r.ok) throw new Error(`coding-choice GET failed: HTTP ${r.status}`);
|
|
7540
|
+
const j = await r.json().catch(() => ({}));
|
|
7541
|
+
const c = j?.choice;
|
|
7542
|
+
if (!c || typeof c !== "object") return null;
|
|
7543
|
+
const str = (x) => (typeof x === "string" ? x : "");
|
|
7544
|
+
return {
|
|
7545
|
+
codingKeyId: str(c.codingKeyId),
|
|
7546
|
+
codingModel: str(c.codingModel),
|
|
7547
|
+
codingLabel: str(c.codingLabel),
|
|
7548
|
+
codingUrl: str(c.codingUrl),
|
|
7549
|
+
};
|
|
7550
|
+
}
|
|
7551
|
+
|
|
7552
|
+
/** Persist this folder's coding choice to the SAME Mongo that holds the key secrets (task #225). */
|
|
7553
|
+
async function saveCodingChoice(folderHash, choice) {
|
|
7554
|
+
const r = await fetch(`${apiBase}/api/worker/coding-choice`, {
|
|
7555
|
+
method: "POST",
|
|
7556
|
+
headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
|
|
7557
|
+
body: JSON.stringify({ folder: folderHash, ...choice }),
|
|
7558
|
+
signal: AbortSignal.timeout(API_CALL_TIMEOUT_MS),
|
|
7559
|
+
});
|
|
7560
|
+
if (!r.ok) throw new Error(`coding-choice POST failed: HTTP ${r.status}`);
|
|
7561
|
+
}
|
|
7562
|
+
|
|
7563
|
+
/** The folder-hash key the choice is stored under — the SAME sha256-32 the session file uses. */
|
|
7564
|
+
function folderHashFor(cwd) {
|
|
7565
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 32);
|
|
7566
|
+
}
|
|
7503
7567
|
/**
|
|
7504
7568
|
* Ask for a name, then the secret, and store it. Returns the new id or "".
|
|
7505
7569
|
*
|
|
@@ -10000,15 +10064,56 @@ async function main() {
|
|
|
10000
10064
|
sessionMaxCodeTokens = await loadSessionMaxCodeTokens(resolve(process.cwd()));
|
|
10001
10065
|
sessionRagStorage = await loadSessionRagStorage(resolve(process.cwd())); // task #133/#180
|
|
10002
10066
|
// Restored here, VALIDATED below once the probe says what this account actually allows.
|
|
10067
|
+
// The CHOICE lives in the SAME Mongo as the key secrets (task #225) — the local session file
|
|
10068
|
+
// is only a cache. The database is read FIRST; a failure to reach it is a real error, said
|
|
10069
|
+
// aloud, never a silent fall-back to a stale file.
|
|
10003
10070
|
{
|
|
10004
|
-
const
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10071
|
+
const folderHash = folderHashFor(resolve(process.cwd()));
|
|
10072
|
+
let dbChoice = null;
|
|
10073
|
+
let dbUnreachable = false;
|
|
10074
|
+
try {
|
|
10075
|
+
dbChoice = await fetchCodingChoice(folderHash);
|
|
10076
|
+
} catch (err) {
|
|
10077
|
+
// Local mode has no "offline" — the database is on THIS machine, so a failure means the
|
|
10078
|
+
// local mongodb is unreachable. Cloud mode failing to reach the hosted API means the
|
|
10079
|
+
// computer has lost its internet connection. Two different messages, one per mode.
|
|
10080
|
+
dbUnreachable = true;
|
|
10081
|
+
if (isLocalApiBase(apiBase)) {
|
|
10082
|
+
console.error(dim("(cannot access local mongodb — your saved model/key choice is unavailable)"));
|
|
10083
|
+
} else {
|
|
10084
|
+
console.error(dim("(your computer has lost its internet connection — your saved model/key choice is unavailable)"));
|
|
10085
|
+
}
|
|
10086
|
+
}
|
|
10087
|
+
if (dbChoice) {
|
|
10088
|
+
if (dbChoice.codingModel) {
|
|
10089
|
+
sessionCodingModel = dbChoice.codingModel;
|
|
10090
|
+
sessionCodingLabel = dbChoice.codingLabel;
|
|
10091
|
+
sessionCodingUrl = dbChoice.codingUrl;
|
|
10092
|
+
}
|
|
10093
|
+
if (dbChoice.codingKeyId) sessionCodingKeyId = dbChoice.codingKeyId;
|
|
10094
|
+
// Refresh the local cache so the file matches the database.
|
|
10095
|
+
try {
|
|
10096
|
+
await updateSessionFile(resolve(process.cwd()), (raw) => {
|
|
10097
|
+
raw.codingKeyId = dbChoice.codingKeyId;
|
|
10098
|
+
raw.codingModel = dbChoice.codingModel;
|
|
10099
|
+
raw.codingLabel = dbChoice.codingLabel;
|
|
10100
|
+
raw.codingUrl = dbChoice.codingUrl;
|
|
10101
|
+
raw.updatedAt = new Date().toISOString();
|
|
10102
|
+
});
|
|
10103
|
+
} catch { /* cache write is best-effort */ }
|
|
10104
|
+
} else if (!dbUnreachable) {
|
|
10105
|
+
// The database WAS reached but has no choice for this folder — fall back to the local file
|
|
10106
|
+
// (a folder chosen before task #225 existed has its choice ONLY in the file). When the
|
|
10107
|
+
// database could NOT be reached, the error above already said so and there is no fallback.
|
|
10108
|
+
const c = await loadSessionCoding(resolve(process.cwd()));
|
|
10109
|
+
if (c) {
|
|
10110
|
+
sessionCodingModel = c.model;
|
|
10111
|
+
sessionCodingLabel = c.label;
|
|
10112
|
+
sessionCodingUrl = c.url;
|
|
10113
|
+
}
|
|
10114
|
+
sessionCodingKeyId = await loadSessionCodingKeyId(resolve(process.cwd())); // task #222
|
|
10009
10115
|
}
|
|
10010
10116
|
}
|
|
10011
|
-
sessionCodingKeyId = await loadSessionCodingKeyId(resolve(process.cwd())); // task #222
|
|
10012
10117
|
// Fetch the API key tail after loading the session, if a key is configured
|
|
10013
10118
|
if (sessionCodingKeyId) {
|
|
10014
10119
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tiens.nguyen/gu-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.717",
|
|
4
4
|
"description": "Gu CLI — the gu terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|