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.
- package/README.md +44 -14
- package/dist-bundle/cli.js +410 -118
- 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
|
|
5
|
-
your laptop, phone, or the web app can then run agents on it,
|
|
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
|
|
18
|
-
|
|
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
|
|
24
|
-
tesseract-agent install # run as a background service (systemd
|
|
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 +
|
|
27
|
+
tesseract-agent logout # revoke this device + wipe local state
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
`login`
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
|
38
|
-
tesseract-agent
|
|
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.
|
package/dist-bundle/cli.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 &&
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
7534
|
-
import { join as
|
|
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 =
|
|
7712
|
-
if (
|
|
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/
|
|
7732
|
-
import {
|
|
7733
|
-
import {
|
|
7734
|
-
import {
|
|
7735
|
-
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
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
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
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
|
-
|
|
8036
|
+
var isAlive = (pid) => {
|
|
7798
8037
|
try {
|
|
7799
|
-
|
|
8038
|
+
process.kill(pid, 0);
|
|
8039
|
+
return true;
|
|
7800
8040
|
} catch {
|
|
8041
|
+
return false;
|
|
7801
8042
|
}
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
unlinkSync2(p);
|
|
7806
|
-
} catch {
|
|
7807
|
-
}
|
|
7808
|
-
}
|
|
8043
|
+
};
|
|
8044
|
+
function runningDaemonInfo() {
|
|
8045
|
+
const lock = join11(daemonDir(), "daemon.lock");
|
|
7809
8046
|
try {
|
|
7810
|
-
|
|
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
|
|
7816
|
-
|
|
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
|
|
7819
|
-
|
|
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
|
|
7822
|
-
|
|
8068
|
+
async function start(instanceId) {
|
|
8069
|
+
await controller(instanceId).start();
|
|
7823
8070
|
}
|
|
7824
|
-
function
|
|
7825
|
-
|
|
8071
|
+
async function stop(instanceId) {
|
|
8072
|
+
await controller(instanceId).stop();
|
|
7826
8073
|
}
|
|
7827
|
-
function
|
|
7828
|
-
|
|
7829
|
-
|
|
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.
|
|
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 [--
|
|
7855
|
-
tesseract-agent uninstall
|
|
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
|
|
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
|
|
7868
|
-
if (
|
|
7869
|
-
|
|
7870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7897
|
-
|
|
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
|
-
|
|
7901
|
-
uninstall(system);
|
|
8193
|
+
await uninstall(instance);
|
|
7902
8194
|
break;
|
|
7903
8195
|
case "start":
|
|
7904
|
-
|
|
7905
|
-
start(system);
|
|
8196
|
+
await start(instance);
|
|
7906
8197
|
break;
|
|
7907
8198
|
case "stop":
|
|
7908
|
-
|
|
7909
|
-
stop(system);
|
|
8199
|
+
await stop(instance);
|
|
7910
8200
|
break;
|
|
7911
8201
|
case "restart":
|
|
7912
|
-
|
|
7913
|
-
restart(system);
|
|
8202
|
+
await restart(instance, cliEntry, CLI_VERSION);
|
|
7914
8203
|
break;
|
|
7915
8204
|
case "status":
|
|
7916
|
-
|
|
7917
|
-
status(system);
|
|
8205
|
+
await status(instance);
|
|
7918
8206
|
break;
|
|
7919
8207
|
case "logs":
|
|
7920
|
-
|
|
7921
|
-
|
|
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":
|