@exaudeus/workrail 0.6.0 → 0.6.1-beta.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.
@@ -0,0 +1,22 @@
1
+ import { SessionManager } from './SessionManager.js';
2
+ export interface ServerConfig {
3
+ port?: number;
4
+ autoOpen?: boolean;
5
+ }
6
+ export declare class HttpServer {
7
+ private sessionManager;
8
+ private config;
9
+ private app;
10
+ private server;
11
+ private port;
12
+ private baseUrl;
13
+ constructor(sessionManager: SessionManager, config?: ServerConfig);
14
+ private setupMiddleware;
15
+ private setupRoutes;
16
+ start(): Promise<string>;
17
+ private printBanner;
18
+ openDashboard(sessionId?: string): Promise<string>;
19
+ stop(): Promise<void>;
20
+ getBaseUrl(): string;
21
+ getPort(): number;
22
+ }
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HttpServer = void 0;
7
+ const express_1 = __importDefault(require("express"));
8
+ const http_1 = require("http");
9
+ const path_1 = __importDefault(require("path"));
10
+ const cors_1 = __importDefault(require("cors"));
11
+ const open_1 = __importDefault(require("open"));
12
+ class HttpServer {
13
+ constructor(sessionManager, config = {}) {
14
+ this.sessionManager = sessionManager;
15
+ this.config = config;
16
+ this.server = null;
17
+ this.baseUrl = '';
18
+ this.port = config.port || 3456;
19
+ this.app = (0, express_1.default)();
20
+ this.setupMiddleware();
21
+ this.setupRoutes();
22
+ }
23
+ setupMiddleware() {
24
+ this.app.use((0, cors_1.default)({
25
+ origin: '*',
26
+ methods: ['GET', 'HEAD', 'OPTIONS'],
27
+ allowedHeaders: ['Content-Type', 'If-None-Match']
28
+ }));
29
+ this.app.set('etag', 'strong');
30
+ this.app.use(express_1.default.json());
31
+ this.app.use((req, res, next) => {
32
+ const start = Date.now();
33
+ res.on('finish', () => {
34
+ const duration = Date.now() - start;
35
+ console.log(`[HTTP] ${req.method} ${req.path} ${res.statusCode} (${duration}ms)`);
36
+ });
37
+ next();
38
+ });
39
+ }
40
+ setupRoutes() {
41
+ const webDir = path_1.default.join(__dirname, '../../../web');
42
+ this.app.use('/web', express_1.default.static(webDir));
43
+ this.app.get('/', async (req, res) => {
44
+ try {
45
+ const indexPath = path_1.default.join(webDir, 'index.html');
46
+ res.sendFile(indexPath);
47
+ }
48
+ catch (error) {
49
+ res.status(500).json({
50
+ error: 'Dashboard UI not found',
51
+ message: 'The dashboard web files are not yet built. This is expected during development.',
52
+ details: 'Web files will be available in a future step.'
53
+ });
54
+ }
55
+ });
56
+ this.app.get('/api/sessions', async (req, res) => {
57
+ try {
58
+ const sessions = await this.sessionManager.listAllSessions();
59
+ res.json({
60
+ success: true,
61
+ count: sessions.length,
62
+ sessions: sessions.map(s => ({
63
+ id: s.id,
64
+ workflowId: s.workflowId,
65
+ createdAt: s.createdAt,
66
+ updatedAt: s.updatedAt,
67
+ url: `/api/sessions/${s.workflowId}/${s.id}`
68
+ }))
69
+ });
70
+ }
71
+ catch (error) {
72
+ res.status(500).json({
73
+ success: false,
74
+ error: 'Failed to list sessions',
75
+ message: error.message
76
+ });
77
+ }
78
+ });
79
+ this.app.get('/api/sessions/:workflow/:id', async (req, res) => {
80
+ try {
81
+ const { workflow, id } = req.params;
82
+ const session = await this.sessionManager.getSession(workflow, id);
83
+ if (!session) {
84
+ return res.status(404).json({
85
+ success: false,
86
+ error: 'Session not found',
87
+ workflowId: workflow,
88
+ sessionId: id
89
+ });
90
+ }
91
+ res.json({
92
+ success: true,
93
+ session
94
+ });
95
+ }
96
+ catch (error) {
97
+ res.status(500).json({
98
+ success: false,
99
+ error: 'Failed to get session',
100
+ message: error.message
101
+ });
102
+ }
103
+ });
104
+ this.app.get('/api/current-project', async (req, res) => {
105
+ try {
106
+ const project = await this.sessionManager.getCurrentProject();
107
+ res.json({
108
+ success: true,
109
+ project
110
+ });
111
+ }
112
+ catch (error) {
113
+ res.status(500).json({
114
+ success: false,
115
+ error: 'Failed to get project info',
116
+ message: error.message
117
+ });
118
+ }
119
+ });
120
+ this.app.get('/api/projects', async (req, res) => {
121
+ try {
122
+ const projects = await this.sessionManager.listProjects();
123
+ res.json({
124
+ success: true,
125
+ count: projects.length,
126
+ projects
127
+ });
128
+ }
129
+ catch (error) {
130
+ res.status(500).json({
131
+ success: false,
132
+ error: 'Failed to list projects',
133
+ message: error.message
134
+ });
135
+ }
136
+ });
137
+ this.app.get('/api/health', (req, res) => {
138
+ res.json({
139
+ success: true,
140
+ status: 'healthy',
141
+ uptime: process.uptime(),
142
+ timestamp: new Date().toISOString()
143
+ });
144
+ });
145
+ this.app.use((req, res) => {
146
+ res.status(404).json({
147
+ success: false,
148
+ error: 'Not found',
149
+ path: req.path
150
+ });
151
+ });
152
+ }
153
+ async start() {
154
+ while (this.port < 3500) {
155
+ try {
156
+ await new Promise((resolve, reject) => {
157
+ this.server = (0, http_1.createServer)(this.app);
158
+ this.server.on('error', (error) => {
159
+ if (error.code === 'EADDRINUSE') {
160
+ reject(new Error('Port in use'));
161
+ }
162
+ else {
163
+ reject(error);
164
+ }
165
+ });
166
+ this.server.listen(this.port, () => {
167
+ resolve();
168
+ });
169
+ });
170
+ this.baseUrl = `http://localhost:${this.port}`;
171
+ this.printBanner();
172
+ return this.baseUrl;
173
+ }
174
+ catch (error) {
175
+ if (error.message === 'Port in use') {
176
+ this.port++;
177
+ continue;
178
+ }
179
+ throw error;
180
+ }
181
+ }
182
+ throw new Error('No available ports in range 3456-3499');
183
+ }
184
+ printBanner() {
185
+ const line = '═'.repeat(60);
186
+ console.log(`\n${line}`);
187
+ console.log(`🔧 Workrail MCP Server Started`);
188
+ console.log(line);
189
+ console.log(`📊 Dashboard: ${this.baseUrl}`);
190
+ console.log(`💾 Sessions: ${this.sessionManager.getSessionsRoot()}`);
191
+ console.log(`🏗️ Project: ${this.sessionManager.getProjectId()}`);
192
+ console.log(line);
193
+ console.log();
194
+ }
195
+ async openDashboard(sessionId) {
196
+ let url = this.baseUrl;
197
+ if (sessionId) {
198
+ url += `?session=${sessionId}`;
199
+ }
200
+ if (this.config.autoOpen !== false) {
201
+ try {
202
+ await (0, open_1.default)(url);
203
+ console.log(`🌐 Opened dashboard: ${url}`);
204
+ }
205
+ catch (error) {
206
+ console.log(`🌐 Dashboard URL: ${url} (auto-open failed, please open manually)`);
207
+ }
208
+ }
209
+ return url;
210
+ }
211
+ async stop() {
212
+ return new Promise((resolve) => {
213
+ if (this.server) {
214
+ this.server.close(() => {
215
+ console.log('HTTP server stopped');
216
+ resolve();
217
+ });
218
+ }
219
+ else {
220
+ resolve();
221
+ }
222
+ });
223
+ }
224
+ getBaseUrl() {
225
+ return this.baseUrl;
226
+ }
227
+ getPort() {
228
+ return this.port;
229
+ }
230
+ }
231
+ exports.HttpServer = HttpServer;
@@ -0,0 +1,38 @@
1
+ export interface Session {
2
+ id: string;
3
+ workflowId: string;
4
+ projectId: string;
5
+ projectPath: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ data: Record<string, any>;
9
+ }
10
+ export interface ProjectMetadata {
11
+ id: string;
12
+ path: string;
13
+ updatedAt: string;
14
+ sessionCount: number;
15
+ workflows: string[];
16
+ }
17
+ export declare class SessionManager {
18
+ private projectPath;
19
+ private sessionsRoot;
20
+ private projectId;
21
+ constructor(projectPath?: string);
22
+ private hashProjectPath;
23
+ private getProjectRoot;
24
+ getSessionPath(workflowId: string, sessionId: string): string;
25
+ createSession(workflowId: string, sessionId: string, initialData?: Record<string, any>): Promise<Session>;
26
+ updateSession(workflowId: string, sessionId: string, updates: Record<string, any>): Promise<Session>;
27
+ readSession(workflowId: string, sessionId: string, queryPath?: string): Promise<any>;
28
+ getSession(workflowId: string, sessionId: string): Promise<Session | null>;
29
+ listAllSessions(): Promise<Session[]>;
30
+ getCurrentProject(): Promise<ProjectMetadata>;
31
+ listProjects(): Promise<ProjectMetadata[]>;
32
+ getSessionsRoot(): string;
33
+ getProjectId(): string;
34
+ private updateProjectMetadata;
35
+ private deepMerge;
36
+ private getPath;
37
+ private atomicWrite;
38
+ }
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SessionManager = void 0;
7
+ const promises_1 = __importDefault(require("fs/promises"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = require("crypto");
10
+ const os_1 = __importDefault(require("os"));
11
+ class SessionManager {
12
+ constructor(projectPath = process.cwd()) {
13
+ this.projectPath = projectPath;
14
+ this.sessionsRoot = path_1.default.join(os_1.default.homedir(), '.workrail', 'sessions');
15
+ this.projectId = this.hashProjectPath(projectPath);
16
+ }
17
+ hashProjectPath(projectPath) {
18
+ return (0, crypto_1.createHash)('sha256')
19
+ .update(path_1.default.resolve(projectPath))
20
+ .digest('hex')
21
+ .substring(0, 12);
22
+ }
23
+ getProjectRoot() {
24
+ return path_1.default.join(this.sessionsRoot, this.projectId);
25
+ }
26
+ getSessionPath(workflowId, sessionId) {
27
+ return path_1.default.join(this.getProjectRoot(), workflowId, `${sessionId}.json`);
28
+ }
29
+ async createSession(workflowId, sessionId, initialData = {}) {
30
+ const sessionPath = this.getSessionPath(workflowId, sessionId);
31
+ await promises_1.default.mkdir(path_1.default.dirname(sessionPath), { recursive: true });
32
+ const session = {
33
+ id: sessionId,
34
+ workflowId,
35
+ projectId: this.projectId,
36
+ projectPath: this.projectPath,
37
+ createdAt: new Date().toISOString(),
38
+ updatedAt: new Date().toISOString(),
39
+ data: initialData
40
+ };
41
+ await this.atomicWrite(sessionPath, session);
42
+ await this.updateProjectMetadata();
43
+ return session;
44
+ }
45
+ async updateSession(workflowId, sessionId, updates) {
46
+ const sessionPath = this.getSessionPath(workflowId, sessionId);
47
+ const session = await this.getSession(workflowId, sessionId);
48
+ if (!session) {
49
+ throw new Error(`Session not found: ${workflowId}/${sessionId}`);
50
+ }
51
+ session.data = this.deepMerge(session.data, updates);
52
+ session.updatedAt = new Date().toISOString();
53
+ await this.atomicWrite(sessionPath, session);
54
+ return session;
55
+ }
56
+ async readSession(workflowId, sessionId, queryPath) {
57
+ const session = await this.getSession(workflowId, sessionId);
58
+ if (!session) {
59
+ throw new Error(`Session not found: ${workflowId}/${sessionId}`);
60
+ }
61
+ if (!queryPath) {
62
+ return session.data;
63
+ }
64
+ return this.getPath(session.data, queryPath);
65
+ }
66
+ async getSession(workflowId, sessionId) {
67
+ try {
68
+ const sessionPath = this.getSessionPath(workflowId, sessionId);
69
+ const content = await promises_1.default.readFile(sessionPath, 'utf-8');
70
+ return JSON.parse(content);
71
+ }
72
+ catch (error) {
73
+ if (error.code === 'ENOENT')
74
+ return null;
75
+ throw error;
76
+ }
77
+ }
78
+ async listAllSessions() {
79
+ const sessions = [];
80
+ const projectRoot = this.getProjectRoot();
81
+ try {
82
+ const workflows = await promises_1.default.readdir(projectRoot);
83
+ for (const workflow of workflows) {
84
+ if (workflow === 'project.json')
85
+ continue;
86
+ const workflowPath = path_1.default.join(projectRoot, workflow);
87
+ const stat = await promises_1.default.stat(workflowPath);
88
+ if (!stat.isDirectory())
89
+ continue;
90
+ const files = await promises_1.default.readdir(workflowPath);
91
+ for (const file of files) {
92
+ if (!file.endsWith('.json'))
93
+ continue;
94
+ const sessionPath = path_1.default.join(workflowPath, file);
95
+ const content = await promises_1.default.readFile(sessionPath, 'utf-8');
96
+ sessions.push(JSON.parse(content));
97
+ }
98
+ }
99
+ }
100
+ catch (error) {
101
+ if (error.code !== 'ENOENT')
102
+ throw error;
103
+ }
104
+ return sessions.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
105
+ }
106
+ async getCurrentProject() {
107
+ const projectPath = path_1.default.join(this.getProjectRoot(), 'project.json');
108
+ try {
109
+ const content = await promises_1.default.readFile(projectPath, 'utf-8');
110
+ return JSON.parse(content);
111
+ }
112
+ catch (error) {
113
+ if (error.code === 'ENOENT') {
114
+ return {
115
+ id: this.projectId,
116
+ path: this.projectPath,
117
+ updatedAt: new Date().toISOString(),
118
+ sessionCount: 0,
119
+ workflows: []
120
+ };
121
+ }
122
+ throw error;
123
+ }
124
+ }
125
+ async listProjects() {
126
+ const projects = [];
127
+ try {
128
+ const projectDirs = await promises_1.default.readdir(this.sessionsRoot);
129
+ for (const projectId of projectDirs) {
130
+ const projectPath = path_1.default.join(this.sessionsRoot, projectId, 'project.json');
131
+ try {
132
+ const content = await promises_1.default.readFile(projectPath, 'utf-8');
133
+ projects.push(JSON.parse(content));
134
+ }
135
+ catch (error) {
136
+ }
137
+ }
138
+ }
139
+ catch (error) {
140
+ if (error.code !== 'ENOENT')
141
+ throw error;
142
+ }
143
+ return projects;
144
+ }
145
+ getSessionsRoot() {
146
+ return this.sessionsRoot;
147
+ }
148
+ getProjectId() {
149
+ return this.projectId;
150
+ }
151
+ async updateProjectMetadata() {
152
+ const projectPath = path_1.default.join(this.getProjectRoot(), 'project.json');
153
+ const sessions = await this.listAllSessions();
154
+ const metadata = {
155
+ id: this.projectId,
156
+ path: this.projectPath,
157
+ updatedAt: new Date().toISOString(),
158
+ sessionCount: sessions.length,
159
+ workflows: [...new Set(sessions.map(s => s.workflowId))]
160
+ };
161
+ await promises_1.default.mkdir(path_1.default.dirname(projectPath), { recursive: true });
162
+ await promises_1.default.writeFile(projectPath, JSON.stringify(metadata, null, 2));
163
+ }
164
+ deepMerge(target, source) {
165
+ if (!source)
166
+ return target;
167
+ if (!target)
168
+ return source;
169
+ if (Array.isArray(source)) {
170
+ return source;
171
+ }
172
+ if (typeof source !== 'object') {
173
+ return source;
174
+ }
175
+ const output = { ...target };
176
+ for (const key in source) {
177
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
178
+ output[key] = this.deepMerge(target[key] || {}, source[key]);
179
+ }
180
+ else {
181
+ output[key] = source[key];
182
+ }
183
+ }
184
+ return output;
185
+ }
186
+ getPath(obj, queryPath) {
187
+ const parts = queryPath.split('.');
188
+ let current = obj;
189
+ for (const part of parts) {
190
+ if (current === undefined || current === null)
191
+ return undefined;
192
+ const arrayMatch = part.match(/^(\w+)\[(.+)\]$/);
193
+ if (arrayMatch) {
194
+ const [, key, query] = arrayMatch;
195
+ current = current[key];
196
+ if (!current)
197
+ return undefined;
198
+ if (Array.isArray(current) && query.startsWith('?')) {
199
+ const filterMatch = query.match(/\?(\w+)==[']?([^']+)[']?/);
200
+ if (filterMatch) {
201
+ const [, field, value] = filterMatch;
202
+ current = current.filter(item => String(item[field]) === value);
203
+ }
204
+ }
205
+ else if (query.match(/^\d+$/)) {
206
+ current = current[parseInt(query)];
207
+ }
208
+ }
209
+ else {
210
+ current = current[part];
211
+ }
212
+ }
213
+ return current;
214
+ }
215
+ async atomicWrite(filePath, data) {
216
+ const tempPath = `${filePath}.tmp.${Date.now()}`;
217
+ try {
218
+ await promises_1.default.writeFile(tempPath, JSON.stringify(data, null, 2), 'utf-8');
219
+ await promises_1.default.rename(tempPath, filePath);
220
+ }
221
+ catch (error) {
222
+ try {
223
+ await promises_1.default.unlink(tempPath);
224
+ }
225
+ catch { }
226
+ throw error;
227
+ }
228
+ }
229
+ }
230
+ exports.SessionManager = SessionManager;
@@ -0,0 +1,2 @@
1
+ export { SessionManager, Session, ProjectMetadata } from './SessionManager.js';
2
+ export { HttpServer, ServerConfig } from './HttpServer.js';
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HttpServer = exports.SessionManager = void 0;
4
+ var SessionManager_js_1 = require("./SessionManager.js");
5
+ Object.defineProperty(exports, "SessionManager", { enumerable: true, get: function () { return SessionManager_js_1.SessionManager; } });
6
+ var HttpServer_js_1 = require("./HttpServer.js");
7
+ Object.defineProperty(exports, "HttpServer", { enumerable: true, get: function () { return HttpServer_js_1.HttpServer; } });
@@ -35,9 +35,23 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  })();
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
37
  const container_js_1 = require("./container.js");
38
+ const index_js_1 = require("./infrastructure/session/index.js");
39
+ const session_tools_js_1 = require("./tools/session-tools.js");
38
40
  class WorkflowOrchestrationServer {
39
41
  constructor() {
40
42
  this.container = (0, container_js_1.createAppContainer)();
43
+ this.sessionManager = new index_js_1.SessionManager();
44
+ this.httpServer = new index_js_1.HttpServer(this.sessionManager, { autoOpen: false });
45
+ this.sessionTools = (0, session_tools_js_1.createSessionTools)(this.sessionManager, this.httpServer);
46
+ }
47
+ async initialize() {
48
+ await this.httpServer.start();
49
+ }
50
+ getSessionTools() {
51
+ return this.sessionTools;
52
+ }
53
+ async handleSessionTool(name, args) {
54
+ return (0, session_tools_js_1.handleSessionTool)(name, args, this.sessionManager, this.httpServer);
41
55
  }
42
56
  async callWorkflowMethod(method, params) {
43
57
  try {
@@ -341,6 +355,7 @@ async function runServer() {
341
355
  },
342
356
  });
343
357
  const workflowServer = new WorkflowOrchestrationServer();
358
+ await workflowServer.initialize();
344
359
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
345
360
  tools: [
346
361
  WORKFLOW_LIST_TOOL,
@@ -348,7 +363,8 @@ async function runServer() {
348
363
  WORKFLOW_NEXT_TOOL,
349
364
  WORKFLOW_VALIDATE_TOOL,
350
365
  WORKFLOW_VALIDATE_JSON_TOOL,
351
- WORKFLOW_GET_SCHEMA_TOOL
366
+ WORKFLOW_GET_SCHEMA_TOOL,
367
+ ...workflowServer.getSessionTools()
352
368
  ],
353
369
  }));
354
370
  server.setRequestHandler(ListRootsRequestSchema, async () => {
@@ -366,6 +382,9 @@ async function runServer() {
366
382
  });
367
383
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
368
384
  const { name, arguments: args } = request.params;
385
+ if (name.startsWith('workrail_')) {
386
+ return await workflowServer.handleSessionTool(name, args || {});
387
+ }
369
388
  switch (name) {
370
389
  case "workflow_list":
371
390
  return await workflowServer.listWorkflows();
@@ -0,0 +1,5 @@
1
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ import { SessionManager } from '../infrastructure/session/SessionManager.js';
3
+ import { HttpServer } from '../infrastructure/session/HttpServer.js';
4
+ export declare function createSessionTools(sessionManager: SessionManager, httpServer: HttpServer): Tool[];
5
+ export declare function handleSessionTool(name: string, args: any, sessionManager: SessionManager, httpServer: HttpServer): Promise<any>;