tesseract-agent 0.1.5 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +44 -14
  2. package/dist-bundle/cli.js +410 -118
  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";
@@ -7395,7 +7397,7 @@ async function runDaemon(opts = {}) {
7395
7397
  const log2 = (...args) => console.log(`[${TAG} ${(/* @__PURE__ */ new Date()).toISOString()}]`, ...args);
7396
7398
  const pidfile = () => join6(daemonDir(), "daemon.lock");
7397
7399
  const statusFile = () => join6(daemonDir(), "status.json");
7398
- const isAlive = (pid) => {
7400
+ const isAlive2 = (pid) => {
7399
7401
  try {
7400
7402
  process.kill(pid, 0);
7401
7403
  return true;
@@ -7408,7 +7410,7 @@ async function runDaemon(opts = {}) {
7408
7410
  try {
7409
7411
  if (existsSync5(f)) {
7410
7412
  const prev = Number(readFileSync2(f, "utf-8").trim());
7411
- if (prev && prev !== process.pid && isAlive(prev))
7413
+ if (prev && prev !== process.pid && isAlive2(prev))
7412
7414
  return false;
7413
7415
  }
7414
7416
  } catch {
@@ -7527,11 +7529,295 @@ async function runDaemon(opts = {}) {
7527
7529
  process.on("SIGINT", () => shutdown(0));
7528
7530
  }
7529
7531
 
7532
+ // ../../packages/host-core/dist/service/index.js
7533
+ import { platform as platform2 } from "node:os";
7534
+
7535
+ // ../../packages/host-core/dist/service/launchd.js
7536
+ import { writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3, unlinkSync as unlinkSync2 } from "node:fs";
7537
+ import { dirname as dirname5, join as join7 } from "node:path";
7538
+ import { homedir as homedir4, userInfo } from "node:os";
7539
+
7540
+ // ../../packages/host-core/dist/service/exec.js
7541
+ import { execFile, spawn as spawn3 } from "node:child_process";
7542
+ import { promisify } from "node:util";
7543
+ var run = promisify(execFile);
7544
+ function runInherit(cmd, args) {
7545
+ return new Promise((resolve3) => {
7546
+ const p = spawn3(cmd, args, { stdio: "inherit" });
7547
+ p.on("error", () => resolve3());
7548
+ p.on("close", () => resolve3());
7549
+ });
7550
+ }
7551
+
7552
+ // ../../packages/host-core/dist/service/types.js
7553
+ var SERVICE_LABEL_BASE = "ai.fineguide.tesseract.host";
7554
+
7555
+ // ../../packages/host-core/dist/service/launchd.js
7556
+ function xml(s) {
7557
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
7558
+ }
7559
+ function makeLaunchd(instanceId) {
7560
+ const label = SERVICE_LABEL_BASE + (instanceId ? `.${instanceId}` : "");
7561
+ const plistPath = () => join7(homedir4(), "Library", "LaunchAgents", `${label}.plist`);
7562
+ const domainTarget = () => `gui/${userInfo().uid}`;
7563
+ const serviceTarget = () => `${domainTarget()}/${label}`;
7564
+ const logFile = () => join7(daemonDir(), "daemon.log");
7565
+ function buildPlist(spec) {
7566
+ const args = [spec.exec, ...spec.args].map((a) => ` <string>${xml(a)}</string>`).join("\n");
7567
+ const env = Object.entries(spec.env).map(([k, v]) => ` <key>${xml(k)}</key>
7568
+ <string>${xml(v)}</string>`).join("\n");
7569
+ return `<?xml version="1.0" encoding="UTF-8"?>
7570
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
7571
+ <plist version="1.0">
7572
+ <dict>
7573
+ <key>Label</key>
7574
+ <string>${label}</string>
7575
+ <key>ProgramArguments</key>
7576
+ <array>
7577
+ ${args}
7578
+ </array>
7579
+ <key>EnvironmentVariables</key>
7580
+ <dict>
7581
+ ${env}
7582
+ </dict>
7583
+ <key>RunAtLoad</key>
7584
+ <true/>
7585
+ <key>KeepAlive</key>
7586
+ <dict>
7587
+ <key>SuccessfulExit</key>
7588
+ <false/>
7589
+ </dict>
7590
+ <key>ThrottleInterval</key>
7591
+ <integer>30</integer>
7592
+ <key>ProcessType</key>
7593
+ <string>Background</string>
7594
+ <key>StandardOutPath</key>
7595
+ <string>${xml(spec.logPath)}</string>
7596
+ <key>StandardErrorPath</key>
7597
+ <string>${xml(spec.logPath)}</string>
7598
+ </dict>
7599
+ </plist>
7600
+ `;
7601
+ }
7602
+ return {
7603
+ async install(spec) {
7604
+ mkdirSync3(dirname5(plistPath()), { recursive: true });
7605
+ mkdirSync3(dirname5(spec.logPath), { recursive: true });
7606
+ writeFileSync3(plistPath(), buildPlist(spec));
7607
+ await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
7608
+ });
7609
+ await run("launchctl", ["bootstrap", domainTarget(), plistPath()]);
7610
+ await run("launchctl", ["enable", serviceTarget()]).catch(() => {
7611
+ });
7612
+ await run("launchctl", ["kickstart", "-k", serviceTarget()]).catch(() => {
7613
+ });
7614
+ },
7615
+ async uninstall() {
7616
+ await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
7617
+ });
7618
+ if (existsSync6(plistPath())) {
7619
+ try {
7620
+ unlinkSync2(plistPath());
7621
+ } catch {
7622
+ }
7623
+ }
7624
+ },
7625
+ async restart(spec) {
7626
+ await this.install(spec);
7627
+ },
7628
+ async isInstalled() {
7629
+ return existsSync6(plistPath());
7630
+ },
7631
+ async start() {
7632
+ await run("launchctl", ["bootstrap", domainTarget(), plistPath()]).catch(() => {
7633
+ });
7634
+ await run("launchctl", ["kickstart", serviceTarget()]).catch(() => {
7635
+ });
7636
+ },
7637
+ async stop() {
7638
+ await run("launchctl", ["bootout", serviceTarget()]).catch(() => {
7639
+ });
7640
+ },
7641
+ async status() {
7642
+ await runInherit("launchctl", ["print", serviceTarget()]);
7643
+ },
7644
+ async logs(follow) {
7645
+ const f = logFile();
7646
+ if (!existsSync6(f)) {
7647
+ console.log(`No log yet at ${f} \u2014 has the service started?`);
7648
+ return;
7649
+ }
7650
+ await runInherit("tail", follow ? ["-f", f] : ["-n", "200", f]);
7651
+ }
7652
+ };
7653
+ }
7654
+
7655
+ // ../../packages/host-core/dist/service/systemd.js
7656
+ import { writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync4, unlinkSync as unlinkSync3 } from "node:fs";
7657
+ import { dirname as dirname6, join as join8 } from "node:path";
7658
+ import { homedir as homedir5, userInfo as userInfo2 } from "node:os";
7659
+ function shq(s) {
7660
+ return `"${s.replace(/(["\\$`])/g, "\\$1")}"`;
7661
+ }
7662
+ function makeSystemd(instanceId) {
7663
+ const unit = `tesseract-host${instanceId ? `-${instanceId}` : ""}.service`;
7664
+ const unitPath = () => {
7665
+ const base = process.env.XDG_CONFIG_HOME || join8(homedir5(), ".config");
7666
+ return join8(base, "systemd", "user", unit);
7667
+ };
7668
+ function buildUnit(spec) {
7669
+ const execStart = [spec.exec, ...spec.args].map(shq).join(" ");
7670
+ const env = Object.entries(spec.env).map(([k, v]) => `Environment=${k}=${v}`).join("\n");
7671
+ return `[Unit]
7672
+ Description=Fineguide Tesseract host daemon${instanceId ? ` (${instanceId})` : ""}
7673
+ After=network-online.target
7674
+ Wants=network-online.target
7675
+
7676
+ [Service]
7677
+ Type=simple
7678
+ ${env}
7679
+ ExecStart=${execStart}
7680
+ Restart=on-failure
7681
+ RestartSec=30
7682
+ StandardOutput=append:${spec.logPath}
7683
+ StandardError=append:${spec.logPath}
7684
+
7685
+ [Install]
7686
+ WantedBy=default.target
7687
+ `;
7688
+ }
7689
+ return {
7690
+ async install(spec) {
7691
+ mkdirSync4(dirname6(unitPath()), { recursive: true });
7692
+ mkdirSync4(dirname6(spec.logPath), { recursive: true });
7693
+ writeFileSync4(unitPath(), buildUnit(spec));
7694
+ await run("loginctl", ["enable-linger", userInfo2().username]).catch(() => {
7695
+ });
7696
+ await run("systemctl", ["--user", "daemon-reload"]).catch(() => {
7697
+ });
7698
+ await run("systemctl", ["--user", "enable", unit]).catch(() => {
7699
+ });
7700
+ await run("systemctl", ["--user", "restart", unit]);
7701
+ },
7702
+ async uninstall() {
7703
+ await run("systemctl", ["--user", "disable", "--now", unit]).catch(() => {
7704
+ });
7705
+ if (existsSync7(unitPath())) {
7706
+ try {
7707
+ unlinkSync3(unitPath());
7708
+ } catch {
7709
+ }
7710
+ }
7711
+ await run("systemctl", ["--user", "daemon-reload"]).catch(() => {
7712
+ });
7713
+ },
7714
+ async restart(spec) {
7715
+ await this.install(spec);
7716
+ },
7717
+ async isInstalled() {
7718
+ return existsSync7(unitPath());
7719
+ },
7720
+ async start() {
7721
+ await run("systemctl", ["--user", "start", unit]);
7722
+ },
7723
+ async stop() {
7724
+ await run("systemctl", ["--user", "stop", unit]);
7725
+ },
7726
+ async status() {
7727
+ await runInherit("systemctl", ["--user", "status", unit, "--no-pager"]);
7728
+ },
7729
+ async logs(follow) {
7730
+ await runInherit("journalctl", ["--user-unit", unit, "-n", "200", ...follow ? ["-f"] : ["--no-pager"]]);
7731
+ }
7732
+ };
7733
+ }
7734
+
7735
+ // ../../packages/host-core/dist/service/windows.js
7736
+ import { writeFileSync as writeFileSync5, existsSync as existsSync8, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "node:fs";
7737
+ import { dirname as dirname7, join as join9 } from "node:path";
7738
+ function makeWindowsTask(instanceId) {
7739
+ const task = `Fineguide\\TesseractHost${instanceId ? `-${instanceId}` : ""}`;
7740
+ const wrapperPath = () => join9(daemonDir(), `run-host${instanceId ? `-${instanceId}` : ""}.cmd`);
7741
+ const logFile = () => join9(daemonDir(), "daemon.log");
7742
+ function buildWrapper(spec) {
7743
+ const env = Object.entries(spec.env).map(([k, v]) => `set "${k}=${v}"`).join("\r\n");
7744
+ const cmd = [spec.exec, ...spec.args].map((a) => `"${a}"`).join(" ");
7745
+ return `@echo off\r
7746
+ ${env}\r
7747
+ start "" /b ${cmd} >> "${spec.logPath}" 2>&1\r
7748
+ `;
7749
+ }
7750
+ return {
7751
+ async install(spec) {
7752
+ mkdirSync5(dirname7(spec.logPath), { recursive: true });
7753
+ mkdirSync5(dirname7(wrapperPath()), { recursive: true });
7754
+ writeFileSync5(wrapperPath(), buildWrapper(spec));
7755
+ await run("schtasks", ["/Create", "/TN", task, "/SC", "ONLOGON", "/RL", "LIMITED", "/F", "/TR", `"${wrapperPath()}"`]);
7756
+ await run("schtasks", ["/Run", "/TN", task]).catch(() => {
7757
+ });
7758
+ },
7759
+ async uninstall() {
7760
+ await run("schtasks", ["/Delete", "/TN", task, "/F"]).catch(() => {
7761
+ });
7762
+ if (existsSync8(wrapperPath())) {
7763
+ try {
7764
+ unlinkSync4(wrapperPath());
7765
+ } catch {
7766
+ }
7767
+ }
7768
+ },
7769
+ async restart(spec) {
7770
+ await this.install(spec);
7771
+ },
7772
+ async isInstalled() {
7773
+ try {
7774
+ await run("schtasks", ["/Query", "/TN", task]);
7775
+ return true;
7776
+ } catch {
7777
+ return false;
7778
+ }
7779
+ },
7780
+ async start() {
7781
+ await run("schtasks", ["/Run", "/TN", task]);
7782
+ },
7783
+ async stop() {
7784
+ await run("schtasks", ["/End", "/TN", task]).catch(() => {
7785
+ });
7786
+ },
7787
+ async status() {
7788
+ await runInherit("schtasks", ["/Query", "/TN", task, "/V", "/FO", "LIST"]);
7789
+ },
7790
+ async logs(follow) {
7791
+ const f = logFile();
7792
+ if (!existsSync8(f)) {
7793
+ console.log(`No log yet at ${f} \u2014 has the task run?`);
7794
+ return;
7795
+ }
7796
+ await runInherit("powershell", ["-NoProfile", "-Command", `Get-Content -Path "${f}" -Tail 200${follow ? " -Wait" : ""}`]);
7797
+ }
7798
+ };
7799
+ }
7800
+
7801
+ // ../../packages/host-core/dist/service/index.js
7802
+ function getServiceController(instanceId) {
7803
+ const id = instanceId && instanceId.length ? instanceId : void 0;
7804
+ switch (platform2()) {
7805
+ case "darwin":
7806
+ return makeLaunchd(id);
7807
+ case "linux":
7808
+ return makeSystemd(id);
7809
+ case "win32":
7810
+ return makeWindowsTask(id);
7811
+ default:
7812
+ return null;
7813
+ }
7814
+ }
7815
+
7530
7816
  // src/auth.ts
7531
7817
  var import_qrcode_terminal = __toESM(require_main(), 1);
7532
7818
  import { randomUUID as randomUUID2 } from "node:crypto";
7533
- import { existsSync as existsSync6, rmSync } from "node:fs";
7534
- import { join as join7 } from "node:path";
7819
+ import { existsSync as existsSync9, rmSync } from "node:fs";
7820
+ import { join as join10 } from "node:path";
7535
7821
 
7536
7822
  // src/config.ts
7537
7823
  import { hostname as hostname2 } from "node:os";
@@ -7708,8 +7994,8 @@ async function logout() {
7708
7994
  }
7709
7995
  for (const f of ["credentials.json", "status.json", "daemon.lock"]) {
7710
7996
  try {
7711
- const p = join7(daemonDir(), f);
7712
- if (existsSync6(p)) rmSync(p);
7997
+ const p = join10(daemonDir(), f);
7998
+ if (existsSync9(p)) rmSync(p);
7713
7999
  } catch {
7714
8000
  }
7715
8001
  }
@@ -7728,109 +8014,72 @@ function whoami() {
7728
8014
  console.log(`expires: ${c.expiresAt}`);
7729
8015
  }
7730
8016
 
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
- }
8017
+ // src/service.ts
8018
+ import { join as join11 } from "node:path";
8019
+ import { platform as platform3 } from "node:os";
8020
+ import { existsSync as existsSync10, readFileSync as readFileSync3 } from "node:fs";
8021
+ function controller(instanceId) {
8022
+ const c = getServiceController(instanceId);
8023
+ if (!c) {
8024
+ 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
8025
  }
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\`.`);
8026
+ return c;
8027
+ }
8028
+ function buildSpec(cliEntry, version) {
8029
+ return {
8030
+ exec: process.execPath,
8031
+ args: [cliEntry, "run"],
8032
+ env: { CCB_DAEMON_DIR: daemonDir(), CCB_DAEMON_VERSION: version },
8033
+ logPath: join11(daemonDir(), "daemon.log")
8034
+ };
7796
8035
  }
7797
- function uninstall(system) {
8036
+ var isAlive = (pid) => {
7798
8037
  try {
7799
- run("systemctl", sc(system, ["disable", "--now", UNIT]));
8038
+ process.kill(pid, 0);
8039
+ return true;
7800
8040
  } catch {
8041
+ return false;
7801
8042
  }
7802
- const p = unitPath(system);
7803
- if (existsSync7(p)) {
7804
- try {
7805
- unlinkSync2(p);
7806
- } catch {
7807
- }
7808
- }
8043
+ };
8044
+ function runningDaemonInfo() {
8045
+ const lock = join11(daemonDir(), "daemon.lock");
7809
8046
  try {
7810
- run("systemctl", sc(system, ["daemon-reload"]));
8047
+ if (!existsSync10(lock)) return null;
8048
+ const pid = Number(readFileSync3(lock, "utf-8").trim());
8049
+ if (!pid || !isAlive(pid)) return null;
8050
+ return { pid, device: readCredentials()?.deviceId };
7811
8051
  } catch {
8052
+ return null;
7812
8053
  }
7813
- console.log(`\u2713 Removed ${UNIT}.`);
7814
8054
  }
7815
- function start(system) {
7816
- run("systemctl", sc(system, ["start", UNIT]));
8055
+ async function install(instanceId, cliEntry, version) {
8056
+ await controller(instanceId).install(buildSpec(cliEntry, version));
8057
+ const where = instanceId ? `instance "${instanceId}"` : "this machine";
8058
+ console.log(`\u2713 Installed + started the tesseract-agent service for ${where}. Check it with \`tesseract-agent status${instanceId ? ` --instance ${instanceId}` : ""}\`.`);
8059
+ }
8060
+ async function uninstall(instanceId) {
8061
+ await controller(instanceId).uninstall();
8062
+ console.log(`\u2713 Removed the tesseract-agent service${instanceId ? ` (instance "${instanceId}")` : ""}.`);
7817
8063
  }
7818
- function stop(system) {
7819
- run("systemctl", sc(system, ["stop", UNIT]));
8064
+ async function restart(instanceId, cliEntry, version) {
8065
+ await controller(instanceId).restart(buildSpec(cliEntry, version));
8066
+ console.log(`\u2713 Restarted the tesseract-agent service${instanceId ? ` (instance "${instanceId}")` : ""}.`);
7820
8067
  }
7821
- function restart(system) {
7822
- run("systemctl", sc(system, ["restart", UNIT]));
8068
+ async function start(instanceId) {
8069
+ await controller(instanceId).start();
7823
8070
  }
7824
- function status(system) {
7825
- run("systemctl", sc(system, ["status", UNIT, "--no-pager"]));
8071
+ async function stop(instanceId) {
8072
+ await controller(instanceId).stop();
7826
8073
  }
7827
- function logs(system, follow) {
7828
- const base = system ? ["-u", UNIT] : ["--user-unit", UNIT];
7829
- run("journalctl", [...base, "-n", "200", ...follow ? ["-f"] : ["--no-pager"]]);
8074
+ async function status(instanceId) {
8075
+ await controller(instanceId).status();
8076
+ }
8077
+ async function logs(instanceId, follow) {
8078
+ await controller(instanceId).logs(follow);
7830
8079
  }
7831
8080
 
7832
8081
  // src/cli.ts
7833
- var CLI_VERSION = process.env.CCB_DAEMON_VERSION ?? "0.1.5";
8082
+ var CLI_VERSION = process.env.CCB_DAEMON_VERSION ?? "0.2.0";
7834
8083
  function parseFlags(argv) {
7835
8084
  const out = {};
7836
8085
  for (let i = 0; i < argv.length; i++) {
@@ -7847,33 +8096,60 @@ function parseFlags(argv) {
7847
8096
  }
7848
8097
  return out;
7849
8098
  }
7850
- var HELP = `tesseract-agent \u2014 run agents on this machine remotely, under your account.
8099
+ var HELP = `tesseract-agent v${CLI_VERSION} \u2014 run agents on this machine remotely, under your account.
7851
8100
 
7852
8101
  Usage:
7853
8102
  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
8103
+ tesseract-agent install [--yes] Install + start the background service
8104
+ tesseract-agent uninstall Stop + remove the service
7856
8105
  tesseract-agent start | stop | restart Control the service
7857
8106
  tesseract-agent status Show the service status
7858
- tesseract-agent logs [--no-follow] Tail the service logs (journalctl)
8107
+ tesseract-agent logs [--no-follow] Tail the service logs
7859
8108
  tesseract-agent run Run the daemon in the foreground (Docker/manual)
7860
8109
  tesseract-agent whoami Show the provisioned device summary
7861
8110
  tesseract-agent logout Revoke this device's token + wipe local state
8111
+ tesseract-agent --version
7862
8112
  tesseract-agent --help
7863
8113
 
8114
+ Common flags:
8115
+ --instance ID Run/manage a SEPARATE agent under this OS user (its own state +
8116
+ device + service). Omit for the default agent. Valid id: A-Z a-z 0-9 _ -
8117
+ --yes Skip the confirmation when install finds a running daemon.
8118
+
8119
+ Services: launchd (macOS), systemd --user (Linux), Task Scheduler (Windows). In a
8120
+ container there's no init system \u2014 run \`tesseract-agent run\` as the entrypoint.
8121
+
7864
8122
  login: opens a browser sign-in (Google or email) and prints a URL + QR; sign in
7865
8123
  there and paste the one-time code shown. --token JWT skips the browser (automation).
7866
8124
  --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);
8125
+ async function confirm(question) {
8126
+ if (!process.stdin.isTTY) return false;
8127
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
8128
+ try {
8129
+ const a = (await rl.question(question)).trim().toLowerCase();
8130
+ return a === "y" || a === "yes";
8131
+ } finally {
8132
+ rl.close();
7871
8133
  }
7872
8134
  }
7873
8135
  async function main() {
7874
8136
  const [cmd, ...rest] = process.argv.slice(2);
7875
8137
  const flags = parseFlags(rest);
7876
- const system = flags.system === true;
8138
+ let instance;
8139
+ if (flags.instance !== void 0) {
8140
+ if (flags.instance === true || typeof flags.instance !== "string") {
8141
+ console.error("--instance needs an id, e.g. `--instance work2`.");
8142
+ process.exit(1);
8143
+ }
8144
+ if (!/^[A-Za-z0-9_-]+$/.test(flags.instance)) {
8145
+ console.error("Invalid --instance id \u2014 use letters, digits, dash or underscore only.");
8146
+ process.exit(1);
8147
+ }
8148
+ instance = flags.instance;
8149
+ process.env.CCB_DAEMON_DIR = join12(homedir6(), ".tesseract", `daemon-${instance}`);
8150
+ }
8151
+ const force = flags.yes === true || flags.force === true;
8152
+ const cliEntry = fileURLToPath2(import.meta.url);
7877
8153
  switch (cmd) {
7878
8154
  case "login":
7879
8155
  await login({
@@ -7892,33 +8168,49 @@ async function main() {
7892
8168
  case "run":
7893
8169
  await runDaemon({ tag: "tesseract", version: CLI_VERSION });
7894
8170
  break;
7895
- case "install":
7896
- requireLinux("install");
7897
- install({ system, cliEntry: fileURLToPath2(import.meta.url), node: process.execPath, daemonDir: daemonDir(), version: CLI_VERSION });
8171
+ case "install": {
8172
+ const info = runningDaemonInfo();
8173
+ if (info && !force) {
8174
+ const where = instance ? `the "${instance}" instance` : "this machine (default instance)";
8175
+ const dev = info.device ? `, device ${info.device}` : "";
8176
+ const msg = `A tesseract-agent daemon is already running for ${where} (pid ${info.pid}${dev}).`;
8177
+ if (!process.stdin.isTTY) {
8178
+ console.error(`${msg}
8179
+ Re-run with --yes to replace it, or --instance <id> to install a SEPARATE agent.`);
8180
+ process.exit(1);
8181
+ }
8182
+ console.log(msg);
8183
+ console.log("(To run a separate agent instead, cancel and use `--instance <id>`.)");
8184
+ if (!await confirm("Reinstall/replace it? [y/N] ")) {
8185
+ console.log("Cancelled.");
8186
+ break;
8187
+ }
8188
+ }
8189
+ await install(instance, cliEntry, CLI_VERSION);
7898
8190
  break;
8191
+ }
7899
8192
  case "uninstall":
7900
- requireLinux("uninstall");
7901
- uninstall(system);
8193
+ await uninstall(instance);
7902
8194
  break;
7903
8195
  case "start":
7904
- requireLinux("start");
7905
- start(system);
8196
+ await start(instance);
7906
8197
  break;
7907
8198
  case "stop":
7908
- requireLinux("stop");
7909
- stop(system);
8199
+ await stop(instance);
7910
8200
  break;
7911
8201
  case "restart":
7912
- requireLinux("restart");
7913
- restart(system);
8202
+ await restart(instance, cliEntry, CLI_VERSION);
7914
8203
  break;
7915
8204
  case "status":
7916
- requireLinux("status");
7917
- status(system);
8205
+ await status(instance);
7918
8206
  break;
7919
8207
  case "logs":
7920
- requireLinux("logs");
7921
- logs(system, flags["no-follow"] !== true);
8208
+ await logs(instance, flags["no-follow"] !== true);
8209
+ break;
8210
+ case "--version":
8211
+ case "-v":
8212
+ case "version":
8213
+ console.log(CLI_VERSION);
7922
8214
  break;
7923
8215
  case void 0:
7924
8216
  case "--help":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tesseract-agent",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "license": "UNLICENSED",
5
5
  "publishConfig": {
6
6
  "access": "public"