@canonmsg/core 0.8.0 → 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.
@@ -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, CanonControlAvailability, CanonControlDescriptor, CanonControlLiveBehavior, CanonControlSelectionPolicy, CanonControlValue, CanonContactRequest, CanonContactRequestStatus, ContactApprovedPayload, ContactRequestPayload, CanonStreamEvent, CreateContactRequestResult, MediaAttachment, MediaAttachmentKind, ModelOption, PermissionModeOption, CanonRuntimeDescriptor, CanonRuntimeExecutionMetadata, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, RuntimeUpdatedPayload, RuntimeInfoPayload, 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, 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, CanonRuntimeExecutionMetadata, CanonRuntimeInventory, CanonRuntimeInventoryEntry, CanonRuntimeStreamingMode, CanonRuntimeStatusItem, CanonRuntimeSurfaceMode, CanonWorkspaceRootMetadata, ModelOption, PermissionModeOption, RuntimeInfoPayload, 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';
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';
@@ -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
@@ -133,10 +133,18 @@ export interface ModelOption {
133
133
  value: string;
134
134
  label: string;
135
135
  description?: string;
136
+ workspaceRootId?: string;
137
+ workspaceRelativePath?: string;
138
+ source?: WorkspaceOptionSource;
136
139
  }
140
+ export type WorkspaceOptionSource = 'default' | 'explicit' | 'discovered';
137
141
  export interface WorkspaceOption {
138
142
  id: string;
139
143
  label: string;
144
+ description?: string;
145
+ workspaceRootId?: string;
146
+ workspaceRelativePath?: string;
147
+ source?: WorkspaceOptionSource;
140
148
  }
141
149
  export interface CanonWorkspaceRootMetadata {
142
150
  id: string;
@@ -152,6 +160,36 @@ export type CanonRuntimeStreamingMode = 'none' | 'status' | 'snapshot' | 'block'
152
160
  export type CanonRuntimeSurfaceMode = 'host' | 'channel' | 'limited_channel' | 'operator';
153
161
  export type CanonRuntimeInventoryStatus = 'ready' | 'auth_needed' | 'unknown' | 'configured' | 'running' | 'error';
154
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
+ }
155
193
  export interface CanonControlDescriptor {
156
194
  id: string;
157
195
  label: string;
@@ -165,6 +203,7 @@ export interface CanonControlDescriptor {
165
203
  export interface CanonRuntimeDescriptor {
166
204
  coreControls: ReadonlyArray<CanonControlDescriptor>;
167
205
  runtimeControls?: ReadonlyArray<CanonControlDescriptor>;
206
+ actions?: ReadonlyArray<CanonRuntimeActionDescriptor>;
168
207
  /**
169
208
  * Optional setup-time local roots advertised by a runtime. These are
170
209
  * metadata only for now; existing session config still selects concrete
@@ -410,6 +449,9 @@ export declare const CLAUDE_PERMISSION_MODE_OPTIONS: readonly [{
410
449
  }, {
411
450
  readonly value: "plan";
412
451
  readonly label: "Plan";
452
+ }, {
453
+ readonly value: "dontAsk";
454
+ readonly label: "Don't ask";
413
455
  }, {
414
456
  readonly value: "bypassPermissions";
415
457
  readonly label: "Bypass";
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.8.0",
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": {