@exaudeus/workrail 3.32.0 → 3.33.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.
- package/dist/cli/commands/index.d.ts +1 -0
- package/dist/cli/commands/index.js +3 -1
- package/dist/cli/commands/worktrain-await.js +11 -9
- package/dist/cli/commands/worktrain-daemon-install.d.ts +35 -0
- package/dist/cli/commands/worktrain-daemon-install.js +291 -0
- package/dist/cli/commands/worktrain-daemon.d.ts +31 -0
- package/dist/cli/commands/worktrain-daemon.js +272 -0
- package/dist/cli/commands/worktrain-spawn.js +11 -9
- package/dist/cli-worktrain.js +329 -0
- package/dist/cli.js +1 -22
- package/dist/console/standalone-console.d.ts +28 -0
- package/dist/console/standalone-console.js +142 -0
- package/dist/{console/assets/index-Cb_LO718.js → console-ui/assets/index-BuJFLLfY.js} +1 -1
- package/dist/{console → console-ui}/index.html +1 -1
- package/dist/daemon/agent-loop.d.ts +26 -0
- package/dist/daemon/agent-loop.js +39 -1
- package/dist/daemon/daemon-events.d.ts +47 -1
- package/dist/daemon/workflow-runner.d.ts +3 -2
- package/dist/daemon/workflow-runner.js +205 -41
- package/dist/infrastructure/session/HttpServer.js +133 -34
- package/dist/manifest.json +118 -62
- package/dist/mcp/output-schemas.d.ts +30 -30
- package/dist/mcp/transports/bridge-events.d.ts +4 -0
- package/dist/mcp/transports/fatal-exit.js +4 -0
- package/dist/mcp/transports/http-entry.js +2 -0
- package/dist/mcp/transports/stdio-entry.js +26 -6
- package/dist/mcp/v2/tools.d.ts +4 -4
- package/dist/trigger/adapters/github-poller.d.ts +44 -0
- package/dist/trigger/adapters/github-poller.js +190 -0
- package/dist/trigger/adapters/gitlab-poller.d.ts +27 -0
- package/dist/trigger/adapters/gitlab-poller.js +81 -0
- package/dist/trigger/index.d.ts +4 -1
- package/dist/trigger/index.js +5 -1
- package/dist/trigger/polled-event-store.d.ts +22 -0
- package/dist/trigger/polled-event-store.js +173 -0
- package/dist/trigger/polling-scheduler.d.ts +20 -0
- package/dist/trigger/polling-scheduler.js +249 -0
- package/dist/trigger/trigger-listener.d.ts +3 -0
- package/dist/trigger/trigger-listener.js +47 -3
- package/dist/trigger/trigger-store.js +114 -33
- package/dist/trigger/types.d.ts +17 -1
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +224 -224
- package/dist/v2/durable-core/schemas/session/events.d.ts +42 -42
- package/dist/v2/durable-core/schemas/session/manifest.d.ts +6 -6
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +2 -2
- package/dist/v2/durable-core/tokens/payloads.d.ts +52 -52
- package/dist/v2/usecases/console-routes.js +3 -3
- package/dist/v2/usecases/console-service.js +133 -9
- package/dist/v2/usecases/console-types.d.ts +7 -0
- package/docs/design/daemon-conversation-logging-plan.md +98 -0
- package/docs/design/daemon-conversation-logging-review.md +55 -0
- package/docs/design/daemon-conversation-logging.md +129 -0
- package/docs/design/github-polling-adapter-design-candidates.md +226 -0
- package/docs/design/github-polling-adapter-design-review-findings.md +131 -0
- package/docs/design/github-polling-adapter-implementation-plan.md +284 -0
- package/docs/design/implementation_plan.md +192 -0
- package/docs/design/workflow-id-validation-at-startup.md +146 -0
- package/docs/design/workflow-id-validation-design-review.md +87 -0
- package/docs/design/workflow-id-validation-implementation-plan.md +185 -0
- package/docs/design/worktrain-system-prompt-report-issue-candidates.md +135 -0
- package/docs/design/worktrain-system-prompt-report-issue-design-review.md +73 -0
- package/docs/ideas/backlog.md +361 -0
- package/package.json +1 -1
- package/workflows/architecture-scalability-audit.json +1 -1
- package/workflows/bug-investigation.agentic.v2.json +3 -3
- package/workflows/coding-task-workflow-agentic.json +32 -32
- package/workflows/coding-task-workflow-agentic.lean.v2.json +1 -1
- package/workflows/coding-task-workflow-agentic.v2.json +7 -7
- package/workflows/mr-review-workflow.agentic.v2.json +21 -12
- package/workflows/personal-learning-materials-creation-branched.json +2 -2
- package/workflows/production-readiness-audit.json +1 -1
- package/workflows/relocation-workflow-us.json +2 -2
- package/workflows/ui-ux-design-workflow.json +14 -14
- package/workflows/workflow-for-workflows.json +3 -3
- package/workflows/workflow-for-workflows.v2.json +2 -2
- package/workflows/wr.discovery.json +1 -1
- /package/dist/{console → console-ui}/assets/index-8dh0Psu-.css +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWorktrainDaemonCommand = executeWorktrainDaemonCommand;
|
|
4
|
+
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
|
+
const LAUNCHD_LABEL = 'io.worktrain.daemon';
|
|
6
|
+
const PLIST_FILENAME = `${LAUNCHD_LABEL}.plist`;
|
|
7
|
+
const CAPTURED_ENV_VARS = [
|
|
8
|
+
'AWS_PROFILE',
|
|
9
|
+
'AWS_ACCESS_KEY_ID',
|
|
10
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
11
|
+
'AWS_SESSION_TOKEN',
|
|
12
|
+
'ANTHROPIC_API_KEY',
|
|
13
|
+
'WORKRAIL_TRIGGERS_ENABLED',
|
|
14
|
+
'WORKRAIL_DEFAULT_WORKSPACE',
|
|
15
|
+
'GITHUB_TOKEN',
|
|
16
|
+
'GITLAB_TOKEN',
|
|
17
|
+
'HOME',
|
|
18
|
+
'USER',
|
|
19
|
+
'PATH',
|
|
20
|
+
'WORKRAIL_DEV',
|
|
21
|
+
'WORKRAIL_LOG_LEVEL',
|
|
22
|
+
'WORKRAIL_VERBOSE_LOGGING',
|
|
23
|
+
];
|
|
24
|
+
function buildPlist(nodeBinPath, worktrainBinPath, envVars, logDir, homeDir) {
|
|
25
|
+
const envEntries = Object.entries(envVars)
|
|
26
|
+
.map(([k, v]) => ` <key>${escapeXml(k)}</key>\n <string>${escapeXml(v)}</string>`)
|
|
27
|
+
.join('\n');
|
|
28
|
+
const stdoutLog = `${logDir}/daemon.stdout.log`;
|
|
29
|
+
const stderrLog = `${logDir}/daemon.stderr.log`;
|
|
30
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
31
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
32
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
33
|
+
<plist version="1.0">
|
|
34
|
+
<dict>
|
|
35
|
+
<key>Label</key>
|
|
36
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
37
|
+
|
|
38
|
+
<key>ProgramArguments</key>
|
|
39
|
+
<array>
|
|
40
|
+
<string>${escapeXml(nodeBinPath)}</string>
|
|
41
|
+
<string>${escapeXml(worktrainBinPath)}</string>
|
|
42
|
+
<string>daemon</string>
|
|
43
|
+
</array>
|
|
44
|
+
|
|
45
|
+
<key>WorkingDirectory</key>
|
|
46
|
+
<string>${escapeXml(homeDir)}</string>
|
|
47
|
+
|
|
48
|
+
<key>EnvironmentVariables</key>
|
|
49
|
+
<dict>
|
|
50
|
+
${envEntries}
|
|
51
|
+
</dict>
|
|
52
|
+
|
|
53
|
+
<key>StandardOutPath</key>
|
|
54
|
+
<string>${escapeXml(stdoutLog)}</string>
|
|
55
|
+
|
|
56
|
+
<key>StandardErrorPath</key>
|
|
57
|
+
<string>${escapeXml(stderrLog)}</string>
|
|
58
|
+
|
|
59
|
+
<key>RunAtLoad</key>
|
|
60
|
+
<true/>
|
|
61
|
+
|
|
62
|
+
<key>KeepAlive</key>
|
|
63
|
+
<true/>
|
|
64
|
+
|
|
65
|
+
<!--
|
|
66
|
+
ThrottleInterval: minimum seconds between launchd restarts.
|
|
67
|
+
WHY 30s: prevents launchd from spinning in a tight restart loop if the daemon
|
|
68
|
+
exits immediately (e.g., missing credentials or invalid workspace path).
|
|
69
|
+
Without this, a misconfigured service consumes CPU and spams logs.
|
|
70
|
+
-->
|
|
71
|
+
<key>ThrottleInterval</key>
|
|
72
|
+
<integer>30</integer>
|
|
73
|
+
</dict>
|
|
74
|
+
</plist>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
function escapeXml(s) {
|
|
78
|
+
return s
|
|
79
|
+
.replace(/&/g, '&')
|
|
80
|
+
.replace(/</g, '<')
|
|
81
|
+
.replace(/>/g, '>')
|
|
82
|
+
.replace(/"/g, '"')
|
|
83
|
+
.replace(/'/g, ''');
|
|
84
|
+
}
|
|
85
|
+
function captureEnvVars(env, warn) {
|
|
86
|
+
const captured = {};
|
|
87
|
+
for (const key of CAPTURED_ENV_VARS) {
|
|
88
|
+
const value = env[key];
|
|
89
|
+
if (value !== undefined && value !== '') {
|
|
90
|
+
captured[key] = value;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const existing = captured['WORKRAIL_TRIGGERS_ENABLED'];
|
|
94
|
+
if (!existing) {
|
|
95
|
+
captured['WORKRAIL_TRIGGERS_ENABLED'] = 'true';
|
|
96
|
+
}
|
|
97
|
+
else if (existing !== 'true') {
|
|
98
|
+
warn(`[worktrain daemon --install] WORKRAIL_TRIGGERS_ENABLED is set to '${existing}' in your environment. ` +
|
|
99
|
+
`The plist will override this with 'true' so the daemon can start. ` +
|
|
100
|
+
`Remove WORKRAIL_TRIGGERS_ENABLED from your shell environment if you do not want this warning.`);
|
|
101
|
+
captured['WORKRAIL_TRIGGERS_ENABLED'] = 'true';
|
|
102
|
+
}
|
|
103
|
+
return captured;
|
|
104
|
+
}
|
|
105
|
+
function parseLaunchctlList(stdout, exitCode) {
|
|
106
|
+
if (exitCode !== 0) {
|
|
107
|
+
return { running: false, pid: null, loaded: false };
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(stdout);
|
|
111
|
+
const pid = typeof parsed['PID'] === 'number' ? parsed['PID'] : null;
|
|
112
|
+
return { running: pid !== null, pid, loaded: true };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return { running: false, pid: null, loaded: false };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function runInstall(deps) {
|
|
119
|
+
const home = deps.homedir();
|
|
120
|
+
const plistDir = deps.joinPath(home, 'Library', 'LaunchAgents');
|
|
121
|
+
const plistPath = deps.joinPath(plistDir, PLIST_FILENAME);
|
|
122
|
+
const logDir = deps.joinPath(home, '.workrail', 'logs');
|
|
123
|
+
const env = deps.env;
|
|
124
|
+
const hasBedrock = !!(env['AWS_PROFILE'] || env['AWS_ACCESS_KEY_ID']);
|
|
125
|
+
const hasAnthropic = !!env['ANTHROPIC_API_KEY'];
|
|
126
|
+
if (!hasBedrock && !hasAnthropic) {
|
|
127
|
+
return (0, cli_result_js_1.failure)('No LLM credentials found in the current environment. ' +
|
|
128
|
+
'Set AWS_PROFILE (for Bedrock) or ANTHROPIC_API_KEY (for Anthropic) ' +
|
|
129
|
+
'before running --install so the daemon can authenticate.', {
|
|
130
|
+
suggestions: [
|
|
131
|
+
'export AWS_PROFILE=your-sso-profile',
|
|
132
|
+
'export ANTHROPIC_API_KEY=sk-ant-...',
|
|
133
|
+
],
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
deps.print('Installing WorkTrain daemon as a launchd service...');
|
|
137
|
+
await deps.mkdir(plistDir, { recursive: true });
|
|
138
|
+
await deps.mkdir(logDir, { recursive: true });
|
|
139
|
+
const alreadyInstalled = await deps.exists(plistPath);
|
|
140
|
+
if (alreadyInstalled) {
|
|
141
|
+
deps.print(' Existing service found -- unloading before reinstall...');
|
|
142
|
+
await deps.exec('launchctl', ['unload', plistPath]);
|
|
143
|
+
}
|
|
144
|
+
const capturedEnv = captureEnvVars(env, (msg) => console.warn(msg));
|
|
145
|
+
const plist = buildPlist(deps.nodeBinPath, deps.worktrainBinPath, capturedEnv, logDir, home);
|
|
146
|
+
await deps.writeFile(plistPath, plist);
|
|
147
|
+
await deps.chmod(plistPath, 0o600);
|
|
148
|
+
deps.print(` Plist written: ${plistPath}`);
|
|
149
|
+
const loadResult = await deps.exec('launchctl', ['load', plistPath]);
|
|
150
|
+
if (loadResult.exitCode !== 0) {
|
|
151
|
+
return (0, cli_result_js_1.failure)(`launchctl load failed (exit ${loadResult.exitCode}): ${loadResult.stderr.trim() || loadResult.stdout.trim()}`, {
|
|
152
|
+
suggestions: [
|
|
153
|
+
`Check the plist manually: plutil -lint ${plistPath}`,
|
|
154
|
+
`View daemon logs: tail -f ${logDir}/daemon.stderr.log`,
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
await deps.sleep(1500);
|
|
159
|
+
const listResult = await deps.exec('launchctl', ['list', LAUNCHD_LABEL]);
|
|
160
|
+
const status = parseLaunchctlList(listResult.stdout, listResult.exitCode);
|
|
161
|
+
if (!status.loaded) {
|
|
162
|
+
return (0, cli_result_js_1.failure)(`Service loaded but launchctl cannot find it. This may be a transient issue.`, {
|
|
163
|
+
suggestions: [
|
|
164
|
+
`Check: launchctl list ${LAUNCHD_LABEL}`,
|
|
165
|
+
`View daemon logs: tail -f ${logDir}/daemon.stderr.log`,
|
|
166
|
+
],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
deps.print('');
|
|
170
|
+
if (status.running) {
|
|
171
|
+
deps.print(`WorkTrain daemon installed and running (PID ${status.pid}).`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
deps.print(`WorkTrain daemon installed. Service loaded but not yet running.`);
|
|
175
|
+
deps.print(`This may be normal if WORKRAIL_TRIGGERS_ENABLED was not set.`);
|
|
176
|
+
}
|
|
177
|
+
deps.print(`Logs: ${logDir}/daemon.stdout.log`);
|
|
178
|
+
deps.print(` ${logDir}/daemon.stderr.log`);
|
|
179
|
+
return (0, cli_result_js_1.success)({
|
|
180
|
+
message: status.running
|
|
181
|
+
? `WorkTrain daemon installed and running (PID ${status.pid})`
|
|
182
|
+
: 'WorkTrain daemon installed (service loaded, not yet running)',
|
|
183
|
+
details: [
|
|
184
|
+
`Plist: ${plistPath}`,
|
|
185
|
+
`Logs: ${logDir}/daemon.stdout.log`,
|
|
186
|
+
` ${logDir}/daemon.stderr.log`,
|
|
187
|
+
],
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
async function runUninstall(deps) {
|
|
191
|
+
const home = deps.homedir();
|
|
192
|
+
const plistPath = deps.joinPath(home, 'Library', 'LaunchAgents', PLIST_FILENAME);
|
|
193
|
+
const exists = await deps.exists(plistPath);
|
|
194
|
+
if (!exists) {
|
|
195
|
+
return (0, cli_result_js_1.failure)('WorkTrain daemon is not installed (plist not found).', { suggestions: [`Expected: ${plistPath}`] });
|
|
196
|
+
}
|
|
197
|
+
deps.print('Uninstalling WorkTrain daemon...');
|
|
198
|
+
const unloadResult = await deps.exec('launchctl', ['unload', plistPath]);
|
|
199
|
+
if (unloadResult.exitCode !== 0) {
|
|
200
|
+
deps.print(` Warning: launchctl unload returned non-zero: ${unloadResult.stderr.trim()}`);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
deps.print(' Service unloaded.');
|
|
204
|
+
}
|
|
205
|
+
await deps.removeFile(plistPath);
|
|
206
|
+
deps.print(` Plist removed: ${plistPath}`);
|
|
207
|
+
return (0, cli_result_js_1.success)({ message: 'WorkTrain daemon uninstalled successfully.' });
|
|
208
|
+
}
|
|
209
|
+
async function runStatus(deps) {
|
|
210
|
+
const home = deps.homedir();
|
|
211
|
+
const plistPath = deps.joinPath(home, 'Library', 'LaunchAgents', PLIST_FILENAME);
|
|
212
|
+
const logDir = deps.joinPath(home, '.workrail', 'logs');
|
|
213
|
+
const plistExists = await deps.exists(plistPath);
|
|
214
|
+
const listResult = await deps.exec('launchctl', ['list', LAUNCHD_LABEL]);
|
|
215
|
+
const status = parseLaunchctlList(listResult.stdout, listResult.exitCode);
|
|
216
|
+
deps.print('');
|
|
217
|
+
deps.print('WorkTrain daemon status:');
|
|
218
|
+
deps.print(` Plist installed : ${plistExists ? `yes (${plistPath})` : 'no'}`);
|
|
219
|
+
deps.print(` Service loaded : ${status.loaded ? 'yes' : 'no'}`);
|
|
220
|
+
deps.print(` Running : ${status.running ? `yes (PID ${status.pid})` : 'no'}`);
|
|
221
|
+
if (plistExists || status.loaded) {
|
|
222
|
+
deps.print(` Logs (stdout) : ${logDir}/daemon.stdout.log`);
|
|
223
|
+
deps.print(` Logs (stderr) : ${logDir}/daemon.stderr.log`);
|
|
224
|
+
}
|
|
225
|
+
if (!plistExists && !status.loaded) {
|
|
226
|
+
deps.print('');
|
|
227
|
+
deps.print('Daemon is not installed. Run: worktrain daemon --install');
|
|
228
|
+
}
|
|
229
|
+
else if (plistExists && !status.running) {
|
|
230
|
+
deps.print('');
|
|
231
|
+
deps.print(`Daemon installed but not running. Check logs: tail -f ${logDir}/daemon.stderr.log`);
|
|
232
|
+
}
|
|
233
|
+
deps.print('');
|
|
234
|
+
return (0, cli_result_js_1.success)({
|
|
235
|
+
message: status.running
|
|
236
|
+
? `WorkTrain daemon is running (PID ${status.pid})`
|
|
237
|
+
: plistExists
|
|
238
|
+
? 'WorkTrain daemon is installed but not running'
|
|
239
|
+
: 'WorkTrain daemon is not installed',
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
async function executeWorktrainDaemonCommand(deps, opts) {
|
|
243
|
+
const flagCount = [opts.install, opts.uninstall, opts.status].filter(Boolean).length;
|
|
244
|
+
if (flagCount === 0) {
|
|
245
|
+
if (deps.startDaemon) {
|
|
246
|
+
await deps.startDaemon();
|
|
247
|
+
return (0, cli_result_js_1.success)({ message: 'WorkTrain daemon stopped.' });
|
|
248
|
+
}
|
|
249
|
+
return (0, cli_result_js_1.misuse)('Specify one of: --install, --uninstall, or --status', [
|
|
250
|
+
'worktrain daemon --install Install and start as a launchd service',
|
|
251
|
+
'worktrain daemon --uninstall Stop and remove the launchd service',
|
|
252
|
+
'worktrain daemon --status Show service status',
|
|
253
|
+
]);
|
|
254
|
+
}
|
|
255
|
+
if (flagCount > 1) {
|
|
256
|
+
return (0, cli_result_js_1.misuse)('--install, --uninstall, and --status are mutually exclusive. Specify only one.');
|
|
257
|
+
}
|
|
258
|
+
if (deps.platform !== 'darwin') {
|
|
259
|
+
return (0, cli_result_js_1.failure)(`worktrain daemon --install requires macOS (launchd). ` +
|
|
260
|
+
`Current platform: ${deps.platform}.`, {
|
|
261
|
+
suggestions: [
|
|
262
|
+
'On Linux, use systemd: create a user service with systemctl --user.',
|
|
263
|
+
'See docs/daemon-service.md for platform-specific instructions.',
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (opts.install)
|
|
268
|
+
return runInstall(deps);
|
|
269
|
+
if (opts.uninstall)
|
|
270
|
+
return runUninstall(deps);
|
|
271
|
+
return runStatus(deps);
|
|
272
|
+
}
|
|
@@ -3,20 +3,22 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.executeWorktrainSpawnCommand = executeWorktrainSpawnCommand;
|
|
4
4
|
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
5
|
const DEFAULT_CONSOLE_PORT = 3456;
|
|
6
|
-
const
|
|
6
|
+
const LOCK_FILE_NAMES = ['daemon-console.lock', 'dashboard.lock'];
|
|
7
7
|
async function discoverConsolePort(deps, portOverride) {
|
|
8
8
|
if (portOverride !== undefined && portOverride > 0) {
|
|
9
9
|
return portOverride;
|
|
10
10
|
}
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
for (const lockFileName of LOCK_FILE_NAMES) {
|
|
12
|
+
const lockPath = deps.joinPath(deps.homedir(), '.workrail', lockFileName);
|
|
13
|
+
try {
|
|
14
|
+
const raw = await deps.readFile(lockPath);
|
|
15
|
+
const parsed = JSON.parse(raw);
|
|
16
|
+
if (typeof parsed.port === 'number' && parsed.port > 0) {
|
|
17
|
+
return parsed.port;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
17
21
|
}
|
|
18
|
-
}
|
|
19
|
-
catch {
|
|
20
22
|
}
|
|
21
23
|
return DEFAULT_CONSOLE_PORT;
|
|
22
24
|
}
|
package/dist/cli-worktrain.js
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
3
36
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
4
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
38
|
};
|
|
@@ -166,4 +199,300 @@ program
|
|
|
166
199
|
});
|
|
167
200
|
(0, interpret_result_js_1.interpretCliResultWithoutDI)(result);
|
|
168
201
|
});
|
|
202
|
+
program
|
|
203
|
+
.command('console')
|
|
204
|
+
.description('Start the WorkRail console UI (reads session files directly, no daemon required)')
|
|
205
|
+
.option('-p, --port <n>', 'Port to bind the console server (default: 3456)', parseInt)
|
|
206
|
+
.option('-w, --workspace <path>', 'Workspace path (reserved for future scoped view)')
|
|
207
|
+
.action(async (options) => {
|
|
208
|
+
const { startStandaloneConsole } = await Promise.resolve().then(() => __importStar(require('./console/standalone-console.js')));
|
|
209
|
+
const result = await startStandaloneConsole({
|
|
210
|
+
port: options.port,
|
|
211
|
+
});
|
|
212
|
+
if (result.kind === 'port_conflict') {
|
|
213
|
+
process.stderr.write(`[Console] Port ${result.port} is already in use. ` +
|
|
214
|
+
`Use --port to choose a different port, or stop the process holding port ${result.port}.\n`);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
if (result.kind === 'io_error') {
|
|
218
|
+
process.stderr.write(`[Console] Failed to start: ${result.message}\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
const line = '='.repeat(60);
|
|
222
|
+
process.stdout.write(`\n${line}\n`);
|
|
223
|
+
process.stdout.write(`WorkRail Console\n`);
|
|
224
|
+
process.stdout.write(`${line}\n`);
|
|
225
|
+
process.stdout.write(`Console: http://localhost:${result.port}/console\n`);
|
|
226
|
+
process.stdout.write(`Sessions: ${path_1.default.join(os_1.default.homedir(), '.workrail', 'data', 'sessions')}\n`);
|
|
227
|
+
process.stdout.write(`${line}\n\n`);
|
|
228
|
+
process.stdout.write(`Press Ctrl+C to stop.\n`);
|
|
229
|
+
const shutdown = async () => {
|
|
230
|
+
process.stdout.write('\n[Console] Shutting down...\n');
|
|
231
|
+
await result.stop();
|
|
232
|
+
process.exit(0);
|
|
233
|
+
};
|
|
234
|
+
process.on('SIGINT', () => { void shutdown(); });
|
|
235
|
+
process.on('SIGTERM', () => { void shutdown(); });
|
|
236
|
+
});
|
|
237
|
+
program
|
|
238
|
+
.command('daemon')
|
|
239
|
+
.description('Start the WorkTrain daemon, or manage it as a macOS launchd service')
|
|
240
|
+
.option('--install', 'Create the launchd plist and start the daemon service')
|
|
241
|
+
.option('--uninstall', 'Stop the daemon service and remove the launchd plist')
|
|
242
|
+
.option('--status', 'Show the current status of the daemon service')
|
|
243
|
+
.action(async (options) => {
|
|
244
|
+
const { execFile: execFileRaw } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
245
|
+
const execFilePromise = (0, util_1.promisify)(execFileRaw);
|
|
246
|
+
const result = await (0, index_js_1.executeWorktrainDaemonCommand)({
|
|
247
|
+
env: process_1.env,
|
|
248
|
+
platform: process.platform,
|
|
249
|
+
worktrainBinPath: process.argv[1],
|
|
250
|
+
nodeBinPath: process.execPath,
|
|
251
|
+
homedir: os_1.default.homedir,
|
|
252
|
+
joinPath: path_1.default.join,
|
|
253
|
+
mkdir: (p, opts) => fs_1.default.promises.mkdir(p, opts),
|
|
254
|
+
writeFile: (p, content) => fs_1.default.promises.writeFile(p, content, 'utf-8'),
|
|
255
|
+
chmod: (p, mode) => fs_1.default.promises.chmod(p, mode),
|
|
256
|
+
readFile: (p) => fs_1.default.promises.readFile(p, 'utf-8'),
|
|
257
|
+
removeFile: (p) => fs_1.default.promises.unlink(p),
|
|
258
|
+
exists: async (p) => {
|
|
259
|
+
try {
|
|
260
|
+
await fs_1.default.promises.access(p);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
exec: async (command, args) => {
|
|
268
|
+
try {
|
|
269
|
+
const { stdout, stderr } = await execFilePromise(command, args, { encoding: 'utf-8' });
|
|
270
|
+
return { stdout: stdout ?? '', stderr: stderr ?? '', exitCode: 0 };
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
const e = err;
|
|
274
|
+
return {
|
|
275
|
+
stdout: e.stdout ?? '',
|
|
276
|
+
stderr: e.stderr ?? '',
|
|
277
|
+
exitCode: typeof e.code === 'number' ? e.code : 1,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
},
|
|
281
|
+
print: (line) => console.log(line),
|
|
282
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
283
|
+
startDaemon: async () => {
|
|
284
|
+
const { startTriggerListener } = await Promise.resolve().then(() => __importStar(require('./trigger/trigger-listener.js')));
|
|
285
|
+
const { startDaemonConsole } = await Promise.resolve().then(() => __importStar(require('./trigger/daemon-console.js')));
|
|
286
|
+
const { DaemonEventEmitter } = await Promise.resolve().then(() => __importStar(require('./daemon/daemon-events.js')));
|
|
287
|
+
const { initializeContainer, container } = await Promise.resolve().then(() => __importStar(require('./di/container.js')));
|
|
288
|
+
const { DI } = await Promise.resolve().then(() => __importStar(require('./di/tokens.js')));
|
|
289
|
+
await initializeContainer({ runtimeMode: { kind: 'cli' } });
|
|
290
|
+
const { createToolContext } = await Promise.resolve().then(() => __importStar(require('./mcp/server.js')));
|
|
291
|
+
const { requireV2Context } = await Promise.resolve().then(() => __importStar(require('./mcp/types.js')));
|
|
292
|
+
const rawCtx = await createToolContext();
|
|
293
|
+
const v2Guard = requireV2Context(rawCtx);
|
|
294
|
+
if (!v2Guard.ok) {
|
|
295
|
+
console.error('v2 engine not available -- ensure WorkRail is fully initialized');
|
|
296
|
+
process.exit(1);
|
|
297
|
+
}
|
|
298
|
+
const ctx = v2Guard.ctx;
|
|
299
|
+
const { loadWorkrailConfigFile } = await Promise.resolve().then(() => __importStar(require('./config/config-file.js')));
|
|
300
|
+
const configResult = loadWorkrailConfigFile();
|
|
301
|
+
const configWorkspace = configResult.kind === 'ok' ? configResult.value['WORKRAIL_DEFAULT_WORKSPACE'] : undefined;
|
|
302
|
+
const workspacePath = configWorkspace?.trim() || process.cwd();
|
|
303
|
+
const usesBedrock = !!process.env['AWS_PROFILE'] || !!process.env['AWS_ACCESS_KEY_ID'];
|
|
304
|
+
const apiKey = process.env['ANTHROPIC_API_KEY'];
|
|
305
|
+
if (!usesBedrock && !apiKey) {
|
|
306
|
+
console.error('No LLM credentials found. Set AWS_PROFILE (Bedrock) or ANTHROPIC_API_KEY.');
|
|
307
|
+
process.exit(1);
|
|
308
|
+
}
|
|
309
|
+
const emitter = new DaemonEventEmitter();
|
|
310
|
+
const handle = await startTriggerListener(ctx, {
|
|
311
|
+
workspacePath,
|
|
312
|
+
apiKey: apiKey,
|
|
313
|
+
env: process.env,
|
|
314
|
+
emitter,
|
|
315
|
+
});
|
|
316
|
+
if (handle === null) {
|
|
317
|
+
console.error('Daemon is disabled. Set WORKRAIL_TRIGGERS_ENABLED=true to enable.');
|
|
318
|
+
process.exit(1);
|
|
319
|
+
}
|
|
320
|
+
if ('_kind' in handle) {
|
|
321
|
+
console.error('Failed to start daemon:', handle.error);
|
|
322
|
+
process.exit(1);
|
|
323
|
+
}
|
|
324
|
+
console.log(`WorkRail daemon running on port ${handle.port}`);
|
|
325
|
+
console.log(`Workspace: ${workspacePath}`);
|
|
326
|
+
console.log('Waiting for webhook triggers...');
|
|
327
|
+
const pkg = require('../package.json');
|
|
328
|
+
const workflowService = container.resolve(DI.Services.Workflow);
|
|
329
|
+
const consoleResult = await startDaemonConsole(ctx, {
|
|
330
|
+
triggerRouter: handle.router,
|
|
331
|
+
serverVersion: pkg.version,
|
|
332
|
+
workflowService,
|
|
333
|
+
});
|
|
334
|
+
let consoleHandle = null;
|
|
335
|
+
if (consoleResult.kind === 'ok') {
|
|
336
|
+
consoleHandle = consoleResult.value;
|
|
337
|
+
}
|
|
338
|
+
else if (consoleResult.error.kind === 'port_conflict') {
|
|
339
|
+
console.warn(`[DaemonConsole] Port ${consoleResult.error.port} is already held. ` +
|
|
340
|
+
`The daemon is running but the console is unavailable.`);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
console.warn(`[DaemonConsole] Could not start console: ${consoleResult.error.message}`);
|
|
344
|
+
}
|
|
345
|
+
await new Promise((resolve) => {
|
|
346
|
+
const shutdown = async () => {
|
|
347
|
+
console.log('\nShutting down daemon...');
|
|
348
|
+
if (consoleHandle) {
|
|
349
|
+
await consoleHandle.stop();
|
|
350
|
+
}
|
|
351
|
+
await handle.stop();
|
|
352
|
+
resolve();
|
|
353
|
+
};
|
|
354
|
+
process.once('SIGINT', () => void shutdown());
|
|
355
|
+
process.once('SIGTERM', () => void shutdown());
|
|
356
|
+
});
|
|
357
|
+
},
|
|
358
|
+
}, {
|
|
359
|
+
install: options.install,
|
|
360
|
+
uninstall: options.uninstall,
|
|
361
|
+
status: options.status,
|
|
362
|
+
});
|
|
363
|
+
(0, interpret_result_js_1.interpretCliResultWithoutDI)(result);
|
|
364
|
+
});
|
|
365
|
+
function formatDaemonEventLine(raw) {
|
|
366
|
+
let obj;
|
|
367
|
+
try {
|
|
368
|
+
obj = JSON.parse(raw);
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
const ts = typeof obj['ts'] === 'number'
|
|
374
|
+
? new Date(obj['ts']).toISOString().replace('T', ' ').slice(0, 23)
|
|
375
|
+
: '?';
|
|
376
|
+
const kind = typeof obj['kind'] === 'string' ? obj['kind'] : 'unknown';
|
|
377
|
+
const sessionId = typeof obj['sessionId'] === 'string' ? obj['sessionId'].slice(0, 8) : null;
|
|
378
|
+
const prefix = sessionId ? `[${ts}] [${sessionId}] ${kind}` : `[${ts}] ${kind}`;
|
|
379
|
+
switch (kind) {
|
|
380
|
+
case 'llm_turn_started':
|
|
381
|
+
return `${prefix} msgs=${obj['messageCount'] ?? '?'}`;
|
|
382
|
+
case 'llm_turn_completed':
|
|
383
|
+
return `${prefix} stop=${obj['stopReason'] ?? '?'} in=${obj['inputTokens'] ?? '?'} out=${obj['outputTokens'] ?? '?'} tools=[${Array.isArray(obj['toolNamesRequested']) ? obj['toolNamesRequested'].join(',') : ''}]`;
|
|
384
|
+
case 'tool_call_started':
|
|
385
|
+
return `${prefix} tool=${obj['toolName'] ?? '?'} args=${String(obj['argsSummary'] ?? '').slice(0, 80)}`;
|
|
386
|
+
case 'tool_call_completed':
|
|
387
|
+
return `${prefix} tool=${obj['toolName'] ?? '?'} ${obj['durationMs'] ?? '?'}ms result=${String(obj['resultSummary'] ?? '').slice(0, 60)}`;
|
|
388
|
+
case 'tool_call_failed':
|
|
389
|
+
return `${prefix} tool=${obj['toolName'] ?? '?'} ${obj['durationMs'] ?? '?'}ms err=${String(obj['errorMessage'] ?? '').slice(0, 80)}`;
|
|
390
|
+
case 'tool_called':
|
|
391
|
+
return `${prefix} tool=${obj['toolName'] ?? '?'} ${obj['summary'] ? String(obj['summary']).slice(0, 80) : ''}`;
|
|
392
|
+
case 'tool_error':
|
|
393
|
+
return `${prefix} tool=${obj['toolName'] ?? '?'} err=${String(obj['error'] ?? '').slice(0, 80)}`;
|
|
394
|
+
case 'session_started':
|
|
395
|
+
return `${prefix} workflow=${obj['workflowId'] ?? '?'} workspace=${obj['workspacePath'] ?? '?'}`;
|
|
396
|
+
case 'session_completed':
|
|
397
|
+
return `${prefix} workflow=${obj['workflowId'] ?? '?'} outcome=${obj['outcome'] ?? '?'}${obj['detail'] ? ` (${obj['detail']})` : ''}`;
|
|
398
|
+
case 'step_advanced':
|
|
399
|
+
return `${prefix}`;
|
|
400
|
+
case 'issue_reported':
|
|
401
|
+
return `${prefix} severity=${obj['severity'] ?? '?'} ${String(obj['summary'] ?? '').slice(0, 80)}`;
|
|
402
|
+
default:
|
|
403
|
+
return `${prefix} ${JSON.stringify(obj).slice(0, 120)}`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
program
|
|
407
|
+
.command('logs')
|
|
408
|
+
.description('Read and display the WorkRail daemon event log. Use --follow to stream new events in real time.')
|
|
409
|
+
.option('--follow', 'Continuously poll the log file for new events (like tail -f)')
|
|
410
|
+
.option('--session <id>', 'Filter events by sessionId prefix (first 8 chars or full UUID)')
|
|
411
|
+
.action(async (options) => {
|
|
412
|
+
const eventsDir = path_1.default.join(os_1.default.homedir(), '.workrail', 'events', 'daemon');
|
|
413
|
+
function todayFilePath() {
|
|
414
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
415
|
+
return path_1.default.join(eventsDir, `${date}.jsonl`);
|
|
416
|
+
}
|
|
417
|
+
function readNewLines(filePath, fromOffset) {
|
|
418
|
+
let stat;
|
|
419
|
+
try {
|
|
420
|
+
stat = fs_1.default.statSync(filePath);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
if (stat.size <= fromOffset) {
|
|
426
|
+
return { lines: [], newOffset: fromOffset };
|
|
427
|
+
}
|
|
428
|
+
const fd = fs_1.default.openSync(filePath, 'r');
|
|
429
|
+
try {
|
|
430
|
+
const len = stat.size - fromOffset;
|
|
431
|
+
const buf = Buffer.alloc(len);
|
|
432
|
+
fs_1.default.readSync(fd, buf, 0, len, fromOffset);
|
|
433
|
+
const text = buf.toString('utf8');
|
|
434
|
+
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
|
435
|
+
return { lines, newOffset: stat.size };
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
fs_1.default.closeSync(fd);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function printLines(lines) {
|
|
442
|
+
for (const line of lines) {
|
|
443
|
+
if (options.session) {
|
|
444
|
+
try {
|
|
445
|
+
const obj = JSON.parse(line);
|
|
446
|
+
const sid = typeof obj['sessionId'] === 'string' ? obj['sessionId'] : '';
|
|
447
|
+
if (!sid.startsWith(options.session) && sid !== options.session) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const formatted = formatDaemonEventLine(line);
|
|
456
|
+
if (formatted !== null) {
|
|
457
|
+
process.stdout.write(formatted + '\n');
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const filePath = todayFilePath();
|
|
462
|
+
if (!options.follow) {
|
|
463
|
+
const result = readNewLines(filePath, 0);
|
|
464
|
+
if (result === null) {
|
|
465
|
+
process.stdout.write(`No events yet. Is the daemon running? (Expected: ${filePath})\n`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
printLines(result.lines);
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
let currentFilePath = filePath;
|
|
472
|
+
let offset = 0;
|
|
473
|
+
const initial = readNewLines(currentFilePath, 0);
|
|
474
|
+
if (initial !== null) {
|
|
475
|
+
printLines(initial.lines);
|
|
476
|
+
offset = initial.newOffset;
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
process.stdout.write(`Waiting for events... (${currentFilePath})\n`);
|
|
480
|
+
}
|
|
481
|
+
while (true) {
|
|
482
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
483
|
+
const newFilePath = todayFilePath();
|
|
484
|
+
if (newFilePath !== currentFilePath) {
|
|
485
|
+
currentFilePath = newFilePath;
|
|
486
|
+
offset = 0;
|
|
487
|
+
}
|
|
488
|
+
const result = readNewLines(currentFilePath, offset);
|
|
489
|
+
if (result !== null && result.lines.length > 0) {
|
|
490
|
+
printLines(result.lines);
|
|
491
|
+
offset = result.newOffset;
|
|
492
|
+
}
|
|
493
|
+
else if (result !== null) {
|
|
494
|
+
offset = result.newOffset;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
169
498
|
program.parse();
|
package/dist/cli.js
CHANGED
|
@@ -190,7 +190,6 @@ program
|
|
|
190
190
|
.option('-w, --workspace <path>', 'Path to workspace containing triggers.yml')
|
|
191
191
|
.action(async (options) => {
|
|
192
192
|
const { startTriggerListener } = await Promise.resolve().then(() => __importStar(require('./trigger/trigger-listener.js')));
|
|
193
|
-
const { startDaemonConsole } = await Promise.resolve().then(() => __importStar(require('./trigger/daemon-console.js')));
|
|
194
193
|
const { DaemonEventEmitter } = await Promise.resolve().then(() => __importStar(require('./daemon/daemon-events.js')));
|
|
195
194
|
await (0, container_js_1.initializeContainer)({ runtimeMode: { kind: 'cli' } });
|
|
196
195
|
const { createToolContext } = await Promise.resolve().then(() => __importStar(require('./mcp/server.js')));
|
|
@@ -235,29 +234,9 @@ program
|
|
|
235
234
|
console.log(`WorkRail daemon running on port ${handle.port}`);
|
|
236
235
|
console.log(`Workspace: ${workspacePath}`);
|
|
237
236
|
console.log('Waiting for webhook triggers...');
|
|
238
|
-
|
|
239
|
-
const consoleResult = await startDaemonConsole(ctx, {
|
|
240
|
-
triggerRouter: handle.router,
|
|
241
|
-
serverVersion: pkg.version,
|
|
242
|
-
workflowService: rawCtx.workflowService,
|
|
243
|
-
});
|
|
244
|
-
let consoleHandle = null;
|
|
245
|
-
if (consoleResult.kind === 'ok') {
|
|
246
|
-
consoleHandle = consoleResult.value;
|
|
247
|
-
}
|
|
248
|
-
else if (consoleResult.error.kind === 'port_conflict') {
|
|
249
|
-
console.warn(`[DaemonConsole] Port ${consoleResult.error.port} is already held (likely by an MCP server). ` +
|
|
250
|
-
`The daemon is running but the console is unavailable. ` +
|
|
251
|
-
`Restart the MCP server while the daemon is running to enable the daemon console.`);
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
console.warn(`[DaemonConsole] Could not start console: ${consoleResult.error.message}`);
|
|
255
|
-
}
|
|
237
|
+
console.log('Run "worktrain console" in a separate terminal to start the console UI.');
|
|
256
238
|
const shutdown = async () => {
|
|
257
239
|
console.log('\nShutting down daemon...');
|
|
258
|
-
if (consoleHandle) {
|
|
259
|
-
await consoleHandle.stop();
|
|
260
|
-
}
|
|
261
240
|
await handle.stop();
|
|
262
241
|
process.exit(0);
|
|
263
242
|
};
|