mtech-ai-local-runner 0.1.1 → 0.1.3

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/src/cli.js CHANGED
@@ -184,7 +184,7 @@ export async function main(argv, env, io = {}) {
184
184
  const fetchFn = io.fetchFn || fetch;
185
185
  if (isServiceConfig(config)) {
186
186
  const localStore = new FileLocalConfigStore(config.stateRoot);
187
- const launchd = io.launchdService ?? new LaunchdService({ home: env.HOME ?? "", uid: process.getuid?.() ?? 0 });
187
+ const launchd = io.launchdService ?? new LaunchdService({ home: env.HOME ?? "", uid: process.getuid?.() ?? 0, path: env.PATH });
188
188
  try {
189
189
  if (config.mode === "setup") {
190
190
  if (process.platform !== "darwin")
@@ -203,7 +203,7 @@ export async function main(argv, env, io = {}) {
203
203
  const authenticated = await runFile(binary, ["auth", "status"], { encoding: "utf8" }).then(() => true).catch(() => false);
204
204
  const registered = await gateway.registerLocalRunner({
205
205
  deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${config.gatewayMcpUrl}`).digest("hex"),
206
- displayName: runnerDisplayName(), packageVersion: "0.1.1",
206
+ displayName: runnerDisplayName(), packageVersion: "0.1.3",
207
207
  capabilities: [{ adapter: "claude_code_cli", version: "1", capabilities: ["repository_read", "repository_edit", "shell_test", "git_diff"], authenticated }],
208
208
  });
209
209
  await localStore.save({ schemaVersion: 1, gatewayMcpUrl: config.gatewayMcpUrl, runnerId: registered.runner.id, workspaceRoot: config.workspaceRoot, concurrency: 1 });
@@ -10,6 +10,7 @@ export class JobPoller {
10
10
  if (this.stopped)
11
11
  return;
12
12
  try {
13
+ await this.deps.heartbeat?.();
13
14
  const claim = await this.deps.wait();
14
15
  this.failures = 0;
15
16
  if (claim.status === "claimed" && claim.job)
@@ -1,4 +1,4 @@
1
- import { mkdir, open, rm } from "node:fs/promises";
1
+ import { mkdir, open, rm, readFile, unlink } from "node:fs/promises";
2
2
  import { dirname } from "node:path";
3
3
  export class ProcessLock {
4
4
  path;
@@ -8,15 +8,41 @@ export class ProcessLock {
8
8
  }
9
9
  async acquire() {
10
10
  await mkdir(dirname(this.path), { recursive: true, mode: 0o700 });
11
+ const guard = `${this.path}.acquire`;
11
12
  try {
12
- this.handle = await open(this.path, "wx", 0o600);
13
+ await mkdir(guard, { mode: 0o700 });
13
14
  }
14
15
  catch (error) {
15
16
  if (error.code === "EEXIST")
16
17
  throw new Error("local runner daemon is already running");
17
18
  throw error;
18
19
  }
19
- await this.handle.writeFile(String(process.pid));
20
+ try {
21
+ try {
22
+ const stored = (await readFile(this.path, "utf8")).trim();
23
+ const pid = Number(stored);
24
+ if (!/^\d+$/.test(stored) || !Number.isSafeInteger(pid) || pid <= 0)
25
+ throw new Error("local runner daemon lock has an invalid owner");
26
+ try {
27
+ process.kill(pid, 0);
28
+ throw new Error("local runner daemon is already running");
29
+ }
30
+ catch (error) {
31
+ if (error.code !== "ESRCH")
32
+ throw error;
33
+ }
34
+ await unlink(this.path);
35
+ }
36
+ catch (error) {
37
+ if (error.code !== "ENOENT")
38
+ throw error;
39
+ }
40
+ this.handle = await open(this.path, "wx", 0o600);
41
+ await this.handle.writeFile(String(process.pid));
42
+ }
43
+ finally {
44
+ await rm(guard, { recursive: true });
45
+ }
20
46
  }
21
47
  async release() {
22
48
  await this.handle?.close();
@@ -1,6 +1,7 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  import { hostname } from "node:os";
4
+ import { createHash } from "node:crypto";
4
5
  import { join } from "node:path";
5
6
  import { mkdirSync, writeFileSync, chmodSync } from "node:fs";
6
7
  import { McpGatewayClient } from "../gateway-client.js";
@@ -72,6 +73,12 @@ export async function buildDaemon(input) {
72
73
  state,
73
74
  });
74
75
  const poller = new JobPoller({
76
+ heartbeat: () => gateway.heartbeatLocalRunner({
77
+ deviceFingerprint: createHash("sha256").update(`${process.getuid?.() ?? 0}|${runnerDisplayName()}|${input.gatewayMcpUrl}`).digest("hex"),
78
+ displayName: runnerDisplayName(), packageVersion: "0.1.3",
79
+ capabilities: probes.map((probe) => ({ adapter: probe.id, version: probe.version,
80
+ capabilities: probe.capabilities, authenticated: probe.available && probe.authenticated })),
81
+ }).then(() => undefined),
75
82
  wait: async () => {
76
83
  const claim = await gateway.waitForLocalJob(versions, 25);
77
84
  if (claim.status !== "claimed" || !claim.job || !claim.task_package || !claim.lease_token || !claim.launch_grant)
@@ -403,6 +403,17 @@ export class McpGatewayClient {
403
403
  capabilities: input.capabilities,
404
404
  });
405
405
  }
406
+ async heartbeatLocalRunner(input) {
407
+ return this.callTool("local_agent_runner_heartbeat", {
408
+ runner_id: this.runnerId,
409
+ runner: {
410
+ device_fingerprint: input.deviceFingerprint,
411
+ display_name: input.displayName,
412
+ package_version: input.packageVersion,
413
+ capabilities: input.capabilities,
414
+ },
415
+ });
416
+ }
406
417
  async localRunnerStatus() {
407
418
  return this.callTool("local_agent_runner_status", { runner_id: this.runnerId });
408
419
  }
@@ -30,7 +30,8 @@ export class LaunchdService {
30
30
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
31
31
  <plist version="1.0"><dict>
32
32
  <key>Label</key><string>${LAUNCHD_LABEL}</string>
33
- <key>ProgramArguments</key><array><string>${xml(executablePath)}</string><string>run</string><string>--state-root=${xml(stateRoot)}</string></array>
33
+ <key>ProgramArguments</key><array><string>${xml(process.execPath)}</string><string>${xml(executablePath)}</string><string>run</string><string>--state-root=${xml(stateRoot)}</string></array>
34
+ <key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(this.deps.path ?? process.env.PATH ?? "/usr/bin:/bin:/usr/sbin:/sbin")}</string></dict>
34
35
  <key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
35
36
  <key>ProcessType</key><string>Background</string>
36
37
  </dict></plist>
package/package.json CHANGED
@@ -1,12 +1,20 @@
1
1
  {
2
2
  "name": "mtech-ai-local-runner",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
- "bin": {"mtec-ai-local-runner": "dist/src/cli.js"},
7
- "files": ["dist/src", "README.md", "LICENSE"],
6
+ "bin": {
7
+ "mtec-ai-local-runner": "dist/src/cli.js"
8
+ },
9
+ "files": [
10
+ "dist/src",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
8
14
  "type": "module",
9
- "engines": {"node": ">=20"},
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
10
18
  "scripts": {
11
19
  "build": "tsc",
12
20
  "prepack": "npm run build && chmod 755 dist/src/cli.js",