@canonmsg/claude-code-plugin 0.22.0 → 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.
- package/.claude-plugin/plugin.json +25 -0
- package/README.md +18 -2
- package/dist/approval-diff.d.ts +1 -1
- package/dist/approval-diff.js +1 -1
- package/dist/approval-server.d.ts +33 -0
- package/dist/approval-server.js +200 -0
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -1
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1051 -697
- package/dist/inbound-policy.d.ts +22 -0
- package/dist/inbound-policy.js +46 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp-args.d.ts +45 -0
- package/dist/mcp-args.js +179 -0
- package/dist/register.js +25 -73
- package/dist/server.d.ts +2 -0
- package/dist/server.js +699 -0
- package/dist/session-state.d.ts +6 -54
- package/dist/session-state.js +17 -166
- package/dist/setup.d.ts +3 -2
- package/dist/setup.js +29 -5
- package/dist/startup-recovery.d.ts +46 -0
- package/dist/startup-recovery.js +59 -0
- package/dist/streaming-buffer.d.ts +16 -0
- package/dist/streaming-buffer.js +52 -0
- package/dist/tool-policy.d.ts +1 -2
- package/package.json +12 -12
- package/skills/configure/SKILL.md +10 -4
- package/dist/bridge-bin.d.ts +0 -14
- package/dist/bridge-bin.js +0 -27
- package/dist/session-defaults.d.ts +0 -35
- package/dist/session-defaults.js +0 -124
- package/dist/subagent-activity.d.ts +0 -11
- package/dist/subagent-activity.js +0 -51
package/dist/session-state.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core
|
|
3
|
-
import type { TurnArtifactSnapshot } from '@canonmsg/agent-host';
|
|
4
|
-
import { type ClaudeEffortLevel } from './session-defaults.js';
|
|
5
|
-
export type { ClaudeEffortLevel } from './session-defaults.js';
|
|
1
|
+
import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
+
import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core';
|
|
3
|
+
import type { TurnArtifactSnapshot } from '@canonmsg/coding-agent-host';
|
|
6
4
|
export type ClaudeInputKind = 'seed' | 'canon';
|
|
7
5
|
export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
|
|
8
6
|
export interface ClaudeInputEnvelope {
|
|
@@ -21,46 +19,6 @@ export interface ClaudeRuntimeControlError {
|
|
|
21
19
|
message: string;
|
|
22
20
|
updatedAt: number;
|
|
23
21
|
}
|
|
24
|
-
export type ClaudePermissionMode = Parameters<Query['setPermissionMode']>[0];
|
|
25
|
-
export interface ClaudeSessionControl {
|
|
26
|
-
model?: string;
|
|
27
|
-
permissionMode?: string;
|
|
28
|
-
effort?: string;
|
|
29
|
-
runtimeControlValues?: Record<string, string>;
|
|
30
|
-
updatedAt?: number;
|
|
31
|
-
updatedBy?: string;
|
|
32
|
-
}
|
|
33
|
-
export interface ClaudeHostSessionState {
|
|
34
|
-
model?: string;
|
|
35
|
-
permissionMode?: string;
|
|
36
|
-
effort?: string;
|
|
37
|
-
fallbackModel?: string;
|
|
38
|
-
runtimeControlValues?: Record<string, string>;
|
|
39
|
-
state?: 'idle' | 'running' | 'requires_action';
|
|
40
|
-
contextUsage?: {
|
|
41
|
-
percentage: number;
|
|
42
|
-
totalTokens: number;
|
|
43
|
-
maxTokens: number;
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
export declare function parseClaudePermissionMode(value: unknown): ClaudePermissionMode | null;
|
|
47
|
-
export declare function normalizeClaudeEffortLevel(value: unknown): ClaudeEffortLevel | undefined;
|
|
48
|
-
export declare function buildClaudeInitialSessionState(input: {
|
|
49
|
-
model?: string;
|
|
50
|
-
permissionMode?: string;
|
|
51
|
-
effort?: string;
|
|
52
|
-
fallbackModel?: string;
|
|
53
|
-
runtimeControlValues?: Record<string, string>;
|
|
54
|
-
}): ClaudeHostSessionState;
|
|
55
|
-
export interface ClaudeSessionControlTarget {
|
|
56
|
-
state: ClaudeHostSessionState;
|
|
57
|
-
closed: boolean;
|
|
58
|
-
query: Pick<Query, 'setModel' | 'setPermissionMode' | 'applyFlagSettings'>;
|
|
59
|
-
writeState: () => void;
|
|
60
|
-
setRuntimeControlError: (controlId: string, value: string, error: unknown) => void;
|
|
61
|
-
clearRuntimeControlError: (controlId: string) => void;
|
|
62
|
-
}
|
|
63
|
-
export declare function applyClaudeSessionControl(target: ClaudeSessionControlTarget, ctrl: ClaudeSessionControl): void;
|
|
64
22
|
export interface ClaudePendingFinalDelivery {
|
|
65
23
|
turnKey: string;
|
|
66
24
|
text: string;
|
|
@@ -102,15 +60,9 @@ export declare function shouldDeliverClaudeFinal(input: {
|
|
|
102
60
|
interruptedTurnKeys: ReadonlySet<string>;
|
|
103
61
|
}): boolean;
|
|
104
62
|
export declare function resetClaudeCompletedTurnState(session: ClaudeCompletedTurnState): void;
|
|
105
|
-
export
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
description?: string;
|
|
109
|
-
}
|
|
110
|
-
export declare function claudeModelInfoToOption(model: ClaudeModelInfoLike): ModelOption;
|
|
111
|
-
export declare function isClaudeCustomModelOption(option: ModelOption | null | undefined): boolean;
|
|
112
|
-
export declare function resolveClaudeModelOptions(input: {
|
|
113
|
-
discovered: ReadonlyArray<ModelOption>;
|
|
63
|
+
export declare function mergeClaudeModelOptions(input: {
|
|
64
|
+
previous: ReadonlyArray<ModelOption>;
|
|
65
|
+
next: ReadonlyArray<ModelOption>;
|
|
114
66
|
selectedModel?: string | null;
|
|
115
67
|
}): ModelOption[];
|
|
116
68
|
export declare function formatClaudeControlError(error: unknown): string;
|
package/dist/session-state.js
CHANGED
|
@@ -1,140 +1,4 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import { CLAUDE_EFFORT_LEVELS, claudeThinkingToFlagValue, parseClaudeThinkingMode, parseClaudeUltracodeMode, } from './session-defaults.js';
|
|
3
|
-
const CLAUDE_PERMISSION_MODE_VALUES = new Set([
|
|
4
|
-
'acceptEdits',
|
|
5
|
-
'auto',
|
|
6
|
-
'bypassPermissions',
|
|
7
|
-
'default',
|
|
8
|
-
'dontAsk',
|
|
9
|
-
'plan',
|
|
10
|
-
]);
|
|
11
|
-
const CLAUDE_EFFORT_VALUES = new Set(CLAUDE_EFFORT_LEVELS);
|
|
12
|
-
export function parseClaudePermissionMode(value) {
|
|
13
|
-
return typeof value === 'string' && CLAUDE_PERMISSION_MODE_VALUES.has(value)
|
|
14
|
-
? value
|
|
15
|
-
: null;
|
|
16
|
-
}
|
|
17
|
-
export function normalizeClaudeEffortLevel(value) {
|
|
18
|
-
return typeof value === 'string' && CLAUDE_EFFORT_VALUES.has(value)
|
|
19
|
-
? value
|
|
20
|
-
: undefined;
|
|
21
|
-
}
|
|
22
|
-
export function buildClaudeInitialSessionState(input) {
|
|
23
|
-
return {
|
|
24
|
-
...(input.model ? { model: input.model } : {}),
|
|
25
|
-
...(input.permissionMode ? { permissionMode: input.permissionMode } : {}),
|
|
26
|
-
...(input.effort ? { effort: input.effort } : {}),
|
|
27
|
-
...(input.fallbackModel ? { fallbackModel: input.fallbackModel } : {}),
|
|
28
|
-
...(input.runtimeControlValues ? { runtimeControlValues: input.runtimeControlValues } : {}),
|
|
29
|
-
state: 'idle',
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Apply a control to the Claude SDK. W2-A2 deleted the seq-guarded optimistic
|
|
34
|
-
* revert machinery (commit c356bdf8) that masked the 2–11s control poll: the
|
|
35
|
-
* bridge is event-push now (~100ms), so the state is already published by the
|
|
36
|
-
* caller's writeState() and the SDK apply is fired without a revert dance. A
|
|
37
|
-
* failed apply surfaces as a runtime-control error (the display truth the app
|
|
38
|
-
* reads) — the same shape codex uses. `value` is the requested value for the
|
|
39
|
-
* error record; `controlId` scopes the error.
|
|
40
|
-
*/
|
|
41
|
-
function applyControl(input) {
|
|
42
|
-
// Fire the SDK apply SYNCHRONOUSLY (an async setter may still resolve later);
|
|
43
|
-
// wrap so a synchronous throw is caught in the same rejection path.
|
|
44
|
-
void (async () => input.apply())().catch((error) => {
|
|
45
|
-
if (input.target.closed)
|
|
46
|
-
return;
|
|
47
|
-
input.target.setRuntimeControlError(input.controlId, input.value, error);
|
|
48
|
-
input.target.writeState();
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
function setRuntimeControlValue(state, controlId, value) {
|
|
52
|
-
state.runtimeControlValues = {
|
|
53
|
-
...(state.runtimeControlValues ?? {}),
|
|
54
|
-
[controlId]: value,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
export function applyClaudeSessionControl(target, ctrl) {
|
|
58
|
-
const nextModel = typeof ctrl.model === 'string' && ctrl.model.trim() ? ctrl.model.trim() : null;
|
|
59
|
-
if (ctrl.model !== undefined && !nextModel) {
|
|
60
|
-
target.setRuntimeControlError('model', String(ctrl.model), new Error('Unsupported Claude model.'));
|
|
61
|
-
}
|
|
62
|
-
else if (nextModel && nextModel !== target.state.model) {
|
|
63
|
-
target.state.model = nextModel;
|
|
64
|
-
target.clearRuntimeControlError('model');
|
|
65
|
-
applyControl({
|
|
66
|
-
target,
|
|
67
|
-
controlId: 'model',
|
|
68
|
-
value: nextModel,
|
|
69
|
-
apply: () => target.query.setModel(nextModel),
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
const nextPermissionMode = parseClaudePermissionMode(ctrl.permissionMode);
|
|
73
|
-
if (ctrl.permissionMode !== undefined && !nextPermissionMode) {
|
|
74
|
-
target.setRuntimeControlError('permissionMode', ctrl.permissionMode, new Error(`Unsupported Claude permission mode: ${ctrl.permissionMode}`));
|
|
75
|
-
}
|
|
76
|
-
else if (nextPermissionMode && nextPermissionMode !== target.state.permissionMode) {
|
|
77
|
-
// Claude Code applies permission-mode changes asynchronously; publish the
|
|
78
|
-
// requested value immediately (writeState below) and surface an error if the
|
|
79
|
-
// apply fails.
|
|
80
|
-
target.state.permissionMode = nextPermissionMode;
|
|
81
|
-
target.clearRuntimeControlError('permissionMode');
|
|
82
|
-
applyControl({
|
|
83
|
-
target,
|
|
84
|
-
controlId: 'permissionMode',
|
|
85
|
-
value: nextPermissionMode,
|
|
86
|
-
apply: () => target.query.setPermissionMode(nextPermissionMode),
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
const nextEffort = normalizeClaudeEffortLevel(ctrl.effort);
|
|
90
|
-
if (nextEffort && nextEffort !== target.state.effort) {
|
|
91
|
-
target.state.effort = nextEffort;
|
|
92
|
-
target.clearRuntimeControlError('effort');
|
|
93
|
-
applyControl({
|
|
94
|
-
target,
|
|
95
|
-
controlId: 'effort',
|
|
96
|
-
value: nextEffort,
|
|
97
|
-
apply: () => target.query.applyFlagSettings({ effortLevel: nextEffort }),
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
else if (ctrl.effort !== undefined && !nextEffort) {
|
|
101
|
-
target.setRuntimeControlError('effort', String(ctrl.effort), new Error('Unsupported Claude thinking level.'));
|
|
102
|
-
}
|
|
103
|
-
const runtimeControlValues = ctrl.runtimeControlValues ?? {};
|
|
104
|
-
if ('thinking' in runtimeControlValues) {
|
|
105
|
-
const nextThinking = parseClaudeThinkingMode(runtimeControlValues.thinking);
|
|
106
|
-
if (!nextThinking) {
|
|
107
|
-
target.setRuntimeControlError('thinking', String(runtimeControlValues.thinking), new Error('Unsupported Claude thinking mode.'));
|
|
108
|
-
}
|
|
109
|
-
else if (nextThinking !== target.state.runtimeControlValues?.thinking) {
|
|
110
|
-
setRuntimeControlValue(target.state, 'thinking', nextThinking);
|
|
111
|
-
target.clearRuntimeControlError('thinking');
|
|
112
|
-
applyControl({
|
|
113
|
-
target,
|
|
114
|
-
controlId: 'thinking',
|
|
115
|
-
value: nextThinking,
|
|
116
|
-
apply: () => target.query.applyFlagSettings({ alwaysThinkingEnabled: claudeThinkingToFlagValue(nextThinking) }),
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
if ('ultracode' in runtimeControlValues) {
|
|
121
|
-
const nextUltracode = parseClaudeUltracodeMode(runtimeControlValues.ultracode);
|
|
122
|
-
if (!nextUltracode) {
|
|
123
|
-
target.setRuntimeControlError('ultracode', String(runtimeControlValues.ultracode), new Error('Unsupported Claude ultracode mode.'));
|
|
124
|
-
}
|
|
125
|
-
else if (nextUltracode !== target.state.runtimeControlValues?.ultracode) {
|
|
126
|
-
setRuntimeControlValue(target.state, 'ultracode', nextUltracode);
|
|
127
|
-
target.clearRuntimeControlError('ultracode');
|
|
128
|
-
applyControl({
|
|
129
|
-
target,
|
|
130
|
-
controlId: 'ultracode',
|
|
131
|
-
value: nextUltracode,
|
|
132
|
-
apply: () => target.query.applyFlagSettings({ ultracode: nextUltracode === 'on' }),
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
target.writeState();
|
|
137
|
-
}
|
|
138
2
|
export function createClaudeInputEnvelope(input) {
|
|
139
3
|
const sourceMessageId = input.sourceMessageId ?? null;
|
|
140
4
|
const fallbackId = randomUUID();
|
|
@@ -182,45 +46,32 @@ export function resetClaudeCompletedTurnState(session) {
|
|
|
182
46
|
session.toolInProgress = false;
|
|
183
47
|
session.turnState = 'idle';
|
|
184
48
|
}
|
|
185
|
-
function
|
|
186
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
187
|
-
}
|
|
188
|
-
function displayLabelWithClaudeVersion(input) {
|
|
189
|
-
const label = input.displayName.trim() || input.value;
|
|
190
|
-
const description = normalizeModelDescription(input.description);
|
|
191
|
-
if (!description || label.includes('(') || /\d/.test(label))
|
|
192
|
-
return label;
|
|
193
|
-
const [summary] = description.split('·');
|
|
194
|
-
const versionedLabel = summary?.trim();
|
|
195
|
-
return versionedLabel
|
|
196
|
-
&& versionedLabel.toLowerCase().startsWith(label.toLowerCase())
|
|
197
|
-
&& /\d/.test(versionedLabel)
|
|
198
|
-
? versionedLabel
|
|
199
|
-
: label;
|
|
200
|
-
}
|
|
201
|
-
export function claudeModelInfoToOption(model) {
|
|
202
|
-
return {
|
|
203
|
-
value: model.value,
|
|
204
|
-
label: displayLabelWithClaudeVersion(model),
|
|
205
|
-
...(normalizeModelDescription(model.description) ? { description: normalizeModelDescription(model.description) } : {}),
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
export function isClaudeCustomModelOption(option) {
|
|
209
|
-
return option?.description?.trim().toLowerCase() === 'custom model';
|
|
210
|
-
}
|
|
211
|
-
export function resolveClaudeModelOptions(input) {
|
|
49
|
+
export function mergeClaudeModelOptions(input) {
|
|
212
50
|
const byValue = new Map();
|
|
213
|
-
for (const option of input.
|
|
51
|
+
for (const option of input.previous) {
|
|
52
|
+
byValue.set(option.value, { ...option });
|
|
53
|
+
}
|
|
54
|
+
for (const option of input.next) {
|
|
214
55
|
byValue.set(option.value, { ...option });
|
|
215
56
|
}
|
|
216
57
|
if (input.selectedModel && !byValue.has(input.selectedModel)) {
|
|
217
58
|
byValue.set(input.selectedModel, {
|
|
218
59
|
value: input.selectedModel,
|
|
219
60
|
label: input.selectedModel,
|
|
220
|
-
description: 'Selected for this Canon session but not returned by Claude model
|
|
61
|
+
description: 'Selected for this Canon session but not returned by the latest Claude model probe.',
|
|
221
62
|
});
|
|
222
63
|
}
|
|
223
|
-
|
|
64
|
+
const nextValues = new Set(input.next.map((option) => option.value));
|
|
65
|
+
return [...byValue.values()].map((option) => {
|
|
66
|
+
if (nextValues.has(option.value))
|
|
67
|
+
return option;
|
|
68
|
+
return {
|
|
69
|
+
...option,
|
|
70
|
+
description: option.description
|
|
71
|
+
? `${option.description} Previously advertised; not returned by the latest Claude model probe.`
|
|
72
|
+
: 'Previously advertised; not returned by the latest Claude model probe.',
|
|
73
|
+
};
|
|
74
|
+
});
|
|
224
75
|
}
|
|
225
76
|
export function formatClaudeControlError(error) {
|
|
226
77
|
if (error instanceof Error && error.message.trim()) {
|
package/dist/setup.d.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Setup script for the Canon Claude Code plugin.
|
|
4
4
|
*
|
|
5
|
-
* Run after `npm install -g @canonmsg/claude-code-plugin` to
|
|
6
|
-
* /canon-register and /canon-configure skills
|
|
5
|
+
* Run after `npm install -g @canonmsg/claude-code-plugin` to:
|
|
6
|
+
* 1. Install the /canon-register and /canon-configure skills
|
|
7
|
+
* 2. Show the user how to add the MCP server to their .mcp.json
|
|
7
8
|
*/
|
|
8
9
|
export declare function main(): void;
|
package/dist/setup.js
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Setup script for the Canon Claude Code plugin.
|
|
4
4
|
*
|
|
5
|
-
* Run after `npm install -g @canonmsg/claude-code-plugin` to
|
|
6
|
-
* /canon-register and /canon-configure skills
|
|
5
|
+
* Run after `npm install -g @canonmsg/claude-code-plugin` to:
|
|
6
|
+
* 1. Install the /canon-register and /canon-configure skills
|
|
7
|
+
* 2. Show the user how to add the MCP server to their .mcp.json
|
|
7
8
|
*/
|
|
8
9
|
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
9
10
|
import { join, dirname } from 'node:path';
|
|
10
11
|
import { fileURLToPath } from 'node:url';
|
|
11
12
|
import { runCli } from './cli-entry.js';
|
|
12
|
-
const HELP = `canon-setup — install Claude Code Canon skills
|
|
13
|
+
const HELP = `canon-setup — install Claude Code Canon skills and print MCP setup
|
|
13
14
|
|
|
14
15
|
USAGE
|
|
15
16
|
canon-setup
|
|
@@ -18,7 +19,8 @@ FLAGS
|
|
|
18
19
|
--help, -h Show this help
|
|
19
20
|
--version, -V Show package version
|
|
20
21
|
|
|
21
|
-
This command writes Canon Claude Code skill files under ~/.claude/skills
|
|
22
|
+
This command writes Canon Claude Code skill files under ~/.claude/skills and
|
|
23
|
+
prints the MCP server configuration for Claude Code channel mode.`;
|
|
22
24
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
25
|
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
24
26
|
const skillsDir = join(home, '.claude', 'skills');
|
|
@@ -43,6 +45,27 @@ function installSkills() {
|
|
|
43
45
|
console.log(` Installed /canon-${skill}`);
|
|
44
46
|
}
|
|
45
47
|
}
|
|
48
|
+
// ── Show MCP config instructions ───────────────────────────────────────
|
|
49
|
+
function showMcpInstructions() {
|
|
50
|
+
// Find the installed server.js path
|
|
51
|
+
const serverPath = join(__dirname, 'server.js');
|
|
52
|
+
console.log(`
|
|
53
|
+
Add this to your project's .mcp.json (or ~/.mcp.json for global):
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
"mcpServers": {
|
|
57
|
+
"canon-channel": {
|
|
58
|
+
"command": "node",
|
|
59
|
+
"args": ["${serverPath}"]
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Then start Claude Code with:
|
|
65
|
+
|
|
66
|
+
claude --dangerously-load-development-channels server:canon-channel
|
|
67
|
+
`);
|
|
68
|
+
}
|
|
46
69
|
// ── Main ───────────────────────────────────────────────────────────────
|
|
47
70
|
export function main() {
|
|
48
71
|
console.log('Canon Claude Code Plugin Setup');
|
|
@@ -52,7 +75,8 @@ export function main() {
|
|
|
52
75
|
console.log('\nRegistering your agent...');
|
|
53
76
|
console.log(' Run /canon-register in Claude Code to register a new agent');
|
|
54
77
|
console.log(' Run /canon-configure if you already have an API key\n');
|
|
55
|
-
console.log('
|
|
78
|
+
console.log('MCP server configuration:');
|
|
79
|
+
showMcpInstructions();
|
|
56
80
|
}
|
|
57
81
|
runCli(import.meta.url, main, (error) => {
|
|
58
82
|
console.error('[canon-setup] Fatal error:', error);
|
|
@@ -0,0 +1,46 @@
|
|
|
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 declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
14
|
+
export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
15
|
+
export interface StartupRecoveryMessage {
|
|
16
|
+
id: string;
|
|
17
|
+
senderId: string;
|
|
18
|
+
createdAt?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface StartupRecoveryPage {
|
|
21
|
+
messages: StartupRecoveryMessage[];
|
|
22
|
+
}
|
|
23
|
+
export type StartupRecoveryMode =
|
|
24
|
+
/** Cursor found — `messages` is everything strictly after it. */
|
|
25
|
+
'after-cursor'
|
|
26
|
+
/** Cursor present but not found within the bound — `messages` is the bounded recent window. */
|
|
27
|
+
| 'truncated-window'
|
|
28
|
+
/**
|
|
29
|
+
* No usable cursor (fresh runtime file, or the cursor message no longer
|
|
30
|
+
* exists in history) — only the newest inbound message is recovered, since
|
|
31
|
+
* a full-history replay could fire mass duplicate turns.
|
|
32
|
+
*/
|
|
33
|
+
| 'latest-only';
|
|
34
|
+
export interface StartupRecoveryResult<TPage extends StartupRecoveryPage> {
|
|
35
|
+
mode: StartupRecoveryMode;
|
|
36
|
+
/** Missed inbound messages (own messages excluded), oldest first. */
|
|
37
|
+
messages: TPage['messages'];
|
|
38
|
+
/** First page fetched — reusable as hydration context for recovered turns. */
|
|
39
|
+
newestPage: TPage;
|
|
40
|
+
}
|
|
41
|
+
export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
|
|
42
|
+
fetchPage: (before?: string) => Promise<TPage>;
|
|
43
|
+
cursor: string | null | undefined;
|
|
44
|
+
agentId: string;
|
|
45
|
+
maxMessages?: number;
|
|
46
|
+
}): Promise<StartupRecoveryResult<TPage>>;
|
|
@@ -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
|
+
}
|
package/dist/tool-policy.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Canon
|
|
3
|
+
"version": "0.23.0",
|
|
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",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -12,28 +12,28 @@
|
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
14
|
"bin": {
|
|
15
|
+
"canon-channel-server": "dist/server.js",
|
|
15
16
|
"canon-claude": "dist/host.js",
|
|
16
17
|
"canon-register": "dist/register.js",
|
|
17
18
|
"canon-setup": "dist/setup.js"
|
|
18
19
|
},
|
|
19
20
|
"files": [
|
|
20
21
|
"dist",
|
|
21
|
-
"skills"
|
|
22
|
+
"skills",
|
|
23
|
+
".claude-plugin"
|
|
22
24
|
],
|
|
23
25
|
"scripts": {
|
|
24
|
-
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs
|
|
26
|
+
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
|
|
25
27
|
"build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
26
28
|
"dev": "npm run prepare:workspace-deps && tsc --watch",
|
|
27
29
|
"prepack": "npm run build",
|
|
28
30
|
"test": "vitest run"
|
|
29
31
|
},
|
|
30
32
|
"dependencies": {
|
|
31
|
-
"@anthropic-ai/claude-agent-sdk": "^0.
|
|
32
|
-
"@canonmsg/agent-
|
|
33
|
-
"@canonmsg/agent-
|
|
34
|
-
"@canonmsg/
|
|
35
|
-
"@canonmsg/core": "^3.3.0",
|
|
36
|
-
"@canonmsg/framework": "^0.3.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",
|
|
37
37
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"repository": {
|
|
51
51
|
"type": "git",
|
|
52
52
|
"url": "https://github.com/HeyBobChan/canon",
|
|
53
|
-
"directory": "
|
|
53
|
+
"directory": "packages/claude-code-plugin"
|
|
54
54
|
},
|
|
55
|
-
"homepage": "https://github.com/HeyBobChan/canon/tree/main/
|
|
55
|
+
"homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/claude-code-plugin",
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
|
58
58
|
},
|
|
@@ -36,9 +36,15 @@ Add an existing Canon agent API key as a named profile.
|
|
|
36
36
|
|
|
37
37
|
5. Write the updated profiles to `~/.canon/agents.json`.
|
|
38
38
|
|
|
39
|
-
6. Tell the user: **"API key saved as profile '<profile>'.
|
|
39
|
+
6. Tell the user: **"API key saved as profile '<profile>'. Restart Claude Code to connect."**
|
|
40
40
|
|
|
41
|
-
If they have multiple agents and want to pin one,
|
|
42
|
-
```
|
|
43
|
-
|
|
41
|
+
If they have multiple agents and want to pin one, add to `.mcp.json`:
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"mcpServers": {
|
|
45
|
+
"canon-channel": {
|
|
46
|
+
"env": { "CANON_AGENT": "<profile>" }
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
44
50
|
```
|
package/dist/bridge-bin.d.ts
DELETED
|
@@ -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;
|
package/dist/bridge-bin.js
DELETED
|
@@ -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
|
-
}
|