agent-relay 2.0.32 → 2.0.34

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.
Files changed (68) hide show
  1. package/README.md +44 -0
  2. package/dist/index.cjs +7231 -6234
  3. package/package.json +19 -18
  4. package/packages/api-types/.trajectories/active/traj_xbsvuzogscey.json +15 -0
  5. package/packages/api-types/.trajectories/index.json +12 -0
  6. package/packages/api-types/package.json +1 -1
  7. package/packages/benchmark/package.json +4 -4
  8. package/packages/bridge/dist/spawner.d.ts.map +1 -1
  9. package/packages/bridge/dist/spawner.js +127 -0
  10. package/packages/bridge/dist/spawner.js.map +1 -1
  11. package/packages/bridge/package.json +8 -8
  12. package/packages/bridge/src/spawner.ts +137 -0
  13. package/packages/cli-tester/package.json +1 -1
  14. package/packages/config/package.json +2 -2
  15. package/packages/continuity/package.json +1 -1
  16. package/packages/daemon/package.json +12 -12
  17. package/packages/hooks/package.json +4 -4
  18. package/packages/mcp/package.json +3 -3
  19. package/packages/memory/package.json +2 -2
  20. package/packages/policy/package.json +2 -2
  21. package/packages/protocol/package.json +1 -1
  22. package/packages/resiliency/package.json +1 -1
  23. package/packages/sdk/package.json +2 -2
  24. package/packages/spawner/package.json +1 -1
  25. package/packages/state/package.json +1 -1
  26. package/packages/storage/package.json +2 -2
  27. package/packages/telemetry/package.json +1 -1
  28. package/packages/trajectory/package.json +2 -2
  29. package/packages/user-directory/package.json +2 -2
  30. package/packages/utils/package.json +2 -2
  31. package/packages/wrapper/dist/base-wrapper.d.ts.map +1 -1
  32. package/packages/wrapper/dist/base-wrapper.js +27 -7
  33. package/packages/wrapper/dist/base-wrapper.js.map +1 -1
  34. package/packages/wrapper/dist/client.d.ts +27 -0
  35. package/packages/wrapper/dist/client.d.ts.map +1 -1
  36. package/packages/wrapper/dist/client.js +116 -0
  37. package/packages/wrapper/dist/client.js.map +1 -1
  38. package/packages/wrapper/dist/index.d.ts +3 -0
  39. package/packages/wrapper/dist/index.d.ts.map +1 -1
  40. package/packages/wrapper/dist/index.js +6 -0
  41. package/packages/wrapper/dist/index.js.map +1 -1
  42. package/packages/wrapper/dist/opencode-api.d.ts +106 -0
  43. package/packages/wrapper/dist/opencode-api.d.ts.map +1 -0
  44. package/packages/wrapper/dist/opencode-api.js +219 -0
  45. package/packages/wrapper/dist/opencode-api.js.map +1 -0
  46. package/packages/wrapper/dist/opencode-wrapper.d.ts +157 -0
  47. package/packages/wrapper/dist/opencode-wrapper.d.ts.map +1 -0
  48. package/packages/wrapper/dist/opencode-wrapper.js +414 -0
  49. package/packages/wrapper/dist/opencode-wrapper.js.map +1 -0
  50. package/packages/wrapper/dist/relay-pty-orchestrator.d.ts.map +1 -1
  51. package/packages/wrapper/dist/relay-pty-orchestrator.js +18 -0
  52. package/packages/wrapper/dist/relay-pty-orchestrator.js.map +1 -1
  53. package/packages/wrapper/dist/wrapper-events.d.ts +489 -0
  54. package/packages/wrapper/dist/wrapper-events.d.ts.map +1 -0
  55. package/packages/wrapper/dist/wrapper-events.js +252 -0
  56. package/packages/wrapper/dist/wrapper-events.js.map +1 -0
  57. package/packages/wrapper/package.json +7 -6
  58. package/packages/wrapper/src/base-wrapper.ts +23 -7
  59. package/packages/wrapper/src/client.test.ts +92 -3
  60. package/packages/wrapper/src/client.ts +163 -0
  61. package/packages/wrapper/src/index.ts +29 -0
  62. package/packages/wrapper/src/opencode-api.test.ts +292 -0
  63. package/packages/wrapper/src/opencode-api.ts +285 -0
  64. package/packages/wrapper/src/opencode-wrapper.ts +513 -0
  65. package/packages/wrapper/src/relay-pty-orchestrator.test.ts +176 -0
  66. package/packages/wrapper/src/relay-pty-orchestrator.ts +20 -0
  67. package/packages/wrapper/src/wrapper-events.ts +395 -0
  68. package/scripts/postinstall.js +147 -2
@@ -57,6 +57,13 @@ import {
57
57
 
58
58
  const MAX_SOCKET_PATH_LENGTH = 107;
59
59
 
60
+ /**
61
+ * Maximum size for output buffers (rawBuffer, outputBuffer) in bytes.
62
+ * Prevents RangeError: Invalid string length when agents produce lots of output.
63
+ * Set to 10MB - enough to capture context but won't exhaust memory.
64
+ */
65
+ const MAX_OUTPUT_BUFFER_SIZE = 10 * 1024 * 1024; // 10MB
66
+
60
67
  function hashWorkspaceId(workspaceId: string): string {
61
68
  return createHash('sha256').update(workspaceId).digest('hex').slice(0, 12);
62
69
  }
@@ -956,6 +963,19 @@ export class RelayPtyOrchestrator extends BaseWrapper {
956
963
  this.outputBuffer += data;
957
964
  this.hasReceivedOutput = true;
958
965
 
966
+ // Trim buffers if they exceed max size to prevent RangeError: Invalid string length
967
+ // Keep the most recent output (tail) as it's more relevant for pattern matching
968
+ if (this.rawBuffer.length > MAX_OUTPUT_BUFFER_SIZE) {
969
+ const trimAmount = this.rawBuffer.length - MAX_OUTPUT_BUFFER_SIZE;
970
+ this.rawBuffer = this.rawBuffer.slice(-MAX_OUTPUT_BUFFER_SIZE);
971
+ // Adjust lastParsedLength to stay in sync with the trimmed buffer
972
+ // This ensures parseRelayCommands() doesn't skip content or re-parse old content
973
+ this.lastParsedLength = Math.max(0, this.lastParsedLength - trimAmount);
974
+ }
975
+ if (this.outputBuffer.length > MAX_OUTPUT_BUFFER_SIZE) {
976
+ this.outputBuffer = this.outputBuffer.slice(-MAX_OUTPUT_BUFFER_SIZE);
977
+ }
978
+
959
979
  // Feed to idle detector
960
980
  this.feedIdleDetectorOutput(data);
961
981
 
@@ -0,0 +1,395 @@
1
+ /**
2
+ * Typed Event Definitions for Agent Relay Wrapper
3
+ *
4
+ * Inspired by opencode's BusEvent pattern, this provides type-safe event
5
+ * definitions with Zod schema validation. Events can be used for:
6
+ * - Wrapper internal notifications
7
+ * - SDK client subscriptions
8
+ * - OpenAPI spec generation
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * import { RelayEvent, emitEvent, onEvent } from './wrapper-events.js';
13
+ *
14
+ * // Subscribe to events
15
+ * onEvent(RelayEvent.AgentConnected, (event) => {
16
+ * console.log(`Agent ${event.properties.agentName} connected`);
17
+ * });
18
+ *
19
+ * // Emit events
20
+ * emitEvent(RelayEvent.AgentConnected, {
21
+ * agentName: 'MyAgent',
22
+ * connectionId: 'abc123',
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ import { z } from 'zod';
28
+ import { EventEmitter } from 'node:events';
29
+
30
+ // =========================================================================
31
+ // Event Definition Factory
32
+ // =========================================================================
33
+
34
+ /**
35
+ * Event definition with type and schema
36
+ */
37
+ export interface EventDefinition<
38
+ Type extends string = string,
39
+ Schema extends z.ZodType = z.ZodType
40
+ > {
41
+ type: Type;
42
+ schema: Schema;
43
+ }
44
+
45
+ /**
46
+ * Define a typed event with Zod schema validation
47
+ */
48
+ export function defineEvent<Type extends string, Schema extends z.ZodType>(
49
+ type: Type,
50
+ schema: Schema
51
+ ): EventDefinition<Type, Schema> {
52
+ return { type, schema };
53
+ }
54
+
55
+ /**
56
+ * Infer the properties type from an event definition
57
+ */
58
+ export type EventProperties<E extends EventDefinition> = z.infer<E['schema']>;
59
+
60
+ /**
61
+ * Event payload with type and properties
62
+ */
63
+ export interface EventPayload<E extends EventDefinition = EventDefinition> {
64
+ type: E['type'];
65
+ properties: EventProperties<E>;
66
+ timestamp: number;
67
+ }
68
+
69
+ // =========================================================================
70
+ // Daemon Event Definitions
71
+ // =========================================================================
72
+
73
+ export namespace RelayEvent {
74
+ // --- Agent Lifecycle Events ---
75
+
76
+ export const AgentConnected = defineEvent(
77
+ 'daemon.agent.connected',
78
+ z.object({
79
+ agentName: z.string(),
80
+ connectionId: z.string(),
81
+ cli: z.string().optional(),
82
+ task: z.string().optional(),
83
+ workingDirectory: z.string().optional(),
84
+ })
85
+ );
86
+
87
+ export const AgentDisconnected = defineEvent(
88
+ 'daemon.agent.disconnected',
89
+ z.object({
90
+ agentName: z.string(),
91
+ connectionId: z.string(),
92
+ reason: z.enum(['clean', 'error', 'timeout', 'replaced']).optional(),
93
+ })
94
+ );
95
+
96
+ export const AgentSpawned = defineEvent(
97
+ 'daemon.agent.spawned',
98
+ z.object({
99
+ agentName: z.string(),
100
+ parentAgent: z.string(),
101
+ cli: z.string(),
102
+ task: z.string(),
103
+ })
104
+ );
105
+
106
+ export const AgentReleased = defineEvent(
107
+ 'daemon.agent.released',
108
+ z.object({
109
+ agentName: z.string(),
110
+ releasedBy: z.string(),
111
+ })
112
+ );
113
+
114
+ // --- Message Events ---
115
+
116
+ export const MessageRouted = defineEvent(
117
+ 'daemon.message.routed',
118
+ z.object({
119
+ messageId: z.string(),
120
+ from: z.string(),
121
+ to: z.string(),
122
+ kind: z.string().optional(),
123
+ bodyPreview: z.string().optional(),
124
+ })
125
+ );
126
+
127
+ export const MessageDelivered = defineEvent(
128
+ 'daemon.message.delivered',
129
+ z.object({
130
+ messageId: z.string(),
131
+ to: z.string(),
132
+ deliverySeq: z.number(),
133
+ })
134
+ );
135
+
136
+ export const MessageFailed = defineEvent(
137
+ 'daemon.message.failed',
138
+ z.object({
139
+ messageId: z.string(),
140
+ to: z.string(),
141
+ error: z.string(),
142
+ })
143
+ );
144
+
145
+ // --- Channel Events ---
146
+
147
+ export const ChannelJoined = defineEvent(
148
+ 'daemon.channel.joined',
149
+ z.object({
150
+ channel: z.string(),
151
+ member: z.string(),
152
+ })
153
+ );
154
+
155
+ export const ChannelLeft = defineEvent(
156
+ 'daemon.channel.left',
157
+ z.object({
158
+ channel: z.string(),
159
+ member: z.string(),
160
+ })
161
+ );
162
+
163
+ export const ChannelMessage = defineEvent(
164
+ 'daemon.channel.message',
165
+ z.object({
166
+ channel: z.string(),
167
+ from: z.string(),
168
+ bodyPreview: z.string().optional(),
169
+ })
170
+ );
171
+
172
+ // --- Processing State Events ---
173
+
174
+ export const AgentProcessingStarted = defineEvent(
175
+ 'daemon.agent.processing.started',
176
+ z.object({
177
+ agentName: z.string(),
178
+ messageId: z.string(),
179
+ })
180
+ );
181
+
182
+ export const AgentProcessingEnded = defineEvent(
183
+ 'daemon.agent.processing.ended',
184
+ z.object({
185
+ agentName: z.string(),
186
+ durationMs: z.number().optional(),
187
+ })
188
+ );
189
+
190
+ // --- Shadow Events ---
191
+
192
+ export const ShadowBound = defineEvent(
193
+ 'daemon.shadow.bound',
194
+ z.object({
195
+ shadowAgent: z.string(),
196
+ primaryAgent: z.string(),
197
+ speakOn: z.array(z.string()),
198
+ })
199
+ );
200
+
201
+ export const ShadowUnbound = defineEvent(
202
+ 'daemon.shadow.unbound',
203
+ z.object({
204
+ shadowAgent: z.string(),
205
+ primaryAgent: z.string(),
206
+ })
207
+ );
208
+
209
+ // --- System Events ---
210
+
211
+ export const DaemonStarted = defineEvent(
212
+ 'daemon.system.started',
213
+ z.object({
214
+ socketPath: z.string(),
215
+ version: z.string().optional(),
216
+ })
217
+ );
218
+
219
+ export const DaemonStopped = defineEvent(
220
+ 'daemon.system.stopped',
221
+ z.object({
222
+ reason: z.string().optional(),
223
+ })
224
+ );
225
+
226
+ export const RateLimitExceeded = defineEvent(
227
+ 'daemon.system.rate_limit_exceeded',
228
+ z.object({
229
+ agentName: z.string(),
230
+ })
231
+ );
232
+
233
+ // --- All event definitions for iteration ---
234
+
235
+ export const all = [
236
+ AgentConnected,
237
+ AgentDisconnected,
238
+ AgentSpawned,
239
+ AgentReleased,
240
+ MessageRouted,
241
+ MessageDelivered,
242
+ MessageFailed,
243
+ ChannelJoined,
244
+ ChannelLeft,
245
+ ChannelMessage,
246
+ AgentProcessingStarted,
247
+ AgentProcessingEnded,
248
+ ShadowBound,
249
+ ShadowUnbound,
250
+ DaemonStarted,
251
+ DaemonStopped,
252
+ RateLimitExceeded,
253
+ ] as const;
254
+ }
255
+
256
+ // =========================================================================
257
+ // Event Bus
258
+ // =========================================================================
259
+
260
+ /**
261
+ * Type-safe event bus for daemon events
262
+ */
263
+ class RelayEventBus extends EventEmitter {
264
+ private static instance: RelayEventBus;
265
+
266
+ private constructor() {
267
+ super();
268
+ this.setMaxListeners(100); // Allow many subscribers
269
+ }
270
+
271
+ static getInstance(): RelayEventBus {
272
+ if (!RelayEventBus.instance) {
273
+ RelayEventBus.instance = new RelayEventBus();
274
+ }
275
+ return RelayEventBus.instance;
276
+ }
277
+
278
+ /**
279
+ * Emit a typed event
280
+ */
281
+ emitEvent<E extends EventDefinition>(
282
+ definition: E,
283
+ properties: EventProperties<E>
284
+ ): void {
285
+ const payload: EventPayload<E> = {
286
+ type: definition.type,
287
+ properties,
288
+ timestamp: Date.now(),
289
+ };
290
+
291
+ // Validate properties against schema
292
+ const result = definition.schema.safeParse(properties);
293
+ if (!result.success) {
294
+ console.error(`[RelayEventBus] Invalid event properties for ${definition.type}:`, result.error);
295
+ return;
296
+ }
297
+
298
+ // Emit to specific subscribers and wildcard subscribers
299
+ this.emit(definition.type, payload);
300
+ this.emit('*', payload);
301
+ }
302
+
303
+ /**
304
+ * Subscribe to a typed event
305
+ */
306
+ onEvent<E extends EventDefinition>(
307
+ definition: E,
308
+ callback: (event: EventPayload<E>) => void
309
+ ): () => void {
310
+ this.on(definition.type, callback);
311
+ return () => this.off(definition.type, callback);
312
+ }
313
+
314
+ /**
315
+ * Subscribe to all events (wildcard)
316
+ */
317
+ onAnyEvent(callback: (event: EventPayload) => void): () => void {
318
+ this.on('*', callback);
319
+ return () => this.off('*', callback);
320
+ }
321
+
322
+ /**
323
+ * Subscribe to an event once
324
+ */
325
+ onceEvent<E extends EventDefinition>(
326
+ definition: E,
327
+ callback: (event: EventPayload<E>) => void
328
+ ): void {
329
+ this.once(definition.type, callback);
330
+ }
331
+ }
332
+
333
+ // =========================================================================
334
+ // Exports
335
+ // =========================================================================
336
+
337
+ /**
338
+ * Global daemon event bus instance
339
+ */
340
+ export const relayEventBus = RelayEventBus.getInstance();
341
+
342
+ /**
343
+ * Emit a daemon event
344
+ */
345
+ export function emitEvent<E extends EventDefinition>(
346
+ definition: E,
347
+ properties: EventProperties<E>
348
+ ): void {
349
+ relayEventBus.emitEvent(definition, properties);
350
+ }
351
+
352
+ /**
353
+ * Subscribe to a daemon event
354
+ */
355
+ export function onEvent<E extends EventDefinition>(
356
+ definition: E,
357
+ callback: (event: EventPayload<E>) => void
358
+ ): () => void {
359
+ return relayEventBus.onEvent(definition, callback);
360
+ }
361
+
362
+ /**
363
+ * Subscribe to all daemon events
364
+ */
365
+ export function onAnyEvent(callback: (event: EventPayload) => void): () => void {
366
+ return relayEventBus.onAnyEvent(callback);
367
+ }
368
+
369
+ // =========================================================================
370
+ // OpenAPI Schema Generation
371
+ // =========================================================================
372
+
373
+ /**
374
+ * Generate OpenAPI-compatible schema for all daemon events
375
+ * This can be used to auto-generate SDK types
376
+ */
377
+ export function generateEventSchemas(): Record<string, unknown> {
378
+ const schemas: Record<string, unknown> = {};
379
+
380
+ for (const definition of RelayEvent.all) {
381
+ // Extract JSON Schema from Zod schema
382
+ // Note: This is a simplified version - production use should use zod-to-json-schema
383
+ schemas[definition.type] = {
384
+ type: 'object',
385
+ properties: {
386
+ type: { type: 'string', const: definition.type },
387
+ properties: definition.schema._def, // Simplified - use zod-to-json-schema for production
388
+ timestamp: { type: 'number' },
389
+ },
390
+ required: ['type', 'properties', 'timestamp'],
391
+ };
392
+ }
393
+
394
+ return schemas;
395
+ }
@@ -279,6 +279,132 @@ function hasSystemTmux() {
279
279
  }
280
280
  }
281
281
 
282
+ /**
283
+ * Setup workspace package symlinks for global/bundled installs.
284
+ *
285
+ * When agent-relay is installed globally (npm install -g), the workspace packages
286
+ * are included in the tarball at packages/* but Node.js module resolution expects
287
+ * them at node_modules/@agent-relay/*. This function creates symlinks to bridge
288
+ * the gap.
289
+ *
290
+ * This is needed because npm's bundledDependencies doesn't properly handle
291
+ * workspace packages (which are symlinks during development).
292
+ */
293
+ function setupWorkspacePackageLinks() {
294
+ const pkgRoot = getPackageRoot();
295
+ const packagesDir = path.join(pkgRoot, 'packages');
296
+ const nodeModulesDir = path.join(pkgRoot, 'node_modules');
297
+ const scopeDir = path.join(nodeModulesDir, '@agent-relay');
298
+
299
+ // Check if packages/ exists (we're in a bundled/global install)
300
+ if (!fs.existsSync(packagesDir)) {
301
+ // Not a bundled install, workspace packages should be in node_modules already
302
+ return { needed: false };
303
+ }
304
+
305
+ // Check if node_modules/@agent-relay/daemon exists
306
+ const testPackage = path.join(scopeDir, 'daemon');
307
+ if (fs.existsSync(testPackage)) {
308
+ // Already set up (either normal npm install or previously linked)
309
+ info('Workspace packages already available in node_modules');
310
+ return { needed: false, alreadySetup: true };
311
+ }
312
+
313
+ // We need to create symlinks
314
+ info('Setting up workspace package links for global install...');
315
+
316
+ // Create node_modules/@agent-relay/ directory
317
+ try {
318
+ fs.mkdirSync(scopeDir, { recursive: true });
319
+ } catch (err) {
320
+ warn(`Failed to create @agent-relay scope directory: ${err.message}`);
321
+ return { needed: true, success: false, error: err.message };
322
+ }
323
+
324
+ // Map from package directory name to npm package name
325
+ const packageDirs = fs.readdirSync(packagesDir).filter(dir => {
326
+ const pkgJsonPath = path.join(packagesDir, dir, 'package.json');
327
+ return fs.existsSync(pkgJsonPath);
328
+ });
329
+
330
+ let linked = 0;
331
+ let failed = 0;
332
+ const errors = [];
333
+
334
+ for (const dir of packageDirs) {
335
+ const sourcePath = path.join(packagesDir, dir);
336
+ const targetPath = path.join(scopeDir, dir);
337
+
338
+ // Skip if already exists
339
+ if (fs.existsSync(targetPath)) {
340
+ continue;
341
+ }
342
+
343
+ try {
344
+ // Use relative symlink for portability
345
+ const relativeSource = path.relative(scopeDir, sourcePath);
346
+ fs.symlinkSync(relativeSource, targetPath, 'dir');
347
+ linked++;
348
+ } catch (err) {
349
+ // If symlink fails (e.g., on Windows without admin), try copying
350
+ try {
351
+ // Copy the package directory
352
+ copyDirSync(sourcePath, targetPath);
353
+ linked++;
354
+ } catch (copyErr) {
355
+ failed++;
356
+ errors.push(`${dir}: ${copyErr.message}`);
357
+ }
358
+ }
359
+ }
360
+
361
+ if (linked > 0) {
362
+ success(`Linked ${linked} workspace packages to node_modules/@agent-relay/`);
363
+ }
364
+
365
+ if (failed > 0) {
366
+ warn(`Failed to link ${failed} packages: ${errors.join(', ')}`);
367
+ return { needed: true, success: false, linked, failed, errors };
368
+ }
369
+
370
+ return { needed: true, success: true, linked };
371
+ }
372
+
373
+ /**
374
+ * Recursively copy a directory
375
+ */
376
+ function copyDirSync(src, dest) {
377
+ fs.mkdirSync(dest, { recursive: true });
378
+ const entries = fs.readdirSync(src, { withFileTypes: true });
379
+
380
+ for (const entry of entries) {
381
+ const srcPath = path.join(src, entry.name);
382
+ const destPath = path.join(dest, entry.name);
383
+
384
+ // Skip node_modules in package copies
385
+ if (entry.name === 'node_modules') {
386
+ continue;
387
+ }
388
+
389
+ if (entry.isDirectory()) {
390
+ copyDirSync(srcPath, destPath);
391
+ } else if (entry.isSymbolicLink()) {
392
+ // Resolve symlink and copy the target
393
+ const linkTarget = fs.readlinkSync(srcPath);
394
+ const resolvedTarget = path.resolve(path.dirname(srcPath), linkTarget);
395
+ if (fs.existsSync(resolvedTarget)) {
396
+ if (fs.statSync(resolvedTarget).isDirectory()) {
397
+ copyDirSync(resolvedTarget, destPath);
398
+ } else {
399
+ fs.copyFileSync(resolvedTarget, destPath);
400
+ }
401
+ }
402
+ } else {
403
+ fs.copyFileSync(srcPath, destPath);
404
+ }
405
+ }
406
+ }
407
+
282
408
  /**
283
409
  * Install dashboard dependencies
284
410
  */
@@ -362,7 +488,16 @@ function patchAgentTrajectories() {
362
488
  success('Patched agent-trajectories to record agent on trail start');
363
489
  }
364
490
 
365
- function logPostinstallDiagnostics(hasRelayPty, sqliteStatus) {
491
+ function logPostinstallDiagnostics(hasRelayPty, sqliteStatus, linkResult) {
492
+ // Workspace packages status (for global installs)
493
+ if (linkResult && linkResult.needed) {
494
+ if (linkResult.success) {
495
+ console.log(`✓ Workspace packages linked (${linkResult.linked} packages)`);
496
+ } else {
497
+ console.log('⚠ Workspace package linking failed - CLI may not work');
498
+ }
499
+ }
500
+
366
501
  if (hasRelayPty) {
367
502
  console.log('✓ relay-pty binary installed');
368
503
  } else {
@@ -388,6 +523,16 @@ function logPostinstallDiagnostics(hasRelayPty, sqliteStatus) {
388
523
  * Main postinstall routine
389
524
  */
390
525
  async function main() {
526
+ // Setup workspace package links for global installs
527
+ // This MUST run first so that other postinstall steps can find the packages
528
+ const linkResult = setupWorkspacePackageLinks();
529
+ if (linkResult.needed && !linkResult.success) {
530
+ warn('Workspace package linking failed - CLI may not work correctly');
531
+ if (linkResult.errors) {
532
+ linkResult.errors.forEach(e => warn(` ${e}`));
533
+ }
534
+ }
535
+
391
536
  // Install relay-pty binary for current platform (primary mode)
392
537
  const hasRelayPty = installRelayPtyBinary();
393
538
 
@@ -401,7 +546,7 @@ async function main() {
401
546
  installDashboardDeps();
402
547
 
403
548
  // Always print diagnostics (even in CI)
404
- logPostinstallDiagnostics(hasRelayPty, sqliteStatus);
549
+ logPostinstallDiagnostics(hasRelayPty, sqliteStatus, linkResult);
405
550
 
406
551
  // Skip tmux check in CI environments
407
552
  if (process.env.CI === 'true') {