tesseract-agent 0.1.6 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +44 -14
  2. package/dist-bundle/cli.js +452 -121
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # tesseract-agent
2
2
 
3
3
  Run your **tesseract** agents on a remote machine. Install this on a headless
4
- Linux server (or in Docker) to make that box available to your tesseract account —
5
- your laptop, phone, or the web app can then run agents on it, even with no GUI and
6
- even when nothing else is open.
4
+ Linux/macOS/Windows box (or in Docker) to make that machine available to your
5
+ tesseract account — your laptop, phone, or the web app can then run agents on it,
6
+ even with no GUI and even when nothing else is open.
7
7
 
8
8
  It's the headless counterpart to the desktop app's "share this machine" hosting:
9
9
  the same agent runtime, no Electron.
@@ -14,29 +14,59 @@ the same agent runtime, no Electron.
14
14
  npm i -g tesseract-agent
15
15
  ```
16
16
 
17
- Requires Node 20+. Hosting is a paid-plan feature — a Free account will be denied
18
- and the daemon backs off.
17
+ Requires Node 20+. Hosting is a paid-plan feature — a Free account is denied and
18
+ the daemon backs off gracefully.
19
19
 
20
20
  ## Use
21
21
 
22
22
  ```bash
23
- tesseract-agent login # sign in + provision this machine (device token)
24
- tesseract-agent install # run as a background service (systemd; add --system for a root unit)
23
+ tesseract-agent login # browser sign-in (Google/email) → paste a one-time code
24
+ tesseract-agent install # run as a background service (launchd/systemd/Task Scheduler)
25
25
  tesseract-agent status # is it running / connected?
26
26
  tesseract-agent logs # tail the service logs
27
- tesseract-agent logout # revoke this device + remove the service
27
+ tesseract-agent logout # revoke this device + wipe local state
28
28
  ```
29
29
 
30
- `login` takes `--token <jwt>` (or interactive email/password — set
31
- `CCB_LOGIN_PASSWORD` to script it), plus `--api` / `--relay` to point at a
32
- self-hosted backend (default: `https://tesseract.fineguide.ai`).
30
+ `login` prints a URL (and a scannable QR) to `https://tesseract.fineguide.ai/?authorize=…`;
31
+ sign in there, then paste the one-time code back in the terminal. For automation,
32
+ `--token <device-JWT>` skips the browser. `--api` / `--relay` point at a self-hosted
33
+ backend (default `https://tesseract.fineguide.ai`).
33
34
 
34
- ### Docker / no systemd
35
+ The service auto-starts at login/boot and restarts on crash: **launchd** LaunchAgent
36
+ on macOS, **systemd `--user`** unit on Linux (with `loginctl enable-linger` so it
37
+ survives logout), **Task Scheduler** ONLOGON task on Windows. Run `install` as your
38
+ normal user — not `sudo` (a per-user service needs your own session, and finds your
39
+ credentials + `~/.claude`).
40
+
41
+ ## Multiple agents under one user
42
+
43
+ By default there's one agent per OS user. To run a **separate** agent under the same
44
+ user, give it an instance id — it gets its own state dir, device, and service:
35
45
 
36
46
  ```bash
37
- tesseract-agent login --token "$TESSERACT_TOKEN"
38
- tesseract-agent run # run the daemon in the foreground
47
+ tesseract-agent login --instance work2
48
+ tesseract-agent install --instance work2
49
+ tesseract-agent status --instance work2
39
50
  ```
40
51
 
52
+ `install` warns and asks for confirmation if a daemon is already running for the
53
+ target instance (pass `--yes` to skip the prompt in scripts). Different OS users are
54
+ always isolated — no instance id needed.
55
+
56
+ ## Docker / no init system
57
+
58
+ A container has no launchd/systemd, so skip `install` and run the daemon as the
59
+ entrypoint:
60
+
61
+ ```bash
62
+ docker run --rm \
63
+ -e TESSERACT_TOKEN \
64
+ tesseract-agent \
65
+ sh -c 'tesseract-agent login --token "$TESSERACT_TOKEN" && tesseract-agent run'
66
+ ```
67
+
68
+ Or pre-provision credentials and mount them (persist `~/.tesseract` or set
69
+ `CCB_DAEMON_DIR` to a mounted volume), then just `tesseract-agent run`.
70
+
41
71
  The agent runs under the machine's own Claude credentials — a `claude` login in
42
72
  `~/.claude`, or an `ANTHROPIC_API_KEY` in the environment.
@@ -5841,7 +5841,9 @@ var require_main = __commonJS({
5841
5841
 
5842
5842
  // src/cli.ts
5843
5843
  import { fileURLToPath as fileURLToPath2 } from "node:url";
5844
- import { platform as platform2 } from "node:os";
5844
+ import { homedir as homedir6 } from "node:os";
5845
+ import { join as join12 } from "node:path";
5846
+ import { createInterface } from "node:readline/promises";
5845
5847
 
5846
5848
  // ../../packages/host-core/dist/control-link.js
5847
5849
  import { hostname, homedir } from "node:os";
@@ -6293,6 +6295,38 @@ function unwatchAllRemote() {
6293
6295
  remoteWatches.clear();
6294
6296
  }
6295
6297
 
6298
+ // ../../packages/host-core/dist/host-metrics.js
6299
+ import os from "node:os";
6300
+ function primaryIpv4(ifaces = os.networkInterfaces()) {
6301
+ for (const name of Object.keys(ifaces)) {
6302
+ for (const ni of ifaces[name] ?? []) {
6303
+ if (ni.family === "IPv4" && !ni.internal)
6304
+ return ni.address;
6305
+ }
6306
+ }
6307
+ return void 0;
6308
+ }
6309
+ function staticMetrics(version) {
6310
+ const cpus = os.cpus();
6311
+ return {
6312
+ version: version || void 0,
6313
+ arch: os.arch(),
6314
+ cpuModel: cpus[0]?.model?.trim() || void 0,
6315
+ cpuCount: cpus.length || void 0,
6316
+ totalMem: os.totalmem() || void 0,
6317
+ ip: primaryIpv4()
6318
+ };
6319
+ }
6320
+ function dynamicMetrics() {
6321
+ const la = os.loadavg();
6322
+ return {
6323
+ load: la[0] > 0 ? Number(la[0].toFixed(2)) : void 0,
6324
+ // loadavg is [0,0,0] on Windows
6325
+ freeMem: os.freemem() || void 0,
6326
+ uptime: Math.round(os.uptime()) || void 0
6327
+ };
6328
+ }
6329
+
6296
6330
  // ../../packages/host-core/dist/fs-transfer.js
6297
6331
  import { readdir as readdir2, lstat, mkdir as mkdir3, chmod, rm as rm2, readlink, symlink, stat as stat2 } from "node:fs/promises";
6298
6332
  import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, existsSync as existsSync3, createWriteStream } from "node:fs";
@@ -6679,6 +6713,7 @@ var SERVER_HOST_ID = "server";
6679
6713
  var cfg = null;
6680
6714
  var deviceId = "";
6681
6715
  var backendOrigin = "";
6716
+ var hostVersion;
6682
6717
  function getToken() {
6683
6718
  return cfg ? cfg.tokenSource.getToken() : null;
6684
6719
  }
@@ -6748,7 +6783,17 @@ function ownHostId() {
6748
6783
  function presentedHosts() {
6749
6784
  const out = serverHosts.slice();
6750
6785
  if (!out.some((h) => h.hostId.endsWith(`:${deviceId}`))) {
6751
- out.unshift({ hostId: "local", name: hostname(), kind: "electron", platform: process.platform, online: true, caps: ["agent", "fs"] });
6786
+ out.unshift({
6787
+ hostId: "local",
6788
+ name: hostname(),
6789
+ kind: "electron",
6790
+ platform: process.platform,
6791
+ online: true,
6792
+ caps: ["agent", "fs"],
6793
+ ...staticMetrics(hostVersion),
6794
+ ...dynamicMetrics(),
6795
+ lastSeen: Date.now()
6796
+ });
6752
6797
  }
6753
6798
  return out;
6754
6799
  }
@@ -6971,9 +7016,9 @@ function stopHeartbeat() {
6971
7016
  }
6972
7017
  }
6973
7018
  function registerAsHost() {
6974
- send("host", { type: "host:register", name: hostname(), platform: process.platform, deviceId, caps: ["agent", "fs"] });
7019
+ send("host", { type: "host:register", name: hostname(), platform: process.platform, deviceId, caps: ["agent", "fs"], ...staticMetrics(hostVersion) });
6975
7020
  if (!heartbeat)
6976
- heartbeat = setInterval(() => send("host", { type: "host:heartbeat" }), 2e4);
7021
+ heartbeat = setInterval(() => send("host", { type: "host:heartbeat", ...dynamicMetrics() }), 2e4);
6977
7022
  }
6978
7023
  function cleanup() {
6979
7024
  stopHeartbeat();
@@ -7104,6 +7149,7 @@ function startControlLink(config, h) {
7104
7149
  cfg = config;
7105
7150
  deviceId = config.deviceId;
7106
7151
  backendOrigin = config.tokenSource.backendOrigin;
7152
+ hostVersion = config.version;
7107
7153
  hooks = h;
7108
7154
  stopped = false;
7109
7155
  hooks.pushHosts(presentedHosts());
@@ -7395,7 +7441,7 @@ async function runDaemon(opts = {}) {
7395
7441
  const log2 = (...args) => console.log(`[${TAG} ${(/* @__PURE__ */ new Date()).toISOString()}]`, ...args);
7396
7442
  const pidfile = () => join6(daemonDir(), "daemon.lock");
7397
7443
  const statusFile = () => join6(daemonDir(), "status.json");
7398
- const isAlive = (pid) => {
7444
+ const isAlive2 = (pid) => {
7399
7445
  try {
7400
7446
  process.kill(pid, 0);
7401
7447
  return true;
@@ -7408,7 +7454,7 @@ async function runDaemon(opts = {}) {
7408
7454
  try {
7409
7455
  if (existsSync5(f)) {
7410
7456
  const prev = Number(readFileSync2(f, "utf-8").trim());
7411
- if (prev && prev !== process.pid && isAlive(prev))
7457
+ if (prev && prev !== process.pid && isAlive2(prev))
7412
7458
  return false;
7413
7459
  }
7414
7460
  } catch {
@@ -7517,7 +7563,7 @@ async function runDaemon(opts = {}) {
7517
7563
  log2("local listener failed to start", e.message);
7518
7564
  }
7519
7565
  writeStatus({ deviceId: c.deviceId, connected: false });
7520
- startControlLink({ deviceId: c.deviceId, power: createNodePowerController(), tokenSource }, hooks2);
7566
+ startControlLink({ deviceId: c.deviceId, power: createNodePowerController(), tokenSource, version: VERSION }, hooks2);
7521
7567
  log2(`started \u2014 device ${c.deviceId}, user ${c.userId}, relay ${c.backendOrigin}, local :${localInfo?.localPort ?? "off"}, version ${VERSION}`);
7522
7568
  await refresh();
7523
7569
  timer = setInterval(() => {
@@ -7527,11 +7573,295 @@ async function runDaemon(opts = {}) {
7527
7573
  process.on("SIGINT", () => shutdown(0));
7528
7574
  }
7529
7575
 
7576
+ // ../../packages/host-core/dist/service/index.js
7577
+ import { platform as platform2 } from "node:os";
7578
+
7579
+ // ../../packages/host-core/dist/service/launchd.js
7580
+ import { writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
7581
+ import { dirname as dirname5, join as join7 } from "node:path";
7582
+ import { homedir as homedir4, userInfo } from "node:os";
7583
+
7584
+ // ../../packages/host-core/dist/service/exec.js
7585
+ import { execFile, spawn as spawn3 } from "node:child_process";
7586
+ import { promisify } from "node:util";
7587
+ var run = promisify(execFile);
7588
+ function runInherit(cmd, args) {
7589
+ return new Promise((resolve3) => {
7590
+ const p = spawn3(cmd, args, { stdio: "inherit" });
7591
+ p.on("error", () => resolve3());
7592
+ p.on("close", () => resolve3());
7593
+ });
7594
+ }
7595
+
7596
+ // ../../packages/host-core/dist/service/types.js
7597
+ var SERVICE_LABEL_BASE = "ai.fineguide.tesseract.host";
7598
+
7599
+ // ../../packages/host-core/dist/service/launchd.js
7600
+ function xml(s) {
7601
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7602
+ }
7603
+ function makeLaunchd(instanceId) {
7604
+ const label = SERVICE_LABEL_BASE + (instanceId ? `.${instanceId}` : "");
7605
+ const plistPath = () => join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
7606
+ const domainTarget = () => `gui/${userInfo().uid}`;
7607
+ const serviceTarget = () => `${domainTarget()}/${label}`;
7608
+ const logFile = () => join7(daemonDir(), "daemon.log");
7609
+ function buildPlist(spec) {
7610
+ const args = [spec.exec, ...spec.args].map((a) => ` <string>${xml(a)}</string>`).join("\n");
7611
+ const env = Object.entries(spec.env).map(([k, v]) => ` <key>${xml(k)}</key>
7612
+ <string>${xml(v)}</string>`).join("\n");
7613
+ return `<?xml version="1.0" encoding="UTF-8"?>
7614
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7615
+ <plist version="1.0">
7616
+ <dict>
7617
+ <key>Label</key>
7618
+ <string>${label}</string>
7619
+ <key>ProgramArguments</key>
7620
+ <array>
7621
+ ${args}
7622
+ </array>
7623
+ <key>EnvironmentVariables</key>
7624
+ <dict>
7625
+ ${env}
7626
+ </dict>
7627
+ <key>RunAtLoad</key>
7628
+ <true/>
7629
+ <key>KeepAlive</key>
7630
+ <dict>
7631
+ <key>SuccessfulExit</key>
7632
+ <false/>
7633
+ </dict>
7634
+ <key>ThrottleInterval</key>
7635
+ <integer>30</integer>
7636
+ <key>ProcessType</key>
7637
+ <string>Background</string>
7638
+ <key>StandardOutPath</key>
7639
+ <string>${xml(spec.logPath)}</string>
7640
+ <key>StandardErrorPath</key>
7641
+ <string>${xml(spec.logPath)}</string>
7642
+ </dict>
7643
+ </plist>
7644
+ `;
7645
+ }
7646
+ return {
7647
+ async install(spec) {
7648
+ mkdirSync3(dirname5(plistPath()), { recursive: true });
7649
+ mkdirSync3(dirname5(spec.logPath), { recursive: true });
7650
+ writeFileSync3(plistPath(), buildPlist(spec));
7651
+ await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
7652
+ });
7653
+ await run("launchctl", ["bootstrap", domainTarget(), plistPath()]);
7654
+ await run("launchctl", ["enable", serviceTarget()]).catch(() => {
7655
+ });
7656
+ await run("launchctl", ["kickstart", "-k", serviceTarget()]).catch(() => {
7657
+ });
7658
+ },
7659
+ async uninstall() {
7660
+ await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
7661
+ });
7662
+ if (existsSync6(plistPath())) {
7663
+ try {
7664
+ unlinkSync2(plistPath());
7665
+ } catch {
7666
+ }
7667
+ }
7668
+ },
7669
+ async restart(spec) {
7670
+ await this.install(spec);
7671
+ },
7672
+ async isInstalled() {
7673
+ return existsSync6(plistPath());
7674
+ },
7675
+ async start() {
7676
+ await run("launchctl", ["bootstrap", domainTarget(), plistPath()]).catch(() => {
7677
+ });
7678
+ await run("launchctl", ["kickstart", serviceTarget()]).catch(() => {
7679
+ });
7680
+ },
7681
+ async stop() {
7682
+ await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
7683
+ });
7684
+ },
7685
+ async status() {
7686
+ await runInherit("launchctl", ["print", serviceTarget()]);
7687
+ },
7688
+ async logs(follow) {
7689
+ const f = logFile();
7690
+ if (!existsSync6(f)) {
7691
+ console.log(`No log yet at ${f} \u2014 has the service started?`);
7692
+ return;
7693
+ }
7694
+ await runInherit("tail", follow ? ["-f", f] : ["-n", "200", f]);
7695
+ }
7696
+ };
7697
+ }
7698
+
7699
+ // ../../packages/host-core/dist/service/systemd.js
7700
+ import { writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3 } from "node:fs";
7701
+ import { dirname as dirname6, join as join8 } from "node:path";
7702
+ import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
7703
+ function shq(s) {
7704
+ return `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
7705
+ }
7706
+ function makeSystemd(instanceId) {
7707
+ const unit = `tesseract-host${instanceId ? `-${instanceId}` : ""}.service`;
7708
+ const unitPath = () => {
7709
+ const base = process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config");
7710
+ return join8(base, "systemd", "user", unit);
7711
+ };
7712
+ function buildUnit(spec) {
7713
+ const execStart = [spec.exec, ...spec.args].map(shq).join(" ");
7714
+ const env = Object.entries(spec.env).map(([k, v]) => `Environment=${k}=${v}`).join("\n");
7715
+ return `[Unit]
7716
+ Description=Fineguide Tesseract host daemon${instanceId ? ` (${instanceId})` : ""}
7717
+ After=network-online.target
7718
+ Wants=network-online.target
7719
+
7720
+ [Service]
7721
+ Type=simple
7722
+ ${env}
7723
+ ExecStart=${execStart}
7724
+ Restart=on-failure
7725
+ RestartSec=30
7726
+ StandardOutput=append:${spec.logPath}
7727
+ StandardError=append:${spec.logPath}
7728
+
7729
+ [Install]
7730
+ WantedBy=default.target
7731
+ `;
7732
+ }
7733
+ return {
7734
+ async install(spec) {
7735
+ mkdirSync4(dirname6(unitPath()), { recursive: true });
7736
+ mkdirSync4(dirname6(spec.logPath), { recursive: true });
7737
+ writeFileSync4(unitPath(), buildUnit(spec));
7738
+ await run("loginctl", ["enable-linger", userInfo2().username]).catch(() => {
7739
+ });
7740
+ await run("systemctl", ["--user", "daemon-reload"]).catch(() => {
7741
+ });
7742
+ await run("systemctl", ["--user", "enable", unit]).catch(() => {
7743
+ });
7744
+ await run("systemctl", ["--user", "restart", unit]);
7745
+ },
7746
+ async uninstall() {
7747
+ await run("systemctl", ["--user", "disable", "--now", unit]).catch(() => {
7748
+ });
7749
+ if (existsSync7(unitPath())) {
7750
+ try {
7751
+ unlinkSync3(unitPath());
7752
+ } catch {
7753
+ }
7754
+ }
7755
+ await run("systemctl", ["--user", "daemon-reload"]).catch(() => {
7756
+ });
7757
+ },
7758
+ async restart(spec) {
7759
+ await this.install(spec);
7760
+ },
7761
+ async isInstalled() {
7762
+ return existsSync7(unitPath());
7763
+ },
7764
+ async start() {
7765
+ await run("systemctl", ["--user", "start", unit]);
7766
+ },
7767
+ async stop() {
7768
+ await run("systemctl", ["--user", "stop", unit]);
7769
+ },
7770
+ async status() {
7771
+ await runInherit("systemctl", ["--user", "status", unit, "--no-pager"]);
7772
+ },
7773
+ async logs(follow) {
7774
+ await runInherit("journalctl", ["--user-unit", unit, "-n", "200", ...follow ? ["-f"] : ["--no-pager"]]);
7775
+ }
7776
+ };
7777
+ }
7778
+
7779
+ // ../../packages/host-core/dist/service/windows.js
7780
+ import { writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
7781
+ import { dirname as dirname7, join as join9 } from "node:path";
7782
+ function makeWindowsTask(instanceId) {
7783
+ const task = `Fineguide\\TesseractHost${instanceId ? `-${instanceId}` : ""}`;
7784
+ const wrapperPath = () => join9(daemonDir(), `run-host${instanceId ? `-${instanceId}` : ""}.cmd`);
7785
+ const logFile = () => join9(daemonDir(), "daemon.log");
7786
+ function buildWrapper(spec) {
7787
+ const env = Object.entries(spec.env).map(([k, v]) => `set "${k}=${v}"`).join("\r\n");
7788
+ const cmd = [spec.exec, ...spec.args].map((a) => `"${a}"`).join(" ");
7789
+ return `@echo off\r
7790
+ ${env}\r
7791
+ start "" /b ${cmd} >> "${spec.logPath}" 2>&1\r
7792
+ `;
7793
+ }
7794
+ return {
7795
+ async install(spec) {
7796
+ mkdirSync5(dirname7(spec.logPath), { recursive: true });
7797
+ mkdirSync5(dirname7(wrapperPath()), { recursive: true });
7798
+ writeFileSync5(wrapperPath(), buildWrapper(spec));
7799
+ await run("schtasks", ["/Create", "/TN", task, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", `"${wrapperPath()}"`]);
7800
+ await run("schtasks", ["/Run", "/TN", task]).catch(() => {
7801
+ });
7802
+ },
7803
+ async uninstall() {
7804
+ await run("schtasks", ["/Delete", "/TN", task, "/F"]).catch(() => {
7805
+ });
7806
+ if (existsSync8(wrapperPath())) {
7807
+ try {
7808
+ unlinkSync4(wrapperPath());
7809
+ } catch {
7810
+ }
7811
+ }
7812
+ },
7813
+ async restart(spec) {
7814
+ await this.install(spec);
7815
+ },
7816
+ async isInstalled() {
7817
+ try {
7818
+ await run("schtasks", ["/Query", "/TN", task]);
7819
+ return true;
7820
+ } catch {
7821
+ return false;
7822
+ }
7823
+ },
7824
+ async start() {
7825
+ await run("schtasks", ["/Run", "/TN", task]);
7826
+ },
7827
+ async stop() {
7828
+ await run("schtasks", ["/End", "/TN", task]).catch(() => {
7829
+ });
7830
+ },
7831
+ async status() {
7832
+ await runInherit("schtasks", ["/Query", "/TN", task, "/V", "/FO", "LIST"]);
7833
+ },
7834
+ async logs(follow) {
7835
+ const f = logFile();
7836
+ if (!existsSync8(f)) {
7837
+ console.log(`No log yet at ${f} \u2014 has the task run?`);
7838
+ return;
7839
+ }
7840
+ await runInherit("powershell", ["-NoProfile", "-Command", `Get-Content -Path "${f}" -Tail 200${follow ? " -Wait" : ""}`]);
7841
+ }
7842
+ };
7843
+ }
7844
+
7845
+ // ../../packages/host-core/dist/service/index.js
7846
+ function getServiceController(instanceId) {
7847
+ const id = instanceId && instanceId.length ? instanceId : void 0;
7848
+ switch (platform2()) {
7849
+ case "darwin":
7850
+ return makeLaunchd(id);
7851
+ case "linux":
7852
+ return makeSystemd(id);
7853
+ case "win32":
7854
+ return makeWindowsTask(id);
7855
+ default:
7856
+ return null;
7857
+ }
7858
+ }
7859
+
7530
7860
  // src/auth.ts
7531
7861
  var import_qrcode_terminal = __toESM(require_main(), 1);
7532
7862
  import { randomUUID as randomUUID2 } from "node:crypto";
7533
- import { existsSync as existsSync6, rmSync } from "node:fs";
7534
- import { join as join7 } from "node:path";
7863
+ import { existsSync as existsSync9, rmSync } from "node:fs";
7864
+ import { join as join10 } from "node:path";
7535
7865
 
7536
7866
  // src/config.ts
7537
7867
  import { hostname as hostname2 } from "node:os";
@@ -7708,8 +8038,8 @@ async function logout() {
7708
8038
  }
7709
8039
  for (const f of ["credentials.json", "status.json", "daemon.lock"]) {
7710
8040
  try {
7711
- const p = join7(daemonDir(), f);
7712
- if (existsSync6(p)) rmSync(p);
8041
+ const p = join10(daemonDir(), f);
8042
+ if (existsSync9(p)) rmSync(p);
7713
8043
  } catch {
7714
8044
  }
7715
8045
  }
@@ -7728,109 +8058,72 @@ function whoami() {
7728
8058
  console.log(`expires: ${c.expiresAt}`);
7729
8059
  }
7730
8060
 
7731
- // src/systemd.ts
7732
- import { writeFileSync as writeFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
7733
- import { dirname as dirname5, join as join8 } from "node:path";
7734
- import { homedir as homedir4, userInfo } from "node:os";
7735
- import { execFileSync, spawnSync } from "node:child_process";
7736
- var UNIT = "tesseract-host.service";
7737
- function unitPath(system) {
7738
- if (system) return join8("/etc/systemd/system", UNIT);
7739
- const base = process.env.XDG_CONFIG_HOME || join8(homedir4(), ".config");
7740
- return join8(base, "systemd", "user", UNIT);
7741
- }
7742
- function shq(s) {
7743
- return `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
7744
- }
7745
- function sc(system, args) {
7746
- return system ? args : ["--user", ...args];
7747
- }
7748
- function buildUnit(o) {
7749
- const execStart = `${shq(o.node)} ${shq(o.cliEntry)} run`;
7750
- const env = [
7751
- `Environment=CCB_DAEMON_DIR=${o.daemonDir}`,
7752
- `Environment=CCB_DAEMON_VERSION=${o.version}`
7753
- ];
7754
- const userLines = o.system ? [`User=${userInfo().username}`, `Environment=HOME=${homedir4()}`] : [];
7755
- return `[Unit]
7756
- Description=Fineguide Tesseract host daemon
7757
- After=network-online.target
7758
- Wants=network-online.target
7759
-
7760
- [Service]
7761
- Type=simple
7762
- ${userLines.join("\n")}
7763
- ${env.join("\n")}
7764
- ExecStart=${execStart}
7765
- Restart=on-failure
7766
- RestartSec=30
7767
-
7768
- [Install]
7769
- WantedBy=${o.system ? "multi-user.target" : "default.target"}
7770
- `;
7771
- }
7772
- function run(cmd, args) {
7773
- const r = spawnSync(cmd, args, { stdio: "inherit" });
7774
- if (r.error) throw r.error;
7775
- if (r.status !== 0) throw new Error(`\`${cmd} ${args.join(" ")}\` exited ${r.status}`);
7776
- }
7777
- function install(o) {
7778
- if (!o.system && typeof process.getuid === "function" && process.getuid() === 0) {
7779
- throw new Error(
7780
- "Run `tesseract-agent install` as your normal user (no sudo) \u2014 it installs a per-user systemd service.\nFor a root-managed system service instead, use `sudo tesseract-agent install --system`."
7781
- );
7782
- }
7783
- const p = unitPath(o.system);
7784
- mkdirSync3(dirname5(p), { recursive: true });
7785
- writeFileSync3(p, buildUnit(o));
7786
- if (!o.system) {
7787
- try {
7788
- execFileSync("loginctl", ["enable-linger", userInfo().username], { stdio: "ignore" });
7789
- } catch {
7790
- }
8061
+ // src/service.ts
8062
+ import { join as join11 } from "node:path";
8063
+ import { platform as platform3 } from "node:os";
8064
+ import { existsSync as existsSync10, readFileSync as readFileSync3 } from "node:fs";
8065
+ function controller(instanceId) {
8066
+ const c = getServiceController(instanceId);
8067
+ if (!c) {
8068
+ throw new Error(`tesseract-agent can't manage a background service on ${platform3()}. Run it in the foreground instead: \`tesseract-agent run\` (e.g. as a Docker entrypoint).`);
7791
8069
  }
7792
- run("systemctl", sc(o.system, ["daemon-reload"]));
7793
- run("systemctl", sc(o.system, ["enable", UNIT]));
7794
- run("systemctl", sc(o.system, ["restart", UNIT]));
7795
- console.log(`\u2713 Installed + started ${UNIT} (${o.system ? "system" : "user"}). Check it with \`tesseract-agent status\`.`);
8070
+ return c;
7796
8071
  }
7797
- function uninstall(system) {
8072
+ function buildSpec(cliEntry, version) {
8073
+ return {
8074
+ exec: process.execPath,
8075
+ args: [cliEntry, "run"],
8076
+ env: { CCB_DAEMON_DIR: daemonDir(), CCB_DAEMON_VERSION: version },
8077
+ logPath: join11(daemonDir(), "daemon.log")
8078
+ };
8079
+ }
8080
+ var isAlive = (pid) => {
7798
8081
  try {
7799
- run("systemctl", sc(system, ["disable", "--now", UNIT]));
8082
+ process.kill(pid, 0);
8083
+ return true;
7800
8084
  } catch {
8085
+ return false;
7801
8086
  }
7802
- const p = unitPath(system);
7803
- if (existsSync7(p)) {
7804
- try {
7805
- unlinkSync2(p);
7806
- } catch {
7807
- }
7808
- }
8087
+ };
8088
+ function runningDaemonInfo() {
8089
+ const lock = join11(daemonDir(), "daemon.lock");
7809
8090
  try {
7810
- run("systemctl", sc(system, ["daemon-reload"]));
8091
+ if (!existsSync10(lock)) return null;
8092
+ const pid = Number(readFileSync3(lock, "utf-8").trim());
8093
+ if (!pid || !isAlive(pid)) return null;
8094
+ return { pid, device: readCredentials()?.deviceId };
7811
8095
  } catch {
8096
+ return null;
7812
8097
  }
7813
- console.log(`\u2713 Removed ${UNIT}.`);
7814
8098
  }
7815
- function start(system) {
7816
- run("systemctl", sc(system, ["start", UNIT]));
8099
+ async function install(instanceId, cliEntry, version) {
8100
+ await controller(instanceId).install(buildSpec(cliEntry, version));
8101
+ const where = instanceId ? `instance "${instanceId}"` : "this machine";
8102
+ console.log(`\u2713 Installed + started the tesseract-agent service for ${where}. Check it with \`tesseract-agent status${instanceId ? ` --instance ${instanceId}` : ""}\`.`);
8103
+ }
8104
+ async function uninstall(instanceId) {
8105
+ await controller(instanceId).uninstall();
8106
+ console.log(`\u2713 Removed the tesseract-agent service${instanceId ? ` (instance "${instanceId}")` : ""}.`);
8107
+ }
8108
+ async function restart(instanceId, cliEntry, version) {
8109
+ await controller(instanceId).restart(buildSpec(cliEntry, version));
8110
+ console.log(`\u2713 Restarted the tesseract-agent service${instanceId ? ` (instance "${instanceId}")` : ""}.`);
7817
8111
  }
7818
- function stop(system) {
7819
- run("systemctl", sc(system, ["stop", UNIT]));
8112
+ async function start(instanceId) {
8113
+ await controller(instanceId).start();
7820
8114
  }
7821
- function restart(system) {
7822
- run("systemctl", sc(system, ["restart", UNIT]));
8115
+ async function stop(instanceId) {
8116
+ await controller(instanceId).stop();
7823
8117
  }
7824
- function status(system) {
7825
- run("systemctl", sc(system, ["status", UNIT, "--no-pager"]));
8118
+ async function status(instanceId) {
8119
+ await controller(instanceId).status();
7826
8120
  }
7827
- function logs(system, follow) {
7828
- const base = system ? ["-u", UNIT] : ["--user-unit", UNIT];
7829
- run("journalctl", [...base, "-n", "200", ...follow ? ["-f"] : ["--no-pager"]]);
8121
+ async function logs(instanceId, follow) {
8122
+ await controller(instanceId).logs(follow);
7830
8123
  }
7831
8124
 
7832
8125
  // src/cli.ts
7833
- var CLI_VERSION = process.env.CCB_DAEMON_VERSION ?? "0.1.6";
8126
+ var CLI_VERSION = process.env.CCB_DAEMON_VERSION ?? "0.2.1";
7834
8127
  function parseFlags(argv) {
7835
8128
  const out = {};
7836
8129
  for (let i = 0; i < argv.length; i++) {
@@ -7851,29 +8144,56 @@ var HELP = `tesseract-agent v${CLI_VERSION} \u2014 run agents on this machine re
7851
8144
 
7852
8145
  Usage:
7853
8146
  tesseract-agent login [--token JWT] [--name LABEL] [--api URL] [--relay URL]
7854
- tesseract-agent install [--system] Install + start the background service (systemd)
7855
- tesseract-agent uninstall [--system] Stop + remove the service
8147
+ tesseract-agent install [--yes] Install + start the background service
8148
+ tesseract-agent uninstall Stop + remove the service
7856
8149
  tesseract-agent start | stop | restart Control the service
7857
8150
  tesseract-agent status Show the service status
7858
- tesseract-agent logs [--no-follow] Tail the service logs (journalctl)
8151
+ tesseract-agent logs [--no-follow] Tail the service logs
7859
8152
  tesseract-agent run Run the daemon in the foreground (Docker/manual)
7860
8153
  tesseract-agent whoami Show the provisioned device summary
7861
8154
  tesseract-agent logout Revoke this device's token + wipe local state
8155
+ tesseract-agent --version
7862
8156
  tesseract-agent --help
7863
8157
 
8158
+ Common flags:
8159
+ --instance ID Run/manage a SEPARATE agent under this OS user (its own state +
8160
+ device + service). Omit for the default agent. Valid id: A-Z a-z 0-9 _ -
8161
+ --yes Skip the confirmation when install finds a running daemon.
8162
+
8163
+ Services: launchd (macOS), systemd --user (Linux), Task Scheduler (Windows). In a
8164
+ container there's no init system \u2014 run \`tesseract-agent run\` as the entrypoint.
8165
+
7864
8166
  login: opens a browser sign-in (Google or email) and prints a URL + QR; sign in
7865
8167
  there and paste the one-time code shown. --token JWT skips the browser (automation).
7866
8168
  --api defaults to https://tesseract.fineguide.ai; --relay defaults to --api.`;
7867
- function requireLinux(cmd) {
7868
- if (platform2() !== "linux") {
7869
- console.error(`\`tesseract-agent ${cmd}\` manages a systemd service and is Linux-only. Use \`tesseract-agent run\` elsewhere (e.g. in Docker, or under launchd via the desktop app).`);
7870
- process.exit(1);
8169
+ async function confirm(question) {
8170
+ if (!process.stdin.isTTY) return false;
8171
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8172
+ try {
8173
+ const a = (await rl.question(question)).trim().toLowerCase();
8174
+ return a === "y" || a === "yes";
8175
+ } finally {
8176
+ rl.close();
7871
8177
  }
7872
8178
  }
7873
8179
  async function main() {
7874
8180
  const [cmd, ...rest] = process.argv.slice(2);
7875
8181
  const flags = parseFlags(rest);
7876
- const system = flags.system === true;
8182
+ let instance;
8183
+ if (flags.instance !== void 0) {
8184
+ if (flags.instance === true || typeof flags.instance !== "string") {
8185
+ console.error("--instance needs an id, e.g. `--instance work2`.");
8186
+ process.exit(1);
8187
+ }
8188
+ if (!/^[A-Za-z0-9_-]+$/.test(flags.instance)) {
8189
+ console.error("Invalid --instance id \u2014 use letters, digits, dash or underscore only.");
8190
+ process.exit(1);
8191
+ }
8192
+ instance = flags.instance;
8193
+ process.env.CCB_DAEMON_DIR = join12(homedir6(), ".tesseract", `daemon-${instance}`);
8194
+ }
8195
+ const force = flags.yes === true || flags.force === true;
8196
+ const cliEntry = fileURLToPath2(import.meta.url);
7877
8197
  switch (cmd) {
7878
8198
  case "login":
7879
8199
  await login({
@@ -7892,33 +8212,44 @@ async function main() {
7892
8212
  case "run":
7893
8213
  await runDaemon({ tag: "tesseract", version: CLI_VERSION });
7894
8214
  break;
7895
- case "install":
7896
- requireLinux("install");
7897
- install({ system, cliEntry: fileURLToPath2(import.meta.url), node: process.execPath, daemonDir: daemonDir(), version: CLI_VERSION });
8215
+ case "install": {
8216
+ const info = runningDaemonInfo();
8217
+ if (info && !force) {
8218
+ const where = instance ? `the "${instance}" instance` : "this machine (default instance)";
8219
+ const dev = info.device ? `, device ${info.device}` : "";
8220
+ const msg = `A tesseract-agent daemon is already running for ${where} (pid ${info.pid}${dev}).`;
8221
+ if (!process.stdin.isTTY) {
8222
+ console.error(`${msg}
8223
+ Re-run with --yes to replace it, or --instance <id> to install a SEPARATE agent.`);
8224
+ process.exit(1);
8225
+ }
8226
+ console.log(msg);
8227
+ console.log("(To run a separate agent instead, cancel and use `--instance <id>`.)");
8228
+ if (!await confirm("Reinstall/replace it? [y/N] ")) {
8229
+ console.log("Cancelled.");
8230
+ break;
8231
+ }
8232
+ }
8233
+ await install(instance, cliEntry, CLI_VERSION);
7898
8234
  break;
8235
+ }
7899
8236
  case "uninstall":
7900
- requireLinux("uninstall");
7901
- uninstall(system);
8237
+ await uninstall(instance);
7902
8238
  break;
7903
8239
  case "start":
7904
- requireLinux("start");
7905
- start(system);
8240
+ await start(instance);
7906
8241
  break;
7907
8242
  case "stop":
7908
- requireLinux("stop");
7909
- stop(system);
8243
+ await stop(instance);
7910
8244
  break;
7911
8245
  case "restart":
7912
- requireLinux("restart");
7913
- restart(system);
8246
+ await restart(instance, cliEntry, CLI_VERSION);
7914
8247
  break;
7915
8248
  case "status":
7916
- requireLinux("status");
7917
- status(system);
8249
+ await status(instance);
7918
8250
  break;
7919
8251
  case "logs":
7920
- requireLinux("logs");
7921
- logs(system, flags["no-follow"] !== true);
8252
+ await logs(instance, flags["no-follow"] !== true);
7922
8253
  break;
7923
8254
  case "--version":
7924
8255
  case "-v":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tesseract-agent",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "license": "UNLICENSED",
5
5
  "publishConfig": {
6
6
  "access": "public"