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.
- package/README.md +44 -14
- package/dist-bundle/cli.js +452 -121
- 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";
|
|
@@ -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({
|
|
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
|
|
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 &&
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
|
7534
|
-
import { join as
|
|
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 =
|
|
7712
|
-
if (
|
|
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/
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
8082
|
+
process.kill(pid, 0);
|
|
8083
|
+
return true;
|
|
7800
8084
|
} catch {
|
|
8085
|
+
return false;
|
|
7801
8086
|
}
|
|
7802
|
-
|
|
7803
|
-
|
|
7804
|
-
|
|
7805
|
-
unlinkSync2(p);
|
|
7806
|
-
} catch {
|
|
7807
|
-
}
|
|
7808
|
-
}
|
|
8087
|
+
};
|
|
8088
|
+
function runningDaemonInfo() {
|
|
8089
|
+
const lock = join11(daemonDir(), "daemon.lock");
|
|
7809
8090
|
try {
|
|
7810
|
-
|
|
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
|
|
7816
|
-
|
|
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
|
|
7819
|
-
|
|
8112
|
+
async function start(instanceId) {
|
|
8113
|
+
await controller(instanceId).start();
|
|
7820
8114
|
}
|
|
7821
|
-
function
|
|
7822
|
-
|
|
8115
|
+
async function stop(instanceId) {
|
|
8116
|
+
await controller(instanceId).stop();
|
|
7823
8117
|
}
|
|
7824
|
-
function status(
|
|
7825
|
-
|
|
8118
|
+
async function status(instanceId) {
|
|
8119
|
+
await controller(instanceId).status();
|
|
7826
8120
|
}
|
|
7827
|
-
function logs(
|
|
7828
|
-
|
|
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
|
|
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 [--
|
|
7855
|
-
tesseract-agent uninstall
|
|
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
|
|
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
|
|
7868
|
-
if (
|
|
7869
|
-
|
|
7870
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7897
|
-
|
|
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
|
-
|
|
7901
|
-
uninstall(system);
|
|
8237
|
+
await uninstall(instance);
|
|
7902
8238
|
break;
|
|
7903
8239
|
case "start":
|
|
7904
|
-
|
|
7905
|
-
start(system);
|
|
8240
|
+
await start(instance);
|
|
7906
8241
|
break;
|
|
7907
8242
|
case "stop":
|
|
7908
|
-
|
|
7909
|
-
stop(system);
|
|
8243
|
+
await stop(instance);
|
|
7910
8244
|
break;
|
|
7911
8245
|
case "restart":
|
|
7912
|
-
|
|
7913
|
-
restart(system);
|
|
8246
|
+
await restart(instance, cliEntry, CLI_VERSION);
|
|
7914
8247
|
break;
|
|
7915
8248
|
case "status":
|
|
7916
|
-
|
|
7917
|
-
status(system);
|
|
8249
|
+
await status(instance);
|
|
7918
8250
|
break;
|
|
7919
8251
|
case "logs":
|
|
7920
|
-
|
|
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":
|