@canonmsg/claude-code-plugin 0.29.1 → 0.29.2
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 +1 -1
- package/dist/approval-server.d.ts +0 -1
- package/dist/approval-server.js +4 -18
- package/dist/host.js +17 -35
- package/dist/register.d.ts +9 -0
- package/dist/register.js +13 -113
- package/dist/server.js +6 -5
- package/dist/setup.js +4 -6
- package/package.json +6 -6
- package/dist/cli-entry.d.ts +0 -2
- package/dist/cli-entry.js +0 -1
- package/dist/inbound-policy.d.ts +0 -22
- package/dist/inbound-policy.js +0 -46
- package/dist/startup-recovery.d.ts +0 -54
- package/dist/startup-recovery.js +0 -102
- package/dist/streaming-buffer.d.ts +0 -16
- package/dist/streaming-buffer.js +0 -52
package/dist/approval-server.js
CHANGED
|
@@ -131,8 +131,9 @@ export class ApprovalHttpServer {
|
|
|
131
131
|
}
|
|
132
132
|
const toolName = payload.tool_name ?? 'Unknown';
|
|
133
133
|
const toolInput = payload.tool_input ?? {};
|
|
134
|
-
//
|
|
135
|
-
const
|
|
134
|
+
// Single source of truth for risk, shared with host mode (tool-policy.ts).
|
|
135
|
+
const risk = classifyClaudeApprovalRisk(toolName, toolInput);
|
|
136
|
+
const riskLevel = risk === 'destructive' ? 'destructive' : 'normal';
|
|
136
137
|
const isOwnerTurn = this.getIsOwnerTurn();
|
|
137
138
|
// Find the conversation to send the approval to.
|
|
138
139
|
const conversationId = this.getConversationId();
|
|
@@ -154,7 +155,7 @@ export class ApprovalHttpServer {
|
|
|
154
155
|
try {
|
|
155
156
|
const result = await this.manager.requestApproval(conversationId, toolName, toolInput, {
|
|
156
157
|
riskLevel,
|
|
157
|
-
risk
|
|
158
|
+
risk,
|
|
158
159
|
category: classifyClaudeApprovalCategory(toolName),
|
|
159
160
|
native: {
|
|
160
161
|
runtime: 'claude-code',
|
|
@@ -182,19 +183,4 @@ export class ApprovalHttpServer {
|
|
|
182
183
|
},
|
|
183
184
|
}));
|
|
184
185
|
}
|
|
185
|
-
classifyRisk(toolName, toolInput) {
|
|
186
|
-
if (toolName !== 'Bash')
|
|
187
|
-
return 'normal';
|
|
188
|
-
const cmd = typeof toolInput.command === 'string' ? toolInput.command : '';
|
|
189
|
-
const destructivePatterns = [
|
|
190
|
-
/\brm\s+(-rf?|--force)/,
|
|
191
|
-
/\bgit\s+push\s+--force/,
|
|
192
|
-
/\bgit\s+reset\s+--hard/,
|
|
193
|
-
/\bdrop\s+(table|database)/i,
|
|
194
|
-
/\bformat\b/,
|
|
195
|
-
];
|
|
196
|
-
return destructivePatterns.some((p) => p.test(cmd))
|
|
197
|
-
? 'destructive'
|
|
198
|
-
: 'normal';
|
|
199
|
-
}
|
|
200
186
|
}
|
package/dist/host.js
CHANGED
|
@@ -27,16 +27,14 @@ import { existsSync } from 'node:fs';
|
|
|
27
27
|
import { readFile } from 'node:fs/promises';
|
|
28
28
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
29
29
|
import { isAnthropicImageAttachment, materializeMessageMedia, materializeReplyContextMedia, sendMediaFileMessage, toAnthropicImageBlock, } from '@canonmsg/agent-sdk';
|
|
30
|
-
import { captureTurnArtifactSnapshot, collectTurnArtifacts, IDLE_TIMEOUT_MS, } from '@canonmsg/coding-agent-host';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import { runCli } from './cli-entry.js';
|
|
30
|
+
import { captureTurnArtifactSnapshot, collectTurnArtifacts, IDLE_TIMEOUT_MS, collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from '@canonmsg/coding-agent-host';
|
|
31
|
+
import { EFFORT_OPTIONS, CLAUDE_PERMISSION_MODE_OPTIONS, RUNTIME_NEW_SESSION_ACTION, RUNTIME_STOP_ACTION, RUNTIME_STOP_AND_DROP_ACTION, buildCanonInboundFrameV1, buildCanonTurnContextV2, buildConfiguredWorkspaceOptionsWithRoots, buildConversationEnvironmentKey, buildFirstPartyCodingRuntimeDescriptor, buildHydratedInboundContext, diffCanonMemberIds, buildPublicWorkspaceRoots, buildPublicWorkspaceOptions, buildRuntimePresentationPolicy, normalizeRuntimeCommandDescriptors, DEFAULT_FIRST_PARTY_RUNTIME_PRESENTATION, buildBoundedTurnTrail, createConversationMetadataLoader, createTurnOutputController, createRuntimeStatePublisher, createTypingStatusPublisher, EXECUTION_ENVIRONMENT_MODES, CanonClient, CanonStream, ControlChannelPoller, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, DEFAULT_RUNTIME_CAPABILITIES, HOST_ADMISSION_ACTIONS_DISABLED, ApprovalManager, RuntimeRequestManager, ExecutionEnvironmentError, FINAL_MESSAGE_HANDOFF_MS, buildLocalRuntimeId, getActiveProfileLock, heartbeatLocalRuntimeEntry, normalizeTurnMetadata, prepareConversationEnvironment, resolveCanonAgent, verifyResolvedAgentEnvironment, decideAutoReply, initRTDBAuth, isChunkedSendMessageError, sendMessageWithRetry, sendMessageWithRetryChunked, loadHostSessionConfig, loadRuntimeSessionState, markLocalRuntimeStopped, releaseConversationEnvironment, saveRuntimeSessionState, clearRuntimeSessionState, publishHostAgentRuntime, publishHostSessionSnapshots, renderCanonHostInboundContent, renderCodingHostInboundPrompt, resolveHostWorkspaceCwd, shouldTriggerAgentTurn, upsertLocalRuntimeEntry, } from '@canonmsg/core';
|
|
32
|
+
import { runCli } from '@canonmsg/core';
|
|
34
33
|
import { CANON_VERB_MCP_SERVER_NAME, createCanonVerbMcpServer } from '@canonmsg/agent-tools';
|
|
35
34
|
import { synthesizeClaudeApprovalDiff } from './approval-diff.js';
|
|
36
35
|
import { decideClaudeToolPermissionForMode, parseAllowedNonOwnerClaudeTools, } from './tool-policy.js';
|
|
37
36
|
import { applyClaudeSessionControl, boundClaudeFinalMetadata, buildClaudeFinalChunkingOptions, buildClaudeFinalMessageId, buildClaudeFinalTurnMetadata, buildClaudePendingFinalDelivery, buildClaudeTurnFailureNotice, buildTruncatedFinalText, buildUndeliverableFinalNotice, canDrainClaudeQueuedInput, claudeFinalWillChunk, classifyFinalDeliveryFailure, claudeInputOwnsTurnSlot, createClaudeTurnActivityState, decideClaudeControlSignalAction, decideClaudeInboundDispatch, dispatchClaudeInput, isClaudeTurnSlotReserved, readClaudeFinalDeliveryResume, releaseClaudeTurnSlot, runClaudeExhaustedFinalDelivery, shouldApplyClaudeEchoedSessionState, shouldReleaseClaudeTurnSlot, isClaudeMainTurnMessage, openClaudeTurn, planClaudeAssistantTrail, planClaudeToolCallStart, planClaudeToolProgress, planClaudeToolResults, prepareTurnTrailForDelivery, rememberDispatchedClaudeInput, resetClaudeTurnActivityState, shouldOpenClaudeTurnOnRunning, isOpenClaudeTurnState, takeClaudeResultOwner, takeClaudeToolBlockIdByIndex, claudeModelInfoToOption, claudeOriginForCanonSender, composeClaudeFinalText, describeUndeliveredClaudeFinal, confirmClaudeInterrupt, createClaudeInputEnvelope, deriveClaudeSupplementalModelProbes, formatClaudeCliVersion, formatClaudeControlError, isClaudeCustomModelOption, isSupportedClaudeCliVersion, mergeClaudeDiscoveredModelOptions, parseClaudeCliVersion, resolveClaudeTurnResponseRouting, resolveClaudeModelOptions, resetClaudeCompletedTurnState, shouldDeliverClaudeFinal, MINIMUM_CLAUDE_CLI_VERSION, } from './session-state.js';
|
|
38
37
|
import { CLAUDE_SUPPORTED_DIALOG_KINDS, buildClaudeAskUserPermissionDenied, buildClaudeAskUserPermissionResult, createClaudeUserDialogCoordinator, parseClaudeAskUserDialog, parseClaudeAskUserToolInput, resolveClaudeUserDialogRequestId, } from './user-dialog.js';
|
|
39
|
-
import { collectMissedInboundMessages, createReconnectRecoveryGate, STARTUP_RECOVERY_MAX_MESSAGES, STARTUP_RECOVERY_PAGE_SIZE, } from './startup-recovery.js';
|
|
40
38
|
function parseRuntimeVisibilityPreset(value) {
|
|
41
39
|
return value === 'normal' || value === 'minimal' || value === 'full' ? value : undefined;
|
|
42
40
|
}
|
|
@@ -194,7 +192,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
194
192
|
label: 'Level',
|
|
195
193
|
kind: 'enum',
|
|
196
194
|
required: true,
|
|
197
|
-
choices: [...
|
|
195
|
+
choices: [...EFFORT_OPTIONS],
|
|
198
196
|
}],
|
|
199
197
|
dispatch: {
|
|
200
198
|
kind: 'control',
|
|
@@ -237,7 +235,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
237
235
|
executionModes: input.executionModes,
|
|
238
236
|
permissionModes: CLAUDE_PERMISSION_MODE_OPTIONS,
|
|
239
237
|
defaultPermissionMode: 'default',
|
|
240
|
-
effortOptions: [...
|
|
238
|
+
effortOptions: [...EFFORT_OPTIONS],
|
|
241
239
|
extraRuntimeControls: [{
|
|
242
240
|
id: 'ultracode',
|
|
243
241
|
label: 'Ultracode',
|
|
@@ -272,7 +270,7 @@ function buildClaudeRuntimeDescriptor(input) {
|
|
|
272
270
|
commands,
|
|
273
271
|
});
|
|
274
272
|
}
|
|
275
|
-
const CLAUDE_EFFORT_VALUES = new Set(
|
|
273
|
+
const CLAUDE_EFFORT_VALUES = new Set(EFFORT_OPTIONS.map((option) => option.value));
|
|
276
274
|
function parseClaudePermissionMode(value) {
|
|
277
275
|
if (value === 'acceptEdits'
|
|
278
276
|
|| value === 'auto'
|
|
@@ -376,13 +374,14 @@ function acceptClaudeCli(source, cliPath) {
|
|
|
376
374
|
function toModelOptions(models) {
|
|
377
375
|
return models.map(claudeModelInfoToOption);
|
|
378
376
|
}
|
|
379
|
-
async function publishAgentRuntime(agentId, runtime) {
|
|
380
|
-
await publishHostAgentRuntime(agentId, 'claude-code', runtime);
|
|
377
|
+
async function publishAgentRuntime(agentId, runtime, rtdb) {
|
|
378
|
+
await publishHostAgentRuntime(agentId, 'claude-code', runtime, rtdb);
|
|
381
379
|
}
|
|
382
|
-
async function loadSessionConfig(conversationId, agentId) {
|
|
380
|
+
async function loadSessionConfig(conversationId, agentId, rtdb) {
|
|
383
381
|
const config = await loadHostSessionConfig({
|
|
384
382
|
conversationId,
|
|
385
383
|
agentId,
|
|
384
|
+
rtdb,
|
|
386
385
|
extraStringFields: ['permissionMode', 'effort'],
|
|
387
386
|
});
|
|
388
387
|
return {
|
|
@@ -2415,6 +2414,7 @@ export async function main() {
|
|
|
2415
2414
|
agentId,
|
|
2416
2415
|
clientType: 'claude-code',
|
|
2417
2416
|
hostMode: true,
|
|
2417
|
+
rtdb,
|
|
2418
2418
|
});
|
|
2419
2419
|
let streamConnected = false;
|
|
2420
2420
|
const hostAvailableExecutionModes = [
|
|
@@ -2455,6 +2455,7 @@ export async function main() {
|
|
|
2455
2455
|
await publishHostSessionSnapshots({
|
|
2456
2456
|
conversationIds,
|
|
2457
2457
|
agentId,
|
|
2458
|
+
rtdb,
|
|
2458
2459
|
clientType: 'claude-code',
|
|
2459
2460
|
runtime: runtimeDescriptor,
|
|
2460
2461
|
workspaceOptions,
|
|
@@ -2499,7 +2500,7 @@ export async function main() {
|
|
|
2499
2500
|
console.error('[canon-host] Failed to refresh Claude runtime metadata:', error);
|
|
2500
2501
|
return runtimeMetadata;
|
|
2501
2502
|
});
|
|
2502
|
-
await publishAgentRuntime(agentId, runtimeDescriptor).catch((error) => {
|
|
2503
|
+
await publishAgentRuntime(agentId, runtimeDescriptor, rtdb).catch((error) => {
|
|
2503
2504
|
console.error('[canon-host] Failed to publish agent runtime:', error);
|
|
2504
2505
|
});
|
|
2505
2506
|
await publishSessionSnapshots(Array.from(knownConversationIds));
|
|
@@ -2860,7 +2861,7 @@ export async function main() {
|
|
|
2860
2861
|
evictOldestIdle();
|
|
2861
2862
|
}
|
|
2862
2863
|
const creation = (async () => {
|
|
2863
|
-
const config = await loadSessionConfig(conversationId, agentId);
|
|
2864
|
+
const config = await loadSessionConfig(conversationId, agentId, rtdb);
|
|
2864
2865
|
const sessionExecutionMode = resolveSessionExecutionMode(config);
|
|
2865
2866
|
const workspaceCwd = resolveWorkspaceCwd(config);
|
|
2866
2867
|
const environment = prepareConversationEnvironment({
|
|
@@ -3114,40 +3115,21 @@ export async function main() {
|
|
|
3114
3115
|
}
|
|
3115
3116
|
return consumed;
|
|
3116
3117
|
}
|
|
3117
|
-
function
|
|
3118
|
-
|
|
3118
|
+
function persistedConversationCursor(conversationId) {
|
|
3119
|
+
return loadRuntimeSessionState(runtimeId, {
|
|
3119
3120
|
conversationId,
|
|
3120
3121
|
baseCwd: workingDir,
|
|
3121
3122
|
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3122
3123
|
})?.lastInboundMessageId ?? null;
|
|
3123
|
-
if (canonicalCursor)
|
|
3124
|
-
return { canonicalCursor, legacyCursors: [] };
|
|
3125
|
-
const legacyCursors = Object.values(readLocalRuntimeEntry(runtimeId)?.sessions ?? {})
|
|
3126
|
-
.filter((state) => state.conversationId === conversationId)
|
|
3127
|
-
.map((state) => state.lastInboundMessageId)
|
|
3128
|
-
.filter((messageId) => Boolean(messageId));
|
|
3129
|
-
return { canonicalCursor: null, legacyCursors: [...new Set(legacyCursors)] };
|
|
3130
3124
|
}
|
|
3131
3125
|
async function performMissedInboundRecovery() {
|
|
3132
3126
|
for (const conversationId of knownConversationIds) {
|
|
3133
3127
|
try {
|
|
3134
|
-
const cursors = persistedConversationCursors(conversationId);
|
|
3135
3128
|
const recovered = await collectMissedInboundMessages({
|
|
3136
3129
|
fetchPage: (before) => client.getMessagesPage(conversationId, STARTUP_RECOVERY_PAGE_SIZE, before),
|
|
3137
|
-
cursor:
|
|
3138
|
-
cursorCandidates: cursors.legacyCursors,
|
|
3130
|
+
cursor: persistedConversationCursor(conversationId),
|
|
3139
3131
|
agentId,
|
|
3140
3132
|
});
|
|
3141
|
-
if (!cursors.canonicalCursor
|
|
3142
|
-
&& recovered.cursor
|
|
3143
|
-
&& recovered.messages.length === 0) {
|
|
3144
|
-
saveRuntimeSessionState(runtimeId, {
|
|
3145
|
-
conversationId,
|
|
3146
|
-
baseCwd: workingDir,
|
|
3147
|
-
workspaceId: CLAUDE_RECOVERY_CURSOR_WORKSPACE_ID,
|
|
3148
|
-
lastInboundMessageId: recovered.cursor,
|
|
3149
|
-
});
|
|
3150
|
-
}
|
|
3151
3133
|
if (recovered.mode === 'truncated-window') {
|
|
3152
3134
|
console.error(`[canon-host] [${conversationId.slice(0, 8)}] Recovery cursor was not found within ${STARTUP_RECOVERY_MAX_MESSAGES} messages; replaying the bounded recent window`);
|
|
3153
3135
|
}
|
package/dist/register.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* CLI script for Canon agent registration.
|
|
4
|
+
* Invoked by the /canon-register skill.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node register.js --name "My Agent" --description "What it does" --phone "+15551234567" [--profile "my-agent"]
|
|
8
|
+
*
|
|
9
|
+
* On approval, saves the agent to ~/.canon/agents.json under the profile name.
|
|
10
|
+
*/
|
|
2
11
|
export declare function main(): Promise<void>;
|
package/dist/register.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { setDefaultResultOrder } from 'node:dns';
|
|
3
2
|
/**
|
|
4
3
|
* CLI script for Canon agent registration.
|
|
5
4
|
* Invoked by the /canon-register skill.
|
|
@@ -9,9 +8,7 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
9
8
|
*
|
|
10
9
|
* On approval, saves the agent to ~/.canon/agents.json under the profile name.
|
|
11
10
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { parseArgs } from 'node:util';
|
|
14
|
-
import { runCli } from './cli-entry.js';
|
|
11
|
+
import { registerRegistrationCli, runRegistrationCli, } from '@canonmsg/core';
|
|
15
12
|
const HELP = `canon-register — register or reconnect a Claude Code agent in Canon
|
|
16
13
|
|
|
17
14
|
USAGE
|
|
@@ -37,114 +34,17 @@ EXAMPLES
|
|
|
37
34
|
canon-register --name "Frontend" --description "React work" --phone "+15551234567" --profile frontend
|
|
38
35
|
|
|
39
36
|
After approval, start it with CANON_AGENT=<profile> canon-claude --cwd /path/to/project.`;
|
|
37
|
+
const OPTIONS = {
|
|
38
|
+
moduleUrl: import.meta.url,
|
|
39
|
+
clientType: 'claude-code',
|
|
40
|
+
cliName: 'canon-register',
|
|
41
|
+
hostBinName: 'canon-claude',
|
|
42
|
+
developerInfo: 'Claude Code plugin',
|
|
43
|
+
registeringLabel: 'agent',
|
|
44
|
+
usage: 'Usage: node register.js --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]',
|
|
45
|
+
help: HELP,
|
|
46
|
+
};
|
|
40
47
|
export async function main() {
|
|
41
|
-
|
|
42
|
-
const { values } = parseArgs({
|
|
43
|
-
options: {
|
|
44
|
-
name: { type: 'string' },
|
|
45
|
-
description: { type: 'string' },
|
|
46
|
-
phone: { type: 'string' },
|
|
47
|
-
profile: { type: 'string' },
|
|
48
|
-
environment: { type: 'string' },
|
|
49
|
-
'base-url': { type: 'string' },
|
|
50
|
-
'stream-url': { type: 'string' },
|
|
51
|
-
'rtdb-url': { type: 'string' },
|
|
52
|
-
'firebase-api-key': { type: 'string' },
|
|
53
|
-
},
|
|
54
|
-
strict: true,
|
|
55
|
-
});
|
|
56
|
-
if (!values.name || !values.description || !values.phone) {
|
|
57
|
-
console.error('Usage: node register.js --name "Agent Name" --description "Description" --phone "+15551234567" [--profile "my-agent"]');
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
// Default profile name: sanitized agent name
|
|
61
|
-
const profileName = values.profile || values.name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-');
|
|
62
|
-
const environmentId = values.environment || process.env.CANON_ENVIRONMENT_ID;
|
|
63
|
-
if (!environmentId) {
|
|
64
|
-
throw new Error('--environment or CANON_ENVIRONMENT_ID is required');
|
|
65
|
-
}
|
|
66
|
-
const connection = resolveCanonRuntimeConnection({
|
|
67
|
-
environmentId,
|
|
68
|
-
apiBaseUrl: values['base-url'],
|
|
69
|
-
streamUrl: values['stream-url'],
|
|
70
|
-
rtdbUrl: values['rtdb-url'],
|
|
71
|
-
firebaseWebApiKey: values['firebase-api-key'],
|
|
72
|
-
});
|
|
73
|
-
await verifyCanonRuntimeConnection(connection);
|
|
74
|
-
const existingProfile = loadProfiles()[profileName];
|
|
75
|
-
const existingAgentId = existingProfile?.environmentId === connection.environmentId
|
|
76
|
-
? existingProfile.agentId
|
|
77
|
-
: undefined;
|
|
78
|
-
console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
|
|
79
|
-
const pending = getOrCreatePendingRegistration(profileName, 'claude-code', connection);
|
|
80
|
-
const result = await registerAndWaitForApproval({
|
|
81
|
-
name: values.name,
|
|
82
|
-
description: values.description,
|
|
83
|
-
ownerPhone: values.phone,
|
|
84
|
-
developerInfo: 'Claude Code plugin',
|
|
85
|
-
clientType: 'claude-code',
|
|
86
|
-
baseUrl: connection.apiBaseUrl,
|
|
87
|
-
requestedAgentId: existingAgentId,
|
|
88
|
-
localRegistrationId: pending.localRegistrationId,
|
|
89
|
-
}, {
|
|
90
|
-
onSubmitted: (requestId, pollToken) => {
|
|
91
|
-
updatePendingRegistration(profileName, {
|
|
92
|
-
requestId,
|
|
93
|
-
pollToken,
|
|
94
|
-
clientType: 'claude-code',
|
|
95
|
-
});
|
|
96
|
-
console.log(`Registration submitted (request ID: ${requestId}).`);
|
|
97
|
-
console.log('Waiting for approval in Canon app...');
|
|
98
|
-
},
|
|
99
|
-
onPollUpdate: () => {
|
|
100
|
-
process.stdout.write('.');
|
|
101
|
-
},
|
|
102
|
-
});
|
|
103
|
-
console.log(''); // newline after dots
|
|
104
|
-
switch (result.status) {
|
|
105
|
-
case 'approved': {
|
|
106
|
-
if (!result.apiKey || !result.agentId || !result.agentName) {
|
|
107
|
-
console.error('Approval completed but Canon did not return a usable API key. Run this command again to resume key pickup.');
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
upsertAgentProfile(profileName, {
|
|
111
|
-
apiKey: result.apiKey,
|
|
112
|
-
agentId: result.agentId,
|
|
113
|
-
agentName: result.agentName,
|
|
114
|
-
registeredAt: new Date().toISOString(),
|
|
115
|
-
environmentId: connection.environmentId,
|
|
116
|
-
baseUrl: connection.apiBaseUrl,
|
|
117
|
-
streamUrl: connection.streamUrl,
|
|
118
|
-
rtdbUrl: connection.rtdbUrl,
|
|
119
|
-
firebaseApiKey: connection.firebaseWebApiKey,
|
|
120
|
-
clientType: 'claude-code',
|
|
121
|
-
});
|
|
122
|
-
if (result.requestId) {
|
|
123
|
-
await ackRegistrationApproval(connection.apiBaseUrl, result.requestId, result.pollToken);
|
|
124
|
-
}
|
|
125
|
-
clearPendingRegistration(profileName);
|
|
126
|
-
console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
|
|
127
|
-
console.log(`Saved as profile "${profileName}" in ~/.canon/agents.json`);
|
|
128
|
-
console.log('Start it with: CANON_AGENT=' + profileName + ' canon-claude --cwd /path/to/project');
|
|
129
|
-
console.log('Keep that terminal open while you want Canon to reach the agent.');
|
|
130
|
-
console.log('Closing it, logging out, rebooting, or sleeping long enough to stop the process takes this local agent offline until you revive it.');
|
|
131
|
-
console.log('Docs: https://canonmail.com/agents/integrations');
|
|
132
|
-
break;
|
|
133
|
-
}
|
|
134
|
-
case 'rejected':
|
|
135
|
-
console.log('Registration was rejected.');
|
|
136
|
-
process.exit(1);
|
|
137
|
-
break;
|
|
138
|
-
case 'timeout':
|
|
139
|
-
console.log('Registration timed out (5 minutes). Try again later.');
|
|
140
|
-
process.exit(1);
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
48
|
+
await runRegistrationCli(OPTIONS);
|
|
143
49
|
}
|
|
144
|
-
|
|
145
|
-
console.error('[canon-register] Fatal error:', error);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
}, {
|
|
148
|
-
name: 'canon-register',
|
|
149
|
-
help: HELP,
|
|
150
|
-
});
|
|
50
|
+
registerRegistrationCli(OPTIONS);
|
package/dist/server.js
CHANGED
|
@@ -10,10 +10,10 @@ import { setDefaultResultOrder } from 'node:dns';
|
|
|
10
10
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
12
12
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
13
|
-
import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth,
|
|
13
|
+
import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, getActiveProfileLock, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, verifyResolvedAgentEnvironment, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, renderCanonHostInboundContent, } from '@canonmsg/core';
|
|
14
14
|
import { materializeMessageMedia, sendMediaFileMessage, } from '@canonmsg/agent-sdk';
|
|
15
15
|
import { ApprovalHttpServer } from './approval-server.js';
|
|
16
|
-
import { runCli } from '
|
|
16
|
+
import { runCli } from '@canonmsg/core';
|
|
17
17
|
import { parseReplyArgs, parseSendMessageArgs, parseSetTypingArgs, } from './mcp-args.js';
|
|
18
18
|
import { canonVerbToolDefinitions, executeCanonVerbTool, isCanonToolVerb, stampSendToTurnComplete, } from '@canonmsg/agent-tools';
|
|
19
19
|
const HELP = `canon-channel-server — Claude Code MCP channel server for Canon
|
|
@@ -39,6 +39,7 @@ let stream = null;
|
|
|
39
39
|
let agentContext = null;
|
|
40
40
|
let approvalManager = null;
|
|
41
41
|
let approvalServer = null;
|
|
42
|
+
let rtdb = null;
|
|
42
43
|
const conversationCache = new Map();
|
|
43
44
|
const TURN_COMPLETE_METADATA = {
|
|
44
45
|
turnSemantics: 'turn_complete',
|
|
@@ -486,7 +487,7 @@ async function startChannel() {
|
|
|
486
487
|
}
|
|
487
488
|
}
|
|
488
489
|
// Initialize RTDB auth for session state reporting
|
|
489
|
-
initRTDBAuth(client, {
|
|
490
|
+
rtdb = initRTDBAuth(client, {
|
|
490
491
|
rtdbUrl: resolvedRuntime.rtdbUrl,
|
|
491
492
|
firebaseApiKey: resolvedRuntime.firebaseApiKey,
|
|
492
493
|
});
|
|
@@ -524,7 +525,7 @@ async function startChannel() {
|
|
|
524
525
|
// Write initial session state
|
|
525
526
|
const initialState = { cwd: process.cwd(), isActive: true };
|
|
526
527
|
for (const convoId of conversationCache.keys()) {
|
|
527
|
-
writeSessionState(convoId, agentId, initialState).catch((err) => console.error(`[canon] Failed to write initial state for ${convoId}:`, err));
|
|
528
|
+
rtdb?.writeSessionState(convoId, agentId, initialState).catch((err) => console.error(`[canon] Failed to write initial state for ${convoId}:`, err));
|
|
528
529
|
}
|
|
529
530
|
// Start SSE stream
|
|
530
531
|
stream = new CanonStream({
|
|
@@ -580,7 +581,7 @@ export async function main() {
|
|
|
580
581
|
// Mark all conversations as inactive
|
|
581
582
|
if (agentContext) {
|
|
582
583
|
for (const convoId of conversationCache.keys()) {
|
|
583
|
-
clearSessionState(convoId, agentContext.agentId).catch(() => { });
|
|
584
|
+
rtdb?.clearSessionState(convoId, agentContext.agentId).catch(() => { });
|
|
584
585
|
}
|
|
585
586
|
}
|
|
586
587
|
approvalManager?.dispose();
|
package/dist/setup.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
|
|
10
10
|
import { join, dirname } from 'node:path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
|
-
import { runCli } from '
|
|
12
|
+
import { runCli } from '@canonmsg/core';
|
|
13
13
|
const HELP = `canon-setup — install Claude Code Canon skills and print MCP setup
|
|
14
14
|
|
|
15
15
|
USAGE
|
|
@@ -47,23 +47,21 @@ function installSkills() {
|
|
|
47
47
|
}
|
|
48
48
|
// ── Show MCP config instructions ───────────────────────────────────────
|
|
49
49
|
function showMcpInstructions() {
|
|
50
|
-
// Find the installed server.js path
|
|
51
|
-
const serverPath = join(__dirname, 'server.js');
|
|
52
50
|
console.log(`
|
|
53
51
|
Add this to your project's .mcp.json (or ~/.mcp.json for global):
|
|
54
52
|
|
|
55
53
|
{
|
|
56
54
|
"mcpServers": {
|
|
57
55
|
"canon-channel": {
|
|
58
|
-
"command": "
|
|
59
|
-
"args": ["${serverPath}"]
|
|
56
|
+
"command": "canon-channel-server"
|
|
60
57
|
}
|
|
61
58
|
}
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
Then start Claude Code with:
|
|
65
62
|
|
|
66
|
-
|
|
63
|
+
CANON_API_KEY=agk_live_... CANON_ENVIRONMENT_ID=canon-prod-v1 \
|
|
64
|
+
claude --dangerously-load-development-channels server:canon-channel
|
|
67
65
|
`);
|
|
68
66
|
}
|
|
69
67
|
// ── Main ───────────────────────────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/claude-code-plugin",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.2",
|
|
4
4
|
"description": "Canon channel plugin for Claude Code — messaging where AI agents are first-class citizens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@anthropic-ai/claude-agent-sdk": "0.3.220",
|
|
34
|
-
"@canonmsg/agent-sdk": "^
|
|
35
|
-
"@canonmsg/agent-tools": "^0.3.
|
|
36
|
-
"@canonmsg/coding-agent-host": "^0.
|
|
37
|
-
"@canonmsg/core": "^
|
|
38
|
-
"@canonmsg/rich-cards": "^0.8.
|
|
34
|
+
"@canonmsg/agent-sdk": "^8.0.0",
|
|
35
|
+
"@canonmsg/agent-tools": "^0.3.3",
|
|
36
|
+
"@canonmsg/coding-agent-host": "^0.4.0",
|
|
37
|
+
"@canonmsg/core": "^9.0.0",
|
|
38
|
+
"@canonmsg/rich-cards": "^0.8.7",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.29.0"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
package/dist/cli-entry.d.ts
DELETED
package/dist/cli-entry.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { handleCliMetadataRequest, isDirectExecution, readCliPackageVersion, runCli, } from '@canonmsg/core';
|
package/dist/inbound-policy.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { type CanonGroupContext, type CanonGroupContextMode, type CanonConversation, type ResolvedAgentBehaviorPolicy } from '@canonmsg/core';
|
|
2
|
-
export interface InboundParticipantContext {
|
|
3
|
-
conversationType: CanonConversation['type'] | 'unknown';
|
|
4
|
-
memberCount: number | null;
|
|
5
|
-
senderType: 'human' | 'ai_agent';
|
|
6
|
-
senderName: string;
|
|
7
|
-
isOwner: boolean;
|
|
8
|
-
mentionedAgent: boolean;
|
|
9
|
-
groupContext?: CanonGroupContext;
|
|
10
|
-
groupContextMode?: CanonGroupContextMode;
|
|
11
|
-
recentSenderTypes: Array<'human' | 'ai_agent'>;
|
|
12
|
-
recentHumanCount: number;
|
|
13
|
-
recentAgentCount: number;
|
|
14
|
-
consecutiveAgentTurns: number;
|
|
15
|
-
currentAgentStreakStartedByHuman: boolean;
|
|
16
|
-
}
|
|
17
|
-
export interface AutoReplyDecision {
|
|
18
|
-
allow: boolean;
|
|
19
|
-
reason: string;
|
|
20
|
-
}
|
|
21
|
-
export declare function buildInboundContextLines(context: InboundParticipantContext): string[];
|
|
22
|
-
export declare function decideAutoReply(context: InboundParticipantContext, behavior?: ResolvedAgentBehaviorPolicy | null): AutoReplyDecision;
|
package/dist/inbound-policy.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { buildCompactGroupContextLines, evaluateParticipationPolicy, resolveAgentBehaviorPolicy, } from '@canonmsg/core';
|
|
2
|
-
function formatRecentSenders(senderTypes) {
|
|
3
|
-
if (senderTypes.length === 0)
|
|
4
|
-
return 'none';
|
|
5
|
-
return senderTypes.map((senderType) => (senderType === 'ai_agent' ? 'agent' : 'human')).join(' -> ');
|
|
6
|
-
}
|
|
7
|
-
export function buildInboundContextLines(context) {
|
|
8
|
-
const conversationTypeLabel = context.conversationType === 'unknown'
|
|
9
|
-
? 'unknown'
|
|
10
|
-
: `${context.conversationType}${context.memberCount ? ` (${context.memberCount} members)` : ''}`;
|
|
11
|
-
const senderRole = context.isOwner
|
|
12
|
-
? 'The latest sender is the verified human owner of this Canon agent.'
|
|
13
|
-
: context.senderType === 'ai_agent'
|
|
14
|
-
? 'The latest sender is another AI agent in Canon.'
|
|
15
|
-
: 'The latest sender is a human Canon participant.';
|
|
16
|
-
return [
|
|
17
|
-
senderRole,
|
|
18
|
-
`Latest sender name: ${context.senderName}`,
|
|
19
|
-
`Latest sender type: ${context.senderType}`,
|
|
20
|
-
`Conversation type: ${conversationTypeLabel}`,
|
|
21
|
-
...(context.groupContext && context.groupContextMode
|
|
22
|
-
? buildCompactGroupContextLines(context.groupContext, context.groupContextMode)
|
|
23
|
-
: []),
|
|
24
|
-
`Directly addressed to this agent: ${context.mentionedAgent ? 'yes' : 'no'}`,
|
|
25
|
-
`Recent sender pattern: ${formatRecentSenders(context.recentSenderTypes)}`,
|
|
26
|
-
`Recent human messages: ${context.recentHumanCount}`,
|
|
27
|
-
`Recent agent messages: ${context.recentAgentCount}`,
|
|
28
|
-
`Consecutive recent agent turns: ${context.consecutiveAgentTurns}`,
|
|
29
|
-
`Current agent streak started after a human message: ${context.currentAgentStreakStartedByHuman ? 'yes' : 'no'}`,
|
|
30
|
-
];
|
|
31
|
-
}
|
|
32
|
-
export function decideAutoReply(context, behavior) {
|
|
33
|
-
const decision = evaluateParticipationPolicy(behavior ?? resolveAgentBehaviorPolicy(), {
|
|
34
|
-
conversationType: context.conversationType,
|
|
35
|
-
senderType: context.senderType,
|
|
36
|
-
isOwner: context.isOwner,
|
|
37
|
-
mentionedAgent: context.mentionedAgent,
|
|
38
|
-
recentHumanCount: context.recentHumanCount,
|
|
39
|
-
consecutiveAgentTurns: context.consecutiveAgentTurns,
|
|
40
|
-
currentAgentStreakStartedByHuman: context.currentAgentStreakStartedByHuman,
|
|
41
|
-
});
|
|
42
|
-
return {
|
|
43
|
-
allow: decision.allow,
|
|
44
|
-
reason: decision.reason,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
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
|
-
* Claude also accepts legacy cursor candidates so it can migrate old
|
|
10
|
-
* execution-mode-scoped state into one conversation-level recovery cursor.
|
|
11
|
-
*/
|
|
12
|
-
export declare const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
13
|
-
export declare const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
14
|
-
export interface StartupRecoveryMessage {
|
|
15
|
-
id: string;
|
|
16
|
-
senderId: string;
|
|
17
|
-
createdAt?: string;
|
|
18
|
-
metadata?: Record<string, unknown>;
|
|
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
|
-
/** Cursor found in history and used for this recovery pass. */
|
|
37
|
-
cursor: string | null;
|
|
38
|
-
/** Missed inbound messages (own messages excluded), oldest first. */
|
|
39
|
-
messages: TPage['messages'];
|
|
40
|
-
/** First page fetched — reusable as hydration context for recovered turns. */
|
|
41
|
-
newestPage: TPage;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Startup recovery runs before the stream starts. The first SSE connection
|
|
45
|
-
* therefore needs no second pass, while later reconnects still need catch-up.
|
|
46
|
-
*/
|
|
47
|
-
export declare function createReconnectRecoveryGate(): () => boolean;
|
|
48
|
-
export declare function collectMissedInboundMessages<TPage extends StartupRecoveryPage>(input: {
|
|
49
|
-
fetchPage: (before?: string) => Promise<TPage>;
|
|
50
|
-
cursor: string | null | undefined;
|
|
51
|
-
cursorCandidates?: readonly string[];
|
|
52
|
-
agentId: string;
|
|
53
|
-
maxMessages?: number;
|
|
54
|
-
}): Promise<StartupRecoveryResult<TPage>>;
|
package/dist/startup-recovery.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
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
|
-
* Claude also accepts legacy cursor candidates so it can migrate old
|
|
10
|
-
* execution-mode-scoped state into one conversation-level recovery cursor.
|
|
11
|
-
*/
|
|
12
|
-
export const STARTUP_RECOVERY_PAGE_SIZE = 25;
|
|
13
|
-
export const STARTUP_RECOVERY_MAX_MESSAGES = 500;
|
|
14
|
-
/**
|
|
15
|
-
* Startup recovery runs before the stream starts. The first SSE connection
|
|
16
|
-
* therefore needs no second pass, while later reconnects still need catch-up.
|
|
17
|
-
*/
|
|
18
|
-
export function createReconnectRecoveryGate() {
|
|
19
|
-
let hasConnected = false;
|
|
20
|
-
return () => {
|
|
21
|
-
const shouldRecover = hasConnected;
|
|
22
|
-
hasConnected = true;
|
|
23
|
-
return shouldRecover;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
export async function collectMissedInboundMessages(input) {
|
|
27
|
-
const maxMessages = input.maxMessages ?? STARTUP_RECOVERY_MAX_MESSAGES;
|
|
28
|
-
const newestPage = await input.fetchPage();
|
|
29
|
-
const collected = [...newestPage.messages];
|
|
30
|
-
const seenIds = new Set(collected.map((message) => message.id));
|
|
31
|
-
const candidateCursorIds = new Set(input.cursor
|
|
32
|
-
? [input.cursor]
|
|
33
|
-
: (input.cursorCandidates ?? []).filter(Boolean));
|
|
34
|
-
const foundCursorIds = new Set();
|
|
35
|
-
const recordFoundCursors = (messages) => {
|
|
36
|
-
for (const message of messages) {
|
|
37
|
-
if (candidateCursorIds.has(message.id))
|
|
38
|
-
foundCursorIds.add(message.id);
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
const cursorSearchComplete = () => foundCursorIds.size > 0;
|
|
42
|
-
const findNewestCursor = (messages) => messages.find((message) => candidateCursorIds.has(message.id))?.id ?? null;
|
|
43
|
-
recordFoundCursors(collected);
|
|
44
|
-
if (candidateCursorIds.size > 0) {
|
|
45
|
-
while (!cursorSearchComplete() && collected.length < maxMessages) {
|
|
46
|
-
// Pages are newest-first, so the last collected message is the oldest.
|
|
47
|
-
const before = collected[collected.length - 1]?.id;
|
|
48
|
-
if (!before)
|
|
49
|
-
break;
|
|
50
|
-
const page = await input.fetchPage(before);
|
|
51
|
-
const fresh = page.messages.filter((message) => !seenIds.has(message.id));
|
|
52
|
-
// No pagination progress (history exhausted, or the server ignored the
|
|
53
|
-
// `before` cursor because that message was hard-deleted) — stop here.
|
|
54
|
-
if (fresh.length === 0)
|
|
55
|
-
break;
|
|
56
|
-
for (const message of fresh)
|
|
57
|
-
seenIds.add(message.id);
|
|
58
|
-
collected.push(...fresh);
|
|
59
|
-
recordFoundCursors(fresh);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
const resolvedCursor = cursorSearchComplete()
|
|
63
|
-
? findNewestCursor(collected)
|
|
64
|
-
: null;
|
|
65
|
-
// Preserve the API's deterministic newest-first order. Timestamps are
|
|
66
|
-
// display data and may be equal or absent, so reversing wire order is the
|
|
67
|
-
// only lossless oldest-first replay sequence.
|
|
68
|
-
const ascending = [...collected].reverse();
|
|
69
|
-
const completedSourceMessageIds = new Set(collected.flatMap((message) => {
|
|
70
|
-
if (message.senderId !== input.agentId)
|
|
71
|
-
return [];
|
|
72
|
-
const metadata = message.metadata;
|
|
73
|
-
if (metadata?.turnSemantics !== 'turn_complete')
|
|
74
|
-
return [];
|
|
75
|
-
return typeof metadata.sourceMessageId === 'string'
|
|
76
|
-
? [metadata.sourceMessageId]
|
|
77
|
-
: [];
|
|
78
|
-
}));
|
|
79
|
-
const inboundOnly = (messages) => messages.filter((message) => message.senderId !== input.agentId && !completedSourceMessageIds.has(message.id));
|
|
80
|
-
let mode;
|
|
81
|
-
let missed;
|
|
82
|
-
if (resolvedCursor) {
|
|
83
|
-
const cursorIndex = ascending.findIndex((message) => message.id === resolvedCursor);
|
|
84
|
-
mode = 'after-cursor';
|
|
85
|
-
missed = inboundOnly(ascending.slice(cursorIndex + 1));
|
|
86
|
-
}
|
|
87
|
-
else if (candidateCursorIds.size > 0 && collected.length >= maxMessages) {
|
|
88
|
-
mode = 'truncated-window';
|
|
89
|
-
missed = inboundOnly(ascending.slice(-maxMessages));
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
mode = 'latest-only';
|
|
93
|
-
missed = inboundOnly(ascending).slice(-1);
|
|
94
|
-
}
|
|
95
|
-
// Safe: `missed` only holds elements of pages returned by `fetchPage`.
|
|
96
|
-
return {
|
|
97
|
-
mode,
|
|
98
|
-
cursor: resolvedCursor,
|
|
99
|
-
messages: missed,
|
|
100
|
-
newestPage,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
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
|
-
};
|
package/dist/streaming-buffer.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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
|
-
}
|