@canonmsg/claude-code-plugin 0.9.0 → 0.9.2

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.
@@ -0,0 +1,2 @@
1
+ export declare function isDirectExecution(moduleUrl: string): boolean;
2
+ export declare function runCli(moduleUrl: string, main: () => void | Promise<void>, onError: (error: unknown) => void): void;
@@ -0,0 +1,16 @@
1
+ import { resolve } from 'node:path';
2
+ import { pathToFileURL } from 'node:url';
3
+ export function isDirectExecution(moduleUrl) {
4
+ const entry = process.argv[1];
5
+ if (!entry)
6
+ return false;
7
+ return pathToFileURL(resolve(entry)).href === moduleUrl;
8
+ }
9
+ export function runCli(moduleUrl, main, onError) {
10
+ if (!isDirectExecution(moduleUrl)) {
11
+ return;
12
+ }
13
+ Promise.resolve()
14
+ .then(() => main())
15
+ .catch(onError);
16
+ }
@@ -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.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare function main(): Promise<void>;
package/dist/host.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { setDefaultResultOrder } from 'node:dns';
3
- setDefaultResultOrder('ipv4first');
4
3
  /**
5
4
  * Canon Agent SDK host wrapper — multi-session.
6
5
  *
@@ -28,6 +27,8 @@ import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBl
28
27
  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
28
  import { buildCanonHostPrompt, buildHydratedInboundContext, createConversationMetadataLoader, loadHostSessionConfig, publishHostAgentRuntime, renderCanonHostInboundContent, resolveHostWorkspaceCwd, } from './host-runtime.js';
30
29
  import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
30
+ import { createStreamingBuffer } from './streaming-buffer.js';
31
+ import { runCli } from './cli-entry.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(() => { });
@@ -664,7 +666,8 @@ function createSession(conversationId, environment, agentId, client, onSessionEn
664
666
  return session;
665
667
  }
666
668
  // ── Main ────────────────────────────────────────────────────────────
667
- async function main() {
669
+ export async function main() {
670
+ setDefaultResultOrder('ipv4first');
668
671
  const { values: args } = parseArgs({
669
672
  options: {
670
673
  cwd: { type: 'string' },
@@ -783,6 +786,8 @@ async function main() {
783
786
  }
784
787
  if (session.streamingTimer)
785
788
  clearTimeout(session.streamingTimer);
789
+ session.streamingText = '';
790
+ rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
786
791
  if (session.idleResetTimer) {
787
792
  clearTimeout(session.idleResetTimer);
788
793
  session.idleResetTimer = null;
@@ -791,7 +796,6 @@ async function main() {
791
796
  clearTimeout(session.finalDeliveryTimer);
792
797
  session.finalDeliveryTimer = null;
793
798
  }
794
- rtdbWrite(`/streaming/${conversationId}/${agentId}`, null).catch(() => { });
795
799
  clearSessionState(conversationId, agentId).catch(() => { });
796
800
  clearTurnState(conversationId, agentId).catch(() => { });
797
801
  client.setTyping(conversationId, false).catch(() => { });
@@ -911,10 +915,7 @@ async function main() {
911
915
  try {
912
916
  materialized = await materializeMessageMedia({
913
917
  id: m.id,
914
- attachments: m.attachments,
915
- imageUrl: m.imageUrl ?? null,
916
- audioUrl: m.audioUrl ?? null,
917
- audioDurationMs: m.audioDurationMs ?? null,
918
+ attachments: m.attachments ?? [],
918
919
  }, { agentId, conversationId: convoId });
919
920
  }
920
921
  catch (error) {
@@ -1156,7 +1157,7 @@ async function main() {
1156
1157
  await new Promise(() => { });
1157
1158
  }
1158
1159
  // ── Entry ───────────────────────────────────────────────────────────
1159
- main().catch((err) => {
1160
+ runCli(import.meta.url, main, (err) => {
1160
1161
  console.error('[canon-host] Fatal error:', err);
1161
1162
  const ap = getActiveProfile();
1162
1163
  if (ap)
@@ -0,0 +1,4 @@
1
+ export { main as serverMain } from './server.js';
2
+ export { main as hostMain } from './host.js';
3
+ export { main as registerMain } from './register.js';
4
+ export { main as setupMain } from './setup.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { main as serverMain } from './server.js';
2
+ export { main as hostMain } from './host.js';
3
+ export { main as registerMain } from './register.js';
4
+ export { main as setupMain } from './setup.js';
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare function main(): Promise<void>;
package/dist/register.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { setDefaultResultOrder } from 'node:dns';
3
- setDefaultResultOrder('ipv4first');
4
3
  /**
5
4
  * CLI script for Canon agent registration.
6
5
  * Invoked by the /canon-register skill.
@@ -15,80 +14,88 @@ import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
15
14
  import { join } from 'node:path';
16
15
  import { homedir } from 'node:os';
17
16
  import { parseArgs } from 'node:util';
17
+ import { runCli } from './cli-entry.js';
18
18
  const CANON_DIR = join(homedir(), '.canon');
19
19
  const AGENTS_PATH = join(CANON_DIR, 'agents.json');
20
- const { values } = parseArgs({
21
- options: {
22
- name: { type: 'string' },
23
- description: { type: 'string' },
24
- phone: { type: 'string' },
25
- profile: { type: 'string' },
26
- 'base-url': { type: 'string' },
27
- },
28
- strict: true,
29
- });
30
- if (!values.name || !values.description || !values.phone) {
31
- console.error('Usage: node register.js --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]');
32
- process.exit(1);
33
- }
34
- // Default profile name: sanitized agent name
35
- const profileName = values.profile || values.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
36
- let existingAgentId;
37
- try {
38
- const profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
39
- existingAgentId = profiles[profileName]?.agentId;
40
- }
41
- catch {
42
- // File doesn't exist yet
43
- }
44
- console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
45
- const result = await registerAndWaitForApproval({
46
- name: values.name,
47
- description: values.description,
48
- ownerPhone: values.phone,
49
- developerInfo: 'Claude Code plugin',
50
- clientType: 'claude-code',
51
- baseUrl: values['base-url'],
52
- requestedAgentId: existingAgentId,
53
- }, {
54
- onSubmitted: (requestId) => {
55
- console.log(`Registration submitted (request ID: ${requestId}).`);
56
- console.log('Waiting for approval in Canon app...');
57
- },
58
- onPollUpdate: () => {
59
- process.stdout.write('.');
60
- },
61
- });
62
- console.log(''); // newline after dots
63
- switch (result.status) {
64
- case 'approved': {
65
- // Save to ~/.canon/agents.json
66
- mkdirSync(CANON_DIR, { recursive: true });
67
- let profiles = {};
68
- try {
69
- profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
70
- }
71
- catch {
72
- // File doesn't exist yet
20
+ export async function main() {
21
+ setDefaultResultOrder('ipv4first');
22
+ const { values } = parseArgs({
23
+ options: {
24
+ name: { type: 'string' },
25
+ description: { type: 'string' },
26
+ phone: { type: 'string' },
27
+ profile: { type: 'string' },
28
+ 'base-url': { type: 'string' },
29
+ },
30
+ strict: true,
31
+ });
32
+ if (!values.name || !values.description || !values.phone) {
33
+ console.error('Usage: node register.js --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]');
34
+ process.exit(1);
35
+ }
36
+ // Default profile name: sanitized agent name
37
+ const profileName = values.profile || values.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
38
+ let existingAgentId;
39
+ try {
40
+ const profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
41
+ existingAgentId = profiles[profileName]?.agentId;
42
+ }
43
+ catch {
44
+ // File doesn't exist yet
45
+ }
46
+ console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
47
+ const result = await registerAndWaitForApproval({
48
+ name: values.name,
49
+ description: values.description,
50
+ ownerPhone: values.phone,
51
+ developerInfo: 'Claude Code plugin',
52
+ clientType: 'claude-code',
53
+ baseUrl: values['base-url'],
54
+ requestedAgentId: existingAgentId,
55
+ }, {
56
+ onSubmitted: (requestId) => {
57
+ console.log(`Registration submitted (request ID: ${requestId}).`);
58
+ console.log('Waiting for approval in Canon app...');
59
+ },
60
+ onPollUpdate: () => {
61
+ process.stdout.write('.');
62
+ },
63
+ });
64
+ console.log(''); // newline after dots
65
+ switch (result.status) {
66
+ case 'approved': {
67
+ // Save to ~/.canon/agents.json
68
+ mkdirSync(CANON_DIR, { recursive: true });
69
+ let profiles = {};
70
+ try {
71
+ profiles = JSON.parse(readFileSync(AGENTS_PATH, 'utf-8'));
72
+ }
73
+ catch {
74
+ // File doesn't exist yet
75
+ }
76
+ profiles[profileName] = {
77
+ apiKey: result.apiKey,
78
+ agentId: result.agentId,
79
+ agentName: result.agentName,
80
+ registeredAt: new Date().toISOString(),
81
+ };
82
+ writeFileSync(AGENTS_PATH, JSON.stringify(profiles, null, 2));
83
+ console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
84
+ console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
85
+ console.log('Restart Claude Code to connect.');
86
+ break;
73
87
  }
74
- profiles[profileName] = {
75
- apiKey: result.apiKey,
76
- agentId: result.agentId,
77
- agentName: result.agentName,
78
- registeredAt: new Date().toISOString(),
79
- };
80
- writeFileSync(AGENTS_PATH, JSON.stringify(profiles, null, 2));
81
- console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
82
- console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
83
- console.log('Restart Claude Code to connect.');
84
- break;
88
+ case 'rejected':
89
+ console.log('Registration was rejected.');
90
+ process.exit(1);
91
+ break;
92
+ case 'timeout':
93
+ console.log('Registration timed out (5 minutes). Try again later.');
94
+ process.exit(1);
95
+ break;
85
96
  }
86
- case 'rejected':
87
- console.log('Registration was rejected.');
88
- process.exit(1);
89
- break;
90
- case 'timeout':
91
- console.log('Registration timed out (5 minutes). Try again later.');
92
- process.exit(1);
93
- break;
94
97
  }
98
+ runCli(import.meta.url, main, (error) => {
99
+ console.error('[canon-register] Fatal error:', error);
100
+ process.exit(1);
101
+ });
package/dist/server.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ export declare function main(): Promise<void>;
package/dist/server.js CHANGED
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import { setDefaultResultOrder } from 'node:dns';
3
- setDefaultResultOrder('ipv4first');
4
3
  /**
5
4
  * Canon channel MCP server for Claude Code.
6
5
  *
@@ -13,6 +12,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
13
12
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
14
13
  import { CanonClient, CanonStream, ApprovalManager, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
15
14
  import { ApprovalHttpServer } from './approval-server.js';
15
+ import { runCli } from './cli-entry.js';
16
16
  // ── MCP server ─────────────────────────────────────────────────────────
17
17
  const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
18
18
  capabilities: {
@@ -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;
@@ -602,7 +593,8 @@ async function startChannel() {
602
593
  }
603
594
  }
604
595
  // ── Main ───────────────────────────────────────────────────────────────
605
- async function main() {
596
+ export async function main() {
597
+ setDefaultResultOrder('ipv4first');
606
598
  const transport = new StdioServerTransport();
607
599
  await server.connect(transport);
608
600
  // Start the Canon channel after MCP connection is established
@@ -626,7 +618,7 @@ async function main() {
626
618
  process.on('SIGINT', shutdown);
627
619
  process.on('SIGTERM', shutdown);
628
620
  }
629
- main().catch((err) => {
621
+ runCli(import.meta.url, main, (err) => {
630
622
  console.error('[canon] Fatal error:', err);
631
623
  process.exit(1);
632
624
  });
package/dist/setup.d.ts CHANGED
@@ -6,4 +6,4 @@
6
6
  * 1. Install the /canon-register and /canon-configure skills
7
7
  * 2. Show the user how to add the MCP server to their .mcp.json
8
8
  */
9
- export {};
9
+ export declare function main(): void;
package/dist/setup.js CHANGED
@@ -9,6 +9,7 @@
9
9
  import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
10
10
  import { join, dirname } from 'node:path';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import { runCli } from './cli-entry.js';
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
14
  const home = process.env.HOME || process.env.USERPROFILE || '';
14
15
  const skillsDir = join(home, '.claude', 'skills');
@@ -55,12 +56,18 @@ Then start Claude Code with:
55
56
  `);
56
57
  }
57
58
  // ── Main ───────────────────────────────────────────────────────────────
58
- console.log('Canon Claude Code Plugin Setup');
59
- console.log('==============================\n');
60
- console.log('Installing skills...');
61
- installSkills();
62
- console.log('\nRegistering your agent...');
63
- console.log(' Run /canon-register in Claude Code to register a new agent');
64
- console.log(' Run /canon-configure if you already have an API key\n');
65
- console.log('MCP server configuration:');
66
- showMcpInstructions();
59
+ export function main() {
60
+ console.log('Canon Claude Code Plugin Setup');
61
+ console.log('==============================\n');
62
+ console.log('Installing skills...');
63
+ installSkills();
64
+ console.log('\nRegistering your agent...');
65
+ console.log(' Run /canon-register in Claude Code to register a new agent');
66
+ console.log(' Run /canon-configure if you already have an API key\n');
67
+ console.log('MCP server configuration:');
68
+ showMcpInstructions();
69
+ }
70
+ runCli(import.meta.url, main, (error) => {
71
+ console.error('[canon-setup] Fatal error:', error);
72
+ process.exit(1);
73
+ });
@@ -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,9 +1,16 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
5
5
  "type": "module",
6
- "main": "dist/server.js",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
7
14
  "bin": {
8
15
  "canon-channel-server": "dist/server.js",
9
16
  "canon-claude": "dist/host.js",
@@ -23,8 +30,8 @@
23
30
  },
24
31
  "dependencies": {
25
32
  "@anthropic-ai/claude-agent-sdk": "^0.2.101",
26
- "@canonmsg/agent-sdk": "^0.8.0",
27
- "@canonmsg/core": "^0.7.0",
33
+ "@canonmsg/agent-sdk": "^0.8.1",
34
+ "@canonmsg/core": "^0.7.1",
28
35
  "@modelcontextprotocol/sdk": "^1.12.0"
29
36
  },
30
37
  "engines": {