@canonmsg/claude-code-plugin 0.22.0 → 0.23.0

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.
@@ -1,8 +1,6 @@
1
- import type { Query, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
- import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core/contract';
3
- import type { TurnArtifactSnapshot } from '@canonmsg/agent-host';
4
- import { type ClaudeEffortLevel } from './session-defaults.js';
5
- export type { ClaudeEffortLevel } from './session-defaults.js';
1
+ import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
2
+ import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
3
+ import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
6
4
  export type ClaudeInputKind = 'seed' | 'canon';
7
5
  export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
8
6
  export interface ClaudeInputEnvelope {
@@ -21,46 +19,6 @@ export interface ClaudeRuntimeControlError {
21
19
  message: string;
22
20
  updatedAt: number;
23
21
  }
24
- export type ClaudePermissionMode = Parameters<Query['setPermissionMode']>[0];
25
- export interface ClaudeSessionControl {
26
- model?: string;
27
- permissionMode?: string;
28
- effort?: string;
29
- runtimeControlValues?: Record<string, string>;
30
- updatedAt?: number;
31
- updatedBy?: string;
32
- }
33
- export interface ClaudeHostSessionState {
34
- model?: string;
35
- permissionMode?: string;
36
- effort?: string;
37
- fallbackModel?: string;
38
- runtimeControlValues?: Record<string, string>;
39
- state?: 'idle' | 'running' | 'requires_action';
40
- contextUsage?: {
41
- percentage: number;
42
- totalTokens: number;
43
- maxTokens: number;
44
- };
45
- }
46
- export declare function parseClaudePermissionMode(value: unknown): ClaudePermissionMode | null;
47
- export declare function normalizeClaudeEffortLevel(value: unknown): ClaudeEffortLevel | undefined;
48
- export declare function buildClaudeInitialSessionState(input: {
49
- model?: string;
50
- permissionMode?: string;
51
- effort?: string;
52
- fallbackModel?: string;
53
- runtimeControlValues?: Record<string, string>;
54
- }): ClaudeHostSessionState;
55
- export interface ClaudeSessionControlTarget {
56
- state: ClaudeHostSessionState;
57
- closed: boolean;
58
- query: Pick<Query, 'setModel' | 'setPermissionMode' | 'applyFlagSettings'>;
59
- writeState: () => void;
60
- setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
61
- clearRuntimeControlError: (controlId: string) => void;
62
- }
63
- export declare function applyClaudeSessionControl(target: ClaudeSessionControlTarget, ctrl: ClaudeSessionControl): void;
64
22
  export interface ClaudePendingFinalDelivery {
65
23
  turnKey: string;
66
24
  text: string;
@@ -102,15 +60,9 @@ export declare function shouldDeliverClaudeFinal(input: {
102
60
  interruptedTurnKeys: ReadonlySet<string>;
103
61
  }): boolean;
104
62
  export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
105
- export interface ClaudeModelInfoLike {
106
- value: string;
107
- displayName: string;
108
- description?: string;
109
- }
110
- export declare function claudeModelInfoToOption(model: ClaudeModelInfoLike): ModelOption;
111
- export declare function isClaudeCustomModelOption(option: ModelOption | null | undefined): boolean;
112
- export declare function resolveClaudeModelOptions(input: {
113
- discovered: ReadonlyArray<ModelOption>;
63
+ export declare function mergeClaudeModelOptions(input: {
64
+ previous: ReadonlyArray<ModelOption>;
65
+ next: ReadonlyArray<ModelOption>;
114
66
  selectedModel?: string | null;
115
67
  }): ModelOption[];
116
68
  export declare function formatClaudeControlError(error: unknown): string;
@@ -1,140 +1,4 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { CLAUDE_EFFORT_LEVELS, claudeThinkingToFlagValue, parseClaudeThinkingMode, parseClaudeUltracodeMode, } from './session-defaults.js';
3
- const CLAUDE_PERMISSION_MODE_VALUES = new Set([
4
- 'acceptEdits',
5
- 'auto',
6
- 'bypassPermissions',
7
- 'default',
8
- 'dontAsk',
9
- 'plan',
10
- ]);
11
- const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_LEVELS);
12
- export function parseClaudePermissionMode(value) {
13
- return typeof value === 'string' && CLAUDE_PERMISSION_MODE_VALUES.has(value)
14
- ? value
15
- : null;
16
- }
17
- export function normalizeClaudeEffortLevel(value) {
18
- return typeof value === 'string' && CLAUDE_EFFORT_VALUES.has(value)
19
- ? value
20
- : undefined;
21
- }
22
- export function buildClaudeInitialSessionState(input) {
23
- return {
24
- ...(input.model ? { model: input.model } : {}),
25
- ...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
26
- ...(input.effort ? { effort: input.effort } : {}),
27
- ...(input.fallbackModel ? { fallbackModel: input.fallbackModel } : {}),
28
- ...(input.runtimeControlValues ? { runtimeControlValues: input.runtimeControlValues } : {}),
29
- state: 'idle',
30
- };
31
- }
32
- /**
33
- * Apply a control to the Claude SDK. W2-A2 deleted the seq-guarded optimistic
34
- * revert machinery (commit c356bdf8) that masked the 2–11s control poll: the
35
- * bridge is event-push now (~100ms), so the state is already published by the
36
- * caller's writeState() and the SDK apply is fired without a revert dance. A
37
- * failed apply surfaces as a runtime-control error (the display truth the app
38
- * reads) — the same shape codex uses. `value` is the requested value for the
39
- * error record; `controlId` scopes the error.
40
- */
41
- function applyControl(input) {
42
- // Fire the SDK apply SYNCHRONOUSLY (an async setter may still resolve later);
43
- // wrap so a synchronous throw is caught in the same rejection path.
44
- void (async () => input.apply())().catch((error) => {
45
- if (input.target.closed)
46
- return;
47
- input.target.setRuntimeControlError(input.controlId, input.value, error);
48
- input.target.writeState();
49
- });
50
- }
51
- function setRuntimeControlValue(state, controlId, value) {
52
- state.runtimeControlValues = {
53
- ...(state.runtimeControlValues ?? {}),
54
- [controlId]: value,
55
- };
56
- }
57
- export function applyClaudeSessionControl(target, ctrl) {
58
- const nextModel = typeof ctrl.model === 'string' && ctrl.model.trim() ? ctrl.model.trim() : null;
59
- if (ctrl.model !== undefined && !nextModel) {
60
- target.setRuntimeControlError('model', String(ctrl.model), new Error('Unsupported Claude model.'));
61
- }
62
- else if (nextModel && nextModel !== target.state.model) {
63
- target.state.model = nextModel;
64
- target.clearRuntimeControlError('model');
65
- applyControl({
66
- target,
67
- controlId: 'model',
68
- value: nextModel,
69
- apply: () => target.query.setModel(nextModel),
70
- });
71
- }
72
- const nextPermissionMode = parseClaudePermissionMode(ctrl.permissionMode);
73
- if (ctrl.permissionMode !== undefined && !nextPermissionMode) {
74
- target.setRuntimeControlError('permissionMode', ctrl.permissionMode, new Error(`Unsupported Claude permission mode: ${ctrl.permissionMode}`));
75
- }
76
- else if (nextPermissionMode && nextPermissionMode !== target.state.permissionMode) {
77
- // Claude Code applies permission-mode changes asynchronously; publish the
78
- // requested value immediately (writeState below) and surface an error if the
79
- // apply fails.
80
- target.state.permissionMode = nextPermissionMode;
81
- target.clearRuntimeControlError('permissionMode');
82
- applyControl({
83
- target,
84
- controlId: 'permissionMode',
85
- value: nextPermissionMode,
86
- apply: () => target.query.setPermissionMode(nextPermissionMode),
87
- });
88
- }
89
- const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
90
- if (nextEffort && nextEffort !== target.state.effort) {
91
- target.state.effort = nextEffort;
92
- target.clearRuntimeControlError('effort');
93
- applyControl({
94
- target,
95
- controlId: 'effort',
96
- value: nextEffort,
97
- apply: () => target.query.applyFlagSettings({ effortLevel: nextEffort }),
98
- });
99
- }
100
- else if (ctrl.effort !== undefined && !nextEffort) {
101
- target.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
102
- }
103
- const runtimeControlValues = ctrl.runtimeControlValues ?? {};
104
- if ('thinking' in runtimeControlValues) {
105
- const nextThinking = parseClaudeThinkingMode(runtimeControlValues.thinking);
106
- if (!nextThinking) {
107
- target.setRuntimeControlError('thinking', String(runtimeControlValues.thinking), new Error('Unsupported Claude thinking mode.'));
108
- }
109
- else if (nextThinking !== target.state.runtimeControlValues?.thinking) {
110
- setRuntimeControlValue(target.state, 'thinking', nextThinking);
111
- target.clearRuntimeControlError('thinking');
112
- applyControl({
113
- target,
114
- controlId: 'thinking',
115
- value: nextThinking,
116
- apply: () => target.query.applyFlagSettings({ alwaysThinkingEnabled: claudeThinkingToFlagValue(nextThinking) }),
117
- });
118
- }
119
- }
120
- if ('ultracode' in runtimeControlValues) {
121
- const nextUltracode = parseClaudeUltracodeMode(runtimeControlValues.ultracode);
122
- if (!nextUltracode) {
123
- target.setRuntimeControlError('ultracode', String(runtimeControlValues.ultracode), new Error('Unsupported Claude ultracode mode.'));
124
- }
125
- else if (nextUltracode !== target.state.runtimeControlValues?.ultracode) {
126
- setRuntimeControlValue(target.state, 'ultracode', nextUltracode);
127
- target.clearRuntimeControlError('ultracode');
128
- applyControl({
129
- target,
130
- controlId: 'ultracode',
131
- value: nextUltracode,
132
- apply: () => target.query.applyFlagSettings({ ultracode: nextUltracode === 'on' }),
133
- });
134
- }
135
- }
136
- target.writeState();
137
- }
138
2
  export function createClaudeInputEnvelope(input) {
139
3
  const sourceMessageId = input.sourceMessageId ?? null;
140
4
  const fallbackId = randomUUID();
@@ -182,45 +46,32 @@ export function resetClaudeCompletedTurnState(session) {
182
46
  session.toolInProgress = false;
183
47
  session.turnState = 'idle';
184
48
  }
185
- function normalizeModelDescription(value) {
186
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
187
- }
188
- function displayLabelWithClaudeVersion(input) {
189
- const label = input.displayName.trim() || input.value;
190
- const description = normalizeModelDescription(input.description);
191
- if (!description || label.includes('(') || /\d/.test(label))
192
- return label;
193
- const [summary] = description.split('·');
194
- const versionedLabel = summary?.trim();
195
- return versionedLabel
196
- && versionedLabel.toLowerCase().startsWith(label.toLowerCase())
197
- && /\d/.test(versionedLabel)
198
- ? versionedLabel
199
- : label;
200
- }
201
- export function claudeModelInfoToOption(model) {
202
- return {
203
- value: model.value,
204
- label: displayLabelWithClaudeVersion(model),
205
- ...(normalizeModelDescription(model.description) ? { description: normalizeModelDescription(model.description) } : {}),
206
- };
207
- }
208
- export function isClaudeCustomModelOption(option) {
209
- return option?.description?.trim().toLowerCase() === 'custom model';
210
- }
211
- export function resolveClaudeModelOptions(input) {
49
+ export function mergeClaudeModelOptions(input) {
212
50
  const byValue = new Map();
213
- for (const option of input.discovered) {
51
+ for (const option of input.previous) {
52
+ byValue.set(option.value, { ...option });
53
+ }
54
+ for (const option of input.next) {
214
55
  byValue.set(option.value, { ...option });
215
56
  }
216
57
  if (input.selectedModel && !byValue.has(input.selectedModel)) {
217
58
  byValue.set(input.selectedModel, {
218
59
  value: input.selectedModel,
219
60
  label: input.selectedModel,
220
- description: 'Selected for this Canon session but not returned by Claude model discovery.',
61
+ description: 'Selected for this Canon session but not returned by the latest Claude model probe.',
221
62
  });
222
63
  }
223
- return [...byValue.values()];
64
+ const nextValues = new Set(input.next.map((option) => option.value));
65
+ return [...byValue.values()].map((option) => {
66
+ if (nextValues.has(option.value))
67
+ return option;
68
+ return {
69
+ ...option,
70
+ description: option.description
71
+ ? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
72
+ : 'Previously advertised; not returned by the latest Claude model probe.',
73
+ };
74
+ });
224
75
  }
225
76
  export function formatClaudeControlError(error) {
226
77
  if (error instanceof Error && error.message.trim()) {
package/dist/setup.d.ts CHANGED
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * Setup script for the Canon Claude Code plugin.
4
4
  *
5
- * Run after `npm install -g @canonmsg/claude-code-plugin` to install the
6
- * /canon-register and /canon-configure skills.
5
+ * Run after `npm install -g @canonmsg/claude-code-plugin` to:
6
+ * 1. Install the /canon-register and /canon-configure skills
7
+ * 2. Show the user how to add the MCP server to their .mcp.json
7
8
  */
8
9
  export declare function main(): void;
package/dist/setup.js CHANGED
@@ -2,14 +2,15 @@
2
2
  /**
3
3
  * Setup script for the Canon Claude Code plugin.
4
4
  *
5
- * Run after `npm install -g @canonmsg/claude-code-plugin` to install the
6
- * /canon-register and /canon-configure skills.
5
+ * Run after `npm install -g @canonmsg/claude-code-plugin` to:
6
+ * 1. Install the /canon-register and /canon-configure skills
7
+ * 2. Show the user how to add the MCP server to their .mcp.json
7
8
  */
8
9
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
9
10
  import { join, dirname } from 'node:path';
10
11
  import { fileURLToPath } from 'node:url';
11
12
  import { runCli } from './cli-entry.js';
12
- const HELP = `canon-setup — install Claude Code Canon skills
13
+ const HELP = `canon-setup — install Claude Code Canon skills and print MCP setup
13
14
 
14
15
  USAGE
15
16
  canon-setup
@@ -18,7 +19,8 @@ FLAGS
18
19
  --help, -h Show this help
19
20
  --version, -V Show package version
20
21
 
21
- This command writes Canon Claude Code skill files under ~/.claude/skills.`;
22
+ This command writes Canon Claude Code skill files under ~/.claude/skills and
23
+ prints the MCP server configuration for Claude Code channel mode.`;
22
24
  const __dirname = dirname(fileURLToPath(import.meta.url));
23
25
  const home = process.env.HOME || process.env.USERPROFILE || '';
24
26
  const skillsDir = join(home, '.claude', 'skills');
@@ -43,6 +45,27 @@ function installSkills() {
43
45
  console.log(` Installed /canon-${skill}`);
44
46
  }
45
47
  }
48
+ // ── Show MCP config instructions ───────────────────────────────────────
49
+ function showMcpInstructions() {
50
+ // Find the installed server.js path
51
+ const serverPath = join(__dirname, 'server.js');
52
+ console.log(`
53
+ Add this to your project's .mcp.json (or ~/.mcp.json for global):
54
+
55
+ {
56
+ "mcpServers": {
57
+ "canon-channel": {
58
+ "command": "node",
59
+ "args": ["${serverPath}"]
60
+ }
61
+ }
62
+ }
63
+
64
+ Then start Claude Code with:
65
+
66
+ claude --dangerously-load-development-channels server:canon-channel
67
+ `);
68
+ }
46
69
  // ── Main ───────────────────────────────────────────────────────────────
47
70
  export function main() {
48
71
  console.log('Canon Claude Code Plugin Setup');
@@ -52,7 +75,8 @@ export function main() {
52
75
  console.log('\nRegistering your agent...');
53
76
  console.log(' Run /canon-register in Claude Code to register a new agent');
54
77
  console.log(' Run /canon-configure if you already have an API key\n');
55
- console.log('Run `canon-claude` to connect this agent to Canon.');
78
+ console.log('MCP server configuration:');
79
+ showMcpInstructions();
56
80
  }
57
81
  runCli(import.meta.url, main, (error) => {
58
82
  console.error('[canon-setup] Fatal error:', error);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Startup recovery for inbound messages missed while the host was offline.
3
+ *
4
+ * The host persists a `lastInboundMessageId` cursor per conversation. On
5
+ * startup we paginate `getMessagesPage` (newest-first pages, older pages via
6
+ * its `before` message-id parameter) until the cursor is found or a hard
7
+ * per-conversation bound is hit, then replay everything after the cursor.
8
+ *
9
+ * This module is intentionally identical in packages/claude-code-plugin and
10
+ * packages/codex-plugin — keep both copies in sync (future consolidation
11
+ * candidate).
12
+ */
13
+ export declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
14
+ export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
15
+ export interface StartupRecoveryMessage {
16
+ id: string;
17
+ senderId: string;
18
+ createdAt?: string;
19
+ }
20
+ export interface StartupRecoveryPage {
21
+ messages: StartupRecoveryMessage[];
22
+ }
23
+ export type StartupRecoveryMode =
24
+ /** Cursor found — `messages` is everything strictly after it. */
25
+ 'after-cursor'
26
+ /** Cursor present but not found within the bound — `messages` is the bounded recent window. */
27
+ | 'truncated-window'
28
+ /**
29
+ * No usable cursor (fresh runtime file, or the cursor message no longer
30
+ * exists in history) — only the newest inbound message is recovered, since
31
+ * a full-history replay could fire mass duplicate turns.
32
+ */
33
+ | 'latest-only';
34
+ export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
35
+ mode: StartupRecoveryMode;
36
+ /** Missed inbound messages (own messages excluded), oldest first. */
37
+ messages: TPage['messages'];
38
+ /** First page fetched — reusable as hydration context for recovered turns. */
39
+ newestPage: TPage;
40
+ }
41
+ export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
42
+ fetchPage: (before?: string) => Promise<TPage>;
43
+ cursor: string | null | undefined;
44
+ agentId: string;
45
+ maxMessages?: number;
46
+ }): Promise<StartupRecoveryResult<TPage>>;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Startup recovery for inbound messages missed while the host was offline.
3
+ *
4
+ * The host persists a `lastInboundMessageId` cursor per conversation. On
5
+ * startup we paginate `getMessagesPage` (newest-first pages, older pages via
6
+ * its `before` message-id parameter) until the cursor is found or a hard
7
+ * per-conversation bound is hit, then replay everything after the cursor.
8
+ *
9
+ * This module is intentionally identical in packages/claude-code-plugin and
10
+ * packages/codex-plugin — keep both copies in sync (future consolidation
11
+ * candidate).
12
+ */
13
+ export const STARTUP_RECOVERY_PAGE_SIZE = 25;
14
+ export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
15
+ export async function collectMissedInboundMessages(input) {
16
+ const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
17
+ const newestPage = await input.fetchPage();
18
+ const collected = [...newestPage.messages];
19
+ const seenIds = new Set(collected.map((message) => message.id));
20
+ const hasCursor = (messages) => input.cursor != null && messages.some((message) => message.id === input.cursor);
21
+ let cursorFound = hasCursor(collected);
22
+ if (input.cursor != null) {
23
+ while (!cursorFound && collected.length < maxMessages) {
24
+ // Pages are newest-first, so the last collected message is the oldest.
25
+ const before = collected[collected.length - 1]?.id;
26
+ if (!before)
27
+ break;
28
+ const page = await input.fetchPage(before);
29
+ const fresh = page.messages.filter((message) => !seenIds.has(message.id));
30
+ // No pagination progress (history exhausted, or the server ignored the
31
+ // `before` cursor because that message was hard-deleted) — stop here.
32
+ if (fresh.length === 0)
33
+ break;
34
+ for (const message of fresh)
35
+ seenIds.add(message.id);
36
+ collected.push(...fresh);
37
+ cursorFound = hasCursor(fresh);
38
+ }
39
+ }
40
+ const ascending = [...collected].sort((a, b) => String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')));
41
+ const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId);
42
+ let mode;
43
+ let missed;
44
+ if (cursorFound) {
45
+ const cursorIndex = ascending.findIndex((message) => message.id === input.cursor);
46
+ mode = 'after-cursor';
47
+ missed = inboundOnly(ascending.slice(cursorIndex + 1));
48
+ }
49
+ else if (input.cursor != null && collected.length >= maxMessages) {
50
+ mode = 'truncated-window';
51
+ missed = inboundOnly(ascending.slice(-maxMessages));
52
+ }
53
+ else {
54
+ mode = 'latest-only';
55
+ missed = inboundOnly(ascending).slice(-1);
56
+ }
57
+ // Safe: `missed` only holds elements of pages returned by `fetchPage`.
58
+ return { mode, messages: missed, newestPage };
59
+ }
@@ -0,0 +1,16 @@
1
+ export interface StreamingBufferOptions {
2
+ throttleMs: number;
3
+ getText: () => string;
4
+ setText: (text: string) => void;
5
+ getTimer: () => ReturnType<typeof setTimeout> | null;
6
+ setTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
7
+ onWrite: (text: string) => void;
8
+ schedule?: typeof setTimeout;
9
+ cancel?: typeof clearTimeout;
10
+ }
11
+ export declare function createStreamingBuffer(options: StreamingBufferOptions): {
12
+ append(delta: string): void;
13
+ flush: () => void;
14
+ clear(): void;
15
+ replace(text: string): void;
16
+ };
@@ -0,0 +1,52 @@
1
+ export function createStreamingBuffer(options) {
2
+ const schedule = options.schedule ?? setTimeout;
3
+ const cancel = options.cancel ?? clearTimeout;
4
+ const flush = () => {
5
+ const timer = options.getTimer();
6
+ if (timer) {
7
+ cancel(timer);
8
+ options.setTimer(null);
9
+ }
10
+ const text = options.getText();
11
+ if (text) {
12
+ options.onWrite(text);
13
+ }
14
+ };
15
+ return {
16
+ append(delta) {
17
+ if (!delta)
18
+ return;
19
+ options.setText(`${options.getText()}${delta}`);
20
+ if (options.getTimer())
21
+ return;
22
+ const timer = schedule(() => {
23
+ options.setTimer(null);
24
+ const text = options.getText();
25
+ if (text) {
26
+ options.onWrite(text);
27
+ }
28
+ }, options.throttleMs);
29
+ options.setTimer(timer);
30
+ },
31
+ flush,
32
+ clear() {
33
+ const timer = options.getTimer();
34
+ if (timer) {
35
+ cancel(timer);
36
+ options.setTimer(null);
37
+ }
38
+ options.setText('');
39
+ },
40
+ replace(text) {
41
+ const timer = options.getTimer();
42
+ if (timer) {
43
+ cancel(timer);
44
+ options.setTimer(null);
45
+ }
46
+ options.setText(text);
47
+ if (text) {
48
+ options.onWrite(text);
49
+ }
50
+ },
51
+ };
52
+ }
@@ -1,5 +1,4 @@
1
- import type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core/contract';
2
- import type { ApprovalManager } from '@canonmsg/core/host';
1
+ import type { ApprovalManager, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core';
3
2
  export type ClaudeToolPolicyDecision = {
4
3
  behavior: 'allow';
5
4
  } | {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.22.0",
4
- "description": "Canon host for Claude Code — messaging where AI agents are first-class citizens",
3
+ "version": "0.23.0",
4
+ "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -12,28 +12,28 @@
12
12
  }
13
13
  },
14
14
  "bin": {
15
+ "canon-channel-server": "dist/server.js",
15
16
  "canon-claude": "dist/host.js",
16
17
  "canon-register": "dist/register.js",
17
18
  "canon-setup": "dist/setup.js"
18
19
  },
19
20
  "files": [
20
21
  "dist",
21
- "skills"
22
+ "skills",
23
+ ".claude-plugin"
22
24
  ],
23
25
  "scripts": {
24
- "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../../packages/core ../../packages/agent-sdk ../../packages/framework ../../packages/bridge ../../packages/agent-host",
26
+ "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
25
27
  "build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
26
28
  "dev": "npm run prepare:workspace-deps && tsc --watch",
27
29
  "prepack": "npm run build",
28
30
  "test": "vitest run"
29
31
  },
30
32
  "dependencies": {
31
- "@anthropic-ai/claude-agent-sdk": "^0.3.200",
32
- "@canonmsg/agent-host": "^0.5.0",
33
- "@canonmsg/agent-sdk": "^4.0.0",
34
- "@canonmsg/bridge": "^0.3.0",
35
- "@canonmsg/core": "^3.3.0",
36
- "@canonmsg/framework": "^0.3.0",
33
+ "@anthropic-ai/claude-agent-sdk": "^0.2.141",
34
+ "@canonmsg/agent-sdk": "^5.0.0",
35
+ "@canonmsg/coding-agent-host": "^0.2.2",
36
+ "@canonmsg/core": "^4.0.0",
37
37
  "@modelcontextprotocol/sdk": "^1.29.0"
38
38
  },
39
39
  "engines": {
@@ -50,9 +50,9 @@
50
50
  "repository": {
51
51
  "type": "git",
52
52
  "url": "https://github.com/HeyBobChan/canon",
53
- "directory": "adapters/claude-code-plugin"
53
+ "directory": "packages/claude-code-plugin"
54
54
  },
55
- "homepage": "https://github.com/HeyBobChan/canon/tree/main/adapters/claude-code-plugin",
55
+ "homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/claude-code-plugin",
56
56
  "publishConfig": {
57
57
  "access": "public"
58
58
  },
@@ -36,9 +36,15 @@ Add an existing Canon agent API key as a named profile.
36
36
 
37
37
  5. Write the updated profiles to `~/.canon/agents.json`.
38
38
 
39
- 6. Tell the user: **"API key saved as profile '<profile>'. Run `canon-claude` to connect."**
39
+ 6. Tell the user: **"API key saved as profile '<profile>'. Restart Claude Code to connect."**
40
40
 
41
- If they have multiple agents and want to pin one, start the host with the profile name:
42
- ```bash
43
- CANON_AGENT=<profile> canon-claude
41
+ If they have multiple agents and want to pin one, add to `.mcp.json`:
42
+ ```json
43
+ {
44
+ "mcpServers": {
45
+ "canon-channel": {
46
+ "env": { "CANON_AGENT": "<profile>" }
47
+ }
48
+ }
49
+ }
44
50
  ```
@@ -1,14 +0,0 @@
1
- /**
2
- * Packaged canon-bridge daemon resolution (bridge plan Phase 6, §F3).
3
- *
4
- * On a fresh npm install nothing has populated ~/.canon/bin yet, so
5
- * connectBridge()'s fallback chain (explicit binPath → $CANON_BRIDGE_BIN →
6
- * the shared install) has nothing to spawn. The plugin therefore ships the
7
- * daemon as its own @canonmsg/bridge dependency and passes the packaged
8
- * dist entry as binPath.
9
- *
10
- * $CANON_BRIDGE_BIN still wins: when the env override is set we return
11
- * undefined and let connectBridge()'s own env resolution take it (an
12
- * options.binPath would out-rank the env var otherwise).
13
- */
14
- export declare function resolvePackagedBridgeBin(env?: NodeJS.ProcessEnv): string | undefined;
@@ -1,27 +0,0 @@
1
- /**
2
- * Packaged canon-bridge daemon resolution (bridge plan Phase 6, §F3).
3
- *
4
- * On a fresh npm install nothing has populated ~/.canon/bin yet, so
5
- * connectBridge()'s fallback chain (explicit binPath → $CANON_BRIDGE_BIN →
6
- * the shared install) has nothing to spawn. The plugin therefore ships the
7
- * daemon as its own @canonmsg/bridge dependency and passes the packaged
8
- * dist entry as binPath.
9
- *
10
- * $CANON_BRIDGE_BIN still wins: when the env override is set we return
11
- * undefined and let connectBridge()'s own env resolution take it (an
12
- * options.binPath would out-rank the env var otherwise).
13
- */
14
- import { createRequire } from 'node:module';
15
- import { dirname, join as joinPath } from 'node:path';
16
- export function resolvePackagedBridgeBin(env = process.env) {
17
- if (env.CANON_BRIDGE_BIN)
18
- return undefined;
19
- try {
20
- const require = createRequire(import.meta.url);
21
- const manifestPath = require.resolve('@canonmsg/bridge/package.json');
22
- return joinPath(dirname(manifestPath), 'dist', 'main.js');
23
- }
24
- catch {
25
- return undefined;
26
- }
27
- }