impel-cli 0.16.4 → 0.16.5
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 +26 -0
- package/package.json +1 -1
- package/src/apps.js +8 -2
- package/src/cli.js +5 -0
- package/src/cliProfiles.js +5 -1
- package/src/commands/sessions.js +114 -0
- package/src/config.js +8 -3
- package/src/sessionCollector.js +996 -0
- package/src/sessionHooks.js +242 -0
package/README.md
CHANGED
|
@@ -25,6 +25,32 @@ of the protected Store directory and launches it with tenant-specific Codex and
|
|
|
25
25
|
browser profiles. Each approach keeps the user's normal app profile and
|
|
26
26
|
signed-in account untouched.
|
|
27
27
|
|
|
28
|
+
Every Impel-managed Claude Code, Codex CLI, Claude Desktop, and ChatGPT/Codex
|
|
29
|
+
Desktop profile also installs tenant-scoped lifecycle hooks for cloud session
|
|
30
|
+
persistence. Hook input is first written to a private local outbox, so an
|
|
31
|
+
offline network, interrupted hook, force-quit, or client restart cannot make a
|
|
32
|
+
provider turn fail. The next hook invocation retries any pending batches,
|
|
33
|
+
including batches left by an older session. Remote payloads are stored as
|
|
34
|
+
independent zstd frames by `impel-sessions`; the local collector sends bounded
|
|
35
|
+
newline-complete transcript deltas and a compact lifecycle ledger. Pending data
|
|
36
|
+
is capped at 256 MiB or 2,048 batches per session by default; corrupt or
|
|
37
|
+
permanently invalid batches are quarantined so they cannot block later data.
|
|
38
|
+
Set `IMPEL_SESSIONS_MAX_OUTBOX_BYTES` or `IMPEL_SESSIONS_MAX_OUTBOX_BATCHES` to
|
|
39
|
+
lower those local limits.
|
|
40
|
+
|
|
41
|
+
The service attributes each session to the PAT-resolved user and selected
|
|
42
|
+
tenant and records the provider, CLI/desktop surface, provider session ID, and
|
|
43
|
+
repository metadata. Set `IMPEL_TASK_ID` before launching a client to link new
|
|
44
|
+
sessions explicitly to an Impel task for cross-device discovery. The collector
|
|
45
|
+
never writes the configured PAT into hook configuration, and it redacts Impel
|
|
46
|
+
PAT/tenant credentials found in hook input or transcript content before
|
|
47
|
+
spooling. Task linkage is retried independently and cannot block payload
|
|
48
|
+
persistence when the current PAT lacks the `tasks` scope.
|
|
49
|
+
|
|
50
|
+
Codex hook trust is pinned to the exact generated handler and its array index.
|
|
51
|
+
If you manually reorder the managed group in `hooks.json`, rerun `impel setup`,
|
|
52
|
+
`impel app update`, or relaunch the isolated CLI to regenerate the trust entry.
|
|
53
|
+
|
|
28
54
|
## Gateway-only vendor package
|
|
29
55
|
|
|
30
56
|
White-labelled gateway launchers import the deliberately narrow
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { normalizeTenantId } from "./tenants.js";
|
|
13
13
|
import { impelCliInvocation } from "./selfInvocation.js";
|
|
14
14
|
import { crossAppModelsEnabled, redactSecretText } from "./config.js";
|
|
15
|
+
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
15
16
|
import { ADHOC_IDENTITY, codesignIdentityArgs, desiredSigningMode, resolveSigningIdentity } from "./codesign.js";
|
|
16
17
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
17
18
|
|
|
@@ -163,7 +164,7 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
|
|
|
163
164
|
|
|
164
165
|
// Bump when the written config/manifest schema changes; a mismatch forces the
|
|
165
166
|
// slow open path (and thus a full config rewrite) after a CLI update.
|
|
166
|
-
export const CURRENT_CONFIG_VERSION =
|
|
167
|
+
export const CURRENT_CONFIG_VERSION = 16;
|
|
167
168
|
|
|
168
169
|
// Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
|
|
169
170
|
// helper rebranding, and signing. A vendored bundle is rebuilt only when this
|
|
@@ -617,8 +618,12 @@ export function installManagedAppFiles({
|
|
|
617
618
|
writeClaudeDefaultApp(paths);
|
|
618
619
|
writeClaudeCodeSettings(paths);
|
|
619
620
|
writeClaudeConfig(paths, config, models);
|
|
621
|
+
ensureClaudeSessionHooks(paths.claude.userData, config.tenantId, "claude_desktop");
|
|
622
|
+
}
|
|
623
|
+
else {
|
|
624
|
+
writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
|
|
625
|
+
ensureCodexSessionHooks(paths.chatgpt.codexHome, config.tenantId, "codex_desktop");
|
|
620
626
|
}
|
|
621
|
-
else writeChatGPTConfig(paths, config, models, vendorCodexModels, chatgptInvocations);
|
|
622
627
|
// Non-bundle targets are the background-refresh / fast-open path: configs,
|
|
623
628
|
// token helper, catalog, and manifest only. Bundle swaps require the app
|
|
624
629
|
// to be closed (see quitBlockingApps), so those paths never attempt one.
|
|
@@ -1024,6 +1029,7 @@ function writeChatGPTConfig(paths, config, models, vendorCodexModels, invocation
|
|
|
1024
1029
|
const managedToml = [
|
|
1025
1030
|
CHATGPT_CONFIG_START,
|
|
1026
1031
|
"# Managed by `impel app update`. This profile is isolated from ~/.codex.",
|
|
1032
|
+
"# Impel session-hook trust is pinned separately by exact handler hash.",
|
|
1027
1033
|
`model = ${tomlString(selectedModel.slug)}`,
|
|
1028
1034
|
'model_provider = "impel"',
|
|
1029
1035
|
`chatgpt_base_url = ${tomlString(`${config.gatewayUrl}/chatgpt_passthrough/backend-api`)}`,
|
package/src/cli.js
CHANGED
|
@@ -15,6 +15,7 @@ import { cmdAgents } from "./commands/agents.js";
|
|
|
15
15
|
import { cmdTenant } from "./commands/tenant.js";
|
|
16
16
|
import { cmdDoctor } from "./commands/doctor.js";
|
|
17
17
|
import { cmdSetup } from "./commands/setup.js";
|
|
18
|
+
import { cmdSessions } from "./commands/sessions.js";
|
|
18
19
|
import { cmdUpdate } from "./commands/update.js";
|
|
19
20
|
import { cmdExperimental } from "./commands/experimental.js";
|
|
20
21
|
|
|
@@ -121,6 +122,10 @@ export async function main(argv) {
|
|
|
121
122
|
case "mcp":
|
|
122
123
|
return cmdMcp(rest);
|
|
123
124
|
|
|
125
|
+
// Hidden transport used by managed Claude/Codex lifecycle hooks.
|
|
126
|
+
case "sessions":
|
|
127
|
+
return cmdSessions(rest);
|
|
128
|
+
|
|
124
129
|
case "claude":
|
|
125
130
|
case "codex":
|
|
126
131
|
return cmdLaunch(cmd, rest);
|
package/src/cliProfiles.js
CHANGED
|
@@ -12,6 +12,7 @@ import { CONFIG_DIR } from "./config.js";
|
|
|
12
12
|
import { normalizeTenantId } from "./tenants.js";
|
|
13
13
|
import { impelCliInvocation, impelMcpInvocation } from "./selfInvocation.js";
|
|
14
14
|
import { applyImpelClaudeSandbox } from "./claudeSandbox.js";
|
|
15
|
+
import { ensureClaudeSessionHooks, ensureCodexSessionHooks } from "./sessionHooks.js";
|
|
15
16
|
|
|
16
17
|
export const IMPEL_CLI_PROFILES_DIR = path.join(CONFIG_DIR, "cli");
|
|
17
18
|
|
|
@@ -131,6 +132,7 @@ export function ensureImpelClaudeProfile(gatewayUrl, tenantId, { crossAppModels
|
|
|
131
132
|
|
|
132
133
|
writePrivateFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`);
|
|
133
134
|
writePrivateFile(userConfigPath, `${JSON.stringify(userConfig, null, 2)}\n`);
|
|
135
|
+
ensureClaudeSessionHooks(configDir, tenantId, "claude_cli");
|
|
134
136
|
|
|
135
137
|
return { configDir, settingsPath, userConfigPath };
|
|
136
138
|
}
|
|
@@ -203,9 +205,10 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
203
205
|
|
|
204
206
|
const { preamble, rest } = splitTomlPreamble(withoutManagedBlock);
|
|
205
207
|
const providerLine = 'model_provider = "impel"';
|
|
206
|
-
|
|
208
|
+
let nextPreamble = CODEX_PROVIDER_LINE_RE.test(preamble)
|
|
207
209
|
? preamble.replace(CODEX_PROVIDER_LINE_RE, providerLine)
|
|
208
210
|
: `${preamble.trimEnd()}${preamble.trim() ? "\n" : ""}${providerLine}\n`;
|
|
211
|
+
nextPreamble = nextPreamble.replace(/^bypass_hook_trust[ \t]*=[ \t]*(?:true|false)[ \t]*\n?/gm, "");
|
|
209
212
|
const restText = rest.trim();
|
|
210
213
|
const next = [nextPreamble.trimEnd(), codexManagedBlock(gatewayUrl, tenantId), restText]
|
|
211
214
|
.filter(Boolean)
|
|
@@ -213,6 +216,7 @@ export function ensureImpelCodexProfile(gatewayUrl, tenantId) {
|
|
|
213
216
|
.concat("\n");
|
|
214
217
|
|
|
215
218
|
writePrivateFile(configPath, hardenManagedCodexToml(next, configPath));
|
|
219
|
+
ensureCodexSessionHooks(codexHome, tenantId, "codex_cli");
|
|
216
220
|
secureManagedCodexHome(codexHome);
|
|
217
221
|
return { codexHome, configPath };
|
|
218
222
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
import { parseFlags } from "../args.js";
|
|
4
|
+
import { collectSessionHook, flushCollectedSession, readHookInput, sessionOutboxStatus } from "../sessionCollector.js";
|
|
5
|
+
import { loadConfig, redactSecretText } from "../config.js";
|
|
6
|
+
import { impelCliInvocation } from "../selfInvocation.js";
|
|
7
|
+
|
|
8
|
+
const SPEC = {
|
|
9
|
+
provider: { type: "string" },
|
|
10
|
+
surface: { type: "string" },
|
|
11
|
+
tenant: { type: "string" },
|
|
12
|
+
session: { type: "string" },
|
|
13
|
+
"impel-managed-session-hook-v1": { type: "boolean" },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function startDetachedFlush({ provider, tenant, session }) {
|
|
17
|
+
const invocation = impelCliInvocation([
|
|
18
|
+
"sessions",
|
|
19
|
+
"flush",
|
|
20
|
+
"--provider",
|
|
21
|
+
provider,
|
|
22
|
+
"--tenant",
|
|
23
|
+
tenant,
|
|
24
|
+
"--session",
|
|
25
|
+
session,
|
|
26
|
+
"--impel-managed-session-hook-v1",
|
|
27
|
+
]);
|
|
28
|
+
const child = spawn(invocation.command, invocation.args, {
|
|
29
|
+
detached: true,
|
|
30
|
+
stdio: "ignore",
|
|
31
|
+
env: { ...process.env, IMPEL_SESSIONS_FLUSH_CHILD: "1" },
|
|
32
|
+
windowsHide: true,
|
|
33
|
+
});
|
|
34
|
+
child.once("error", () => {});
|
|
35
|
+
child.unref();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function cmdSessions(argv) {
|
|
39
|
+
const [action, ...rest] = argv;
|
|
40
|
+
if (action === "hook") {
|
|
41
|
+
try {
|
|
42
|
+
const { flags, positionals } = parseFlags(rest, SPEC);
|
|
43
|
+
if (positionals.length > 0) throw new Error("unexpected positional arguments");
|
|
44
|
+
const input = await readHookInput();
|
|
45
|
+
let config = null;
|
|
46
|
+
try {
|
|
47
|
+
config = loadConfig();
|
|
48
|
+
} catch {
|
|
49
|
+
// Capture remains available even while the user's config needs repair.
|
|
50
|
+
}
|
|
51
|
+
await collectSessionHook({
|
|
52
|
+
provider: flags.provider,
|
|
53
|
+
surface: flags.surface,
|
|
54
|
+
tenantId: flags.tenant,
|
|
55
|
+
input,
|
|
56
|
+
config,
|
|
57
|
+
flush: false,
|
|
58
|
+
});
|
|
59
|
+
if (config && (config.tenantId === flags.tenant || process.env.IMPEL_SESSIONS_DEV_ORG_ID)) {
|
|
60
|
+
startDetachedFlush({
|
|
61
|
+
provider: flags.provider,
|
|
62
|
+
tenant: flags.tenant,
|
|
63
|
+
session: String(input.session_id || ""),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Session persistence is observational. A collector outage must never
|
|
68
|
+
// block a model turn or change a provider hook's decision semantics.
|
|
69
|
+
if (process.env.IMPEL_SESSIONS_DEBUG === "1") {
|
|
70
|
+
console.error(`impel sessions hook: ${redactSecretText(error?.message || error)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const { flags, positionals } = parseFlags(rest, SPEC);
|
|
76
|
+
if (positionals.length > 0) {
|
|
77
|
+
console.error("impel sessions: unexpected positional arguments");
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const config = loadConfig();
|
|
82
|
+
if (action === "flush") {
|
|
83
|
+
if (!flags.provider || !flags.session || !flags.tenant) return;
|
|
84
|
+
if (!["claude_code", "codex"].includes(flags.provider)) return;
|
|
85
|
+
if (!config || flags.tenant !== config.tenantId && !process.env.IMPEL_SESSIONS_DEV_ORG_ID) return;
|
|
86
|
+
const deadline = Date.now() + 90_000;
|
|
87
|
+
while (Date.now() < deadline) {
|
|
88
|
+
const result = await flushCollectedSession({
|
|
89
|
+
tenantId: flags.tenant,
|
|
90
|
+
provider: flags.provider,
|
|
91
|
+
sessionKey: flags.session,
|
|
92
|
+
config,
|
|
93
|
+
});
|
|
94
|
+
if (result.pending === 0) break;
|
|
95
|
+
await new Promise((resolve) => setTimeout(resolve, result.error || result.busy ? 1000 : 250));
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (action === "status") {
|
|
100
|
+
if (!flags.provider || !flags.session || !flags.tenant) {
|
|
101
|
+
console.error("impel sessions status requires --provider, --session, and --tenant");
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
console.log(JSON.stringify(sessionOutboxStatus({
|
|
106
|
+
tenantId: flags.tenant,
|
|
107
|
+
provider: flags.provider,
|
|
108
|
+
sessionKey: flags.session,
|
|
109
|
+
}), null, 2));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
console.error("impel sessions: use `hook` (managed internally) or `status`");
|
|
113
|
+
process.exitCode = 1;
|
|
114
|
+
}
|
package/src/config.js
CHANGED
|
@@ -89,11 +89,16 @@ const PAT_RE = /impel_pat_[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?/gu;
|
|
|
89
89
|
const ANSI_ESCAPE_RE = /\u001B(?:\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\[[0-?]*[ -/]*[@-~]|[@-_])/gu;
|
|
90
90
|
const TERMINAL_CONTROL_RE = /[\u0000-\u001F\u007F-\u009F]/gu;
|
|
91
91
|
|
|
92
|
-
/**
|
|
93
|
-
export function
|
|
92
|
+
/** Redact Impel bearer credentials without changing JSON/NDJSON whitespace. */
|
|
93
|
+
export function redactCredentialText(value) {
|
|
94
94
|
return String(value ?? "")
|
|
95
95
|
.replace(TENANT_CREDENTIAL_RE, "[REDACTED IMPEL CREDENTIAL]")
|
|
96
|
-
.replace(PAT_RE, "[REDACTED IMPEL CREDENTIAL]")
|
|
96
|
+
.replace(PAT_RE, "[REDACTED IMPEL CREDENTIAL]");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Remove credentials and terminal control sequences from untrusted text. */
|
|
100
|
+
export function redactSecretText(value) {
|
|
101
|
+
return redactCredentialText(value)
|
|
97
102
|
.replace(ANSI_ESCAPE_RE, "")
|
|
98
103
|
.replace(TERMINAL_CONTROL_RE, " ");
|
|
99
104
|
}
|