@adhdev/mesh-shared 1.0.82-rc.22 → 1.0.82-rc.24

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.
@@ -51,7 +51,7 @@ export interface LaunchDefaultAxis {
51
51
  defaultRef?: LaunchDefaultRef;
52
52
  }
53
53
  export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
54
- export type LaunchDefaultValidationReason = 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled' | 'provider_not_detected' | 'provider_not_eligible' | 'model_unsupported' | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported' | 'thinking_not_offered' | 'catalog_unavailable';
54
+ export type LaunchDefaultValidationReason = 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled' | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported' | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported' | 'thinking_not_offered' | 'catalog_unavailable';
55
55
  export type LaunchDefaultResolution = {
56
56
  ok: true;
57
57
  selection: LaunchDefaultSelection;
@@ -18,10 +18,10 @@
18
18
  * Order mirrors mcp-server `ALL_MESH_TOOLS` for easy visual diffing, but the consistency
19
19
  * checks are set-based (order-insensitive).
20
20
  */
21
- export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots"];
21
+ export declare const CANONICAL_MESH_TOOL_NAMES: readonly ["mesh_status", "mesh_route_preview", "mesh_list_nodes", "mesh_enqueue_batch", "mesh_enqueue_task", "mesh_view_queue", "mesh_queue_cancel", "mesh_queue_requeue", "mesh_send_task", "mesh_read_chat", "mesh_read_debug", "mesh_read_terminal", "mesh_send_keys", "mesh_launch_session", "mesh_git_status", "mesh_read_node_logs", "mesh_fast_forward_node", "mesh_restart_daemon", "mesh_checkpoint", "mesh_approve", "mesh_answer_question", "mesh_list_pending_approvals", "mesh_create", "mesh_add_node", "mesh_attach_remote", "mesh_clone_node", "mesh_remove_node", "mesh_cleanup_worktree_nodes", "mesh_refine_node", "mesh_refine_batch", "mesh_config", "mesh_init", "mesh_cleanup_sessions", "mesh_task_history", "mesh_ledger_query", "mesh_note", "mesh_reconcile_ledger", "mesh_mission_upsert", "mesh_mission_list", "mesh_review_inbox", "mesh_node_slots", "driver_list", "driver_describe", "artifact_list"];
22
22
  export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
23
23
  /** The count the `NN tools` barrel doc comments and consistency test assert against. */
24
- export declare const CANONICAL_MESH_TOOL_COUNT: 41;
24
+ export declare const CANONICAL_MESH_TOOL_COUNT: 44;
25
25
  /**
26
26
  * Tool names retired by the 2026-09-26 tool-surface consolidation (60 → 48).
27
27
  * Every capability behind them is still reachable: each retired name maps to the
@@ -203,3 +203,10 @@ export declare function isSubmitOutcome(value: unknown): value is SubmitOutcome;
203
203
  export declare function isInputEnvelopeWire(value: unknown): value is InputEnvelopeWire;
204
204
  /** Minimal structural check for the wire boundary — not a full decoder (mesh-shared stays a pure leaf; semantic validation stays with the handler that owns it). */
205
205
  export declare function isOutboundMessage(value: unknown): value is OutboundMessage;
206
+ /**
207
+ * Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
208
+ * input. Only a text that OPENS with the envelope is touched; anything else is
209
+ * returned unchanged. Shared across daemon transcript merge and web echo/display
210
+ * normalization so wrapped turns correlate identically.
211
+ */
212
+ export declare function unwrapPastedContent(text: string): string;
@@ -213,11 +213,15 @@ export type P2POfferWirePayload = {
213
213
  sdp: string;
214
214
  type: string;
215
215
  peerId: string;
216
+ attemptId?: number;
217
+ attemptEpoch?: string;
216
218
  };
217
219
  export type P2PIceWirePayload = {
218
220
  candidate: string;
219
221
  mid: string;
220
222
  peerId: string;
223
+ attemptId?: number;
224
+ attemptEpoch?: string;
221
225
  };
222
226
  /**
223
227
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
@@ -229,6 +233,8 @@ export type P2PIceWirePayload = {
229
233
  export type RelayedP2PSignalWirePayload = {
230
234
  peerId?: string;
231
235
  daemonId?: string;
236
+ attemptId?: number;
237
+ attemptEpoch?: string;
232
238
  sharePermission?: string;
233
239
  shareTargetSessionId?: string;
234
240
  sdp?: string;
@@ -34,7 +34,7 @@ export type ChunkFrame<T extends string> = {
34
34
  export declare function isChunkType(value: unknown): value is `${string}_chunk`;
35
35
  export declare const DASHBOARD_TO_DAEMON_P2P_TYPES: readonly ["ping", "pong", "command", "input", "pty_input", "pty_resize", "screenshot_start", "screenshot_stop", "subscribe", "unsubscribe", "seqscribe_session_interest", "read", "write", "list"];
36
36
  export type DashboardToDaemonP2PType = typeof DASHBOARD_TO_DAEMON_P2P_TYPES[number];
37
- export declare const isDashboardToDaemonP2PType: (value: unknown) => value is "list" | "command" | "ping" | "pong" | "input" | "pty_input" | "pty_resize" | "screenshot_start" | "screenshot_stop" | "subscribe" | "unsubscribe" | "seqscribe_session_interest" | "read" | "write";
37
+ export declare const isDashboardToDaemonP2PType: (value: unknown) => value is "read" | "list" | "command" | "ping" | "pong" | "input" | "pty_input" | "pty_resize" | "screenshot_start" | "screenshot_stop" | "subscribe" | "unsubscribe" | "seqscribe_session_interest" | "write";
38
38
  export type DashboardToDaemonChunkType = `${DashboardToDaemonP2PType}_chunk`;
39
39
  export declare function isDashboardToDaemonChunkType(value: unknown): value is DashboardToDaemonChunkType;
40
40
  export type P2PPingMsg = {
@@ -109,12 +109,16 @@ export type SystemAnnouncementWirePayload = {
109
109
  export type DashboardP2POfferWirePayload = {
110
110
  daemonId: string;
111
111
  peerId?: string;
112
+ attemptId?: number;
113
+ attemptEpoch?: string;
112
114
  sdp: string;
113
115
  type: string;
114
116
  };
115
117
  export type DashboardP2PIceWirePayload = {
116
118
  daemonId: string;
117
119
  peerId?: string;
120
+ attemptId?: number;
121
+ attemptEpoch?: string;
118
122
  candidate: string;
119
123
  mid?: string;
120
124
  sdpMid?: string | null;
@@ -154,16 +158,22 @@ export declare const isDashboardP2PSignalType: (value: unknown) => value is "p2p
154
158
  export type DashboardP2PReadyWirePayload = {
155
159
  daemonId: string;
156
160
  peerId: string;
161
+ attemptId?: number;
162
+ attemptEpoch?: string;
157
163
  };
158
164
  export type DashboardP2PAnswerWirePayload = {
159
165
  daemonId: string;
160
166
  peerId: string;
161
167
  sdp: string;
162
168
  type: string;
169
+ attemptId?: number;
170
+ attemptEpoch?: string;
163
171
  };
164
172
  export type DashboardP2PLocalIceWirePayload = {
165
173
  daemonId: string;
166
174
  peerId: string;
175
+ attemptId?: number;
176
+ attemptEpoch?: string;
167
177
  candidate: string;
168
178
  sdpMid: string | null;
169
179
  sdpMLineIndex: number | null;
@@ -0,0 +1,11 @@
1
+ /** Persisted role-launch intent. Restart replays this selection rather than
2
+ * consulting newer machine defaults; restore preserves its original provenance. */
3
+ import { type LaunchDefaultRole, type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
4
+ export interface RoleLaunchSelection {
5
+ role: LaunchDefaultRole;
6
+ selection: LaunchDefaultSelection;
7
+ applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
8
+ defaultRef?: LaunchDefaultRef;
9
+ documentRevision: number;
10
+ }
11
+ export declare function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined;
@@ -1,25 +1,5 @@
1
- /**
2
- * session-launch — how a session was launched: provider, model and thinking
3
- * level, and WHERE each value came from.
4
- *
5
- * Wiring-unification Phase E (docs/design/2026-09-23-wiring-unification.md §7,
6
- * RC5). Before this record the only trace of a session's model was a display
7
- * string in recent-activity (`buildLegacyModelModeSummaryMetadata`), so nobody
8
- * could answer "did I pick this model, did the dashboard restore it from last
9
- * time, did a mesh slot choose it, or is it just the provider default?" — and
10
- * after a daemon restart not even the value survived.
11
- *
12
- * The daemon owns the record (daemon-core `SessionRegistry`); this file holds the
13
- * shape and the pure readers so daemon-core, web-core, the server and the MCP
14
- * server all read one definition. It is a dependency-free leaf, like the rest of
15
- * mesh-shared.
16
- *
17
- * ★Server content boundary (CLAUDE.md): the full record rides P2P only. The
18
- * server path carries exactly two derived fields — `model` (an identifier,
19
- * passed through {@link sanitizeModelIdentifier}) and `modelSource` (an enum,
20
- * passed through {@link isModelAxisSource}). Never widen that to the record.
21
- */
22
- export declare const MODEL_AXIS_SOURCES: readonly ["user", "remembered", "mesh_slot", "task_override", "provider_default", "unspecified"];
1
+ import { type RoleLaunchSelection } from './role-launch';
2
+ export declare const MODEL_AXIS_SOURCES: readonly ["user", "remembered", "stored_default", "project_default", "mesh_slot", "task_override", "provider_default", "unspecified"];
23
3
  export type ModelAxisSource = typeof MODEL_AXIS_SOURCES[number];
24
4
  export declare function isModelAxisSource(value: unknown): value is ModelAxisSource;
25
5
  /**
@@ -65,6 +45,7 @@ export interface ModelSelection {
65
45
  history: ModelSelectionHistoryEntry[];
66
46
  }
67
47
  export interface SessionLaunchRecord {
48
+ roleLaunch?: RoleLaunchSelection;
68
49
  sessionId: string;
69
50
  providerType: string;
70
51
  providerVersion?: string;
@@ -35,6 +35,13 @@ import { type MeshTaskDifficulty, type NodeCapabilitySlot } from './brain-routin
35
35
  export interface CliSlotRecipe {
36
36
  /** Optional model to pin on the slot. Best-effort at launch. */
37
37
  model?: string;
38
+ /**
39
+ * Optional ordered candidate models to prefer when the provider's live
40
+ * `modelOptions` (from model discovery) is present on `DetectedCliProvider`.
41
+ * The first candidate present in `modelOptions` wins; falls back to `model`
42
+ * when `modelOptions` is absent or contains none of the candidates.
43
+ */
44
+ modelCandidates?: readonly string[];
38
45
  /** Optional thinking level, in the provider's own vocabulary. */
39
46
  thinkingLevel?: string;
40
47
  /** Difficulty range this slot handles. Empty = general-purpose. */
@@ -76,6 +83,13 @@ export interface DetectedCliProvider {
76
83
  displayName?: string;
77
84
  /** Detected version, when known. Display only — never affects the mapping. */
78
85
  version?: string;
86
+ /**
87
+ * Live discovered / resolved `modelOptions` for this provider on the node,
88
+ * when available on the status catalog. When present and non-empty, recipes
89
+ * prefer the first matching model from `[recipe.model, ...recipe.modelCandidates]`
90
+ * that exists in `modelOptions`.
91
+ */
92
+ modelOptions?: readonly string[];
79
93
  }
80
94
  /** One proposed slot plus why it was proposed. */
81
95
  export interface ProposedSlotEntry {
@@ -30,7 +30,7 @@
30
30
  * the protocol contract the marker demarcates.
31
31
  */
32
32
  import { type MissionBrief } from './mission-brief';
33
- export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff"];
33
+ export declare const WORKER_TOOLS: readonly ["report_completion", "progress_update", "peer_context_pull", "git_status", "git_log", "git_diff", "driver_list", "driver_describe", "driver_invoke", "artifact_register"];
34
34
  export type WorkerTool = typeof WORKER_TOOLS[number];
35
35
  export declare function isWorkerTool(value: unknown): value is WorkerTool;
36
36
  /** Stable marker line so a body is never footered twice and tests can find it. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/mesh-shared",
3
- "version": "1.0.82-rc.22",
3
+ "version": "1.0.82-rc.24",
4
4
  "description": "ADHDev mesh-shared — pure mesh/git status normalizers shared by daemon-core and web-core",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,9 +11,8 @@
11
11
  * - the daemon-only verbs (launch / MCP-only pull / owner actions) that have
12
12
  * no tool.
13
13
  *
14
- * Assistant tools are a separate set from the mesh tools: none of these names
15
- * belongs in `CANONICAL_MESH_TOOL_NAMES`, and the coordinator/worker MCP modes
16
- * never publish them.
14
+ * Assistant-specific tools remain separate from mesh tools. Driver discovery
15
+ * (list/describe) is shared across all three modes; invoke remains Worker-only.
17
16
  */
18
17
 
19
18
  export const ASSISTANT_TOOLS = [
@@ -33,6 +32,8 @@ export const ASSISTANT_TOOLS = [
33
32
  'project_note',
34
33
  'launch_defaults',
35
34
  'launch_defaults_set',
35
+ 'driver_list',
36
+ 'driver_describe',
36
37
  ] as const
37
38
 
38
39
  export type AssistantTool = typeof ASSISTANT_TOOLS[number]
@@ -46,6 +47,8 @@ export function isAssistantTool(value: unknown): value is AssistantTool {
46
47
  /** Daemon verb names (§4.4 table + the five verbs below it). */
47
48
  export const ASSISTANT_VERB = {
48
49
  // tool verbs
50
+ driverList: 'driver_list',
51
+ driverDescribe: 'driver_describe',
49
52
  projects: 'assistant_projects',
50
53
  environment: 'assistant_environment',
51
54
  action: 'assistant_action',
@@ -80,6 +83,8 @@ export type AssistantVerb = typeof ASSISTANT_VERB[keyof typeof ASSISTANT_VERB]
80
83
 
81
84
  /** The daemon verb each assistant tool calls. */
82
85
  export const ASSISTANT_TOOL_VERBS: Readonly<Record<AssistantTool, AssistantVerb>> = {
86
+ driver_list: ASSISTANT_VERB.driverList,
87
+ driver_describe: ASSISTANT_VERB.driverDescribe,
83
88
  projects: ASSISTANT_VERB.projects,
84
89
  environment: ASSISTANT_VERB.environment,
85
90
  assistant_action: ASSISTANT_VERB.action,
@@ -123,8 +128,9 @@ export const ASSISTANT_REVIEW_TURN_VERBS: readonly AssistantVerb[] = [
123
128
  ASSISTANT_VERB.projectNote,
124
129
  ]
125
130
 
126
- /**
127
- * Env var the daemon sets on the assistant CLI's process (§4.5), read by the
131
+ import { canonicalDaemonId } from './daemon-normalize'
132
+
133
+ /** Env var the daemon sets on the assistant CLI's process (§4.5), read by the
128
134
  * assistant MCP server and forwarded on every verb as
129
135
  * `ASSISTANT_SESSION_ID_ARG`. Routing / ownership only — never an auth gate.
130
136
  */
@@ -132,3 +138,208 @@ export const ASSISTANT_SESSION_ID_ENV = 'ADHDEV_ASSISTANT_SESSION_ID'
132
138
 
133
139
  /** Request arg carrying the calling assistant session id. */
134
140
  export const ASSISTANT_SESSION_ID_ARG = 'assistantSessionId'
141
+
142
+ // ─── Remote Assistant Project Follow & Host-Authoritative Grant (rc24) ──────
143
+
144
+ /**
145
+ * Conceptual single-assistant slot in the verified account namespace.
146
+ * Fixed to `'personal'` — one logical assistant per account with one
147
+ * owner-selected active execution daemon binding per project ACL.
148
+ */
149
+ export const ASSISTANT_ACCOUNT_SLOT = 'personal' as const
150
+ export type AssistantAccountSlot = typeof ASSISTANT_ACCOUNT_SLOT
151
+
152
+ /**
153
+ * Daemon commands for remote project discovery, host ACL grant/revoke, and
154
+ * assistant-side follow/unfollow. Registered outside `ASSISTANT_VERB` so the
155
+ * assistant MCP server tool/verb parity table remains untouched.
156
+ */
157
+ export const ASSISTANT_REMOTE_PROJECT_COMMAND = 'assistant_remote_project' as const
158
+ export const ASSISTANT_REMOTE_PROJECT_CATALOG_COMMAND = 'assistant_remote_project_catalog' as const
159
+ export const ASSISTANT_REMOTE_PROJECT_ACCESS_COMMAND = 'assistant_remote_project_access' as const
160
+ export const ASSISTANT_REMOTE_FOLLOW_COMMAND = 'assistant_remote_follow' as const
161
+
162
+ /**
163
+ * Narrow operations a non-roster follower deputy is permitted to invoke on
164
+ * `assistant_remote_project` when the host's `assistantRemoteAccess` ACL is active.
165
+ * `note`, `terminal`, and `inventory` remain strictly roster-member-only.
166
+ */
167
+ export const ASSISTANT_FOLLOWER_ALLOWED_OPS = ['status', 'read', 'poll', 'report', 'send'] as const
168
+ export type AssistantFollowerAllowedOp = typeof ASSISTANT_FOLLOWER_ALLOWED_OPS[number]
169
+
170
+ const ASSISTANT_FOLLOWER_ALLOWED_OP_SET: ReadonlySet<string> = new Set(ASSISTANT_FOLLOWER_ALLOWED_OPS)
171
+
172
+ export function isAssistantFollowerAllowedOp(value: unknown): value is AssistantFollowerAllowedOp {
173
+ return typeof value === 'string' && ASSISTANT_FOLLOWER_ALLOWED_OP_SET.has(value)
174
+ }
175
+
176
+ /**
177
+ * Host-authoritative narrow project ACL persisted on the hosting daemon's
178
+ * `LocalMeshEntry` in `meshes.json`. Revocation increments `grantRevision`
179
+ * and retains a `'revoked'` tombstone so stale follower caches cannot replay.
180
+ */
181
+ export interface AssistantRemoteAccessBinding {
182
+ hostDaemonId: string
183
+ meshId: string
184
+ accountSlot: AssistantAccountSlot
185
+ activeDaemonId: string
186
+ bindingRevision: number
187
+ grantRevision: number
188
+ status: 'active' | 'revoked'
189
+ allowedOps: readonly AssistantFollowerAllowedOp[]
190
+ grantedAt: number
191
+ revokedAt?: number
192
+ }
193
+
194
+ /**
195
+ * Durable owner-binding receipt persisted in the assistant daemon's
196
+ * `assistant.json`. Survives ordinary daemon restarts and session/provider
197
+ * replacements (`bindSession`), while missing/corrupt/mismatched receipts
198
+ * fail closed until owner reconciliation.
199
+ */
200
+ export interface AssistantOwnerBindingReceipt {
201
+ accountSlot: AssistantAccountSlot
202
+ daemonId: string
203
+ bindingRevision: number
204
+ boundAt: number | string
205
+ }
206
+
207
+ export type FollowedRemoteProjectState = 'followed' | 'unfollowed' | 'revoked'
208
+
209
+ export type RemoteAccessState =
210
+ | 'not_granted'
211
+ | 'granted_unfollowed'
212
+ | 'followed'
213
+ | 'revoked'
214
+ | 'host_unreachable'
215
+ | 'binding_mismatch'
216
+
217
+ /**
218
+ * Assistant-local followed remote project reference persisted in `assistant.json`.
219
+ * Never injected into `meshes.json`, `mesh-host-records.json`, `mesh-node-state-push.json`,
220
+ * or `inlineMeshCache`.
221
+ */
222
+ export interface FollowedRemoteProjectEntry {
223
+ /** Canonical key: `${canonicalDaemonId(hostDaemonId)}::${meshId}` */
224
+ key: string
225
+ hostDaemonId: string
226
+ hostLabel?: string
227
+ meshId: string
228
+ name: string
229
+ slug: string
230
+ sanitizedRepoIdentity: string | null
231
+ repoIdentity?: string
232
+ accountSlot: AssistantAccountSlot
233
+ bindingRevision: number
234
+ grantRevision: number
235
+ state: FollowedRemoteProjectState
236
+ followedAt: number | string
237
+ updatedAt: number | string
238
+ unfollowedAt?: number | string
239
+ revokedAt?: number | string
240
+ lastCursor?: string | null
241
+ coordinatorSessionId?: string | null
242
+ lastErrorCode?: string
243
+ lastObserved?: {
244
+ observedAt: number | string
245
+ coordinatorState?: string
246
+ coordinatorMissing?: boolean
247
+ coordinatorPresent?: boolean
248
+ lastHeadline?: string
249
+ summary?: string
250
+ }
251
+ }
252
+
253
+ /**
254
+ * Content-free canonical catalog metadata returned by `assistant_remote_project_catalog`
255
+ * for meshes actually hosted on the queried remote daemon.
256
+ */
257
+ export interface RemoteProjectCatalogItem {
258
+ hostDaemonId: string
259
+ meshId: string
260
+ name: string
261
+ sanitizedRepoIdentity: string | null
262
+ coordinatorState?: 'none' | 'idle' | 'working' | 'waiting' | string
263
+ capabilities: {
264
+ status: true
265
+ read: true
266
+ poll: true
267
+ report: true
268
+ send: true
269
+ coordinatorPresent?: boolean
270
+ }
271
+ observedAt: number | string
272
+ }
273
+
274
+ /** Build the canonical `(hostDaemonId, meshId)` key used across assistant follow state. */
275
+ export function followedRemoteProjectKey(hostDaemonId: string, meshId: string): string {
276
+ const canonHost = canonicalDaemonId(hostDaemonId) ?? hostDaemonId.trim()
277
+ return `${canonHost}::${meshId.trim()}`
278
+ }
279
+
280
+ /**
281
+ * Sanitize a repository identity or remote URL into a content-free `host/path`
282
+ * identifier suitable for remote discovery catalogs. Strips credentials, query
283
+ * strings, fragments, and `.git` suffixes, and rejects local filesystem paths.
284
+ */
285
+ export function sanitizeRemoteRepoIdentity(raw: string | null | undefined): string | null {
286
+ if (typeof raw !== 'string') return null
287
+ const trimmed = raw.trim()
288
+ if (!trimmed) return null
289
+
290
+ // Reject local filesystem paths (Unix absolute/relative/home, Windows drive/UNC, file:// URLs).
291
+ if (
292
+ trimmed.startsWith('/')
293
+ || trimmed.startsWith('./')
294
+ || trimmed.startsWith('../')
295
+ || trimmed.startsWith('~/')
296
+ || trimmed.startsWith('\\')
297
+ || /^[a-zA-Z]:[\\/]/.test(trimmed)
298
+ || /^file:/i.test(trimmed)
299
+ ) {
300
+ return null
301
+ }
302
+
303
+ const withoutQuery = trimmed.replace(/[?#].*$/, '').replace(/\/+$/, '')
304
+ if (!withoutQuery) return null
305
+
306
+ let host = ''
307
+ let repoPath = ''
308
+
309
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(withoutQuery)) {
310
+ try {
311
+ const url = new URL(withoutQuery)
312
+ if (url.protocol === 'file:') return null
313
+ host = url.hostname.toLowerCase()
314
+ repoPath = decodeURIComponent(url.pathname)
315
+ .replace(/^\/+|\/+$/g, '')
316
+ .replace(/\.git$/i, '')
317
+ } catch {
318
+ return null
319
+ }
320
+ } else {
321
+ const scpMatch = withoutQuery.match(/^(?:[^@/:]+@)?(\[[^\]]+\]|[^/:]+):(.+)$/)
322
+ if (scpMatch) {
323
+ host = scpMatch[1].replace(/^\[|\]$/g, '').toLowerCase()
324
+ repoPath = scpMatch[2].replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
325
+ } else {
326
+ const slash = withoutQuery.indexOf('/')
327
+ if (slash > 0) {
328
+ const rawHost = withoutQuery.slice(0, slash)
329
+ const atIdx = rawHost.lastIndexOf('@')
330
+ host = (atIdx >= 0 ? rawHost.slice(atIdx + 1) : rawHost).toLowerCase()
331
+ repoPath = withoutQuery.slice(slash + 1).replace(/^\/+|\/+$/g, '').replace(/\.git$/i, '')
332
+ }
333
+ }
334
+ }
335
+
336
+ if (!host || !repoPath) return null
337
+ if (!host.includes('.') && host !== 'localhost') return null
338
+ if (/[\s@:\\]/.test(host)) return null
339
+ const segments = repoPath.split('/').filter(Boolean)
340
+ if (segments.length === 0) return null
341
+ if (segments.some(seg => seg === '.' || seg === '..' || /[\s@:\\]/.test(seg))) return null
342
+
343
+ return `${host}/${segments.join('/')}`.slice(0, 200)
344
+ }
345
+
package/src/index.ts CHANGED
@@ -45,3 +45,5 @@ export * from './keyed-doc-delta'
45
45
  export * from './provider-approval'
46
46
  export * from './provider-question'
47
47
  export * from './launch-defaults'
48
+
49
+ export * from './role-launch';
@@ -57,7 +57,7 @@ export interface LaunchDefaultAxis {
57
57
  export type LaunchDefaultWarning = 'catalog_unverified' | 'manual_mcp_setup_required';
58
58
  export type LaunchDefaultValidationReason =
59
59
  | 'invalid_schema' | 'provider_unknown' | 'provider_not_cli' | 'provider_disabled'
60
- | 'provider_not_detected' | 'provider_not_eligible' | 'model_unsupported'
60
+ | 'provider_not_detected' | 'provider_not_eligible' | 'provider_quota_gated' | 'model_unsupported'
61
61
  | 'model_provider_mismatch' | 'model_not_offered' | 'thinking_unsupported'
62
62
  | 'thinking_not_offered' | 'catalog_unavailable';
63
63
  export type LaunchDefaultResolution =
@@ -65,6 +65,9 @@ export const CANONICAL_MESH_TOOL_NAMES = [
65
65
  'mesh_mission_list',
66
66
  'mesh_review_inbox',
67
67
  'mesh_node_slots',
68
+ 'driver_list',
69
+ 'driver_describe',
70
+ 'artifact_list',
68
71
  ] as const;
69
72
 
70
73
  export type CanonicalMeshToolName = typeof CANONICAL_MESH_TOOL_NAMES[number];
@@ -321,3 +321,20 @@ export function isOutboundMessage(value: unknown): value is OutboundMessage {
321
321
  && isSendPolicy(candidate.policy)
322
322
  && typeof candidate.createdAt === 'number'
323
323
  }
324
+
325
+ const PASTED_OPEN_RE = /^\s*<pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
326
+ const PASTED_CLOSE_RE = /^\s*<\/pasted_content(?:\s+id="[^"]*")?\s*>\s*$/
327
+
328
+ /**
329
+ * Drop the `<pasted_content>` envelope lines Claude Code wraps around a pasted
330
+ * input. Only a text that OPENS with the envelope is touched; anything else is
331
+ * returned unchanged. Shared across daemon transcript merge and web echo/display
332
+ * normalization so wrapped turns correlate identically.
333
+ */
334
+ export function unwrapPastedContent(text: string): string {
335
+ const lines = text.replace(/\r\n?/g, '\n').split('\n')
336
+ const first = lines.findIndex((line) => line.trim())
337
+ if (first < 0 || !PASTED_OPEN_RE.test(lines[first])) return text
338
+ return lines.filter((line) => !PASTED_OPEN_RE.test(line) && !PASTED_CLOSE_RE.test(line)).join('\n')
339
+ }
340
+
@@ -260,8 +260,8 @@ export const DAEMON_P2P_SIGNAL_TYPES = ['p2p_offer', 'p2p_ice'] as const
260
260
  export type DaemonP2PSignalType = typeof DAEMON_P2P_SIGNAL_TYPES[number]
261
261
  export const isDaemonP2PSignalType = makeTypeGuard(DAEMON_P2P_SIGNAL_TYPES)
262
262
 
263
- export type P2POfferWirePayload = { sdp: string; type: string; peerId: string }
264
- export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string }
263
+ export type P2POfferWirePayload = { sdp: string; type: string; peerId: string; attemptId?: number; attemptEpoch?: string }
264
+ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string; attemptId?: number; attemptEpoch?: string }
265
265
 
266
266
  /**
267
267
  * A dashboard-originated signal as relayed to the daemon. `peerId` is stamped
@@ -273,6 +273,8 @@ export type P2PIceWirePayload = { candidate: string; mid: string; peerId: string
273
273
  export type RelayedP2PSignalWirePayload = {
274
274
  peerId?: string
275
275
  daemonId?: string
276
+ attemptId?: number
277
+ attemptEpoch?: string
276
278
  sharePermission?: string
277
279
  shareTargetSessionId?: string
278
280
  sdp?: string
@@ -152,6 +152,8 @@ export type SystemAnnouncementWirePayload = {
152
152
  export type DashboardP2POfferWirePayload = {
153
153
  daemonId: string
154
154
  peerId?: string
155
+ attemptId?: number
156
+ attemptEpoch?: string
155
157
  sdp: string
156
158
  type: string
157
159
  }
@@ -159,6 +161,8 @@ export type DashboardP2POfferWirePayload = {
159
161
  export type DashboardP2PIceWirePayload = {
160
162
  daemonId: string
161
163
  peerId?: string
164
+ attemptId?: number
165
+ attemptEpoch?: string
162
166
  candidate: string
163
167
  mid?: string
164
168
  sdpMid?: string | null
@@ -234,11 +238,13 @@ export const DASHBOARD_P2P_SIGNAL_TYPES = ['p2p_ready', 'p2p_answer', 'p2p_ice']
234
238
  export type DashboardP2PSignalType = typeof DASHBOARD_P2P_SIGNAL_TYPES[number]
235
239
  export const isDashboardP2PSignalType = makeTypeGuard(DASHBOARD_P2P_SIGNAL_TYPES)
236
240
 
237
- export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string }
238
- export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string }
241
+ export type DashboardP2PReadyWirePayload = { daemonId: string; peerId: string; attemptId?: number; attemptEpoch?: string }
242
+ export type DashboardP2PAnswerWirePayload = { daemonId: string; peerId: string; sdp: string; type: string; attemptId?: number; attemptEpoch?: string }
239
243
  export type DashboardP2PLocalIceWirePayload = {
240
244
  daemonId: string
241
245
  peerId: string
246
+ attemptId?: number
247
+ attemptEpoch?: string
242
248
  candidate: string
243
249
  sdpMid: string | null
244
250
  sdpMLineIndex: number | null
@@ -0,0 +1,51 @@
1
+ /** Persisted role-launch intent. Restart replays this selection rather than
2
+ * consulting newer machine defaults; restore preserves its original provenance. */
3
+ import { isLaunchDefaultRole, isLaunchDefaultSource, type LaunchDefaultRole,
4
+ type LaunchDefaultSelection, type LaunchDefaultAxis, type LaunchDefaultRef } from './launch-defaults';
5
+ export interface RoleLaunchSelection {
6
+ role: LaunchDefaultRole;
7
+ selection: LaunchDefaultSelection;
8
+ applied: Record<keyof LaunchDefaultSelection, LaunchDefaultAxis>;
9
+ defaultRef?: LaunchDefaultRef;
10
+ documentRevision: number;
11
+ }
12
+ function record(value: unknown): Record<string, unknown> | undefined {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
14
+ ? value as Record<string, unknown> : undefined;
15
+ }
16
+ function ref(value: unknown): LaunchDefaultRef | undefined {
17
+ const r = record(value);
18
+ if (!r || !isLaunchDefaultRole(r.role) || (r.scope !== 'global' && r.scope !== 'project')
19
+ || !Number.isSafeInteger(r.revision) || (r.revision as number) < 1
20
+ || (r.scope === 'project' && (typeof r.projectId !== 'string' || !r.projectId))) return undefined;
21
+ return { role: r.role, scope: r.scope, revision: r.revision as number,
22
+ ...(r.scope === 'project' ? { projectId: r.projectId as string } : {}) };
23
+ }
24
+ function axis(value: unknown): LaunchDefaultAxis | undefined {
25
+ const a = record(value);
26
+ if (!a || !isLaunchDefaultSource(a.source) || (a.value !== null && typeof a.value !== 'string')) return undefined;
27
+ const defaultRef = ref(a.defaultRef);
28
+ if (a.source === 'stored_default' && defaultRef?.scope !== 'global'
29
+ || a.source === 'project_default' && defaultRef?.scope !== 'project'
30
+ || (a.source === 'user' || a.source === 'provider_default') && a.defaultRef !== undefined) return undefined;
31
+ return { value: a.value as string | null, source: a.source, ...(defaultRef ? { defaultRef } : {}) };
32
+ }
33
+ export function parseRoleLaunchSelection(value: unknown): RoleLaunchSelection | undefined {
34
+ const r = record(value), s = record(r?.selection), a = record(r?.applied);
35
+ if (!r || !isLaunchDefaultRole(r.role) || !s || typeof s.provider !== 'string' || !s.provider
36
+ || !Number.isSafeInteger(r.documentRevision) || (r.documentRevision as number) < 1
37
+ || (s.model !== undefined && typeof s.model !== 'string')
38
+ || (s.thinkingLevel !== undefined && typeof s.thinkingLevel !== 'string')) return undefined;
39
+ const provider = axis(a?.provider), model = axis(a?.model), thinkingLevel = axis(a?.thinkingLevel);
40
+ if (!provider || !model || !thinkingLevel
41
+ || [provider, model, thinkingLevel].some(axis => axis.defaultRef && (axis.defaultRef.role !== r.role
42
+ || axis.defaultRef.scope === 'global' && axis.defaultRef.revision > (r.documentRevision as number)))
43
+ || provider.value !== s.provider
44
+ || model.value !== (s.model ?? null) || thinkingLevel.value !== (s.thinkingLevel ?? null)) return undefined;
45
+ const defaultRef = ref(r.defaultRef);
46
+ return { role: r.role, selection: { provider: s.provider,
47
+ ...(typeof s.model === 'string' ? { model: s.model } : {}),
48
+ ...(typeof s.thinkingLevel === 'string' ? { thinkingLevel: s.thinkingLevel } : {}) },
49
+ applied: { provider, model, thinkingLevel }, ...(defaultRef ? { defaultRef } : {}),
50
+ documentRevision: r.documentRevision as number };
51
+ }