impel-cli 0.17.14 → 0.17.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/package.json +1 -1
- package/src/apps.js +23 -0
- package/src/commands/apps.js +3 -2
- package/src/commands/sessions.js +29 -0
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -1355,6 +1355,29 @@ function mergeManagedChatGPTToml(current, managed) {
|
|
|
1355
1355
|
return `${managed}\n${remainder ? `\n${remainder.replace(/\s*$/u, "")}\n` : ""}`;
|
|
1356
1356
|
}
|
|
1357
1357
|
|
|
1358
|
+
/**
|
|
1359
|
+
* True when a managed ChatGPT profile exists but its config no longer routes
|
|
1360
|
+
* inference through the Impel gateway. The Codex desktop app rewrites
|
|
1361
|
+
* config.toml with its own settings writer (model picker, project trust,
|
|
1362
|
+
* migrations) and can drop the managed top-level `model_provider` key; without
|
|
1363
|
+
* it the built-in ChatGPT provider derives its inference endpoint from
|
|
1364
|
+
* chatgpt.com and sends the Impel token there, which chatgpt.com rejects with
|
|
1365
|
+
* 403 "Unknown personal access token". Stale-only refreshes treat this drift
|
|
1366
|
+
* as staleness so the profile heals immediately instead of waiting out the
|
|
1367
|
+
* manifest TTL.
|
|
1368
|
+
*/
|
|
1369
|
+
export function managedChatGPTConfigDrifted(paths) {
|
|
1370
|
+
let current;
|
|
1371
|
+
try {
|
|
1372
|
+
current = fs.readFileSync(path.join(paths.chatgpt.codexHome, "config.toml"), "utf8");
|
|
1373
|
+
} catch {
|
|
1374
|
+
return false; // No managed profile; install/update owns creating one.
|
|
1375
|
+
}
|
|
1376
|
+
return !current.includes(CHATGPT_CONFIG_START)
|
|
1377
|
+
|| readTopLevelTomlString(current, "model_provider") !== "impel"
|
|
1378
|
+
|| !readTopLevelTomlString(current, "chatgpt_base_url");
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1358
1381
|
function readTopLevelTomlString(toml, key) {
|
|
1359
1382
|
const topLevel = toml.split(/^\s*\[/mu, 1)[0];
|
|
1360
1383
|
const match = topLevel.match(new RegExp(`^\\s*${escapeRegex(key)}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")\\s*$`, "mu"));
|
package/src/commands/apps.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
ensureVendorApp,
|
|
33
33
|
fetchGatewayModels,
|
|
34
34
|
installManagedAppFiles,
|
|
35
|
+
managedChatGPTConfigDrifted,
|
|
35
36
|
managedLauncherName,
|
|
36
37
|
normalizeAppTarget,
|
|
37
38
|
quitBlockingApps,
|
|
@@ -662,7 +663,7 @@ export async function cmdWindowsApps(argv, overrides = {}) {
|
|
|
662
663
|
claudeUserData: io.claudeUserData(io.environment, staleTenantId),
|
|
663
664
|
tenantName: staleTenantId === stored.tenantId ? stored.tenantName : null,
|
|
664
665
|
});
|
|
665
|
-
if (manifestIsFresh(stalePaths, stored)) return true;
|
|
666
|
+
if (manifestIsFresh(stalePaths, stored) && !managedChatGPTConfigDrifted(stalePaths)) return true;
|
|
666
667
|
}
|
|
667
668
|
}
|
|
668
669
|
const config = await io.selectedConfig(targets, flags.tenant || null);
|
|
@@ -1241,7 +1242,7 @@ async function refreshApps(targets, { staleOnly = false, tenantId = null } = {},
|
|
|
1241
1242
|
? stored.tenantName || manifest?.tenantName
|
|
1242
1243
|
: manifest?.tenantName,
|
|
1243
1244
|
});
|
|
1244
|
-
if (staleOnly && manifestIsFresh(paths, stored)) return;
|
|
1245
|
+
if (staleOnly && manifestIsFresh(paths, stored) && !managedChatGPTConfigDrifted(paths)) return;
|
|
1245
1246
|
|
|
1246
1247
|
let config;
|
|
1247
1248
|
try {
|
package/src/commands/sessions.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
import { parseFlags } from "../args.js";
|
|
5
|
+
import { appPaths, managedChatGPTConfigDrifted } from "../apps.js";
|
|
4
6
|
import {
|
|
5
7
|
clearSessionFlushLock,
|
|
6
8
|
collectSessionHook,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
} from "../sessionCollector.js";
|
|
14
16
|
import { loadConfig, redactSecretText } from "../config.js";
|
|
15
17
|
import { impelCliInvocation } from "../selfInvocation.js";
|
|
18
|
+
import { spawnDetachedAppRefresh } from "../updates.js";
|
|
16
19
|
|
|
17
20
|
const SPEC = {
|
|
18
21
|
provider: { type: "string" },
|
|
@@ -22,6 +25,31 @@ const SPEC = {
|
|
|
22
25
|
"impel-managed-session-hook-v1": { type: "boolean" },
|
|
23
26
|
};
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Repair a managed ChatGPT profile the desktop app just rewrote out from under
|
|
30
|
+
* the gateway. Hooks are the only Impel code guaranteed to run while the app
|
|
31
|
+
* is broken (the app stops calling the provider token helper once the managed
|
|
32
|
+
* `model_provider` key is gone), so each codex hook event checks for drift and
|
|
33
|
+
* kicks one detached stale-only refresh — which the drift-aware staleness gate
|
|
34
|
+
* turns into a real repair. The heartbeat lock keeps repeated hook events from
|
|
35
|
+
* stacking refresh children while one is already running.
|
|
36
|
+
*/
|
|
37
|
+
export function maybeRepairManagedCodexApp(tenantId, {
|
|
38
|
+
drifted = managedChatGPTConfigDrifted,
|
|
39
|
+
lockIsFresh = sessionFlushLockIsFresh,
|
|
40
|
+
touchLock = touchSessionFlushLock,
|
|
41
|
+
spawnRefresh = spawnDetachedAppRefresh,
|
|
42
|
+
} = {}) {
|
|
43
|
+
if (!tenantId) return false;
|
|
44
|
+
const paths = appPaths(undefined, tenantId);
|
|
45
|
+
if (!drifted(paths)) return false;
|
|
46
|
+
const lock = path.join(paths.tenantRoot, "app-repair-heartbeat");
|
|
47
|
+
if (lockIsFresh(lock, 60_000)) return false;
|
|
48
|
+
touchLock(lock);
|
|
49
|
+
spawnRefresh(tenantId);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
function startDetachedFlush({ provider, tenant, session }) {
|
|
26
54
|
const invocation = impelCliInvocation([
|
|
27
55
|
"sessions",
|
|
@@ -65,6 +93,7 @@ export async function cmdSessions(argv) {
|
|
|
65
93
|
config,
|
|
66
94
|
flush: false,
|
|
67
95
|
});
|
|
96
|
+
if (flags.provider === "codex") maybeRepairManagedCodexApp(flags.tenant);
|
|
68
97
|
if (config && (config.tenantId === flags.tenant || process.env.IMPEL_SESSIONS_DEV_ORG_ID)) {
|
|
69
98
|
// Hooks fire on every session event; only spawn a flush child when no
|
|
70
99
|
// live one is already polling this session's outbox (heartbeat lock).
|