@canonmsg/claude-code-plugin 0.14.18 → 0.15.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.js +165 -57
- package/package.json +4 -4
package/dist/host.js
CHANGED
|
@@ -26,8 +26,8 @@ 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, materializeReplyContextMedia, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
29
|
-
import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION,
|
|
30
|
-
import {
|
|
29
|
+
import { CLAUDE_EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPlanApprovalRequest, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, initRTDBAuth, rtdbRead, rtdbWrite, sendMessageWithRetry, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCanonTurnBriefPrompt, resolveHostWorkspaceCwd, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
30
|
+
import { decideAutoReply, } from './inbound-policy.js';
|
|
31
31
|
import { runCli } from './cli-entry.js';
|
|
32
32
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
33
33
|
import { buildClaudeFinalMessageId, createClaudeInputEnvelope, formatClaudeControlError, mergeClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, } from './session-state.js';
|
|
@@ -87,21 +87,6 @@ let runtimeMetadata = {
|
|
|
87
87
|
let runtimeMetadataRefresh = null;
|
|
88
88
|
function buildClaudeRuntimeDescriptor(input) {
|
|
89
89
|
const commands = [
|
|
90
|
-
{
|
|
91
|
-
id: 'plan',
|
|
92
|
-
label: 'Plan first',
|
|
93
|
-
description: 'Switch Claude into Plan mode. Text after /plan is kept as a ready prompt while the mode applies.',
|
|
94
|
-
aliases: ['plan'],
|
|
95
|
-
category: 'plan',
|
|
96
|
-
placements: ['composer_slash', 'command_palette'],
|
|
97
|
-
availability: ['always'],
|
|
98
|
-
trailingTextBehavior: 'send_as_prompt',
|
|
99
|
-
dispatch: {
|
|
100
|
-
kind: 'control',
|
|
101
|
-
controlId: 'permissionMode',
|
|
102
|
-
value: 'plan',
|
|
103
|
-
},
|
|
104
|
-
},
|
|
105
90
|
{
|
|
106
91
|
id: 'runtime-status',
|
|
107
92
|
label: 'Runtime status',
|
|
@@ -190,6 +175,25 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
190
175
|
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
191
176
|
defaultPermissionMode: 'default',
|
|
192
177
|
streamingTextMode: 'delta',
|
|
178
|
+
turnModes: [
|
|
179
|
+
{
|
|
180
|
+
id: 'normal',
|
|
181
|
+
label: 'Normal',
|
|
182
|
+
description: 'Let Claude answer or act normally.',
|
|
183
|
+
scope: 'session',
|
|
184
|
+
default: true,
|
|
185
|
+
activation: { kind: 'control', controlId: 'permissionMode', value: 'default' },
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'plan',
|
|
189
|
+
label: 'Plan',
|
|
190
|
+
description: 'Switch Claude Code into Plan mode.',
|
|
191
|
+
scope: 'session',
|
|
192
|
+
ownerOnly: true,
|
|
193
|
+
aliases: ['plan'],
|
|
194
|
+
activation: { kind: 'control', controlId: 'permissionMode', value: 'plan' },
|
|
195
|
+
},
|
|
196
|
+
],
|
|
193
197
|
admissionActions: HOST_ADMISSION_ACTIONS_DISABLED,
|
|
194
198
|
presentation: input.presentation,
|
|
195
199
|
commands,
|
|
@@ -380,6 +384,71 @@ async function refreshClaudeRuntimeMetadata(input) {
|
|
|
380
384
|
});
|
|
381
385
|
return runtimeMetadataRefresh;
|
|
382
386
|
}
|
|
387
|
+
function sleep(ms) {
|
|
388
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
389
|
+
}
|
|
390
|
+
function mapClaudeQuestions(value) {
|
|
391
|
+
if (!Array.isArray(value))
|
|
392
|
+
return undefined;
|
|
393
|
+
const questions = value.slice(0, 12).flatMap((entry) => {
|
|
394
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
395
|
+
return [];
|
|
396
|
+
const record = entry;
|
|
397
|
+
const id = typeof record.id === 'string' && record.id.trim()
|
|
398
|
+
? record.id.trim().slice(0, 120)
|
|
399
|
+
: typeof record.question === 'string' && record.question.trim()
|
|
400
|
+
? record.question.trim().slice(0, 80).replace(/[^A-Za-z0-9_.:-]+/g, '-')
|
|
401
|
+
: null;
|
|
402
|
+
const question = typeof record.question === 'string' && record.question.trim()
|
|
403
|
+
? record.question.trim().slice(0, 1000)
|
|
404
|
+
: null;
|
|
405
|
+
if (!id || !/^[A-Za-z0-9_.:-]{1,120}$/.test(id) || !question)
|
|
406
|
+
return [];
|
|
407
|
+
const header = typeof record.header === 'string' && record.header.trim()
|
|
408
|
+
? record.header.trim().slice(0, 120)
|
|
409
|
+
: undefined;
|
|
410
|
+
const rawOptions = Array.isArray(record.options) ? record.options : [];
|
|
411
|
+
const choices = rawOptions.slice(0, 12).flatMap((option) => {
|
|
412
|
+
if (!option || typeof option !== 'object' || Array.isArray(option))
|
|
413
|
+
return [];
|
|
414
|
+
const optionRecord = option;
|
|
415
|
+
const label = typeof optionRecord.label === 'string' && optionRecord.label.trim()
|
|
416
|
+
? optionRecord.label.trim().slice(0, 120)
|
|
417
|
+
: typeof optionRecord.value === 'string' && optionRecord.value.trim()
|
|
418
|
+
? optionRecord.value.trim().slice(0, 120)
|
|
419
|
+
: null;
|
|
420
|
+
if (!label)
|
|
421
|
+
return [];
|
|
422
|
+
const optionValue = typeof optionRecord.value === 'string' && optionRecord.value.trim()
|
|
423
|
+
? optionRecord.value.trim().slice(0, 120)
|
|
424
|
+
: label;
|
|
425
|
+
return [{
|
|
426
|
+
label,
|
|
427
|
+
value: optionValue,
|
|
428
|
+
...(typeof optionRecord.description === 'string' && optionRecord.description.trim()
|
|
429
|
+
? { description: optionRecord.description.trim().slice(0, 240) }
|
|
430
|
+
: {}),
|
|
431
|
+
}];
|
|
432
|
+
});
|
|
433
|
+
return [{
|
|
434
|
+
id,
|
|
435
|
+
question,
|
|
436
|
+
...(header ? { header } : {}),
|
|
437
|
+
...(choices.length ? { choices } : {}),
|
|
438
|
+
...(record.isOther === true || record.allowOther === true ? { allowOther: true } : {}),
|
|
439
|
+
...(record.isSecret === true ? { isSecret: true } : {}),
|
|
440
|
+
...(record.multiSelect === true ? { multiSelect: true } : {}),
|
|
441
|
+
}];
|
|
442
|
+
});
|
|
443
|
+
return questions.length ? questions : undefined;
|
|
444
|
+
}
|
|
445
|
+
function flattenRuntimeInputAnswers(answers) {
|
|
446
|
+
const flattened = {};
|
|
447
|
+
for (const [questionId, answer] of Object.entries(answers ?? {})) {
|
|
448
|
+
flattened[questionId] = answer.answers.join(', ');
|
|
449
|
+
}
|
|
450
|
+
return flattened;
|
|
451
|
+
}
|
|
383
452
|
function mapClaudeMcpStatus(status) {
|
|
384
453
|
switch (status) {
|
|
385
454
|
case 'connected':
|
|
@@ -535,35 +604,24 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
535
604
|
supportsRequiresAction: true,
|
|
536
605
|
supportsNonFinalPermanentMessages: false,
|
|
537
606
|
};
|
|
538
|
-
const HOST_PROMPT_MODE = resolveCanonHostPromptMode(process.env.CANON_HOST_PROMPT_MODE);
|
|
539
607
|
function controlPollDelayMs(hasActiveWork) {
|
|
540
608
|
const base = hasActiveWork ? CONTROL_POLL_MS : IDLE_CONTROL_POLL_MS;
|
|
541
609
|
return base + Math.floor(Math.random() * CONTROL_POLL_JITTER_MS);
|
|
542
610
|
}
|
|
543
611
|
function buildCanonPrompt(input) {
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
provenance: input.provenance,
|
|
555
|
-
replyContext: input.replyContext,
|
|
556
|
-
sessionContextLines,
|
|
557
|
-
message: input.message,
|
|
558
|
-
hydratedFromPage: input.hydratedFromPage,
|
|
559
|
-
}));
|
|
560
|
-
}
|
|
561
|
-
return buildCanonHostPrompt({
|
|
562
|
-
hostLabel: 'Claude Code',
|
|
563
|
-
buildInboundContextLines,
|
|
612
|
+
const sessionContextLines = input.session ? buildClaudeSessionContextLines(input.session) : undefined;
|
|
613
|
+
return renderCanonTurnBriefPrompt(buildCanonTurnContextV2({
|
|
614
|
+
content: input.content,
|
|
615
|
+
conversationId: input.conversationId,
|
|
616
|
+
participantContext: input.participantContext,
|
|
617
|
+
behavior: input.behavior,
|
|
618
|
+
selfContexts: input.selfContexts,
|
|
619
|
+
activeSelfContextId: input.activeSelfContextId,
|
|
620
|
+
provenance: input.provenance,
|
|
621
|
+
replyContext: input.replyContext,
|
|
564
622
|
sessionContextLines,
|
|
565
|
-
|
|
566
|
-
});
|
|
623
|
+
message: input.message,
|
|
624
|
+
}));
|
|
567
625
|
}
|
|
568
626
|
function optionLabel(value, options) {
|
|
569
627
|
if (!value)
|
|
@@ -703,6 +761,30 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
703
761
|
};
|
|
704
762
|
});
|
|
705
763
|
}
|
|
764
|
+
async function waitForRuntimeInputResponse(input) {
|
|
765
|
+
session.lastActivity = Date.now();
|
|
766
|
+
while (Date.now() < input.expiresAt) {
|
|
767
|
+
const response = await client.consumeRuntimeInputResponse({
|
|
768
|
+
conversationId,
|
|
769
|
+
inputId: input.inputId,
|
|
770
|
+
}).catch(() => null);
|
|
771
|
+
if (response?.status === 'submitted') {
|
|
772
|
+
return { status: 'submitted', answers: response.answers, value: response.value };
|
|
773
|
+
}
|
|
774
|
+
if (response?.status === 'cancelled' || response?.status === 'timeout') {
|
|
775
|
+
return { status: response.status };
|
|
776
|
+
}
|
|
777
|
+
await sleep(1_000);
|
|
778
|
+
}
|
|
779
|
+
const response = await client.consumeRuntimeInputResponse({
|
|
780
|
+
conversationId,
|
|
781
|
+
inputId: input.inputId,
|
|
782
|
+
}).catch(() => null);
|
|
783
|
+
if (response?.status === 'submitted') {
|
|
784
|
+
return { status: 'submitted', answers: response.answers, value: response.value };
|
|
785
|
+
}
|
|
786
|
+
return { status: 'timeout' };
|
|
787
|
+
}
|
|
706
788
|
const claudeCliPath = resolveClaudeCliPath();
|
|
707
789
|
const queryOptions = {
|
|
708
790
|
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
|
@@ -726,24 +808,43 @@ function createSession(conversationId, environment, agentId, client, typingSigna
|
|
|
726
808
|
if (toolName === 'AskUserQuestion') {
|
|
727
809
|
try {
|
|
728
810
|
const questionId = randomUUID();
|
|
729
|
-
const questions = input.questions
|
|
730
|
-
const
|
|
731
|
-
await
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
811
|
+
const questions = mapClaudeQuestions(input.questions);
|
|
812
|
+
const expiresAt = Date.now() + REPLY_TIMEOUT_MS;
|
|
813
|
+
await client.createRuntimeInputRequest({
|
|
814
|
+
conversationId,
|
|
815
|
+
inputId: questionId,
|
|
816
|
+
kind: 'clarify',
|
|
817
|
+
expiresAt,
|
|
818
|
+
title: 'Claude needs input',
|
|
819
|
+
prompt: questions?.length
|
|
820
|
+
? 'Claude needs your input to continue.'
|
|
821
|
+
: 'Claude needs input to continue.',
|
|
822
|
+
...(questions ? { questions } : {}),
|
|
823
|
+
sensitive: Boolean(questions?.some((question) => question.isSecret)),
|
|
824
|
+
...(cardResponseUserId ? { responseUserId: cardResponseUserId } : {}),
|
|
825
|
+
native: {
|
|
826
|
+
runtime: 'claude-code',
|
|
827
|
+
method: 'AskUserQuestion',
|
|
828
|
+
requestId: questionId,
|
|
829
|
+
turnId: session.currentTurnId ?? undefined,
|
|
740
830
|
},
|
|
831
|
+
turnId: session.currentTurnId ?? undefined,
|
|
741
832
|
});
|
|
742
|
-
console.error(`[canon-host] [${conversationId.slice(0, 8)}]
|
|
743
|
-
const reply = await
|
|
833
|
+
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Runtime input question sent (${questionId.slice(0, 8)})`);
|
|
834
|
+
const reply = await waitForRuntimeInputResponse({ inputId: questionId, expiresAt });
|
|
835
|
+
if (reply.status !== 'submitted') {
|
|
836
|
+
return { behavior: 'deny', message: 'No reply received (timed out)' };
|
|
837
|
+
}
|
|
744
838
|
return {
|
|
745
839
|
behavior: 'allow',
|
|
746
|
-
updatedInput: {
|
|
840
|
+
updatedInput: {
|
|
841
|
+
...input,
|
|
842
|
+
answers: reply.answers
|
|
843
|
+
? flattenRuntimeInputAnswers(reply.answers)
|
|
844
|
+
: reply.value
|
|
845
|
+
? { answer: reply.value }
|
|
846
|
+
: {},
|
|
847
|
+
},
|
|
747
848
|
};
|
|
748
849
|
}
|
|
749
850
|
catch {
|
|
@@ -2088,7 +2189,6 @@ export async function main() {
|
|
|
2088
2189
|
provenance: hydrated.provenance,
|
|
2089
2190
|
replyContext,
|
|
2090
2191
|
message: m,
|
|
2091
|
-
hydratedFromPage: hydrated.hydratedFromPage,
|
|
2092
2192
|
session,
|
|
2093
2193
|
});
|
|
2094
2194
|
const messageContent = await buildCanonUserContent({
|
|
@@ -2196,6 +2296,10 @@ export async function main() {
|
|
|
2196
2296
|
}
|
|
2197
2297
|
const sender = m.senderName || m.senderId;
|
|
2198
2298
|
const isOwner = m.isOwner ?? (ownerId != null && m.senderId === ownerId);
|
|
2299
|
+
if (payload.turnDispatch && payload.turnDispatch.kind !== 'run_turn') {
|
|
2300
|
+
console.error(`[canon-host] [${convoId.slice(0, 8)}] Ignoring server-dispatched observe-only message: ${payload.turnDispatch.reason}`);
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2199
2303
|
void (async () => {
|
|
2200
2304
|
let materialized = [];
|
|
2201
2305
|
if (m.id) {
|
|
@@ -2230,7 +2334,12 @@ export async function main() {
|
|
|
2230
2334
|
});
|
|
2231
2335
|
const replyContext = replyMedia.replyContext;
|
|
2232
2336
|
const participantContext = hydrated.participantContext;
|
|
2233
|
-
const autoReply =
|
|
2337
|
+
const autoReply = payload.turnDispatch?.kind === 'run_turn'
|
|
2338
|
+
? {
|
|
2339
|
+
allow: true,
|
|
2340
|
+
reason: payload.turnDispatch.reason || 'server dispatch allowed this turn',
|
|
2341
|
+
}
|
|
2342
|
+
: decideAutoReply(participantContext, behavior);
|
|
2234
2343
|
if (!autoReply.allow) {
|
|
2235
2344
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Suppressed auto-reply: ${autoReply.reason}`);
|
|
2236
2345
|
return;
|
|
@@ -2270,7 +2379,6 @@ export async function main() {
|
|
|
2270
2379
|
provenance: hydrated.provenance,
|
|
2271
2380
|
replyContext,
|
|
2272
2381
|
message: m,
|
|
2273
|
-
hydratedFromPage: hydrated.hydratedFromPage,
|
|
2274
2382
|
session,
|
|
2275
2383
|
});
|
|
2276
2384
|
const messageContent = await buildCanonUserContent({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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/index.js",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.141",
|
|
34
|
-
"@canonmsg/agent-sdk": "^
|
|
35
|
-
"@canonmsg/core": "^1.
|
|
34
|
+
"@canonmsg/agent-sdk": "^3.1.0",
|
|
35
|
+
"@canonmsg/core": "^2.1.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"devDependencies": {
|
|
59
59
|
"@types/node": "^22.0.0",
|
|
60
60
|
"typescript": "~5.7.0",
|
|
61
|
-
"vitest": "^
|
|
61
|
+
"vitest": "^4.1.8"
|
|
62
62
|
},
|
|
63
63
|
"license": "MIT"
|
|
64
64
|
}
|