@canonmsg/core 0.2.2 → 0.4.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/dist/agent-resolver.d.ts +4 -0
- package/dist/agent-resolver.js +38 -15
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +3 -0
- package/dist/client.d.ts +6 -2
- package/dist/client.js +21 -2
- package/dist/execution-environment.d.ts +59 -0
- package/dist/execution-environment.js +284 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.js +7 -2
- package/dist/policy.d.ts +156 -0
- package/dist/policy.js +189 -0
- package/dist/rtdb-rest.d.ts +24 -0
- package/dist/rtdb-rest.js +40 -0
- package/dist/turn-protocol.d.ts +56 -0
- package/dist/turn-protocol.js +152 -0
- package/dist/types.d.ts +31 -8
- package/dist/types.js +7 -6
- package/package.json +7 -3
package/dist/agent-resolver.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface ResolvedAgent {
|
|
|
17
17
|
}
|
|
18
18
|
/** Get the currently locked profile name (for cleanup on shutdown). */
|
|
19
19
|
export declare function getActiveProfile(): string | null;
|
|
20
|
+
export declare function resolveCanonProfile(name: string, opts?: {
|
|
21
|
+
logPrefix?: string;
|
|
22
|
+
lock?: boolean;
|
|
23
|
+
}): ResolvedAgent;
|
|
20
24
|
/**
|
|
21
25
|
* Resolve Canon agent credentials.
|
|
22
26
|
*
|
package/dist/agent-resolver.js
CHANGED
|
@@ -16,6 +16,26 @@ let activeResolvedProfile = null;
|
|
|
16
16
|
export function getActiveProfile() {
|
|
17
17
|
return activeResolvedProfile;
|
|
18
18
|
}
|
|
19
|
+
export function resolveCanonProfile(name, opts) {
|
|
20
|
+
const prefix = opts?.logPrefix ?? '[canon]';
|
|
21
|
+
const profileName = name.trim();
|
|
22
|
+
if (!profileName) {
|
|
23
|
+
throw new Error(`${prefix} Profile name is required`);
|
|
24
|
+
}
|
|
25
|
+
const profiles = loadProfiles();
|
|
26
|
+
const profile = profiles[profileName];
|
|
27
|
+
if (!profile) {
|
|
28
|
+
throw new Error(`${prefix} Profile "${profileName}" not found in ~/.canon/agents.json`);
|
|
29
|
+
}
|
|
30
|
+
if (opts?.lock) {
|
|
31
|
+
acquireLock(profileName);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
apiKey: profile.apiKey,
|
|
35
|
+
agentId: profile.agentId,
|
|
36
|
+
profile: profileName,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
19
39
|
/**
|
|
20
40
|
* Resolve Canon agent credentials.
|
|
21
41
|
*
|
|
@@ -36,32 +56,35 @@ export function resolveCanonAgent(opts) {
|
|
|
36
56
|
const names = Object.keys(profiles);
|
|
37
57
|
// 2. Named profile via env var
|
|
38
58
|
if (process.env.CANON_AGENT) {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
activeResolvedProfile = name;
|
|
46
|
-
return { apiKey: p.apiKey, agentId: p.agentId, profile: name };
|
|
59
|
+
const resolved = resolveCanonProfile(process.env.CANON_AGENT, {
|
|
60
|
+
logPrefix: prefix,
|
|
61
|
+
lock: true,
|
|
62
|
+
});
|
|
63
|
+
activeResolvedProfile = resolved.profile;
|
|
64
|
+
return resolved;
|
|
47
65
|
}
|
|
48
66
|
// 3. Auto-select from profiles
|
|
49
67
|
if (names.length === 0) {
|
|
50
68
|
throw new Error(`${prefix} No agents registered. Run canon-register first.`);
|
|
51
69
|
}
|
|
52
70
|
if (names.length === 1) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
const resolved = resolveCanonProfile(names[0], {
|
|
72
|
+
logPrefix: prefix,
|
|
73
|
+
lock: true,
|
|
74
|
+
});
|
|
75
|
+
activeResolvedProfile = resolved.profile;
|
|
76
|
+
return resolved;
|
|
57
77
|
}
|
|
58
78
|
// Multiple agents — pick first unlocked
|
|
59
79
|
for (const name of names) {
|
|
60
80
|
if (!isProfileLocked(name).locked) {
|
|
61
|
-
|
|
62
|
-
|
|
81
|
+
const resolved = resolveCanonProfile(name, {
|
|
82
|
+
logPrefix: prefix,
|
|
83
|
+
lock: true,
|
|
84
|
+
});
|
|
85
|
+
activeResolvedProfile = resolved.profile;
|
|
63
86
|
console.error(`${prefix} Auto-selected agent "${name}" (${profiles[name].agentName})`);
|
|
64
|
-
return
|
|
87
|
+
return resolved;
|
|
65
88
|
}
|
|
66
89
|
}
|
|
67
90
|
throw new Error(`${prefix} All agents are in use by other sessions.`);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
+
export type { AgentCapabilities, AgentClientType, AgentRuntime, MediaAttachment, MediaAttachmentKind, ModelOption, SessionConfig, WorkspaceOption, } from './types.js';
|
|
3
|
+
export type { AgentBehaviorSettings, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationStyle, ResolvedAgentBehaviorPolicy, } from './policy.js';
|
|
4
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
5
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
6
|
+
export type { DeliveryIntent, InboundDisposition, RuntimeCapabilities, TriggerDecision, TurnLifecycleState, TurnMessageSemantics, TurnMetadata, TurnState, } from './turn-protocol.js';
|
package/dist/browser.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
3
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type CanonMessage, type CanonConversation, type AgentContext, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
1
|
+
import { type CanonMessage, type CanonConversation, type CanonMessagesPage, type AgentContext, type MediaAttachment, type SendMessageOptions, type CreateConversationOptions, type RegistrationStatus, type SetStreamingOptions } from './types.js';
|
|
2
|
+
import type { InboundDisposition } from './turn-protocol.js';
|
|
2
3
|
/**
|
|
3
4
|
* Thin REST client for Canon's agent API.
|
|
4
5
|
* Uses native fetch — no runtime dependencies.
|
|
@@ -16,17 +17,20 @@ export declare class CanonClient {
|
|
|
16
17
|
getAgentMe(): Promise<AgentContext>;
|
|
17
18
|
getConversations(): Promise<CanonConversation[]>;
|
|
18
19
|
getMessages(conversationId: string, limit?: number, before?: string): Promise<CanonMessage[]>;
|
|
20
|
+
getMessagesPage(conversationId: string, limit?: number, before?: string): Promise<CanonMessagesPage>;
|
|
19
21
|
sendMessage(conversationId: string, text: string, options?: SendMessageOptions): Promise<{
|
|
20
22
|
messageId: string;
|
|
21
23
|
}>;
|
|
22
24
|
createConversation(options: CreateConversationOptions): Promise<{
|
|
23
25
|
conversationId: string;
|
|
24
26
|
}>;
|
|
25
|
-
uploadMedia(conversationId: string, data: string, mimeType: string): Promise<{
|
|
27
|
+
uploadMedia(conversationId: string, data: string, mimeType: string, fileName?: string): Promise<{
|
|
26
28
|
url: string;
|
|
29
|
+
attachment: MediaAttachment;
|
|
27
30
|
}>;
|
|
28
31
|
updateTopic(conversationId: string, topic: string): Promise<void>;
|
|
29
32
|
deleteMessage(conversationId: string, messageId: string): Promise<void>;
|
|
33
|
+
updateMessageDisposition(conversationId: string, messageId: string, inboundDisposition: InboundDisposition): Promise<void>;
|
|
30
34
|
markAsRead(conversationId: string): Promise<void>;
|
|
31
35
|
leaveConversation(conversationId: string): Promise<void>;
|
|
32
36
|
react(conversationId: string, messageId: string, emoji: string): Promise<void>;
|
package/dist/client.js
CHANGED
|
@@ -53,6 +53,16 @@ export class CanonClient {
|
|
|
53
53
|
const data = await res.json();
|
|
54
54
|
return data.messages;
|
|
55
55
|
}
|
|
56
|
+
async getMessagesPage(conversationId, limit = 50, before) {
|
|
57
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
58
|
+
if (before)
|
|
59
|
+
params.set('before', before);
|
|
60
|
+
params.set('includeBehavior', '1');
|
|
61
|
+
const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages?${params}`, { headers: this.authHeaders() });
|
|
62
|
+
if (!res.ok)
|
|
63
|
+
throw new CanonApiError(res.status, await res.text());
|
|
64
|
+
return res.json();
|
|
65
|
+
}
|
|
56
66
|
async sendMessage(conversationId, text, options) {
|
|
57
67
|
const res = await fetch(`${this.baseUrl}/messages/send`, {
|
|
58
68
|
method: 'POST',
|
|
@@ -73,11 +83,11 @@ export class CanonClient {
|
|
|
73
83
|
throw new CanonApiError(res.status, await res.text());
|
|
74
84
|
return res.json();
|
|
75
85
|
}
|
|
76
|
-
async uploadMedia(conversationId, data, mimeType) {
|
|
86
|
+
async uploadMedia(conversationId, data, mimeType, fileName) {
|
|
77
87
|
const res = await fetch(`${this.baseUrl}/media/upload`, {
|
|
78
88
|
method: 'POST',
|
|
79
89
|
headers: this.authHeaders(),
|
|
80
|
-
body: JSON.stringify({ conversationId, mimeType, data }),
|
|
90
|
+
body: JSON.stringify({ conversationId, mimeType, data, ...(fileName ? { fileName } : {}) }),
|
|
81
91
|
});
|
|
82
92
|
if (!res.ok)
|
|
83
93
|
throw new CanonApiError(res.status, await res.text());
|
|
@@ -100,6 +110,15 @@ export class CanonClient {
|
|
|
100
110
|
if (!res.ok)
|
|
101
111
|
throw new CanonApiError(res.status, await res.text());
|
|
102
112
|
}
|
|
113
|
+
async updateMessageDisposition(conversationId, messageId, inboundDisposition) {
|
|
114
|
+
const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/messages/${messageId}/disposition`, {
|
|
115
|
+
method: 'PATCH',
|
|
116
|
+
headers: this.authHeaders(),
|
|
117
|
+
body: JSON.stringify({ inboundDisposition }),
|
|
118
|
+
});
|
|
119
|
+
if (!res.ok)
|
|
120
|
+
throw new CanonApiError(res.status, await res.text());
|
|
121
|
+
}
|
|
103
122
|
async markAsRead(conversationId) {
|
|
104
123
|
const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/read`, {
|
|
105
124
|
method: 'POST',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { WorkspaceOption } from './types.js';
|
|
2
|
+
export type ExecutionEnvironmentMode = 'worktree' | 'locked';
|
|
3
|
+
export interface PreparedExecutionEnvironment {
|
|
4
|
+
cwd: string;
|
|
5
|
+
baseCwd: string;
|
|
6
|
+
mode: ExecutionEnvironmentMode;
|
|
7
|
+
repoRoot?: string;
|
|
8
|
+
branch?: string;
|
|
9
|
+
worktreePath?: string;
|
|
10
|
+
lockPath?: string;
|
|
11
|
+
reason?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class ExecutionEnvironmentError extends Error {
|
|
14
|
+
readonly userMessage: string;
|
|
15
|
+
constructor(message: string, userMessage?: string);
|
|
16
|
+
}
|
|
17
|
+
interface WorkspaceResolverOption {
|
|
18
|
+
id: string;
|
|
19
|
+
cwd: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SessionWorkspaceConfig {
|
|
25
|
+
workspaceId?: string;
|
|
26
|
+
legacyCwd?: string;
|
|
27
|
+
model?: string;
|
|
28
|
+
}
|
|
29
|
+
export declare function normalizeOptionalString(value: unknown): string | undefined;
|
|
30
|
+
export declare function isEnabledFlag(value: unknown): boolean;
|
|
31
|
+
export declare function buildConversationEnvironmentKey(conversationId: string, workspaceCwd: string): string;
|
|
32
|
+
export declare function buildWorkspaceOptionId(workspaceCwd: string): string;
|
|
33
|
+
export declare function buildConfiguredWorkspaceOptions(primaryCwd: string, configured: string[]): ConfiguredWorkspaceOption[];
|
|
34
|
+
export declare function buildPublicWorkspaceOptions(workspaceOptions: Array<Pick<ConfiguredWorkspaceOption, 'id' | 'label'>>): WorkspaceOption[];
|
|
35
|
+
export declare function readSessionWorkspaceConfig(raw: unknown): SessionWorkspaceConfig | null;
|
|
36
|
+
export declare function resolveConfiguredWorkspaceCwd(input: {
|
|
37
|
+
workspaceOptions: WorkspaceResolverOption[];
|
|
38
|
+
config: {
|
|
39
|
+
workspaceId?: string;
|
|
40
|
+
legacyCwd?: string;
|
|
41
|
+
} | null;
|
|
42
|
+
defaultCwd: string;
|
|
43
|
+
}): string;
|
|
44
|
+
export declare function buildConversationWorktreeSpec(input: {
|
|
45
|
+
agentId: string;
|
|
46
|
+
conversationId: string;
|
|
47
|
+
workspaceCwd: string;
|
|
48
|
+
}): {
|
|
49
|
+
branch: string;
|
|
50
|
+
worktreePath: string;
|
|
51
|
+
};
|
|
52
|
+
export declare function prepareConversationEnvironment(input: {
|
|
53
|
+
agentId: string;
|
|
54
|
+
conversationId: string;
|
|
55
|
+
workspaceCwd: string;
|
|
56
|
+
allowWorktrees?: boolean;
|
|
57
|
+
}): PreparedExecutionEnvironment;
|
|
58
|
+
export declare function releaseConversationEnvironment(environment: Pick<PreparedExecutionEnvironment, 'lockPath'>): void;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { CANON_DIR, isProcessAlive } from './agent-profiles.js';
|
|
6
|
+
export class ExecutionEnvironmentError extends Error {
|
|
7
|
+
userMessage;
|
|
8
|
+
constructor(message, userMessage = message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'ExecutionEnvironmentError';
|
|
11
|
+
this.userMessage = userMessage;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const WORKTREE_ROOT = join(CANON_DIR, 'conversation-worktrees');
|
|
15
|
+
const WORKSPACE_LOCKS_ROOT = join(CANON_DIR, 'workspace-locks');
|
|
16
|
+
export function normalizeOptionalString(value) {
|
|
17
|
+
if (typeof value !== 'string')
|
|
18
|
+
return undefined;
|
|
19
|
+
const trimmed = value.trim();
|
|
20
|
+
return trimmed ? trimmed : undefined;
|
|
21
|
+
}
|
|
22
|
+
export function isEnabledFlag(value) {
|
|
23
|
+
if (value === true)
|
|
24
|
+
return true;
|
|
25
|
+
if (typeof value !== 'string')
|
|
26
|
+
return false;
|
|
27
|
+
const normalized = value.trim().toLowerCase();
|
|
28
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
29
|
+
}
|
|
30
|
+
function shortHash(value) {
|
|
31
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 12);
|
|
32
|
+
}
|
|
33
|
+
function sanitizeSegment(value) {
|
|
34
|
+
const normalized = value
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
37
|
+
.replace(/^-+|-+$/g, '');
|
|
38
|
+
return normalized.slice(0, 24) || 'workspace';
|
|
39
|
+
}
|
|
40
|
+
function runGit(cwd, args) {
|
|
41
|
+
const result = spawnSync('git', ['-C', cwd, ...args], {
|
|
42
|
+
encoding: 'utf-8',
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
ok: result.status === 0,
|
|
46
|
+
stdout: result.stdout ?? '',
|
|
47
|
+
stderr: result.stderr ?? '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function detectRepoRoot(cwd) {
|
|
51
|
+
const result = runGit(cwd, ['rev-parse', '--show-toplevel']);
|
|
52
|
+
if (!result.ok)
|
|
53
|
+
return null;
|
|
54
|
+
const repoRoot = result.stdout.trim();
|
|
55
|
+
return repoRoot ? resolve(repoRoot) : null;
|
|
56
|
+
}
|
|
57
|
+
function isRepoClean(repoRoot) {
|
|
58
|
+
const result = runGit(repoRoot, ['status', '--porcelain']);
|
|
59
|
+
return result.ok && result.stdout.trim() === '';
|
|
60
|
+
}
|
|
61
|
+
function branchExists(repoRoot, branch) {
|
|
62
|
+
return runGit(repoRoot, ['show-ref', '--verify', '--quiet', `refs/heads/${branch}`]).ok;
|
|
63
|
+
}
|
|
64
|
+
function loadWorkspaceLock(lockPath) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(readFileSync(lockPath, 'utf-8'));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function cleanupStaleWorkspaceLock(lockPath) {
|
|
73
|
+
const record = loadWorkspaceLock(lockPath);
|
|
74
|
+
if (!record) {
|
|
75
|
+
try {
|
|
76
|
+
unlinkSync(lockPath);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (isProcessAlive(record.pid))
|
|
84
|
+
return false;
|
|
85
|
+
try {
|
|
86
|
+
unlinkSync(lockPath);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function acquireWorkspaceLock(input) {
|
|
94
|
+
mkdirSync(WORKSPACE_LOCKS_ROOT, { recursive: true });
|
|
95
|
+
const workspaceCwd = resolve(input.workspaceCwd);
|
|
96
|
+
const lockPath = join(WORKSPACE_LOCKS_ROOT, `${shortHash(workspaceCwd)}.json`);
|
|
97
|
+
const record = {
|
|
98
|
+
pid: process.pid,
|
|
99
|
+
agentId: input.agentId,
|
|
100
|
+
conversationId: input.conversationId,
|
|
101
|
+
workspaceCwd,
|
|
102
|
+
createdAt: new Date().toISOString(),
|
|
103
|
+
};
|
|
104
|
+
while (true) {
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(lockPath, JSON.stringify(record, null, 2), { flag: 'wx' });
|
|
107
|
+
return lockPath;
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
const code = error.code;
|
|
111
|
+
if (code !== 'EEXIST') {
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
const existing = loadWorkspaceLock(lockPath);
|
|
115
|
+
if (!existing) {
|
|
116
|
+
if (cleanupStaleWorkspaceLock(lockPath))
|
|
117
|
+
continue;
|
|
118
|
+
throw new Error(`Workspace lock file is unreadable: ${workspaceCwd}`);
|
|
119
|
+
}
|
|
120
|
+
if (!isProcessAlive(existing.pid)) {
|
|
121
|
+
if (cleanupStaleWorkspaceLock(lockPath))
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (existing.pid === process.pid
|
|
125
|
+
&& existing.agentId === input.agentId
|
|
126
|
+
&& existing.conversationId === input.conversationId) {
|
|
127
|
+
return lockPath;
|
|
128
|
+
}
|
|
129
|
+
throw new ExecutionEnvironmentError(`Workspace is already in use by Canon session ${existing.conversationId.slice(0, 8)} (PID ${existing.pid}).`, 'That workspace is already in use by another Canon coding session on this machine.');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export function buildConversationEnvironmentKey(conversationId, workspaceCwd) {
|
|
134
|
+
return `${conversationId}:${resolve(workspaceCwd)}`;
|
|
135
|
+
}
|
|
136
|
+
export function buildWorkspaceOptionId(workspaceCwd) {
|
|
137
|
+
return `workspace-${shortHash(resolve(workspaceCwd))}`;
|
|
138
|
+
}
|
|
139
|
+
export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
|
|
140
|
+
const uniqueDirs = Array.from(new Set([primaryCwd, ...configured].map((dir) => resolve(dir))));
|
|
141
|
+
const seenLabels = new Map();
|
|
142
|
+
return uniqueDirs.map((cwd) => {
|
|
143
|
+
const baseLabel = basename(cwd) || cwd;
|
|
144
|
+
const seenCount = (seenLabels.get(baseLabel) ?? 0) + 1;
|
|
145
|
+
seenLabels.set(baseLabel, seenCount);
|
|
146
|
+
return {
|
|
147
|
+
id: buildWorkspaceOptionId(cwd),
|
|
148
|
+
label: seenCount === 1 ? baseLabel : `${baseLabel} (${seenCount})`,
|
|
149
|
+
cwd,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
export function buildPublicWorkspaceOptions(workspaceOptions) {
|
|
154
|
+
return workspaceOptions.map(({ id, label }) => ({ id, label }));
|
|
155
|
+
}
|
|
156
|
+
export function readSessionWorkspaceConfig(raw) {
|
|
157
|
+
if (!raw || typeof raw !== 'object')
|
|
158
|
+
return null;
|
|
159
|
+
const data = raw;
|
|
160
|
+
return {
|
|
161
|
+
workspaceId: normalizeOptionalString(data.workspaceId),
|
|
162
|
+
legacyCwd: normalizeOptionalString(data.cwd),
|
|
163
|
+
model: normalizeOptionalString(data.model),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function findWorkspaceByLegacyCwd(workspaceOptions, legacyCwd) {
|
|
167
|
+
if (!legacyCwd)
|
|
168
|
+
return undefined;
|
|
169
|
+
const resolvedLegacyCwd = resolve(legacyCwd);
|
|
170
|
+
return workspaceOptions.find((workspace) => resolve(workspace.cwd) === resolvedLegacyCwd);
|
|
171
|
+
}
|
|
172
|
+
export function resolveConfiguredWorkspaceCwd(input) {
|
|
173
|
+
const fallbackCwd = input.workspaceOptions[0]?.cwd ?? resolve(input.defaultCwd);
|
|
174
|
+
if (!input.config)
|
|
175
|
+
return fallbackCwd;
|
|
176
|
+
const legacyWorkspace = findWorkspaceByLegacyCwd(input.workspaceOptions, input.config.legacyCwd);
|
|
177
|
+
const workspaceId = input.config.workspaceId;
|
|
178
|
+
if (workspaceId) {
|
|
179
|
+
const workspace = input.workspaceOptions.find((option) => option.id === workspaceId);
|
|
180
|
+
if (workspace)
|
|
181
|
+
return workspace.cwd;
|
|
182
|
+
if (workspaceId === 'default') {
|
|
183
|
+
return fallbackCwd;
|
|
184
|
+
}
|
|
185
|
+
const legacyWorkspaceMatch = /^workspace-(\d+)$/.exec(workspaceId);
|
|
186
|
+
if (legacyWorkspaceMatch) {
|
|
187
|
+
const legacyIndex = Number.parseInt(legacyWorkspaceMatch[1] ?? '', 10) - 1;
|
|
188
|
+
if (legacyIndex >= 0 && input.workspaceOptions[legacyIndex]) {
|
|
189
|
+
return input.workspaceOptions[legacyIndex].cwd;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (legacyWorkspace)
|
|
193
|
+
return legacyWorkspace.cwd;
|
|
194
|
+
throw new ExecutionEnvironmentError(`Workspace ${workspaceId} is not configured on this machine.`, 'The workspace saved for this Canon coding session is no longer configured on this machine.');
|
|
195
|
+
}
|
|
196
|
+
if (legacyWorkspace)
|
|
197
|
+
return legacyWorkspace.cwd;
|
|
198
|
+
return fallbackCwd;
|
|
199
|
+
}
|
|
200
|
+
export function buildConversationWorktreeSpec(input) {
|
|
201
|
+
const resolvedWorkspace = resolve(input.workspaceCwd);
|
|
202
|
+
const baseLabel = sanitizeSegment(basename(resolvedWorkspace) || 'workspace');
|
|
203
|
+
const workspaceHash = shortHash(resolvedWorkspace);
|
|
204
|
+
const conversationHash = shortHash(`${input.agentId}:${input.conversationId}`);
|
|
205
|
+
return {
|
|
206
|
+
branch: `canon/${baseLabel}-${workspaceHash.slice(0, 6)}-${conversationHash.slice(0, 8)}`,
|
|
207
|
+
worktreePath: join(WORKTREE_ROOT, baseLabel, workspaceHash, conversationHash),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
export function prepareConversationEnvironment(input) {
|
|
211
|
+
const baseCwd = resolve(input.workspaceCwd);
|
|
212
|
+
const allowWorktrees = input.allowWorktrees === true;
|
|
213
|
+
if (!allowWorktrees) {
|
|
214
|
+
return {
|
|
215
|
+
cwd: baseCwd,
|
|
216
|
+
baseCwd,
|
|
217
|
+
mode: 'locked',
|
|
218
|
+
lockPath: acquireWorkspaceLock({
|
|
219
|
+
agentId: input.agentId,
|
|
220
|
+
conversationId: input.conversationId,
|
|
221
|
+
workspaceCwd: baseCwd,
|
|
222
|
+
}),
|
|
223
|
+
reason: 'Worktree isolation is disabled for this host',
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
const repoRoot = detectRepoRoot(baseCwd);
|
|
227
|
+
if (repoRoot) {
|
|
228
|
+
const spec = buildConversationWorktreeSpec(input);
|
|
229
|
+
const relativeCwd = relative(repoRoot, baseCwd);
|
|
230
|
+
const sessionCwd = relativeCwd ? join(spec.worktreePath, relativeCwd) : spec.worktreePath;
|
|
231
|
+
if (existsSync(spec.worktreePath)) {
|
|
232
|
+
if (detectRepoRoot(spec.worktreePath)) {
|
|
233
|
+
return {
|
|
234
|
+
cwd: sessionCwd,
|
|
235
|
+
baseCwd,
|
|
236
|
+
mode: 'worktree',
|
|
237
|
+
repoRoot,
|
|
238
|
+
branch: spec.branch,
|
|
239
|
+
worktreePath: spec.worktreePath,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else if (isRepoClean(repoRoot)) {
|
|
244
|
+
mkdirSync(dirname(spec.worktreePath), { recursive: true });
|
|
245
|
+
const addArgs = branchExists(repoRoot, spec.branch)
|
|
246
|
+
? ['worktree', 'add', spec.worktreePath, spec.branch]
|
|
247
|
+
: ['worktree', 'add', '-b', spec.branch, spec.worktreePath, 'HEAD'];
|
|
248
|
+
const addResult = runGit(repoRoot, addArgs);
|
|
249
|
+
if (addResult.ok && detectRepoRoot(spec.worktreePath)) {
|
|
250
|
+
return {
|
|
251
|
+
cwd: sessionCwd,
|
|
252
|
+
baseCwd,
|
|
253
|
+
mode: 'worktree',
|
|
254
|
+
repoRoot,
|
|
255
|
+
branch: spec.branch,
|
|
256
|
+
worktreePath: spec.worktreePath,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
cwd: baseCwd,
|
|
263
|
+
baseCwd,
|
|
264
|
+
mode: 'locked',
|
|
265
|
+
lockPath: acquireWorkspaceLock({
|
|
266
|
+
agentId: input.agentId,
|
|
267
|
+
conversationId: input.conversationId,
|
|
268
|
+
workspaceCwd: baseCwd,
|
|
269
|
+
}),
|
|
270
|
+
reason: repoRoot
|
|
271
|
+
? 'Base repository is dirty or worktree creation failed'
|
|
272
|
+
: 'Workspace is not a git repository',
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
export function releaseConversationEnvironment(environment) {
|
|
276
|
+
if (!environment.lockPath)
|
|
277
|
+
return;
|
|
278
|
+
try {
|
|
279
|
+
unlinkSync(environment.lockPath);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
// Ignore lock cleanup failures; stale locks are cleaned up on next acquire.
|
|
283
|
+
}
|
|
284
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
export { AGENT_CAPABILITIES, } from './types.js';
|
|
2
|
-
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, AgentContext, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
2
|
+
export type { AgentCapabilities, AgentClientType, CanonMessage, CanonConversation, CanonMessagesPage, AgentContext, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, WorkspaceOption, } from './types.js';
|
|
3
3
|
export { CanonClient, CanonApiError } from './client.js';
|
|
4
4
|
export { CanonStream } from './stream.js';
|
|
5
5
|
export type { StreamHandler } from './stream.js';
|
|
6
|
+
export type { PolicyRole, ParticipationStyle, RepresentationMode, PermissionLevel, ConversationScope, AgentBehaviorSettings, Participant, Relationship, ContextOverlay, BehaviorProfile, AdmissionPolicy, RuntimeControlPolicy, ActionApprovalPolicy, ParticipationPolicy, WorkSession, ResolvedPolicy, ResolvedTurnEligibility, ResolvedAgentBehaviorPolicy, ParticipationHistoryMessage, ParticipationHistorySnapshot, ParticipationDecisionInput, ParticipationDecision, } from './policy.js';
|
|
7
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
8
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
9
|
+
export type { DeliveryIntent, TurnMessageSemantics, InboundDisposition, TurnLifecycleState, RuntimeCapabilities, TurnState, TurnMetadata, TriggerDecision, } from './turn-protocol.js';
|
|
6
10
|
export { registerAndWaitForApproval } from './registration.js';
|
|
7
11
|
export { ApprovalManager } from './approval-manager.js';
|
|
8
12
|
export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
|
|
@@ -12,8 +16,10 @@ export { createStreamingHelper } from './streaming.js';
|
|
|
12
16
|
export type { RTDBHandle, RTDBRef, ServerTimestamp, StreamingHelperOptions, StreamingNode } from './streaming.js';
|
|
13
17
|
export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
|
|
14
18
|
export type { AgentProfile } from './agent-profiles.js';
|
|
15
|
-
export { resolveCanonAgent, getActiveProfile } from './agent-resolver.js';
|
|
19
|
+
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
16
20
|
export type { ResolvedAgent } from './agent-resolver.js';
|
|
17
|
-
export {
|
|
18
|
-
export type {
|
|
21
|
+
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
22
|
+
export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
|
|
23
|
+
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
24
|
+
export type { SessionStatePayload, TurnStatePayload } from './rtdb-rest.js';
|
|
19
25
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,9 @@ export { AGENT_CAPABILITIES, } from './types.js';
|
|
|
4
4
|
export { CanonClient, CanonApiError } from './client.js';
|
|
5
5
|
// Stream
|
|
6
6
|
export { CanonStream } from './stream.js';
|
|
7
|
+
export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots, buildBehaviorPolicyLines, DEFAULT_PARTICIPATION_HISTORY_FETCH_LIMIT, evaluateParticipationPolicy, getDefaultParticipationPolicy, resolveAgentBehaviorPolicy, } from './policy.js';
|
|
8
|
+
// Turn protocol
|
|
9
|
+
export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
|
|
7
10
|
// Registration
|
|
8
11
|
export { registerAndWaitForApproval } from './registration.js';
|
|
9
12
|
// Approval
|
|
@@ -15,8 +18,10 @@ export { createStreamingHelper } from './streaming.js';
|
|
|
15
18
|
// Agent profiles (loading, locking, resolution)
|
|
16
19
|
export { loadProfiles, isProfileLocked, acquireLock, releaseLock, isProcessAlive, CANON_DIR, AGENTS_PATH, LOCKS_DIR, } from './agent-profiles.js';
|
|
17
20
|
// Agent resolver
|
|
18
|
-
export { resolveCanonAgent, getActiveProfile } from './agent-resolver.js';
|
|
21
|
+
export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agent-resolver.js';
|
|
22
|
+
// Execution environments for host-mode coding sessions
|
|
23
|
+
export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, isEnabledFlag, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
|
|
19
24
|
// RTDB REST helpers (token exchange, session state, generic read/write)
|
|
20
|
-
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState } from './rtdb-rest.js';
|
|
25
|
+
export { initRTDBAuth, rtdbWrite, rtdbRead, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
|
|
21
26
|
// Constants
|
|
22
27
|
export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
|