@exaudeus/workrail 3.19.1 → 3.20.1

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>WorkRail Console</title>
7
- <script type="module" crossorigin src="/console/assets/index-QhCFuxQV.js"></script>
8
- <link rel="stylesheet" crossorigin href="/console/assets/index-ibLhWBmX.css">
7
+ <script type="module" crossorigin src="/console/assets/index-De5T1Dpm.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/console/assets/index-B8NW82uK.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -362,8 +362,14 @@ async function startAsyncServices() {
362
362
  const flags = tsyringe_1.container.resolve(tokens_js_1.DI.Infra.FeatureFlags);
363
363
  if (flags.isEnabled('sessionTools')) {
364
364
  const server = tsyringe_1.container.resolve(tokens_js_1.DI.Infra.HttpServer);
365
- await server.start();
366
- console.error('[DI] HTTP server started');
365
+ try {
366
+ await server.start();
367
+ console.error('[DI] HTTP server started');
368
+ }
369
+ catch (httpError) {
370
+ const message = httpError instanceof Error ? httpError.message : String(httpError);
371
+ console.error(`[DI] Dashboard HTTP server unavailable: ${message}. MCP tools will still work.`);
372
+ }
367
373
  }
368
374
  asyncInitialized = true;
369
375
  }
@@ -33,6 +33,7 @@ export declare class HttpServer {
33
33
  private isPrimary;
34
34
  private lockFile;
35
35
  private heartbeat;
36
+ private _stopPromise;
36
37
  constructor(sessionManager: SessionManager, processLifecyclePolicy: ProcessLifecyclePolicy, processSignals: ProcessSignals, shutdownEvents: ShutdownEvents, dashboardMode: DashboardMode, browserBehavior: BrowserBehavior);
37
38
  private config;
38
39
  setConfig(config: ServerConfig): this;
@@ -48,6 +49,7 @@ export declare class HttpServer {
48
49
  private printBanner;
49
50
  openDashboard(sessionId?: string): Promise<string>;
50
51
  stop(): Promise<void>;
52
+ private _runStop;
51
53
  mountRoutes(installer: (app: Application) => (() => void) | void): void;
52
54
  private readonly _routeDisposers;
53
55
  finalize(): void;
@@ -42,6 +42,7 @@ let HttpServer = class HttpServer {
42
42
  this.server = null;
43
43
  this.baseUrl = '';
44
44
  this.isPrimary = false;
45
+ this._stopPromise = null;
45
46
  this.config = {};
46
47
  this._routeDisposers = [];
47
48
  this.port = 3456;
@@ -331,6 +332,7 @@ let HttpServer = class HttpServer {
331
332
  });
332
333
  }
333
334
  async start() {
335
+ this._stopPromise = null;
334
336
  const mode = this.config.dashboardMode ?? this.dashboardMode;
335
337
  if (mode.kind === 'legacy') {
336
338
  console.error('[Dashboard] Unified dashboard disabled, using legacy mode');
@@ -556,6 +558,7 @@ let HttpServer = class HttpServer {
556
558
  throw error;
557
559
  }
558
560
  }
561
+ this.server = null;
559
562
  throw new Error('No available ports in range 3457-3499');
560
563
  }
561
564
  printBanner() {
@@ -570,6 +573,10 @@ let HttpServer = class HttpServer {
570
573
  console.error();
571
574
  }
572
575
  async openDashboard(sessionId) {
576
+ if (!this.baseUrl) {
577
+ throw new Error('Dashboard is unavailable -- the HTTP server did not start successfully ' +
578
+ '(likely due to port exhaustion). MCP tools still work normally.');
579
+ }
573
580
  let url = this.baseUrl;
574
581
  if (sessionId) {
575
582
  url += `?session=${sessionId}`;
@@ -587,6 +594,14 @@ let HttpServer = class HttpServer {
587
594
  return url;
588
595
  }
589
596
  async stop() {
597
+ if (this._stopPromise !== null)
598
+ return this._stopPromise;
599
+ if (this.server === null)
600
+ return Promise.resolve();
601
+ this._stopPromise = this._runStop();
602
+ return this._stopPromise;
603
+ }
604
+ async _runStop() {
590
605
  this.heartbeat.stop();
591
606
  this.sessionManager.unwatchAll();
592
607
  for (const dispose of this._routeDisposers) {
@@ -369,16 +369,16 @@
369
369
  "sha256": "5fe866e54f796975dec5d8ba9983aefd86074db212d3fccd64eed04bc9f0b3da",
370
370
  "bytes": 8011
371
371
  },
372
- "console/assets/index-QhCFuxQV.js": {
373
- "sha256": "0096a86267fdf969abc3e3976ca44b3d3447271c7394345c8d6c27c084e6412c",
374
- "bytes": 719937
372
+ "console/assets/index-B8NW82uK.css": {
373
+ "sha256": "8588d6702df899c724f5abce3e45dd4a6f7ee06bf2b280cb3909f9cabcc0a32c",
374
+ "bytes": 58801
375
375
  },
376
- "console/assets/index-ibLhWBmX.css": {
377
- "sha256": "346e55635d4d3dc2836dae83edb8563872721bf4b0e7e1ecf47fb9603424c206",
378
- "bytes": 58826
376
+ "console/assets/index-De5T1Dpm.js": {
377
+ "sha256": "bad5fc857501a1253c800b6863e95cc854f3c8a52fea80959306ba32e15fd907",
378
+ "bytes": 721395
379
379
  },
380
380
  "console/index.html": {
381
- "sha256": "4232baa5128b860125a649d8e135da858f07290bbb9da04861945649b53ac85d",
381
+ "sha256": "3165437d9c2abc907ab1333dde87b0064e7c8fba62c3ffd2567e8bc02e75329b",
382
382
  "bytes": 417
383
383
  },
384
384
  "core/error-handler.d.ts": {
@@ -394,8 +394,8 @@
394
394
  "bytes": 620
395
395
  },
396
396
  "di/container.js": {
397
- "sha256": "6b5ca3f79bff1d6e3bbf9d19d76574611460d6a5f908ebe83fe202706fe4ae2c",
398
- "bytes": 21727
397
+ "sha256": "7956cb925d40f61746ccdcec245c5ee40d7d317893eff240a10cff595304d467",
398
+ "bytes": 22026
399
399
  },
400
400
  "di/tokens.d.ts": {
401
401
  "sha256": "04db6db34348aa36d897c85ab07eb1a386cb04e3595dbcb33525bd33974111ef",
@@ -542,12 +542,12 @@
542
542
  "bytes": 818
543
543
  },
544
544
  "infrastructure/session/HttpServer.d.ts": {
545
- "sha256": "e2676f55158fb35249fc6b8d8ae051e6b043f6445e153f1f20fe837acc2ab65c",
546
- "bytes": 1977
545
+ "sha256": "79c0d8855d9437084756186f5e115aab82c829f48bafc1a5742903a1e7d808b6",
546
+ "bytes": 2025
547
547
  },
548
548
  "infrastructure/session/HttpServer.js": {
549
- "sha256": "92c00fa2f82a503bdd35ce0b1d3665cc63a261d16be53ff82a7007a55b08994e",
550
- "bytes": 30162
549
+ "sha256": "3f2bc1fb56b8191da261567eb7fb8e78f4c3e33b54c71da361aeea51d047ccd8",
550
+ "bytes": 30743
551
551
  },
552
552
  "infrastructure/session/SessionDataNormalizer.d.ts": {
553
553
  "sha256": "c89bb5e00d7d01fb4aa6d0095602541de53c425c6b99b67fa8367eb29cb63e9e",
@@ -1102,20 +1102,20 @@
1102
1102
  "bytes": 3534
1103
1103
  },
1104
1104
  "mcp/transports/shutdown-hooks.d.ts": {
1105
- "sha256": "956a334d06ba7b967992e1ebfef67e2bac1e40cf3c1c151684d5aa7a3166f64e",
1106
- "bytes": 334
1105
+ "sha256": "abf3799e44183c8a7e3614e2f14c1d7c8c4bb4ce0b720d3005ee165e4dd6f211",
1106
+ "bytes": 547
1107
1107
  },
1108
1108
  "mcp/transports/shutdown-hooks.js": {
1109
- "sha256": "5f5e1411db09d05588731bbaa08fc42db48a9023e87043e95aa01cd8ad426191",
1110
- "bytes": 1937
1109
+ "sha256": "b44d0daaabfc4a896a910691637473d238127157f762743fec35625b6d12d4eb",
1110
+ "bytes": 2701
1111
1111
  },
1112
1112
  "mcp/transports/stdio-entry.d.ts": {
1113
1113
  "sha256": "4ced3c9e00ef67555781dea74315290eea8a9dbffd38155bc00c3fb07b0c1794",
1114
1114
  "bytes": 59
1115
1115
  },
1116
1116
  "mcp/transports/stdio-entry.js": {
1117
- "sha256": "aa2f863ee779a0f31c729e54d929dfd5cf416424f63044f8d0eda956bdcc9b9e",
1118
- "bytes": 3702
1117
+ "sha256": "d934d0a7bb5a5d4331ce424aa754c063dac0a2f914d6b9d5f75489740d325b0a",
1118
+ "bytes": 4039
1119
1119
  },
1120
1120
  "mcp/transports/transport-mode.d.ts": {
1121
1121
  "sha256": "1c59128ab0174bd2a113fff17521e6339ca367f2b8980c2f2c164ec393c10518",
@@ -2278,12 +2278,12 @@
2278
2278
  "bytes": 7393
2279
2279
  },
2280
2280
  "v2/infra/local/session-lock/index.d.ts": {
2281
- "sha256": "3b381b372da6039df678dfddf377600547e1067cf921cc1b4cdf578e5df0a1bb",
2282
- "bytes": 853
2281
+ "sha256": "e180a4202587af03e09dc82e2d8557ee2d3139ac6c5f1edee9a277a7032b9986",
2282
+ "bytes": 883
2283
2283
  },
2284
2284
  "v2/infra/local/session-lock/index.js": {
2285
- "sha256": "3ad09372cb30d959016cc2a69e91de7c7f3a251c22048a917d9862fd0d8a77f9",
2286
- "bytes": 1700
2285
+ "sha256": "a1cd3eaea35faa014922fe1502a9e029a58528421586c67ab44dc7df0183a0fe",
2286
+ "bytes": 2793
2287
2287
  },
2288
2288
  "v2/infra/local/session-store/index.d.ts": {
2289
2289
  "sha256": "33b18ccce92374f8e444a61c63238634087d222f09e834b49dd2ea38d2be3796",
@@ -1,7 +1,12 @@
1
1
  export interface ShutdownHookOptions {
2
2
  readonly onBeforeTerminate: () => Promise<void>;
3
+ readonly stdout?: NodeJS.WritableStream;
3
4
  }
4
5
  export declare function wireShutdownHooks(opts: ShutdownHookOptions): void;
6
+ export interface StdoutShutdownOptions {
7
+ readonly stdout?: NodeJS.WritableStream;
8
+ }
9
+ export declare function wireStdoutShutdown(opts?: StdoutShutdownOptions): void;
5
10
  export interface StdinShutdownOptions {
6
11
  readonly stdin?: NodeJS.ReadableStream;
7
12
  }
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.wireShutdownHooks = wireShutdownHooks;
4
+ exports.wireStdoutShutdown = wireStdoutShutdown;
4
5
  exports.wireStdinShutdown = wireStdinShutdown;
5
6
  const container_js_1 = require("../../di/container.js");
6
7
  const tokens_js_1 = require("../../di/tokens.js");
@@ -8,6 +9,10 @@ function wireShutdownHooks(opts) {
8
9
  const shutdownEvents = container_js_1.container.resolve(tokens_js_1.DI.Runtime.ShutdownEvents);
9
10
  const processSignals = container_js_1.container.resolve(tokens_js_1.DI.Runtime.ProcessSignals);
10
11
  const terminator = container_js_1.container.resolve(tokens_js_1.DI.Runtime.ProcessTerminator);
12
+ const stdout = opts.stdout ?? process.stdout;
13
+ stdout.on('error', () => {
14
+ stdout.emit('drain');
15
+ });
11
16
  processSignals.on('SIGINT', () => shutdownEvents.emit({ kind: 'shutdown_requested', signal: 'SIGINT' }));
12
17
  processSignals.on('SIGTERM', () => shutdownEvents.emit({ kind: 'shutdown_requested', signal: 'SIGTERM' }));
13
18
  processSignals.on('SIGHUP', () => shutdownEvents.emit({ kind: 'shutdown_requested', signal: 'SIGHUP' }));
@@ -29,6 +34,20 @@ function wireShutdownHooks(opts) {
29
34
  })();
30
35
  });
31
36
  }
37
+ function wireStdoutShutdown(opts) {
38
+ const shutdownEvents = container_js_1.container.resolve(tokens_js_1.DI.Runtime.ShutdownEvents);
39
+ const stdout = opts?.stdout ?? process.stdout;
40
+ stdout.on('error', (err) => {
41
+ const code = err.code;
42
+ if (code === 'EPIPE' || code === 'ERR_STREAM_DESTROYED') {
43
+ console.error('[MCP] stdout pipe broken (client disconnected), initiating shutdown');
44
+ }
45
+ else {
46
+ console.error('[MCP] stdout error:', err);
47
+ }
48
+ shutdownEvents.emit({ kind: 'shutdown_requested', signal: 'SIGHUP' });
49
+ });
50
+ }
32
51
  function wireStdinShutdown(opts) {
33
52
  const shutdownEvents = container_js_1.container.resolve(tokens_js_1.DI.Runtime.ShutdownEvents);
34
53
  const stdin = opts?.stdin ?? process.stdin;
@@ -46,6 +46,13 @@ async function fetchInitialRootsWithTimeout(server) {
46
46
  ]);
47
47
  }
48
48
  async function startStdioServer() {
49
+ process.on('uncaughtException', (err) => {
50
+ console.error('[MCP] Uncaught exception -- process will exit:', err);
51
+ });
52
+ process.on('unhandledRejection', (reason) => {
53
+ console.error('[MCP] Unhandled promise rejection:', reason);
54
+ process.exit(1);
55
+ });
49
56
  const { server, ctx, rootsManager } = await (0, server_js_1.composeServer)();
50
57
  const { StdioServerTransport } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/server/stdio.js')));
51
58
  const { RootsListChangedNotificationSchema, } = await Promise.resolve().then(() => __importStar(require('@modelcontextprotocol/sdk/types.js')));
@@ -59,6 +66,7 @@ async function startStdioServer() {
59
66
  console.error('[Roots] Failed to fetch updated roots after change notification');
60
67
  }
61
68
  });
69
+ (0, shutdown_hooks_js_1.wireStdoutShutdown)();
62
70
  const transport = new StdioServerTransport();
63
71
  await server.connect(transport);
64
72
  console.error('[Transport] WorkRail MCP Server running on stdio');
@@ -9,6 +9,7 @@ export declare class LocalSessionLockV2 implements SessionLockPortV2 {
9
9
  private readonly fs;
10
10
  private readonly clock;
11
11
  constructor(dataDir: DataDirPortV2, fs: FileSystemPortV2, clock: TimeClockPortV2);
12
+ private clearIfStaleLock;
12
13
  acquire(sessionId: SessionId): ResultAsync<SessionLockHandleV2, SessionLockError>;
13
14
  release(handle: SessionLockHandleV2): ResultAsync<void, SessionLockError>;
14
15
  }
@@ -1,12 +1,42 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalSessionLockV2 = void 0;
4
+ const neverthrow_1 = require("neverthrow");
4
5
  class LocalSessionLockV2 {
5
6
  constructor(dataDir, fs, clock) {
6
7
  this.dataDir = dataDir;
7
8
  this.fs = fs;
8
9
  this.clock = clock;
9
10
  }
11
+ clearIfStaleLock(lockPath) {
12
+ return this.fs
13
+ .readFileUtf8(lockPath)
14
+ .map((content) => {
15
+ try {
16
+ const data = JSON.parse(content);
17
+ const pid = typeof data.pid === 'number' ? data.pid : null;
18
+ if (pid === null)
19
+ return false;
20
+ try {
21
+ process.kill(pid, 0);
22
+ return false;
23
+ }
24
+ catch (e) {
25
+ return e.code === 'ESRCH';
26
+ }
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ })
32
+ .andThen((isStale) => {
33
+ if (!isStale)
34
+ return (0, neverthrow_1.okAsync)(undefined);
35
+ console.error(`[SessionLock] Removing stale lock at ${lockPath} (process no longer alive)`);
36
+ return this.fs.unlink(lockPath);
37
+ })
38
+ .orElse(() => (0, neverthrow_1.okAsync)(undefined));
39
+ }
10
40
  acquire(sessionId) {
11
41
  const sessionDir = this.dataDir.sessionDir(sessionId);
12
42
  const lockPath = this.dataDir.sessionLockPath(sessionId);
@@ -23,6 +53,7 @@ class LocalSessionLockV2 {
23
53
  };
24
54
  return this.fs
25
55
  .mkdirp(sessionDir)
56
+ .andThen(() => this.clearIfStaleLock(lockPath))
26
57
  .andThen(() => this.fs.openExclusive(lockPath, new TextEncoder().encode(JSON.stringify({
27
58
  v: 1,
28
59
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exaudeus/workrail",
3
- "version": "3.19.1",
3
+ "version": "3.20.1",
4
4
  "description": "Step-by-step workflow enforcement for AI agents via MCP",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,6 +47,8 @@
47
47
  "validate:workflow-discovery": "node scripts/validate-workflow-discovery.js",
48
48
  "precommit": "npm run validate:registry",
49
49
  "preinstall": "node -e \"const v=parseInt(process.versions.node.split('.')[0],10); if(v<20){console.error('WorkRail requires Node.js >=20. Current: '+process.versions.node+'\\nPlease upgrade: https://nodejs.org/'); process.exit(1);}\"",
50
+ "dev:mcp": "pkill -f 'dist/mcp-server.js' 2>/dev/null; sleep 0.5; WORKRAIL_TRANSPORT=http WORKRAIL_ENABLE_SESSION_TOOLS=true node dist/mcp-server.js",
51
+ "dev:mcp:watch": "WORKRAIL_TRANSPORT=http WORKRAIL_ENABLE_SESSION_TOOLS=true nodemon --watch dist --ext js --delay 2 --exec 'node dist/mcp-server.js'",
50
52
  "web:dev": "npm run build && WORKRAIL_ENABLE_SESSION_TOOLS=true node dist/mcp-server.js",
51
53
  "web:ci": "WORKRAIL_ENABLE_SESSION_TOOLS=true node dist/mcp-server.js",
52
54
  "web:typecheck": "tsc -p tsconfig.web.json",
@@ -113,17 +115,20 @@
113
115
  "@semantic-release/exec": "^7.1.0",
114
116
  "@semantic-release/git": "^10.0.1",
115
117
  "@semantic-release/github": "^12.0.2",
118
+ "@testing-library/react": "^16.3.2",
119
+ "@testing-library/user-event": "^14.6.1",
116
120
  "@types/cors": "^2.8.19",
117
121
  "@types/express": "^5.0.3",
118
122
  "@types/node": "^20.19.9",
119
123
  "@types/semver": "^7.7.0",
120
124
  "@vitest/ui": "^3.2.4",
121
125
  "conventional-changelog-conventionalcommits": "^9.1.0",
122
- "fast-check": "^3.15.0",
126
+ "fast-check": "^4.6.0",
123
127
  "happy-dom": "^20.0.11",
124
- "jsdom": "^27.0.0",
128
+ "jsdom": "^29.0.2",
125
129
  "lit": "^3.3.1",
126
130
  "node-fetch": "^3.3.2",
131
+ "nodemon": "^3.0.0",
127
132
  "semantic-release": "^25.0.3",
128
133
  "ts-morph": "^27.0.2",
129
134
  "typescript": "^5.9.3",