@exaudeus/workrail 3.24.4 → 3.26.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 +6 -0
- package/dist/cli/commands/index.js +14 -1
- package/dist/cli/commands/version.d.ts +6 -0
- package/dist/cli/commands/version.js +14 -0
- package/dist/cli/commands/worktrain-await.d.ts +35 -0
- package/dist/cli/commands/worktrain-await.js +207 -0
- package/dist/cli/commands/worktrain-inbox.d.ts +23 -0
- package/dist/cli/commands/worktrain-inbox.js +82 -0
- package/dist/cli/commands/worktrain-init.d.ts +23 -0
- package/dist/cli/commands/worktrain-init.js +338 -0
- package/dist/cli/commands/worktrain-spawn.d.ts +28 -0
- package/dist/cli/commands/worktrain-spawn.js +106 -0
- package/dist/cli/commands/worktrain-tell.d.ts +25 -0
- package/dist/cli/commands/worktrain-tell.js +32 -0
- package/dist/cli-worktrain.d.ts +2 -0
- package/dist/cli-worktrain.js +169 -0
- package/dist/cli.js +100 -0
- package/dist/config/config-file.d.ts +2 -0
- package/dist/config/config-file.js +55 -0
- package/dist/console/assets/index-8dh0Psu-.css +1 -0
- package/dist/console/assets/{index-TMfptYpQ.js → index-HhtarvD5.js} +10 -10
- package/dist/console/index.html +2 -2
- package/dist/daemon/agent-loop.d.ts +90 -0
- package/dist/daemon/agent-loop.js +214 -0
- package/dist/daemon/pi-mono-loader.d.ts +0 -0
- package/dist/daemon/pi-mono-loader.js +1 -0
- package/dist/daemon/soul-template.d.ts +2 -0
- package/dist/daemon/soul-template.js +22 -0
- package/dist/daemon/workflow-runner.d.ts +63 -0
- package/dist/daemon/workflow-runner.js +689 -0
- package/dist/infrastructure/session/HttpServer.js +2 -2
- package/dist/manifest.json +226 -50
- package/dist/mcp/handlers/v2-execution/start.d.ts +2 -1
- package/dist/mcp/handlers/v2-execution/start.js +4 -3
- package/dist/mcp/output-schemas.d.ts +154 -154
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/transports/bridge-entry.js +20 -2
- package/dist/mcp/transports/bridge-events.d.ts +34 -0
- package/dist/mcp/transports/bridge-events.js +24 -0
- package/dist/mcp/transports/fatal-exit.d.ts +5 -0
- package/dist/mcp/transports/fatal-exit.js +82 -0
- package/dist/mcp/transports/http-entry.js +3 -0
- package/dist/mcp/transports/stdio-entry.js +3 -7
- package/dist/mcp/v2/tools.d.ts +7 -7
- package/dist/trigger/delivery-action.d.ts +37 -0
- package/dist/trigger/delivery-action.js +204 -0
- package/dist/trigger/delivery-client.d.ts +11 -0
- package/dist/trigger/delivery-client.js +27 -0
- package/dist/trigger/index.d.ts +5 -0
- package/dist/trigger/index.js +8 -0
- package/dist/trigger/trigger-listener.d.ts +32 -0
- package/dist/trigger/trigger-listener.js +176 -0
- package/dist/trigger/trigger-router.d.ts +38 -0
- package/dist/trigger/trigger-router.js +343 -0
- package/dist/trigger/trigger-store.d.ts +39 -0
- package/dist/trigger/trigger-store.js +698 -0
- package/dist/trigger/types.d.ts +70 -0
- package/dist/trigger/types.js +10 -0
- package/dist/v2/durable-core/schemas/execution-snapshot/blocked-snapshot.d.ts +22 -22
- package/dist/v2/durable-core/schemas/execution-snapshot/execution-snapshot.v1.d.ts +114 -114
- package/dist/v2/durable-core/schemas/export-bundle/index.d.ts +454 -454
- package/dist/v2/durable-core/schemas/session/blockers.d.ts +14 -14
- package/dist/v2/durable-core/schemas/session/events.d.ts +93 -93
- package/dist/v2/durable-core/schemas/session/gaps.d.ts +2 -2
- package/dist/v2/durable-core/schemas/session/validation-event.d.ts +4 -4
- package/dist/v2/infra/in-memory/daemon-registry/index.d.ts +14 -0
- package/dist/v2/infra/in-memory/daemon-registry/index.js +32 -0
- package/dist/v2/infra/in-memory/keyed-async-queue/index.d.ts +5 -0
- package/dist/v2/infra/in-memory/keyed-async-queue/index.js +32 -0
- package/dist/v2/usecases/console-routes.d.ts +3 -1
- package/dist/v2/usecases/console-routes.js +132 -1
- package/dist/v2/usecases/console-service.d.ts +2 -0
- package/dist/v2/usecases/console-service.js +18 -2
- package/dist/v2/usecases/console-types.d.ts +2 -0
- package/package.json +6 -2
- package/spec/workflow-tags.json +1 -0
- package/workflows/classify-task-workflow.json +68 -0
- package/workflows/coding-task-workflow-agentic.lean.v2.json +43 -13
- package/workflows/workflow-for-workflows.json +4 -2
- package/workflows/workflow-for-workflows.v2.json +4 -2
- package/dist/console/assets/index-BXRk3te_.css +0 -1
- package/workflows/rich-object-contribution.json +0 -258
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWorktrainInitCommand = executeWorktrainInitCommand;
|
|
4
|
+
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
|
+
const soul_template_js_1 = require("../../daemon/soul-template.js");
|
|
6
|
+
function buildTriggersYml(workspacePath) {
|
|
7
|
+
return `# WorkRail trigger configuration
|
|
8
|
+
# Generated by worktrain init
|
|
9
|
+
#
|
|
10
|
+
# This file is read by the WorkRail daemon (workrail daemon --workspace <path>).
|
|
11
|
+
# Add or edit triggers to control which workflows run and when.
|
|
12
|
+
#
|
|
13
|
+
# Polling triggers (github, gitlab) are coming in a future release.
|
|
14
|
+
# See: https://github.com/exaudeus/workrail/blob/main/docs/triggers.md
|
|
15
|
+
|
|
16
|
+
triggers:
|
|
17
|
+
- id: my-first-trigger
|
|
18
|
+
provider: generic
|
|
19
|
+
workflowId: coding-task-workflow-agentic
|
|
20
|
+
workspacePath: ${workspacePath}
|
|
21
|
+
goal: "Handle incoming coding task"
|
|
22
|
+
# hmacSecret: $WEBHOOK_SECRET # uncomment and set env var to enable HMAC validation
|
|
23
|
+
#
|
|
24
|
+
# To fire this trigger manually:
|
|
25
|
+
# curl -X POST http://localhost:3200/webhook/my-first-trigger \\
|
|
26
|
+
# -H "Content-Type: application/json" \\
|
|
27
|
+
# -d '{"task": "describe the task here"}'
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
async function runCredentialsSection(deps, opts) {
|
|
31
|
+
const hasBedrock = !!deps.env['AWS_PROFILE'] || !!deps.env['AWS_ACCESS_KEY_ID'];
|
|
32
|
+
const hasAnthropic = !!deps.env['ANTHROPIC_API_KEY'];
|
|
33
|
+
if (hasBedrock) {
|
|
34
|
+
return {
|
|
35
|
+
kind: 'skipped',
|
|
36
|
+
reason: `Bedrock credentials detected (AWS_PROFILE=${deps.env['AWS_PROFILE'] ?? 'set'})`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
if (hasAnthropic) {
|
|
40
|
+
return {
|
|
41
|
+
kind: 'skipped',
|
|
42
|
+
reason: 'Anthropic API key detected (ANTHROPIC_API_KEY is set)',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
deps.print('');
|
|
46
|
+
deps.print('LLM credentials not found.');
|
|
47
|
+
let choice;
|
|
48
|
+
if (opts.yes) {
|
|
49
|
+
choice = 'anthropic';
|
|
50
|
+
deps.print(' Using default: Anthropic API key (--yes mode)');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
deps.print(' Which LLM provider do you want to use?');
|
|
54
|
+
deps.print(' 1) AWS Bedrock (requires AWS SSO profile)');
|
|
55
|
+
deps.print(' 2) Anthropic API key (direct access)');
|
|
56
|
+
choice = await deps.prompt(' Enter 1 or 2 [2]: ', '2');
|
|
57
|
+
}
|
|
58
|
+
if (choice === '1') {
|
|
59
|
+
deps.print('');
|
|
60
|
+
deps.print(' To use AWS Bedrock, set your AWS SSO profile:');
|
|
61
|
+
deps.print(' export AWS_PROFILE=your-sso-profile-name');
|
|
62
|
+
deps.print(' Then run: aws sso login --profile your-sso-profile-name');
|
|
63
|
+
return {
|
|
64
|
+
kind: 'configured',
|
|
65
|
+
summary: 'Printed Bedrock setup instructions (set AWS_PROFILE in your shell profile)',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
deps.print('');
|
|
70
|
+
deps.print(' To use the Anthropic API, set your API key:');
|
|
71
|
+
deps.print(' export ANTHROPIC_API_KEY=sk-ant-...');
|
|
72
|
+
deps.print(' Add this line to ~/.zshrc or ~/.bashrc to persist across sessions.');
|
|
73
|
+
return {
|
|
74
|
+
kind: 'configured',
|
|
75
|
+
summary: 'Printed Anthropic API key instructions (set ANTHROPIC_API_KEY in your shell profile)',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function runWorkspaceSection(deps, opts) {
|
|
80
|
+
const configDir = deps.joinPath(deps.homedir(), '.workrail');
|
|
81
|
+
const configPath = deps.joinPath(configDir, 'config.json');
|
|
82
|
+
let existingConfig = {};
|
|
83
|
+
try {
|
|
84
|
+
const raw = await deps.readFile(configPath);
|
|
85
|
+
existingConfig = JSON.parse(raw);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
}
|
|
89
|
+
if (typeof existingConfig['WORKRAIL_DEFAULT_WORKSPACE'] === 'string' &&
|
|
90
|
+
existingConfig['WORKRAIL_DEFAULT_WORKSPACE'].trim() !== '') {
|
|
91
|
+
const existing = existingConfig['WORKRAIL_DEFAULT_WORKSPACE'];
|
|
92
|
+
if (opts.yes) {
|
|
93
|
+
return {
|
|
94
|
+
result: {
|
|
95
|
+
kind: 'skipped',
|
|
96
|
+
reason: `Workspace already set to: ${existing}`,
|
|
97
|
+
},
|
|
98
|
+
workspacePath: existing,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
deps.print('');
|
|
102
|
+
deps.print(` Current workspace: ${existing}`);
|
|
103
|
+
}
|
|
104
|
+
let workspacePath;
|
|
105
|
+
if (opts.yes) {
|
|
106
|
+
workspacePath = deps.cwd();
|
|
107
|
+
deps.print(` Using default workspace: ${workspacePath} (--yes mode)`);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
deps.print('');
|
|
111
|
+
deps.print(' Enter the default workspace path for the daemon.');
|
|
112
|
+
deps.print(' This is the root directory of the repository the agent will work in.');
|
|
113
|
+
const defaultPath = typeof existingConfig['WORKRAIL_DEFAULT_WORKSPACE'] === 'string'
|
|
114
|
+
? existingConfig['WORKRAIL_DEFAULT_WORKSPACE']
|
|
115
|
+
: deps.homedir();
|
|
116
|
+
workspacePath = await deps.prompt(` Workspace path [${defaultPath}]: `, defaultPath);
|
|
117
|
+
if (!workspacePath.trim()) {
|
|
118
|
+
workspacePath = defaultPath;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const pathExists = await deps.exists(workspacePath);
|
|
122
|
+
let warning;
|
|
123
|
+
if (!pathExists) {
|
|
124
|
+
warning = `Directory not found: ${workspacePath}. Create it before starting the daemon.`;
|
|
125
|
+
}
|
|
126
|
+
const updatedConfig = {
|
|
127
|
+
...existingConfig,
|
|
128
|
+
WORKRAIL_DEFAULT_WORKSPACE: workspacePath,
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
await deps.mkdir(configDir, { recursive: true });
|
|
132
|
+
await deps.writeFile(configPath, JSON.stringify(updatedConfig, null, 2) + '\n');
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
return {
|
|
136
|
+
result: {
|
|
137
|
+
kind: 'error',
|
|
138
|
+
message: `Failed to write config.json: ${err instanceof Error ? err.message : String(err)}`,
|
|
139
|
+
},
|
|
140
|
+
workspacePath,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (warning) {
|
|
144
|
+
return {
|
|
145
|
+
result: {
|
|
146
|
+
kind: 'warning',
|
|
147
|
+
summary: `Workspace path written to config.json: ${workspacePath}`,
|
|
148
|
+
warning,
|
|
149
|
+
},
|
|
150
|
+
workspacePath,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
result: {
|
|
155
|
+
kind: 'configured',
|
|
156
|
+
summary: `Workspace path written to config.json: ${workspacePath}`,
|
|
157
|
+
},
|
|
158
|
+
workspacePath,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
async function runScmTokenSection(deps, opts) {
|
|
162
|
+
if (opts.yes) {
|
|
163
|
+
return {
|
|
164
|
+
kind: 'skipped',
|
|
165
|
+
reason: 'SCM token setup skipped in --yes mode (set GITHUB_TOKEN or GITLAB_TOKEN manually)',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
const hasGithub = !!deps.env['GITHUB_TOKEN'];
|
|
169
|
+
const hasGitlab = !!deps.env['GITLAB_TOKEN'];
|
|
170
|
+
if (hasGithub && hasGitlab) {
|
|
171
|
+
return { kind: 'skipped', reason: 'GITHUB_TOKEN and GITLAB_TOKEN already set' };
|
|
172
|
+
}
|
|
173
|
+
if (hasGithub) {
|
|
174
|
+
return { kind: 'skipped', reason: 'GITHUB_TOKEN already set' };
|
|
175
|
+
}
|
|
176
|
+
if (hasGitlab) {
|
|
177
|
+
return { kind: 'skipped', reason: 'GITLAB_TOKEN already set' };
|
|
178
|
+
}
|
|
179
|
+
deps.print('');
|
|
180
|
+
deps.print(' GitHub / GitLab token (optional -- press Enter to skip).');
|
|
181
|
+
deps.print(' Required if you want to use polling triggers for MR/PR reviews.');
|
|
182
|
+
const token = await deps.prompt(' Personal access token: ', '');
|
|
183
|
+
if (!token.trim()) {
|
|
184
|
+
return {
|
|
185
|
+
kind: 'skipped',
|
|
186
|
+
reason: 'No token provided (skipped)',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
deps.print('');
|
|
190
|
+
deps.print(' Add the following to your ~/.zshrc or ~/.bashrc:');
|
|
191
|
+
deps.print(` export GITHUB_TOKEN=${token}`);
|
|
192
|
+
deps.print(' (Use GITLAB_TOKEN instead if you are using GitLab.)');
|
|
193
|
+
deps.print(' Then run: source ~/.zshrc (or restart your terminal)');
|
|
194
|
+
return {
|
|
195
|
+
kind: 'configured',
|
|
196
|
+
summary: 'Printed token export instructions (token NOT written to disk)',
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
async function runTriggersSection(deps, opts, workspacePath) {
|
|
200
|
+
const triggersPath = deps.joinPath(workspacePath, 'triggers.yml');
|
|
201
|
+
const exists = await deps.exists(triggersPath);
|
|
202
|
+
if (exists) {
|
|
203
|
+
return { kind: 'skipped', reason: `${triggersPath} already exists` };
|
|
204
|
+
}
|
|
205
|
+
if (!workspacePath.trim()) {
|
|
206
|
+
return {
|
|
207
|
+
kind: 'skipped',
|
|
208
|
+
reason: 'Workspace path not set -- skipping triggers.yml generation',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const content = buildTriggersYml(workspacePath);
|
|
212
|
+
try {
|
|
213
|
+
await deps.mkdir(workspacePath, { recursive: true });
|
|
214
|
+
await deps.writeFile(triggersPath, content);
|
|
215
|
+
}
|
|
216
|
+
catch (err) {
|
|
217
|
+
return {
|
|
218
|
+
kind: 'error',
|
|
219
|
+
message: `Failed to write triggers.yml: ${err instanceof Error ? err.message : String(err)}`,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
kind: 'configured',
|
|
224
|
+
summary: `Written: ${triggersPath}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function runDaemonSoulSection(deps) {
|
|
228
|
+
const soulPath = deps.joinPath(deps.homedir(), '.workrail', 'daemon-soul.md');
|
|
229
|
+
const exists = await deps.exists(soulPath);
|
|
230
|
+
if (exists) {
|
|
231
|
+
return { kind: 'skipped', reason: `${soulPath} already exists` };
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await deps.mkdir(deps.joinPath(deps.homedir(), '.workrail'), { recursive: true });
|
|
235
|
+
await deps.writeFile(soulPath, soul_template_js_1.DAEMON_SOUL_TEMPLATE);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
return {
|
|
239
|
+
kind: 'error',
|
|
240
|
+
message: `Failed to write daemon-soul.md: ${err instanceof Error ? err.message : String(err)}`,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
kind: 'configured',
|
|
245
|
+
summary: `Written: ${soulPath}`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async function runSmokeSection(deps) {
|
|
249
|
+
const result = await deps.runSmoke();
|
|
250
|
+
if (result.ok) {
|
|
251
|
+
return {
|
|
252
|
+
kind: 'configured',
|
|
253
|
+
summary: `workrail list succeeded:\n${result.output}`,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
kind: 'warning',
|
|
258
|
+
summary: 'Smoke test failed (installation may not be complete)',
|
|
259
|
+
warning: result.output || 'workrail list returned an error. Is workrail installed and on your PATH?',
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async function executeWorktrainInitCommand(deps, opts = {}) {
|
|
263
|
+
deps.print('');
|
|
264
|
+
deps.print('WorkTrain Onboarding');
|
|
265
|
+
deps.print('════════════════════════════════════════');
|
|
266
|
+
if (opts.yes) {
|
|
267
|
+
deps.print('Running in non-interactive mode (--yes). Using safe defaults.');
|
|
268
|
+
}
|
|
269
|
+
deps.print('');
|
|
270
|
+
const sectionLabels = [
|
|
271
|
+
'LLM credentials',
|
|
272
|
+
'Workspace',
|
|
273
|
+
'SCM token',
|
|
274
|
+
'triggers.yml',
|
|
275
|
+
'daemon-soul.md',
|
|
276
|
+
'Smoke test',
|
|
277
|
+
];
|
|
278
|
+
deps.print('[ 1/6 ] LLM credentials');
|
|
279
|
+
const credResult = await runCredentialsSection(deps, opts);
|
|
280
|
+
printSectionResult(deps, credResult);
|
|
281
|
+
deps.print('[ 2/6 ] Workspace');
|
|
282
|
+
const { result: wsResult, workspacePath } = await runWorkspaceSection(deps, opts);
|
|
283
|
+
printSectionResult(deps, wsResult);
|
|
284
|
+
if (wsResult.kind === 'error') {
|
|
285
|
+
return (0, cli_result_js_1.failure)(wsResult.message, {
|
|
286
|
+
suggestions: ['Check write permissions for ~/.workrail/config.json'],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
deps.print('[ 3/6 ] SCM token (GitHub / GitLab)');
|
|
290
|
+
const scmResult = await runScmTokenSection(deps, opts);
|
|
291
|
+
printSectionResult(deps, scmResult);
|
|
292
|
+
deps.print('[ 4/6 ] triggers.yml');
|
|
293
|
+
const triggersResult = await runTriggersSection(deps, opts, workspacePath);
|
|
294
|
+
printSectionResult(deps, triggersResult);
|
|
295
|
+
deps.print('[ 5/6 ] daemon-soul.md');
|
|
296
|
+
const soulResult = await runDaemonSoulSection(deps);
|
|
297
|
+
printSectionResult(deps, soulResult);
|
|
298
|
+
deps.print('[ 6/6 ] Smoke test');
|
|
299
|
+
const smokeResult = await runSmokeSection(deps);
|
|
300
|
+
printSectionResult(deps, smokeResult);
|
|
301
|
+
deps.print('');
|
|
302
|
+
deps.print('Setup complete. To start the daemon:');
|
|
303
|
+
deps.print(` workrail daemon --workspace ${workspacePath || '<your-workspace>'}`);
|
|
304
|
+
deps.print('');
|
|
305
|
+
void sectionLabels;
|
|
306
|
+
return (0, cli_result_js_1.success)({
|
|
307
|
+
message: 'WorkTrain onboarding complete',
|
|
308
|
+
details: buildSummaryDetails(credResult, wsResult, scmResult, triggersResult, soulResult, smokeResult),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function printSectionResult(deps, result) {
|
|
312
|
+
switch (result.kind) {
|
|
313
|
+
case 'skipped':
|
|
314
|
+
deps.print(` Skipped: ${result.reason}`);
|
|
315
|
+
break;
|
|
316
|
+
case 'configured':
|
|
317
|
+
deps.print(` Done: ${result.summary}`);
|
|
318
|
+
break;
|
|
319
|
+
case 'warning':
|
|
320
|
+
deps.print(` Done: ${result.summary}`);
|
|
321
|
+
deps.print(` Warning: ${result.warning}`);
|
|
322
|
+
break;
|
|
323
|
+
case 'error':
|
|
324
|
+
deps.print(` Error: ${result.message}`);
|
|
325
|
+
break;
|
|
326
|
+
}
|
|
327
|
+
deps.print('');
|
|
328
|
+
}
|
|
329
|
+
function buildSummaryDetails(...results) {
|
|
330
|
+
return results.map((r) => {
|
|
331
|
+
switch (r.kind) {
|
|
332
|
+
case 'skipped': return `skipped: ${r.reason}`;
|
|
333
|
+
case 'configured': return `configured: ${r.summary}`;
|
|
334
|
+
case 'warning': return `warning: ${r.summary} -- ${r.warning}`;
|
|
335
|
+
case 'error': return `error: ${r.message}`;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CliResult } from '../types/cli-result.js';
|
|
2
|
+
export interface WorktrainSpawnCommandDeps {
|
|
3
|
+
readonly fetch: (url: string, opts: {
|
|
4
|
+
method: string;
|
|
5
|
+
headers: Record<string, string>;
|
|
6
|
+
body: string;
|
|
7
|
+
}) => Promise<{
|
|
8
|
+
readonly ok: boolean;
|
|
9
|
+
readonly status: number;
|
|
10
|
+
readonly json: () => Promise<unknown>;
|
|
11
|
+
}>;
|
|
12
|
+
readonly readFile: (path: string) => Promise<string>;
|
|
13
|
+
readonly stdout: (line: string) => void;
|
|
14
|
+
readonly stderr: (line: string) => void;
|
|
15
|
+
readonly homedir: () => string;
|
|
16
|
+
readonly joinPath: (...paths: string[]) => string;
|
|
17
|
+
readonly pathIsAbsolute: (p: string) => boolean;
|
|
18
|
+
readonly statPath: (p: string) => Promise<{
|
|
19
|
+
isDirectory: () => boolean;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export interface WorktrainSpawnCommandOpts {
|
|
23
|
+
readonly workflow: string;
|
|
24
|
+
readonly goal: string;
|
|
25
|
+
readonly workspace: string;
|
|
26
|
+
readonly port?: number;
|
|
27
|
+
}
|
|
28
|
+
export declare function executeWorktrainSpawnCommand(deps: WorktrainSpawnCommandDeps, opts: WorktrainSpawnCommandOpts): Promise<CliResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWorktrainSpawnCommand = executeWorktrainSpawnCommand;
|
|
4
|
+
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
|
+
const DEFAULT_CONSOLE_PORT = 3456;
|
|
6
|
+
const LOCK_FILE_NAME = 'dashboard.lock';
|
|
7
|
+
async function discoverConsolePort(deps, portOverride) {
|
|
8
|
+
if (portOverride !== undefined && portOverride > 0) {
|
|
9
|
+
return portOverride;
|
|
10
|
+
}
|
|
11
|
+
const lockPath = deps.joinPath(deps.homedir(), '.workrail', LOCK_FILE_NAME);
|
|
12
|
+
try {
|
|
13
|
+
const raw = await deps.readFile(lockPath);
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (typeof parsed.port === 'number' && parsed.port > 0) {
|
|
16
|
+
return parsed.port;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
}
|
|
21
|
+
return DEFAULT_CONSOLE_PORT;
|
|
22
|
+
}
|
|
23
|
+
async function executeWorktrainSpawnCommand(deps, opts) {
|
|
24
|
+
const workflowId = opts.workflow.trim();
|
|
25
|
+
if (!workflowId) {
|
|
26
|
+
return (0, cli_result_js_1.misuse)('--workflow is required and must not be empty.');
|
|
27
|
+
}
|
|
28
|
+
const goal = opts.goal.trim();
|
|
29
|
+
if (!goal) {
|
|
30
|
+
return (0, cli_result_js_1.misuse)('--goal is required and must not be empty.');
|
|
31
|
+
}
|
|
32
|
+
const workspace = opts.workspace.trim();
|
|
33
|
+
if (!workspace) {
|
|
34
|
+
return (0, cli_result_js_1.misuse)('--workspace is required and must not be empty.');
|
|
35
|
+
}
|
|
36
|
+
if (!deps.pathIsAbsolute(workspace)) {
|
|
37
|
+
return (0, cli_result_js_1.misuse)(`--workspace must be an absolute path, got: ${workspace}`);
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const stat = await deps.statPath(workspace);
|
|
41
|
+
if (!stat.isDirectory()) {
|
|
42
|
+
return (0, cli_result_js_1.failure)(`--workspace must be an existing directory: ${workspace}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return (0, cli_result_js_1.failure)(`--workspace does not exist: ${workspace}`);
|
|
47
|
+
}
|
|
48
|
+
const port = await discoverConsolePort(deps, opts.port);
|
|
49
|
+
const url = `http://127.0.0.1:${port}/api/v2/auto/dispatch`;
|
|
50
|
+
deps.stderr(`Dispatching workflow '${workflowId}' to daemon at port ${port}...`);
|
|
51
|
+
let responseBody;
|
|
52
|
+
try {
|
|
53
|
+
const response = await deps.fetch(url, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify({ workflowId, goal, workspacePath: workspace }),
|
|
57
|
+
});
|
|
58
|
+
responseBody = await response.json();
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorMsg = isErrorResponse(responseBody)
|
|
61
|
+
? responseBody.error
|
|
62
|
+
: `HTTP ${response.status}`;
|
|
63
|
+
if (response.status === 503) {
|
|
64
|
+
return (0, cli_result_js_1.failure)(`WorkTrain daemon is not ready: ${errorMsg}\n` +
|
|
65
|
+
'Ensure the daemon is running with: worktrain daemon');
|
|
66
|
+
}
|
|
67
|
+
return (0, cli_result_js_1.failure)(`Dispatch failed: ${errorMsg}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
72
|
+
const isConnectionRefused = message.includes('ECONNREFUSED') ||
|
|
73
|
+
message.includes('fetch failed') ||
|
|
74
|
+
message.includes('connect ECONNREFUSED');
|
|
75
|
+
if (isConnectionRefused) {
|
|
76
|
+
return (0, cli_result_js_1.failure)(`Could not connect to WorkTrain daemon on port ${port}.\n` +
|
|
77
|
+
'Ensure the daemon is running with: worktrain daemon\n' +
|
|
78
|
+
`If the daemon is running on a different port, use: --port <n>`);
|
|
79
|
+
}
|
|
80
|
+
return (0, cli_result_js_1.failure)(`Dispatch request failed: ${message}`);
|
|
81
|
+
}
|
|
82
|
+
if (!isSuccessResponse(responseBody)) {
|
|
83
|
+
const errorMsg = isErrorResponse(responseBody)
|
|
84
|
+
? responseBody.error
|
|
85
|
+
: 'unexpected response shape';
|
|
86
|
+
return (0, cli_result_js_1.failure)(`Dispatch failed: ${errorMsg}`);
|
|
87
|
+
}
|
|
88
|
+
const sessionHandle = responseBody.data.sessionHandle;
|
|
89
|
+
if (typeof sessionHandle !== 'string' || !sessionHandle) {
|
|
90
|
+
return (0, cli_result_js_1.failure)('Dispatch succeeded but no session handle was returned. Is the daemon up to date?');
|
|
91
|
+
}
|
|
92
|
+
deps.stdout(sessionHandle);
|
|
93
|
+
return { kind: 'success' };
|
|
94
|
+
}
|
|
95
|
+
function isSuccessResponse(body) {
|
|
96
|
+
return (body !== null &&
|
|
97
|
+
typeof body === 'object' &&
|
|
98
|
+
body['success'] === true &&
|
|
99
|
+
typeof body['data'] === 'object');
|
|
100
|
+
}
|
|
101
|
+
function isErrorResponse(body) {
|
|
102
|
+
return (body !== null &&
|
|
103
|
+
typeof body === 'object' &&
|
|
104
|
+
body['success'] === false &&
|
|
105
|
+
typeof body['error'] === 'string');
|
|
106
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { CliResult } from '../types/cli-result.js';
|
|
2
|
+
export type Priority = 'high' | 'normal' | 'low';
|
|
3
|
+
export interface QueuedMessage {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly timestamp: string;
|
|
7
|
+
readonly workspaceHint?: string;
|
|
8
|
+
readonly priority: Priority;
|
|
9
|
+
}
|
|
10
|
+
export interface WorktrainTellCommandDeps {
|
|
11
|
+
readonly appendFile: (path: string, content: string) => Promise<void>;
|
|
12
|
+
readonly mkdir: (path: string, options: {
|
|
13
|
+
recursive: boolean;
|
|
14
|
+
}) => Promise<string | undefined>;
|
|
15
|
+
readonly homedir: () => string;
|
|
16
|
+
readonly joinPath: (...paths: string[]) => string;
|
|
17
|
+
readonly print: (line: string) => void;
|
|
18
|
+
readonly now: () => string;
|
|
19
|
+
readonly generateId: () => string;
|
|
20
|
+
}
|
|
21
|
+
export interface WorktrainTellCommandOpts {
|
|
22
|
+
readonly workspace?: string;
|
|
23
|
+
readonly priority?: Priority;
|
|
24
|
+
}
|
|
25
|
+
export declare function executeWorktrainTellCommand(messageText: string, deps: WorktrainTellCommandDeps, opts?: WorktrainTellCommandOpts): Promise<CliResult>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeWorktrainTellCommand = executeWorktrainTellCommand;
|
|
4
|
+
const cli_result_js_1 = require("../types/cli-result.js");
|
|
5
|
+
async function executeWorktrainTellCommand(messageText, deps, opts = {}) {
|
|
6
|
+
if (!messageText || !messageText.trim()) {
|
|
7
|
+
return (0, cli_result_js_1.misuse)('Message text cannot be empty.', [
|
|
8
|
+
'Usage: worktrain tell "<message>"',
|
|
9
|
+
]);
|
|
10
|
+
}
|
|
11
|
+
const priority = opts.priority ?? 'normal';
|
|
12
|
+
const queueDir = deps.joinPath(deps.homedir(), '.workrail');
|
|
13
|
+
const queuePath = deps.joinPath(queueDir, 'message-queue.jsonl');
|
|
14
|
+
const entry = {
|
|
15
|
+
id: deps.generateId(),
|
|
16
|
+
message: messageText.trim(),
|
|
17
|
+
timestamp: deps.now(),
|
|
18
|
+
...(opts.workspace ? { workspaceHint: opts.workspace } : {}),
|
|
19
|
+
priority,
|
|
20
|
+
};
|
|
21
|
+
try {
|
|
22
|
+
await deps.mkdir(queueDir, { recursive: true });
|
|
23
|
+
await deps.appendFile(queuePath, JSON.stringify(entry) + '\n');
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
return (0, cli_result_js_1.failure)(`Failed to queue message: ${err instanceof Error ? err.message : String(err)}`, { suggestions: [`Check write permissions for ${queuePath}`] });
|
|
27
|
+
}
|
|
28
|
+
deps.print(`Message queued (priority: ${priority}).`);
|
|
29
|
+
return (0, cli_result_js_1.success)({
|
|
30
|
+
message: `Message queued (priority: ${priority}).`,
|
|
31
|
+
});
|
|
32
|
+
}
|