@canonmsg/core 0.7.5 → 0.10.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.
@@ -39,6 +39,9 @@ export function buildAgentSessionSnapshot(input) {
39
39
  ?? input.sessionConfig?.permissionMode
40
40
  ?? input.runtime?.defaultPermissionMode,
41
41
  permissionModeOptions: input.runtime?.availablePermissionModes ?? [],
42
+ effort: input.sessionState?.effort ?? input.sessionConfig?.effort,
43
+ runtimeControlValues: input.sessionState?.runtimeControlValues
44
+ ?? input.sessionConfig?.runtimeControlValues,
42
45
  workspaceId: input.sessionConfig?.workspaceId
43
46
  ?? input.runtime?.defaultWorkspaceId
44
47
  ?? workspaceOptions[0]?.id,
@@ -49,8 +52,13 @@ export function buildAgentSessionSnapshot(input) {
49
52
  ?? input.runtime?.availableExecutionModes
50
53
  ?? [],
51
54
  executionBranch: input.sessionState?.executionBranch,
55
+ resolvedWorkspaceLabel: undefined,
56
+ resolvedCwd: input.sessionState?.cwd,
57
+ worktreePath: input.sessionState?.worktreePath,
58
+ executionFallbackReason: input.sessionState?.executionFallbackReason,
52
59
  state: input.sessionState?.state,
53
60
  turnState: input.turnState?.state,
61
+ supportsQueue: input.turnState?.capabilities?.supportsQueue,
54
62
  queueDepth: input.turnState?.queueDepth ?? 0,
55
63
  waitingForInput: input.turnState?.state === 'waiting_input'
56
64
  || input.sessionState?.state === 'requires_action',
@@ -41,7 +41,7 @@ export class ApprovalManager {
41
41
  const summary = this.summarizeTool(toolName, toolInput);
42
42
  const logMsg = buildApprovalOutcome('', toolName, summary, decision, 'session-rule');
43
43
  this.client.sendMessage(conversationId, logMsg, {
44
- metadata: { type: 'approval_outcome' },
44
+ metadata: { type: 'approval_outcome', decision, reason: 'session-rule' },
45
45
  }).catch(() => { });
46
46
  return { decision };
47
47
  }
@@ -64,7 +64,12 @@ export class ApprovalManager {
64
64
  const summary = this.summarizeTool(toolName, toolInput);
65
65
  const msg = buildApprovalOutcome(approvalId, toolName, summary, 'deny', 'timeout');
66
66
  this.client.sendMessage(conversationId, msg, {
67
- metadata: { type: 'approval_outcome' },
67
+ metadata: {
68
+ type: 'approval_outcome',
69
+ approvalId,
70
+ decision: 'deny',
71
+ reason: 'timeout',
72
+ },
68
73
  }).catch(() => { });
69
74
  resolve({ decision: 'deny' });
70
75
  }, this.config.timeoutSeconds * 1000);
@@ -164,14 +169,19 @@ export class ApprovalManager {
164
169
  // Send confirmation (fire-and-forget)
165
170
  const msg = buildApprovalOutcome(approvalId, entry.toolName, entry.toolSummary, decision, 'replied');
166
171
  this.client.sendMessage(conversationId, msg, {
167
- metadata: { type: 'approval_outcome' },
172
+ metadata: {
173
+ type: 'approval_outcome',
174
+ approvalId,
175
+ decision,
176
+ reason: 'replied',
177
+ },
168
178
  }).catch(() => { });
169
179
  // If session rule was set, log that too
170
180
  if (sessionRule) {
171
181
  const ruleDesc = this.describeRule(sessionRule);
172
182
  this.client
173
183
  .sendMessage(conversationId, `Session rule set: ${ruleDesc}`, {
174
- metadata: { type: 'approval_outcome' },
184
+ metadata: { type: 'approval_outcome', decision, reason: 'session-rule' },
175
185
  })
176
186
  .catch(() => { });
177
187
  }
@@ -7,6 +7,12 @@ export interface ApprovalRequestMetadata {
7
7
  riskLevel?: 'normal' | 'destructive';
8
8
  expiresAt: string;
9
9
  }
10
+ export interface ApprovalOutcomeMetadata {
11
+ type: 'approval_outcome';
12
+ approvalId?: string;
13
+ decision?: 'allow' | 'deny';
14
+ reason?: 'replied' | 'timeout' | 'session-rule';
15
+ }
10
16
  export interface ApprovalReplyMetadata {
11
17
  type: 'approval_reply';
12
18
  approvalId: string;
package/dist/browser.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
2
  export { resolveCanonBaseUrl } from './base-url.js';
3
3
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
4
- export type { AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentSessionWorkSessionSummary, AgentRuntime, CanonContactRequest, CanonContactRequestStatus, ContactApprovedPayload, ContactRequestPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, RuntimeUpdatedPayload, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, } from './types.js';
4
+ export type { AgentCapabilities, AgentClientType, AgentSessionSnapshot, AgentSessionWorkSessionSummary, AgentRuntime, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContactRequest, CanonContactRequestStatus, ContactApprovedPayload, ContactRequestPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeExecutionMetadata, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, ResolvedAdmission, SessionConfig, TurnUpdatedPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
5
5
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, } from './execution-environment-mode.js';
6
6
  export type { ExecutionEnvironmentMode } from './execution-environment-mode.js';
7
7
  export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, CreateWorkSessionOptions, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
@@ -12,6 +12,6 @@ export { buildParticipationHistorySnapshot, buildParticipationHistorySnapshots,
12
12
  export { DEFAULT_RUNTIME_CAPABILITIES, FINAL_MESSAGE_HANDOFF_MS, isTurnOpen, normalizeTurnMetadata, normalizeTurnState, resolveTurnMessageSemantics, shouldPromoteConversationMessage, shouldTriggerAgentTurn, } from './turn-protocol.js';
13
13
  export type { DeliveryIntent, InboundDisposition, RuntimeCapabilities, TriggerDecision, TurnLifecycleState, TurnMessageSemantics, TurnMetadata, TurnState, } from './turn-protocol.js';
14
14
  export { buildApprovalReply, buildApprovalRequest, buildApprovalOutcome, generateApprovalId, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
15
- export type { ApprovalRequestMetadata, ApprovalReplyMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
15
+ export type { ApprovalRequestMetadata, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
16
16
  export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, } from './runtime-cards.js';
17
17
  export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
@@ -1,4 +1,4 @@
1
- import type { WorkspaceOption } from './types.js';
1
+ import type { WorkspaceOption, WorkspaceOptionSource } from './types.js';
2
2
  import { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode, type ExecutionEnvironmentMode } from './execution-environment-mode.js';
3
3
  export { EXECUTION_ENVIRONMENT_MODES, isExecutionEnvironmentMode };
4
4
  export type { ExecutionEnvironmentMode };
@@ -22,6 +22,10 @@ interface WorkspaceResolverOption {
22
22
  }
23
23
  export interface ConfiguredWorkspaceOption extends WorkspaceResolverOption {
24
24
  label: string;
25
+ description?: string;
26
+ workspaceRootId?: string;
27
+ workspaceRelativePath?: string;
28
+ source?: WorkspaceOptionSource;
25
29
  }
26
30
  export interface SessionWorkspaceConfig {
27
31
  workspaceId?: string;
@@ -34,7 +38,7 @@ export declare function isEnabledFlag(value: unknown): boolean;
34
38
  export declare function buildConversationEnvironmentKey(conversationId: string, workspaceCwd: string): string;
35
39
  export declare function buildWorkspaceOptionId(workspaceCwd: string): string;
36
40
  export declare function buildConfiguredWorkspaceOptions(primaryCwd: string, configured: string[]): ConfiguredWorkspaceOption[];
37
- export declare function buildPublicWorkspaceOptions(workspaceOptions: Array<Pick<ConfiguredWorkspaceOption, 'id' | 'label'>>): WorkspaceOption[];
41
+ export declare function buildPublicWorkspaceOptions(workspaceOptions: Array<Pick<ConfiguredWorkspaceOption, 'id' | 'label'> & Partial<Pick<ConfiguredWorkspaceOption, 'description' | 'workspaceRootId' | 'workspaceRelativePath' | 'source'>>>): WorkspaceOption[];
38
42
  export declare function readSessionWorkspaceConfig(raw: unknown): SessionWorkspaceConfig | null;
39
43
  export declare function resolveConfiguredWorkspaceCwd(input: {
40
44
  workspaceOptions: WorkspaceResolverOption[];
@@ -93,7 +93,7 @@ export function buildWorkspaceOptionId(workspaceCwd) {
93
93
  export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
94
94
  const uniqueDirs = Array.from(new Set([primaryCwd, ...configured].map((dir) => resolve(dir))));
95
95
  const seenLabels = new Map();
96
- return uniqueDirs.map((cwd) => {
96
+ return uniqueDirs.map((cwd, index) => {
97
97
  const baseLabel = basename(cwd) || cwd;
98
98
  const seenCount = (seenLabels.get(baseLabel) ?? 0) + 1;
99
99
  seenLabels.set(baseLabel, seenCount);
@@ -101,11 +101,19 @@ export function buildConfiguredWorkspaceOptions(primaryCwd, configured) {
101
101
  id: buildWorkspaceOptionId(cwd),
102
102
  label: seenCount === 1 ? baseLabel : `${baseLabel} (${seenCount})`,
103
103
  cwd,
104
+ source: index === 0 ? 'default' : 'explicit',
104
105
  };
105
106
  });
106
107
  }
107
108
  export function buildPublicWorkspaceOptions(workspaceOptions) {
108
- return workspaceOptions.map(({ id, label }) => ({ id, label }));
109
+ return workspaceOptions.map((workspace) => ({
110
+ id: workspace.id,
111
+ label: workspace.label,
112
+ ...(workspace.description ? { description: workspace.description } : {}),
113
+ ...(workspace.workspaceRootId ? { workspaceRootId: workspace.workspaceRootId } : {}),
114
+ ...(workspace.workspaceRelativePath ? { workspaceRelativePath: workspace.workspaceRelativePath } : {}),
115
+ ...(workspace.source ? { source: workspace.source } : {}),
116
+ }));
109
117
  }
110
118
  function isRetiredWorkspaceId(value) {
111
119
  if (!value)
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
2
- export type { AgentCapabilities, AgentClientType, CanonContactRequest, CanonContactRequestStatus, ContactApprovedPayload, ContactRequestPayload, CanonMessage, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, AgentSessionWorkSessionSummary, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, ModelOption, PermissionModeOption, WorkspaceOption, } from './types.js';
2
+ export type { AgentCapabilities, AgentClientType, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContactRequest, CanonContactRequestStatus, ContactApprovedPayload, ContactRequestPayload, CanonMessage, CanonConversation, CanonMessagesPage, CreateContactRequestResult, AgentContext, CanonStreamEvent, AgentSessionSnapshot, AgentSessionWorkSessionSummary, ResolvedAdmission, MediaAttachment, MediaAttachmentKind, MessageCreatedPayload, TypingPayload, PresencePayload, RuntimeUpdatedPayload, TurnUpdatedPayload, SendMessageOptions, CreateConversationOptions, RegistrationInput, RegistrationResult, RegistrationStatus, StreamingStatus, SetStreamingOptions, SessionControl, SessionState, SessionConfig, AgentRuntime, CanonRuntimeDescriptor, CanonRuntimeActionAvailability, CanonRuntimeActionCategory, CanonRuntimeActionDescriptor, CanonRuntimeActionDispatch, CanonRuntimeActionPlacement, CanonRuntimeExecutionMetadata, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, WorkspaceOption, WorkspaceOptionSource, } from './types.js';
3
3
  export type { CanonResolvedWorkSession, CanonWorkSession, CanonWorkSessionContext, CanonWorkSessionConversationRole, CreateWorkSessionOptions, CanonWorkSessionDisclosureMode, CanonWorkSessionParticipant, CanonWorkSessionStatus, SendLinkedMessageOptions, SendLinkedMessageResult, UpdateWorkSessionConversationOptions, WorkSessionPromptRenderOptions, } from './work-session.js';
4
4
  export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
5
+ export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, buildWorkspaceRootId, discoverWorkspaceProjects, } from './workspace-discovery.js';
6
+ export type { ConfiguredWorkspaceRoot, WorkspaceDiscoveryResult, } from './workspace-discovery.js';
5
7
  export { CanonClient, CanonApiError } from './client.js';
6
8
  export { buildAgentSessionSnapshot } from './agent-session.js';
7
9
  export { CanonStream } from './stream.js';
@@ -14,7 +16,7 @@ export { registerAndWaitForApproval } from './registration.js';
14
16
  export { ApprovalManager } from './approval-manager.js';
15
17
  export { generateApprovalId, buildApprovalRequest, buildApprovalReply, buildApprovalOutcome, parseTextApprovalReply, redactSecrets, } from './approval-format.js';
16
18
  export { DEFAULT_APPROVAL_CONFIG, } from './approval-types.js';
17
- export type { ApprovalRequestMetadata, ApprovalReplyMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
19
+ export type { ApprovalRequestMetadata, ApprovalReplyMetadata, ApprovalOutcomeMetadata, SessionRule, ApprovalResult, ApprovalConfig, } from './approval-types.js';
18
20
  export { buildPlanApprovalReply, buildPlanApprovalRequest, buildQuestionReply, buildQuestionRequest, } from './runtime-cards.js';
19
21
  export type { ClaudeQuestionMetadata, ClaudeQuestionReplyMetadata, PlanApprovalMetadata, PlanApprovalReplyMetadata, RuntimeQuestionDefinition, RuntimeQuestionOption, } from './runtime-cards.js';
20
22
  export { createStreamingHelper } from './streaming.js';
@@ -25,7 +27,7 @@ export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agen
25
27
  export type { ResolvedAgent } from './agent-resolver.js';
26
28
  export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
27
29
  export type { ConfiguredWorkspaceOption, ExecutionEnvironmentMode, PreparedExecutionEnvironment, SessionWorkspaceConfig, } from './execution-environment.js';
28
- export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
29
- export type { AgentSessionSnapshotPatch, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
30
+ export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
31
+ export type { AgentSessionSnapshotPatch, RuntimeInfoPayloadData, SessionStatePayload, TurnStatePayload, } from './rtdb-rest.js';
30
32
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
31
33
  export { resolveCanonBaseUrl } from './base-url.js';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Types
2
2
  export { AGENT_CAPABILITIES, CLAUDE_PERMISSION_MODE_OPTIONS, } from './types.js';
3
3
  export { buildWorkSessionPromptLines, buildWorkSessionsPromptLines, mergeWorkSessionContexts, } from './work-session.js';
4
+ export { buildConfiguredWorkspaceOptionsWithRoots, buildPublicWorkspaceRoots, buildWorkspaceRootId, discoverWorkspaceProjects, } from './workspace-discovery.js';
4
5
  // Client
5
6
  export { CanonClient, CanonApiError } from './client.js';
6
7
  export { buildAgentSessionSnapshot } from './agent-session.js';
@@ -25,7 +26,7 @@ export { resolveCanonAgent, resolveCanonProfile, getActiveProfile } from './agen
25
26
  // Execution environments for host-mode coding sessions
26
27
  export { buildConfiguredWorkspaceOptions, buildConversationEnvironmentKey, buildConversationWorktreeSpec, buildPublicWorkspaceOptions, buildWorkspaceOptionId, EXECUTION_ENVIRONMENT_MODES, isEnabledFlag, isExecutionEnvironmentMode, normalizeOptionalString, readSessionWorkspaceConfig, resolveConfiguredWorkspaceCwd, ExecutionEnvironmentError, prepareConversationEnvironment, releaseConversationEnvironment, } from './execution-environment.js';
27
28
  // RTDB REST helpers (token exchange, session state, generic read/write)
28
- export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
29
+ export { initRTDBAuth, rtdbWrite, rtdbRead, patchAgentSessionSnapshot, patchRuntimeInfo, writeRuntimeInfo, clearRuntimeInfo, writeSessionState, clearSessionState, writeTurnState, clearTurnState, } from './rtdb-rest.js';
29
30
  // Constants
30
31
  export { DEFAULT_BASE_URL, DEFAULT_STREAM_URL, DEFAULT_RTDB_URL, FIREBASE_WEB_API_KEY } from './constants.js';
31
32
  // Base URL resolver
@@ -7,14 +7,18 @@
7
7
  */
8
8
  import type { CanonClient } from './client.js';
9
9
  import type { DeliveryIntent, RuntimeCapabilities, TurnLifecycleState } from './turn-protocol.js';
10
+ import type { CanonControlValue, RuntimeInfoPayload } from './types.js';
10
11
  export interface SessionStatePayload {
11
12
  lastError?: string;
12
13
  model?: string;
13
14
  permissionMode?: string;
14
15
  effort?: string;
16
+ runtimeControlValues?: Record<string, CanonControlValue>;
15
17
  cwd?: string;
16
18
  executionMode?: 'worktree' | 'locked';
17
19
  executionBranch?: string;
20
+ worktreePath?: string;
21
+ executionFallbackReason?: string;
18
22
  hostMode?: boolean;
19
23
  clientType?: string;
20
24
  isActive: boolean;
@@ -63,6 +67,8 @@ export interface AgentSessionSnapshotPatch {
63
67
  value: string;
64
68
  label: string;
65
69
  }> | null;
70
+ effort?: string | null;
71
+ runtimeControlValues?: Record<string, CanonControlValue> | null;
66
72
  workspaceId?: string | null;
67
73
  workspaceOptions?: Array<{
68
74
  id: string;
@@ -71,8 +77,12 @@ export interface AgentSessionSnapshotPatch {
71
77
  executionMode?: 'worktree' | 'locked' | null;
72
78
  availableExecutionModes?: Array<'worktree' | 'locked'> | null;
73
79
  executionBranch?: string | null;
80
+ resolvedCwd?: string | null;
81
+ worktreePath?: string | null;
82
+ executionFallbackReason?: string | null;
74
83
  state?: 'idle' | 'running' | 'requires_action' | null;
75
84
  turnState?: TurnLifecycleState | null;
85
+ supportsQueue?: boolean | null;
76
86
  queueDepth?: number;
77
87
  waitingForInput?: boolean;
78
88
  contextUsage?: {
@@ -95,6 +105,11 @@ export interface AgentSessionSnapshotPatch {
95
105
  '.sv': 'timestamp';
96
106
  };
97
107
  }
108
+ export interface RuntimeInfoPayloadData extends Omit<RuntimeInfoPayload, 'updatedAt'> {
109
+ updatedAt: {
110
+ '.sv': 'timestamp';
111
+ };
112
+ }
98
113
  interface RTDBAuthOptions {
99
114
  rtdbUrl?: string;
100
115
  firebaseApiKey?: string;
@@ -109,6 +124,9 @@ interface RTDBClientHandle {
109
124
  writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
110
125
  clearTurnState(conversationId: string, agentId: string): Promise<void>;
111
126
  patchAgentSessionSnapshot(conversationId: string, agentId: string, snapshot: Omit<AgentSessionSnapshotPatch, 'updatedAt'>): Promise<void>;
127
+ writeRuntimeInfo(conversationId: string, agentId: string, payload: Omit<RuntimeInfoPayload, 'updatedAt'>): Promise<void>;
128
+ patchRuntimeInfo(conversationId: string, agentId: string, payload: Partial<Omit<RuntimeInfoPayload, 'updatedAt'>>): Promise<void>;
129
+ clearRuntimeInfo(conversationId: string, agentId: string): Promise<void>;
112
130
  }
113
131
  /**
114
132
  * Initializes the default RTDB helper and returns a scoped client for callers
@@ -131,4 +149,7 @@ export declare function clearSessionState(conversationId: string, agentId: strin
131
149
  export declare function writeTurnState(conversationId: string, agentId: string, state: Omit<TurnStatePayload, 'updatedAt'>): Promise<void>;
132
150
  export declare function clearTurnState(conversationId: string, agentId: string): Promise<void>;
133
151
  export declare function patchAgentSessionSnapshot(conversationId: string, agentId: string, snapshot: Omit<AgentSessionSnapshotPatch, 'updatedAt'>): Promise<void>;
152
+ export declare function writeRuntimeInfo(conversationId: string, agentId: string, payload: Omit<RuntimeInfoPayload, 'updatedAt'>): Promise<void>;
153
+ export declare function patchRuntimeInfo(conversationId: string, agentId: string, payload: Partial<Omit<RuntimeInfoPayload, 'updatedAt'>>): Promise<void>;
154
+ export declare function clearRuntimeInfo(conversationId: string, agentId: string): Promise<void>;
134
155
  export {};
package/dist/rtdb-rest.js CHANGED
@@ -18,6 +18,9 @@ function normalizeRTDBPath(path) {
18
18
  function buildAgentSessionPath(conversationId, agentId) {
19
19
  return `/agent-session/${conversationId}/${agentId}`;
20
20
  }
21
+ function buildRuntimeInfoPath(conversationId, agentId) {
22
+ return `/runtime-info/${conversationId}/${agentId}`;
23
+ }
21
24
  function createRTDBClientHandle(client, options) {
22
25
  const rtdbBase = normalizeRTDBBase(options?.rtdbUrl || DEFAULT_RTDB_BASE);
23
26
  const firebaseApiKey = options?.firebaseApiKey || DEFAULT_FIREBASE_API_KEY;
@@ -137,9 +140,18 @@ function createRTDBClientHandle(client, options) {
137
140
  ...(typeof state.hostMode === 'boolean' ? { hostMode: state.hostMode } : {}),
138
141
  ...(state.model !== undefined ? { model: state.model } : {}),
139
142
  ...(state.permissionMode !== undefined ? { permissionMode: state.permissionMode } : {}),
143
+ ...(state.effort !== undefined ? { effort: state.effort } : {}),
144
+ ...(state.runtimeControlValues !== undefined
145
+ ? { runtimeControlValues: state.runtimeControlValues }
146
+ : {}),
140
147
  ...(state.availableModels !== undefined ? { modelOptions: state.availableModels } : {}),
148
+ ...(state.cwd !== undefined ? { resolvedCwd: state.cwd } : {}),
141
149
  ...(state.executionMode !== undefined ? { executionMode: state.executionMode } : {}),
142
150
  ...(state.executionBranch !== undefined ? { executionBranch: state.executionBranch } : {}),
151
+ ...(state.worktreePath !== undefined ? { worktreePath: state.worktreePath } : {}),
152
+ ...(state.executionFallbackReason !== undefined
153
+ ? { executionFallbackReason: state.executionFallbackReason }
154
+ : {}),
143
155
  ...(state.state !== undefined ? { state: state.state } : {}),
144
156
  waitingForInput: state.state === 'requires_action',
145
157
  ...(state.contextUsage !== undefined ? { contextUsage: state.contextUsage } : {}),
@@ -160,6 +172,8 @@ function createRTDBClientHandle(client, options) {
160
172
  lastError: null,
161
173
  executionBranch: null,
162
174
  contextUsage: null,
175
+ worktreePath: null,
176
+ executionFallbackReason: null,
163
177
  updatedAt: { '.sv': 'timestamp' },
164
178
  });
165
179
  }
@@ -174,6 +188,9 @@ function createRTDBClientHandle(client, options) {
174
188
  });
175
189
  await patch(buildAgentSessionPath(conversationId, agentId), {
176
190
  turnState: state.state,
191
+ ...(state.capabilities?.supportsQueue !== undefined
192
+ ? { supportsQueue: state.capabilities.supportsQueue }
193
+ : {}),
177
194
  queueDepth: state.queueDepth,
178
195
  waitingForInput: state.state === 'waiting_input',
179
196
  lastHeartbeatAt: { '.sv': 'timestamp' },
@@ -205,6 +222,21 @@ function createRTDBClientHandle(client, options) {
205
222
  updatedAt: { '.sv': 'timestamp' },
206
223
  });
207
224
  }
225
+ async function writeRuntimeInfoImpl(conversationId, agentId, payload) {
226
+ await write(buildRuntimeInfoPath(conversationId, agentId), {
227
+ ...payload,
228
+ updatedAt: { '.sv': 'timestamp' },
229
+ });
230
+ }
231
+ async function patchRuntimeInfoImpl(conversationId, agentId, payload) {
232
+ await patch(buildRuntimeInfoPath(conversationId, agentId), {
233
+ ...payload,
234
+ updatedAt: { '.sv': 'timestamp' },
235
+ });
236
+ }
237
+ async function clearRuntimeInfoImpl(conversationId, agentId) {
238
+ await remove(buildRuntimeInfoPath(conversationId, agentId));
239
+ }
208
240
  return {
209
241
  read,
210
242
  write,
@@ -215,6 +247,9 @@ function createRTDBClientHandle(client, options) {
215
247
  writeTurnState: writeTurnStateImpl,
216
248
  clearTurnState: clearTurnStateImpl,
217
249
  patchAgentSessionSnapshot: patchAgentSessionSnapshotImpl,
250
+ writeRuntimeInfo: writeRuntimeInfoImpl,
251
+ patchRuntimeInfo: patchRuntimeInfoImpl,
252
+ clearRuntimeInfo: clearRuntimeInfoImpl,
218
253
  };
219
254
  }
220
255
  /**
@@ -260,3 +295,12 @@ export async function clearTurnState(conversationId, agentId) {
260
295
  export async function patchAgentSessionSnapshot(conversationId, agentId, snapshot) {
261
296
  await getDefaultRTDBClient()?.patchAgentSessionSnapshot(conversationId, agentId, snapshot);
262
297
  }
298
+ export async function writeRuntimeInfo(conversationId, agentId, payload) {
299
+ await getDefaultRTDBClient()?.writeRuntimeInfo(conversationId, agentId, payload);
300
+ }
301
+ export async function patchRuntimeInfo(conversationId, agentId, payload) {
302
+ await getDefaultRTDBClient()?.patchRuntimeInfo(conversationId, agentId, payload);
303
+ }
304
+ export async function clearRuntimeInfo(conversationId, agentId) {
305
+ await getDefaultRTDBClient()?.clearRuntimeInfo(conversationId, agentId);
306
+ }
@@ -21,6 +21,13 @@ export interface ClaudeQuestionReplyMetadata {
21
21
  export interface PlanApprovalMetadata {
22
22
  type: 'plan_approval';
23
23
  planId: string;
24
+ title?: string;
25
+ summary?: string;
26
+ body?: string;
27
+ allowedPrompts?: ReadonlyArray<{
28
+ tool: string;
29
+ prompt: string;
30
+ }>;
24
31
  }
25
32
  export interface PlanApprovalReplyMetadata {
26
33
  type: 'plan_approval_reply';
@@ -36,7 +43,7 @@ export declare function buildQuestionReply(questionId: string, answers: Record<s
36
43
  text: string;
37
44
  metadata: ClaudeQuestionReplyMetadata;
38
45
  };
39
- export declare function buildPlanApprovalRequest(planId: string, text?: string): {
46
+ export declare function buildPlanApprovalRequest(planId: string, text?: string, details?: Omit<PlanApprovalMetadata, 'type' | 'planId'>): {
40
47
  text: string;
41
48
  metadata: PlanApprovalMetadata;
42
49
  };
@@ -22,12 +22,13 @@ export function buildQuestionReply(questionId, answers) {
22
22
  },
23
23
  };
24
24
  }
25
- export function buildPlanApprovalRequest(planId, text = 'Plan ready for review.') {
25
+ export function buildPlanApprovalRequest(planId, text = 'Plan ready for review.', details) {
26
26
  return {
27
27
  text,
28
28
  metadata: {
29
29
  type: 'plan_approval',
30
30
  planId,
31
+ ...(details ?? {}),
31
32
  },
32
33
  };
33
34
  }
package/dist/types.d.ts CHANGED
@@ -132,10 +132,130 @@ export interface AgentCapabilities {
132
132
  export interface ModelOption {
133
133
  value: string;
134
134
  label: string;
135
+ description?: string;
136
+ workspaceRootId?: string;
137
+ workspaceRelativePath?: string;
138
+ source?: WorkspaceOptionSource;
135
139
  }
140
+ export type WorkspaceOptionSource = 'default' | 'explicit' | 'discovered';
136
141
  export interface WorkspaceOption {
137
142
  id: string;
138
143
  label: string;
144
+ description?: string;
145
+ workspaceRootId?: string;
146
+ workspaceRelativePath?: string;
147
+ source?: WorkspaceOptionSource;
148
+ }
149
+ export interface CanonWorkspaceRootMetadata {
150
+ id: string;
151
+ label: string;
152
+ description?: string;
153
+ defaultRelativePath?: string | null;
154
+ }
155
+ export type CanonControlValue = string;
156
+ export type CanonControlAvailability = 'setup' | 'live' | 'setup_and_live';
157
+ export type CanonControlLiveBehavior = 'immediate' | 'next_turn' | 'none';
158
+ export type CanonControlSelectionPolicy = 'inherit' | 'required_explicit';
159
+ export type CanonRuntimeStreamingMode = 'none' | 'status' | 'snapshot' | 'block' | 'delta';
160
+ export type CanonRuntimeSurfaceMode = 'host' | 'channel' | 'limited_channel' | 'operator';
161
+ export type CanonRuntimeInventoryStatus = 'ready' | 'auth_needed' | 'unknown' | 'configured' | 'running' | 'error';
162
+ export type CanonRuntimeStatusTone = 'default' | 'success' | 'warning' | 'danger';
163
+ export type CanonRuntimeActionAvailability = 'idle' | 'busy' | 'busy_with_queue' | 'waiting_input' | 'always';
164
+ export type CanonRuntimeActionPlacement = 'composer_slash' | 'command_palette' | 'session_strip';
165
+ export type CanonRuntimeActionCategory = 'plan' | 'turn' | 'session' | 'details' | 'custom';
166
+ export type CanonRuntimeActionDispatch = {
167
+ kind: 'control';
168
+ controlId: string;
169
+ value: CanonControlValue;
170
+ } | {
171
+ kind: 'signal';
172
+ signal: 'interrupt' | 'stop_and_drop';
173
+ } | {
174
+ kind: 'compose';
175
+ text: string;
176
+ } | {
177
+ kind: 'open_details';
178
+ target?: string;
179
+ };
180
+ export interface CanonRuntimeActionDescriptor {
181
+ id: string;
182
+ label: string;
183
+ description?: string;
184
+ aliases?: ReadonlyArray<string>;
185
+ category?: CanonRuntimeActionCategory;
186
+ placements?: ReadonlyArray<CanonRuntimeActionPlacement>;
187
+ availability?: ReadonlyArray<CanonRuntimeActionAvailability>;
188
+ ownerOnly?: boolean;
189
+ disabledReason?: string | null;
190
+ trailingTextBehavior?: 'ignore' | 'send_as_prompt';
191
+ dispatch: CanonRuntimeActionDispatch;
192
+ }
193
+ export interface CanonControlDescriptor {
194
+ id: string;
195
+ label: string;
196
+ options?: ReadonlyArray<ModelOption>;
197
+ defaultValue?: CanonControlValue | null;
198
+ availability: CanonControlAvailability;
199
+ liveBehavior: CanonControlLiveBehavior;
200
+ selectionPolicy: CanonControlSelectionPolicy;
201
+ description?: string;
202
+ }
203
+ export interface CanonRuntimeDescriptor {
204
+ coreControls: ReadonlyArray<CanonControlDescriptor>;
205
+ runtimeControls?: ReadonlyArray<CanonControlDescriptor>;
206
+ actions?: ReadonlyArray<CanonRuntimeActionDescriptor>;
207
+ /**
208
+ * Optional setup-time local roots advertised by a runtime. These are
209
+ * metadata only for now; existing session config still selects concrete
210
+ * workspace IDs until root-relative directory selection lands.
211
+ */
212
+ workspaceRoots?: ReadonlyArray<CanonWorkspaceRootMetadata>;
213
+ writableRoots?: ReadonlyArray<CanonWorkspaceRootMetadata>;
214
+ supportsInterrupt?: boolean;
215
+ /**
216
+ * Fidelity of live text exposed through Canon's streaming bubble path.
217
+ * `delta` means token/content deltas, `block` means chunked live previews,
218
+ * `snapshot` means completed assistant-message snapshots, and `status`
219
+ * means activity/tool state without live assistant text.
220
+ */
221
+ streamingTextMode?: CanonRuntimeStreamingMode;
222
+ }
223
+ export interface CanonRuntimeExecutionMetadata {
224
+ resolvedWorkspaceLabel?: string | null;
225
+ resolvedCwd?: string | null;
226
+ workspaceRootId?: string | null;
227
+ workspaceRelativePath?: string | null;
228
+ executionMode?: ExecutionEnvironmentMode | null;
229
+ executionBranch?: string | null;
230
+ worktreePath?: string | null;
231
+ fallbackReason?: string | null;
232
+ }
233
+ export interface CanonRuntimeStatusItem {
234
+ id: string;
235
+ label: string;
236
+ value: string;
237
+ tone?: CanonRuntimeStatusTone;
238
+ }
239
+ export interface CanonRuntimeInventoryEntry {
240
+ id: string;
241
+ label: string;
242
+ status?: CanonRuntimeInventoryStatus;
243
+ description?: string;
244
+ }
245
+ export interface CanonRuntimeInventory {
246
+ id: string;
247
+ label: string;
248
+ entries: ReadonlyArray<CanonRuntimeInventoryEntry>;
249
+ }
250
+ export interface RuntimeInfoPayload {
251
+ descriptor: CanonRuntimeDescriptor;
252
+ surfaceMode?: CanonRuntimeSurfaceMode;
253
+ surfaceLabel?: string;
254
+ statusItems?: ReadonlyArray<CanonRuntimeStatusItem>;
255
+ inventories?: ReadonlyArray<CanonRuntimeInventory>;
256
+ execution?: CanonRuntimeExecutionMetadata | null;
257
+ notes?: ReadonlyArray<string>;
258
+ updatedAt?: number;
139
259
  }
140
260
  /** Capability map keyed by clientType. Add new agent types here. */
141
261
  export declare const AGENT_CAPABILITIES: Record<AgentClientType, AgentCapabilities>;
@@ -193,6 +313,7 @@ export interface RuntimeUpdatedPayload {
193
313
  conversationId: string;
194
314
  agentId: string;
195
315
  runtime: AgentRuntime | null;
316
+ runtimeInfo?: RuntimeInfoPayload | null;
196
317
  sessionState: SessionState | null;
197
318
  sessionConfig: SessionConfig | null;
198
319
  }
@@ -265,8 +386,9 @@ export interface SetStreamingOptions {
265
386
  /** Written by Canon app to /control/{convoId}/{agentId}/session in RTDB */
266
387
  export interface SessionControl {
267
388
  model?: string;
268
- permissionMode?: 'default' | 'acceptEdits' | 'plan' | 'bypassPermissions' | 'auto';
269
- effort?: 'low' | 'medium' | 'high' | 'max';
389
+ permissionMode?: string;
390
+ effort?: string;
391
+ runtimeControlValues?: Record<string, CanonControlValue>;
270
392
  updatedAt: number;
271
393
  updatedBy: string;
272
394
  }
@@ -276,9 +398,12 @@ export interface SessionState {
276
398
  model?: string;
277
399
  permissionMode?: string;
278
400
  effort?: string;
401
+ runtimeControlValues?: Record<string, CanonControlValue>;
279
402
  cwd?: string;
280
403
  executionMode?: 'worktree' | 'locked';
281
404
  executionBranch?: string;
405
+ worktreePath?: string;
406
+ executionFallbackReason?: string;
282
407
  clientType?: AgentClientType;
283
408
  /** True when the agent is running under the host wrapper (host.ts) which can apply control signals */
284
409
  hostMode?: boolean;
@@ -297,6 +422,8 @@ export interface SessionConfig {
297
422
  hostMode?: boolean;
298
423
  model?: string;
299
424
  permissionMode?: string;
425
+ effort?: string;
426
+ runtimeControlValues?: Record<string, CanonControlValue>;
300
427
  workspaceId?: string;
301
428
  /**
302
429
  * Explicitly selected execution mode. Sessions created before this field
@@ -322,6 +449,9 @@ export declare const CLAUDE_PERMISSION_MODE_OPTIONS: readonly [{
322
449
  }, {
323
450
  readonly value: "plan";
324
451
  readonly label: "Plan";
452
+ }, {
453
+ readonly value: "dontAsk";
454
+ readonly label: "Don't ask";
325
455
  }, {
326
456
  readonly value: "bypassPermissions";
327
457
  readonly label: "Bypass";
@@ -335,6 +465,7 @@ export interface AgentRuntime {
335
465
  defaultModel?: string;
336
466
  defaultPermissionMode?: string;
337
467
  availablePermissionModes?: PermissionModeOption[];
468
+ runtimeDescriptor?: CanonRuntimeDescriptor;
338
469
  defaultWorkspaceId?: string;
339
470
  /**
340
471
  * Execution modes the host will accept. The runtime advertises this so the
@@ -367,13 +498,22 @@ export interface AgentSessionSnapshot {
367
498
  modelOptions?: ModelOption[];
368
499
  permissionMode?: string;
369
500
  permissionModeOptions?: PermissionModeOption[];
501
+ effort?: string;
502
+ runtimeControlValues?: Record<string, CanonControlValue>;
503
+ runtimeDescriptor?: CanonRuntimeDescriptor | null;
504
+ runtimeInfo?: RuntimeInfoPayload | null;
370
505
  workspaceId?: string;
371
506
  workspaceOptions?: WorkspaceOption[];
372
507
  executionMode?: ExecutionEnvironmentMode;
373
508
  availableExecutionModes?: ExecutionEnvironmentMode[];
374
509
  executionBranch?: string;
510
+ resolvedWorkspaceLabel?: string | null;
511
+ resolvedCwd?: string | null;
512
+ worktreePath?: string | null;
513
+ executionFallbackReason?: string | null;
375
514
  state?: SessionState['state'];
376
515
  turnState?: TurnLifecycleState;
516
+ supportsQueue?: boolean;
377
517
  queueDepth: number;
378
518
  waitingForInput: boolean;
379
519
  contextUsage?: SessionState['contextUsage'];
package/dist/types.js CHANGED
@@ -36,6 +36,7 @@ export const CLAUDE_PERMISSION_MODE_OPTIONS = [
36
36
  { value: 'default', label: 'Default' },
37
37
  { value: 'acceptEdits', label: 'Auto-edit' },
38
38
  { value: 'plan', label: 'Plan' },
39
+ { value: 'dontAsk', label: "Don't ask" },
39
40
  { value: 'bypassPermissions', label: 'Bypass' },
40
41
  { value: 'auto', label: 'Auto' },
41
42
  ];
@@ -0,0 +1,18 @@
1
+ import { type ConfiguredWorkspaceOption } from './execution-environment.js';
2
+ import type { CanonWorkspaceRootMetadata } from './types.js';
3
+ export interface ConfiguredWorkspaceRoot extends CanonWorkspaceRootMetadata {
4
+ cwd: string;
5
+ }
6
+ export interface WorkspaceDiscoveryResult {
7
+ workspaceOptions: ConfiguredWorkspaceOption[];
8
+ workspaceRoots: ConfiguredWorkspaceRoot[];
9
+ warnings: string[];
10
+ }
11
+ export declare function buildWorkspaceRootId(workspaceRoot: string): string;
12
+ export declare function discoverWorkspaceProjects(root: ConfiguredWorkspaceRoot, warnings?: string[]): ConfiguredWorkspaceOption[];
13
+ export declare function buildPublicWorkspaceRoots(roots: ReadonlyArray<ConfiguredWorkspaceRoot>): CanonWorkspaceRootMetadata[];
14
+ export declare function buildConfiguredWorkspaceOptionsWithRoots(input: {
15
+ primaryCwd: string;
16
+ configuredWorkspaces?: string[];
17
+ workspaceRoots?: string[];
18
+ }): WorkspaceDiscoveryResult;
@@ -0,0 +1,193 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
3
+ import { basename, isAbsolute, join, relative, resolve, sep } from 'node:path';
4
+ import { buildConfiguredWorkspaceOptions, buildWorkspaceOptionId, } from './execution-environment.js';
5
+ const PROJECT_MARKERS = [
6
+ '.git',
7
+ 'package.json',
8
+ 'pyproject.toml',
9
+ 'Cargo.toml',
10
+ 'go.mod',
11
+ ];
12
+ const IGNORED_PROJECT_DIRS = new Set([
13
+ '.cache',
14
+ '.git',
15
+ '.hg',
16
+ '.svn',
17
+ '.turbo',
18
+ '.yarn',
19
+ 'build',
20
+ 'coverage',
21
+ 'dist',
22
+ 'node_modules',
23
+ 'target',
24
+ 'vendor',
25
+ ]);
26
+ function shortHash(value) {
27
+ return createHash('sha256').update(value).digest('hex').slice(0, 12);
28
+ }
29
+ export function buildWorkspaceRootId(workspaceRoot) {
30
+ let stablePath = resolve(workspaceRoot);
31
+ try {
32
+ stablePath = realpathSync(stablePath);
33
+ }
34
+ catch {
35
+ // Nonexistent roots are validated elsewhere; keep deterministic IDs.
36
+ }
37
+ return `workspace-root-${shortHash(stablePath)}`;
38
+ }
39
+ function realDirectory(path) {
40
+ try {
41
+ const resolved = resolve(path);
42
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
43
+ return null;
44
+ }
45
+ return realpathSync(resolved);
46
+ }
47
+ catch {
48
+ return null;
49
+ }
50
+ }
51
+ function isWithin(parent, child) {
52
+ const rel = relative(parent, child);
53
+ return rel === '' || (rel.length > 0 && !rel.startsWith('..') && !isAbsolute(rel));
54
+ }
55
+ function toRelativeProjectPath(rootCwd, projectCwd) {
56
+ const rel = relative(rootCwd, projectCwd).split(sep).join('/');
57
+ return rel || '.';
58
+ }
59
+ function hasProjectMarker(cwd) {
60
+ return PROJECT_MARKERS.some((marker) => existsSync(join(cwd, marker)));
61
+ }
62
+ function normalizeWorkspaceRoots(workspaceRoots) {
63
+ const roots = [];
64
+ const warnings = [];
65
+ const seen = new Set();
66
+ const seenLabels = new Map();
67
+ for (const rawRoot of workspaceRoots) {
68
+ const cwd = realDirectory(rawRoot);
69
+ if (!cwd) {
70
+ warnings.push(`Workspace root is not a readable directory: ${rawRoot}`);
71
+ continue;
72
+ }
73
+ if (seen.has(cwd)) {
74
+ continue;
75
+ }
76
+ seen.add(cwd);
77
+ const baseLabel = basename(cwd) || cwd;
78
+ const seenLabelCount = (seenLabels.get(baseLabel) ?? 0) + 1;
79
+ seenLabels.set(baseLabel, seenLabelCount);
80
+ roots.push({
81
+ id: buildWorkspaceRootId(cwd),
82
+ label: seenLabelCount === 1 ? baseLabel : `${baseLabel} (${seenLabelCount})`,
83
+ cwd,
84
+ });
85
+ }
86
+ return { roots, warnings };
87
+ }
88
+ function findWorkspaceRoot(roots, cwd) {
89
+ const resolved = realDirectory(cwd) ?? resolve(cwd);
90
+ return [...roots]
91
+ .sort((a, b) => b.cwd.length - a.cwd.length)
92
+ .find((root) => isWithin(root.cwd, resolved)) ?? null;
93
+ }
94
+ function annotateWorkspaceOption(option, roots) {
95
+ const root = findWorkspaceRoot(roots, option.cwd);
96
+ if (!root) {
97
+ return option;
98
+ }
99
+ const stableCwd = realDirectory(option.cwd) ?? resolve(option.cwd);
100
+ const relativePath = toRelativeProjectPath(root.cwd, stableCwd);
101
+ return {
102
+ ...option,
103
+ workspaceRootId: root.id,
104
+ workspaceRelativePath: relativePath,
105
+ description: relativePath === '.'
106
+ ? `${root.label} root`
107
+ : `${root.label}/${relativePath}`,
108
+ };
109
+ }
110
+ export function discoverWorkspaceProjects(root, warnings) {
111
+ const projects = [];
112
+ if (hasProjectMarker(root.cwd)) {
113
+ projects.push({
114
+ id: buildWorkspaceOptionId(root.cwd),
115
+ label: root.label,
116
+ cwd: root.cwd,
117
+ workspaceRootId: root.id,
118
+ workspaceRelativePath: '.',
119
+ description: `${root.label} root`,
120
+ source: 'discovered',
121
+ });
122
+ }
123
+ let entries;
124
+ try {
125
+ entries = readdirSync(root.cwd, { withFileTypes: true });
126
+ }
127
+ catch {
128
+ warnings?.push(`Workspace root could not be scanned: ${root.cwd}`);
129
+ return projects;
130
+ }
131
+ for (const entry of entries) {
132
+ if (!entry.isDirectory() || IGNORED_PROJECT_DIRS.has(entry.name)) {
133
+ continue;
134
+ }
135
+ const cwd = realDirectory(join(root.cwd, entry.name));
136
+ if (!cwd || !isWithin(root.cwd, cwd) || !hasProjectMarker(cwd)) {
137
+ continue;
138
+ }
139
+ const relativePath = toRelativeProjectPath(root.cwd, cwd);
140
+ projects.push({
141
+ id: buildWorkspaceOptionId(cwd),
142
+ label: basename(cwd) || relativePath,
143
+ cwd,
144
+ workspaceRootId: root.id,
145
+ workspaceRelativePath: relativePath,
146
+ description: `${root.label}/${relativePath}`,
147
+ source: 'discovered',
148
+ });
149
+ }
150
+ return projects;
151
+ }
152
+ export function buildPublicWorkspaceRoots(roots) {
153
+ return roots.map((root) => ({
154
+ id: root.id,
155
+ label: root.label,
156
+ ...(root.description ? { description: root.description } : {}),
157
+ ...(root.defaultRelativePath !== undefined
158
+ ? { defaultRelativePath: root.defaultRelativePath }
159
+ : {}),
160
+ }));
161
+ }
162
+ function relabelWorkspaceOptions(options) {
163
+ const seenLabels = new Map();
164
+ return options.map((option) => {
165
+ const seenCount = (seenLabels.get(option.label) ?? 0) + 1;
166
+ seenLabels.set(option.label, seenCount);
167
+ return seenCount === 1
168
+ ? option
169
+ : { ...option, label: `${option.label} (${seenCount})` };
170
+ });
171
+ }
172
+ export function buildConfiguredWorkspaceOptionsWithRoots(input) {
173
+ const normalizedRoots = normalizeWorkspaceRoots(input.workspaceRoots ?? []);
174
+ const explicitOptions = buildConfiguredWorkspaceOptions(input.primaryCwd, input.configuredWorkspaces ?? []).map((option) => annotateWorkspaceOption(option, normalizedRoots.roots));
175
+ const discoveryWarnings = [];
176
+ const discoveredOptions = normalizedRoots.roots.flatMap((root) => (discoverWorkspaceProjects(root, discoveryWarnings)));
177
+ const byCwd = new Map();
178
+ for (const option of discoveredOptions) {
179
+ byCwd.set(resolve(option.cwd), option);
180
+ }
181
+ for (const option of explicitOptions) {
182
+ byCwd.set(resolve(option.cwd), option);
183
+ }
184
+ const ordered = [
185
+ ...explicitOptions,
186
+ ...discoveredOptions.filter((option) => !explicitOptions.some((explicit) => resolve(explicit.cwd) === resolve(option.cwd))),
187
+ ].filter((option, index, options) => (options.findIndex((candidate) => resolve(candidate.cwd) === resolve(option.cwd)) === index)).map((option) => byCwd.get(resolve(option.cwd)) ?? option);
188
+ return {
189
+ workspaceOptions: relabelWorkspaceOptions(ordered),
190
+ workspaceRoots: normalizedRoots.roots,
191
+ warnings: [...normalizedRoots.warnings, ...discoveryWarnings],
192
+ };
193
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canonmsg/core",
3
- "version": "0.7.5",
3
+ "version": "0.10.0",
4
4
  "description": "Canon core — shared types, REST client, SSE stream, and registration for Canon messaging",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -23,6 +23,7 @@
23
23
  "scripts": {
24
24
  "build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc",
25
25
  "dev": "tsc --watch",
26
+ "test": "vitest run",
26
27
  "prepack": "npm run build"
27
28
  },
28
29
  "engines": {