@delegoapp/runner 0.1.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 +47 -0
- package/dist/bin.js +7 -0
- package/dist/config.js +159 -0
- package/dist/credentials-store.js +76 -0
- package/dist/execution-prefs.js +34 -0
- package/dist/executor/adapters/claude.js +124 -0
- package/dist/executor/adapters/codex.js +20 -0
- package/dist/executor/adapters/index.js +6 -0
- package/dist/executor/adapters/types.js +1 -0
- package/dist/executor/process.js +161 -0
- package/dist/executor/prompt.js +21 -0
- package/dist/git/command.js +33 -0
- package/dist/git/commit.js +82 -0
- package/dist/git/publish.js +70 -0
- package/dist/git/workspace.js +213 -0
- package/dist/index.js +6 -0
- package/dist/job-pipeline.js +164 -0
- package/dist/pairing.js +65 -0
- package/dist/relay-client.js +84 -0
- package/dist/run.js +143 -0
- package/dist/thinking.js +24 -0
- package/package.json +49 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export class RelayUnauthorizedError extends Error {
|
|
2
|
+
endpoint;
|
|
3
|
+
constructor(endpoint) {
|
|
4
|
+
super(`relay rejected request to ${endpoint} (HTTP 401)`);
|
|
5
|
+
this.endpoint = endpoint;
|
|
6
|
+
this.name = 'RelayUnauthorizedError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function postJson(config, pathname, body) {
|
|
10
|
+
const url = `${config.relayUrl}${pathname}`;
|
|
11
|
+
let response;
|
|
12
|
+
try {
|
|
13
|
+
response = await fetch(url, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
'content-type': 'application/json',
|
|
17
|
+
authorization: `Bearer ${config.bearer}`,
|
|
18
|
+
},
|
|
19
|
+
body: JSON.stringify(body),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
throw new Error(`Unable to reach relay endpoint ${url}: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
if (response.status === 401) {
|
|
27
|
+
throw new RelayUnauthorizedError(pathname);
|
|
28
|
+
}
|
|
29
|
+
const payload = (await response.json().catch(() => ({})));
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
throw new Error(payload.error ?? `Relay returned HTTP ${response.status}`);
|
|
32
|
+
}
|
|
33
|
+
return payload;
|
|
34
|
+
}
|
|
35
|
+
export async function postRunnerEvent(config, pathname, activeJobId = null) {
|
|
36
|
+
return postJson(config, pathname, {
|
|
37
|
+
version: config.version,
|
|
38
|
+
supportedExecutors: config.supportedExecutors,
|
|
39
|
+
activeJobId,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function describeRunnerResponse(action, response) {
|
|
43
|
+
if (!response.runner) {
|
|
44
|
+
return `${action}: relay accepted request`;
|
|
45
|
+
}
|
|
46
|
+
const heartbeat = response.runner.lastHeartbeatAt ?? 'not recorded';
|
|
47
|
+
return `${action}: ${response.runner.id} is ${response.runner.status}, heartbeat ${heartbeat}`;
|
|
48
|
+
}
|
|
49
|
+
export async function reportProgress(config, job, summary, metadata = {}) {
|
|
50
|
+
await postJson(config, '/api/runner/jobs/progress', {
|
|
51
|
+
jobId: job.id,
|
|
52
|
+
organizationId: job.organizationId,
|
|
53
|
+
summary,
|
|
54
|
+
metadata: {
|
|
55
|
+
attemptId: job.attemptId,
|
|
56
|
+
...metadata,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
export async function reportCompletion(config, job, summary, metadata) {
|
|
61
|
+
await postJson(config, '/api/runner/jobs/complete', {
|
|
62
|
+
jobId: job.id,
|
|
63
|
+
organizationId: job.organizationId,
|
|
64
|
+
agentSessionId: job.agentSessionId,
|
|
65
|
+
summary,
|
|
66
|
+
metadata,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
export async function reportFailure(config, job, summary, metadata, cancelled = false) {
|
|
70
|
+
await postJson(config, '/api/runner/jobs/fail', {
|
|
71
|
+
jobId: job.id,
|
|
72
|
+
organizationId: job.organizationId,
|
|
73
|
+
agentSessionId: job.agentSessionId,
|
|
74
|
+
summary,
|
|
75
|
+
cancelled,
|
|
76
|
+
metadata,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function pollCancellation(config, job) {
|
|
80
|
+
const status = await postJson(config, '/api/runner/jobs/status', {
|
|
81
|
+
jobId: job.id,
|
|
82
|
+
});
|
|
83
|
+
return Boolean(status.job?.cancellationRequested);
|
|
84
|
+
}
|
package/dist/run.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { readConfig, usageMessage, } from './config.js';
|
|
2
|
+
import { getProfile, readStore, resolveCredentialsPath, } from './credentials-store.js';
|
|
3
|
+
import { runMockExecutionOnce, runRealExecutionOnce } from './job-pipeline.js';
|
|
4
|
+
import { pairAndStore } from './pairing.js';
|
|
5
|
+
import { describeRunnerResponse, postRunnerEvent, RelayUnauthorizedError, } from './relay-client.js';
|
|
6
|
+
function sleep(ms) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
setTimeout(resolve, ms);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export async function resolveRunnerConfig(input, env = process.env) {
|
|
12
|
+
const credentialsPath = resolveCredentialsPath(env);
|
|
13
|
+
if (input.pairingToken) {
|
|
14
|
+
if (!input.relayUrl) {
|
|
15
|
+
throw new Error('--relay-url is required when --pairing-token is supplied');
|
|
16
|
+
}
|
|
17
|
+
const result = await pairAndStore({
|
|
18
|
+
relayUrl: input.relayUrl,
|
|
19
|
+
pairingToken: input.pairingToken,
|
|
20
|
+
profileName: input.profileName,
|
|
21
|
+
version: input.version,
|
|
22
|
+
supportedExecutors: input.supportedExecutors,
|
|
23
|
+
credentialsPath,
|
|
24
|
+
});
|
|
25
|
+
if (result.orphanedRunnerId && !input.force) {
|
|
26
|
+
console.warn(`delego-runner: overwrote profile ${input.profileName}. ` +
|
|
27
|
+
`Old runner ${result.orphanedRunnerId} is still in the dashboard; remove it manually if unwanted. ` +
|
|
28
|
+
`Pass --force to suppress this warning.`);
|
|
29
|
+
}
|
|
30
|
+
return buildRunnerConfig(input, result.relayUrl, result.bearer, result.runnerId);
|
|
31
|
+
}
|
|
32
|
+
const store = readStore(credentialsPath);
|
|
33
|
+
const profile = getProfile(store, input.profileName);
|
|
34
|
+
if (!profile) {
|
|
35
|
+
throw new Error(`delego-runner: profile ${JSON.stringify(input.profileName)} not paired. ` +
|
|
36
|
+
`Re-run with --pairing-token <drs_pair_...> from the dashboard to pair it.`);
|
|
37
|
+
}
|
|
38
|
+
if (input.relayUrl && input.relayUrl !== profile.relayUrl) {
|
|
39
|
+
throw new Error(`delego-runner: --relay-url (${input.relayUrl}) does not match the relay this profile was paired against (${profile.relayUrl}). ` +
|
|
40
|
+
`Use --pairing-token to re-pair against the new relay, or pass the matching --relay-url.`);
|
|
41
|
+
}
|
|
42
|
+
return buildRunnerConfig(input, profile.relayUrl, profile.bearer, profile.runnerId);
|
|
43
|
+
}
|
|
44
|
+
function buildRunnerConfig(input, relayUrl, bearer, runnerId) {
|
|
45
|
+
return {
|
|
46
|
+
relayUrl,
|
|
47
|
+
bearer,
|
|
48
|
+
runnerId,
|
|
49
|
+
profileName: input.profileName,
|
|
50
|
+
once: input.once,
|
|
51
|
+
mockExecuteOnce: input.mockExecuteOnce,
|
|
52
|
+
heartbeatIntervalMs: input.heartbeatIntervalMs,
|
|
53
|
+
pollIntervalMs: input.pollIntervalMs,
|
|
54
|
+
cancellationPollIntervalMs: input.cancellationPollIntervalMs,
|
|
55
|
+
workspaceRoot: input.workspaceRoot,
|
|
56
|
+
gitCloneBaseUrl: input.gitCloneBaseUrl,
|
|
57
|
+
executors: input.executors,
|
|
58
|
+
executorTimeoutMs: input.executorTimeoutMs,
|
|
59
|
+
createCommit: input.createCommit,
|
|
60
|
+
version: input.version,
|
|
61
|
+
supportedExecutors: input.supportedExecutors,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function handleUnauthorized(error, config, profileName) {
|
|
65
|
+
console.error(`delego-runner: runner credential rejected by relay (HTTP 401 on ${error.endpoint}). ` +
|
|
66
|
+
`The runner may have been rotated or removed in the dashboard. ` +
|
|
67
|
+
`Re-pair with --pairing-token to recover.\n` +
|
|
68
|
+
`Profile: ${profileName} · Runner: ${config.runnerId} · Relay: ${config.relayUrl}`);
|
|
69
|
+
process.exit(2);
|
|
70
|
+
}
|
|
71
|
+
export async function run(args = process.argv.slice(2)) {
|
|
72
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
73
|
+
console.log(usageMessage());
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const input = readConfig(args);
|
|
77
|
+
const config = await resolveRunnerConfig(input);
|
|
78
|
+
// In v0.3, /api/runner/register is the *pairing* endpoint and only accepts a
|
|
79
|
+
// pairing-token body. Pairing happens once inside resolveRunnerConfig (when
|
|
80
|
+
// --pairing-token is supplied); after that the runner just heartbeats.
|
|
81
|
+
try {
|
|
82
|
+
const heartbeat = await postRunnerEvent(config, '/api/runner/heartbeat');
|
|
83
|
+
console.log(describeRunnerResponse('heartbeat', heartbeat));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error instanceof RelayUnauthorizedError) {
|
|
87
|
+
handleUnauthorized(error, config, input.profileName);
|
|
88
|
+
}
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
if (config.mockExecuteOnce) {
|
|
92
|
+
await runMockExecutionOnce(config);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const heartbeatTimer = startHeartbeatLoop(config, input.profileName);
|
|
96
|
+
try {
|
|
97
|
+
do {
|
|
98
|
+
let handled = false;
|
|
99
|
+
try {
|
|
100
|
+
handled = await runRealExecutionOnce(config);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error instanceof RelayUnauthorizedError) {
|
|
104
|
+
handleUnauthorized(error, config, input.profileName);
|
|
105
|
+
}
|
|
106
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
+
console.warn(`poll cycle failed: ${message}`);
|
|
108
|
+
if (config.once) {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (config.once) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await sleep(handled ? 1_000 : config.pollIntervalMs);
|
|
116
|
+
// oxlint-disable-next-line no-constant-condition
|
|
117
|
+
} while (true);
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearInterval(heartbeatTimer);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function startHeartbeatLoop(config, profileName) {
|
|
124
|
+
let inFlight = false;
|
|
125
|
+
return setInterval(async () => {
|
|
126
|
+
if (inFlight)
|
|
127
|
+
return;
|
|
128
|
+
inFlight = true;
|
|
129
|
+
try {
|
|
130
|
+
const response = await postRunnerEvent(config, '/api/runner/heartbeat', null);
|
|
131
|
+
console.log(describeRunnerResponse('heartbeat', response));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error instanceof RelayUnauthorizedError) {
|
|
135
|
+
handleUnauthorized(error, config, profileName);
|
|
136
|
+
}
|
|
137
|
+
console.warn(`heartbeat failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
inFlight = false;
|
|
141
|
+
}
|
|
142
|
+
}, config.heartbeatIntervalMs);
|
|
143
|
+
}
|
package/dist/thinking.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function normalizeThinkingPreference(value) {
|
|
2
|
+
if (typeof value !== 'string') {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
const normalized = value.trim().toLowerCase();
|
|
6
|
+
if (normalized === 'low' ||
|
|
7
|
+
normalized === 'medium' ||
|
|
8
|
+
normalized === 'high') {
|
|
9
|
+
return normalized;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function promptThinkingInstruction(thinking) {
|
|
14
|
+
if (thinking === 'low') {
|
|
15
|
+
return 'Thinking level: low. Prefer a direct approach and minimize exploration unless required.';
|
|
16
|
+
}
|
|
17
|
+
if (thinking === 'medium') {
|
|
18
|
+
return 'Thinking level: medium. Balance speed with enough reasoning to validate the implementation.';
|
|
19
|
+
}
|
|
20
|
+
if (thinking === 'high') {
|
|
21
|
+
return 'Thinking level: high. Spend extra effort on validation, edge cases, and correctness before finishing.';
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@delegoapp/runner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Delego runner — polls the Delego relay and executes Linear-delegated coding jobs via Codex CLI or Claude Code.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent",
|
|
8
|
+
"claude",
|
|
9
|
+
"codex",
|
|
10
|
+
"delego",
|
|
11
|
+
"linear",
|
|
12
|
+
"runner"
|
|
13
|
+
],
|
|
14
|
+
"license": "UNLICENSED",
|
|
15
|
+
"bin": {
|
|
16
|
+
"delego-runner": "dist/bin.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"imports": {
|
|
24
|
+
"#*.js": {
|
|
25
|
+
"types": "./src/*.ts",
|
|
26
|
+
"source": "./src/*.ts",
|
|
27
|
+
"default": "./dist/*.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"typescript": "^6.0.3",
|
|
35
|
+
"vitest": "^4.1.5",
|
|
36
|
+
"@kit/tsconfig": "0.1.0"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=20"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json && node scripts/add-shebang.mjs",
|
|
43
|
+
"prestart": "pnpm build",
|
|
44
|
+
"start": "node dist/index.js",
|
|
45
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"clean": "rm -rf .turbo dist node_modules"
|
|
48
|
+
}
|
|
49
|
+
}
|