@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.
- package/dist/host-runtime.d.ts +0 -3
- package/dist/host-runtime.js +4 -15
- package/dist/host.js +26 -26
- package/dist/server.js +16 -25
- package/dist/streaming-buffer.d.ts +16 -0
- package/dist/streaming-buffer.js +52 -0
- package/package.json +3 -3
package/dist/host-runtime.d.ts
CHANGED
|
@@ -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;
|
package/dist/host-runtime.js
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
27
|
-
"@canonmsg/core": "^0.7.
|
|
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": {
|