@canonmsg/claude-code-plugin 0.12.7 → 0.13.0
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.js +79 -46
- package/dist/server.js +105 -134
- package/dist/session-state.d.ts +15 -1
- package/dist/session-state.js +11 -0
- package/package.json +3 -3
package/dist/host.js
CHANGED
|
@@ -26,11 +26,11 @@ import { execFileSync } from 'node:child_process';
|
|
|
26
26
|
import { existsSync } from 'node:fs';
|
|
27
27
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
28
28
|
import { isAnthropicImageAttachment, materializeMessageMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
29
|
-
import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
29
|
+
import { CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonHostPrompt, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, buildPlanApprovalRequest, buildQuestionRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, createConversationMetadataLoader, createRuntimeStatePublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
30
30
|
import { buildInboundContextLines, decideAutoReply, } from './inbound-policy.js';
|
|
31
31
|
import { createStreamingBuffer } from './streaming-buffer.js';
|
|
32
32
|
import { runCli } from './cli-entry.js';
|
|
33
|
-
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
33
|
+
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
34
34
|
const HELP = `canon-claude — run a local Claude Code agent host for Canon
|
|
35
35
|
|
|
36
36
|
USAGE
|
|
@@ -154,26 +154,9 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
154
154
|
availability: ['always'],
|
|
155
155
|
dispatch: { kind: 'open_details', target: 'effort' },
|
|
156
156
|
},
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
description: 'Interrupt the current Claude turn.',
|
|
161
|
-
aliases: ['stop'],
|
|
162
|
-
category: 'turn',
|
|
163
|
-
placements: ['composer_slash', 'command_palette'],
|
|
164
|
-
availability: ['busy'],
|
|
165
|
-
dispatch: { kind: 'signal', signal: 'interrupt' },
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: 'stop-and-clear-queue',
|
|
169
|
-
label: 'Stop & clear queue',
|
|
170
|
-
description: 'Interrupt the current Claude turn and drop queued Canon messages.',
|
|
171
|
-
aliases: ['stop-clear', 'clear-queue'],
|
|
172
|
-
category: 'turn',
|
|
173
|
-
placements: ['composer_slash', 'command_palette', 'session_strip'],
|
|
174
|
-
availability: ['busy_with_queue'],
|
|
175
|
-
dispatch: { kind: 'signal', signal: 'stop_and_drop' },
|
|
176
|
-
},
|
|
157
|
+
RUNTIME_STOP_ACTION,
|
|
158
|
+
RUNTIME_STOP_AND_DROP_ACTION,
|
|
159
|
+
RUNTIME_NEW_SESSION_ACTION,
|
|
177
160
|
],
|
|
178
161
|
});
|
|
179
162
|
}
|
|
@@ -654,6 +637,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
654
637
|
const questions = input.questions ?? [];
|
|
655
638
|
const questionCard = buildQuestionRequest(questionId, questions);
|
|
656
639
|
await client.sendMessage(conversationId, questionCard.text, {
|
|
640
|
+
...(session.activeSelfContextId
|
|
641
|
+
? { selfContextId: session.activeSelfContextId }
|
|
642
|
+
: {}),
|
|
657
643
|
metadata: {
|
|
658
644
|
...questionCard.metadata,
|
|
659
645
|
turnSemantics: 'control',
|
|
@@ -699,6 +685,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
699
685
|
summary: 'Claude has prepared a plan and is asking permission to continue.',
|
|
700
686
|
});
|
|
701
687
|
await client.sendMessage(conversationId, planApproval.text, {
|
|
688
|
+
...(session.activeSelfContextId
|
|
689
|
+
? { selfContextId: session.activeSelfContextId }
|
|
690
|
+
: {}),
|
|
702
691
|
metadata: {
|
|
703
692
|
...planApproval.metadata,
|
|
704
693
|
turnSemantics: 'control',
|
|
@@ -760,6 +749,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
760
749
|
turnState: 'idle',
|
|
761
750
|
currentTurnId: null,
|
|
762
751
|
currentTurnOpenedAt: null,
|
|
752
|
+
activeSelfContextId: null,
|
|
763
753
|
lastAcceptedIntent: null,
|
|
764
754
|
lastActivity: Date.now(),
|
|
765
755
|
typingKeepaliveTimer: null,
|
|
@@ -815,15 +805,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
815
805
|
if (session.closed)
|
|
816
806
|
return;
|
|
817
807
|
clearFinalDeliveryTimer();
|
|
818
|
-
session
|
|
819
|
-
session.currentTurnOpenedAt = null;
|
|
820
|
-
session.lastAcceptedIntent = null;
|
|
821
|
-
session.pendingFinalText = null;
|
|
822
|
-
session.pendingFinalDelivery = null;
|
|
823
|
-
session.activeInput = null;
|
|
824
|
-
session.toolInProgress = false;
|
|
825
|
-
session.turnState = 'idle';
|
|
808
|
+
resetClaudeCompletedTurnState(session);
|
|
826
809
|
stopVisibleWorkSignal();
|
|
810
|
+
writeState();
|
|
827
811
|
writeTurn();
|
|
828
812
|
drainPendingInput();
|
|
829
813
|
}
|
|
@@ -875,6 +859,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
875
859
|
try {
|
|
876
860
|
await client.sendMessage(conversationId, finalText, {
|
|
877
861
|
messageId: finalMessageId,
|
|
862
|
+
...(session.activeSelfContextId
|
|
863
|
+
? { selfContextId: session.activeSelfContextId }
|
|
864
|
+
: {}),
|
|
878
865
|
metadata: {
|
|
879
866
|
turnId: session.currentTurnId,
|
|
880
867
|
turnKey: deliverTurn.turnKey,
|
|
@@ -1560,19 +1547,20 @@ export async function main() {
|
|
|
1560
1547
|
isOwner: input.isOwner,
|
|
1561
1548
|
});
|
|
1562
1549
|
}
|
|
1563
|
-
function closeSession(conversationId) {
|
|
1550
|
+
function closeSession(conversationId, options = {}) {
|
|
1564
1551
|
const session = sessions.get(conversationId);
|
|
1565
1552
|
if (!session)
|
|
1566
1553
|
return;
|
|
1554
|
+
const saveResume = options.saveResume !== false;
|
|
1567
1555
|
session.closed = true;
|
|
1568
1556
|
if (session.typingKeepaliveTimer) {
|
|
1569
1557
|
clearInterval(session.typingKeepaliveTimer);
|
|
1570
1558
|
session.typingKeepaliveTimer = null;
|
|
1571
1559
|
}
|
|
1572
1560
|
releaseConversationEnvironment(session.environment);
|
|
1573
|
-
|
|
1574
|
-
if (session.sdkSessionId) {
|
|
1575
|
-
savedSessionIds.set(
|
|
1561
|
+
const resumeKey = buildConversationEnvironmentKey(conversationId, session.environment.baseCwd);
|
|
1562
|
+
if (saveResume && session.sdkSessionId) {
|
|
1563
|
+
savedSessionIds.set(resumeKey, session.sdkSessionId);
|
|
1576
1564
|
saveRuntimeSessionState(runtimeId, {
|
|
1577
1565
|
conversationId,
|
|
1578
1566
|
baseCwd: session.environment.baseCwd,
|
|
@@ -1580,6 +1568,27 @@ export async function main() {
|
|
|
1580
1568
|
claudeSessionId: session.sdkSessionId,
|
|
1581
1569
|
});
|
|
1582
1570
|
}
|
|
1571
|
+
else {
|
|
1572
|
+
const existingState = loadRuntimeSessionState(runtimeId, {
|
|
1573
|
+
conversationId,
|
|
1574
|
+
baseCwd: session.environment.baseCwd,
|
|
1575
|
+
executionMode: session.environment.mode,
|
|
1576
|
+
});
|
|
1577
|
+
savedSessionIds.delete(resumeKey);
|
|
1578
|
+
clearRuntimeSessionState(runtimeId, {
|
|
1579
|
+
conversationId,
|
|
1580
|
+
baseCwd: session.environment.baseCwd,
|
|
1581
|
+
executionMode: session.environment.mode,
|
|
1582
|
+
});
|
|
1583
|
+
if (existingState?.lastInboundMessageId) {
|
|
1584
|
+
saveRuntimeSessionState(runtimeId, {
|
|
1585
|
+
conversationId,
|
|
1586
|
+
baseCwd: session.environment.baseCwd,
|
|
1587
|
+
executionMode: session.environment.mode,
|
|
1588
|
+
lastInboundMessageId: existingState.lastInboundMessageId,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1583
1592
|
if (session.streamingTimer)
|
|
1584
1593
|
clearTimeout(session.streamingTimer);
|
|
1585
1594
|
session.streamingText = '';
|
|
@@ -1600,7 +1609,22 @@ export async function main() {
|
|
|
1600
1609
|
}
|
|
1601
1610
|
catch { }
|
|
1602
1611
|
sessions.delete(conversationId);
|
|
1603
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session suspended (sessionId saved for resume)`);
|
|
1612
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Session ${options.reason ?? (saveResume ? 'suspended (sessionId saved for resume)' : 'cleared')}`);
|
|
1613
|
+
}
|
|
1614
|
+
async function resetRuntimeSession(session) {
|
|
1615
|
+
const conversationId = session.conversationId;
|
|
1616
|
+
const droppedInputs = session.pendingInputs.splice(0);
|
|
1617
|
+
await markSessionPendingInputsRejected(conversationId, droppedInputs);
|
|
1618
|
+
if (session.activeInput) {
|
|
1619
|
+
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
1620
|
+
}
|
|
1621
|
+
session.pendingFinalText = null;
|
|
1622
|
+
session.pendingFinalDelivery = null;
|
|
1623
|
+
session.turnState = 'interrupted';
|
|
1624
|
+
if (session.state.state === 'running') {
|
|
1625
|
+
await session.query.interrupt().catch(() => { });
|
|
1626
|
+
}
|
|
1627
|
+
closeSession(conversationId, { saveResume: false, reason: 'reset for new Canon session' });
|
|
1604
1628
|
}
|
|
1605
1629
|
async function markSessionPendingInputsRejected(conversationId, inputs) {
|
|
1606
1630
|
await Promise.all(inputs.map((input) => {
|
|
@@ -1757,7 +1781,7 @@ export async function main() {
|
|
|
1757
1781
|
isOwner,
|
|
1758
1782
|
});
|
|
1759
1783
|
const behavior = input.hydratedPage?.behavior ?? hydrated.behavior;
|
|
1760
|
-
const
|
|
1784
|
+
const selfContexts = input.hydratedPage?.selfContexts ?? hydrated.selfContexts;
|
|
1761
1785
|
const participantContext = hydrated.participantContext;
|
|
1762
1786
|
const autoReply = decideAutoReply(participantContext, behavior);
|
|
1763
1787
|
if (!autoReply.allow) {
|
|
@@ -1773,6 +1797,7 @@ export async function main() {
|
|
|
1773
1797
|
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
1774
1798
|
console.error(`[canon-host] [${input.conversationId.slice(0, 8)}] Failed to create recovered session: ${message}`);
|
|
1775
1799
|
await client.sendMessage(input.conversationId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
|
|
1800
|
+
...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
|
|
1776
1801
|
metadata: {
|
|
1777
1802
|
turnSemantics: 'turn_complete',
|
|
1778
1803
|
turnComplete: true,
|
|
@@ -1782,6 +1807,7 @@ export async function main() {
|
|
|
1782
1807
|
return;
|
|
1783
1808
|
}
|
|
1784
1809
|
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
1810
|
+
session.activeSelfContextId = selfContexts[0]?.id ?? null;
|
|
1785
1811
|
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
1786
1812
|
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
1787
1813
|
const promptText = buildCanonPrompt({
|
|
@@ -1789,8 +1815,7 @@ export async function main() {
|
|
|
1789
1815
|
conversationId: input.conversationId,
|
|
1790
1816
|
participantContext,
|
|
1791
1817
|
behavior,
|
|
1792
|
-
|
|
1793
|
-
workSessions,
|
|
1818
|
+
selfContexts,
|
|
1794
1819
|
session,
|
|
1795
1820
|
});
|
|
1796
1821
|
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
@@ -1902,11 +1927,11 @@ export async function main() {
|
|
|
1902
1927
|
isOwner,
|
|
1903
1928
|
});
|
|
1904
1929
|
const behavior = payload.behavior ?? hydrated.behavior;
|
|
1905
|
-
const
|
|
1906
|
-
? hydrated.
|
|
1907
|
-
: Array.isArray(payload.
|
|
1908
|
-
? payload.
|
|
1909
|
-
: hydrated.
|
|
1930
|
+
const selfContexts = hydrated.hydratedFromPage
|
|
1931
|
+
? hydrated.selfContexts
|
|
1932
|
+
: Array.isArray(payload.selfContexts)
|
|
1933
|
+
? payload.selfContexts
|
|
1934
|
+
: hydrated.selfContexts;
|
|
1910
1935
|
const participantContext = hydrated.participantContext;
|
|
1911
1936
|
const autoReply = decideAutoReply(participantContext, behavior);
|
|
1912
1937
|
if (!autoReply.allow) {
|
|
@@ -1923,6 +1948,7 @@ export async function main() {
|
|
|
1923
1948
|
const userMessage = error instanceof ExecutionEnvironmentError ? error.userMessage : message;
|
|
1924
1949
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to create session: ${message}`);
|
|
1925
1950
|
await client.sendMessage(convoId, `I couldn't start a coding session for this workspace: ${userMessage}`, {
|
|
1951
|
+
...(selfContexts[0]?.id ? { selfContextId: selfContexts[0].id } : {}),
|
|
1926
1952
|
metadata: {
|
|
1927
1953
|
turnSemantics: 'turn_complete',
|
|
1928
1954
|
turnComplete: true,
|
|
@@ -1932,6 +1958,7 @@ export async function main() {
|
|
|
1932
1958
|
return;
|
|
1933
1959
|
}
|
|
1934
1960
|
const turnMetadata = normalizeTurnMetadata(m.metadata);
|
|
1961
|
+
session.activeSelfContextId = selfContexts[0]?.id ?? null;
|
|
1935
1962
|
const deliveryIntent = turnMetadata?.deliveryIntent ?? 'queue';
|
|
1936
1963
|
const shouldMarkAccepted = turnMetadata?.inboundDisposition === 'queued';
|
|
1937
1964
|
const promptText = buildCanonPrompt({
|
|
@@ -1939,8 +1966,7 @@ export async function main() {
|
|
|
1939
1966
|
conversationId: convoId,
|
|
1940
1967
|
participantContext,
|
|
1941
1968
|
behavior,
|
|
1942
|
-
|
|
1943
|
-
workSessions,
|
|
1969
|
+
selfContexts,
|
|
1944
1970
|
session,
|
|
1945
1971
|
});
|
|
1946
1972
|
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
@@ -2042,10 +2068,17 @@ export async function main() {
|
|
|
2042
2068
|
if (sig && typeof sig === 'object') {
|
|
2043
2069
|
const s = sig;
|
|
2044
2070
|
const ts = s.updatedAt ?? 0;
|
|
2045
|
-
if ((s.type === 'interrupt' || s.type === 'stop_and_drop'
|
|
2071
|
+
if ((s.type === 'interrupt' || s.type === 'stop_and_drop' || s.type === 'new_session')
|
|
2072
|
+
&& ts > (lastSeenSignal.get(convoId) ?? 0)) {
|
|
2046
2073
|
lastSeenSignal.set(convoId, ts);
|
|
2047
2074
|
const session = sessions.get(convoId);
|
|
2048
2075
|
if (session && !session.closed) {
|
|
2076
|
+
if (s.type === 'new_session') {
|
|
2077
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] new_session signal`);
|
|
2078
|
+
await resetRuntimeSession(session);
|
|
2079
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
|
2080
|
+
continue;
|
|
2081
|
+
}
|
|
2049
2082
|
const isActive = session.state.state === 'running';
|
|
2050
2083
|
if (!isActive && (s.type !== 'stop_and_drop' || session.pendingInputs.length === 0)) {
|
|
2051
2084
|
await rtdbWrite(`/control/${convoId}/${agentId}/signal`, null).catch(() => { });
|
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
|
-
|
|
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: '
|
|
140
|
-
description: 'Send a message into another Canon conversation
|
|
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
|
|
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
|
-
|
|
142
|
+
target_user_id: {
|
|
153
143
|
type: 'string',
|
|
154
|
-
description: '
|
|
144
|
+
description: 'The destination Canon user/contact to reach out to',
|
|
155
145
|
},
|
|
156
|
-
|
|
146
|
+
text: {
|
|
157
147
|
type: 'string',
|
|
158
|
-
description: '
|
|
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
|
-
|
|
150
|
+
self_context: {
|
|
169
151
|
type: 'object',
|
|
170
|
-
description: '
|
|
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
|
|
156
|
+
description: 'Optional Canon send options for the contextual outbound message',
|
|
175
157
|
},
|
|
176
158
|
},
|
|
177
|
-
required: ['source_conversation_id', '
|
|
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 = [{
|
|
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
|
-
...
|
|
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 '
|
|
303
|
+
case 'send_contextual_message': {
|
|
296
304
|
const sourceConversationId = args.source_conversation_id;
|
|
297
|
-
const targetConversationId = args.target_conversation_id
|
|
298
|
-
|
|
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
|
|
307
|
-
|
|
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
|
|
313
|
-
const result = await client.
|
|
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
|
-
|
|
318
|
-
...(
|
|
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
|
-
...
|
|
326
|
+
...contextualMessageOptions,
|
|
337
327
|
metadata: {
|
|
338
|
-
...(
|
|
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:
|
|
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 (
|
|
458
|
-
meta.
|
|
459
|
-
|
|
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
|
-
|
|
505
|
-
.map((
|
|
506
|
-
.
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
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/dist/session-state.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import type { DeliveryIntent, ModelOption } from '@canonmsg/core';
|
|
2
|
+
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
|
|
3
3
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
4
4
|
export interface ClaudeInputEnvelope {
|
|
5
5
|
kind: ClaudeInputKind;
|
|
@@ -19,6 +19,19 @@ export interface ClaudePendingFinalDelivery {
|
|
|
19
19
|
text: string;
|
|
20
20
|
messageId: string;
|
|
21
21
|
}
|
|
22
|
+
export interface ClaudeCompletedTurnState {
|
|
23
|
+
state: {
|
|
24
|
+
state?: 'idle' | 'running' | 'requires_action';
|
|
25
|
+
};
|
|
26
|
+
pendingFinalText: string | null;
|
|
27
|
+
pendingFinalDelivery: ClaudePendingFinalDelivery | null;
|
|
28
|
+
activeInput: ClaudeInputEnvelope | null;
|
|
29
|
+
toolInProgress: boolean;
|
|
30
|
+
turnState: TurnLifecycleState;
|
|
31
|
+
currentTurnId: string | null;
|
|
32
|
+
currentTurnOpenedAt: number | null;
|
|
33
|
+
lastAcceptedIntent: DeliveryIntent | null;
|
|
34
|
+
}
|
|
22
35
|
export declare function createClaudeInputEnvelope(input: {
|
|
23
36
|
kind: ClaudeInputKind;
|
|
24
37
|
msg: SDKUserMessage;
|
|
@@ -36,6 +49,7 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
36
49
|
finalizedTurnKeys: ReadonlySet<string>;
|
|
37
50
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
38
51
|
}): boolean;
|
|
52
|
+
export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
|
|
39
53
|
export declare function mergeClaudeModelOptions(input: {
|
|
40
54
|
previous: ReadonlyArray<ModelOption>;
|
|
41
55
|
next: ReadonlyArray<ModelOption>;
|
package/dist/session-state.js
CHANGED
|
@@ -29,6 +29,17 @@ export function shouldDeliverClaudeFinal(input) {
|
|
|
29
29
|
return false;
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
|
+
export function resetClaudeCompletedTurnState(session) {
|
|
33
|
+
session.state.state = 'idle';
|
|
34
|
+
session.currentTurnId = null;
|
|
35
|
+
session.currentTurnOpenedAt = null;
|
|
36
|
+
session.lastAcceptedIntent = null;
|
|
37
|
+
session.pendingFinalText = null;
|
|
38
|
+
session.pendingFinalDelivery = null;
|
|
39
|
+
session.activeInput = null;
|
|
40
|
+
session.toolInProgress = false;
|
|
41
|
+
session.turnState = 'idle';
|
|
42
|
+
}
|
|
32
43
|
export function mergeClaudeModelOptions(input) {
|
|
33
44
|
const byValue = new Map();
|
|
34
45
|
for (const option of input.previous) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
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,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
34
|
-
"@canonmsg/agent-sdk": "^1.
|
|
35
|
-
"@canonmsg/core": "^0.
|
|
34
|
+
"@canonmsg/agent-sdk": "^1.2.0",
|
|
35
|
+
"@canonmsg/core": "^0.16.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|