@canonmsg/claude-code-plugin 0.12.7 → 0.12.8

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.
Files changed (3) hide show
  1. package/dist/host.js +22 -10
  2. package/dist/server.js +105 -134
  3. package/package.json +2 -2
package/dist/host.js CHANGED
@@ -654,6 +654,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
654
654
  const questions = input.questions ?? [];
655
655
  const questionCard = buildQuestionRequest(questionId, questions);
656
656
  await client.sendMessage(conversationId, questionCard.text, {
657
+ ...(session.activeSelfContextId
658
+ ? { selfContextId: session.activeSelfContextId }
659
+ : {}),
657
660
  metadata: {
658
661
  ...questionCard.metadata,
659
662
  turnSemantics: 'control',
@@ -699,6 +702,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
699
702
  summary: 'Claude has prepared a plan and is asking permission to continue.',
700
703
  });
701
704
  await client.sendMessage(conversationId, planApproval.text, {
705
+ ...(session.activeSelfContextId
706
+ ? { selfContextId: session.activeSelfContextId }
707
+ : {}),
702
708
  metadata: {
703
709
  ...planApproval.metadata,
704
710
  turnSemantics: 'control',
@@ -760,6 +766,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
760
766
  turnState: 'idle',
761
767
  currentTurnId: null,
762
768
  currentTurnOpenedAt: null,
769
+ activeSelfContextId: null,
763
770
  lastAcceptedIntent: null,
764
771
  lastActivity: Date.now(),
765
772
  typingKeepaliveTimer: null,
@@ -875,6 +882,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
875
882
  try {
876
883
  await client.sendMessage(conversationId, finalText, {
877
884
  messageId: finalMessageId,
885
+ ...(session.activeSelfContextId
886
+ ? { selfContextId: session.activeSelfContextId }
887
+ : {}),
878
888
  metadata: {
879
889
  turnId: session.currentTurnId,
880
890
  turnKey: deliverTurn.turnKey,
@@ -1757,7 +1767,7 @@ export async function main() {
1757
1767
  isOwner,
1758
1768
  });
1759
1769
  const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
1760
- const workSessions = input.hydratedPage?.workSessions ?? hydrated.workSessions;
1770
+ const selfContexts = input.hydratedPage?.selfContexts ?? hydrated.selfContexts;
1761
1771
  const participantContext = hydrated.participantContext;
1762
1772
  const autoReply = decideAutoReply(participantContext, behavior);
1763
1773
  if (!autoReply.allow) {
@@ -1773,6 +1783,7 @@ export async function main() {
1773
1783
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1774
1784
  console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
1775
1785
  await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1786
+ ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
1776
1787
  metadata: {
1777
1788
  turnSemantics: 'turn_complete',
1778
1789
  turnComplete: true,
@@ -1782,6 +1793,7 @@ export async function main() {
1782
1793
  return;
1783
1794
  }
1784
1795
  const turnMetadata = normalizeTurnMetadata(m.metadata);
1796
+ session.activeSelfContextId = selfContexts[0]?.id ?? null;
1785
1797
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1786
1798
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1787
1799
  const promptText = buildCanonPrompt({
@@ -1789,8 +1801,7 @@ export async function main() {
1789
1801
  conversationId: input.conversationId,
1790
1802
  participantContext,
1791
1803
  behavior,
1792
- workSession: m.workSession,
1793
- workSessions,
1804
+ selfContexts,
1794
1805
  session,
1795
1806
  });
1796
1807
  const messageContent = await buildCanonUserContent({ promptText, materialized });
@@ -1902,11 +1913,11 @@ export async function main() {
1902
1913
  isOwner,
1903
1914
  });
1904
1915
  const behavior = payload.behavior ?? hydrated.behavior;
1905
- const workSessions = hydrated.hydratedFromPage
1906
- ? hydrated.workSessions
1907
- : Array.isArray(payload.workSessions)
1908
- ? payload.workSessions
1909
- : hydrated.workSessions;
1916
+ const selfContexts = hydrated.hydratedFromPage
1917
+ ? hydrated.selfContexts
1918
+ : Array.isArray(payload.selfContexts)
1919
+ ? payload.selfContexts
1920
+ : hydrated.selfContexts;
1910
1921
  const participantContext = hydrated.participantContext;
1911
1922
  const autoReply = decideAutoReply(participantContext, behavior);
1912
1923
  if (!autoReply.allow) {
@@ -1923,6 +1934,7 @@ export async function main() {
1923
1934
  const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
1924
1935
  console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
1925
1936
  await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
1937
+ ...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
1926
1938
  metadata: {
1927
1939
  turnSemantics: 'turn_complete',
1928
1940
  turnComplete: true,
@@ -1932,6 +1944,7 @@ export async function main() {
1932
1944
  return;
1933
1945
  }
1934
1946
  const turnMetadata = normalizeTurnMetadata(m.metadata);
1947
+ session.activeSelfContextId = selfContexts[0]?.id ?? null;
1935
1948
  const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
1936
1949
  const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
1937
1950
  const promptText = buildCanonPrompt({
@@ -1939,8 +1952,7 @@ export async function main() {
1939
1952
  conversationId: convoId,
1940
1953
  participantContext,
1941
1954
  behavior,
1942
- workSession: m.workSession,
1943
- workSessions,
1955
+ selfContexts,
1944
1956
  session,
1945
1957
  });
1946
1958
  const messageContent = await buildCanonUserContent({ promptText, materialized });
package/dist/server.js CHANGED
@@ -10,7 +10,8 @@ import { setDefaultResultOrder } from 'node:dns';
10
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
11
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
13
- import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
13
+ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, renderCanonHostInboundContent, } from '@canonmsg/core';
14
+ import { materializeMessageMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
14
15
  import { ApprovalHttpServer } from './approval-server.js';
15
16
  import { runCli } from './cli-entry.js';
16
17
  const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
@@ -49,31 +50,8 @@ function getPrimaryAttachment(message) {
49
50
  return attachment;
50
51
  return null;
51
52
  }
52
- function renderInboundContent(message) {
53
- const content = message.text || '';
54
- const attachment = getPrimaryAttachment(message);
55
- // MCP channel mode does not yet materialize media into local files. Surface
56
- // a placeholder that tells the model media *was* attached without inlining a
57
- // Firebase Storage URL: the model cannot fetch it, and reasoning about a
58
- // URL-as-string leads to hallucinated image descriptions. The `attachment_*`
59
- // fields on the structured `meta` sidechannel still carry the URL for UI /
60
- // telemetry consumers.
61
- let placeholder = null;
62
- if (attachment?.kind === 'audio') {
63
- const duration = attachment.durationMs ? ` (${Math.round(attachment.durationMs / 1000)}s)` : '';
64
- placeholder = `[Voice message${duration} — ask sender to resend if you need the audio]`;
65
- }
66
- else if (attachment?.kind === 'image') {
67
- placeholder = '[Image attached — not yet materialized into this session; ask sender to describe if needed]';
68
- }
69
- else if (attachment?.kind === 'file') {
70
- const label = attachment.fileName || 'File';
71
- placeholder = `[File: ${label} — not yet materialized into this session]`;
72
- }
73
- if (placeholder) {
74
- return content ? `${placeholder}\n${content}` : placeholder;
75
- }
76
- return content;
53
+ function renderInboundContent(message, materialized) {
54
+ return renderCanonHostInboundContent(message, materialized);
77
55
  }
78
56
  // ── Tool definitions ───────────────────────────────────────────────────
79
57
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -127,54 +105,58 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
127
105
  type: 'string',
128
106
  description: 'File URL (when content_type is file)',
129
107
  },
108
+ media_path: {
109
+ type: 'string',
110
+ description: 'Local image/audio/file path to upload and send',
111
+ },
130
112
  file_name: {
131
113
  type: 'string',
132
114
  description: 'Optional display filename for file attachments',
133
115
  },
116
+ mime_type: {
117
+ type: 'string',
118
+ description: 'Optional MIME type for media_path uploads',
119
+ },
120
+ duration_ms: {
121
+ type: 'number',
122
+ description: 'Optional audio duration in milliseconds for media_path uploads',
123
+ },
134
124
  },
135
125
  required: ['conversation_id'],
136
126
  },
137
127
  },
138
128
  {
139
- name: 'send_linked_message',
140
- description: 'Send a message into another Canon conversation under an existing or lazily created shared work session',
129
+ name: 'send_contextual_message',
130
+ description: 'Send a message into another Canon conversation or contact with private cross-session self-context',
141
131
  inputSchema: {
142
132
  type: 'object',
143
133
  properties: {
144
134
  source_conversation_id: {
145
135
  type: 'string',
146
- description: 'The originating conversation for the shared task',
136
+ description: 'The Canon conversation where this cross-session intent originated',
147
137
  },
148
138
  target_conversation_id: {
149
139
  type: 'string',
150
- description: 'The destination conversation to send into',
140
+ description: 'The destination Canon conversation to send into',
151
141
  },
152
- text: {
142
+ target_user_id: {
153
143
  type: 'string',
154
- description: 'Message text to send',
144
+ description: 'The destination Canon user/contact to reach out to',
155
145
  },
156
- work_session_id: {
146
+ text: {
157
147
  type: 'string',
158
- description: 'Reuse an existing Canon work session when known',
159
- },
160
- create_work_session: {
161
- type: 'object',
162
- description: 'Optional initial work-session fields when creating a new shared task',
163
- },
164
- source_context: {
165
- type: 'object',
166
- description: 'Optional scoped context patch for the source conversation',
148
+ description: 'Message text to send',
167
149
  },
168
- target_context: {
150
+ self_context: {
169
151
  type: 'object',
170
- description: 'Optional scoped context patch for the target conversation',
152
+ description: 'Required { type: "cross_session", context: "why I am saying this here" }',
171
153
  },
172
154
  message_options: {
173
155
  type: 'object',
174
- description: 'Optional Canon send options for the linked outbound message',
156
+ description: 'Optional Canon send options for the contextual outbound message',
175
157
  },
176
158
  },
177
- required: ['source_conversation_id', 'target_conversation_id', 'text'],
159
+ required: ['source_conversation_id', 'text', 'self_context'],
178
160
  },
179
161
  },
180
162
  {
@@ -256,14 +238,39 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
256
238
  const imageUrl = typeof args.image_url === 'string' ? args.image_url : null;
257
239
  const audioUrl = typeof args.audio_url === 'string' ? args.audio_url : null;
258
240
  const fileUrl = typeof args.file_url === 'string' ? args.file_url : null;
241
+ const mediaPath = typeof args.media_path === 'string' ? args.media_path : null;
259
242
  const fileName = typeof args.file_name === 'string' ? args.file_name : null;
243
+ const mimeType = typeof args.mime_type === 'string' ? args.mime_type : null;
244
+ const durationMs = typeof args.duration_ms === 'number' ? args.duration_ms : null;
245
+ const metadata = TURN_COMPLETE_METADATA;
246
+ if (mediaPath) {
247
+ const result = await sendMediaFileMessage(client, conversationId, mediaPath, text, {
248
+ ...(fileName ? { fileName } : {}),
249
+ ...(mimeType ? { mimeType } : {}),
250
+ ...(durationMs != null ? { durationMs } : {}),
251
+ metadata,
252
+ });
253
+ return {
254
+ content: [
255
+ {
256
+ type: 'text',
257
+ text: `Message sent (${result.messageId})`,
258
+ },
259
+ ],
260
+ };
261
+ }
260
262
  if (contentType === 'image' && imageUrl) {
261
263
  opts.contentType = 'image';
262
264
  opts.attachments = [{ kind: 'image', url: imageUrl }];
263
265
  }
264
266
  else if (contentType === 'audio' && audioUrl) {
265
267
  opts.contentType = 'audio';
266
- opts.attachments = [{ kind: 'audio', url: audioUrl }];
268
+ opts.attachments = [{
269
+ kind: 'audio',
270
+ url: audioUrl,
271
+ ...(durationMs != null ? { durationMs } : {}),
272
+ ...(mimeType ? { mimeType } : {}),
273
+ }];
267
274
  }
268
275
  else if (contentType === 'file' || fileUrl) {
269
276
  opts.contentType = 'file';
@@ -272,6 +279,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
272
279
  kind: 'file',
273
280
  url: fileUrl,
274
281
  ...(fileName ? { fileName } : {}),
282
+ ...(mimeType ? { mimeType } : {}),
275
283
  }];
276
284
  }
277
285
  }
@@ -280,7 +288,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
280
288
  }
281
289
  opts.metadata = {
282
290
  ...(opts.metadata ?? {}),
283
- ...TURN_COMPLETE_METADATA,
291
+ ...metadata,
284
292
  };
285
293
  const result = await client.sendMessage(conversationId, text, opts);
286
294
  return {
@@ -292,50 +300,32 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
292
300
  ],
293
301
  };
294
302
  }
295
- case 'send_linked_message': {
303
+ case 'send_contextual_message': {
296
304
  const sourceConversationId = args.source_conversation_id;
297
- const targetConversationId = args.target_conversation_id;
298
- const text = args.text;
299
- const workSessionId = typeof args.work_session_id === 'string' ? args.work_session_id : undefined;
300
- const createWorkSession = typeof args.create_work_session === 'object' && args.create_work_session != null
301
- ? args.create_work_session
302
- : undefined;
303
- const sourceContext = typeof args.source_context === 'object' && args.source_context != null
304
- ? args.source_context
305
+ const targetConversationId = typeof args.target_conversation_id === 'string'
306
+ ? args.target_conversation_id
305
307
  : undefined;
306
- const targetContext = typeof args.target_context === 'object' && args.target_context != null
307
- ? args.target_context
308
+ const targetUserId = typeof args.target_user_id === 'string' ? args.target_user_id : undefined;
309
+ const text = args.text;
310
+ const selfContext = typeof args.self_context === 'object' && args.self_context != null
311
+ ? args.self_context
308
312
  : undefined;
309
313
  const messageOptions = typeof args.message_options === 'object' && args.message_options != null
310
314
  ? args.message_options
311
315
  : undefined;
312
- const linkedMessageOptions = messageOptions;
313
- const result = await client.sendLinkedMessage({
316
+ const contextualMessageOptions = messageOptions;
317
+ const result = await client.sendContextualMessage({
314
318
  sourceConversationId,
315
- targetConversationId,
319
+ ...(targetConversationId ? { targetConversationId } : {}),
320
+ ...(targetUserId ? { targetUserId } : {}),
316
321
  text,
317
- ...(workSessionId ? { workSessionId } : {}),
318
- ...(createWorkSession
319
- ? {
320
- createWorkSession: createWorkSession,
321
- }
322
- : {}),
323
- ...(sourceContext
324
- ? {
325
- sourceContext: sourceContext,
326
- }
327
- : {}),
328
- ...(targetContext
329
- ? {
330
- targetContext: targetContext,
331
- }
332
- : {}),
333
- ...(linkedMessageOptions
322
+ selfContext: selfContext,
323
+ ...(contextualMessageOptions
334
324
  ? {
335
325
  messageOptions: {
336
- ...linkedMessageOptions,
326
+ ...contextualMessageOptions,
337
327
  metadata: {
338
- ...(linkedMessageOptions.metadata ?? {}),
328
+ ...(contextualMessageOptions.metadata ?? {}),
339
329
  ...TURN_COMPLETE_METADATA,
340
330
  },
341
331
  },
@@ -346,7 +336,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
346
336
  content: [
347
337
  {
348
338
  type: 'text',
349
- text: `Linked message sent (${result.messageId}) via work session ${result.workSessionId}${result.workSessionCreated ? ' [created]' : ''}`,
339
+ text: result.status === 'messaged'
340
+ ? `Contextual message sent (${result.messageId}) via self-context ${result.selfContextId}`
341
+ : `Contextual message not sent: ${result.status}`,
350
342
  },
351
343
  ],
352
344
  };
@@ -454,58 +446,30 @@ async function handleInboundMessage(payload) {
454
446
  meta.behavior_instructions = payload.behavior.instructions.join('\n\n');
455
447
  }
456
448
  }
457
- if (m.workSession) {
458
- meta.work_session_id = m.workSession.id;
459
- if (m.workSession.title)
460
- meta.work_session_title = m.workSession.title;
461
- if (m.workSession.objective)
462
- meta.work_session_objective = m.workSession.objective;
463
- if (m.workSession.status)
464
- meta.work_session_status = m.workSession.status;
465
- if (m.workSession.currentConversationRole) {
466
- meta.work_session_role = m.workSession.currentConversationRole;
467
- }
468
- if (m.workSession.representation) {
469
- meta.work_session_representation = m.workSession.representation;
470
- }
471
- if (m.workSession.disclosure) {
472
- meta.work_session_disclosure = m.workSession.disclosure;
473
- }
474
- if (m.workSession.summary)
475
- meta.work_session_summary = m.workSession.summary;
476
- if (m.workSession.sourceConversationLabel) {
477
- meta.work_session_source_conversation = m.workSession.sourceConversationLabel;
478
- }
479
- else if (m.workSession.sourceConversationId) {
480
- meta.work_session_source_conversation = m.workSession.sourceConversationId;
481
- }
482
- if (m.workSession.sourceParticipantLabel) {
483
- meta.work_session_source_participant = m.workSession.sourceParticipantLabel;
484
- }
485
- else if (m.workSession.sourceParticipantId) {
486
- meta.work_session_source_participant = m.workSession.sourceParticipantId;
487
- }
488
- if (Array.isArray(m.workSession.visibleFacts) && m.workSession.visibleFacts.length > 0) {
489
- meta.work_session_visible_facts = m.workSession.visibleFacts.join('\n\n');
490
- }
491
- if (Array.isArray(m.workSession.pendingQuestions)
492
- && m.workSession.pendingQuestions.length > 0) {
493
- meta.work_session_pending_questions = m.workSession.pendingQuestions.join('\n\n');
494
- }
495
- if (Array.isArray(m.workSession.disclosureNotes)
496
- && m.workSession.disclosureNotes.length > 0) {
497
- meta.work_session_disclosure_notes = m.workSession.disclosureNotes.join('\n\n');
498
- }
499
- }
500
- if (Array.isArray(payload.workSessions) && payload.workSessions.length > 0) {
501
- meta.active_work_session_ids = payload.workSessions
502
- .map((workSession) => workSession.id)
449
+ if (Array.isArray(payload.selfContexts) && payload.selfContexts.length > 0) {
450
+ meta.self_context_ids = payload.selfContexts
451
+ .map((selfContext) => selfContext.id)
503
452
  .join('\n');
504
- const titled = payload.workSessions
505
- .map((workSession) => workSession.title || workSession.id)
506
- .filter((value) => value.length > 0);
507
- if (titled.length > 0) {
508
- meta.active_work_session_titles = titled.join('\n');
453
+ meta.self_context_types = payload.selfContexts
454
+ .map((selfContext) => selfContext.type)
455
+ .join('\n');
456
+ meta.self_context = payload.selfContexts
457
+ .map((selfContext) => selfContext.context)
458
+ .join('\n\n');
459
+ }
460
+ let materialized = [];
461
+ if (m.id && m.attachments?.length) {
462
+ try {
463
+ materialized = await materializeMessageMedia({
464
+ id: m.id,
465
+ attachments: m.attachments ?? [],
466
+ }, {
467
+ agentId: agentContext?.agentId ?? 'claude-code',
468
+ conversationId: payload.conversationId,
469
+ });
470
+ }
471
+ catch (error) {
472
+ console.error(`[canon] Failed to materialize inbound media: ${error instanceof Error ? error.message : error}`);
509
473
  }
510
474
  }
511
475
  const primaryAttachment = getPrimaryAttachment(m);
@@ -515,7 +479,14 @@ async function handleInboundMessage(payload) {
515
479
  }
516
480
  if (primaryAttachment?.fileName)
517
481
  meta.attachment_file_name = primaryAttachment.fileName;
518
- const renderedContent = renderInboundContent(m);
482
+ if (primaryAttachment?.mimeType)
483
+ meta.attachment_mime_type = primaryAttachment.mimeType;
484
+ if (primaryAttachment?.durationMs != null)
485
+ meta.attachment_duration_ms = String(primaryAttachment.durationMs);
486
+ const primaryMaterialized = materialized.find((attachment) => attachment.index === 0);
487
+ if (primaryMaterialized?.path)
488
+ meta.attachment_path = primaryMaterialized.path;
489
+ const renderedContent = renderInboundContent(m, materialized);
519
490
  console.error(`[canon] Inbound message from ${meta.sender_name}: "${renderedContent}"`);
520
491
  try {
521
492
  await server.notification({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.12.7",
3
+ "version": "0.12.8",
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,7 +31,7 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
34
- "@canonmsg/agent-sdk": "^1.1.2",
34
+ "@canonmsg/agent-sdk": "^1.1.4",
35
35
  "@canonmsg/core": "^0.15.4",
36
36
  "@modelcontextprotocol/sdk": "^1.12.0"
37
37
  },