@canonmsg/claude-code-plugin 0.34.3 → 0.34.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "Canon",
3
3
  "description": "Connect Claude Code to Canon — messaging where AI agents are first-class citizens",
4
- "version": "0.34.3",
4
+ "version": "0.34.5",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Canon Plugin for Claude Code
2
2
 
3
- Connect Claude Code to [Canon](https://github.com/HeyBobChan/canon) — a messaging app where AI agents are first-class citizens. Control Claude Code from your phone.
3
+ Connect Claude Code to [Canon](https://canonmail.com/agents) — a messaging app where AI agents are first-class citizens. Control Claude Code from your phone.
4
4
 
5
5
  ## Quick start
6
6
 
package/dist/host.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { type PermissionResult, type SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
2
+ import { type PermissionResult, type Query, type SDKControlReloadPluginsResponse, type SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
3
3
  import { type RecoveryCheckpointTracker } from '@canonmsg/coding-agent-host';
4
- import { type CanonRuntimeCommandDescriptor, type ExecutionEnvironmentMode, type HostInboundParticipantContext as InboundParticipantContext, type CanonReplyContext, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type TurnVerbosityConfig } from '@canonmsg/core';
4
+ import { type CanonRuntimeCommandDescriptor, type CanonRuntimeDescriptor, type CanonRuntimeFact, type CanonRuntimePresentationPolicy, type ExecutionEnvironmentMode, type DeliveryIntent, type ModelOption, type HostInboundParticipantContext as InboundParticipantContext, type CanonReplyContext, type MessageCreatedPayload, type ResolvedAgentBehaviorPolicy, type TurnLifecycleState, type TurnVerbosity, type TurnVerbosityConfig, type PreparedExecutionEnvironment, type WorkspaceOption, type CanonWorkspaceRootMetadata } from '@canonmsg/core';
5
+ import { type ClaudeInputEnvelope, type ClaudePendingFinalDelivery, type ClaudeTurnActivityState, type ClaudeTurnModes, type ClaudeRuntimeControlError } from './session-state.js';
5
6
  /**
6
7
  * `--turn-verbosity` beats `CANON_TURN_VERBOSITY`; `null` means "unset, use the
7
8
  * per-conversation-type default".
@@ -19,13 +20,92 @@ export declare function resolveConfiguredClaudeTurnVerbosity(input: {
19
20
  }): TurnVerbosityConfig | null;
20
21
  export declare function buildClaudePlanAllowResult(input: Record<string, unknown>): PermissionResult;
21
22
  /**
22
- * Claude Code's own slash commands (built-ins, plugins, project .claude/
23
- * commands) are executable when sent as prompt text, so the runtime's command
24
- * inventory is exposed as composer slash entries that pass the raw slash text
25
- * through. Canon-injected aliases keep precedence over native names.
23
+ * Publish only commands returned by the SDK, using its canonical name and
24
+ * aliases. Reserve aliases only for implemented Canon session actions.
26
25
  */
27
26
  export declare function buildNativeSlashCommandDescriptors(native: SDKControlReloadPluginsResponse['commands'], reservedAliases: ReadonlySet<string>): CanonRuntimeCommandDescriptor[];
27
+ export declare function buildClaudeRuntimeDescriptor(input: {
28
+ nativeCommands?: SDKControlReloadPluginsResponse['commands'];
29
+ models: ModelOption[];
30
+ workspaces: WorkspaceOption[];
31
+ workspaceRoots?: CanonWorkspaceRootMetadata[];
32
+ executionModes: ExecutionEnvironmentMode[];
33
+ defaultExecutionMode?: ExecutionEnvironmentMode;
34
+ presentation?: CanonRuntimePresentationPolicy;
35
+ }): CanonRuntimeDescriptor;
28
36
  export declare function resolveSessionExecutionMode(defaultExecutionMode?: ExecutionEnvironmentMode): ExecutionEnvironmentMode;
37
+ interface SessionState {
38
+ model?: string;
39
+ permissionMode?: string;
40
+ effort?: string;
41
+ ultracode?: string;
42
+ state?: 'idle' | 'running' | 'requires_action';
43
+ contextUsage?: {
44
+ percentage: number;
45
+ totalTokens: number;
46
+ maxTokens: number;
47
+ };
48
+ }
49
+ interface Session {
50
+ observedModel?: string;
51
+ observedEffort?: string;
52
+ conversationId: string;
53
+ cwd: string;
54
+ environment: PreparedExecutionEnvironment;
55
+ query: Query;
56
+ sendInput: (input: ClaudeInputEnvelope) => void;
57
+ enqueueInbound: (msg: SDKUserMessage, intent?: DeliveryIntent, sourceMessageId?: string | null, markAccepted?: boolean, isOwnerTurn?: boolean, requestingUserId?: string | null, turnModes?: ClaudeTurnModes) => void;
58
+ state: SessionState;
59
+ availableModels: ModelOption[];
60
+ /**
61
+ * The verbosity the RUNNING turn was opened with. Re-affirmed from each
62
+ * turn's envelope at `openTurn` and never re-read mid-turn: Claude replays a
63
+ * final's metadata byte-for-byte across delivery retries, so a trail that
64
+ * appeared or vanished between attempts would turn a harmless replay into a
65
+ * 409.
66
+ */
67
+ turnVerbosity: TurnVerbosity;
68
+ streamingText: string;
69
+ streamingTimer: ReturnType<typeof setTimeout> | null;
70
+ idleResetTimer: ReturnType<typeof setTimeout> | null;
71
+ finalDeliveryTimer: ReturnType<typeof setTimeout> | null;
72
+ pendingInputs: ClaudeInputEnvelope[];
73
+ activeInput: ClaudeInputEnvelope | null;
74
+ /**
75
+ * The input being dispatched: the slot is reserved but the SDK does not have
76
+ * it yet. Only an interrupt cares — there is nothing in flight to stop.
77
+ */
78
+ dispatchingInput: ClaudeInputEnvelope | null;
79
+ finalizedTurnKeys: Set<string>;
80
+ interruptedTurnKeys: Set<string>;
81
+ /** Turns whose model called `no_reply`: end without posting anything. */
82
+ silencedTurnKeys: Set<string>;
83
+ pendingFinalText: string | null;
84
+ pendingFinalDelivery: ClaudePendingFinalDelivery | null;
85
+ runtimeControlErrors: Record<string, ClaudeRuntimeControlError>;
86
+ toolInProgress: boolean;
87
+ turnState: TurnLifecycleState;
88
+ currentTurnId: string | null;
89
+ currentTurnOpenedAt: number | null;
90
+ currentTurnUpdatedAt: number | null;
91
+ /** Per-turn margin-trail bookkeeping (one block per tool call). */
92
+ turnActivity: ClaudeTurnActivityState;
93
+ activeSelfContextId: string | null;
94
+ lastAcceptedIntent: DeliveryIntent | null;
95
+ lastActivity: number;
96
+ typingKeepaliveTimer: ReturnType<typeof setInterval> | null;
97
+ controlInterruptPending: boolean;
98
+ closed: boolean;
99
+ /** SDK session ID — captured from result messages for resume support */
100
+ sdkSessionId: string | null;
101
+ writeState: () => void;
102
+ writeTurn: () => void;
103
+ clearStreaming: () => Promise<void>;
104
+ markInputCompleted: (input: ClaudeInputEnvelope | null | undefined) => void;
105
+ setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
106
+ clearRuntimeControlError: (controlId: string) => void;
107
+ }
108
+ export declare function buildClaudeSessionFacts(session?: Pick<Session, 'closed' | 'observedModel' | 'observedEffort'>): CanonRuntimeFact[];
29
109
  export declare function createClaudeRecoveryCheckpointTracker(persist: (messageId: string) => boolean): RecoveryCheckpointTracker;
30
110
  export declare const NO_REPLY_TOOL_NAME: string;
31
111
  export declare function buildCanonPrompt(input: {
@@ -40,3 +120,4 @@ export declare function buildCanonPrompt(input: {
40
120
  message?: MessageCreatedPayload['message'];
41
121
  }): string;
42
122
  export declare function main(): Promise<void>;
123
+ export {};
package/dist/host.js CHANGED
@@ -29,6 +29,7 @@ import { query, } from '@anthropic-ai/claude-agent-sdk';
29
29
  import { materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
30
30
  import { captureTurnArtifactSnapshot, createTurnArtifactRouter, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createRecoveryCheckpointTracker, createReconnectRecoveryCoordinator, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
31
31
  import { buildCanonUserContent, renderClaudeInboundContent, withClaudeReplyContextMediaReferences, } from './canon-user-content.js';
32
+ import { buildClaudeNativeCommandInput, createClaudeCommandContextHook, getClaudeNativeCommandOutput, } from './native-command-input.js';
32
33
  import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createRuntimeHeartbeat, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, ExecutionEnvironmentError, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, parseTurnVerbosityConfig, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, resolveLocalRuntimeSessionState, saveRuntimeSessionState, clearRuntimeSessionState, publishHostSessionSnapshots, renderCodingHostInboundPrompt, resolveConfiguredWorkspaceCwd, resolveTurnVerbosity, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
33
34
  import { runCli } from '@canonmsg/core';
34
35
  import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
@@ -134,16 +135,14 @@ export function buildClaudePlanAllowResult(input) {
134
135
  return { behavior: 'allow', updatedInput: input };
135
136
  }
136
137
  /**
137
- * Claude Code's own slash commands (built-ins, plugins, project .claude/
138
- * commands) are executable when sent as prompt text, so the runtime's command
139
- * inventory is exposed as composer slash entries that pass the raw slash text
140
- * through. Canon-injected aliases keep precedence over native names.
138
+ * Publish only commands returned by the SDK, using its canonical name and
139
+ * aliases. Reserve aliases only for implemented Canon session actions.
141
140
  */
142
141
  export function buildNativeSlashCommandDescriptors(native, reservedAliases) {
143
142
  const descriptors = [];
144
- for (const command of native.slice(0, MAX_NATIVE_SLASH_COMMANDS)) {
143
+ for (const command of native) {
145
144
  const name = String(command.name ?? '').replace(/^\//, '').trim();
146
- if (!name)
145
+ if (!/^[A-Za-z0-9:_-]+$/.test(name))
147
146
  continue;
148
147
  descriptors.push({
149
148
  id: `claude-native-${name}`,
@@ -151,12 +150,13 @@ export function buildNativeSlashCommandDescriptors(native, reservedAliases) {
151
150
  description: command.description
152
151
  ?? ('argumentHint' in command ? command.argumentHint : undefined)
153
152
  ?? 'Claude Code command (runs in the runtime).',
154
- aliases: [name],
153
+ aliases: [name, ...(command.aliases ?? []).filter((alias) => /^\/?[A-Za-z0-9:_-]+$/.test(alias))],
155
154
  category: 'runtime',
156
155
  placements: ['composer_slash', 'command_palette'],
157
156
  availability: ['always'],
158
157
  trailingTextBehavior: 'send_as_prompt',
159
- dispatch: { kind: 'text_passthrough', template: `/${name}` },
158
+ args: [{ id: 'prompt', label: 'Arguments', kind: 'string', captureRemaining: true }],
159
+ dispatch: { kind: 'text_passthrough', template: `/${name} {argument}` },
160
160
  });
161
161
  }
162
162
  return normalizeRuntimeCommandDescriptors(descriptors, {
@@ -164,100 +164,19 @@ export function buildNativeSlashCommandDescriptors(native, reservedAliases) {
164
164
  maxCommands: MAX_NATIVE_SLASH_COMMANDS,
165
165
  });
166
166
  }
167
- function buildClaudeRuntimeDescriptor(input) {
167
+ export function buildClaudeRuntimeDescriptor(input) {
168
168
  const commands = [
169
169
  {
170
170
  id: 'runtime-status',
171
- label: 'Runtime status',
172
- description: 'Open Claude runtime details, including account, project, and policy status.',
171
+ label: 'Session info',
172
+ description: 'Open Canon session information.',
173
173
  primitive: 'runtime.status',
174
- aliases: ['status'],
174
+ aliases: ['session-info'],
175
175
  category: 'details',
176
176
  placements: ['composer_slash', 'command_palette'],
177
177
  availability: ['always'],
178
178
  dispatch: { kind: 'open_details', target: 'status' },
179
179
  },
180
- {
181
- id: 'mcp-inventory',
182
- label: 'MCP servers',
183
- description: 'Open the Claude MCP inventory published by the host.',
184
- aliases: ['mcp'],
185
- category: 'details',
186
- placements: ['composer_slash', 'command_palette'],
187
- availability: ['always'],
188
- dispatch: { kind: 'open_details', target: 'mcp' },
189
- },
190
- {
191
- id: 'plugin-inventory',
192
- label: 'Plugins',
193
- description: 'Open the Claude plugin and command inventory published by the host.',
194
- aliases: ['plugins'],
195
- category: 'details',
196
- placements: ['composer_slash', 'command_palette'],
197
- availability: ['always'],
198
- dispatch: { kind: 'open_details', target: 'plugins' },
199
- },
200
- {
201
- id: 'model-details',
202
- label: 'Model control',
203
- description: 'Open runtime details and use the live Model control.',
204
- aliases: ['model'],
205
- category: 'details',
206
- placements: ['composer_slash', 'command_palette'],
207
- availability: ['always'],
208
- dispatch: { kind: 'open_details', target: 'model' },
209
- },
210
- {
211
- id: 'permission-details',
212
- label: 'Permission mode',
213
- description: 'Open runtime details and use the live Permission mode control.',
214
- aliases: ['permission'],
215
- category: 'details',
216
- placements: ['composer_slash', 'command_palette'],
217
- availability: ['always'],
218
- dispatch: { kind: 'open_details', target: 'permissionMode' },
219
- },
220
- {
221
- id: 'effort-details',
222
- label: 'Thinking level',
223
- description: 'Set Claude effort using /effort or /think.',
224
- primitive: 'runtime.reasoning.set',
225
- aliases: ['effort', 'think'],
226
- category: 'runtime',
227
- placements: ['composer_slash', 'command_palette'],
228
- availability: ['always'],
229
- args: [{
230
- id: 'level',
231
- label: 'Level',
232
- kind: 'enum',
233
- required: true,
234
- choices: [...EFFORT_OPTIONS],
235
- }],
236
- dispatch: {
237
- kind: 'control',
238
- controlId: 'effort',
239
- },
240
- },
241
- {
242
- id: 'ultracode-details',
243
- label: 'Ultracode',
244
- description: 'Toggle Claude ultracode using /ultracode.',
245
- aliases: ['ultracode'],
246
- category: 'runtime',
247
- placements: ['composer_slash', 'command_palette'],
248
- availability: ['always'],
249
- args: [{
250
- id: 'level',
251
- label: 'Level',
252
- kind: 'enum',
253
- required: true,
254
- choices: [...CLAUDE_ULTRACODE_OPTIONS],
255
- }],
256
- dispatch: {
257
- kind: 'control',
258
- controlId: 'ultracode',
259
- },
260
- },
261
180
  {
262
181
  ...RUNTIME_NEW_SESSION_ACTION,
263
182
  primitive: 'session.new',
@@ -265,7 +184,7 @@ function buildClaudeRuntimeDescriptor(input) {
265
184
  RUNTIME_STOP_ACTION,
266
185
  RUNTIME_STOP_AND_DROP_ACTION,
267
186
  ];
268
- commands.push(...buildNativeSlashCommandDescriptors(runtimeMetadata.commands, new Set(commands.flatMap((command) => [command.id, ...(command.aliases ?? [])]))));
187
+ commands.push(...buildNativeSlashCommandDescriptors(input.nativeCommands ?? runtimeMetadata.commands, new Set(commands.flatMap((command) => [command.id, ...(command.aliases ?? [])]))));
269
188
  return buildFirstPartyCodingRuntimeDescriptor({
270
189
  clientType: 'claude-code',
271
190
  models: input.models,
@@ -551,6 +470,16 @@ async function readApprovalDiffPreImage(filePath) {
551
470
  throw error;
552
471
  }
553
472
  }
473
+ export function buildClaudeSessionFacts(session) {
474
+ if (!session || session.closed)
475
+ return [];
476
+ const facts = [{ id: 'runtime', label: 'Runtime', value: 'Claude Agent SDK', group: 'runtime' }];
477
+ if (session.observedModel)
478
+ facts.push({ id: 'model', label: 'Model', value: session.observedModel, group: 'model' });
479
+ if (session.observedEffort)
480
+ facts.push({ id: 'reasoning', label: 'Reasoning', value: session.observedEffort, group: 'model' });
481
+ return facts;
482
+ }
554
483
  // ── Constants ───────────────────────────────────────────────────────
555
484
  const MAX_SESSIONS = 5;
556
485
  // IDLE_TIMEOUT_MS (30 minutes) is shared with the other coding-agent hosts.
@@ -692,6 +621,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
692
621
  // from here.
693
622
  if (!claudeInputOwnsTurnSlot(input))
694
623
  return;
624
+ // Native commands can change settings without reporting the new value.
625
+ // Wait for this turn's SDK observations instead of repeating old defaults.
626
+ session.observedModel = undefined;
627
+ session.observedEffort = undefined;
695
628
  session.activeInput = input;
696
629
  }
697
630
  const inputStream = {
@@ -793,6 +726,16 @@ function createSession(conversationId, environment, agentId, client, typingSigna
793
726
  ...(config.effort ? { effort: config.effort } : {}),
794
727
  settings: { forceLoginMethod: 'claudeai' },
795
728
  settingSources: ['project', 'local'],
729
+ hooks: {
730
+ UserPromptExpansion: [{
731
+ hooks: [createClaudeCommandContextHook(() => session.activeInput?.commandContext)],
732
+ }],
733
+ Stop: [{ hooks: [async (input) => {
734
+ if (!input.agent_id)
735
+ session.observedEffort = input.effort?.level;
736
+ return {};
737
+ }] }],
738
+ },
796
739
  // Canonical Canon verbs, in-process (projection 2 of canon.verbs.v1).
797
740
  // Fresh instance per session: the SDK connects the instance to its own
798
741
  // in-process transport, so instances are not shared across queries.
@@ -1826,6 +1769,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1826
1769
  // ever saw streamed.
1827
1770
  if (!isClaudeMainTurnMessage(msg.parent_tool_use_id))
1828
1771
  break;
1772
+ if (!msg.error && msg.message.model)
1773
+ session.observedModel = msg.message.model;
1829
1774
  // The assistant message is the first place the tool call's typed
1830
1775
  // INPUT is visible — content_block_start carries only the name. It
1831
1776
  // is what turns "Bash" into "Running: npm test" and TodoWrite into
@@ -1942,6 +1887,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1942
1887
  }
1943
1888
  case 'system': {
1944
1889
  const subtype = msg.subtype;
1890
+ const commandOutput = getClaudeNativeCommandOutput(msg);
1891
+ if (commandOutput && claudeInputOwnsTurnSlot(session.activeInput) && !isActiveTurnInterrupted()) {
1892
+ session.pendingFinalText = [session.pendingFinalText, commandOutput].filter(Boolean).join('\n');
1893
+ }
1945
1894
  if (subtype === 'session_state_changed') {
1946
1895
  const state = msg.state;
1947
1896
  // The echo shares one field with the host's own reservation and
@@ -2425,12 +2374,9 @@ export async function main() {
2425
2374
  return;
2426
2375
  await runtimeState.writeRuntimeInfo(conversationId, {
2427
2376
  surfaceMode: 'host',
2428
- descriptor: {
2429
- coreControls: [],
2430
- supportsInterrupt: descriptor.supportsInterrupt,
2431
- supportsInputInterrupt: descriptor.supportsInputInterrupt,
2432
- streamingTextMode: descriptor.streamingTextMode,
2433
- },
2377
+ // The shared publisher sanitizes configuration while retaining commands.
2378
+ descriptor,
2379
+ facts: buildClaudeSessionFacts(sessions.get(conversationId)),
2434
2380
  });
2435
2381
  }));
2436
2382
  const failure = results.find((result) => result.status === 'rejected');
@@ -2993,11 +2939,20 @@ export async function main() {
2993
2939
  replyContext,
2994
2940
  message: m,
2995
2941
  });
2996
- const messageContent = await buildCanonUserContent({
2942
+ const commandInput = buildClaudeNativeCommandInput({
2943
+ text: m.text,
2944
+ senderType: m.senderType,
2997
2945
  promptText,
2946
+ commands: runtimeMetadata.commands,
2947
+ });
2948
+ const messageContent = await buildCanonUserContent({
2949
+ promptText: commandInput.promptText,
2998
2950
  materialized: [...replyMedia.materialized, ...materialized],
2999
2951
  });
3000
- const turnModes = resolveClaudeTurnModes(participantContext);
2952
+ const turnModes = {
2953
+ ...resolveClaudeTurnModes(participantContext),
2954
+ commandContext: commandInput.commandContext,
2955
+ };
3001
2956
  session.enqueueInbound({
3002
2957
  type: 'user',
3003
2958
  message: {
@@ -3262,12 +3217,19 @@ export async function main() {
3262
3217
  replyContext,
3263
3218
  message: m,
3264
3219
  });
3265
- const messageContent = await buildCanonUserContent({
3220
+ const commandInput = buildClaudeNativeCommandInput({
3221
+ text: m.text,
3222
+ senderType: m.senderType,
3266
3223
  promptText,
3224
+ commands: runtimeMetadata.commands,
3225
+ });
3226
+ const messageContent = await buildCanonUserContent({
3227
+ promptText: commandInput.promptText,
3267
3228
  materialized: [...replyMedia.materialized, ...materialized],
3268
3229
  });
3269
3230
  const turnModes = {
3270
3231
  ...resolveClaudeTurnModes(participantContext),
3232
+ commandContext: commandInput.commandContext,
3271
3233
  replyAuthority: payload.replyAuthority ?? null,
3272
3234
  };
3273
3235
  session.enqueueInbound({
@@ -0,0 +1,18 @@
1
+ import type { HookCallback, SDKControlReloadPluginsResponse } from '@anthropic-ai/claude-agent-sdk';
2
+ /** Keep SDK command syntax intact; pass Canon context to the expansion hook. */
3
+ export declare function buildClaudeNativeCommandInput(input: {
4
+ text?: string | null;
5
+ senderType?: string;
6
+ promptText: string;
7
+ commands: SDKControlReloadPluginsResponse['commands'];
8
+ }): {
9
+ promptText: string;
10
+ commandContext?: string;
11
+ };
12
+ export declare function createClaudeCommandContextHook(getContext: () => string | undefined): HookCallback;
13
+ /** Local SDK commands bypass assistant messages but still have a visible result. */
14
+ export declare function getClaudeNativeCommandOutput(message: {
15
+ type: string;
16
+ subtype?: string;
17
+ content?: unknown;
18
+ }): string | null;
@@ -0,0 +1,26 @@
1
+ /** Keep SDK command syntax intact; pass Canon context to the expansion hook. */
2
+ export function buildClaudeNativeCommandInput(input) {
3
+ const text = input.text?.trim();
4
+ const name = text?.match(/^\/([A-Za-z0-9:_-]+)(?:\s|$)/)?.[1];
5
+ if (input.senderType !== 'human' || !name || !input.commands.some((command) => ([command.name, ...(command.aliases ?? [])].some((alias) => (alias.replace(/^\//, '').toLowerCase() === name.toLowerCase())))))
6
+ return { promptText: input.promptText };
7
+ return {
8
+ promptText: text,
9
+ ...(text !== input.promptText ? { commandContext: input.promptText } : {}),
10
+ };
11
+ }
12
+ export function createClaudeCommandContextHook(getContext) {
13
+ return async (input) => {
14
+ const additionalContext = getContext();
15
+ return input.hook_event_name === 'UserPromptExpansion' && additionalContext
16
+ ? { hookSpecificOutput: { hookEventName: 'UserPromptExpansion', additionalContext } }
17
+ : {};
18
+ };
19
+ }
20
+ /** Local SDK commands bypass assistant messages but still have a visible result. */
21
+ export function getClaudeNativeCommandOutput(message) {
22
+ return message.type === 'system' && message.subtype === 'local_command_output'
23
+ && typeof message.content === 'string' && message.content.trim()
24
+ ? message.content.trim()
25
+ : null;
26
+ }
@@ -5,6 +5,7 @@ import type { TurnArtifactRoutingDecision, TurnArtifactSnapshot } from '@canonms
5
5
  export type ClaudeInputKind = 'seed' | 'canon';
6
6
  export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
7
7
  export interface ClaudeInputEnvelope {
8
+ commandContext?: string;
8
9
  kind: ClaudeInputKind;
9
10
  /** Correlates this envelope with its turn result's `user_message_uuid`. */
10
11
  messageUuid: string;
@@ -32,6 +33,7 @@ export interface ClaudeInputEnvelope {
32
33
  * grouped so they travel together from the enqueue site onto the envelope.
33
34
  */
34
35
  export interface ClaudeTurnModes {
36
+ commandContext?: string;
35
37
  artifactRoutingMode?: ClaudeArtifactRoutingMode;
36
38
  turnVerbosity?: TurnVerbosity;
37
39
  replyAuthority?: AgentReplyAuthorityV1 | null;
@@ -96,6 +98,7 @@ export interface ClaudeCompletedTurnState {
96
98
  turnActivity?: ClaudeTurnActivityState;
97
99
  }
98
100
  export declare function createClaudeInputEnvelope(input: {
101
+ commandContext?: string;
99
102
  kind: ClaudeInputKind;
100
103
  msg: SDKUserMessage;
101
104
  intent?: DeliveryIntent;
@@ -10,6 +10,7 @@ export function createClaudeInputEnvelope(input) {
10
10
  // from a mutable slot and arrival order.
11
11
  const messageUuid = randomUUID();
12
12
  return {
13
+ ...(input.commandContext ? { commandContext: input.commandContext } : {}),
13
14
  kind: input.kind,
14
15
  messageUuid,
15
16
  msg: { ...input.msg, uuid: messageUuid },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.34.3",
3
+ "version": "0.34.5",
4
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",
@@ -31,11 +31,11 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.228",
34
- "@canonmsg/agent-sdk": "^10.2.1",
35
- "@canonmsg/agent-tools": "^0.9.0",
36
- "@canonmsg/coding-agent-host": "^0.7.0",
37
- "@canonmsg/core": "^12.3.0",
38
- "@canonmsg/rich-cards": "^0.10.4",
34
+ "@canonmsg/agent-sdk": "^10.3.0",
35
+ "@canonmsg/agent-tools": "^0.9.1",
36
+ "@canonmsg/coding-agent-host": "^0.7.1",
37
+ "@canonmsg/core": "^12.4.0",
38
+ "@canonmsg/rich-cards": "^0.10.5",
39
39
  "@modelcontextprotocol/sdk": "^1.30.0"
40
40
  },
41
41
  "engines": {
@@ -49,12 +49,7 @@
49
49
  "ai-agents",
50
50
  "messaging"
51
51
  ],
52
- "repository": {
53
- "type": "git",
54
- "url": "https://github.com/HeyBobChan/canon",
55
- "directory": "packages/claude-code-plugin"
56
- },
57
- "homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/claude-code-plugin",
52
+ "homepage": "https://canonmail.com/agents/integrations#claude-code",
58
53
  "publishConfig": {
59
54
  "access": "public"
60
55
  },
@@ -63,5 +58,8 @@
63
58
  "typescript": "~5.7.0",
64
59
  "vitest": "^4.1.8"
65
60
  },
66
- "license": "MIT"
61
+ "license": "MIT",
62
+ "bugs": {
63
+ "url": "https://canonmail.com/support"
64
+ }
67
65
  }