impel-cli 0.20.15 → 0.20.17

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 CHANGED
@@ -378,10 +378,14 @@ control-plane HTTP bearer credential, and fixes tenant selection in the
378
378
  transport header. Generated Claude/Codex MCP entries contain neither the PAT
379
379
  nor a tenant-derived credential.
380
380
 
381
- - The pinned Impel Claude Desktop can render the standard MCP Apps Tasks
382
- resource when its host supports that protocol.
383
- - Claude Code and stable Codex receive complete headless task tools and text
384
- results; no experimental Codex MCP Apps flag is enabled.
381
+ - The pinned Impel Claude Desktop renders the standard MCP Apps task board when
382
+ its host supports that protocol.
383
+ - The exact pinned Impel Codex desktop profile enables its reviewed
384
+ `enable_mcp_apps` contract so `show_tasks` can render the same board. The
385
+ vendor-pin test fails closed if the embedded Codex binary stops accepting it.
386
+ - Claude Code and Codex CLI profiles remain headless and receive complete task
387
+ tools plus text results; the desktop-only renderer flag is never written to
388
+ those profiles.
385
389
  - Profiles preserve unrelated MCP servers. If a user-authored server already
386
390
  owns the `impel-tasks` name, setup/update fails with remediation instead of
387
391
  overwriting it.
package/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.17 — Managed desktop task views
4
+
5
+ - Enables the reviewed MCP Apps renderer only in exact managed Codex Desktop
6
+ profiles, while keeping Codex CLI profiles on their text and tool fallback.
7
+ - Captures Claude permission, notification, and task lifecycle events through
8
+ the existing asynchronous, tenant-bound session hooks.
9
+ - Preserves the additional lifecycle context needed to correlate task updates
10
+ without expanding the oversized-hook payload boundary.
11
+
12
+ ## 0.20.16 — Correlated Codex transport timing
13
+
14
+ - Derives native-agent MCP duration from correlated, timestamped Codex rollout
15
+ events when durable profiles correctly omit transient telemetry destinations.
16
+ - Retries one unestablished tenant-preflight result while still requiring one
17
+ exact selected tenant before every profiled Codex attempt.
18
+
3
19
  ## 0.20.15 — Direct Codex native agents
4
20
 
5
21
  - Classifies the exact `mcp__impel_agent` namespace as direct-only in generated
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.15",
3
+ "version": "0.20.17",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -151,12 +151,15 @@ export function summarizeCodexRollouts(rollouts) {
151
151
  let inputTokens = 0;
152
152
  let cachedInputTokens = 0;
153
153
  let outputTokens = 0;
154
+ let mcpDurationMs = 0;
155
+ let mcpCompletedCalls = 0;
154
156
 
155
157
  for (const events of rollouts) {
156
158
  eventCount += events.length;
157
159
  const metadata = rolloutSessionMetadata(events);
158
160
  const scopedCounts = metadata.parentThreadId ? childToolCounts : parentToolCounts;
159
161
  let latestUsage = null;
162
+ const mcpStarts = new Map();
160
163
  for (const event of events) {
161
164
  const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
162
165
  if (event.type === "response_item"
@@ -166,6 +169,27 @@ export function summarizeCodexRollouts(rollouts) {
166
169
  increment(scopedCounts, tool);
167
170
  if (tool === "exec" || payload.type === "custom_tool_call") codeCells += tool === "exec" ? 1 : 0;
168
171
  if (tool === "wait") codeWaits += 1;
172
+ const startedAt = Date.parse(event.timestamp || "");
173
+ if (SAFE_MCP_TOOLS.has(tool)
174
+ && typeof payload.call_id === "string"
175
+ && Number.isFinite(startedAt)) {
176
+ mcpStarts.set(payload.call_id, startedAt);
177
+ }
178
+ }
179
+ if (event.type === "event_msg"
180
+ && payload.type === "mcp_tool_call_end"
181
+ && typeof payload.call_id === "string") {
182
+ const startedAt = mcpStarts.get(payload.call_id);
183
+ const endedAt = Date.parse(event.timestamp || "");
184
+ const duration = endedAt - startedAt;
185
+ if (Number.isFinite(startedAt)
186
+ && Number.isFinite(endedAt)
187
+ && duration >= 0
188
+ && duration <= 24 * 60 * 60 * 1000) {
189
+ mcpDurationMs += duration;
190
+ mcpCompletedCalls += 1;
191
+ mcpStarts.delete(payload.call_id);
192
+ }
169
193
  }
170
194
  if (event.type === "event_msg" && payload.type === "token_count") {
171
195
  const usage = payload.info?.total_token_usage;
@@ -190,6 +214,8 @@ export function summarizeCodexRollouts(rollouts) {
190
214
  inputTokens,
191
215
  cachedInputTokens,
192
216
  outputTokens,
217
+ mcpDurationMs,
218
+ mcpCompletedCalls,
193
219
  };
194
220
  }
195
221
 
@@ -249,7 +275,9 @@ export function summarizeRawAttempt({ stdoutText, telemetryText = "", rolloutTex
249
275
  codeWaits: rollout.codeWaits,
250
276
  inputTokens: rollout.inputTokens,
251
277
  cachedInputTokens: rollout.cachedInputTokens,
252
- outputTokens: rollout.outputTokens,
278
+ outputTokens: rollout.outputTokens,
279
+ mcpDurationMs: rollout.mcpDurationMs,
280
+ mcpCompletedCalls: rollout.mcpCompletedCalls,
253
281
  } : {}),
254
282
  parentToolCounts: rollout.parentToolCounts,
255
283
  childToolCounts: rollout.childToolCounts,
@@ -211,13 +211,16 @@ function runCaptured(command, args, { environment = process.env, timeoutMs = 60_
211
211
  }
212
212
 
213
213
  async function currentTenant(impelBinary, environment) {
214
- const invocation = impelInvocation(impelBinary, ["tenant", "current"], environment);
215
- const result = await runCaptured(invocation.command, invocation.args, { environment });
216
- const lines = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
217
- if (result.code !== 0 || lines.length !== 1 || !/^[A-Za-z0-9_.:-]{1,160}$/u.test(lines[0])) {
218
- throw new Error("could not establish one exact current Impel tenant");
214
+ for (let attempt = 0; attempt < 2; attempt += 1) {
215
+ const invocation = impelInvocation(impelBinary, ["tenant", "current"], environment);
216
+ const result = await runCaptured(invocation.command, invocation.args, { environment });
217
+ const lines = result.stdout.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean);
218
+ if (result.code === 0 && lines.length === 1 && /^[A-Za-z0-9_.:-]{1,160}$/u.test(lines[0])) {
219
+ return lines[0];
220
+ }
221
+ if (attempt === 0) await delay(250);
219
222
  }
220
- return lines[0];
223
+ throw new Error("could not establish one exact current Impel tenant after two bounded attempts");
221
224
  }
222
225
 
223
226
  export async function assertExpectedTenant({ expectedTenant, impelBinary = "impel", environment = process.env }) {
@@ -373,7 +376,10 @@ function safeAttemptRecord({ options, index, processResult, summary, jsonValid,
373
376
  const codex = summary?.codex || {};
374
377
  const telemetry = summary?.telemetry || {};
375
378
  const durationMs = processResult.endedAtMs - processResult.startedAtMs;
376
- const mcpDurationMs = telemetry.mcpDurationMs || 0;
379
+ const telemetryCompleted = telemetry.eventCounts?.local_tool_completed || 0;
380
+ const mcpDurationMs = telemetryCompleted > 0
381
+ ? telemetry.mcpDurationMs || 0
382
+ : codex.mcpDurationMs || 0;
377
383
  const startCalls = (codex.toolCounts?.answer_native_agent || 0)
378
384
  + (codex.toolCounts?.run_native_agent || 0);
379
385
  const success = processResult.exitCode === 0
@@ -414,6 +420,9 @@ function safeAttemptRecord({ options, index, processResult, summary, jsonValid,
414
420
  cachedInputTokens: codex.cachedInputTokens || 0,
415
421
  outputTokens: codex.outputTokens || 0,
416
422
  mcpDurationMs,
423
+ mcpDurationSource: telemetryCompleted > 0
424
+ ? "telemetry"
425
+ : (codex.mcpCompletedCalls || 0) > 0 ? "rollout" : "unavailable",
417
426
  nonMcpOverheadMs: Math.max(0, durationMs - mcpDurationMs),
418
427
  telemetryEventCount: telemetry.eventCount || 0,
419
428
  telemetryCorrelationCount: telemetry.correlationCount || 0,
package/src/apps.js CHANGED
@@ -5,6 +5,7 @@ import crypto from "node:crypto";
5
5
  import { spawnSync } from "node:child_process";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import {
8
+ enableManagedCodexDesktopMcpApps,
8
9
  hardenManagedCodexToml,
9
10
  secureAllManagedCodexHomes,
10
11
  secureManagedCodexHome,
@@ -281,7 +282,9 @@ export function managedAppIdentity(target, tenantId = null, tenantName = null) {
281
282
  // expose direct-answer native agents through their one-shot local MCP binding.
282
283
  // 30: generate integrity-tracked top-level Codex profiles and direct code-mode
283
284
  // namespaces for fixed native-agent bindings.
284
- export const CURRENT_CONFIG_VERSION = 30;
285
+ // 31: enable the reviewed MCP Apps renderer in pinned Codex Desktop profiles
286
+ // and install Claude task, notification, and permission lifecycle hooks.
287
+ export const CURRENT_CONFIG_VERSION = 31;
285
288
 
286
289
  // Identifies the bundle-BUILDING logic — the asar patches, plist rewrites,
287
290
  // helper rebranding, and signing. A vendored bundle is rebuilt only when this
@@ -1400,7 +1403,12 @@ function writeChatGPTConfig(
1400
1403
  CHATGPT_CONFIG_END,
1401
1404
  ].join("\n");
1402
1405
  const mergedToml = mergeManagedChatGPTToml(preservedToml, managedToml);
1403
- writeAtomic(configPath, hardenManagedCodexToml(mergedToml, configPath), 0o600);
1406
+ const hardenedToml = hardenManagedCodexToml(mergedToml, configPath);
1407
+ writeAtomic(
1408
+ configPath,
1409
+ enableManagedCodexDesktopMcpApps(hardenedToml, configPath),
1410
+ 0o600,
1411
+ );
1404
1412
  secureManagedCodexHome(paths.chatgpt.codexHome);
1405
1413
  }
1406
1414
 
@@ -148,6 +148,24 @@ export function hardenManagedCodexToml(toml, configPath = "managed Codex config"
148
148
  return applyManagedCodexSandboxPolicy(withoutCredentialSnapshots, configPath);
149
149
  }
150
150
 
151
+ /**
152
+ * Enable the standard MCP Apps renderer only in the exact pinned desktop
153
+ * profile. CLI profiles deliberately stay headless, and vendor-pin contract
154
+ * tests must prove that the embedded Codex binary still recognizes this flag.
155
+ */
156
+ export function enableManagedCodexDesktopMcpApps(
157
+ toml,
158
+ configPath = "managed Codex desktop config",
159
+ ) {
160
+ return upsertManagedScalar(
161
+ toml,
162
+ "features",
163
+ "enable_mcp_apps",
164
+ "true",
165
+ configPath,
166
+ );
167
+ }
168
+
151
169
  function assertSafeManagedPath(target, expectedType) {
152
170
  const stat = lstatOrNull(target);
153
171
  if (!stat) return null;
@@ -236,7 +236,8 @@ function compactLedgerPayload(input) {
236
236
  const keys = [
237
237
  "session_id", "hook_event_name", "cwd", "permission_mode", "model", "source", "reason",
238
238
  "turn_id", "prompt_id", "agent_id", "agent_type", "tool_name", "tool_use_id", "error",
239
- "error_details", "last_assistant_message",
239
+ "error_details", "last_assistant_message", "notification_type", "message", "title",
240
+ "task_id", "task_subject", "task_description", "teammate_name", "tool_input",
240
241
  ];
241
242
  const payload = {};
242
243
  for (const key of keys) {
@@ -12,10 +12,14 @@ const CODEX_TRUST_END = `# <<< ${RUNTIME_BRAND.product.id} managed session hook
12
12
  export const CLAUDE_SESSION_EVENTS = Object.freeze([
13
13
  "SessionStart",
14
14
  "UserPromptSubmit",
15
+ "PermissionRequest",
15
16
  "PostToolUse",
16
17
  "PostToolUseFailure",
18
+ "Notification",
17
19
  "SubagentStart",
18
20
  "SubagentStop",
21
+ "TaskCreated",
22
+ "TaskCompleted",
19
23
  "PreCompact",
20
24
  "PostCompact",
21
25
  "Stop",