@canonmsg/claude-code-plugin 0.19.6 → 0.20.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.
@@ -1,4 +1,4 @@
1
- import type { DirectSessionSelection, SendContextualMessageOptions } from '@canonmsg/core';
1
+ import type { DirectSessionSelection, SendContextualMessageOptions } from '@canonmsg/core/contract';
2
2
  export type McpParseResult<T> = {
3
3
  ok: true;
4
4
  value: T;
package/dist/register.js CHANGED
@@ -9,10 +9,47 @@ import { setDefaultResultOrder } from 'node:dns';
9
9
  *
10
10
  * On approval, saves the agent to ~/.canon/agents.json under the profile name.
11
11
  */
12
- import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
12
+ import { ackRegistrationApprovalWith, resolveCanonBaseUrl, runRegistrationFlow, } from '@canonmsg/core/contract';
13
+ import { clearPendingRegistration, getOrCreatePendingRegistration, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core/local';
14
+ import { resolvePackagedBridgeBin } from './bridge-bin.js';
15
+ import { connectBridge } from '@canonmsg/framework';
13
16
  import { readFileSync } from 'node:fs';
14
17
  import { parseArgs } from 'node:util';
15
18
  import { runCli } from './cli-entry.js';
19
+ /**
20
+ * Registration rides the bridge (Phase 6, same shape as the Hermes port): a
21
+ * short-lived PRE-IDENTITY registration daemon (offline stub +
22
+ * CANON_BRIDGE_REGISTRATION_BASE_URL, managed-child so it dies with this
23
+ * process) serves the three unauthenticated §B10 routes over JSON-RPC.
24
+ */
25
+ async function openRegistrationBridge(baseUrl) {
26
+ return connectBridge({
27
+ profile: 'registration',
28
+ mode: 'managed-child',
29
+ ...(resolvePackagedBridgeBin() ? { binPath: resolvePackagedBridgeBin() } : {}),
30
+ env: {
31
+ ...process.env,
32
+ CANON_BRIDGE_AGENT_ID: 'canon-registration',
33
+ CANON_BRIDGE_REGISTRATION_BASE_URL: baseUrl,
34
+ },
35
+ hello: { clientType: 'claude-code', wantFamilies: [] },
36
+ });
37
+ }
38
+ function bridgeRegistrationTransport(bridge) {
39
+ return {
40
+ register: (_baseUrl, body) => bridge.client.call('register', body),
41
+ checkStatus: (_baseUrl, requestId, pollToken) => bridge.client.call('getRegistrationStatus', {
42
+ requestId,
43
+ ...(pollToken ? { pollToken } : {}),
44
+ }),
45
+ ackRegistrationStatus: async (_baseUrl, requestId, pollToken) => {
46
+ await bridge.client.call('ackRegistrationStatus', {
47
+ requestId,
48
+ ...(pollToken ? { pollToken } : {}),
49
+ });
50
+ },
51
+ };
52
+ }
16
53
  const HELP = `canon-register — register or reconnect a Claude Code agent in Canon
17
54
 
18
55
  USAGE
@@ -62,29 +99,39 @@ export async function main() {
62
99
  }
63
100
  console.log(`Registering agent "${values.name}" (profile: ${profileName})...`);
64
101
  const pending = getOrCreatePendingRegistration(profileName, 'claude-code');
65
- const result = await registerAndWaitForApproval({
66
- name: values.name,
67
- description: values.description,
68
- ownerPhone: values.phone,
69
- developerInfo: 'Claude Code plugin',
70
- clientType: 'claude-code',
71
- baseUrl: values['base-url'],
72
- requestedAgentId: existingAgentId,
73
- localRegistrationId: pending.localRegistrationId,
74
- }, {
75
- onSubmitted: (requestId, pollToken) => {
76
- updatePendingRegistration(profileName, {
77
- requestId,
78
- pollToken,
79
- clientType: 'claude-code',
80
- });
81
- console.log(`Registration submitted (request ID: ${requestId}).`);
82
- console.log('Waiting for approval in Canon app...');
83
- },
84
- onPollUpdate: () => {
85
- process.stdout.write('.');
86
- },
87
- });
102
+ const baseUrl = resolveCanonBaseUrl(values['base-url']);
103
+ const bridge = await openRegistrationBridge(baseUrl);
104
+ const transport = bridgeRegistrationTransport(bridge);
105
+ let result;
106
+ try {
107
+ result = await runRegistrationFlow({
108
+ name: values.name,
109
+ description: values.description,
110
+ ownerPhone: values.phone,
111
+ developerInfo: 'Claude Code plugin',
112
+ clientType: 'claude-code',
113
+ baseUrl: values['base-url'],
114
+ requestedAgentId: existingAgentId,
115
+ localRegistrationId: pending.localRegistrationId,
116
+ }, transport, {
117
+ onSubmitted: (requestId, pollToken) => {
118
+ updatePendingRegistration(profileName, {
119
+ requestId,
120
+ pollToken,
121
+ clientType: 'claude-code',
122
+ });
123
+ console.log(`Registration submitted (request ID: ${requestId}).`);
124
+ console.log('Waiting for approval in Canon app...');
125
+ },
126
+ onPollUpdate: () => {
127
+ process.stdout.write('.');
128
+ },
129
+ });
130
+ }
131
+ catch (error) {
132
+ bridge.client.close();
133
+ throw error;
134
+ }
88
135
  console.log(''); // newline after dots
89
136
  switch (result.status) {
90
137
  case 'approved': {
@@ -101,7 +148,7 @@ export async function main() {
101
148
  ...(typeof values['base-url'] === 'string' ? { baseUrl: values['base-url'] } : {}),
102
149
  });
103
150
  if (result.requestId) {
104
- await ackRegistrationApproval(values['base-url'], result.requestId, result.pollToken);
151
+ await ackRegistrationApprovalWith(transport, values['base-url'], result.requestId, result.pollToken);
105
152
  }
106
153
  clearPendingRegistration(profileName);
107
154
  console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
@@ -121,6 +168,7 @@ export async function main() {
121
168
  process.exit(1);
122
169
  break;
123
170
  }
171
+ bridge.client.close();
124
172
  }
125
173
  runCli(import.meta.url, main, (error) => {
126
174
  console.error('[canon-register] Fatal error:', error);
package/dist/server.js CHANGED
@@ -10,7 +10,7 @@ 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, getActiveProfile, releaseLock, upsertLocalRuntimeEntry, initRTDBAuth, writeSessionState, clearSessionState, renderCanonHostInboundContent, } from '@canonmsg/core';
13
+ import { CanonClient, CanonStream, ApprovalManager, buildLocalRuntimeId, markLocalRuntimeStopped, loadProfiles, isProfileLocked, resolveCanonAgent, 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
16
  import { runCli } from './cli-entry.js';
@@ -34,6 +34,16 @@ const server = new Server({ name: 'canon-channel', version: '0.1.0' }, {
34
34
  },
35
35
  });
36
36
  let client = null;
37
+ // Scoped RTDB handle for this channel server's single agent. Held alongside
38
+ // the other per-process singletons (client, stream, ...) so both startChannel
39
+ // and the shutdown handler in main() write session state as this agent — not
40
+ // through any module-global default.
41
+ let rtdb = null;
42
+ // The one agent profile this server resolved+locked, and its lock handle.
43
+ // Held here (not via a core module-global) so the list_agents tool handler and
44
+ // the shutdown/error paths — all outside startChannel's scope — can read them.
45
+ let activeProfile = null;
46
+ let activeLockHandle = null;
37
47
  let stream = null;
38
48
  let agentContext = null;
39
49
  let approvalManager = null;
@@ -386,7 +396,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
386
396
  profile: name,
387
397
  agentName: p.agentName,
388
398
  agentId: p.agentId,
389
- status: getActiveProfile() === name ? 'active (this session)' : lock.locked ? `in use (PID ${lock.pid})` : 'available',
399
+ status: activeProfile === name ? 'active (this session)' : lock.locked ? `in use (PID ${lock.pid})` : 'available',
390
400
  };
391
401
  });
392
402
  if (entries.length === 0) {
@@ -547,6 +557,8 @@ async function startChannel() {
547
557
  profile = resolved.profile;
548
558
  baseUrl = resolved.baseUrl;
549
559
  profileAgentName = resolved.agentName;
560
+ activeProfile = resolved.profile;
561
+ activeLockHandle = resolved.lockHandle ?? null;
550
562
  }
551
563
  catch (err) {
552
564
  console.error(err instanceof Error ? err.message : err);
@@ -577,7 +589,7 @@ async function startChannel() {
577
589
  }
578
590
  }
579
591
  // Initialize RTDB auth for session state reporting
580
- initRTDBAuth(client);
592
+ rtdb = initRTDBAuth(client);
581
593
  const runtimeId = buildLocalRuntimeId({
582
594
  runtime: 'claude-code',
583
595
  profile,
@@ -612,7 +624,7 @@ async function startChannel() {
612
624
  // Write initial session state
613
625
  const initialState = { cwd: process.cwd(), isActive: true };
614
626
  for (const convoId of conversationCache.keys()) {
615
- writeSessionState(convoId, agentId, initialState).catch((err) => console.error(`[canon] Failed to write initial state for ${convoId}:`, err));
627
+ rtdb.writeSessionState(convoId, agentId, initialState).catch((err) => console.error(`[canon] Failed to write initial state for ${convoId}:`, err));
616
628
  }
617
629
  // Start SSE stream
618
630
  stream = new CanonStream({
@@ -665,9 +677,10 @@ export async function main() {
665
677
  // Graceful shutdown — release agent lock and clear session state
666
678
  const shutdown = () => {
667
679
  // Mark all conversations as inactive
668
- if (agentContext) {
680
+ if (agentContext && rtdb) {
681
+ const scopedRtdb = rtdb;
669
682
  for (const convoId of conversationCache.keys()) {
670
- clearSessionState(convoId, agentContext.agentId).catch(() => { });
683
+ scopedRtdb.clearSessionState(convoId, agentContext.agentId).catch(() => { });
671
684
  }
672
685
  }
673
686
  approvalManager?.dispose();
@@ -675,15 +688,14 @@ export async function main() {
675
688
  stream?.stop();
676
689
  const runtimeId = buildLocalRuntimeId({
677
690
  runtime: 'claude-code',
678
- profile: getActiveProfile(),
691
+ profile: activeProfile,
679
692
  cwd: process.cwd(),
680
693
  launchCommand: ['canon-channel-server'],
681
694
  });
682
695
  markLocalRuntimeStopped(runtimeId);
683
- const ap = getActiveProfile();
684
- if (ap)
685
- releaseLock(ap);
686
- getActiveProfileLock()?.release();
696
+ if (activeProfile)
697
+ releaseLock(activeProfile);
698
+ activeLockHandle?.release();
687
699
  process.exit(0);
688
700
  };
689
701
  process.on('SIGINT', shutdown);
@@ -1,6 +1,6 @@
1
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';
2
+ import type { DeliveryIntent, ModelOption, TurnLifecycleState } from '@canonmsg/core/contract';
3
+ import type { TurnArtifactSnapshot } from '@canonmsg/agent-host';
4
4
  export type ClaudeInputKind = 'seed' | 'canon';
5
5
  export type ClaudeArtifactRoutingMode = 'workspace-generated' | 'disabled';
6
6
  export interface ClaudeInputEnvelope {
@@ -1,4 +1,5 @@
1
- import type { ApprovalManager, ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core';
1
+ import type { ApprovalRequestCategory, ApprovalRequestDetail, ApprovalRisk, CanonUnifiedDiff } from '@canonmsg/core/contract';
2
+ import type { ApprovalManager } from '@canonmsg/core/host';
2
3
  export type ClaudeToolPolicyDecision = {
3
4
  behavior: 'allow';
4
5
  } | {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/claude-code-plugin",
3
- "version": "0.19.6",
3
+ "version": "0.20.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,17 +23,19 @@
23
23
  ".claude-plugin"
24
24
  ],
25
25
  "scripts": {
26
- "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
26
+ "prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../../packages/core ../../packages/agent-sdk ../../packages/framework ../../packages/bridge ../../packages/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.2.141",
34
- "@canonmsg/agent-sdk": "^3.4.2",
35
- "@canonmsg/coding-agent-host": "^0.2.2",
36
- "@canonmsg/core": "^3.0.0",
33
+ "@anthropic-ai/claude-agent-sdk": "^0.3.200",
34
+ "@canonmsg/agent-host": "^0.3.0",
35
+ "@canonmsg/agent-sdk": "^3.4.3",
36
+ "@canonmsg/bridge": "^0.2.0",
37
+ "@canonmsg/core": "^3.1.0",
38
+ "@canonmsg/framework": "^0.2.0",
37
39
  "@modelcontextprotocol/sdk": "^1.29.0"
38
40
  },
39
41
  "engines": {
@@ -50,9 +52,9 @@
50
52
  "repository": {
51
53
  "type": "git",
52
54
  "url": "https://github.com/HeyBobChan/canon",
53
- "directory": "packages/claude-code-plugin"
55
+ "directory": "adapters/claude-code-plugin"
54
56
  },
55
- "homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/claude-code-plugin",
57
+ "homepage": "https://github.com/HeyBobChan/canon/tree/main/adapters/claude-code-plugin",
56
58
  "publishConfig": {
57
59
  "access": "public"
58
60
  },
@@ -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;
@@ -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,46 +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
- * 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>>;
@@ -1,59 +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
- * 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
- }
@@ -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
- };
@@ -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
- }