@canonmsg/claude-code-plugin 0.32.0 → 0.34.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.
@@ -0,0 +1,42 @@
1
+ import type { AgentReplyAuthorityV1 } from '@canonmsg/backend-contracts';
2
+ export interface InboundReplySource {
3
+ conversationId: string;
4
+ sourceMessageId: string;
5
+ replyAuthority?: AgentReplyAuthorityV1;
6
+ }
7
+ export interface PendingInboundReplySource extends InboundReplySource {
8
+ readonly generation: number;
9
+ }
10
+ export declare class InboundReplyAuthorityError extends Error {
11
+ constructor();
12
+ }
13
+ /**
14
+ * One-shot authority for Claude channel replies.
15
+ *
16
+ * Every inbound message first supersedes the previous source. The caller then
17
+ * activates that exact source only after deciding the message is a real Claude
18
+ * turn (rather than, for example, an approval response consumed locally).
19
+ */
20
+ export declare class InboundReplyAuthority {
21
+ private readonly ttlMs;
22
+ private readonly now;
23
+ private generation;
24
+ private active;
25
+ constructor(ttlMs?: number, now?: () => number);
26
+ /** Supersede any prior authority before filtering the new inbound message. */
27
+ beginInbound(source: InboundReplySource): PendingInboundReplySource;
28
+ /** Activate a pending source only if no newer inbound has superseded it. */
29
+ authorize(source: PendingInboundReplySource): boolean;
30
+ /** Revoke an exact source without disturbing a newer inbound message. */
31
+ revoke(source: PendingInboundReplySource): void;
32
+ /** Validate a non-final in-turn action without consuming reply authority. */
33
+ validate(source: InboundReplySource): boolean;
34
+ /** Consume the current source when the model deliberately chooses silence. */
35
+ consumeNoReply(source: InboundReplySource): boolean;
36
+ /**
37
+ * Atomically claim one visible reply. A failed operation restores the claim
38
+ * only while the same inbound is still current and unexpired.
39
+ */
40
+ sendVisibleReply<T>(source: InboundReplySource, operation: (replyAuthority: AgentReplyAuthorityV1 | undefined) => Promise<T>): Promise<T>;
41
+ private getLiveActive;
42
+ }
@@ -0,0 +1,107 @@
1
+ const DEFAULT_REPLY_AUTHORITY_TTL_MS = 15 * 60 * 1000;
2
+ export class InboundReplyAuthorityError extends Error {
3
+ constructor() {
4
+ super('This reply is not authorized for the current inbound Canon message.');
5
+ this.name = 'InboundReplyAuthorityError';
6
+ }
7
+ }
8
+ function validSource(source) {
9
+ return source.conversationId.length > 0 && source.sourceMessageId.length > 0;
10
+ }
11
+ function sameSource(a, b) {
12
+ return a.conversationId === b.conversationId
13
+ && a.sourceMessageId === b.sourceMessageId;
14
+ }
15
+ /**
16
+ * One-shot authority for Claude channel replies.
17
+ *
18
+ * Every inbound message first supersedes the previous source. The caller then
19
+ * activates that exact source only after deciding the message is a real Claude
20
+ * turn (rather than, for example, an approval response consumed locally).
21
+ */
22
+ export class InboundReplyAuthority {
23
+ ttlMs;
24
+ now;
25
+ generation = 0;
26
+ active = null;
27
+ constructor(ttlMs = DEFAULT_REPLY_AUTHORITY_TTL_MS, now = Date.now) {
28
+ this.ttlMs = ttlMs;
29
+ this.now = now;
30
+ if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
31
+ throw new TypeError('Reply authority TTL must be positive.');
32
+ }
33
+ }
34
+ /** Supersede any prior authority before filtering the new inbound message. */
35
+ beginInbound(source) {
36
+ if (!validSource(source)) {
37
+ throw new TypeError('Inbound reply authority requires a conversation and source message.');
38
+ }
39
+ this.generation += 1;
40
+ this.active = null;
41
+ return { ...source, generation: this.generation };
42
+ }
43
+ /** Activate a pending source only if no newer inbound has superseded it. */
44
+ authorize(source) {
45
+ if (source.generation !== this.generation)
46
+ return false;
47
+ this.active = {
48
+ ...source,
49
+ expiresAt: this.now() + this.ttlMs,
50
+ claimed: false,
51
+ };
52
+ return true;
53
+ }
54
+ /** Revoke an exact source without disturbing a newer inbound message. */
55
+ revoke(source) {
56
+ if (this.active?.generation === source.generation && sameSource(this.active, source)) {
57
+ this.active = null;
58
+ }
59
+ }
60
+ /** Validate a non-final in-turn action without consuming reply authority. */
61
+ validate(source) {
62
+ const active = this.getLiveActive();
63
+ return active !== null && sameSource(active, source);
64
+ }
65
+ /** Consume the current source when the model deliberately chooses silence. */
66
+ consumeNoReply(source) {
67
+ const active = this.getLiveActive();
68
+ if (!active || active.claimed || !sameSource(active, source))
69
+ return false;
70
+ this.active = null;
71
+ return true;
72
+ }
73
+ /**
74
+ * Atomically claim one visible reply. A failed operation restores the claim
75
+ * only while the same inbound is still current and unexpired.
76
+ */
77
+ async sendVisibleReply(source, operation) {
78
+ const active = this.getLiveActive();
79
+ if (!active || active.claimed || !sameSource(active, source)) {
80
+ throw new InboundReplyAuthorityError();
81
+ }
82
+ active.claimed = true;
83
+ try {
84
+ const result = await operation(active.replyAuthority);
85
+ if (this.active === active)
86
+ this.active = null;
87
+ return result;
88
+ }
89
+ catch (error) {
90
+ if (this.active === active) {
91
+ if (this.now() < active.expiresAt) {
92
+ active.claimed = false;
93
+ }
94
+ else {
95
+ this.active = null;
96
+ }
97
+ }
98
+ throw error;
99
+ }
100
+ }
101
+ getLiveActive() {
102
+ if (this.active && this.now() >= this.active.expiresAt) {
103
+ this.active = null;
104
+ }
105
+ return this.active;
106
+ }
107
+ }
@@ -8,11 +8,13 @@ export type McpParseResult<T> = {
8
8
  type ContentType = 'text' | 'image' | 'audio' | 'video' | 'file';
9
9
  export interface ReplyArgs {
10
10
  conversationId: string;
11
+ sourceMessageId: string;
11
12
  text: string;
12
13
  }
13
14
  export declare function parseReplyArgs(value: unknown): McpParseResult<ReplyArgs>;
14
15
  export interface SendMessageArgs {
15
16
  conversationId: string;
17
+ sourceMessageId: string;
16
18
  text: string;
17
19
  contentType: ContentType;
18
20
  imageUrl?: string;
@@ -27,6 +29,7 @@ export interface SendMessageArgs {
27
29
  export declare function parseSendMessageArgs(value: unknown): McpParseResult<SendMessageArgs>;
28
30
  export interface SetTypingArgs {
29
31
  conversationId: string;
32
+ sourceMessageId: string;
30
33
  typing: boolean;
31
34
  status?: 'typing' | 'thinking';
32
35
  }
package/dist/mcp-args.js CHANGED
@@ -42,6 +42,9 @@ export function parseReplyArgs(value) {
42
42
  const conversationId = requiredString(value, 'conversation_id', 256);
43
43
  if (!conversationId.ok)
44
44
  return conversationId;
45
+ const sourceMessageId = requiredString(value, 'source_message_id', 256);
46
+ if (!sourceMessageId.ok)
47
+ return sourceMessageId;
45
48
  const text = requiredString(value, 'text');
46
49
  if (!text.ok)
47
50
  return text;
@@ -49,6 +52,7 @@ export function parseReplyArgs(value) {
49
52
  ok: true,
50
53
  value: {
51
54
  conversationId: conversationId.value,
55
+ sourceMessageId: sourceMessageId.value,
52
56
  text: text.value,
53
57
  },
54
58
  };
@@ -59,6 +63,9 @@ export function parseSendMessageArgs(value) {
59
63
  const conversationId = requiredString(value, 'conversation_id', 256);
60
64
  if (!conversationId.ok)
61
65
  return conversationId;
66
+ const sourceMessageId = requiredString(value, 'source_message_id', 256);
67
+ if (!sourceMessageId.ok)
68
+ return sourceMessageId;
62
69
  const contentType = parseContentType(value.content_type);
63
70
  if (!contentType.ok)
64
71
  return contentType;
@@ -74,6 +81,7 @@ export function parseSendMessageArgs(value) {
74
81
  ok: true,
75
82
  value: {
76
83
  conversationId: conversationId.value,
84
+ sourceMessageId: sourceMessageId.value,
77
85
  text: optionalString(value, 'text') ?? '',
78
86
  contentType: contentType.value,
79
87
  ...(imageUrl ? { imageUrl } : {}),
@@ -93,6 +101,9 @@ export function parseSetTypingArgs(value) {
93
101
  const conversationId = requiredString(value, 'conversation_id', 256);
94
102
  if (!conversationId.ok)
95
103
  return conversationId;
104
+ const sourceMessageId = requiredString(value, 'source_message_id', 256);
105
+ if (!sourceMessageId.ok)
106
+ return sourceMessageId;
96
107
  if (typeof value.typing !== 'boolean') {
97
108
  return { ok: false, error: 'typing must be a boolean' };
98
109
  }
@@ -104,6 +115,7 @@ export function parseSetTypingArgs(value) {
104
115
  ok: true,
105
116
  value: {
106
117
  conversationId: conversationId.value,
118
+ sourceMessageId: sourceMessageId.value,
107
119
  typing: value.typing,
108
120
  ...(status ? { status } : {}),
109
121
  },
package/dist/register.js CHANGED
@@ -21,6 +21,7 @@ REQUIRED
21
21
 
22
22
  FLAGS
23
23
  --profile <name> Local profile name in ~/.canon/agents.json
24
+ --agent-id <id> Reconnect this exact transferred agent identity
24
25
  --environment <id> Canon environment ID (or CANON_ENVIRONMENT_ID; default: canon-prod-v1)
25
26
  --base-url <url> Canon API base URL override
26
27
  --stream-url <url> Canon stream URL override
@@ -32,12 +33,12 @@ FLAGS
32
33
  EXAMPLES
33
34
  canon-register --name "My Claude" --description "Claude Code agent" --phone "+15551234567"
34
35
  canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
36
+ canon-register --name "Sold Agent" --description "Existing agent" --phone "+15551234567" --agent-id <id>
35
37
 
36
38
  After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
37
39
  const OPTIONS = {
38
40
  moduleUrl: import.meta.url,
39
41
  clientType: 'claude-code',
40
- sessionSetupPolicy: 'runtime_descriptor_required',
41
42
  cliName: 'canon-register',
42
43
  hostBinName: 'canon-claude',
43
44
  developerInfo: 'Claude Code plugin',