@canonmsg/claude-code-plugin 0.21.2 → 0.23.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,59 @@
1
+ /**
2
+ * Startup recovery for inbound messages missed while the host was offline.
3
+ *
4
+ * The host persists a `lastInboundMessageId` cursor per conversation. On
5
+ * startup we paginate `getMessagesPage` (newest-first pages, older pages via
6
+ * its `before` message-id parameter) until the cursor is found or a hard
7
+ * per-conversation bound is hit, then replay everything after the cursor.
8
+ *
9
+ * This module is intentionally identical in packages/claude-code-plugin and
10
+ * packages/codex-plugin — keep both copies in sync (future consolidation
11
+ * candidate).
12
+ */
13
+ export const STARTUP_RECOVERY_PAGE_SIZE = 25;
14
+ export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
15
+ export async function collectMissedInboundMessages(input) {
16
+ const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
17
+ const newestPage = await input.fetchPage();
18
+ const collected = [...newestPage.messages];
19
+ const seenIds = new Set(collected.map((message) => message.id));
20
+ const hasCursor = (messages) => input.cursor != null && messages.some((message) => message.id === input.cursor);
21
+ let cursorFound = hasCursor(collected);
22
+ if (input.cursor != null) {
23
+ while (!cursorFound && collected.length < maxMessages) {
24
+ // Pages are newest-first, so the last collected message is the oldest.
25
+ const before = collected[collected.length - 1]?.id;
26
+ if (!before)
27
+ break;
28
+ const page = await input.fetchPage(before);
29
+ const fresh = page.messages.filter((message) => !seenIds.has(message.id));
30
+ // No pagination progress (history exhausted, or the server ignored the
31
+ // `before` cursor because that message was hard-deleted) — stop here.
32
+ if (fresh.length === 0)
33
+ break;
34
+ for (const message of fresh)
35
+ seenIds.add(message.id);
36
+ collected.push(...fresh);
37
+ cursorFound = hasCursor(fresh);
38
+ }
39
+ }
40
+ const ascending = [...collected].sort((a, b) => String(a.createdAt ?? '').localeCompare(String(b.createdAt ?? '')));
41
+ const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId);
42
+ let mode;
43
+ let missed;
44
+ if (cursorFound) {
45
+ const cursorIndex = ascending.findIndex((message) => message.id === input.cursor);
46
+ mode = 'after-cursor';
47
+ missed = inboundOnly(ascending.slice(cursorIndex + 1));
48
+ }
49
+ else if (input.cursor != null && collected.length >= maxMessages) {
50
+ mode = 'truncated-window';
51
+ missed = inboundOnly(ascending.slice(-maxMessages));
52
+ }
53
+ else {
54
+ mode = 'latest-only';
55
+ missed = inboundOnly(ascending).slice(-1);
56
+ }
57
+ // Safe: `missed` only holds elements of pages returned by `fetchPage`.
58
+ return { mode, messages: missed, newestPage };
59
+ }
@@ -0,0 +1,16 @@
1
+ export interface StreamingBufferOptions {
2
+ throttleMs: number;
3
+ getText: () => string;
4
+ setText: (text: string) => void;
5
+ getTimer: () => ReturnType<typeof setTimeout> | null;
6
+ setTimer: (timer: ReturnType<typeof setTimeout> | null) => void;
7
+ onWrite: (text: string) => void;
8
+ schedule?: typeof setTimeout;
9
+ cancel?: typeof clearTimeout;
10
+ }
11
+ export declare function createStreamingBuffer(options: StreamingBufferOptions): {
12
+ append(delta: string): void;
13
+ flush: () => void;
14
+ clear(): void;
15
+ replace(text: string): void;
16
+ };
@@ -0,0 +1,52 @@
1
+ export function createStreamingBuffer(options) {
2
+ const schedule = options.schedule ?? setTimeout;
3
+ const cancel = options.cancel ?? clearTimeout;
4
+ const flush = () => {
5
+ const timer = options.getTimer();
6
+ if (timer) {
7
+ cancel(timer);
8
+ options.setTimer(null);
9
+ }
10
+ const text = options.getText();
11
+ if (text) {
12
+ options.onWrite(text);
13
+ }
14
+ };
15
+ return {
16
+ append(delta) {
17
+ if (!delta)
18
+ return;
19
+ options.setText(`${options.getText()}${delta}`);
20
+ if (options.getTimer())
21
+ return;
22
+ const timer = schedule(() => {
23
+ options.setTimer(null);
24
+ const text = options.getText();
25
+ if (text) {
26
+ options.onWrite(text);
27
+ }
28
+ }, options.throttleMs);
29
+ options.setTimer(timer);
30
+ },
31
+ flush,
32
+ clear() {
33
+ const timer = options.getTimer();
34
+ if (timer) {
35
+ cancel(timer);
36
+ options.setTimer(null);
37
+ }
38
+ options.setText('');
39
+ },
40
+ replace(text) {
41
+ const timer = options.getTimer();
42
+ if (timer) {
43
+ cancel(timer);
44
+ options.setTimer(null);
45
+ }
46
+ options.setText(text);
47
+ if (text) {
48
+ options.onWrite(text);
49
+ }
50
+ },
51
+ };
52
+ }
@@ -1,5 +1,4 @@
1
- import type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core/contract';
2
- import type { ApprovalManager } from '@canonmsg/core/host';
1
+ import type { ApprovalManager, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core';
3
2
  export type ClaudeToolPolicyDecision = {
4
3
  behavior: 'allow';
5
4
  } | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.21.2",
3
+ "version": "0.23.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",
@@ -23,19 +23,17 @@
23
23
  ".claude-plugin"
24
24
  ],
25
25
  "scripts": {
26
- "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../../packages/core ../../packages/agent-sdk ../../packages/framework ../../packages/bridge ../../packages/agent-host",
26
+ "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
27
27
  "build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
28
28
  "dev": "npm run prepare:workspace-deps && tsc --watch",
29
29
  "prepack": "npm run build",
30
30
  "test": "vitest run"
31
31
  },
32
32
  "dependencies": {
33
- "@anthropic-ai/claude-agent-sdk": "^0.3.200",
34
- "@canonmsg/agent-host": "^0.4.0",
35
- "@canonmsg/agent-sdk": "^3.4.4",
36
- "@canonmsg/bridge": "^0.2.3",
37
- "@canonmsg/core": "^3.2.1",
38
- "@canonmsg/framework": "^0.2.0",
33
+ "@anthropic-ai/claude-agent-sdk": "^0.2.141",
34
+ "@canonmsg/agent-sdk": "^5.0.0",
35
+ "@canonmsg/coding-agent-host": "^0.2.2",
36
+ "@canonmsg/core": "^4.0.0",
39
37
  "@modelcontextprotocol/sdk": "^1.29.0"
40
38
  },
41
39
  "engines": {
@@ -52,9 +50,9 @@
52
50
  "repository": {
53
51
  "type": "git",
54
52
  "url": "https://github.com/HeyBobChan/canon",
55
- "directory": "adapters/claude-code-plugin"
53
+ "directory": "packages/claude-code-plugin"
56
54
  },
57
- "homepage": "https://github.com/HeyBobChan/canon/tree/main/adapters/claude-code-plugin",
55
+ "homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/claude-code-plugin",
58
56
  "publishConfig": {
59
57
  "access": "public"
60
58
  },
@@ -1,14 +0,0 @@
1
- /**
2
- * Packaged canon-bridge daemon resolution (bridge plan Phase 6, §F3).
3
- *
4
- * On a fresh npm install nothing has populated ~/.canon/bin yet, so
5
- * connectBridge()'s fallback chain (explicit binPath → $CANON_BRIDGE_BIN →
6
- * the shared install) has nothing to spawn. The plugin therefore ships the
7
- * daemon as its own @canonmsg/bridge dependency and passes the packaged
8
- * dist entry as binPath.
9
- *
10
- * $CANON_BRIDGE_BIN still wins: when the env override is set we return
11
- * undefined and let connectBridge()'s own env resolution take it (an
12
- * options.binPath would out-rank the env var otherwise).
13
- */
14
- export declare function resolvePackagedBridgeBin(env?: NodeJS.ProcessEnv): string | undefined;
@@ -1,27 +0,0 @@
1
- /**
2
- * Packaged canon-bridge daemon resolution (bridge plan Phase 6, §F3).
3
- *
4
- * On a fresh npm install nothing has populated ~/.canon/bin yet, so
5
- * connectBridge()'s fallback chain (explicit binPath → $CANON_BRIDGE_BIN →
6
- * the shared install) has nothing to spawn. The plugin therefore ships the
7
- * daemon as its own @canonmsg/bridge dependency and passes the packaged
8
- * dist entry as binPath.
9
- *
10
- * $CANON_BRIDGE_BIN still wins: when the env override is set we return
11
- * undefined and let connectBridge()'s own env resolution take it (an
12
- * options.binPath would out-rank the env var otherwise).
13
- */
14
- import { createRequire } from 'node:module';
15
- import { dirname, join as joinPath } from 'node:path';
16
- export function resolvePackagedBridgeBin(env = process.env) {
17
- if (env.CANON_BRIDGE_BIN)
18
- return undefined;
19
- try {
20
- const require = createRequire(import.meta.url);
21
- const manifestPath = require.resolve('@canonmsg/bridge/package.json');
22
- return joinPath(dirname(manifestPath), 'dist', 'main.js');
23
- }
24
- catch {
25
- return undefined;
26
- }
27
- }
@@ -1,35 +0,0 @@
1
- import type { AgentDefinition, EffortLevel, ThinkingConfig } from '@anthropic-ai/claude-agent-sdk';
2
- export type ClaudeThinkingMode = 'adaptive' | 'off';
3
- export type ClaudeUltracodeMode = 'on' | 'off';
4
- export type ClaudeEffortLevel = Extract<EffortLevel, 'low' | 'medium' | 'high' | 'xhigh' | 'max'>;
5
- export interface ClaudeSessionDefaults {
6
- model?: string;
7
- effort?: ClaudeEffortLevel;
8
- thinking?: ClaudeThinkingMode;
9
- ultracode?: ClaudeUltracodeMode;
10
- fallbackModel?: string;
11
- agentProgressSummaries?: boolean;
12
- agents?: Record<string, AgentDefinition>;
13
- }
14
- export interface ClaudeSessionControlValues {
15
- thinking?: ClaudeThinkingMode;
16
- ultracode?: ClaudeUltracodeMode;
17
- }
18
- export interface ClaudeEffectiveSessionSettings extends ClaudeSessionDefaults {
19
- runtimeControlValues: Record<string, string>;
20
- }
21
- export declare const CLAUDE_EFFORT_LEVELS: readonly ClaudeEffortLevel[];
22
- export declare function parseClaudeThinkingMode(value: unknown): ClaudeThinkingMode | undefined;
23
- export declare function parseClaudeUltracodeMode(value: unknown): ClaudeUltracodeMode | undefined;
24
- export declare function parseClaudeSessionDefaults(raw: unknown): ClaudeSessionDefaults;
25
- export declare function parseClaudeSessionControlValues(values: Record<string, string> | null | undefined): ClaudeSessionControlValues;
26
- export declare function claudeThinkingToConfig(mode: ClaudeThinkingMode | undefined): ThinkingConfig | undefined;
27
- export declare function claudeThinkingToFlagValue(mode: ClaudeThinkingMode): boolean;
28
- export declare function resolveEffectiveSessionSettings(input: {
29
- profileDefaults?: ClaudeSessionDefaults;
30
- sessionConfig?: {
31
- model?: string;
32
- effort?: ClaudeEffortLevel;
33
- runtimeControlValues?: Record<string, string>;
34
- } | null;
35
- }): ClaudeEffectiveSessionSettings;
@@ -1,124 +0,0 @@
1
- export const CLAUDE_EFFORT_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max'];
2
- const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_LEVELS);
3
- function optionalString(value) {
4
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
5
- }
6
- function optionalBoolean(value) {
7
- return typeof value === 'boolean' ? value : undefined;
8
- }
9
- export function parseClaudeThinkingMode(value) {
10
- if (value === false || value === 'off' || value === 'disabled')
11
- return 'off';
12
- if (value === true || value === 'on' || value === 'adaptive' || value === 'enabled')
13
- return 'adaptive';
14
- return undefined;
15
- }
16
- export function parseClaudeUltracodeMode(value) {
17
- if (value === true || value === 'on' || value === 'enabled')
18
- return 'on';
19
- if (value === false || value === 'off' || value === 'disabled')
20
- return 'off';
21
- return undefined;
22
- }
23
- function parseEffort(value) {
24
- const normalized = optionalString(value);
25
- if (!normalized)
26
- return undefined;
27
- return CLAUDE_EFFORT_VALUES.has(normalized)
28
- ? normalized
29
- : undefined;
30
- }
31
- function parseStringArray(value) {
32
- if (!Array.isArray(value))
33
- return undefined;
34
- const out = value
35
- .map((entry) => optionalString(entry))
36
- .filter((entry) => Boolean(entry));
37
- return out.length > 0 ? out : undefined;
38
- }
39
- function parseAgentDefinition(value) {
40
- if (!value || typeof value !== 'object' || Array.isArray(value))
41
- return null;
42
- const data = value;
43
- const description = optionalString(data.description);
44
- const prompt = optionalString(data.prompt);
45
- if (!description || !prompt)
46
- return null;
47
- return {
48
- description,
49
- prompt,
50
- ...(parseStringArray(data.tools) ? { tools: parseStringArray(data.tools) } : {}),
51
- ...(parseStringArray(data.disallowedTools) ? { disallowedTools: parseStringArray(data.disallowedTools) } : {}),
52
- ...(optionalString(data.model) ? { model: optionalString(data.model) } : {}),
53
- ...(parseStringArray(data.skills) ? { skills: parseStringArray(data.skills) } : {}),
54
- ...(typeof data.maxTurns === 'number' && Number.isFinite(data.maxTurns) && data.maxTurns > 0
55
- ? { maxTurns: Math.floor(data.maxTurns) }
56
- : {}),
57
- ...(typeof data.background === 'boolean' ? { background: data.background } : {}),
58
- ...(parseEffort(data.effort) ? { effort: parseEffort(data.effort) } : {}),
59
- };
60
- }
61
- function parseAgents(value) {
62
- if (!value || typeof value !== 'object' || Array.isArray(value))
63
- return undefined;
64
- const out = {};
65
- for (const [rawName, rawAgent] of Object.entries(value)) {
66
- const name = rawName.trim();
67
- if (!name)
68
- continue;
69
- const agent = parseAgentDefinition(rawAgent);
70
- if (agent)
71
- out[name] = agent;
72
- }
73
- return Object.keys(out).length > 0 ? out : undefined;
74
- }
75
- export function parseClaudeSessionDefaults(raw) {
76
- if (!raw || typeof raw !== 'object' || Array.isArray(raw))
77
- return {};
78
- const data = raw;
79
- return {
80
- ...(optionalString(data.model) ? { model: optionalString(data.model) } : {}),
81
- ...(parseEffort(data.effort) ? { effort: parseEffort(data.effort) } : {}),
82
- ...(parseClaudeThinkingMode(data.thinking) ? { thinking: parseClaudeThinkingMode(data.thinking) } : {}),
83
- ...(parseClaudeUltracodeMode(data.ultracode) ? { ultracode: parseClaudeUltracodeMode(data.ultracode) } : {}),
84
- ...(optionalString(data.fallbackModel) ? { fallbackModel: optionalString(data.fallbackModel) } : {}),
85
- ...(optionalBoolean(data.agentProgressSummaries) !== undefined
86
- ? { agentProgressSummaries: optionalBoolean(data.agentProgressSummaries) }
87
- : {}),
88
- ...(parseAgents(data.agents) ? { agents: parseAgents(data.agents) } : {}),
89
- };
90
- }
91
- export function parseClaudeSessionControlValues(values) {
92
- return {
93
- ...(parseClaudeThinkingMode(values?.thinking) ? { thinking: parseClaudeThinkingMode(values?.thinking) } : {}),
94
- ...(parseClaudeUltracodeMode(values?.ultracode) ? { ultracode: parseClaudeUltracodeMode(values?.ultracode) } : {}),
95
- };
96
- }
97
- export function claudeThinkingToConfig(mode) {
98
- if (mode === 'adaptive')
99
- return { type: 'adaptive' };
100
- if (mode === 'off')
101
- return { type: 'disabled' };
102
- return undefined;
103
- }
104
- export function claudeThinkingToFlagValue(mode) {
105
- return mode !== 'off';
106
- }
107
- export function resolveEffectiveSessionSettings(input) {
108
- const controlValues = parseClaudeSessionControlValues(input.sessionConfig?.runtimeControlValues);
109
- const runtimeControlValues = {
110
- ...(input.profileDefaults?.thinking ? { thinking: input.profileDefaults.thinking } : {}),
111
- ...(input.profileDefaults?.ultracode ? { ultracode: input.profileDefaults.ultracode } : {}),
112
- ...(input.sessionConfig?.runtimeControlValues ?? {}),
113
- ...(controlValues.thinking ? { thinking: controlValues.thinking } : {}),
114
- ...(controlValues.ultracode ? { ultracode: controlValues.ultracode } : {}),
115
- };
116
- return {
117
- ...(input.profileDefaults ?? {}),
118
- ...(input.sessionConfig?.model ? { model: input.sessionConfig.model } : {}),
119
- ...(input.sessionConfig?.effort ? { effort: input.sessionConfig.effort } : {}),
120
- ...(controlValues.thinking ? { thinking: controlValues.thinking } : {}),
121
- ...(controlValues.ultracode ? { ultracode: controlValues.ultracode } : {}),
122
- runtimeControlValues,
123
- };
124
- }
@@ -1,11 +0,0 @@
1
- import type { SDKTaskNotificationMessage, SDKTaskProgressMessage, SDKTaskStartedMessage, SDKTaskUpdatedMessage } from '@anthropic-ai/claude-agent-sdk';
2
- type ClaudeTaskMessage = SDKTaskNotificationMessage | SDKTaskProgressMessage | SDKTaskStartedMessage | SDKTaskUpdatedMessage;
3
- export interface ClaudeSubagentActivityBlock {
4
- id: string;
5
- kind: 'status';
6
- status: 'running' | 'completed' | 'failed';
7
- title: string;
8
- text?: string;
9
- }
10
- export declare function mapClaudeSubagentActivity(message: ClaudeTaskMessage): ClaudeSubagentActivityBlock | null;
11
- export {};
@@ -1,51 +0,0 @@
1
- function titleForMessage(message) {
2
- const subtype = message.subtype;
3
- if (subtype === 'task_started') {
4
- return message.subagent_type
5
- ? `Subagent started: ${message.subagent_type}`
6
- : 'Subagent started';
7
- }
8
- if (subtype === 'task_progress') {
9
- return message.subagent_type
10
- ? `Subagent progress: ${message.subagent_type}`
11
- : 'Subagent progress';
12
- }
13
- if (subtype === 'task_updated')
14
- return 'Subagent updated';
15
- return `Subagent ${message.status}`;
16
- }
17
- function textForMessage(message) {
18
- if (message.subtype === 'task_started')
19
- return message.description;
20
- if (message.subtype === 'task_progress')
21
- return message.summary ?? message.description;
22
- if (message.subtype === 'task_updated')
23
- return message.patch.description ?? message.patch.error;
24
- return message.summary;
25
- }
26
- function statusForMessage(message) {
27
- if (message.subtype === 'task_notification') {
28
- if (message.status === 'completed')
29
- return 'completed';
30
- return 'failed';
31
- }
32
- if (message.subtype === 'task_updated') {
33
- if (message.patch.status === 'completed')
34
- return 'completed';
35
- if (message.patch.status === 'failed' || message.patch.status === 'killed')
36
- return 'failed';
37
- }
38
- return 'running';
39
- }
40
- export function mapClaudeSubagentActivity(message) {
41
- if ('skip_transcript' in message && message.skip_transcript)
42
- return null;
43
- const text = textForMessage(message);
44
- return {
45
- id: `task:${message.task_id}`,
46
- kind: 'status',
47
- status: statusForMessage(message),
48
- title: titleForMessage(message),
49
- ...(text ? { text } : {}),
50
- };
51
- }