@canonmsg/claude-code-plugin 0.9.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;
@@ -50,21 +50,10 @@ export function renderCanonHostInboundContent(message, materialized) {
50
50
  const body = message.text || '';
51
51
  const placeholders = [];
52
52
  const attachments = message.attachments ?? [];
53
- if (attachments.length > 0) {
54
- for (let i = 0; i < attachments.length; i += 1) {
55
- const att = attachments[i];
56
- const mat = materialized?.find((m) => m.index === i) ?? null;
57
- placeholders.push(describeAttachment(att, mat));
58
- }
59
- }
60
- else if (message.contentType === 'audio' && message.audioUrl) {
61
- const duration = message.audioDurationMs
62
- ? ` (${Math.round(message.audioDurationMs / 1000)}s)`
63
- : '';
64
- placeholders.push(`[Voice message${duration}]`);
65
- }
66
- else if (message.contentType === 'image' && message.imageUrl) {
67
- placeholders.push('[Image attached]');
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));
68
57
  }
69
58
  const rendered = [...placeholders, body].filter(Boolean).join('\n');
70
59
  return rendered || '[Empty message]';
package/dist/host.js CHANGED
@@ -28,6 +28,7 @@ import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBl
28
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';
29
29
  import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
30
30
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
31
+ import { createStreamingBuffer } from './streaming-buffer.js';
31
32
  let workingDir = process.cwd();
32
33
  let workspaceOptions = [];
33
34
  let runtimeModels = [];
@@ -299,31 +300,33 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
299
300
  sdkSessionId: resumeSessionId || null,
300
301
  pendingReply: null,
301
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
+ });
302
321
  // ── Per-session streaming helpers ──
303
322
  function flushStreaming() {
304
- if (!session.streamingText)
305
- return;
306
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, {
307
- text: session.streamingText,
308
- status: 'streaming',
309
- updatedAt: { '.sv': 'timestamp' },
310
- }).catch(() => { });
323
+ streamingBuffer.flush();
311
324
  }
312
325
  function onStreamDelta(text) {
313
- session.streamingText += text;
314
- if (!session.streamingTimer) {
315
- session.streamingTimer = setTimeout(() => {
316
- session.streamingTimer = null;
317
- flushStreaming();
318
- }, STREAMING_THROTTLE_MS);
319
- }
326
+ streamingBuffer.append(text);
320
327
  }
321
328
  function clearStreaming() {
322
- if (session.streamingTimer) {
323
- clearTimeout(session.streamingTimer);
324
- session.streamingTimer = null;
325
- }
326
- session.streamingText = '';
329
+ streamingBuffer.clear();
327
330
  rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
328
331
  }
329
332
  function clearIdleResetTimer() {
@@ -367,8 +370,7 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
367
370
  session.pendingFinalDeliveryText = finalText;
368
371
  session.state.state = 'running';
369
372
  session.turnState = 'streaming';
370
- session.streamingText = finalText;
371
- flushStreaming();
373
+ streamingBuffer.replace(finalText);
372
374
  writeState();
373
375
  writeTurn();
374
376
  client.setTyping(conversationId, true, 'typing').catch(() => { });
@@ -783,6 +785,8 @@ async function main() {
783
785
  }
784
786
  if (session.streamingTimer)
785
787
  clearTimeout(session.streamingTimer);
788
+ session.streamingText = '';
789
+ rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
786
790
  if (session.idleResetTimer) {
787
791
  clearTimeout(session.idleResetTimer);
788
792
  session.idleResetTimer = null;
@@ -791,7 +795,6 @@ async function main() {
791
795
  clearTimeout(session.finalDeliveryTimer);
792
796
  session.finalDeliveryTimer = null;
793
797
  }
794
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
795
798
  clearSessionState(conversationId, agentId).catch(() => { });
796
799
  clearTurnState(conversationId, agentId).catch(() => { });
797
800
  client.setTyping(conversationId, false).catch(() => { });
@@ -911,10 +914,7 @@ async function main() {
911
914
  try {
912
915
  materialized = await materializeMessageMedia({
913
916
  id: m.id,
914
- attachments: m.attachments,
915
- imageUrl: m.imageUrl ?? null,
916
- audioUrl: m.audioUrl ?? null,
917
- audioDurationMs: m.audioDurationMs ?? null,
917
+ attachments: m.attachments ?? [],
918
918
  }, { agentId, conversationId: convoId });
919
919
  }
920
920
  catch (error) {
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.9.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,8 +23,8 @@
23
23
  },
24
24
  "dependencies": {
25
25
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
26
- "@canonmsg/agent-sdk": "^0.8.0",
27
- "@canonmsg/core": "^0.7.0",
26
+ "@canonmsg/agent-sdk": "^0.8.1",
27
+ "@canonmsg/core": "^0.7.1",
28
28
  "@modelcontextprotocol/sdk": "^1.12.0"
29
29
  },
30
30
  "engines": {