@canonmsg/claude-code-plugin 0.34.2 → 0.34.4

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.2",
4
+ "version": "0.34.4",
5
5
  "channels": [
6
6
  {
7
7
  "server": "canon-channel",
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,
@@ -431,14 +350,6 @@ function resolveWorkspaceCwd() {
431
350
  defaultCwd: workingDir,
432
351
  });
433
352
  }
434
- function resolveExecutionFallbackReason(environment) {
435
- if (!environment?.reason || environment.mode !== 'locked') {
436
- return null;
437
- }
438
- return environment.reason === 'Sharing the base workspace (locked mode)'
439
- ? null
440
- : environment.reason;
441
- }
442
353
  async function detectRuntimeModels(cwd) {
443
354
  const discovered = await detectRuntimeModelsForSelection(cwd, 'default');
444
355
  const supplemental = await detectSupplementalRuntimeModels(cwd, deriveClaudeSupplementalModelProbes(discovered));
@@ -559,146 +470,15 @@ async function readApprovalDiffPreImage(filePath) {
559
470
  throw error;
560
471
  }
561
472
  }
562
- function mapClaudeMcpStatus(status) {
563
- switch (status) {
564
- case 'connected':
565
- return 'ready';
566
- case 'needs-auth':
567
- return 'auth_needed';
568
- case 'disabled':
569
- return 'configured';
570
- case 'failed':
571
- return 'error';
572
- default:
573
- return 'unknown';
574
- }
575
- }
576
- function buildClaudeRuntimeInfo(input) {
577
- const { descriptor, metadata } = input;
578
- const statusItems = [
579
- metadata.account?.email
580
- ? {
581
- id: 'account',
582
- label: 'Account',
583
- value: 'Connected',
584
- tone: 'success',
585
- tier: 'diagnostic',
586
- }
587
- : null,
588
- metadata.account?.subscriptionType
589
- ? {
590
- id: 'subscription',
591
- label: 'Plan',
592
- value: metadata.account.subscriptionType,
593
- }
594
- : null,
595
- metadata.account?.apiProvider
596
- ? {
597
- id: 'provider',
598
- label: 'Provider',
599
- value: metadata.account.apiProvider,
600
- }
601
- : null,
602
- metadata.account?.tokenSource || metadata.account?.apiKeySource
603
- ? {
604
- id: 'auth',
605
- label: 'Auth',
606
- value: metadata.account.tokenSource ?? metadata.account?.apiKeySource ?? 'connected',
607
- tier: 'diagnostic',
608
- }
609
- : null,
610
- runtimeModels[0]
611
- ? {
612
- id: 'fallbackModel',
613
- label: 'Fallback model',
614
- value: runtimeModels[0].label,
615
- tier: 'diagnostic',
616
- source: 'host-default',
617
- }
618
- : null,
619
- {
620
- id: 'toolPolicy',
621
- label: 'Tool policy',
622
- value: 'Claude Code preset',
623
- tier: 'detail',
624
- },
625
- {
626
- id: 'browser',
627
- label: 'Browser tools',
628
- value: 'Runtime policy',
629
- tier: 'detail',
630
- },
631
- {
632
- id: 'mediaOut',
633
- label: 'Media out',
634
- value: 'Generated media artifacts',
635
- tier: 'detail',
636
- },
637
- ].filter((item) => Boolean(item));
638
- return {
639
- descriptor,
640
- surfaceMode: 'host',
641
- statusItems,
642
- inventories: [
643
- {
644
- id: 'mcp',
645
- label: 'MCP',
646
- entries: metadata.mcpServers.map((server) => ({
647
- id: server.name,
648
- label: server.name,
649
- status: mapClaudeMcpStatus(server.status),
650
- description: server.error
651
- ?? server.serverInfo?.version
652
- ?? server.scope
653
- ?? undefined,
654
- })),
655
- tier: 'diagnostic',
656
- },
657
- {
658
- id: 'plugins',
659
- label: 'Plugins',
660
- entries: metadata.plugins.map((plugin) => ({
661
- id: plugin.name,
662
- label: plugin.name,
663
- status: 'configured',
664
- description: plugin.source ?? plugin.path,
665
- })),
666
- tier: 'diagnostic',
667
- },
668
- {
669
- id: 'commands',
670
- label: 'Commands',
671
- entries: metadata.commands.map((command) => ({
672
- id: command.name,
673
- label: command.name.startsWith('/') ? command.name : `/${command.name}`,
674
- status: 'configured',
675
- description: command.description ?? command.argumentHint ?? undefined,
676
- })),
677
- tier: 'diagnostic',
678
- },
679
- {
680
- id: 'agents',
681
- label: 'Agents',
682
- entries: metadata.agents.map((agent) => ({
683
- id: agent.name,
684
- label: agent.name,
685
- status: 'configured',
686
- description: agent.description ?? agent.model ?? undefined,
687
- })),
688
- tier: 'diagnostic',
689
- },
690
- ],
691
- execution: {
692
- resolvedWorkspaceLabel: input.workspaceLabel,
693
- resolvedCwd: input.resolvedCwd,
694
- workspaceRootId: input.workspaceRootId ?? null,
695
- workspaceRelativePath: input.workspaceRelativePath ?? null,
696
- executionMode: input.executionMode,
697
- executionBranch: input.executionBranch,
698
- worktreePath: input.worktreePath,
699
- fallbackReason: input.fallbackReason,
700
- },
701
- };
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;
702
482
  }
703
483
  // ── Constants ───────────────────────────────────────────────────────
704
484
  const MAX_SESSIONS = 5;
@@ -841,6 +621,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
841
621
  // from here.
842
622
  if (!claudeInputOwnsTurnSlot(input))
843
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;
844
628
  session.activeInput = input;
845
629
  }
846
630
  const inputStream = {
@@ -942,6 +726,16 @@ function createSession(conversationId, environment, agentId, client, typingSigna
942
726
  ...(config.effort ? { effort: config.effort } : {}),
943
727
  settings: { forceLoginMethod: 'claudeai' },
944
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
+ },
945
739
  // Canonical Canon verbs, in-process (projection 2 of canon.verbs.v1).
946
740
  // Fresh instance per session: the SDK connects the instance to its own
947
741
  // in-process transport, so instances are not shared across queries.
@@ -1975,6 +1769,8 @@ function createSession(conversationId, environment, agentId, client, typingSigna
1975
1769
  // ever saw streamed.
1976
1770
  if (!isClaudeMainTurnMessage(msg.parent_tool_use_id))
1977
1771
  break;
1772
+ if (!msg.error && msg.message.model)
1773
+ session.observedModel = msg.message.model;
1978
1774
  // The assistant message is the first place the tool call's typed
1979
1775
  // INPUT is visible — content_block_start carries only the name. It
1980
1776
  // is what turns "Bash" into "Running: npm test" and TodoWrite into
@@ -2091,6 +1887,10 @@ function createSession(conversationId, environment, agentId, client, typingSigna
2091
1887
  }
2092
1888
  case 'system': {
2093
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
+ }
2094
1894
  if (subtype === 'session_state_changed') {
2095
1895
  const state = msg.state;
2096
1896
  // The echo shares one field with the host's own reservation and
@@ -2541,12 +2341,11 @@ export async function main() {
2541
2341
  if (signal.aborted)
2542
2342
  return;
2543
2343
  const activeSessionQuery = sessions.values().next().value?.query ?? null;
2544
- const metadata = await refreshClaudeRuntimeMetadata({
2344
+ await refreshClaudeRuntimeMetadata({
2545
2345
  cwd: workingDir,
2546
2346
  activeQuery: activeSessionQuery,
2547
2347
  }).catch((error) => {
2548
2348
  console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
2549
- return runtimeMetadata;
2550
2349
  });
2551
2350
  if (signal.aborted)
2552
2351
  return;
@@ -2570,27 +2369,15 @@ export async function main() {
2570
2369
  if (signal.aborted)
2571
2370
  return;
2572
2371
  const results = await Promise.allSettled(Array.from(knownConversationIds).map(async (conversationId) => {
2573
- const session = sessions.get(conversationId);
2574
- const workspaceId = session
2575
- ? resolveWorkspaceIdForBaseCwd(session.environment.baseCwd)
2576
- : runtimeDescriptor.defaultWorkspaceId;
2577
- const workspace = workspaceOptions.find((option) => option.id === workspaceId) ?? null;
2578
2372
  const descriptor = runtimeDescriptor.runtimeDescriptor;
2579
2373
  if (!descriptor)
2580
2374
  return;
2581
- const payload = buildClaudeRuntimeInfo({
2375
+ await runtimeState.writeRuntimeInfo(conversationId, {
2376
+ surfaceMode: 'host',
2377
+ // The shared publisher sanitizes configuration while retaining commands.
2582
2378
  descriptor,
2583
- metadata,
2584
- workspaceLabel: workspace?.label ?? workspaceId ?? null,
2585
- resolvedCwd: session?.cwd ?? workspace?.cwd ?? workingDir,
2586
- workspaceRootId: workspace?.workspaceRootId ?? null,
2587
- workspaceRelativePath: workspace?.workspaceRelativePath ?? null,
2588
- executionMode: session?.environment.mode ?? null,
2589
- executionBranch: session?.environment.branch ?? null,
2590
- worktreePath: session?.environment.worktreePath ?? null,
2591
- fallbackReason: resolveExecutionFallbackReason(session?.environment),
2379
+ facts: buildClaudeSessionFacts(sessions.get(conversationId)),
2592
2380
  });
2593
- await runtimeState.writeRuntimeInfo(conversationId, payload);
2594
2381
  }));
2595
2382
  const failure = results.find((result) => result.status === 'rejected');
2596
2383
  if (failure?.status === 'rejected')
@@ -2715,9 +2502,6 @@ export async function main() {
2715
2502
  client,
2716
2503
  conversationCache,
2717
2504
  });
2718
- function resolveWorkspaceIdForBaseCwd(baseCwd) {
2719
- return workspaceOptions.find((option) => option.cwd === baseCwd)?.id;
2720
- }
2721
2505
  async function refreshKnownConversationIds(force = false) {
2722
2506
  if (!force && Date.now() - lastKnownConversationRefreshAt < HEARTBEAT_MS) {
2723
2507
  return;
@@ -3155,11 +2939,20 @@ export async function main() {
3155
2939
  replyContext,
3156
2940
  message: m,
3157
2941
  });
3158
- const messageContent = await buildCanonUserContent({
2942
+ const commandInput = buildClaudeNativeCommandInput({
2943
+ text: m.text,
2944
+ senderType: m.senderType,
3159
2945
  promptText,
2946
+ commands: runtimeMetadata.commands,
2947
+ });
2948
+ const messageContent = await buildCanonUserContent({
2949
+ promptText: commandInput.promptText,
3160
2950
  materialized: [...replyMedia.materialized, ...materialized],
3161
2951
  });
3162
- const turnModes = resolveClaudeTurnModes(participantContext);
2952
+ const turnModes = {
2953
+ ...resolveClaudeTurnModes(participantContext),
2954
+ commandContext: commandInput.commandContext,
2955
+ };
3163
2956
  session.enqueueInbound({
3164
2957
  type: 'user',
3165
2958
  message: {
@@ -3424,12 +3217,19 @@ export async function main() {
3424
3217
  replyContext,
3425
3218
  message: m,
3426
3219
  });
3427
- const messageContent = await buildCanonUserContent({
3220
+ const commandInput = buildClaudeNativeCommandInput({
3221
+ text: m.text,
3222
+ senderType: m.senderType,
3428
3223
  promptText,
3224
+ commands: runtimeMetadata.commands,
3225
+ });
3226
+ const messageContent = await buildCanonUserContent({
3227
+ promptText: commandInput.promptText,
3429
3228
  materialized: [...replyMedia.materialized, ...materialized],
3430
3229
  });
3431
3230
  const turnModes = {
3432
3231
  ...resolveClaudeTurnModes(participantContext),
3232
+ commandContext: commandInput.commandContext,
3433
3233
  replyAuthority: payload.replyAuthority ?? null,
3434
3234
  };
3435
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.2",
3
+ "version": "0.34.4",
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,10 +31,10 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "0.3.228",
34
- "@canonmsg/agent-sdk": "^10.2.0",
34
+ "@canonmsg/agent-sdk": "^10.2.1",
35
35
  "@canonmsg/agent-tools": "^0.9.0",
36
36
  "@canonmsg/coding-agent-host": "^0.7.0",
37
- "@canonmsg/core": "^12.2.0",
37
+ "@canonmsg/core": "^12.3.1",
38
38
  "@canonmsg/rich-cards": "^0.10.4",
39
39
  "@modelcontextprotocol/sdk": "^1.30.0"
40
40
  },