@canonmsg/claude-code-plugin 0.8.0 → 0.9.1

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.
@@ -28,9 +28,6 @@ export interface HostInboundParticipantContext {
28
28
  type HostInboundMessage = {
29
29
  text?: string | null;
30
30
  contentType?: CanonMessage['contentType'] | null;
31
- audioUrl?: string | null;
32
- audioDurationMs?: number | null;
33
- imageUrl?: string | null;
34
31
  attachments?: CanonMessage['attachments'];
35
32
  senderType?: CanonMessage['senderType'];
36
33
  mentions?: string[] | null;
@@ -49,7 +46,27 @@ export declare function buildCanonHostPrompt(input: {
49
46
  workSessions?: MessageCreatedPayload['workSessions'];
50
47
  buildInboundContextLines: (context: HostInboundParticipantContext) => string[];
51
48
  }): string;
52
- export declare function renderCanonHostInboundContent(message: HostInboundMessage): string;
49
+ /**
50
+ * Render the **text portion** of an inbound Canon message. Images and other
51
+ * binary attachments are referenced by short placeholders — their actual
52
+ * bytes are delivered to the host as native vision/media inputs (Anthropic
53
+ * image blocks, Codex `-i` file args). URLs are intentionally *not* inlined,
54
+ * since the harness never needs to refetch and earlier `[Image: <url>]`
55
+ * inlining caused vision models to see a string about an image instead of
56
+ * the image itself.
57
+ *
58
+ * `materialized` may be passed so non-image attachments can reference a
59
+ * local path the agent can Read. Without it we fall back to an unadorned
60
+ * placeholder; the vision path still works because image blocks carry the
61
+ * bytes directly.
62
+ */
63
+ export declare function renderCanonHostInboundContent(message: HostInboundMessage, materialized?: ReadonlyArray<{
64
+ kind: 'image' | 'audio' | 'file';
65
+ path: string;
66
+ fileName?: string;
67
+ durationMs?: number;
68
+ index: number;
69
+ }>): string;
53
70
  export declare function buildHydratedInboundContext(input: {
54
71
  agentId: string;
55
72
  conversation: CanonConversation | null;
@@ -32,38 +32,46 @@ export function buildCanonHostPrompt(input) {
32
32
  input.content,
33
33
  ].join('\n');
34
34
  }
35
- export function renderCanonHostInboundContent(message) {
36
- let content = message.text || '';
37
- const attachment = message.attachments?.[0];
38
- if (attachment?.kind === 'audio' && attachment.url) {
39
- const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
40
- content = content
41
- ? `[Voice message${duration}: ${attachment.url}]\n${content}`
42
- : `[Voice message${duration}: ${attachment.url}]`;
43
- }
44
- else if (attachment?.kind === 'image' && attachment.url) {
45
- content = content
46
- ? `[Image: ${attachment.url}]\n${content}`
47
- : `[Image: ${attachment.url}]`;
48
- }
49
- else if (attachment?.kind === 'file' && attachment.url) {
50
- const label = attachment.fileName || 'File';
51
- content = content
52
- ? `[File: ${label} ${attachment.url}]\n${content}`
53
- : `[File: ${label} ${attachment.url}]`;
35
+ /**
36
+ * Render the **text portion** of an inbound Canon message. Images and other
37
+ * binary attachments are referenced by short placeholders — their actual
38
+ * bytes are delivered to the host as native vision/media inputs (Anthropic
39
+ * image blocks, Codex `-i` file args). URLs are intentionally *not* inlined,
40
+ * since the harness never needs to refetch and earlier `[Image: <url>]`
41
+ * inlining caused vision models to see a string about an image instead of
42
+ * the image itself.
43
+ *
44
+ * `materialized` may be passed so non-image attachments can reference a
45
+ * local path the agent can Read. Without it we fall back to an unadorned
46
+ * placeholder; the vision path still works because image blocks carry the
47
+ * bytes directly.
48
+ */
49
+ export function renderCanonHostInboundContent(message, materialized) {
50
+ const body = message.text || '';
51
+ const placeholders = [];
52
+ const attachments = message.attachments ?? [];
53
+ for (let i = 0; i < attachments.length; i += 1) {
54
+ const att = attachments[i];
55
+ const mat = materialized?.find((m) => m.index === i) ?? null;
56
+ placeholders.push(describeAttachment(att, mat));
54
57
  }
55
- else if (message.contentType === 'audio' && message.audioUrl) {
56
- const duration = message.audioDurationMs ? ` (${Math.round(message.audioDurationMs / 1000)}s)` : '';
57
- content = content
58
- ? `[Voice message${duration}: ${message.audioUrl}]\n${content}`
59
- : `[Voice message${duration}: ${message.audioUrl}]`;
58
+ const rendered = [...placeholders, body].filter(Boolean).join('\n');
59
+ return rendered || '[Empty message]';
60
+ }
61
+ function describeAttachment(attachment, materialized) {
62
+ if (attachment.kind === 'image') {
63
+ return '[Image attached]';
60
64
  }
61
- else if (message.contentType === 'image' && message.imageUrl) {
62
- content = content
63
- ? `[Image: ${message.imageUrl}]\n${content}`
64
- : `[Image: ${message.imageUrl}]`;
65
+ if (attachment.kind === 'audio') {
66
+ const durationMs = materialized?.durationMs ?? attachment.durationMs;
67
+ const duration = durationMs ? ` (${Math.round(durationMs / 1000)}s)` : '';
68
+ const ref = materialized?.path ? ` ${materialized.path}` : '';
69
+ return `[Voice message${duration}${ref}]`;
65
70
  }
66
- return content || '[Empty message]';
71
+ // file
72
+ const label = materialized?.fileName ?? attachment.fileName ?? 'File';
73
+ const ref = materialized?.path ? ` ${materialized.path}` : '';
74
+ return `[File: ${label}${ref}]`;
67
75
  }
68
76
  export function buildHydratedInboundContext(input) {
69
77
  const history = buildParticipationHistorySnapshot(input.page?.messages ?? [], input.agentId);
package/dist/host.js CHANGED
@@ -24,9 +24,11 @@ setDefaultResultOrder('ipv4first');
24
24
  import { parseArgs } from 'node:util';
25
25
  import { randomUUID } from 'node:crypto';
26
26
  import { query } from '@anthropic-ai/claude-agent-sdk';
27
- import { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildPublicWorkspaceOptions, isEnabledFlag, CanonClient, CanonStream, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, rtdbRead, rtdbWrite, writeSessionState, clearSessionState, writeTurnState, releaseConversationEnvironment, } from '@canonmsg/core';
27
+ import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
28
+ import { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildPublicWorkspaceOptions, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, CanonClient, CanonStream, clearTurnState, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, rtdbRead, rtdbWrite, writeSessionState, clearSessionState, writeTurnState, releaseConversationEnvironment, } from '@canonmsg/core';
28
29
  import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
29
30
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
31
+ import { createStreamingBuffer } from './streaming-buffer.js';
30
32
  let workingDir = process.cwd();
31
33
  let workspaceOptions = [];
32
34
  let runtimeModels = [];
@@ -50,6 +52,14 @@ async function loadSessionConfig(conversationId, agentId) {
50
52
  permissionMode: config?.permissionMode,
51
53
  };
52
54
  }
55
+ const SESSION_EXECUTION_MODE_REQUIRED = 'Session execution mode required; please select a mode before starting the session.';
56
+ function requireSessionExecutionMode(config) {
57
+ const mode = config?.executionMode;
58
+ if (!mode) {
59
+ throw new ExecutionEnvironmentError(SESSION_EXECUTION_MODE_REQUIRED, SESSION_EXECUTION_MODE_REQUIRED);
60
+ }
61
+ return mode;
62
+ }
53
63
  function resolveWorkspaceCwd(config) {
54
64
  return resolveHostWorkspaceCwd({
55
65
  workspaceOptions,
@@ -105,8 +115,28 @@ function buildCanonPrompt(input) {
105
115
  ...input,
106
116
  });
107
117
  }
108
- function renderInboundContent(message) {
109
- return renderCanonHostInboundContent(message);
118
+ function renderInboundContent(message, materialized) {
119
+ return renderCanonHostInboundContent(message, materialized);
120
+ }
121
+ /**
122
+ * Build the multimodal `content` payload handed to the Claude Code SDK.
123
+ *
124
+ * Images are delivered as native Anthropic `image` blocks (base64) so the
125
+ * vision model sees pixels, not a text reference to a URL. Non-image
126
+ * attachments (audio, files) are referenced by their local path inside the
127
+ * text prompt — see `renderCanonHostInboundContent` — so the agent can
128
+ * Read them on demand.
129
+ */
130
+ async function buildCanonUserContent(input) {
131
+ const imageAttachments = input.materialized.filter(isAnthropicImageAttachment);
132
+ if (imageAttachments.length === 0) {
133
+ return input.promptText;
134
+ }
135
+ const imageBlocks = await Promise.all(imageAttachments.map((attachment) => toAnthropicImageBlock(attachment)));
136
+ return [
137
+ { type: 'text', text: input.promptText },
138
+ ...imageBlocks,
139
+ ];
110
140
  }
111
141
  // ── Session factory ─────────────────────────────────────────────────
112
142
  function createSession(conversationId, environment, agentId, client, onSessionEnd, onRuntimeDescriptorUpdate, config, resumeSessionId) {
@@ -270,31 +300,33 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
270
300
  sdkSessionId: resumeSessionId || null,
271
301
  pendingReply: null,
272
302
  };
303
+ const streamingBuffer = createStreamingBuffer({
304
+ throttleMs: STREAMING_THROTTLE_MS,
305
+ getText: () => session.streamingText,
306
+ setText: (text) => {
307
+ session.streamingText = text;
308
+ },
309
+ getTimer: () => session.streamingTimer,
310
+ setTimer: (timer) => {
311
+ session.streamingTimer = timer;
312
+ },
313
+ onWrite: (text) => {
314
+ rtdbWrite(`/streaming/${conversationId}/${agentId}`, {
315
+ text,
316
+ status: 'streaming',
317
+ updatedAt: { '.sv': 'timestamp' },
318
+ }).catch(() => { });
319
+ },
320
+ });
273
321
  // ── Per-session streaming helpers ──
274
322
  function flushStreaming() {
275
- if (!session.streamingText)
276
- return;
277
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, {
278
- text: session.streamingText,
279
- status: 'streaming',
280
- updatedAt: { '.sv': 'timestamp' },
281
- }).catch(() => { });
323
+ streamingBuffer.flush();
282
324
  }
283
325
  function onStreamDelta(text) {
284
- session.streamingText += text;
285
- if (!session.streamingTimer) {
286
- session.streamingTimer = setTimeout(() => {
287
- session.streamingTimer = null;
288
- flushStreaming();
289
- }, STREAMING_THROTTLE_MS);
290
- }
326
+ streamingBuffer.append(text);
291
327
  }
292
328
  function clearStreaming() {
293
- if (session.streamingTimer) {
294
- clearTimeout(session.streamingTimer);
295
- session.streamingTimer = null;
296
- }
297
- session.streamingText = '';
329
+ streamingBuffer.clear();
298
330
  rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
299
331
  }
300
332
  function clearIdleResetTimer() {
@@ -338,8 +370,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
338
370
  session.pendingFinalDeliveryText = finalText;
339
371
  session.state.state = 'running';
340
372
  session.turnState = 'streaming';
341
- session.streamingText = finalText;
342
- flushStreaming();
373
+ streamingBuffer.replace(finalText);
343
374
  writeState();
344
375
  writeTurn();
345
376
  client.setTyping(conversationId, true, 'typing').catch(() => { });
@@ -612,6 +643,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
612
643
  defaultPermissionMode: 'default',
613
644
  availableModels: modelList,
614
645
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
646
+ availableExecutionModes: config.availableExecutionModes,
615
647
  });
616
648
  writeSessionState(conversationId, agentId, {
617
649
  ...session.state,
@@ -674,10 +706,14 @@ async function main() {
674
706
  console.error(`[canon-host] Authenticated as ${agentId}`);
675
707
  }
676
708
  let streamConnected = false;
709
+ const hostAvailableExecutionModes = allowWorktrees
710
+ ? [...EXECUTION_ENVIRONMENT_MODES]
711
+ : ['locked'];
677
712
  let runtimeDescriptor = {
678
713
  defaultWorkspaceId: workspaceOptions[0]?.id,
679
714
  defaultPermissionMode: 'default',
680
715
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
716
+ availableExecutionModes: hostAvailableExecutionModes,
681
717
  };
682
718
  const publishRuntimeHeartbeat = async () => {
683
719
  if (!streamConnected)
@@ -695,6 +731,7 @@ async function main() {
695
731
  defaultPermissionMode: 'default',
696
732
  availableModels: runtimeModels,
697
733
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
734
+ availableExecutionModes: hostAvailableExecutionModes,
698
735
  };
699
736
  }
700
737
  catch {
@@ -702,6 +739,7 @@ async function main() {
702
739
  defaultWorkspaceId: workspaceOptions[0]?.id,
703
740
  defaultPermissionMode: 'default',
704
741
  availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
742
+ availableExecutionModes: hostAvailableExecutionModes,
705
743
  };
706
744
  }
707
745
  try {
@@ -747,6 +785,8 @@ async function main() {
747
785
  }
748
786
  if (session.streamingTimer)
749
787
  clearTimeout(session.streamingTimer);
788
+ session.streamingText = '';
789
+ rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
750
790
  if (session.idleResetTimer) {
751
791
  clearTimeout(session.idleResetTimer);
752
792
  session.idleResetTimer = null;
@@ -755,7 +795,6 @@ async function main() {
755
795
  clearTimeout(session.finalDeliveryTimer);
756
796
  session.finalDeliveryTimer = null;
757
797
  }
758
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
759
798
  clearSessionState(conversationId, agentId).catch(() => { });
760
799
  clearTurnState(conversationId, agentId).catch(() => { });
761
800
  client.setTyping(conversationId, false).catch(() => { });
@@ -794,12 +833,16 @@ async function main() {
794
833
  }
795
834
  const creation = (async () => {
796
835
  const config = await loadSessionConfig(conversationId, agentId);
836
+ const sessionExecutionMode = requireSessionExecutionMode(config);
837
+ if (sessionExecutionMode === 'worktree' && !allowWorktrees) {
838
+ throw new ExecutionEnvironmentError('This host does not allow worktree sessions (launched without --enable-worktrees).', 'This Canon host was started without worktree isolation enabled. Choose "Lock the workspace" or restart the host with --enable-worktrees.');
839
+ }
797
840
  const workspaceCwd = resolveWorkspaceCwd(config);
798
841
  const environment = prepareConversationEnvironment({
799
842
  agentId,
800
843
  conversationId,
801
844
  workspaceCwd,
802
- allowWorktrees,
845
+ allowWorktrees: sessionExecutionMode === 'worktree',
803
846
  });
804
847
  try {
805
848
  const resumeKey = buildConversationEnvironmentKey(conversationId, environment.baseCwd);
@@ -823,6 +866,7 @@ async function main() {
823
866
  model: config?.model ?? runtimeModels[0]?.value,
824
867
  permissionMode: config?.permissionMode ?? 'default',
825
868
  availableModels: runtimeModels,
869
+ availableExecutionModes: hostAvailableExecutionModes,
826
870
  }, resumeId);
827
871
  sessions.set(conversationId, session);
828
872
  return session;
@@ -863,9 +907,21 @@ async function main() {
863
907
  }
864
908
  }
865
909
  const sender = m.senderName || m.senderId;
866
- const content = renderInboundContent(m);
867
910
  const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
868
911
  void (async () => {
912
+ let materialized = [];
913
+ if (m.id) {
914
+ try {
915
+ materialized = await materializeMessageMedia({
916
+ id: m.id,
917
+ attachments: m.attachments ?? [],
918
+ }, { agentId, conversationId: convoId });
919
+ }
920
+ catch (error) {
921
+ console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to materialize media:`, error instanceof Error ? error.message : error);
922
+ }
923
+ }
924
+ const content = renderInboundContent(m, materialized);
869
925
  const hydrated = await loadHydratedInboundContext({
870
926
  conversationId: convoId,
871
927
  message: m,
@@ -899,18 +955,20 @@ async function main() {
899
955
  const turnMetadata = normalizeTurnMetadata(m.metadata);
900
956
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
901
957
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
958
+ const promptText = buildCanonPrompt({
959
+ content,
960
+ conversationId: convoId,
961
+ participantContext,
962
+ behavior,
963
+ workSession: m.workSession,
964
+ workSessions,
965
+ });
966
+ const messageContent = await buildCanonUserContent({ promptText, materialized });
902
967
  session.enqueueInbound({
903
968
  type: 'user',
904
969
  message: {
905
970
  role: 'user',
906
- content: buildCanonPrompt({
907
- content,
908
- conversationId: convoId,
909
- participantContext,
910
- behavior,
911
- workSession: m.workSession,
912
- workSessions,
913
- }),
971
+ content: messageContent,
914
972
  },
915
973
  parent_tool_use_id: null,
916
974
  }, deliveryIntent, m.id ?? null, shouldMarkAccepted);
package/dist/server.js CHANGED
@@ -32,36 +32,31 @@ function getPrimaryAttachment(message) {
32
32
  const attachment = message.attachments?.[0];
33
33
  if (attachment?.url)
34
34
  return attachment;
35
- if (message.audioUrl) {
36
- return {
37
- kind: 'audio',
38
- url: message.audioUrl,
39
- durationMs: message.audioDurationMs,
40
- };
41
- }
42
- if (message.imageUrl) {
43
- return {
44
- kind: 'image',
45
- url: message.imageUrl,
46
- };
47
- }
48
35
  return null;
49
36
  }
50
37
  function renderInboundContent(message) {
51
- let content = message.text || '';
38
+ const content = message.text || '';
52
39
  const attachment = getPrimaryAttachment(message);
40
+ // MCP channel mode does not yet materialize media into local files. Surface
41
+ // a placeholder that tells the model media *was* attached without inlining a
42
+ // Firebase Storage URL: the model cannot fetch it, and reasoning about a
43
+ // URL-as-string leads to hallucinated image descriptions. The `attachment_*`
44
+ // fields on the structured `meta` sidechannel still carry the URL for UI /
45
+ // telemetry consumers.
46
+ let placeholder = null;
53
47
  if (attachment?.kind === 'audio') {
54
48
  const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
55
- return content
56
- ? `[Voice message${duration}: ${attachment.url}]\n${content}`
57
- : `[Voice message${duration}: ${attachment.url}]`;
49
+ placeholder = `[Voice message${duration} — ask sender to resend if you need the audio]`;
58
50
  }
59
- if (attachment?.kind === 'image') {
60
- return content ? `[Image: ${attachment.url}]\n${content}` : `[Image: ${attachment.url}]`;
51
+ else if (attachment?.kind === 'image') {
52
+ placeholder = '[Image attached — not yet materialized into this session; ask sender to describe if needed]';
61
53
  }
62
- if (attachment?.kind === 'file') {
54
+ else if (attachment?.kind === 'file') {
63
55
  const label = attachment.fileName || 'File';
64
- return content ? `[File: ${label} ${attachment.url}]\n${content}` : `[File: ${label} ${attachment.url}]`;
56
+ placeholder = `[File: ${label} — not yet materialized into this session]`;
57
+ }
58
+ if (placeholder) {
59
+ return content ? `${placeholder}\n${content}` : placeholder;
65
60
  }
66
61
  return content;
67
62
  }
@@ -486,10 +481,6 @@ async function handleInboundMessage(payload) {
486
481
  }
487
482
  }
488
483
  const primaryAttachment = getPrimaryAttachment(m);
489
- if (m.imageUrl)
490
- meta.image_url = m.imageUrl;
491
- if (m.audioUrl)
492
- meta.audio_url = m.audioUrl;
493
484
  if (primaryAttachment?.url) {
494
485
  meta.attachment_kind = primaryAttachment.kind;
495
486
  meta.attachment_url = primaryAttachment.url;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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/server.js",
@@ -23,7 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
26
- "@canonmsg/core": "^0.7.0",
26
+ "@canonmsg/agent-sdk": "^0.8.1",
27
+ "@canonmsg/core": "^0.7.1",
27
28
  "@modelcontextprotocol/sdk": "^1.12.0"
28
29
  },
29
30
  "engines": {