@frankleeeee/flowy-runner 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,130 +1,120 @@
1
1
  # Flowy Runner
2
2
 
3
- `@frankleeeee/flowy-runner` connects a machine to Flowy and executes assigned tasks with supported local AI CLIs.
3
+ A daemon that connects to the Flowy hub and executes tasks using AI CLI tools on your local machine.
4
4
 
5
- ## Requirements
5
+ ## Prerequisites
6
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`
7
+ - **Node.js v23+** (required for better-sqlite3 compatibility)
8
+ - One or more AI CLI tools installed:
9
+ - [Claude Code](https://docs.anthropic.com/en/docs/claude-code) `claude`
10
+ - [Codex](https://github.com/openai/codex) `codex`
11
+ - [Cursor Agent](https://docs.cursor.com/agent) — `agent`
11
12
 
12
- ## Install
13
+ ## Build
13
14
 
14
- ### From this repository
15
+ From the repository root:
15
16
 
16
17
  ```bash
17
18
  npm install
18
19
  npm run build --workspace=runner
19
20
  ```
20
21
 
21
- ### As the published npm package
22
+ Or from the `runner/` directory:
22
23
 
23
24
  ```bash
24
- npm install -g @frankleeeee/flowy-runner
25
+ npm install
26
+ npm run build
25
27
  ```
26
28
 
29
+ This compiles TypeScript to `dist/` via `tsc`.
30
+
27
31
  ## Usage
28
32
 
29
- Run the runner with:
33
+ Run via npm scripts:
30
34
 
31
35
  ```bash
32
- flowy-runner --name <name> --url <hub-url> [options]
36
+ # Development (with hot reload)
37
+ npm run dev -- --name <name> --url <hub-url> [options]
38
+
39
+ # Production (after building)
40
+ npm run build
41
+ npm start -- --name <name> --url <hub-url> [options]
33
42
  ```
34
43
 
35
- Example:
44
+ Or install globally to use the `flowy-runner` command directly:
36
45
 
37
46
  ```bash
38
- flowy-runner --name office-mac --url http://localhost:3001 --secret YOUR_SECRET
47
+ npm install -g @frankleeeee/flowy-runner
48
+ flowy-runner --name <name> --url <hub-url> [options]
39
49
  ```
40
50
 
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
+ ### Required flags
51
52
 
52
53
  | Flag | Description |
53
54
  |------|-------------|
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 |
55
+ | `--name <name>` | Unique name for this runner (e.g. `macbook-pro`) |
56
+ | `--url <url>` | URL of the Flowy hub backend (e.g. `http://localhost:3001`) |
58
57
 
59
- ## Behavior
58
+ ### Optional flags
60
59
 
61
- When the runner starts it will:
60
+ | Flag | Default | Description |
61
+ |------|---------|-------------|
62
+ | `--poll-interval <ms>` | `5000` | How often to poll for new tasks (in milliseconds) |
63
+ | `--token <token>` | — | Reuse an existing runner token instead of registering |
64
+ | `--secret <secret>` | — | Registration secret (if the hub requires one) |
65
+ | `--device <info>` | auto-detected | Device info string sent during registration |
62
66
 
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
67
+ ### Examples
69
68
 
70
- Supported provider detection:
69
+ **Development** (with hot reload):
71
70
 
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
71
+ ```bash
72
+ cd runner
73
+ npm run dev -- --name my-laptop --url http://localhost:3001
90
74
  ```
91
75
 
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:
76
+ **Production** (compiled):
97
77
 
98
78
  ```bash
99
- npm install
100
- npm run dev -- --name my-runner --url http://localhost:3001
79
+ cd runner
80
+ npm run build
81
+ npm start -- --name my-laptop --url http://localhost:3001
101
82
  ```
102
83
 
103
- Or from the repository root:
84
+ **Global install** (use from anywhere):
104
85
 
105
86
  ```bash
106
- npm run dev --workspace=runner -- --name my-runner --url http://localhost:3001
87
+ cd runner
88
+ npm run build
89
+ npm link
90
+ flowy-runner --name office-server --url https://hub.example.com --secret my-secret
107
91
  ```
108
92
 
109
- ## Build
93
+ ## How it works
110
94
 
111
- From the `runner/` directory:
95
+ 1. **Register** — On first launch, the runner registers with the hub and receives an authentication token. The token is saved to `~/.config/my-hub/runner-<name>.json` for future sessions.
112
96
 
113
- ```bash
114
- npm run build
115
- ```
97
+ 2. **Heartbeat** — Every 30 seconds, the runner sends a heartbeat to the hub so it appears as "online" in the dashboard.
116
98
 
117
- Or from the repository root:
99
+ 3. **Poll** — Every 5 seconds (configurable), the runner polls the hub for tasks assigned to it with status `todo`.
118
100
 
119
- ```bash
120
- npm run build --workspace=runner
121
- ```
101
+ 4. **Detect CLIs** — On startup, the runner checks the local machine for supported commands (`claude`, `codex`, `agent`) and registers only the providers it finds.
122
102
 
123
- ## Publish
103
+ 5. **Execute** — When a task is picked up, the runner spawns the appropriate AI CLI tool as a child process:
104
+ | Provider | Command |
105
+ |----------|---------|
106
+ | `claude-code` | `claude -p "<task description>"` |
107
+ | `codex` | `codex exec "<task description>"` |
108
+ | `cursor-agent` | `agent --print --force "<task description>"` |
109
+
110
+ 6. **Stream output** — Output is buffered and sent back to the hub every 2 seconds so you can monitor progress in real time from the web UI.
111
+
112
+ 7. **Complete** — Once the process exits, the runner reports success or failure to the hub.
113
+
114
+ ## Token persistence
124
115
 
125
- This repo includes a GitHub Actions workflow for publishing `@frankleeeee/flowy-runner`.
116
+ Runner tokens are saved to `~/.config/my-hub/runner-<name>.json`. On subsequent launches with the same `--name`, the saved token is reused automatically. You can also pass `--token <token>` to use a specific token.
126
117
 
127
- Trigger either of these:
118
+ ## Graceful shutdown
128
119
 
129
- - push a tag like `flowy-runner-v1.2.3`
130
- - run the `Publish Flowy Runner` workflow manually with a version
120
+ The runner handles `SIGINT` and `SIGTERM` signals for clean shutdown. Press `Ctrl+C` to stop the runner gracefully.
package/dist/cli.js CHANGED
File without changes
package/dist/config.js CHANGED
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.childProcess = void 0;
6
7
  exports.parseArgs = parseArgs;
7
8
  exports.detectAvailableProviders = detectAvailableProviders;
8
9
  exports.saveToken = saveToken;
@@ -11,10 +12,12 @@ const os_1 = __importDefault(require("os"));
11
12
  const fs_1 = __importDefault(require("fs"));
12
13
  const path_1 = __importDefault(require("path"));
13
14
  const child_process_1 = require("child_process");
14
- const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.config', 'flowy');
15
+ exports.childProcess = { spawnSync: child_process_1.spawnSync };
16
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.config', 'my-hub');
15
17
  const SUPPORTED_PROVIDERS = [
16
18
  { id: 'claude-code', command: 'claude' },
17
19
  { id: 'codex', command: 'codex' },
20
+ { id: 'cursor-agent', command: 'agent' },
18
21
  ];
19
22
  function parseArgs(argv) {
20
23
  const args = argv.slice(2);
@@ -47,11 +50,11 @@ function parseArgs(argv) {
47
50
  }
48
51
  }
49
52
  if (!name || !url) {
50
- console.error('Usage: flowy-runner --name <name> --url <backend-url> [options]');
53
+ console.error('Usage: flowy-runner --name <name> --url <hub-url> [options]');
51
54
  console.error('');
52
55
  console.error('Required:');
53
56
  console.error(' --name Runner name');
54
- console.error(' --url Backend URL (e.g. http://localhost:3001)');
57
+ console.error(' --url Hub URL (e.g. http://localhost:3001)');
55
58
  console.error('');
56
59
  console.error('Options:');
57
60
  console.error(' --poll-interval Poll interval in seconds (default: 5)');
@@ -67,7 +70,7 @@ function parseArgs(argv) {
67
70
  const providers = detectAvailableProviders();
68
71
  if (providers.length === 0) {
69
72
  console.error('No supported AI CLIs were detected on this machine.');
70
- console.error('Install one of: claude, codex');
73
+ console.error('Install one of: claude, codex, agent');
71
74
  process.exit(1);
72
75
  }
73
76
  return {
@@ -88,7 +91,7 @@ function detectAvailableProviders() {
88
91
  }
89
92
  function isCommandAvailable(command) {
90
93
  const checker = process.platform === 'win32' ? 'where' : 'which';
91
- const result = (0, child_process_1.spawnSync)(checker, [command], { stdio: 'ignore' });
94
+ const result = exports.childProcess.spawnSync(checker, [command], { stdio: 'ignore' });
92
95
  return result.status === 0;
93
96
  }
94
97
  function loadToken(name) {
package/dist/daemon.js CHANGED
@@ -19,7 +19,7 @@ async function startDaemon(config) {
19
19
  api.setToken(token);
20
20
  (0, config_1.saveToken)(config.name, id, token);
21
21
  console.log(`Registered! Runner ID: ${id}`);
22
- console.log(`Token saved to ~/.config/flowy/runner-${config.name}.json`);
22
+ console.log(`Token saved to ~/.config/my-hub/runner-${config.name}.json`);
23
23
  }
24
24
  let executing = false;
25
25
  let killCurrent = null;
package/dist/executor.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCommand = buildCommand;
3
4
  exports.executeTask = executeTask;
4
5
  const child_process_1 = require("child_process");
5
6
  /** Build the CLI command + args for a given AI provider. */
@@ -13,6 +14,8 @@ function buildCommand(aiProvider, prompt) {
13
14
  args: ['exec', prompt, '--sandbox', 'workspace-write', '--color', 'never'],
14
15
  streamOutput: true,
15
16
  };
17
+ case 'cursor-agent':
18
+ return { cmd: 'agent', args: ['--print', '--force', prompt], streamOutput: true };
16
19
  default:
17
20
  throw new Error(`Unknown AI provider: ${aiProvider}`);
18
21
  }
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@frankleeeee/flowy-runner",
3
- "version": "1.0.0",
4
- "private": false,
3
+ "version": "1.0.1",
4
+ "description": "CLI daemon that connects to a Flowy hub and executes AI tasks locally",
5
5
  "bin": {
6
6
  "flowy-runner": "./dist/cli.js"
7
7
  },
8
+ "files": [
9
+ "dist/"
10
+ ],
8
11
  "scripts": {
9
12
  "dev": "ts-node-dev --respawn --transpile-only src/cli.ts --",
10
13
  "build": "tsc",
@@ -13,15 +16,17 @@
13
16
  "dependencies": {
14
17
  "axios": "^1.6.8"
15
18
  },
16
- "publishConfig": {
17
- "access": "public"
18
- },
19
- "engines": {
20
- "node": ">=23"
21
- },
22
19
  "devDependencies": {
23
20
  "@types/node": "^20.11.30",
24
21
  "ts-node-dev": "^2.0.0",
25
22
  "typescript": "^5.4.3"
26
- }
23
+ },
24
+ "engines": {
25
+ "node": ">=18"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/FrankLeeeee/Flowy"
30
+ },
31
+ "license": "MIT"
27
32
  }
package/src/api.ts DELETED
@@ -1,46 +0,0 @@
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 DELETED
@@ -1,11 +0,0 @@
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 DELETED
@@ -1,107 +0,0 @@
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 DELETED
@@ -1,122 +0,0 @@
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 DELETED
@@ -1,96 +0,0 @@
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 DELETED
@@ -1,40 +0,0 @@
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 DELETED
@@ -1,16 +0,0 @@
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
- }