@canonmsg/claude-code-plugin 0.12.4 → 0.12.6
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/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -26
- package/dist/host.js +270 -95
- package/dist/register.js +24 -0
- package/dist/server.js +14 -0
- package/dist/session-state.d.ts +44 -0
- package/dist/session-state.js +67 -0
- package/dist/setup.js +14 -0
- package/package.json +2 -2
package/dist/cli-entry.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
|
2
|
+
export type { CliMetadata, RunCliMetadataOptions, } from '@canonmsg/core';
|
package/dist/cli-entry.js
CHANGED
|
@@ -1,26 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import { resolve } from 'node:path';
|
|
3
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
-
export function isDirectExecution(moduleUrl) {
|
|
5
|
-
const entry = process.argv[1];
|
|
6
|
-
if (!entry)
|
|
7
|
-
return false;
|
|
8
|
-
const abs = resolve(entry);
|
|
9
|
-
let real = abs;
|
|
10
|
-
try {
|
|
11
|
-
real = realpathSync(abs);
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// argv[1] may not exist on disk in rare harnesses; fall back to abs
|
|
15
|
-
}
|
|
16
|
-
return (pathToFileURL(real).href === moduleUrl ||
|
|
17
|
-
pathToFileURL(abs).href === moduleUrl);
|
|
18
|
-
}
|
|
19
|
-
export function runCli(moduleUrl, main, onError) {
|
|
20
|
-
if (!isDirectExecution(moduleUrl)) {
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
Promise.resolve()
|
|
24
|
-
.then(() => main())
|
|
25
|
-
.catch(onError);
|
|
26
|
-
}
|
|
1
|
+
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
package/dist/host.js
CHANGED
|
@@ -30,6 +30,28 @@ import { CLAUDE_PERMISSION_MODE_OPTIONS, buildCanonHostPrompt, buildConfiguredWo
|
|
|
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';
|
|
34
|
+
const HELP = `canon-claude — run a local Claude Code agent host for Canon
|
|
35
|
+
|
|
36
|
+
USAGE
|
|
37
|
+
canon-claude [flags]
|
|
38
|
+
|
|
39
|
+
COMMON FLAGS
|
|
40
|
+
--cwd <path> Project directory to run Claude Code from
|
|
41
|
+
--workspace <path> Additional project to expose in Canon
|
|
42
|
+
--workspace-root <path> Discover projects under an approved root
|
|
43
|
+
--help, -h Show this help
|
|
44
|
+
--version, -V Show package version
|
|
45
|
+
|
|
46
|
+
AUTH
|
|
47
|
+
CANON_AGENT=<profile> canon-claude --cwd /path/to/project
|
|
48
|
+
CANON_API_KEY=agk_live_... canon-claude --cwd /path/to/project
|
|
49
|
+
|
|
50
|
+
EXAMPLES
|
|
51
|
+
canon-claude --cwd ~/dev/canon
|
|
52
|
+
canon-claude --cwd ~/dev/canon --workspace-root ~/dev
|
|
53
|
+
|
|
54
|
+
Keep this terminal open while you want Canon to reach the agent.`;
|
|
33
55
|
let workingDir = process.cwd();
|
|
34
56
|
let workspaceOptions = [];
|
|
35
57
|
let workspaceRoots = [];
|
|
@@ -344,8 +366,9 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
344
366
|
? {
|
|
345
367
|
id: 'account',
|
|
346
368
|
label: 'Account',
|
|
347
|
-
value:
|
|
369
|
+
value: 'Connected',
|
|
348
370
|
tone: 'success',
|
|
371
|
+
tier: 'diagnostic',
|
|
349
372
|
}
|
|
350
373
|
: null,
|
|
351
374
|
metadata.account?.subscriptionType
|
|
@@ -367,6 +390,7 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
367
390
|
id: 'auth',
|
|
368
391
|
label: 'Auth',
|
|
369
392
|
value: metadata.account.tokenSource ?? metadata.account?.apiKeySource ?? 'connected',
|
|
393
|
+
tier: 'diagnostic',
|
|
370
394
|
}
|
|
371
395
|
: null,
|
|
372
396
|
runtimeModels[0]
|
|
@@ -374,17 +398,21 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
374
398
|
id: 'fallbackModel',
|
|
375
399
|
label: 'Fallback model',
|
|
376
400
|
value: runtimeModels[0].label,
|
|
401
|
+
tier: 'diagnostic',
|
|
402
|
+
source: 'host-default',
|
|
377
403
|
}
|
|
378
404
|
: null,
|
|
379
405
|
{
|
|
380
406
|
id: 'toolPolicy',
|
|
381
407
|
label: 'Tool policy',
|
|
382
408
|
value: 'Claude Code preset',
|
|
409
|
+
tier: 'detail',
|
|
383
410
|
},
|
|
384
411
|
{
|
|
385
412
|
id: 'browser',
|
|
386
413
|
label: 'Browser tools',
|
|
387
414
|
value: 'Runtime policy',
|
|
415
|
+
tier: 'detail',
|
|
388
416
|
},
|
|
389
417
|
].filter((item) => Boolean(item));
|
|
390
418
|
return {
|
|
@@ -404,6 +432,7 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
404
432
|
?? server.scope
|
|
405
433
|
?? undefined,
|
|
406
434
|
})),
|
|
435
|
+
tier: 'diagnostic',
|
|
407
436
|
},
|
|
408
437
|
{
|
|
409
438
|
id: 'plugins',
|
|
@@ -414,6 +443,7 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
414
443
|
status: 'configured',
|
|
415
444
|
description: plugin.source ?? plugin.path,
|
|
416
445
|
})),
|
|
446
|
+
tier: 'diagnostic',
|
|
417
447
|
},
|
|
418
448
|
{
|
|
419
449
|
id: 'commands',
|
|
@@ -424,6 +454,7 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
424
454
|
status: 'configured',
|
|
425
455
|
description: command.description ?? command.argumentHint ?? undefined,
|
|
426
456
|
})),
|
|
457
|
+
tier: 'diagnostic',
|
|
427
458
|
},
|
|
428
459
|
{
|
|
429
460
|
id: 'agents',
|
|
@@ -434,6 +465,7 @@ function buildClaudeRuntimeInfo(input) {
|
|
|
434
465
|
status: 'configured',
|
|
435
466
|
description: agent.description ?? agent.model ?? undefined,
|
|
436
467
|
})),
|
|
468
|
+
tier: 'diagnostic',
|
|
437
469
|
},
|
|
438
470
|
],
|
|
439
471
|
execution: {
|
|
@@ -465,12 +497,40 @@ const CLAUDE_RUNTIME_CAPABILITIES = {
|
|
|
465
497
|
supportsNonFinalPermanentMessages: false,
|
|
466
498
|
};
|
|
467
499
|
function buildCanonPrompt(input) {
|
|
500
|
+
const { session, ...promptInput } = input;
|
|
468
501
|
return buildCanonHostPrompt({
|
|
469
502
|
hostLabel: 'Claude Code',
|
|
470
503
|
buildInboundContextLines,
|
|
471
|
-
|
|
504
|
+
sessionContextLines: session ? buildClaudeSessionContextLines(session) : undefined,
|
|
505
|
+
...promptInput,
|
|
472
506
|
});
|
|
473
507
|
}
|
|
508
|
+
function optionLabel(value, options) {
|
|
509
|
+
if (!value)
|
|
510
|
+
return null;
|
|
511
|
+
const label = options.find((option) => option.value === value)?.label ?? value;
|
|
512
|
+
return label === value ? value : `${label} (${value})`;
|
|
513
|
+
}
|
|
514
|
+
function buildClaudeSessionContextLines(session) {
|
|
515
|
+
const workspace = workspaceOptions.find((option) => option.cwd === session.environment.baseCwd);
|
|
516
|
+
const permissionMode = optionLabel(session.state.permissionMode, CLAUDE_PERMISSION_MODE_OPTIONS);
|
|
517
|
+
const effort = optionLabel(session.state.effort, [
|
|
518
|
+
{ value: 'low', label: 'Low' },
|
|
519
|
+
{ value: 'medium', label: 'Medium' },
|
|
520
|
+
{ value: 'high', label: 'High' },
|
|
521
|
+
]);
|
|
522
|
+
const model = optionLabel(session.state.model, session.availableModels);
|
|
523
|
+
return [
|
|
524
|
+
model ? `Selected model: ${model}` : null,
|
|
525
|
+
permissionMode ? `Permission mode: ${permissionMode}` : null,
|
|
526
|
+
effort ? `Thinking level: ${effort}` : null,
|
|
527
|
+
workspace ? `Project: ${workspace.label} (${workspace.id})` : null,
|
|
528
|
+
`Execution mode: ${session.environment.mode}`,
|
|
529
|
+
`Working directory: ${session.cwd}`,
|
|
530
|
+
session.environment.branch ? `Git branch: ${session.environment.branch}` : null,
|
|
531
|
+
session.environment.worktreePath ? `Worktree: ${session.environment.worktreePath}` : null,
|
|
532
|
+
].filter((line) => Boolean(line));
|
|
533
|
+
}
|
|
474
534
|
function renderInboundContent(message, materialized) {
|
|
475
535
|
return renderCanonHostInboundContent(message, materialized);
|
|
476
536
|
}
|
|
@@ -501,19 +561,25 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
501
561
|
let resolveInput = null;
|
|
502
562
|
let inputDone = false;
|
|
503
563
|
const messageQueue = [];
|
|
504
|
-
const sendInput = (
|
|
564
|
+
const sendInput = (input) => {
|
|
505
565
|
if (resolveInput) {
|
|
506
566
|
const r = resolveInput;
|
|
507
567
|
resolveInput = null;
|
|
508
|
-
r(
|
|
568
|
+
r(input);
|
|
509
569
|
}
|
|
510
570
|
else {
|
|
511
571
|
// Queue if iterator isn't waiting yet (e.g., during init)
|
|
512
|
-
messageQueue.push(
|
|
572
|
+
messageQueue.push(input);
|
|
513
573
|
}
|
|
514
574
|
};
|
|
515
|
-
let enqueueInboundMessage = (msg,
|
|
516
|
-
sendInput(
|
|
575
|
+
let enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId, markAccepted = false) => {
|
|
576
|
+
sendInput(createClaudeInputEnvelope({
|
|
577
|
+
kind: 'canon',
|
|
578
|
+
msg,
|
|
579
|
+
intent,
|
|
580
|
+
sourceMessageId,
|
|
581
|
+
markAccepted,
|
|
582
|
+
}));
|
|
517
583
|
};
|
|
518
584
|
const inputStream = {
|
|
519
585
|
[Symbol.asyncIterator]() {
|
|
@@ -523,10 +589,15 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
523
589
|
return Promise.resolve({ done: true, value: undefined });
|
|
524
590
|
// Drain queued messages first (sent before iterator was ready)
|
|
525
591
|
if (messageQueue.length > 0) {
|
|
526
|
-
|
|
592
|
+
const next = messageQueue.shift();
|
|
593
|
+
session.activeInput = next;
|
|
594
|
+
return Promise.resolve({ done: false, value: next.msg });
|
|
527
595
|
}
|
|
528
596
|
return new Promise((resolve) => {
|
|
529
|
-
resolveInput = (
|
|
597
|
+
resolveInput = (input) => {
|
|
598
|
+
session.activeInput = input;
|
|
599
|
+
resolve({ done: false, value: input.msg });
|
|
600
|
+
};
|
|
530
601
|
});
|
|
531
602
|
},
|
|
532
603
|
return() {
|
|
@@ -667,8 +738,9 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
667
738
|
sendInput,
|
|
668
739
|
enqueueInbound: (msg, intent = 'queue', sourceMessageId, markAccepted = false) => enqueueInboundMessage(msg, intent, sourceMessageId, markAccepted),
|
|
669
740
|
state: {
|
|
670
|
-
model:
|
|
741
|
+
model: undefined,
|
|
671
742
|
permissionMode: config.permissionMode,
|
|
743
|
+
effort: undefined,
|
|
672
744
|
state: 'idle',
|
|
673
745
|
},
|
|
674
746
|
availableModels: config.availableModels,
|
|
@@ -678,8 +750,12 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
678
750
|
finalDeliveryTimer: null,
|
|
679
751
|
seedResponseHandled: !!resumeSessionId, // Resumed sessions don't need seed handling
|
|
680
752
|
pendingInputs: [],
|
|
753
|
+
activeInput: null,
|
|
754
|
+
finalizedTurnKeys: new Set(),
|
|
755
|
+
interruptedTurnKeys: new Set(),
|
|
681
756
|
pendingFinalText: null,
|
|
682
|
-
|
|
757
|
+
pendingFinalDelivery: null,
|
|
758
|
+
runtimeControlErrors: {},
|
|
683
759
|
toolInProgress: false,
|
|
684
760
|
turnState: 'idle',
|
|
685
761
|
currentTurnId: null,
|
|
@@ -690,6 +766,10 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
690
766
|
closed: false,
|
|
691
767
|
sdkSessionId: resumeSessionId || null,
|
|
692
768
|
pendingReply: null,
|
|
769
|
+
writeState: () => writeState(),
|
|
770
|
+
writeTurn: () => writeTurn(),
|
|
771
|
+
setRuntimeControlError: (controlId, value, error) => setRuntimeControlError(controlId, value, error),
|
|
772
|
+
clearRuntimeControlError: (controlId) => clearRuntimeControlError(controlId),
|
|
693
773
|
};
|
|
694
774
|
const streamingBuffer = createStreamingBuffer({
|
|
695
775
|
throttleMs: STREAMING_THROTTLE_MS,
|
|
@@ -739,7 +819,8 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
739
819
|
session.currentTurnOpenedAt = null;
|
|
740
820
|
session.lastAcceptedIntent = null;
|
|
741
821
|
session.pendingFinalText = null;
|
|
742
|
-
session.
|
|
822
|
+
session.pendingFinalDelivery = null;
|
|
823
|
+
session.activeInput = null;
|
|
743
824
|
session.toolInProgress = false;
|
|
744
825
|
session.turnState = 'idle';
|
|
745
826
|
stopVisibleWorkSignal();
|
|
@@ -755,10 +836,18 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
755
836
|
resetTurnToIdle();
|
|
756
837
|
}, FINAL_MESSAGE_HANDOFF_MS);
|
|
757
838
|
}
|
|
758
|
-
function markPendingFinalDelivery(finalText) {
|
|
839
|
+
function markPendingFinalDelivery(finalText, turn) {
|
|
759
840
|
clearIdleResetTimer();
|
|
760
841
|
session.pendingFinalText = finalText;
|
|
761
|
-
session.
|
|
842
|
+
session.pendingFinalDelivery = {
|
|
843
|
+
turnKey: turn.turnKey,
|
|
844
|
+
text: finalText,
|
|
845
|
+
messageId: buildClaudeFinalMessageId({
|
|
846
|
+
agentId,
|
|
847
|
+
conversationId,
|
|
848
|
+
turnKey: turn.turnKey,
|
|
849
|
+
}),
|
|
850
|
+
};
|
|
762
851
|
session.state.state = 'running';
|
|
763
852
|
session.turnState = 'streaming';
|
|
764
853
|
stopVisibleWorkSignal();
|
|
@@ -767,49 +856,96 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
767
856
|
writeTurn();
|
|
768
857
|
client.setTyping(conversationId, true, 'typing').catch(() => { });
|
|
769
858
|
}
|
|
770
|
-
async function deliverFinalReply(finalText) {
|
|
859
|
+
async function deliverFinalReply(finalText, turn = session.activeInput) {
|
|
860
|
+
if (!shouldDeliverClaudeFinal({
|
|
861
|
+
turn,
|
|
862
|
+
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
863
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
864
|
+
})) {
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
867
|
+
const deliverTurn = turn;
|
|
868
|
+
if (!deliverTurn)
|
|
869
|
+
return true;
|
|
870
|
+
const finalMessageId = buildClaudeFinalMessageId({
|
|
871
|
+
agentId,
|
|
872
|
+
conversationId,
|
|
873
|
+
turnKey: deliverTurn.turnKey,
|
|
874
|
+
});
|
|
771
875
|
try {
|
|
772
876
|
await client.sendMessage(conversationId, finalText, {
|
|
877
|
+
messageId: finalMessageId,
|
|
773
878
|
metadata: {
|
|
774
879
|
turnId: session.currentTurnId,
|
|
880
|
+
turnKey: deliverTurn.turnKey,
|
|
881
|
+
sourceMessageId: deliverTurn.sourceMessageId ?? undefined,
|
|
775
882
|
turnSemantics: 'turn_complete',
|
|
776
883
|
turnComplete: true,
|
|
777
884
|
deliveryIntent: session.lastAcceptedIntent ?? undefined,
|
|
778
885
|
},
|
|
779
886
|
});
|
|
887
|
+
session.finalizedTurnKeys.add(deliverTurn.turnKey);
|
|
780
888
|
session.state.state = 'idle';
|
|
781
|
-
session.
|
|
889
|
+
session.pendingFinalDelivery = null;
|
|
782
890
|
clearFinalDeliveryTimer();
|
|
783
891
|
writeState();
|
|
784
892
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Sent final reply (${finalText.length} chars)`);
|
|
785
893
|
return true;
|
|
786
894
|
}
|
|
787
895
|
catch (err) {
|
|
788
|
-
markPendingFinalDelivery(finalText);
|
|
896
|
+
markPendingFinalDelivery(finalText, deliverTurn);
|
|
789
897
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Failed to send final reply:`, err);
|
|
790
898
|
return false;
|
|
791
899
|
}
|
|
792
900
|
}
|
|
793
|
-
function scheduleFinalDeliveryRetry(
|
|
794
|
-
if (session.closed || session.
|
|
901
|
+
function scheduleFinalDeliveryRetry(pending = session.pendingFinalDelivery) {
|
|
902
|
+
if (!pending || session.closed || session.pendingFinalDelivery?.turnKey !== pending.turnKey)
|
|
795
903
|
return;
|
|
796
904
|
clearFinalDeliveryTimer();
|
|
797
905
|
session.finalDeliveryTimer = setTimeout(() => {
|
|
798
906
|
session.finalDeliveryTimer = null;
|
|
799
|
-
|
|
907
|
+
const retryTurn = session.activeInput?.turnKey === pending.turnKey
|
|
908
|
+
? session.activeInput
|
|
909
|
+
: null;
|
|
910
|
+
void deliverFinalReply(pending.text, retryTurn).then((sent) => {
|
|
800
911
|
if (sent) {
|
|
801
912
|
scheduleFinalHandoffReset();
|
|
802
913
|
return;
|
|
803
914
|
}
|
|
804
|
-
scheduleFinalDeliveryRetry(
|
|
915
|
+
scheduleFinalDeliveryRetry();
|
|
805
916
|
});
|
|
806
917
|
}, FINAL_DELIVERY_RETRY_MS);
|
|
807
918
|
}
|
|
808
919
|
function writeState() {
|
|
920
|
+
const runtimeControlErrors = Object.keys(session.runtimeControlErrors).length > 0
|
|
921
|
+
? session.runtimeControlErrors
|
|
922
|
+
: null;
|
|
923
|
+
const controlState = {};
|
|
924
|
+
const appliedAt = Date.now();
|
|
925
|
+
if (session.state.model !== undefined) {
|
|
926
|
+
controlState.model = { value: session.state.model, source: 'applied', appliedAt };
|
|
927
|
+
}
|
|
928
|
+
else if (config.model !== undefined) {
|
|
929
|
+
controlState.model = { value: config.model, source: 'requested' };
|
|
930
|
+
}
|
|
931
|
+
if (session.state.permissionMode !== undefined) {
|
|
932
|
+
controlState.permissionMode = { value: session.state.permissionMode, source: 'applied', appliedAt };
|
|
933
|
+
}
|
|
934
|
+
else if (config.permissionMode !== undefined) {
|
|
935
|
+
controlState.permissionMode = { value: config.permissionMode, source: 'requested' };
|
|
936
|
+
}
|
|
937
|
+
if (session.state.effort !== undefined) {
|
|
938
|
+
controlState.effort = { value: session.state.effort, source: 'applied', appliedAt };
|
|
939
|
+
}
|
|
940
|
+
else if (config.effort !== undefined) {
|
|
941
|
+
controlState.effort = { value: config.effort, source: 'requested' };
|
|
942
|
+
}
|
|
809
943
|
runtimeState.writeSessionState(conversationId, {
|
|
810
944
|
model: session.state.model,
|
|
811
945
|
permissionMode: session.state.permissionMode,
|
|
812
946
|
effort: session.state.effort,
|
|
947
|
+
controlState,
|
|
948
|
+
runtimeControlErrors,
|
|
813
949
|
contextUsage: session.state.contextUsage,
|
|
814
950
|
...(session.availableModels.length ? { availableModels: session.availableModels } : {}),
|
|
815
951
|
cwd,
|
|
@@ -823,6 +959,25 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
823
959
|
isActive: true,
|
|
824
960
|
}).catch(() => { });
|
|
825
961
|
}
|
|
962
|
+
function setRuntimeControlError(controlId, value, error) {
|
|
963
|
+
session.runtimeControlErrors = {
|
|
964
|
+
...session.runtimeControlErrors,
|
|
965
|
+
[controlId]: {
|
|
966
|
+
value,
|
|
967
|
+
message: formatClaudeControlError(error),
|
|
968
|
+
updatedAt: Date.now(),
|
|
969
|
+
},
|
|
970
|
+
};
|
|
971
|
+
writeState();
|
|
972
|
+
}
|
|
973
|
+
function clearRuntimeControlError(controlId) {
|
|
974
|
+
if (!(controlId in session.runtimeControlErrors))
|
|
975
|
+
return;
|
|
976
|
+
const next = { ...session.runtimeControlErrors };
|
|
977
|
+
delete next[controlId];
|
|
978
|
+
session.runtimeControlErrors = next;
|
|
979
|
+
writeState();
|
|
980
|
+
}
|
|
826
981
|
function writeTurn() {
|
|
827
982
|
runtimeState.writeTurnState(conversationId, {
|
|
828
983
|
turnId: session.currentTurnId,
|
|
@@ -870,22 +1025,32 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
870
1025
|
session.lastAcceptedIntent = next.intent;
|
|
871
1026
|
writeTurn();
|
|
872
1027
|
void markQueuedMessageAccepted(next.sourceMessageId, next.markAccepted)
|
|
873
|
-
.finally(() => sendInput(next
|
|
1028
|
+
.finally(() => sendInput(next));
|
|
874
1029
|
}
|
|
875
1030
|
enqueueInboundMessage = (msg, intent = 'queue', sourceMessageId = null, markAccepted = false) => {
|
|
876
1031
|
session.lastActivity = Date.now();
|
|
877
|
-
|
|
878
|
-
|
|
1032
|
+
const input = createClaudeInputEnvelope({
|
|
1033
|
+
kind: 'canon',
|
|
1034
|
+
msg,
|
|
1035
|
+
intent,
|
|
1036
|
+
sourceMessageId,
|
|
1037
|
+
markAccepted,
|
|
1038
|
+
});
|
|
1039
|
+
if (session.pendingFinalDelivery) {
|
|
1040
|
+
session.pendingInputs.push(input);
|
|
879
1041
|
writeTurn();
|
|
880
1042
|
return;
|
|
881
1043
|
}
|
|
882
1044
|
if (session.state.state === 'running' && intent !== 'interrupt') {
|
|
883
|
-
session.pendingInputs.push(
|
|
1045
|
+
session.pendingInputs.push(input);
|
|
884
1046
|
writeTurn();
|
|
885
1047
|
return;
|
|
886
1048
|
}
|
|
887
1049
|
if (session.state.state === 'running' && intent === 'interrupt') {
|
|
888
|
-
session.
|
|
1050
|
+
if (session.activeInput) {
|
|
1051
|
+
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
1052
|
+
}
|
|
1053
|
+
session.pendingInputs.unshift(input);
|
|
889
1054
|
session.turnState = 'interrupted';
|
|
890
1055
|
stopVisibleWorkSignal();
|
|
891
1056
|
writeTurn();
|
|
@@ -895,7 +1060,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
895
1060
|
session.lastAcceptedIntent = intent;
|
|
896
1061
|
writeTurn();
|
|
897
1062
|
void markQueuedMessageAccepted(sourceMessageId, markAccepted)
|
|
898
|
-
.finally(() => sendInput(
|
|
1063
|
+
.finally(() => sendInput(input));
|
|
899
1064
|
};
|
|
900
1065
|
writeTurn();
|
|
901
1066
|
// ── Process SDK output messages ──
|
|
@@ -991,12 +1156,22 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
991
1156
|
const resultSessionId = msg.session_id;
|
|
992
1157
|
if (resultSessionId)
|
|
993
1158
|
session.sdkSessionId = resultSessionId;
|
|
1159
|
+
const completedInput = session.activeInput;
|
|
994
1160
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Turn complete (${msg.subtype})`);
|
|
995
1161
|
const resultText = typeof msg.result === 'string'
|
|
996
1162
|
? msg.result.trim()
|
|
997
1163
|
: '';
|
|
998
1164
|
const finalText = resultText || session.pendingFinalText?.trim() || null;
|
|
999
|
-
const
|
|
1165
|
+
const shouldDeliverFinal = finalText
|
|
1166
|
+
? shouldDeliverClaudeFinal({
|
|
1167
|
+
turn: completedInput,
|
|
1168
|
+
finalizedTurnKeys: session.finalizedTurnKeys,
|
|
1169
|
+
interruptedTurnKeys: session.interruptedTurnKeys,
|
|
1170
|
+
})
|
|
1171
|
+
: false;
|
|
1172
|
+
const finalDelivered = finalText && shouldDeliverFinal
|
|
1173
|
+
? await deliverFinalReply(finalText, completedInput)
|
|
1174
|
+
: true;
|
|
1000
1175
|
try {
|
|
1001
1176
|
const usage = await q.getContextUsage();
|
|
1002
1177
|
session.state.contextUsage = {
|
|
@@ -1011,10 +1186,19 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1011
1186
|
// Non-fatal
|
|
1012
1187
|
}
|
|
1013
1188
|
if (finalDelivered) {
|
|
1014
|
-
|
|
1189
|
+
if (completedInput?.turnKey === session.activeInput?.turnKey) {
|
|
1190
|
+
session.activeInput = null;
|
|
1191
|
+
}
|
|
1192
|
+
session.pendingFinalText = null;
|
|
1193
|
+
if (shouldDeliverFinal && finalText) {
|
|
1194
|
+
scheduleFinalHandoffReset();
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
resetTurnToIdle();
|
|
1198
|
+
}
|
|
1015
1199
|
}
|
|
1016
1200
|
else if (finalText) {
|
|
1017
|
-
scheduleFinalDeliveryRetry(
|
|
1201
|
+
scheduleFinalDeliveryRetry();
|
|
1018
1202
|
}
|
|
1019
1203
|
break;
|
|
1020
1204
|
}
|
|
@@ -1056,24 +1240,31 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1056
1240
|
}
|
|
1057
1241
|
if (!resumeSessionId) {
|
|
1058
1242
|
// New session — send seed message to activate streaming input mode
|
|
1059
|
-
sendInput({
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1243
|
+
sendInput(createClaudeInputEnvelope({
|
|
1244
|
+
kind: 'seed',
|
|
1245
|
+
msg: {
|
|
1246
|
+
type: 'user',
|
|
1247
|
+
message: {
|
|
1248
|
+
role: 'user',
|
|
1249
|
+
content: 'You are connected to Canon messaging. Follow-up messages in this session come from Canon participants, and your text responses will be sent back to the conversation. You have full Claude Code capabilities (file read/write, bash, etc).',
|
|
1250
|
+
},
|
|
1251
|
+
parent_tool_use_id: null,
|
|
1064
1252
|
},
|
|
1065
|
-
|
|
1066
|
-
});
|
|
1253
|
+
}));
|
|
1067
1254
|
}
|
|
1068
1255
|
// Refresh the canonical runtime descriptor after Claude reports supported models.
|
|
1069
1256
|
try {
|
|
1070
1257
|
const models = await q.supportedModels();
|
|
1071
|
-
const modelList =
|
|
1258
|
+
const modelList = mergeClaudeModelOptions({
|
|
1259
|
+
previous: runtimeModels,
|
|
1260
|
+
next: toModelOptions(models),
|
|
1261
|
+
selectedModel: session.state.model,
|
|
1262
|
+
});
|
|
1072
1263
|
session.availableModels = modelList;
|
|
1073
1264
|
runtimeModels = modelList;
|
|
1074
1265
|
onRuntimeDescriptorUpdate({
|
|
1075
1266
|
defaultWorkspaceId: workspaceOptions[0]?.id,
|
|
1076
|
-
defaultModel:
|
|
1267
|
+
defaultModel: modelList[0]?.value,
|
|
1077
1268
|
defaultPermissionMode: 'default',
|
|
1078
1269
|
availableModels: modelList,
|
|
1079
1270
|
availableWorkspaces: buildPublicWorkspaceOptions(workspaceOptions),
|
|
@@ -1085,21 +1276,7 @@ function createSession(conversationId, environment, agentId, client, runtimeStat
|
|
|
1085
1276
|
executionModes: config.availableExecutionModes,
|
|
1086
1277
|
}),
|
|
1087
1278
|
});
|
|
1088
|
-
|
|
1089
|
-
model: session.state.model,
|
|
1090
|
-
permissionMode: session.state.permissionMode,
|
|
1091
|
-
effort: session.state.effort,
|
|
1092
|
-
contextUsage: session.state.contextUsage,
|
|
1093
|
-
cwd,
|
|
1094
|
-
executionMode: session.environment.mode,
|
|
1095
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1096
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1097
|
-
...(resolveExecutionFallbackReason(session.environment)
|
|
1098
|
-
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1099
|
-
: {}),
|
|
1100
|
-
hostMode: true,
|
|
1101
|
-
isActive: true,
|
|
1102
|
-
}).catch(() => { });
|
|
1279
|
+
writeState();
|
|
1103
1280
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Models: ${models.map((m) => m.displayName).join(', ')}`);
|
|
1104
1281
|
}
|
|
1105
1282
|
catch {
|
|
@@ -1513,7 +1690,10 @@ export async function main() {
|
|
|
1513
1690
|
availableExecutionModes: hostAvailableExecutionModes,
|
|
1514
1691
|
}, resumeId);
|
|
1515
1692
|
sessions.set(conversationId, session);
|
|
1516
|
-
await
|
|
1693
|
+
await Promise.all([
|
|
1694
|
+
baselineControlSignal(conversationId),
|
|
1695
|
+
baselineSessionControl(conversationId),
|
|
1696
|
+
]);
|
|
1517
1697
|
return session;
|
|
1518
1698
|
}
|
|
1519
1699
|
catch (error) {
|
|
@@ -1540,6 +1720,17 @@ export async function main() {
|
|
|
1540
1720
|
lastSeenSignal.set(conversationId, timestamp);
|
|
1541
1721
|
}
|
|
1542
1722
|
}
|
|
1723
|
+
async function baselineSessionControl(conversationId) {
|
|
1724
|
+
if (lastSeenControl.has(conversationId))
|
|
1725
|
+
return;
|
|
1726
|
+
const raw = await rtdbRead(`/control/${conversationId}/${agentId}/session`).catch(() => null);
|
|
1727
|
+
if (!raw || typeof raw !== 'object')
|
|
1728
|
+
return;
|
|
1729
|
+
const timestamp = Number(raw.updatedAt ?? 0);
|
|
1730
|
+
if (timestamp > 0) {
|
|
1731
|
+
lastSeenControl.set(conversationId, timestamp);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1543
1734
|
async function enqueueRecoveredInboundMessage(input) {
|
|
1544
1735
|
const m = input.message;
|
|
1545
1736
|
if (m.senderId === agentId)
|
|
@@ -1599,6 +1790,7 @@ export async function main() {
|
|
|
1599
1790
|
behavior,
|
|
1600
1791
|
workSession: m.workSession,
|
|
1601
1792
|
workSessions,
|
|
1793
|
+
session,
|
|
1602
1794
|
});
|
|
1603
1795
|
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
1604
1796
|
session.enqueueInbound({
|
|
@@ -1747,6 +1939,7 @@ export async function main() {
|
|
|
1747
1939
|
behavior,
|
|
1748
1940
|
workSession: m.workSession,
|
|
1749
1941
|
workSessions,
|
|
1942
|
+
session,
|
|
1750
1943
|
});
|
|
1751
1944
|
const messageContent = await buildCanonUserContent({ promptText, materialized });
|
|
1752
1945
|
session.enqueueInbound({
|
|
@@ -1801,9 +1994,11 @@ export async function main() {
|
|
|
1801
1994
|
try {
|
|
1802
1995
|
await session.query.setModel(ctrl.model);
|
|
1803
1996
|
session.state.model = ctrl.model;
|
|
1997
|
+
session.clearRuntimeControlError('model');
|
|
1804
1998
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Model → ${ctrl.model}`);
|
|
1805
1999
|
}
|
|
1806
2000
|
catch (err) {
|
|
2001
|
+
session.setRuntimeControlError('model', ctrl.model, err);
|
|
1807
2002
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set model:`, err);
|
|
1808
2003
|
}
|
|
1809
2004
|
}
|
|
@@ -1811,9 +2006,11 @@ export async function main() {
|
|
|
1811
2006
|
try {
|
|
1812
2007
|
await session.query.setPermissionMode(ctrl.permissionMode);
|
|
1813
2008
|
session.state.permissionMode = ctrl.permissionMode;
|
|
2009
|
+
session.clearRuntimeControlError('permissionMode');
|
|
1814
2010
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Mode → ${ctrl.permissionMode}`);
|
|
1815
2011
|
}
|
|
1816
2012
|
catch (err) {
|
|
2013
|
+
session.setRuntimeControlError('permissionMode', ctrl.permissionMode, err);
|
|
1817
2014
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set mode:`, err);
|
|
1818
2015
|
}
|
|
1819
2016
|
}
|
|
@@ -1822,27 +2019,19 @@ export async function main() {
|
|
|
1822
2019
|
try {
|
|
1823
2020
|
await session.query.applyFlagSettings({ effortLevel: nextEffort });
|
|
1824
2021
|
session.state.effort = nextEffort;
|
|
2022
|
+
session.clearRuntimeControlError('effort');
|
|
1825
2023
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Effort → ${nextEffort}`);
|
|
1826
2024
|
}
|
|
1827
2025
|
catch (err) {
|
|
2026
|
+
session.setRuntimeControlError('effort', nextEffort, err);
|
|
1828
2027
|
console.error(`[canon-host] [${convoId.slice(0, 8)}] Failed to set effort:`, err);
|
|
1829
2028
|
}
|
|
1830
2029
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
cwd: session.cwd,
|
|
1837
|
-
executionMode: session.environment.mode,
|
|
1838
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1839
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1840
|
-
...(resolveExecutionFallbackReason(session.environment)
|
|
1841
|
-
? { executionFallbackReason: resolveExecutionFallbackReason(session.environment) ?? undefined }
|
|
1842
|
-
: {}),
|
|
1843
|
-
hostMode: true,
|
|
1844
|
-
isActive: true,
|
|
1845
|
-
}).catch(() => { });
|
|
2030
|
+
else if (ctrl.effort && !nextEffort) {
|
|
2031
|
+
session.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
|
|
2032
|
+
}
|
|
2033
|
+
session.writeState();
|
|
2034
|
+
await rtdbWrite(`/control/${convoId}/${agentId}/session`, null).catch(() => { });
|
|
1846
2035
|
}
|
|
1847
2036
|
}
|
|
1848
2037
|
}
|
|
@@ -1866,6 +2055,10 @@ export async function main() {
|
|
|
1866
2055
|
const droppedInputs = session.pendingInputs.splice(0);
|
|
1867
2056
|
await markSessionPendingInputsRejected(convoId, droppedInputs);
|
|
1868
2057
|
}
|
|
2058
|
+
if (session.activeInput) {
|
|
2059
|
+
session.interruptedTurnKeys.add(session.activeInput.turnKey);
|
|
2060
|
+
}
|
|
2061
|
+
session.pendingFinalText = null;
|
|
1869
2062
|
session.turnState = 'interrupted';
|
|
1870
2063
|
runtimeState.writeTurnState(convoId, {
|
|
1871
2064
|
turnId: session.currentTurnId,
|
|
@@ -1907,30 +2100,8 @@ export async function main() {
|
|
|
1907
2100
|
// ── Heartbeat (per-session) ──
|
|
1908
2101
|
const heartbeat = setInterval(() => {
|
|
1909
2102
|
for (const session of sessions.values()) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
permissionMode: session.state.permissionMode,
|
|
1913
|
-
effort: session.state.effort,
|
|
1914
|
-
contextUsage: session.state.contextUsage,
|
|
1915
|
-
cwd: session.cwd,
|
|
1916
|
-
executionMode: session.environment.mode,
|
|
1917
|
-
...(session.environment.branch ? { executionBranch: session.environment.branch } : {}),
|
|
1918
|
-
...(session.environment.worktreePath ? { worktreePath: session.environment.worktreePath } : {}),
|
|
1919
|
-
hostMode: true,
|
|
1920
|
-
isActive: true,
|
|
1921
|
-
}).catch(() => { });
|
|
1922
|
-
runtimeState.writeTurnState(session.conversationId, {
|
|
1923
|
-
turnId: session.currentTurnId,
|
|
1924
|
-
state: session.turnState,
|
|
1925
|
-
queueDepth: session.pendingInputs.length,
|
|
1926
|
-
currentSpeakerId: agentId,
|
|
1927
|
-
lastAcceptedIntent: session.lastAcceptedIntent,
|
|
1928
|
-
capabilities: CLAUDE_RUNTIME_CAPABILITIES,
|
|
1929
|
-
...(session.currentTurnOpenedAt ? { openedAt: session.currentTurnOpenedAt } : {}),
|
|
1930
|
-
...(session.turnState === 'idle' || session.turnState === 'completed' || session.turnState === 'interrupted'
|
|
1931
|
-
? { completedAt: { '.sv': 'timestamp' } }
|
|
1932
|
-
: {}),
|
|
1933
|
-
}).catch(() => { });
|
|
2103
|
+
session.writeState();
|
|
2104
|
+
session.writeTurn();
|
|
1934
2105
|
}
|
|
1935
2106
|
void publishRuntimeHeartbeat();
|
|
1936
2107
|
}, HEARTBEAT_MS);
|
|
@@ -1971,7 +2142,11 @@ export async function main() {
|
|
|
1971
2142
|
}
|
|
1972
2143
|
// ── Entry ───────────────────────────────────────────────────────────
|
|
1973
2144
|
runCli(import.meta.url, main, (err) => {
|
|
1974
|
-
|
|
2145
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2146
|
+
console.error(`[canon-host] ${message}`);
|
|
1975
2147
|
getActiveProfileLock()?.release();
|
|
1976
2148
|
process.exit(1);
|
|
2149
|
+
}, {
|
|
2150
|
+
name: 'canon-claude',
|
|
2151
|
+
help: HELP,
|
|
1977
2152
|
});
|
package/dist/register.js
CHANGED
|
@@ -13,6 +13,27 @@ import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRe
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import { parseArgs } from 'node:util';
|
|
15
15
|
import { runCli } from './cli-entry.js';
|
|
16
|
+
const HELP = `canon-register — register or reconnect a Claude Code agent in Canon
|
|
17
|
+
|
|
18
|
+
USAGE
|
|
19
|
+
canon-register --name <name> --description <text> --phone <e164> [flags]
|
|
20
|
+
|
|
21
|
+
REQUIRED
|
|
22
|
+
--name <name> Agent display name shown in Canon
|
|
23
|
+
--description <text> Short profile description
|
|
24
|
+
--phone <e164> Owner phone number, for example +15551234567
|
|
25
|
+
|
|
26
|
+
FLAGS
|
|
27
|
+
--profile <name> Local profile name in ~/.canon/agents.json
|
|
28
|
+
--base-url <url> Canon API base URL override
|
|
29
|
+
--help, -h Show this help
|
|
30
|
+
--version, -V Show package version
|
|
31
|
+
|
|
32
|
+
EXAMPLES
|
|
33
|
+
canon-register --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
|
|
34
|
+
canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
35
|
+
|
|
36
|
+
After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
|
|
16
37
|
export async function main() {
|
|
17
38
|
setDefaultResultOrder('ipv4first');
|
|
18
39
|
const { values } = parseArgs({
|
|
@@ -102,4 +123,7 @@ export async function main() {
|
|
|
102
123
|
runCli(import.meta.url, main, (error) => {
|
|
103
124
|
console.error('[canon-register] Fatal error:', error);
|
|
104
125
|
process.exit(1);
|
|
126
|
+
}, {
|
|
127
|
+
name: 'canon-register',
|
|
128
|
+
help: HELP,
|
|
105
129
|
});
|
package/dist/server.js
CHANGED
|
@@ -13,6 +13,17 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
|
|
|
13
13
|
import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, } from '@canonmsg/core';
|
|
14
14
|
import { ApprovalHttpServer } from './approval-server.js';
|
|
15
15
|
import { runCli } from './cli-entry.js';
|
|
16
|
+
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
17
|
+
|
|
18
|
+
USAGE
|
|
19
|
+
canon-channel-server
|
|
20
|
+
|
|
21
|
+
FLAGS
|
|
22
|
+
--help, -h Show this help
|
|
23
|
+
--version, -V Show package version
|
|
24
|
+
|
|
25
|
+
This command is normally launched by Claude Code as an MCP stdio server. For
|
|
26
|
+
phone-controlled local sessions, use canon-claude instead.`;
|
|
16
27
|
// ── MCP server ─────────────────────────────────────────────────────────
|
|
17
28
|
const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
|
|
18
29
|
capabilities: {
|
|
@@ -672,4 +683,7 @@ export async function main() {
|
|
|
672
683
|
runCli(import.meta.url, main, (err) => {
|
|
673
684
|
console.error('[canon] Fatal error:', err);
|
|
674
685
|
process.exit(1);
|
|
686
|
+
}, {
|
|
687
|
+
name: 'canon-channel-server',
|
|
688
|
+
help: HELP,
|
|
675
689
|
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { DeliveryIntent, ModelOption } from '@canonmsg/core';
|
|
3
|
+
export type ClaudeInputKind = 'seed' | 'canon';
|
|
4
|
+
export interface ClaudeInputEnvelope {
|
|
5
|
+
kind: ClaudeInputKind;
|
|
6
|
+
msg: SDKUserMessage;
|
|
7
|
+
intent: DeliveryIntent;
|
|
8
|
+
sourceMessageId: string | null;
|
|
9
|
+
markAccepted: boolean;
|
|
10
|
+
turnKey: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ClaudeRuntimeControlError {
|
|
13
|
+
value: string;
|
|
14
|
+
message: string;
|
|
15
|
+
updatedAt: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ClaudePendingFinalDelivery {
|
|
18
|
+
turnKey: string;
|
|
19
|
+
text: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function createClaudeInputEnvelope(input: {
|
|
23
|
+
kind: ClaudeInputKind;
|
|
24
|
+
msg: SDKUserMessage;
|
|
25
|
+
intent?: DeliveryIntent;
|
|
26
|
+
sourceMessageId?: string | null;
|
|
27
|
+
markAccepted?: boolean;
|
|
28
|
+
}): ClaudeInputEnvelope;
|
|
29
|
+
export declare function buildClaudeFinalMessageId(input: {
|
|
30
|
+
agentId: string;
|
|
31
|
+
conversationId: string;
|
|
32
|
+
turnKey: string;
|
|
33
|
+
}): string;
|
|
34
|
+
export declare function shouldDeliverClaudeFinal(input: {
|
|
35
|
+
turn: ClaudeInputEnvelope | null;
|
|
36
|
+
finalizedTurnKeys: ReadonlySet<string>;
|
|
37
|
+
interruptedTurnKeys: ReadonlySet<string>;
|
|
38
|
+
}): boolean;
|
|
39
|
+
export declare function mergeClaudeModelOptions(input: {
|
|
40
|
+
previous: ReadonlyArray<ModelOption>;
|
|
41
|
+
next: ReadonlyArray<ModelOption>;
|
|
42
|
+
selectedModel?: string | null;
|
|
43
|
+
}): ModelOption[];
|
|
44
|
+
export declare function formatClaudeControlError(error: unknown): string;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
export function createClaudeInputEnvelope(input) {
|
|
3
|
+
const sourceMessageId = input.sourceMessageId ?? null;
|
|
4
|
+
const fallbackId = randomUUID();
|
|
5
|
+
return {
|
|
6
|
+
kind: input.kind,
|
|
7
|
+
msg: input.msg,
|
|
8
|
+
intent: input.intent ?? 'queue',
|
|
9
|
+
sourceMessageId,
|
|
10
|
+
markAccepted: Boolean(input.markAccepted),
|
|
11
|
+
turnKey: input.kind === 'canon' && sourceMessageId
|
|
12
|
+
? `canon:${sourceMessageId}`
|
|
13
|
+
: `${input.kind}:${fallbackId}`,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function buildClaudeFinalMessageId(input) {
|
|
17
|
+
const digest = createHash('sha256')
|
|
18
|
+
.update(`${input.agentId}\n${input.conversationId}\n${input.turnKey}`)
|
|
19
|
+
.digest('hex')
|
|
20
|
+
.slice(0, 32);
|
|
21
|
+
return `claude-final-${digest}`;
|
|
22
|
+
}
|
|
23
|
+
export function shouldDeliverClaudeFinal(input) {
|
|
24
|
+
if (!input.turn || input.turn.kind !== 'canon')
|
|
25
|
+
return false;
|
|
26
|
+
if (input.finalizedTurnKeys.has(input.turn.turnKey))
|
|
27
|
+
return false;
|
|
28
|
+
if (input.interruptedTurnKeys.has(input.turn.turnKey))
|
|
29
|
+
return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
export function mergeClaudeModelOptions(input) {
|
|
33
|
+
const byValue = new Map();
|
|
34
|
+
for (const option of input.previous) {
|
|
35
|
+
byValue.set(option.value, { ...option });
|
|
36
|
+
}
|
|
37
|
+
for (const option of input.next) {
|
|
38
|
+
byValue.set(option.value, { ...option });
|
|
39
|
+
}
|
|
40
|
+
if (input.selectedModel && !byValue.has(input.selectedModel)) {
|
|
41
|
+
byValue.set(input.selectedModel, {
|
|
42
|
+
value: input.selectedModel,
|
|
43
|
+
label: input.selectedModel,
|
|
44
|
+
description: 'Selected for this Canon session but not returned by the latest Claude model probe.',
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const nextValues = new Set(input.next.map((option) => option.value));
|
|
48
|
+
return [...byValue.values()].map((option) => {
|
|
49
|
+
if (nextValues.has(option.value))
|
|
50
|
+
return option;
|
|
51
|
+
return {
|
|
52
|
+
...option,
|
|
53
|
+
description: option.description
|
|
54
|
+
? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
|
|
55
|
+
: 'Previously advertised; not returned by the latest Claude model probe.',
|
|
56
|
+
};
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export function formatClaudeControlError(error) {
|
|
60
|
+
if (error instanceof Error && error.message.trim()) {
|
|
61
|
+
return error.message.trim();
|
|
62
|
+
}
|
|
63
|
+
if (typeof error === 'string' && error.trim()) {
|
|
64
|
+
return error.trim();
|
|
65
|
+
}
|
|
66
|
+
return 'Claude Code did not apply that control.';
|
|
67
|
+
}
|
package/dist/setup.js
CHANGED
|
@@ -10,6 +10,17 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
|
10
10
|
import { join, dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { runCli } from './cli-entry.js';
|
|
13
|
+
const HELP = `canon-setup — install Claude Code Canon skills and print MCP setup
|
|
14
|
+
|
|
15
|
+
USAGE
|
|
16
|
+
canon-setup
|
|
17
|
+
|
|
18
|
+
FLAGS
|
|
19
|
+
--help, -h Show this help
|
|
20
|
+
--version, -V Show package version
|
|
21
|
+
|
|
22
|
+
This command writes Canon Claude Code skill files under ~/.claude/skills and
|
|
23
|
+
prints the MCP server configuration for Claude Code channel mode.`;
|
|
13
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
25
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
15
26
|
const skillsDir = join(home, '.claude', 'skills');
|
|
@@ -70,4 +81,7 @@ export function main() {
|
|
|
70
81
|
runCli(import.meta.url, main, (error) => {
|
|
71
82
|
console.error('[canon-setup] Fatal error:', error);
|
|
72
83
|
process.exit(1);
|
|
84
|
+
}, {
|
|
85
|
+
name: 'canon-setup',
|
|
86
|
+
help: HELP,
|
|
73
87
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.6",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "^0.2.101",
|
|
34
34
|
"@canonmsg/agent-sdk": "^1.1.0",
|
|
35
|
-
"@canonmsg/core": "^0.15.
|
|
35
|
+
"@canonmsg/core": "^0.15.2",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.12.0"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|