@canonmsg/codex-plugin 0.20.0 → 0.21.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/README.md +4 -4
- package/dist/adapter.d.ts +96 -0
- package/dist/adapter.js +310 -0
- package/dist/app-server-adapter.d.ts +3 -64
- package/dist/app-server-adapter.js +7 -54
- package/dist/app-server-approval.d.ts +1 -1
- package/dist/app-server-approval.js +1 -1
- package/dist/cli-entry.d.ts +2 -2
- package/dist/cli-entry.js +1 -1
- package/dist/control-channel.d.ts +46 -26
- package/dist/control-channel.js +50 -37
- package/dist/host-lifecycle.d.ts +4 -0
- package/dist/host-lifecycle.js +3 -0
- package/dist/host.d.ts +1 -1
- package/dist/host.js +1012 -944
- package/dist/inbound-policy.d.ts +22 -0
- package/dist/inbound-policy.js +46 -0
- package/dist/permission-mode.d.ts +1 -1
- package/dist/register.js +25 -73
- package/dist/session-store.d.ts +1 -1
- package/dist/session-store.js +1 -1
- package/dist/startup-recovery.d.ts +46 -0
- package/dist/startup-recovery.js +59 -0
- package/dist/turn-activity.d.ts +8 -23
- package/dist/turn-activity.js +40 -24
- package/package.json +7 -10
- package/dist/bridge-bin.d.ts +0 -14
- package/dist/bridge-bin.js +0 -27
|
@@ -0,0 +1,22 @@
|
|
|
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;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
}
|
package/dist/register.js
CHANGED
|
@@ -2,45 +2,8 @@
|
|
|
2
2
|
import { setDefaultResultOrder } from 'node:dns';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { parseArgs } from 'node:util';
|
|
5
|
-
import {
|
|
6
|
-
import { clearPendingRegistration, getOrCreatePendingRegistration, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core/local';
|
|
7
|
-
import { resolvePackagedBridgeBin } from './bridge-bin.js';
|
|
8
|
-
import { connectBridge } from '@canonmsg/framework';
|
|
5
|
+
import { ackRegistrationApproval, clearPendingRegistration, getOrCreatePendingRegistration, registerAndWaitForApproval, updatePendingRegistration, upsertAgentProfile, AGENTS_PATH, } from '@canonmsg/core';
|
|
9
6
|
import { runCli } from './cli-entry.js';
|
|
10
|
-
/**
|
|
11
|
-
* Registration rides the bridge (Phase 6, same shape as the Hermes port): a
|
|
12
|
-
* short-lived PRE-IDENTITY registration daemon (offline stub +
|
|
13
|
-
* CANON_BRIDGE_REGISTRATION_BASE_URL, managed-child so it dies with this
|
|
14
|
-
* process) serves the three unauthenticated §B10 routes over JSON-RPC.
|
|
15
|
-
*/
|
|
16
|
-
async function openRegistrationBridge(baseUrl) {
|
|
17
|
-
return connectBridge({
|
|
18
|
-
profile: 'registration',
|
|
19
|
-
mode: 'managed-child',
|
|
20
|
-
...(resolvePackagedBridgeBin() ? { binPath: resolvePackagedBridgeBin() } : {}),
|
|
21
|
-
env: {
|
|
22
|
-
...process.env,
|
|
23
|
-
CANON_BRIDGE_AGENT_ID: 'canon-registration',
|
|
24
|
-
CANON_BRIDGE_REGISTRATION_BASE_URL: baseUrl,
|
|
25
|
-
},
|
|
26
|
-
hello: { clientType: 'codex', wantFamilies: [] },
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
function bridgeRegistrationTransport(bridge) {
|
|
30
|
-
return {
|
|
31
|
-
register: (_baseUrl, body) => bridge.client.call('register', body),
|
|
32
|
-
checkStatus: (_baseUrl, requestId, pollToken) => bridge.client.call('getRegistrationStatus', {
|
|
33
|
-
requestId,
|
|
34
|
-
...(pollToken ? { pollToken } : {}),
|
|
35
|
-
}),
|
|
36
|
-
ackRegistrationStatus: async (_baseUrl, requestId, pollToken) => {
|
|
37
|
-
await bridge.client.call('ackRegistrationStatus', {
|
|
38
|
-
requestId,
|
|
39
|
-
...(pollToken ? { pollToken } : {}),
|
|
40
|
-
});
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
7
|
const HELP = `canon-codex-register — register or reconnect a Codex agent in Canon
|
|
45
8
|
|
|
46
9
|
USAGE
|
|
@@ -89,39 +52,29 @@ export async function main() {
|
|
|
89
52
|
}
|
|
90
53
|
console.log(`Registering Codex agent "${values.name}" (profile: ${profileName})...`);
|
|
91
54
|
const pending = getOrCreatePendingRegistration(profileName, 'codex');
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
116
|
-
onPollUpdate: () => {
|
|
117
|
-
process.stdout.write('.');
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
bridge.client.close();
|
|
123
|
-
throw error;
|
|
124
|
-
}
|
|
55
|
+
const result = await registerAndWaitForApproval({
|
|
56
|
+
name: values.name,
|
|
57
|
+
description: values.description,
|
|
58
|
+
ownerPhone: values.phone,
|
|
59
|
+
developerInfo: 'Codex host plugin',
|
|
60
|
+
clientType: 'codex',
|
|
61
|
+
baseUrl: values['base-url'],
|
|
62
|
+
requestedAgentId: existingAgentId,
|
|
63
|
+
localRegistrationId: pending.localRegistrationId,
|
|
64
|
+
}, {
|
|
65
|
+
onSubmitted: (requestId, pollToken) => {
|
|
66
|
+
updatePendingRegistration(profileName, {
|
|
67
|
+
requestId,
|
|
68
|
+
pollToken,
|
|
69
|
+
clientType: 'codex',
|
|
70
|
+
});
|
|
71
|
+
console.log(`Registration submitted (request ID: ${requestId}).`);
|
|
72
|
+
console.log('Waiting for approval in Canon app...');
|
|
73
|
+
},
|
|
74
|
+
onPollUpdate: () => {
|
|
75
|
+
process.stdout.write('.');
|
|
76
|
+
},
|
|
77
|
+
});
|
|
125
78
|
console.log('');
|
|
126
79
|
switch (result.status) {
|
|
127
80
|
case 'approved': {
|
|
@@ -138,7 +91,7 @@ export async function main() {
|
|
|
138
91
|
...(typeof values['base-url'] === 'string' ? { baseUrl: values['base-url'] } : {}),
|
|
139
92
|
});
|
|
140
93
|
if (result.requestId) {
|
|
141
|
-
await
|
|
94
|
+
await ackRegistrationApproval(values['base-url'], result.requestId, result.pollToken);
|
|
142
95
|
}
|
|
143
96
|
clearPendingRegistration(profileName);
|
|
144
97
|
console.log(`Approved! Agent: ${result.agentName} (${result.agentId})`);
|
|
@@ -158,7 +111,6 @@ export async function main() {
|
|
|
158
111
|
process.exit(1);
|
|
159
112
|
break;
|
|
160
113
|
}
|
|
161
|
-
bridge.client.close();
|
|
162
114
|
}
|
|
163
115
|
runCli(import.meta.url, main, (error) => {
|
|
164
116
|
console.error('[canon-codex-register] Fatal error:', error);
|
package/dist/session-store.d.ts
CHANGED
package/dist/session-store.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
-
import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core
|
|
4
|
+
import { CANON_DIR, clearRuntimeSessionState, loadRuntimeSessionState, saveRuntimeSessionState, } from '@canonmsg/core';
|
|
5
5
|
const STORE_PATH = join(CANON_DIR, 'codex-sessions.json');
|
|
6
6
|
function loadStore() {
|
|
7
7
|
try {
|
|
@@ -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
|
+
}
|
package/dist/turn-activity.d.ts
CHANGED
|
@@ -1,39 +1,24 @@
|
|
|
1
|
-
import type { TurnOutputBlock } from '@canonmsg/core
|
|
1
|
+
import type { TurnOutputBlock } from '@canonmsg/core';
|
|
2
2
|
interface RunningCommandBlock {
|
|
3
3
|
command: string;
|
|
4
4
|
blockId: string;
|
|
5
5
|
itemId?: string;
|
|
6
6
|
}
|
|
7
|
-
/**
|
|
8
|
-
* True once the streaming output already carries assistant speech (folded
|
|
9
|
-
* text segments). While speech is flowing the host must not flap the bubble
|
|
10
|
-
* status to tool/thinking or clobber the live text with plan snapshots — the
|
|
11
|
-
* staged margin blocks already narrate that activity, and every status/text
|
|
12
|
-
* swap makes the smoothed bubble snap.
|
|
13
|
-
*/
|
|
14
|
-
export declare function hasSpeechSegmentText(blocks: ReadonlyArray<Pick<TurnOutputBlock, 'kind' | 'text'>>): boolean;
|
|
15
7
|
export interface CommandBlockTracker {
|
|
16
8
|
sequence: number;
|
|
17
9
|
running: RunningCommandBlock[];
|
|
18
10
|
}
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* (snapshot replacement + live-text rebuild around activity blocks) lives in
|
|
23
|
-
* core's TurnOutputController (`replaceTextSegmentSnapshot`) — this module
|
|
24
|
-
* only owns the codex id scheme.
|
|
25
|
-
*/
|
|
26
|
-
export declare function textSegmentBlockId(turnId: string | null | undefined, itemId?: string): string;
|
|
27
|
-
export interface CodexTextSegmentOutput {
|
|
28
|
-
appendTextSegmentDelta(id: string, delta: string): void;
|
|
29
|
-
replaceTextSegmentSnapshot(id: string, text: string): void;
|
|
11
|
+
export interface TextSegmentBlockState {
|
|
12
|
+
turnLiveText: string;
|
|
13
|
+
turnBlocks: TurnOutputBlock[];
|
|
30
14
|
}
|
|
31
|
-
export declare function
|
|
15
|
+
export declare function createCommandBlockTracker(): CommandBlockTracker;
|
|
16
|
+
export declare function applyTextSegmentBlock(state: TextSegmentBlockState, input: {
|
|
32
17
|
turnId: string | null | undefined;
|
|
33
18
|
itemId?: string;
|
|
34
19
|
text: string;
|
|
35
|
-
|
|
36
|
-
}):
|
|
20
|
+
now?: number;
|
|
21
|
+
}): TextSegmentBlockState;
|
|
37
22
|
export declare function beginCommandBlock(tracker: CommandBlockTracker, input: {
|
|
38
23
|
turnId: string | null | undefined;
|
|
39
24
|
command: string;
|
package/dist/turn-activity.js
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* True once the streaming output already carries assistant speech (folded
|
|
3
|
-
* text segments). While speech is flowing the host must not flap the bubble
|
|
4
|
-
* status to tool/thinking or clobber the live text with plan snapshots — the
|
|
5
|
-
* staged margin blocks already narrate that activity, and every status/text
|
|
6
|
-
* swap makes the smoothed bubble snap.
|
|
7
|
-
*/
|
|
8
|
-
export function hasSpeechSegmentText(blocks) {
|
|
9
|
-
return blocks.some((block) => block.kind === 'text' && Boolean(block.text?.trim()));
|
|
10
|
-
}
|
|
11
1
|
export function createCommandBlockTracker() {
|
|
12
2
|
return {
|
|
13
3
|
sequence: 0,
|
|
@@ -18,22 +8,48 @@ function normalizeOptionalString(value) {
|
|
|
18
8
|
const normalized = value?.trim();
|
|
19
9
|
return normalized ? normalized : undefined;
|
|
20
10
|
}
|
|
21
|
-
|
|
22
|
-
* Stable block id for one assistant text segment. The segment state itself
|
|
23
|
-
* (snapshot replacement + live-text rebuild around activity blocks) lives in
|
|
24
|
-
* core's TurnOutputController (`replaceTextSegmentSnapshot`) — this module
|
|
25
|
-
* only owns the codex id scheme.
|
|
26
|
-
*/
|
|
27
|
-
export function textSegmentBlockId(turnId, itemId) {
|
|
11
|
+
function textBlockId(turnId, itemId) {
|
|
28
12
|
return `message:${turnId ?? 'turn'}:${normalizeOptionalString(itemId) ?? 'latest'}`;
|
|
29
13
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
14
|
+
function buildLiveText(blocks) {
|
|
15
|
+
return blocks
|
|
16
|
+
.filter((block) => block.kind === 'text' && block.text?.trim())
|
|
17
|
+
.sort((left, right) => {
|
|
18
|
+
if (left.sequence !== right.sequence)
|
|
19
|
+
return left.sequence - right.sequence;
|
|
20
|
+
return left.id.localeCompare(right.id);
|
|
21
|
+
})
|
|
22
|
+
.map((block) => block.text)
|
|
23
|
+
.join('\n\n');
|
|
24
|
+
}
|
|
25
|
+
export function applyTextSegmentBlock(state, input) {
|
|
26
|
+
const id = textBlockId(input.turnId, input.itemId);
|
|
27
|
+
const now = input.now ?? Date.now();
|
|
28
|
+
const index = state.turnBlocks.findIndex((block) => block.id === id);
|
|
29
|
+
const existing = index >= 0 ? state.turnBlocks[index] : null;
|
|
30
|
+
const next = {
|
|
31
|
+
...(existing ?? {
|
|
32
|
+
sequence: state.turnBlocks.length + 1,
|
|
33
|
+
createdAt: now,
|
|
34
|
+
}),
|
|
35
|
+
id,
|
|
36
|
+
turnId: input.turnId ?? id,
|
|
37
|
+
kind: 'text',
|
|
38
|
+
status: existing?.status ?? 'running',
|
|
39
|
+
text: input.text,
|
|
40
|
+
updatedAt: now,
|
|
41
|
+
};
|
|
42
|
+
const turnBlocks = index >= 0
|
|
43
|
+
? [
|
|
44
|
+
...state.turnBlocks.slice(0, index),
|
|
45
|
+
next,
|
|
46
|
+
...state.turnBlocks.slice(index + 1),
|
|
47
|
+
]
|
|
48
|
+
: [...state.turnBlocks, next];
|
|
49
|
+
return {
|
|
50
|
+
turnBlocks,
|
|
51
|
+
turnLiveText: buildLiveText(turnBlocks) || state.turnLiveText,
|
|
52
|
+
};
|
|
37
53
|
}
|
|
38
54
|
function nextCommandBlockId(tracker, turnId, itemId) {
|
|
39
55
|
const stableTurnId = turnId ?? 'turn';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonmsg/codex-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Canon host integration for Codex CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"scripts"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs
|
|
24
|
+
"prepare:workspace-deps": "node ../../scripts/run-workspace-prep.mjs ../core ../agent-sdk ../coding-agent-host",
|
|
25
25
|
"build": "npm run prepare:workspace-deps && node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
|
|
26
26
|
"dev": "npm run prepare:workspace-deps && tsc --watch",
|
|
27
27
|
"smoke": "node scripts/smoke-test.mjs",
|
|
@@ -29,12 +29,9 @@
|
|
|
29
29
|
"prepack": "npm run build"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@canonmsg/agent-
|
|
33
|
-
"@canonmsg/agent-
|
|
34
|
-
"@canonmsg/
|
|
35
|
-
"@canonmsg/bridge": "^0.3.0",
|
|
36
|
-
"@canonmsg/core": "^3.3.0",
|
|
37
|
-
"@canonmsg/framework": "^0.3.0"
|
|
32
|
+
"@canonmsg/agent-sdk": "^5.0.0",
|
|
33
|
+
"@canonmsg/coding-agent-host": "^0.2.2",
|
|
34
|
+
"@canonmsg/core": "^4.0.0"
|
|
38
35
|
},
|
|
39
36
|
"engines": {
|
|
40
37
|
"node": ">=18.0.0"
|
|
@@ -49,9 +46,9 @@
|
|
|
49
46
|
"repository": {
|
|
50
47
|
"type": "git",
|
|
51
48
|
"url": "https://github.com/HeyBobChan/canon",
|
|
52
|
-
"directory": "
|
|
49
|
+
"directory": "packages/codex-plugin"
|
|
53
50
|
},
|
|
54
|
-
"homepage": "https://github.com/HeyBobChan/canon/tree/main/
|
|
51
|
+
"homepage": "https://github.com/HeyBobChan/canon/tree/main/packages/codex-plugin",
|
|
55
52
|
"publishConfig": {
|
|
56
53
|
"access": "public"
|
|
57
54
|
},
|
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
|
-
}
|