@frankleeeee/flowy-runner 1.0.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/README.md +130 -0
- package/dist/api.js +44 -0
- package/dist/cli.js +10 -0
- package/dist/config.js +117 -0
- package/dist/daemon.js +109 -0
- package/dist/executor.js +72 -0
- package/dist/types.js +2 -0
- package/package.json +27 -0
- package/src/api.ts +46 -0
- package/src/cli.ts +11 -0
- package/src/config.ts +107 -0
- package/src/daemon.ts +122 -0
- package/src/executor.ts +96 -0
- package/src/types.ts +40 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Flowy Runner
|
|
2
|
+
|
|
3
|
+
`@frankleeeee/flowy-runner` connects a machine to Flowy and executes assigned tasks with supported local AI CLIs.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node.js 23+
|
|
8
|
+
- one or more supported CLIs installed:
|
|
9
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) via `claude`
|
|
10
|
+
- [Codex](https://github.com/openai/codex) via `codex`
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
### From this repository
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install
|
|
18
|
+
npm run build --workspace=runner
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### As the published npm package
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g @frankleeeee/flowy-runner
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Run the runner with:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
flowy-runner --name <name> --url <hub-url> [options]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
flowy-runner --name office-mac --url http://localhost:3001 --secret YOUR_SECRET
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Flags
|
|
42
|
+
|
|
43
|
+
### Required
|
|
44
|
+
|
|
45
|
+
| Flag | Description |
|
|
46
|
+
|------|-------------|
|
|
47
|
+
| `--name <name>` | Unique runner name |
|
|
48
|
+
| `--url <url>` | Flowy backend URL |
|
|
49
|
+
|
|
50
|
+
### Optional
|
|
51
|
+
|
|
52
|
+
| Flag | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `--poll-interval <seconds>` | Poll interval in seconds. Default: `5` |
|
|
55
|
+
| `--token <token>` | Reuse an existing runner token |
|
|
56
|
+
| `--secret <secret>` | Registration secret if the server requires one |
|
|
57
|
+
| `--device <info>` | Device info string. Default: hostname |
|
|
58
|
+
|
|
59
|
+
## Behavior
|
|
60
|
+
|
|
61
|
+
When the runner starts it will:
|
|
62
|
+
|
|
63
|
+
1. detect available CLIs on the machine
|
|
64
|
+
2. register with Flowy if it does not already have a saved token
|
|
65
|
+
3. send heartbeats so it appears online
|
|
66
|
+
4. poll for assigned tasks
|
|
67
|
+
5. execute tasks using the selected provider
|
|
68
|
+
6. stream the full CLI output back to Flowy
|
|
69
|
+
|
|
70
|
+
Supported provider detection:
|
|
71
|
+
|
|
72
|
+
- `claude` -> `claude-code`
|
|
73
|
+
- `codex` -> `codex`
|
|
74
|
+
|
|
75
|
+
## Execution
|
|
76
|
+
|
|
77
|
+
Current task execution commands:
|
|
78
|
+
|
|
79
|
+
| Provider | Command shape |
|
|
80
|
+
|----------|---------------|
|
|
81
|
+
| `claude-code` | `claude -p "<task prompt>" --tools all` |
|
|
82
|
+
| `codex` | `codex exec "<task prompt>" --sandbox workspace-write --color never` |
|
|
83
|
+
|
|
84
|
+
## Token persistence
|
|
85
|
+
|
|
86
|
+
Runner tokens are stored at:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
~/.config/flowy/runner-<name>.json
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
If a runner token is rejected with `401`, the runner deletes the local token file so the next launch can register cleanly again.
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
From the `runner/` directory:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm install
|
|
100
|
+
npm run dev -- --name my-runner --url http://localhost:3001
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Or from the repository root:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run dev --workspace=runner -- --name my-runner --url http://localhost:3001
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Build
|
|
110
|
+
|
|
111
|
+
From the `runner/` directory:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npm run build
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Or from the repository root:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
npm run build --workspace=runner
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Publish
|
|
124
|
+
|
|
125
|
+
This repo includes a GitHub Actions workflow for publishing `@frankleeeee/flowy-runner`.
|
|
126
|
+
|
|
127
|
+
Trigger either of these:
|
|
128
|
+
|
|
129
|
+
- push a tag like `flowy-runner-v1.2.3`
|
|
130
|
+
- run the `Publish Flowy Runner` workflow manually with a version
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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.RunnerApi = void 0;
|
|
7
|
+
const axios_1 = __importDefault(require("axios"));
|
|
8
|
+
class RunnerApi {
|
|
9
|
+
client;
|
|
10
|
+
token = '';
|
|
11
|
+
constructor(baseUrl) {
|
|
12
|
+
this.client = axios_1.default.create({ baseURL: baseUrl + '/api' });
|
|
13
|
+
}
|
|
14
|
+
setToken(token) {
|
|
15
|
+
this.token = token;
|
|
16
|
+
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
17
|
+
}
|
|
18
|
+
async register(name, aiProviders, deviceInfo, secret) {
|
|
19
|
+
const { data } = await this.client.post('/runners/register', {
|
|
20
|
+
name, aiProviders, deviceInfo, secret,
|
|
21
|
+
});
|
|
22
|
+
return data;
|
|
23
|
+
}
|
|
24
|
+
async heartbeat(aiProviders, lastCliScanAt) {
|
|
25
|
+
const { data } = await this.client.post('/runners/heartbeat', { aiProviders, lastCliScanAt });
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
async poll() {
|
|
29
|
+
const resp = await this.client.get('/runners/poll');
|
|
30
|
+
if (resp.status === 204)
|
|
31
|
+
return null;
|
|
32
|
+
return resp.data;
|
|
33
|
+
}
|
|
34
|
+
async pickTask(taskId) {
|
|
35
|
+
await this.client.post(`/runners/tasks/${taskId}/pick`);
|
|
36
|
+
}
|
|
37
|
+
async sendOutput(taskId, data) {
|
|
38
|
+
await this.client.post(`/runners/tasks/${taskId}/output`, { data });
|
|
39
|
+
}
|
|
40
|
+
async completeTask(taskId, success, data) {
|
|
41
|
+
await this.client.post(`/runners/tasks/${taskId}/complete`, { success, data });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.RunnerApi = RunnerApi;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const daemon_1 = require("./daemon");
|
|
6
|
+
const config = (0, config_1.parseArgs)(process.argv);
|
|
7
|
+
(0, daemon_1.startDaemon)(config).catch((err) => {
|
|
8
|
+
console.error('Fatal error:', err.message ?? err);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
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.parseArgs = parseArgs;
|
|
7
|
+
exports.detectAvailableProviders = detectAvailableProviders;
|
|
8
|
+
exports.saveToken = saveToken;
|
|
9
|
+
exports.deleteToken = deleteToken;
|
|
10
|
+
const os_1 = __importDefault(require("os"));
|
|
11
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const child_process_1 = require("child_process");
|
|
14
|
+
const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.config', 'flowy');
|
|
15
|
+
const SUPPORTED_PROVIDERS = [
|
|
16
|
+
{ id: 'claude-code', command: 'claude' },
|
|
17
|
+
{ id: 'codex', command: 'codex' },
|
|
18
|
+
];
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = argv.slice(2);
|
|
21
|
+
let name = '';
|
|
22
|
+
let url = '';
|
|
23
|
+
let pollInterval = 5;
|
|
24
|
+
let token;
|
|
25
|
+
let secret;
|
|
26
|
+
let device = os_1.default.hostname();
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
switch (args[i]) {
|
|
29
|
+
case '--name':
|
|
30
|
+
name = args[++i] ?? '';
|
|
31
|
+
break;
|
|
32
|
+
case '--url':
|
|
33
|
+
url = args[++i] ?? '';
|
|
34
|
+
break;
|
|
35
|
+
case '--poll-interval':
|
|
36
|
+
pollInterval = parseInt(args[++i] ?? '5', 10);
|
|
37
|
+
break;
|
|
38
|
+
case '--token':
|
|
39
|
+
token = args[++i];
|
|
40
|
+
break;
|
|
41
|
+
case '--secret':
|
|
42
|
+
secret = args[++i];
|
|
43
|
+
break;
|
|
44
|
+
case '--device':
|
|
45
|
+
device = args[++i] ?? os_1.default.hostname();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (!name || !url) {
|
|
50
|
+
console.error('Usage: flowy-runner --name <name> --url <backend-url> [options]');
|
|
51
|
+
console.error('');
|
|
52
|
+
console.error('Required:');
|
|
53
|
+
console.error(' --name Runner name');
|
|
54
|
+
console.error(' --url Backend URL (e.g. http://localhost:3001)');
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error('Options:');
|
|
57
|
+
console.error(' --poll-interval Poll interval in seconds (default: 5)');
|
|
58
|
+
console.error(' --token Existing runner token (skip registration)');
|
|
59
|
+
console.error(' --secret Registration secret (required if server has one configured)');
|
|
60
|
+
console.error(' --device Device info (default: hostname)');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
// If no token provided, try to load from saved config
|
|
64
|
+
if (!token) {
|
|
65
|
+
token = loadToken(name);
|
|
66
|
+
}
|
|
67
|
+
const providers = detectAvailableProviders();
|
|
68
|
+
if (providers.length === 0) {
|
|
69
|
+
console.error('No supported AI CLIs were detected on this machine.');
|
|
70
|
+
console.error('Install one of: claude, codex');
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
name,
|
|
75
|
+
url: url.replace(/\/$/, ''),
|
|
76
|
+
providers,
|
|
77
|
+
lastCliScanAt: new Date().toISOString(),
|
|
78
|
+
pollInterval,
|
|
79
|
+
token,
|
|
80
|
+
secret,
|
|
81
|
+
device,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function detectAvailableProviders() {
|
|
85
|
+
return SUPPORTED_PROVIDERS
|
|
86
|
+
.filter((provider) => isCommandAvailable(provider.command))
|
|
87
|
+
.map((provider) => provider.id);
|
|
88
|
+
}
|
|
89
|
+
function isCommandAvailable(command) {
|
|
90
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
91
|
+
const result = (0, child_process_1.spawnSync)(checker, [command], { stdio: 'ignore' });
|
|
92
|
+
return result.status === 0;
|
|
93
|
+
}
|
|
94
|
+
function loadToken(name) {
|
|
95
|
+
const file = path_1.default.join(CONFIG_DIR, `runner-${name}.json`);
|
|
96
|
+
try {
|
|
97
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf-8'));
|
|
98
|
+
return data.token;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function saveToken(name, id, token) {
|
|
105
|
+
fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
106
|
+
const file = path_1.default.join(CONFIG_DIR, `runner-${name}.json`);
|
|
107
|
+
fs_1.default.writeFileSync(file, JSON.stringify({ id, token }, null, 2));
|
|
108
|
+
}
|
|
109
|
+
function deleteToken(name) {
|
|
110
|
+
const file = path_1.default.join(CONFIG_DIR, `runner-${name}.json`);
|
|
111
|
+
try {
|
|
112
|
+
fs_1.default.unlinkSync(file);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Ignore missing/unreadable token files.
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/daemon.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startDaemon = startDaemon;
|
|
4
|
+
const api_1 = require("./api");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const executor_1 = require("./executor");
|
|
7
|
+
async function startDaemon(config) {
|
|
8
|
+
const api = new api_1.RunnerApi(config.url);
|
|
9
|
+
let availableProviders = [...config.providers];
|
|
10
|
+
let lastCliScanAt = config.lastCliScanAt;
|
|
11
|
+
// ── Registration ────────────────────────────────────────────────────────
|
|
12
|
+
if (config.token) {
|
|
13
|
+
console.log(`Using existing token for runner "${config.name}"`);
|
|
14
|
+
api.setToken(config.token);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(`Registering runner "${config.name}" at ${config.url}...`);
|
|
18
|
+
const { id, token } = await api.register(config.name, availableProviders, config.device, config.secret);
|
|
19
|
+
api.setToken(token);
|
|
20
|
+
(0, config_1.saveToken)(config.name, id, token);
|
|
21
|
+
console.log(`Registered! Runner ID: ${id}`);
|
|
22
|
+
console.log(`Token saved to ~/.config/flowy/runner-${config.name}.json`);
|
|
23
|
+
}
|
|
24
|
+
let executing = false;
|
|
25
|
+
let killCurrent = null;
|
|
26
|
+
const handleInvalidToken = () => {
|
|
27
|
+
(0, config_1.deleteToken)(config.name);
|
|
28
|
+
console.error('Runner token rejected (401). Deleted local runner token; restart to register again.');
|
|
29
|
+
process.exit(1);
|
|
30
|
+
};
|
|
31
|
+
// ── Heartbeat ───────────────────────────────────────────────────────────
|
|
32
|
+
const heartbeat = async () => {
|
|
33
|
+
try {
|
|
34
|
+
const response = await api.heartbeat(availableProviders, lastCliScanAt);
|
|
35
|
+
if (response.refreshCli) {
|
|
36
|
+
availableProviders = (0, config_1.detectAvailableProviders)();
|
|
37
|
+
lastCliScanAt = new Date().toISOString();
|
|
38
|
+
console.log(`Refreshing available CLIs: ${availableProviders.join(', ') || '(none)'}`);
|
|
39
|
+
await api.heartbeat(availableProviders, lastCliScanAt);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
44
|
+
if (msg.includes('401')) {
|
|
45
|
+
handleInvalidToken();
|
|
46
|
+
}
|
|
47
|
+
console.warn('Heartbeat failed:', msg);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
// Send initial heartbeat, then every 30 seconds
|
|
51
|
+
await heartbeat();
|
|
52
|
+
const heartbeatInterval = setInterval(heartbeat, 30_000);
|
|
53
|
+
// ── Polling ─────────────────────────────────────────────────────────────
|
|
54
|
+
const poll = async () => {
|
|
55
|
+
if (executing)
|
|
56
|
+
return;
|
|
57
|
+
try {
|
|
58
|
+
const task = await api.poll();
|
|
59
|
+
if (!task)
|
|
60
|
+
return;
|
|
61
|
+
console.log(`\nPicked up task: ${task.task_key} - ${task.title}`);
|
|
62
|
+
console.log(` AI Provider: ${task.ai_provider}`);
|
|
63
|
+
console.log(` Description: ${(task.description || task.title).slice(0, 100)}...`);
|
|
64
|
+
await api.pickTask(task.id);
|
|
65
|
+
executing = true;
|
|
66
|
+
const { promise, kill } = (0, executor_1.executeTask)(task, async (chunk) => {
|
|
67
|
+
try {
|
|
68
|
+
await api.sendOutput(task.id, chunk);
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
console.warn('Failed to send output chunk:', err instanceof Error ? err.message : err);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
killCurrent = kill;
|
|
75
|
+
const result = await promise;
|
|
76
|
+
killCurrent = null;
|
|
77
|
+
executing = false;
|
|
78
|
+
console.log(`\nTask ${task.task_key} ${result.success ? 'completed' : 'failed'}`);
|
|
79
|
+
await api.completeTask(task.id, result.success, result.sendOnComplete ? result.output : '');
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
executing = false;
|
|
83
|
+
killCurrent = null;
|
|
84
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
85
|
+
if (msg.includes('401')) {
|
|
86
|
+
handleInvalidToken();
|
|
87
|
+
}
|
|
88
|
+
console.error('Poll/execute error:', err instanceof Error ? err.message : err);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const pollInterval = setInterval(poll, config.pollInterval * 1000);
|
|
92
|
+
console.log(`\nRunner "${config.name}" is online and polling every ${config.pollInterval}s`);
|
|
93
|
+
console.log(`Providers: ${availableProviders.join(', ') || '(none)'}`);
|
|
94
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
95
|
+
// ── Graceful Shutdown ───────────────────────────────────────────────────
|
|
96
|
+
const shutdown = async (signal) => {
|
|
97
|
+
console.log(`\nReceived ${signal}, shutting down...`);
|
|
98
|
+
clearInterval(heartbeatInterval);
|
|
99
|
+
clearInterval(pollInterval);
|
|
100
|
+
if (killCurrent) {
|
|
101
|
+
console.log('Killing running task...');
|
|
102
|
+
killCurrent();
|
|
103
|
+
}
|
|
104
|
+
// Give a moment for cleanup
|
|
105
|
+
setTimeout(() => process.exit(0), 500);
|
|
106
|
+
};
|
|
107
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
108
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
109
|
+
}
|
package/dist/executor.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.executeTask = executeTask;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
/** Build the CLI command + args for a given AI provider. */
|
|
6
|
+
function buildCommand(aiProvider, prompt) {
|
|
7
|
+
switch (aiProvider) {
|
|
8
|
+
case 'claude-code':
|
|
9
|
+
return { cmd: 'claude', args: ['-p', prompt, "--tools", "all"], streamOutput: true };
|
|
10
|
+
case 'codex':
|
|
11
|
+
return {
|
|
12
|
+
cmd: 'codex',
|
|
13
|
+
args: ['exec', prompt, '--sandbox', 'workspace-write', '--color', 'never'],
|
|
14
|
+
streamOutput: true,
|
|
15
|
+
};
|
|
16
|
+
default:
|
|
17
|
+
throw new Error(`Unknown AI provider: ${aiProvider}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Execute a task using the specified AI CLI tool.
|
|
22
|
+
* Calls `onOutput` with buffered chunks every ~2 seconds.
|
|
23
|
+
* Returns when the process exits.
|
|
24
|
+
*/
|
|
25
|
+
function executeTask(task, onOutput) {
|
|
26
|
+
const { cmd, args, streamOutput } = buildCommand(task.ai_provider, task.description || task.title);
|
|
27
|
+
let child;
|
|
28
|
+
let fullOutput = '';
|
|
29
|
+
let buffer = '';
|
|
30
|
+
let flushTimer;
|
|
31
|
+
const promise = new Promise((resolve) => {
|
|
32
|
+
console.log(` Spawning: ${cmd} ${args.join(' ')}`);
|
|
33
|
+
child = (0, child_process_1.spawn)(cmd, args, {
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
env: { ...process.env },
|
|
36
|
+
});
|
|
37
|
+
const flush = () => {
|
|
38
|
+
if (streamOutput && buffer.length > 0) {
|
|
39
|
+
onOutput(buffer);
|
|
40
|
+
buffer = '';
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
flushTimer = setInterval(flush, 2000);
|
|
44
|
+
child.stdout?.on('data', (data) => {
|
|
45
|
+
const text = data.toString();
|
|
46
|
+
fullOutput += text;
|
|
47
|
+
buffer += text;
|
|
48
|
+
});
|
|
49
|
+
child.stderr?.on('data', (data) => {
|
|
50
|
+
const text = data.toString();
|
|
51
|
+
fullOutput += text;
|
|
52
|
+
buffer += text;
|
|
53
|
+
});
|
|
54
|
+
child.on('error', (err) => {
|
|
55
|
+
clearInterval(flushTimer);
|
|
56
|
+
flush();
|
|
57
|
+
fullOutput += `\n[Error: ${err.message}]`;
|
|
58
|
+
resolve({ success: false, output: fullOutput, sendOnComplete: true });
|
|
59
|
+
});
|
|
60
|
+
child.on('close', (code) => {
|
|
61
|
+
clearInterval(flushTimer);
|
|
62
|
+
flush();
|
|
63
|
+
const success = code === 0;
|
|
64
|
+
resolve({ success, output: fullOutput, sendOnComplete: !streamOutput || !success });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
const kill = () => {
|
|
68
|
+
clearInterval(flushTimer);
|
|
69
|
+
child?.kill('SIGTERM');
|
|
70
|
+
};
|
|
71
|
+
return { promise, kill };
|
|
72
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@frankleeeee/flowy-runner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"bin": {
|
|
6
|
+
"flowy-runner": "./dist/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "ts-node-dev --respawn --transpile-only src/cli.ts --",
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"axios": "^1.6.8"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=23"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^20.11.30",
|
|
24
|
+
"ts-node-dev": "^2.0.0",
|
|
25
|
+
"typescript": "^5.4.3"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/api.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import axios, { AxiosInstance } from 'axios';
|
|
2
|
+
import { Task, RegisterResponse, HeartbeatResponse } from './types';
|
|
3
|
+
|
|
4
|
+
export class RunnerApi {
|
|
5
|
+
private client: AxiosInstance;
|
|
6
|
+
private token: string = '';
|
|
7
|
+
|
|
8
|
+
constructor(baseUrl: string) {
|
|
9
|
+
this.client = axios.create({ baseURL: baseUrl + '/api' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setToken(token: string): void {
|
|
13
|
+
this.token = token;
|
|
14
|
+
this.client.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async register(name: string, aiProviders: string[], deviceInfo: string, secret?: string): Promise<RegisterResponse> {
|
|
18
|
+
const { data } = await this.client.post<RegisterResponse>('/runners/register', {
|
|
19
|
+
name, aiProviders, deviceInfo, secret,
|
|
20
|
+
});
|
|
21
|
+
return data;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async heartbeat(aiProviders: string[], lastCliScanAt?: string): Promise<HeartbeatResponse> {
|
|
25
|
+
const { data } = await this.client.post<HeartbeatResponse>('/runners/heartbeat', { aiProviders, lastCliScanAt });
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async poll(): Promise<Task | null> {
|
|
30
|
+
const resp = await this.client.get('/runners/poll');
|
|
31
|
+
if (resp.status === 204) return null;
|
|
32
|
+
return resp.data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async pickTask(taskId: string): Promise<void> {
|
|
36
|
+
await this.client.post(`/runners/tasks/${taskId}/pick`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async sendOutput(taskId: string, data: string): Promise<void> {
|
|
40
|
+
await this.client.post(`/runners/tasks/${taskId}/output`, { data });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async completeTask(taskId: string, success: boolean, data?: string): Promise<void> {
|
|
44
|
+
await this.client.post(`/runners/tasks/${taskId}/complete`, { success, data });
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { parseArgs } from './config';
|
|
4
|
+
import { startDaemon } from './daemon';
|
|
5
|
+
|
|
6
|
+
const config = parseArgs(process.argv);
|
|
7
|
+
|
|
8
|
+
startDaemon(config).catch((err) => {
|
|
9
|
+
console.error('Fatal error:', err.message ?? err);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { RunnerConfig } from './types';
|
|
6
|
+
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.config', 'flowy');
|
|
8
|
+
const SUPPORTED_PROVIDERS = [
|
|
9
|
+
{ id: 'claude-code', command: 'claude' },
|
|
10
|
+
{ id: 'codex', command: 'codex' },
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export function parseArgs(argv: string[]): RunnerConfig {
|
|
14
|
+
const args = argv.slice(2);
|
|
15
|
+
let name = '';
|
|
16
|
+
let url = '';
|
|
17
|
+
let pollInterval = 5;
|
|
18
|
+
let token: string | undefined;
|
|
19
|
+
let secret: string | undefined;
|
|
20
|
+
let device = os.hostname();
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < args.length; i++) {
|
|
23
|
+
switch (args[i]) {
|
|
24
|
+
case '--name': name = args[++i] ?? ''; break;
|
|
25
|
+
case '--url': url = args[++i] ?? ''; break;
|
|
26
|
+
case '--poll-interval': pollInterval = parseInt(args[++i] ?? '5', 10); break;
|
|
27
|
+
case '--token': token = args[++i]; break;
|
|
28
|
+
case '--secret': secret = args[++i]; break;
|
|
29
|
+
case '--device': device = args[++i] ?? os.hostname(); break;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!name || !url) {
|
|
34
|
+
console.error('Usage: flowy-runner --name <name> --url <backend-url> [options]');
|
|
35
|
+
console.error('');
|
|
36
|
+
console.error('Required:');
|
|
37
|
+
console.error(' --name Runner name');
|
|
38
|
+
console.error(' --url Backend URL (e.g. http://localhost:3001)');
|
|
39
|
+
console.error('');
|
|
40
|
+
console.error('Options:');
|
|
41
|
+
console.error(' --poll-interval Poll interval in seconds (default: 5)');
|
|
42
|
+
console.error(' --token Existing runner token (skip registration)');
|
|
43
|
+
console.error(' --secret Registration secret (required if server has one configured)');
|
|
44
|
+
console.error(' --device Device info (default: hostname)');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// If no token provided, try to load from saved config
|
|
49
|
+
if (!token) {
|
|
50
|
+
token = loadToken(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const providers = detectAvailableProviders();
|
|
54
|
+
if (providers.length === 0) {
|
|
55
|
+
console.error('No supported AI CLIs were detected on this machine.');
|
|
56
|
+
console.error('Install one of: claude, codex');
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
url: url.replace(/\/$/, ''),
|
|
63
|
+
providers,
|
|
64
|
+
lastCliScanAt: new Date().toISOString(),
|
|
65
|
+
pollInterval,
|
|
66
|
+
token,
|
|
67
|
+
secret,
|
|
68
|
+
device,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function detectAvailableProviders(): string[] {
|
|
73
|
+
return SUPPORTED_PROVIDERS
|
|
74
|
+
.filter((provider) => isCommandAvailable(provider.command))
|
|
75
|
+
.map((provider) => provider.id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isCommandAvailable(command: string): boolean {
|
|
79
|
+
const checker = process.platform === 'win32' ? 'where' : 'which';
|
|
80
|
+
const result = spawnSync(checker, [command], { stdio: 'ignore' });
|
|
81
|
+
return result.status === 0;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadToken(name: string): string | undefined {
|
|
85
|
+
const file = path.join(CONFIG_DIR, `runner-${name}.json`);
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
88
|
+
return data.token;
|
|
89
|
+
} catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function saveToken(name: string, id: string, token: string): void {
|
|
95
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
96
|
+
const file = path.join(CONFIG_DIR, `runner-${name}.json`);
|
|
97
|
+
fs.writeFileSync(file, JSON.stringify({ id, token }, null, 2));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function deleteToken(name: string): void {
|
|
101
|
+
const file = path.join(CONFIG_DIR, `runner-${name}.json`);
|
|
102
|
+
try {
|
|
103
|
+
fs.unlinkSync(file);
|
|
104
|
+
} catch {
|
|
105
|
+
// Ignore missing/unreadable token files.
|
|
106
|
+
}
|
|
107
|
+
}
|
package/src/daemon.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { RunnerConfig } from './types';
|
|
2
|
+
import { RunnerApi } from './api';
|
|
3
|
+
import { deleteToken, detectAvailableProviders, saveToken } from './config';
|
|
4
|
+
import { executeTask } from './executor';
|
|
5
|
+
|
|
6
|
+
export async function startDaemon(config: RunnerConfig): Promise<void> {
|
|
7
|
+
const api = new RunnerApi(config.url);
|
|
8
|
+
let availableProviders = [...config.providers];
|
|
9
|
+
let lastCliScanAt = config.lastCliScanAt;
|
|
10
|
+
|
|
11
|
+
// ── Registration ────────────────────────────────────────────────────────
|
|
12
|
+
if (config.token) {
|
|
13
|
+
console.log(`Using existing token for runner "${config.name}"`);
|
|
14
|
+
api.setToken(config.token);
|
|
15
|
+
} else {
|
|
16
|
+
console.log(`Registering runner "${config.name}" at ${config.url}...`);
|
|
17
|
+
const { id, token } = await api.register(config.name, availableProviders, config.device, config.secret);
|
|
18
|
+
api.setToken(token);
|
|
19
|
+
saveToken(config.name, id, token);
|
|
20
|
+
console.log(`Registered! Runner ID: ${id}`);
|
|
21
|
+
console.log(`Token saved to ~/.config/flowy/runner-${config.name}.json`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let executing = false;
|
|
25
|
+
let killCurrent: (() => void) | null = null;
|
|
26
|
+
|
|
27
|
+
const handleInvalidToken = () => {
|
|
28
|
+
deleteToken(config.name);
|
|
29
|
+
console.error('Runner token rejected (401). Deleted local runner token; restart to register again.');
|
|
30
|
+
process.exit(1);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ── Heartbeat ───────────────────────────────────────────────────────────
|
|
34
|
+
const heartbeat = async () => {
|
|
35
|
+
try {
|
|
36
|
+
const response = await api.heartbeat(availableProviders, lastCliScanAt);
|
|
37
|
+
if (response.refreshCli) {
|
|
38
|
+
availableProviders = detectAvailableProviders();
|
|
39
|
+
lastCliScanAt = new Date().toISOString();
|
|
40
|
+
console.log(`Refreshing available CLIs: ${availableProviders.join(', ') || '(none)'}`);
|
|
41
|
+
await api.heartbeat(availableProviders, lastCliScanAt);
|
|
42
|
+
}
|
|
43
|
+
} catch (err: unknown) {
|
|
44
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
45
|
+
if (msg.includes('401')) {
|
|
46
|
+
handleInvalidToken();
|
|
47
|
+
}
|
|
48
|
+
console.warn('Heartbeat failed:', msg);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Send initial heartbeat, then every 30 seconds
|
|
53
|
+
await heartbeat();
|
|
54
|
+
const heartbeatInterval = setInterval(heartbeat, 30_000);
|
|
55
|
+
|
|
56
|
+
// ── Polling ─────────────────────────────────────────────────────────────
|
|
57
|
+
const poll = async () => {
|
|
58
|
+
if (executing) return;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
const task = await api.poll();
|
|
62
|
+
if (!task) return;
|
|
63
|
+
|
|
64
|
+
console.log(`\nPicked up task: ${task.task_key} - ${task.title}`);
|
|
65
|
+
console.log(` AI Provider: ${task.ai_provider}`);
|
|
66
|
+
console.log(` Description: ${(task.description || task.title).slice(0, 100)}...`);
|
|
67
|
+
|
|
68
|
+
await api.pickTask(task.id);
|
|
69
|
+
executing = true;
|
|
70
|
+
|
|
71
|
+
const { promise, kill } = executeTask(task, async (chunk) => {
|
|
72
|
+
try {
|
|
73
|
+
await api.sendOutput(task.id, chunk);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.warn('Failed to send output chunk:', err instanceof Error ? err.message : err);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
killCurrent = kill;
|
|
80
|
+
|
|
81
|
+
const result = await promise;
|
|
82
|
+
killCurrent = null;
|
|
83
|
+
executing = false;
|
|
84
|
+
|
|
85
|
+
console.log(`\nTask ${task.task_key} ${result.success ? 'completed' : 'failed'}`);
|
|
86
|
+
await api.completeTask(task.id, result.success, result.sendOnComplete ? result.output : '');
|
|
87
|
+
|
|
88
|
+
} catch (err) {
|
|
89
|
+
executing = false;
|
|
90
|
+
killCurrent = null;
|
|
91
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
92
|
+
if (msg.includes('401')) {
|
|
93
|
+
handleInvalidToken();
|
|
94
|
+
}
|
|
95
|
+
console.error('Poll/execute error:', err instanceof Error ? err.message : err);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const pollInterval = setInterval(poll, config.pollInterval * 1000);
|
|
100
|
+
|
|
101
|
+
console.log(`\nRunner "${config.name}" is online and polling every ${config.pollInterval}s`);
|
|
102
|
+
console.log(`Providers: ${availableProviders.join(', ') || '(none)'}`);
|
|
103
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
104
|
+
|
|
105
|
+
// ── Graceful Shutdown ───────────────────────────────────────────────────
|
|
106
|
+
const shutdown = async (signal: string) => {
|
|
107
|
+
console.log(`\nReceived ${signal}, shutting down...`);
|
|
108
|
+
clearInterval(heartbeatInterval);
|
|
109
|
+
clearInterval(pollInterval);
|
|
110
|
+
|
|
111
|
+
if (killCurrent) {
|
|
112
|
+
console.log('Killing running task...');
|
|
113
|
+
killCurrent();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Give a moment for cleanup
|
|
117
|
+
setTimeout(() => process.exit(0), 500);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
121
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
122
|
+
}
|
package/src/executor.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
2
|
+
import { Task } from './types';
|
|
3
|
+
|
|
4
|
+
export interface ExecutionResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
output: string;
|
|
7
|
+
sendOnComplete: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Build the CLI command + args for a given AI provider. */
|
|
11
|
+
function buildCommand(aiProvider: string, prompt: string): {
|
|
12
|
+
cmd: string;
|
|
13
|
+
args: string[];
|
|
14
|
+
streamOutput: boolean;
|
|
15
|
+
} {
|
|
16
|
+
switch (aiProvider) {
|
|
17
|
+
case 'claude-code':
|
|
18
|
+
return { cmd: 'claude', args: ['-p', prompt, "--tools", "all"], streamOutput: true };
|
|
19
|
+
case 'codex':
|
|
20
|
+
return {
|
|
21
|
+
cmd: 'codex',
|
|
22
|
+
args: ['exec', prompt, '--sandbox', 'workspace-write', '--color', 'never'],
|
|
23
|
+
streamOutput: true,
|
|
24
|
+
};
|
|
25
|
+
default:
|
|
26
|
+
throw new Error(`Unknown AI provider: ${aiProvider}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Execute a task using the specified AI CLI tool.
|
|
32
|
+
* Calls `onOutput` with buffered chunks every ~2 seconds.
|
|
33
|
+
* Returns when the process exits.
|
|
34
|
+
*/
|
|
35
|
+
export function executeTask(
|
|
36
|
+
task: Task,
|
|
37
|
+
onOutput: (chunk: string) => void,
|
|
38
|
+
): { promise: Promise<ExecutionResult>; kill: () => void } {
|
|
39
|
+
const { cmd, args, streamOutput } = buildCommand(task.ai_provider!, task.description || task.title);
|
|
40
|
+
|
|
41
|
+
let child: ChildProcess;
|
|
42
|
+
let fullOutput = '';
|
|
43
|
+
let buffer = '';
|
|
44
|
+
let flushTimer: ReturnType<typeof setInterval>;
|
|
45
|
+
|
|
46
|
+
const promise = new Promise<ExecutionResult>((resolve) => {
|
|
47
|
+
console.log(` Spawning: ${cmd} ${args.join(' ')}`);
|
|
48
|
+
|
|
49
|
+
child = spawn(cmd, args, {
|
|
50
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
51
|
+
env: { ...process.env },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const flush = () => {
|
|
55
|
+
if (streamOutput && buffer.length > 0) {
|
|
56
|
+
onOutput(buffer);
|
|
57
|
+
buffer = '';
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
flushTimer = setInterval(flush, 2000);
|
|
62
|
+
|
|
63
|
+
child.stdout?.on('data', (data: Buffer) => {
|
|
64
|
+
const text = data.toString();
|
|
65
|
+
fullOutput += text;
|
|
66
|
+
buffer += text;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
child.stderr?.on('data', (data: Buffer) => {
|
|
70
|
+
const text = data.toString();
|
|
71
|
+
fullOutput += text;
|
|
72
|
+
buffer += text;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
child.on('error', (err) => {
|
|
76
|
+
clearInterval(flushTimer);
|
|
77
|
+
flush();
|
|
78
|
+
fullOutput += `\n[Error: ${err.message}]`;
|
|
79
|
+
resolve({ success: false, output: fullOutput, sendOnComplete: true });
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
child.on('close', (code) => {
|
|
83
|
+
clearInterval(flushTimer);
|
|
84
|
+
flush();
|
|
85
|
+
const success = code === 0;
|
|
86
|
+
resolve({ success, output: fullOutput, sendOnComplete: !streamOutput || !success });
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const kill = () => {
|
|
91
|
+
clearInterval(flushTimer!);
|
|
92
|
+
child?.kill('SIGTERM');
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
return { promise, kill };
|
|
96
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export interface RunnerConfig {
|
|
2
|
+
name: string;
|
|
3
|
+
url: string;
|
|
4
|
+
providers: string[];
|
|
5
|
+
lastCliScanAt: string;
|
|
6
|
+
pollInterval: number; // seconds
|
|
7
|
+
token?: string;
|
|
8
|
+
secret?: string;
|
|
9
|
+
device: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Task {
|
|
13
|
+
id: string;
|
|
14
|
+
project_id: string;
|
|
15
|
+
task_number: number;
|
|
16
|
+
task_key: string;
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
status: string;
|
|
20
|
+
priority: string;
|
|
21
|
+
runner_id: string | null;
|
|
22
|
+
ai_provider: string | null;
|
|
23
|
+
labels: string;
|
|
24
|
+
output: string | null;
|
|
25
|
+
started_at: string | null;
|
|
26
|
+
completed_at: string | null;
|
|
27
|
+
created_at: string;
|
|
28
|
+
updated_at: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RegisterResponse {
|
|
32
|
+
id: string;
|
|
33
|
+
token: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface HeartbeatResponse {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
status: string;
|
|
39
|
+
refreshCli?: boolean;
|
|
40
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|