@canonmsg/codex-plugin 0.9.4 → 0.9.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/dist/adapter.d.ts CHANGED
@@ -60,6 +60,7 @@ export declare class CodexConversationAdapter {
60
60
  bypassApprovalsAndSandbox?: boolean;
61
61
  });
62
62
  getThreadId(): string | null;
63
+ clearThreadId(): void;
63
64
  setModel(model: string | null): void;
64
65
  isRunning(): boolean;
65
66
  interrupt(): Promise<void>;
package/dist/adapter.js CHANGED
@@ -31,6 +31,9 @@ export class CodexConversationAdapter {
31
31
  getThreadId() {
32
32
  return this.threadId;
33
33
  }
34
+ clearThreadId() {
35
+ this.threadId = null;
36
+ }
34
37
  setModel(model) {
35
38
  this.model = model;
36
39
  }
@@ -1,2 +1,2 @@
1
- export declare function isDirectExecution(moduleUrl: string): boolean;
2
- export declare function runCli(moduleUrl: string, main: () => void | Promise<void>, onError: (error: unknown) => void): void;
1
+ export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
2
+ export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
package/dist/cli-entry.js CHANGED
@@ -1,26 +1 @@
1
- import { realpathSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
- export function isDirectExecution(moduleUrl) {
5
- const entry = process.argv[1];
6
- if (!entry)
7
- return false;
8
- const abs = resolve(entry);
9
- let real = abs;
10
- try {
11
- real = realpathSync(abs);
12
- }
13
- catch {
14
- // argv[1] may not exist on disk in rare harnesses; fall back to abs
15
- }
16
- return (pathToFileURL(real).href === moduleUrl ||
17
- pathToFileURL(abs).href === moduleUrl);
18
- }
19
- export function runCli(moduleUrl, main, onError) {
20
- if (!isDirectExecution(moduleUrl)) {
21
- return;
22
- }
23
- Promise.resolve()
24
- .then(() => main())
25
- .catch(onError);
26
- }
1
+ export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
@@ -0,0 +1,2 @@
1
+ import { type CodexCliVersionStatus } from './error-format.js';
2
+ export declare function detectCodexCliVersion(codexBin: string, timeoutMs?: number): CodexCliVersionStatus;
@@ -0,0 +1,22 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { parseCodexCliVersion, } from './error-format.js';
3
+ export function detectCodexCliVersion(codexBin, timeoutMs = 2_000) {
4
+ const result = spawnSync(codexBin, ['--version'], {
5
+ encoding: 'utf-8',
6
+ timeout: timeoutMs,
7
+ });
8
+ const raw = [result.stdout, result.stderr]
9
+ .filter((value) => typeof value === 'string' && value.trim().length > 0)
10
+ .join('\n')
11
+ .trim();
12
+ return {
13
+ codexBin,
14
+ raw: raw || null,
15
+ version: parseCodexCliVersion(raw),
16
+ error: result.error
17
+ ? result.error.message
18
+ : result.status === 0
19
+ ? null
20
+ : `codex --version exited with status ${result.status ?? 'unknown'}`,
21
+ };
22
+ }
@@ -0,0 +1,16 @@
1
+ export declare const MIN_CODEX_VERSION_FOR_GPT_5_5 = "0.125.0";
2
+ export interface CodexCliVersionStatus {
3
+ codexBin: string;
4
+ raw: string | null;
5
+ version: string | null;
6
+ error: string | null;
7
+ }
8
+ export declare function normalizeCodexDiagnosticText(value: string | null | undefined): string | null;
9
+ export declare function isRecoverableCodexThreadError(value: string | null | undefined): boolean;
10
+ export declare function isCodexUpgradeRequiredError(value: string | null | undefined): boolean;
11
+ export declare function compareSemverLike(a: string, b: string): number;
12
+ export declare function parseCodexCliVersion(raw: string | null | undefined): string | null;
13
+ export declare function isGpt55Model(model: string | null | undefined): boolean;
14
+ export declare function codexCliUpgradeMessage(model: string): string;
15
+ export declare function buildCodexModelGuardMessage(model: string | null | undefined, status: Pick<CodexCliVersionStatus, 'version'>): string | null;
16
+ export declare function formatCodexTurnFailure(errorText: string | null | undefined): string;
@@ -0,0 +1,106 @@
1
+ export const MIN_CODEX_VERSION_FOR_GPT_5_5 = '0.125.0';
2
+ function extractErrorMessage(value) {
3
+ if (!value || typeof value !== 'object')
4
+ return null;
5
+ const data = value;
6
+ const direct = typeof data.message === 'string' ? data.message.trim() : '';
7
+ if (direct)
8
+ return direct;
9
+ const error = data.error;
10
+ if (typeof error === 'string' && error.trim())
11
+ return error.trim();
12
+ if (error && typeof error === 'object') {
13
+ const nested = error;
14
+ const message = typeof nested.message === 'string' ? nested.message.trim() : '';
15
+ if (message)
16
+ return message;
17
+ const type = typeof nested.type === 'string' ? nested.type.trim() : '';
18
+ if (type)
19
+ return type;
20
+ }
21
+ return null;
22
+ }
23
+ function parseJsonMessage(text) {
24
+ try {
25
+ return extractErrorMessage(JSON.parse(text));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ export function normalizeCodexDiagnosticText(value) {
32
+ if (!value)
33
+ return null;
34
+ const normalized = value.replace(/^error:\s*/i, '').trim();
35
+ if (!normalized)
36
+ return null;
37
+ const directJson = parseJsonMessage(normalized);
38
+ if (directJson)
39
+ return directJson;
40
+ for (const line of normalized.split(/\r?\n/)) {
41
+ const trimmed = line.trim();
42
+ if (!trimmed.startsWith('{'))
43
+ continue;
44
+ const lineJson = parseJsonMessage(trimmed);
45
+ if (lineJson)
46
+ return lineJson;
47
+ }
48
+ return normalized;
49
+ }
50
+ export function isRecoverableCodexThreadError(value) {
51
+ const normalized = normalizeCodexDiagnosticText(value)?.toLowerCase() ?? '';
52
+ return /thread .*not found/.test(normalized)
53
+ || normalized.includes('unknown thread')
54
+ || normalized.includes('find_thread_path')
55
+ || normalized.includes('state db discrepancy');
56
+ }
57
+ export function isCodexUpgradeRequiredError(value) {
58
+ const normalized = normalizeCodexDiagnosticText(value)?.toLowerCase() ?? '';
59
+ return normalized.includes('requires a newer version of codex')
60
+ || (normalized.includes('upgrade') && normalized.includes('codex'));
61
+ }
62
+ export function compareSemverLike(a, b) {
63
+ const parse = (value) => value.split('.').map((part) => Number.parseInt(part, 10) || 0);
64
+ const left = parse(a);
65
+ const right = parse(b);
66
+ const length = Math.max(left.length, right.length, 3);
67
+ for (let index = 0; index < length; index += 1) {
68
+ const diff = (left[index] ?? 0) - (right[index] ?? 0);
69
+ if (diff !== 0)
70
+ return diff;
71
+ }
72
+ return 0;
73
+ }
74
+ export function parseCodexCliVersion(raw) {
75
+ if (!raw)
76
+ return null;
77
+ const match = raw.match(/\b(\d+\.\d+\.\d+)\b/);
78
+ return match?.[1] ?? null;
79
+ }
80
+ export function isGpt55Model(model) {
81
+ return typeof model === 'string' && /^gpt-5\.5(?:$|[-_:])/i.test(model.trim());
82
+ }
83
+ export function codexCliUpgradeMessage(model) {
84
+ return `The selected Codex model (${model}) requires a newer local Codex CLI. Update @openai/codex to ${MIN_CODEX_VERSION_FOR_GPT_5_5} or newer, then restart this Canon host.`;
85
+ }
86
+ export function buildCodexModelGuardMessage(model, status) {
87
+ const modelId = typeof model === 'string' ? model.trim() : '';
88
+ if (!isGpt55Model(modelId))
89
+ return null;
90
+ if (!status.version)
91
+ return null;
92
+ return compareSemverLike(status.version, MIN_CODEX_VERSION_FOR_GPT_5_5) < 0
93
+ ? codexCliUpgradeMessage(modelId)
94
+ : null;
95
+ }
96
+ export function formatCodexTurnFailure(errorText) {
97
+ const normalized = normalizeCodexDiagnosticText(errorText);
98
+ if (!normalized) {
99
+ return 'The Codex session stopped unexpectedly before sending a final reply.';
100
+ }
101
+ if (isCodexUpgradeRequiredError(normalized)) {
102
+ return codexCliUpgradeMessage('gpt-5.5');
103
+ }
104
+ const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
105
+ return `Codex failed before sending a final reply: ${shortened}`;
106
+ }
@@ -0,0 +1,4 @@
1
+ export interface LongLivedStream {
2
+ start(): Promise<void>;
3
+ }
4
+ export declare function startCodexStreamInBackground(stream: LongLivedStream, onError: (error: unknown) => void): void;
@@ -0,0 +1,3 @@
1
+ export function startCodexStreamInBackground(stream, onError) {
2
+ stream.start().catch(onError);
3
+ }
package/dist/host.js CHANGED
@@ -6,9 +6,38 @@ import { getCodexImagePath, materializeMessageMedia, } from '@canonmsg/agent-sdk
6
6
  import { buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, getActiveProfileLock, initRTDBAuth, buildLocalRuntimeId, heartbeatLocalRuntimeEntry, loadRuntimeSessionState, markLocalRuntimeStopped, normalizeTurnMetadata, normalizeTurnState, prepareConversationEnvironment, loadHostSessionConfig, releaseConversationEnvironment, resolveCanonAgent, rtdbRead, rtdbWrite, shouldTriggerAgentTurn, saveRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
7
7
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
8
8
  import { CodexConversationAdapter, } from './adapter.js';
9
- import { clearStoredThreadId, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
9
+ import { clearStoredThreadId, buildCodexThreadPolicyFingerprint, loadStoredThreadId, saveStoredThreadId, } from './session-store.js';
10
10
  import { deriveCodexPermissionEnvelope, mapCanonPermissionToCodex, } from './permission-mode.js';
11
+ import { detectCodexCliVersion } from './codex-cli-version.js';
12
+ import { buildCodexModelGuardMessage, formatCodexTurnFailure, isRecoverableCodexThreadError, } from './error-format.js';
13
+ import { startCodexStreamInBackground } from './host-lifecycle.js';
11
14
  import { runCli } from './cli-entry.js';
15
+ const HELP = `canon-codex — run a local Codex agent host for Canon
16
+
17
+ USAGE
18
+ canon-codex [flags]
19
+
20
+ COMMON FLAGS
21
+ --cwd <path> Project directory to run Codex from
22
+ --workspace <path> Additional project to expose in Canon
23
+ --workspace-root <path> Discover projects under an approved root
24
+ --model <model> Default Codex model for new turns
25
+ --sandbox <mode> Codex sandbox mode
26
+ --full-auto Allow non-interactive write access
27
+ --codex-bin <path> Codex CLI binary to run
28
+ --config <key=value> Forward config to Codex CLI
29
+ --help, -h Show this help
30
+ --version, -V Show package version
31
+
32
+ AUTH
33
+ CANON_AGENT=<profile> canon-codex --cwd /path/to/project
34
+ CANON_API_KEY=agk_live_... canon-codex --cwd /path/to/project
35
+
36
+ EXAMPLES
37
+ canon-codex --cwd ~/dev/canon
38
+ canon-codex --cwd ~/dev/canon --workspace-root ~/dev --full-auto
39
+
40
+ Keep this terminal open while you want Canon to reach the agent.`;
12
41
  const MAX_SESSIONS = 12;
13
42
  const IDLE_TIMEOUT_MS = 30 * 60 * 1000;
14
43
  const HEARTBEAT_MS = 30_000;
@@ -25,7 +54,7 @@ let workspaceOptions = [];
25
54
  let workspaceRoots = [];
26
55
  let workspaceRootMetadata = [];
27
56
  function buildCodexRuntimeDescriptor(input) {
28
- return buildFirstPartyCodingRuntimeDescriptor({
57
+ const descriptor = buildFirstPartyCodingRuntimeDescriptor({
29
58
  clientType: 'codex',
30
59
  models: input.models,
31
60
  workspaces: input.workspaces,
@@ -57,6 +86,27 @@ function buildCodexRuntimeDescriptor(input) {
57
86
  },
58
87
  ],
59
88
  });
89
+ if (input.models.length > 0) {
90
+ return descriptor;
91
+ }
92
+ return {
93
+ ...descriptor,
94
+ coreControls: descriptor.coreControls.filter((control) => control.id !== 'model'),
95
+ };
96
+ }
97
+ function modelOptionLabel(model) {
98
+ if (/^gpt-5\.5(?:$|[-_:])/i.test(model))
99
+ return 'GPT-5.5';
100
+ if (/^gpt-5\.4-mini(?:$|[-_:])/i.test(model))
101
+ return 'GPT-5.4 Mini';
102
+ if (/^gpt-5\.4(?:$|[-_:])/i.test(model))
103
+ return 'GPT-5.4';
104
+ return model;
105
+ }
106
+ function buildCodexModelOptions(model) {
107
+ return typeof model === 'string' && model.trim()
108
+ ? [{ value: model.trim(), label: modelOptionLabel(model.trim()) }]
109
+ : [];
60
110
  }
61
111
  function normalizeRuntimeTurnState(value) {
62
112
  const normalizedTurn = normalizeTurnState(value);
@@ -112,14 +162,6 @@ function summarizeCommand(command) {
112
162
  const shortened = trimmed.length > 140 ? `${trimmed.slice(0, 137)}...` : trimmed;
113
163
  return `Running: ${shortened}`;
114
164
  }
115
- function formatTurnFailure(errorText) {
116
- if (!errorText) {
117
- return 'The Codex session stopped unexpectedly before sending a final reply.';
118
- }
119
- const normalized = errorText.replace(/^error:\s*/i, '').trim();
120
- const shortened = normalized.length > 280 ? `${normalized.slice(0, 277)}...` : normalized;
121
- return `Codex failed before sending a final reply: ${shortened}`;
122
- }
123
165
  function sleep(ms) {
124
166
  return new Promise((resolve) => setTimeout(resolve, ms));
125
167
  }
@@ -157,6 +199,14 @@ export async function main() {
157
199
  if (typeof args['ask-for-approval'] === 'string') {
158
200
  console.error('[canon-codex] Note: newer Codex CLI releases do not accept --ask-for-approval for `codex exec`; Canon will translate compatible legacy usage when possible.');
159
201
  }
202
+ const codexBin = typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex';
203
+ const codexCliStatus = detectCodexCliVersion(codexBin);
204
+ if (codexCliStatus.version) {
205
+ console.error(`[canon-codex] Detected Codex CLI ${codexCliStatus.version} (${codexBin})`);
206
+ }
207
+ else {
208
+ console.error(`[canon-codex] Could not detect Codex CLI version for ${codexBin}: ${codexCliStatus.error ?? 'unknown result'}`);
209
+ }
160
210
  const { apiKey, agentId: profileAgentId, agentName: profileAgentName, profile, baseUrl, lockHandle, } = resolveCanonAgent({ logPrefix: '[canon-codex]', expectedClientType: 'codex' });
161
211
  console.error(`[canon-codex] Starting${profile ? ` (profile: ${profile})` : ''} in ${workingDir}`);
162
212
  const client = new CanonClient(apiKey, baseUrl);
@@ -264,6 +314,7 @@ export async function main() {
264
314
  runtimeState.writeSessionState(session.conversationId, {
265
315
  lastError: session.state.lastError,
266
316
  model: session.state.model,
317
+ permissionMode: session.state.permissionMode,
267
318
  cwd: session.cwd,
268
319
  executionMode: session.environment.mode,
269
320
  ...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
@@ -383,15 +434,37 @@ export async function main() {
383
434
  try {
384
435
  const sessionCwd = environment.cwd;
385
436
  const sessionModel = config?.model ?? (typeof args.model === 'string' ? args.model : undefined);
386
- const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode);
387
- if (config?.permissionMode
388
- && !codexPermissionEnvelope.availablePermissionModes.some((option) => option.value === config.permissionMode)) {
389
- throw new ExecutionEnvironmentError(`Permission mode "${config.permissionMode}" is not supported by this Codex host.`, 'This Canon host was started with stricter approval settings. Choose one of the advertised permission modes or restart the host with more permissive flags.');
437
+ const modelGuard = buildCodexModelGuardMessage(sessionModel, codexCliStatus);
438
+ if (modelGuard) {
439
+ throw new ExecutionEnvironmentError(modelGuard, modelGuard);
440
+ }
441
+ const effectivePermissionMode = config?.permissionMode ?? codexPermissionEnvelope.defaultPermissionMode;
442
+ if (effectivePermissionMode
443
+ && !codexPermissionEnvelope.availablePermissionModes.some((option) => option.value === effectivePermissionMode)) {
444
+ throw new ExecutionEnvironmentError(`Permission mode "${effectivePermissionMode}" is not supported by this Codex host.`, 'This Canon host was started with stricter approval settings. Choose one of the advertised permission modes or restart the host with more permissive flags.');
390
445
  }
391
- const approvalOverride = mapCanonPermissionToCodex(config?.permissionMode);
446
+ const approvalOverride = mapCanonPermissionToCodex(effectivePermissionMode);
392
447
  const defaultSandbox = (typeof args.sandbox === 'string' ? args.sandbox : null);
393
448
  const defaultFullAuto = Boolean(args['full-auto']);
394
449
  const defaultBypass = Boolean(args['dangerously-bypass-approvals-and-sandbox']);
450
+ const legacyApprovalPolicy = (typeof args['ask-for-approval'] === 'string'
451
+ ? args['ask-for-approval']
452
+ : null);
453
+ const effectiveSandbox = approvalOverride ? approvalOverride.sandbox : defaultSandbox;
454
+ const effectiveFullAuto = approvalOverride ? approvalOverride.fullAuto : defaultFullAuto;
455
+ const effectiveBypass = approvalOverride
456
+ ? approvalOverride.bypassApprovalsAndSandbox
457
+ : defaultBypass;
458
+ const policyFingerprint = buildCodexThreadPolicyFingerprint({
459
+ baseCwd: environment.baseCwd,
460
+ executionMode: environment.mode,
461
+ permissionMode: effectivePermissionMode ?? null,
462
+ sandbox: effectiveSandbox,
463
+ approvalPolicy: approvalOverride ? null : legacyApprovalPolicy,
464
+ fullAuto: effectiveFullAuto,
465
+ bypassApprovalsAndSandbox: effectiveBypass,
466
+ });
467
+ const storedThreadId = loadStoredThreadId(runtimeId, agentId, conversationId, environment.baseCwd, environment.mode, policyFingerprint);
395
468
  const session = {
396
469
  conversationId,
397
470
  cwd: sessionCwd,
@@ -399,26 +472,24 @@ export async function main() {
399
472
  adapter: new CodexConversationAdapter({
400
473
  cwd: sessionCwd,
401
474
  threadId: storedThreadId,
402
- codexBin: typeof args['codex-bin'] === 'string' ? args['codex-bin'] : 'codex',
475
+ codexBin,
403
476
  model: sessionModel ?? null,
404
- sandbox: approvalOverride ? approvalOverride.sandbox : defaultSandbox,
405
- approvalPolicy: (typeof args['ask-for-approval'] === 'string'
406
- ? args['ask-for-approval']
407
- : null),
477
+ sandbox: effectiveSandbox,
478
+ approvalPolicy: approvalOverride ? null : legacyApprovalPolicy,
408
479
  codexProfile: typeof args['codex-profile'] === 'string' ? args['codex-profile'] : null,
409
480
  addDirs: args['add-dir'] ?? [],
410
481
  configOverrides: args.config ?? [],
411
- fullAuto: approvalOverride ? approvalOverride.fullAuto : defaultFullAuto,
412
- bypassApprovalsAndSandbox: approvalOverride
413
- ? approvalOverride.bypassApprovalsAndSandbox
414
- : defaultBypass,
482
+ fullAuto: effectiveFullAuto,
483
+ bypassApprovalsAndSandbox: effectiveBypass,
415
484
  }),
416
485
  queue: [],
417
486
  running: false,
418
487
  state: {
419
488
  model: sessionModel,
489
+ permissionMode: effectivePermissionMode,
420
490
  state: 'idle',
421
491
  },
492
+ policyFingerprint,
422
493
  turnState: 'idle',
423
494
  currentTurnId: null,
424
495
  currentTurnOpenedAt: null,
@@ -428,7 +499,10 @@ export async function main() {
428
499
  closed: false,
429
500
  };
430
501
  sessions.set(conversationId, session);
431
- await baselineControlSignal(conversationId);
502
+ await Promise.all([
503
+ baselineControlSignal(conversationId),
504
+ baselineSessionControl(conversationId),
505
+ ]);
432
506
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Environment → ${environment.mode} (${sessionCwd})`);
433
507
  writeState(session);
434
508
  writeTurn(session);
@@ -557,11 +631,15 @@ export async function main() {
557
631
  status: 'thinking',
558
632
  }).catch(() => { });
559
633
  try {
634
+ const modelGuard = buildCodexModelGuardMessage(session.state.model, codexCliStatus);
635
+ if (modelGuard) {
636
+ throw new ExecutionEnvironmentError(modelGuard, modelGuard);
637
+ }
560
638
  const turnImagePaths = nextTurn.imagePaths ?? [];
561
- const result = await session.adapter.runTurn(nextTurn.prompt, (event) => {
639
+ const handleCodexEvent = (event) => {
562
640
  session.lastActivity = Date.now();
563
641
  if (event.type === 'thread.started') {
564
- saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode);
642
+ saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, event.threadId, session.environment.mode, session.policyFingerprint);
565
643
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Thread ${event.threadId}`);
566
644
  return;
567
645
  }
@@ -589,13 +667,32 @@ export async function main() {
589
667
  if (event.type === 'turn.completed') {
590
668
  writeState(session);
591
669
  }
592
- }, (line) => {
670
+ };
671
+ const logCodexLine = (line) => {
593
672
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] ${line}`);
594
- }, turnImagePaths);
673
+ };
674
+ const clearStoredThread = () => {
675
+ clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
676
+ session.adapter.clearThreadId();
677
+ };
678
+ const runTurnOnce = () => session.adapter.runTurn(nextTurn.prompt, handleCodexEvent, logCodexLine, turnImagePaths);
679
+ let result = await runTurnOnce();
680
+ if (!result.interrupted
681
+ && !result.finalMessage
682
+ && result.exitCode
683
+ && result.exitCode !== 0
684
+ && isRecoverableCodexThreadError(result.errorText)) {
685
+ console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Stored thread was not found; clearing and retrying once`);
686
+ clearStoredThread();
687
+ result = await runTurnOnce();
688
+ }
595
689
  if (result.threadId) {
596
- saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode);
690
+ saveStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, result.threadId, session.environment.mode, session.policyFingerprint);
597
691
  }
598
692
  if (!result.interrupted && result.finalMessage) {
693
+ if (isRecoverableCodexThreadError(result.errorText)) {
694
+ clearStoredThread();
695
+ }
599
696
  await client.sendMessage(session.conversationId, result.finalMessage, {
600
697
  metadata: {
601
698
  turnId: session.currentTurnId,
@@ -608,7 +705,7 @@ export async function main() {
608
705
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Sent reply (${result.finalMessage.length} chars)`);
609
706
  }
610
707
  else if (!result.interrupted && result.exitCode && result.exitCode !== 0) {
611
- const userVisibleError = formatTurnFailure(result.errorText);
708
+ const userVisibleError = formatCodexTurnFailure(result.errorText);
612
709
  session.state.lastError = userVisibleError;
613
710
  writeState(session);
614
711
  if (result.errorText) {
@@ -637,7 +734,9 @@ export async function main() {
637
734
  }
638
735
  }
639
736
  catch (error) {
640
- const message = `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
737
+ const message = error instanceof ExecutionEnvironmentError
738
+ ? error.userMessage
739
+ : `The Codex host failed to start a turn: ${error instanceof Error ? error.message : String(error)}`;
641
740
  session.state.lastError = message;
642
741
  writeState(session);
643
742
  await client.sendMessage(session.conversationId, message, {
@@ -649,7 +748,7 @@ export async function main() {
649
748
  },
650
749
  }).catch(() => { });
651
750
  await handoffFinalMessage(session.conversationId);
652
- if (error instanceof Error && /invalid|not found|unknown thread/i.test(error.message)) {
751
+ if (error instanceof Error && isRecoverableCodexThreadError(error.message)) {
653
752
  clearStoredThreadId(runtimeId, agentId, session.conversationId, session.environment.baseCwd, session.environment.mode);
654
753
  }
655
754
  console.error(`[canon-codex] [${session.conversationId.slice(0, 8)}] Turn failed:`, error);
@@ -678,6 +777,7 @@ export async function main() {
678
777
  ...EXECUTION_ENVIRONMENT_MODES,
679
778
  ];
680
779
  const codexPermissionEnvelope = deriveCodexPermissionEnvelope(args);
780
+ const codexModelOptions = buildCodexModelOptions(args.model);
681
781
  let runtimeDescriptor = {
682
782
  defaultWorkspaceId: workspaceOptions[0]?.id,
683
783
  ...(typeof args.model === 'string' ? { defaultModel: args.model } : {}),
@@ -688,7 +788,7 @@ export async function main() {
688
788
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
689
789
  : {}),
690
790
  runtimeDescriptor: buildCodexRuntimeDescriptor({
691
- models: [],
791
+ models: codexModelOptions,
692
792
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
693
793
  workspaceRoots: workspaceRootMetadata,
694
794
  executionModes: hostAvailableExecutionModes,
@@ -707,6 +807,18 @@ export async function main() {
707
807
  lastSeenSignal.set(conversationId, timestamp);
708
808
  }
709
809
  }
810
+ async function baselineSessionControl(conversationId) {
811
+ if (lastSeenControl.has(conversationId))
812
+ return;
813
+ const raw = await rtdbRead(`/control/${conversationId}/${agentId}/session`).catch(() => null);
814
+ if (!raw || typeof raw !== 'object')
815
+ return;
816
+ const timestamp = Number(raw.updatedAt ?? 0);
817
+ if (timestamp > 0) {
818
+ lastSeenControl.set(conversationId, timestamp);
819
+ }
820
+ }
821
+ let publishRuntimeDetailsInFlight = false;
710
822
  const publishRuntimeHeartbeat = async () => {
711
823
  heartbeatLocalRuntimeEntry(runtimeId, {
712
824
  agentId,
@@ -716,84 +828,99 @@ export async function main() {
716
828
  });
717
829
  if (!streamConnected)
718
830
  return;
719
- await refreshKnownConversationIds().catch((error) => {
720
- console.error('[canon-codex] Failed to refresh known conversations:', error);
721
- });
722
831
  await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
723
832
  console.error('[canon-codex] Failed to publish agent runtime:', error);
724
833
  });
725
- await publishHostSessionSnapshots({
726
- conversationIds: Array.from(knownConversationIds),
727
- agentId,
728
- clientType: 'codex',
729
- runtime: runtimeDescriptor,
730
- workspaceOptions,
731
- defaultCwd: workingDir,
732
- extraSessionConfigFields: ['permissionMode'],
733
- liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
734
- const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
735
- return [
736
- session.conversationId,
737
- {
738
- ...(session.state.model ? { model: session.state.model } : {}),
739
- ...(workspaceId ? { workspaceId } : {}),
740
- executionMode: session.environment.mode,
741
- executionBranch: session.environment.branch ?? null,
742
- },
743
- ];
744
- })),
745
- }).catch((error) => {
746
- console.error('[canon-codex] Failed to publish session snapshots:', error);
747
- });
748
- await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
749
- const session = sessions.get(conversationId);
750
- const workspaceId = session
751
- ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
752
- : runtimeDescriptor.defaultWorkspaceId;
753
- const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
754
- const descriptor = runtimeDescriptor.runtimeDescriptor;
755
- if (!descriptor)
756
- return;
757
- const payload = {
758
- descriptor,
759
- surfaceMode: 'host',
760
- statusItems: [
761
- {
762
- id: 'transport',
763
- label: 'Transport',
764
- value: 'exec --json',
765
- },
766
- {
767
- id: 'streaming',
768
- label: 'Live output',
769
- value: 'Thinking, tools, and completed-message previews',
770
- },
771
- {
772
- id: 'nativeActions',
773
- label: 'Native actions',
774
- value: 'Limited until app-server transport',
775
- tone: 'warning',
834
+ if (publishRuntimeDetailsInFlight)
835
+ return;
836
+ publishRuntimeDetailsInFlight = true;
837
+ try {
838
+ await refreshKnownConversationIds().catch((error) => {
839
+ console.error('[canon-codex] Failed to refresh known conversations:', error);
840
+ });
841
+ await publishHostSessionSnapshots({
842
+ conversationIds: Array.from(knownConversationIds),
843
+ agentId,
844
+ clientType: 'codex',
845
+ runtime: runtimeDescriptor,
846
+ workspaceOptions,
847
+ defaultCwd: workingDir,
848
+ extraSessionConfigFields: ['permissionMode'],
849
+ liveSessionConfigByConversation: new Map(Array.from(sessions.values()).map((session) => {
850
+ const workspaceId = resolveWorkspaceIdForBaseCwd(session.environment.baseCwd);
851
+ return [
852
+ session.conversationId,
853
+ {
854
+ ...(session.state.model ? { model: session.state.model } : {}),
855
+ ...(session.state.permissionMode ? { permissionMode: session.state.permissionMode } : {}),
856
+ ...(workspaceId ? { workspaceId } : {}),
857
+ executionMode: session.environment.mode,
858
+ executionBranch: session.environment.branch ?? null,
859
+ },
860
+ ];
861
+ })),
862
+ }).catch((error) => {
863
+ console.error('[canon-codex] Failed to publish session snapshots:', error);
864
+ });
865
+ await Promise.all(Array.from(knownConversationIds).map(async (conversationId) => {
866
+ const session = sessions.get(conversationId);
867
+ const workspaceId = session
868
+ ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
869
+ : runtimeDescriptor.defaultWorkspaceId;
870
+ const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
871
+ const descriptor = runtimeDescriptor.runtimeDescriptor;
872
+ if (!descriptor)
873
+ return;
874
+ const payload = {
875
+ descriptor,
876
+ surfaceMode: 'host',
877
+ statusItems: [
878
+ {
879
+ id: 'transport',
880
+ label: 'Transport',
881
+ value: 'exec --json',
882
+ },
883
+ {
884
+ id: 'streaming',
885
+ label: 'Live output',
886
+ value: 'Thinking, tools, and completed-message previews',
887
+ },
888
+ {
889
+ id: 'codex-cli',
890
+ label: 'Codex CLI',
891
+ value: codexCliStatus.version ?? (codexCliStatus.raw ?? 'Version unknown'),
892
+ tone: codexCliStatus.version ? 'default' : 'warning',
893
+ },
894
+ {
895
+ id: 'nativeActions',
896
+ label: 'Native actions',
897
+ value: 'Limited until app-server transport',
898
+ tone: 'warning',
899
+ },
900
+ ],
901
+ execution: {
902
+ resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
903
+ resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
904
+ workspaceRootId: workspace?.workspaceRootId ?? null,
905
+ workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
906
+ executionMode: session?.environment.mode ?? null,
907
+ executionBranch: session?.environment.branch ?? null,
908
+ worktreePath: session?.environment.worktreePath ?? null,
909
+ fallbackReason: resolveExecutionFallbackReason(session?.environment),
776
910
  },
777
- ],
778
- execution: {
779
- resolvedWorkspaceLabel: workspace?.label ?? workspaceId ?? null,
780
- resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
781
- workspaceRootId: workspace?.workspaceRootId ?? null,
782
- workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
783
- executionMode: session?.environment.mode ?? null,
784
- executionBranch: session?.environment.branch ?? null,
785
- worktreePath: session?.environment.worktreePath ?? null,
786
- fallbackReason: resolveExecutionFallbackReason(session?.environment),
787
- },
788
- notes: [
789
- 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not token-by-token text deltas.',
790
- 'Codex review, compact/rollback, live plan/diff/reasoning updates, PTY command execution, plugin/app/MCP inventory, and structured approvals require the future app-server transport.',
791
- ],
792
- };
793
- await runtimeState.writeRuntimeInfo(conversationId, payload);
794
- })).catch((error) => {
795
- console.error('[canon-codex] Failed to publish runtime info:', error);
796
- });
911
+ notes: [
912
+ 'This Codex host uses the current exec --json transport, so Canon can show thinking, tool activity, and completed assistant-message previews, but not token-by-token text deltas.',
913
+ 'Codex review, compact/rollback, live plan/diff/reasoning updates, PTY command execution, plugin/app/MCP inventory, and structured approvals require the future app-server transport.',
914
+ ],
915
+ };
916
+ await runtimeState.writeRuntimeInfo(conversationId, payload);
917
+ })).catch((error) => {
918
+ console.error('[canon-codex] Failed to publish runtime info:', error);
919
+ });
920
+ }
921
+ finally {
922
+ publishRuntimeDetailsInFlight = false;
923
+ }
797
924
  };
798
925
  const stream = new CanonStream({
799
926
  apiKey,
@@ -843,7 +970,7 @@ export async function main() {
843
970
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
844
971
  : {}),
845
972
  runtimeDescriptor: buildCodexRuntimeDescriptor({
846
- models: [],
973
+ models: codexModelOptions,
847
974
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
848
975
  workspaceRoots: workspaceRootMetadata,
849
976
  executionModes: hostAvailableExecutionModes,
@@ -862,7 +989,7 @@ export async function main() {
862
989
  ? { defaultPermissionMode: codexPermissionEnvelope.defaultPermissionMode }
863
990
  : {}),
864
991
  runtimeDescriptor: buildCodexRuntimeDescriptor({
865
- models: [],
992
+ models: codexModelOptions,
866
993
  workspaces: buildPublicWorkspaceOptions(workspaceOptions),
867
994
  workspaceRoots: workspaceRootMetadata,
868
995
  executionModes: hostAvailableExecutionModes,
@@ -932,7 +1059,7 @@ export async function main() {
932
1059
  catch (error) {
933
1060
  console.error('[canon-codex] Failed to load startup conversations:', error);
934
1061
  }
935
- await stream.start().catch((error) => {
1062
+ startCodexStreamInBackground(stream, (error) => {
936
1063
  console.error('[canon-codex] SSE start error:', error instanceof Error ? error.message : error);
937
1064
  });
938
1065
  const pollControl = async () => {
@@ -948,6 +1075,14 @@ export async function main() {
948
1075
  const session = sessions.get(conversationId);
949
1076
  if (session && !session.closed) {
950
1077
  if (control.model && control.model !== session.state.model) {
1078
+ const modelGuard = buildCodexModelGuardMessage(control.model, codexCliStatus);
1079
+ if (modelGuard) {
1080
+ session.state.lastError = modelGuard;
1081
+ console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${modelGuard}`);
1082
+ writeState(session);
1083
+ await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
1084
+ continue;
1085
+ }
951
1086
  session.adapter.setModel(control.model);
952
1087
  session.state.model = control.model;
953
1088
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] Model set for next turn -> ${control.model}`);
@@ -960,6 +1095,7 @@ export async function main() {
960
1095
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] effort control is not mapped yet (${control.effort})`);
961
1096
  }
962
1097
  }
1098
+ await rtdbWrite(`/control/${conversationId}/${agentId}/session`, null).catch(() => { });
963
1099
  }
964
1100
  }
965
1101
  const raw = await rtdbRead(`/control/${conversationId}/${agentId}/signal`);
@@ -980,14 +1116,14 @@ export async function main() {
980
1116
  continue;
981
1117
  }
982
1118
  console.error(`[canon-codex] [${conversationId.slice(0, 8)}] ${signal.type} signal`);
983
- if (session.running) {
984
- await session.adapter.interrupt();
985
- }
986
- session.turnState = 'interrupted';
987
1119
  if (signal.type === 'stop_and_drop') {
988
1120
  const droppedPrompts = session.queue.splice(0);
989
1121
  await markQueuedPromptsRejected(conversationId, droppedPrompts);
990
1122
  }
1123
+ if (session.running) {
1124
+ await session.adapter.interrupt();
1125
+ }
1126
+ session.turnState = 'interrupted';
991
1127
  writeTurn(session);
992
1128
  clearStreaming(conversationId);
993
1129
  client.setTyping(conversationId, false).catch(() => { });
@@ -1042,7 +1178,11 @@ export async function main() {
1042
1178
  await new Promise(() => { });
1043
1179
  }
1044
1180
  runCli(import.meta.url, main, (error) => {
1045
- console.error('[canon-codex] Fatal error:', error);
1181
+ const message = error instanceof Error ? error.message : String(error);
1182
+ console.error(`[canon-codex] ${message}`);
1046
1183
  getActiveProfileLock()?.release();
1047
1184
  process.exit(1);
1185
+ }, {
1186
+ name: 'canon-codex',
1187
+ help: HELP,
1048
1188
  });
package/dist/register.js CHANGED
@@ -4,6 +4,27 @@ import { readFileSync } from 'node:fs';
4
4
  import { parseArgs } from 'node:util';
5
5
  import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
6
6
  import { runCli } from './cli-entry.js';
7
+ const HELP = `canon-codex-register — register or reconnect a Codex agent in Canon
8
+
9
+ USAGE
10
+ canon-codex-register --name <name> --description <text> --phone <e164> [flags]
11
+
12
+ REQUIRED
13
+ --name <name> Agent display name shown in Canon
14
+ --description <text> Short profile description
15
+ --phone <e164> Owner phone number, for example +15551234567
16
+
17
+ FLAGS
18
+ --profile <name> Local profile name in ~/.canon/agents.json
19
+ --base-url <url> Canon API base URL override
20
+ --help, -h Show this help
21
+ --version, -V Show package version
22
+
23
+ EXAMPLES
24
+ canon-codex-register --name "My Codex" --description "Local coding agent" --phone "+15551234567"
25
+ canon-codex-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
26
+
27
+ After approval, start it with CANON_AGENT=<profile> canon-codex --cwd /path/to/project.`;
7
28
  export async function main() {
8
29
  setDefaultResultOrder('ipv4first');
9
30
  const { values } = parseArgs({
@@ -92,4 +113,7 @@ export async function main() {
92
113
  runCli(import.meta.url, main, (error) => {
93
114
  console.error('[canon-codex-register] Fatal error:', error);
94
115
  process.exit(1);
116
+ }, {
117
+ name: 'canon-codex-register',
118
+ help: HELP,
95
119
  });
@@ -1,4 +1,13 @@
1
1
  import { type ExecutionEnvironmentMode } from '@canonmsg/core';
2
- export declare function loadStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, executionMode?: ExecutionEnvironmentMode): string | null;
3
- export declare function saveStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, threadId: string, executionMode?: ExecutionEnvironmentMode): void;
2
+ export declare function buildCodexThreadPolicyFingerprint(input: {
3
+ baseCwd: string;
4
+ executionMode?: ExecutionEnvironmentMode;
5
+ permissionMode?: string | null;
6
+ sandbox?: string | null;
7
+ approvalPolicy?: string | null;
8
+ fullAuto?: boolean;
9
+ bypassApprovalsAndSandbox?: boolean;
10
+ }): string;
11
+ export declare function loadStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): string | null;
12
+ export declare function saveStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd: string, threadId: string, executionMode?: ExecutionEnvironmentMode, policyFingerprint?: string): void;
4
13
  export declare function clearStoredThreadId(runtimeId: string | null, agentId: string, conversationId: string, baseCwd?: string, executionMode?: ExecutionEnvironmentMode): void;
@@ -1,4 +1,5 @@
1
1
  import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { join } from 'node:path';
3
4
  import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
4
5
  const STORE_PATH = join(CANON_DIR, 'codex-sessions.json');
@@ -14,29 +15,60 @@ function saveStore(store) {
14
15
  mkdirSync(CANON_DIR, { recursive: true });
15
16
  writeFileSync(STORE_PATH, JSON.stringify(store, null, 2));
16
17
  }
17
- export function loadStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode) {
18
+ export function buildCodexThreadPolicyFingerprint(input) {
19
+ return createHash('sha256').update(JSON.stringify({
20
+ version: 1,
21
+ baseCwd: input.baseCwd,
22
+ executionMode: input.executionMode ?? null,
23
+ permissionMode: input.permissionMode ?? null,
24
+ sandbox: input.sandbox ?? null,
25
+ approvalPolicy: input.approvalPolicy ?? null,
26
+ fullAuto: input.fullAuto === true,
27
+ bypassApprovalsAndSandbox: input.bypassApprovalsAndSandbox === true,
28
+ })).digest('hex').slice(0, 24);
29
+ }
30
+ export function loadStoredThreadId(runtimeId, agentId, conversationId, baseCwd, executionMode, policyFingerprint) {
18
31
  if (runtimeId) {
19
32
  const state = loadRuntimeSessionState(runtimeId, {
20
33
  conversationId,
21
34
  baseCwd,
22
35
  executionMode,
23
36
  });
24
- if (state?.threadId)
37
+ if (state?.threadId) {
38
+ if (policyFingerprint && state.codexPolicyFingerprint !== policyFingerprint) {
39
+ clearRuntimeSessionState(runtimeId, {
40
+ conversationId,
41
+ baseCwd,
42
+ executionMode,
43
+ });
44
+ return null;
45
+ }
25
46
  return state.threadId;
47
+ }
48
+ return null;
26
49
  }
27
50
  const store = loadStore();
28
51
  const record = store.agents[agentId]?.[conversationId];
29
52
  if (!record || record.cwd !== baseCwd)
30
53
  return null;
54
+ if (policyFingerprint && record.policyFingerprint !== policyFingerprint) {
55
+ delete store.agents[agentId][conversationId];
56
+ if (Object.keys(store.agents[agentId]).length === 0) {
57
+ delete store.agents[agentId];
58
+ }
59
+ saveStore(store);
60
+ return null;
61
+ }
31
62
  return record.threadId;
32
63
  }
33
- export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode) {
64
+ export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd, threadId, executionMode, policyFingerprint) {
34
65
  if (runtimeId) {
35
66
  saveRuntimeSessionState(runtimeId, {
36
67
  conversationId,
37
68
  baseCwd,
38
69
  executionMode,
39
70
  threadId,
71
+ ...(policyFingerprint ? { codexPolicyFingerprint: policyFingerprint } : {}),
40
72
  });
41
73
  return;
42
74
  }
@@ -45,6 +77,7 @@ export function saveStoredThreadId(runtimeId, agentId, conversationId, baseCwd,
45
77
  store.agents[agentId][conversationId] = {
46
78
  threadId,
47
79
  cwd: baseCwd,
80
+ ...(policyFingerprint ? { policyFingerprint } : {}),
48
81
  updatedAt: new Date().toISOString(),
49
82
  };
50
83
  saveStore(store);
package/dist/setup.js CHANGED
@@ -1,6 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { runCli } from './cli-entry.js';
4
+ const HELP = `canon-codex-setup — print Canon Codex setup guidance
5
+
6
+ USAGE
7
+ canon-codex-setup
8
+
9
+ FLAGS
10
+ --help, -h Show this help
11
+ --version, -V Show package version
12
+
13
+ This command does not register an agent. It checks for the Codex CLI and prints
14
+ the registration and host commands to run next.`;
4
15
  export function main() {
5
16
  console.log('Canon Codex Plugin Setup');
6
17
  console.log('========================\n');
@@ -38,4 +49,7 @@ export function main() {
38
49
  runCli(import.meta.url, main, (error) => {
39
50
  console.error('[canon-codex-setup] Fatal error:', error);
40
51
  process.exit(1);
52
+ }, {
53
+ name: 'canon-codex-setup',
54
+ help: HELP,
41
55
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/codex-plugin",
3
- "version": "0.9.4",
3
+ "version": "0.9.5",
4
4
  "description": "Canon host integration for Codex CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "@canonmsg/agent-sdk": "^1.1.0",
33
- "@canonmsg/core": "^0.15.0"
33
+ "@canonmsg/core": "^0.15.1"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=18.0.0"