@the-seeker/server-agent 0.1.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.
@@ -0,0 +1,53 @@
1
+ import { execFile } from "node:child_process";
2
+ export function run(file, args, options = {}) {
3
+ return new Promise((resolve, reject) => {
4
+ execFile(file, [...args], { timeout: 8000, maxBuffer: 8 * 1024 * 1024, ...options, shell: false, encoding: "utf8" }, (error, stdout, stderr) => {
5
+ if (error)
6
+ reject(Object.assign(error, { stderr }));
7
+ else
8
+ resolve({ stdout, stderr });
9
+ });
10
+ });
11
+ }
12
+ export async function locate(binary) {
13
+ try {
14
+ const { stdout } = await run("which", [binary]);
15
+ return stdout.trim() || null;
16
+ }
17
+ catch (error) {
18
+ if (error instanceof Error && "code" in error && (error.code === 1 || error.code === "ENOENT"))
19
+ return null;
20
+ throw error;
21
+ }
22
+ }
23
+ export class CollectionError extends Error {
24
+ code;
25
+ constructor(code) {
26
+ super(code);
27
+ this.code = code;
28
+ this.name = "CollectionError";
29
+ }
30
+ }
31
+ export function responseError(error) {
32
+ if (error instanceof CollectionError)
33
+ return error;
34
+ if (!(error instanceof Error))
35
+ throw error;
36
+ const code = "code" in error ? error.code : undefined;
37
+ const stderr = "stderr" in error && typeof error.stderr === "string" ? error.stderr : "";
38
+ if (code === "EACCES" || code === "EPERM" || code === "ELOOP" || /No journal files|permission|access denied/i.test(stderr))
39
+ return new CollectionError("permission_denied");
40
+ if (code === "ENOENT")
41
+ return new CollectionError("not_found");
42
+ if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER")
43
+ return new CollectionError("too_large");
44
+ if (error.name === "AbortError" || ("killed" in error && error.killed === true))
45
+ return new CollectionError("timeout");
46
+ return new CollectionError("internal");
47
+ }
48
+ export function capabilityError(error) {
49
+ const mapped = responseError(error);
50
+ if (mapped.code === "not_found")
51
+ return "not_installed";
52
+ return mapped.code === "permission_denied" ? "permission_denied" : "error";
53
+ }
@@ -0,0 +1,16 @@
1
+ import si from "systeminformation";
2
+ import type { Host } from "../protocol.js";
3
+ type NetworkCounter = Readonly<Pick<si.Systeminformation.NetworkStatsData, "iface" | "rx_bytes" | "tx_bytes">>;
4
+ export type NetworkSample = {
5
+ readonly measuredAt: number;
6
+ readonly interfaces: readonly NetworkCounter[];
7
+ };
8
+ export type HostCollection = {
9
+ readonly host: Host;
10
+ readonly collected_at: string;
11
+ readonly sample: NetworkSample;
12
+ };
13
+ export declare function networkRates(current: NetworkSample, previous?: NetworkSample): Host["net"];
14
+ export declare function filterDisks(disks: readonly Pick<si.Systeminformation.FsSizeData, "fs" | "type" | "mount" | "used" | "size">[]): Host["disks"];
15
+ export declare function collectHost(previous?: NetworkSample): Promise<HostCollection>;
16
+ export {};
@@ -0,0 +1,43 @@
1
+ import * as os from "node:os";
2
+ import { performance } from "node:perf_hooks";
3
+ import si from "systeminformation";
4
+ export function networkRates(current, previous) {
5
+ const missing = { rx_bps: null, tx_bps: null };
6
+ if (!previous || current.measuredAt <= previous.measuredAt || !current.interfaces.length)
7
+ return missing;
8
+ if (current.interfaces.length !== previous.interfaces.length)
9
+ return missing;
10
+ const elapsed = (current.measuredAt - previous.measuredAt) / 1000;
11
+ let rx = 0;
12
+ let tx = 0;
13
+ for (const counter of current.interfaces) {
14
+ const prior = previous.interfaces.find((entry) => entry.iface === counter.iface);
15
+ if (!prior)
16
+ return missing;
17
+ rx += counter.rx_bytes < prior.rx_bytes || prior.rx_bytes < 0 ? NaN : counter.rx_bytes - prior.rx_bytes;
18
+ tx += counter.tx_bytes < prior.tx_bytes || prior.tx_bytes < 0 ? NaN : counter.tx_bytes - prior.tx_bytes;
19
+ }
20
+ return { rx_bps: Number.isFinite(rx) ? rx / elapsed : null, tx_bps: Number.isFinite(tx) ? tx / elapsed : null };
21
+ }
22
+ export function filterDisks(disks) {
23
+ return disks.filter((disk) => !["devfs", "tmpfs", "squashfs", "overlay"].includes(disk.type.toLowerCase())
24
+ && disk.mount !== "/System/Volumes" && !disk.mount.startsWith("/System/Volumes/"))
25
+ .slice(0, 32).map((disk) => ({ mount: disk.mount, fs: disk.fs, used: disk.used, total: disk.size }));
26
+ }
27
+ export async function collectHost(previous) {
28
+ const [cpu, memory, disks, sample] = await Promise.all([
29
+ si.currentLoad(), si.mem(), si.fsSize(),
30
+ si.networkStats().then((interfaces) => ({ measuredAt: performance.now(), interfaces: interfaces.map(({ iface, rx_bytes, tx_bytes }) => ({ iface, rx_bytes, tx_bytes })) })),
31
+ ]);
32
+ const [load1 = 0, load5 = 0, load15 = 0] = os.loadavg();
33
+ return {
34
+ collected_at: new Date().toISOString(), sample,
35
+ host: {
36
+ cpu_pct: Number.isFinite(cpu.currentLoad) ? Math.max(0, Math.min(100, cpu.currentLoad)) : null,
37
+ cpu_count: os.cpus().length, load: [load1, load5, load15],
38
+ mem: { used: memory.total - memory.available, total: memory.total },
39
+ swap: { used: memory.swapused, total: memory.swaptotal }, disks: filterDisks(disks),
40
+ net: networkRates(sample, previous), uptime_sec: os.uptime(),
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,8 @@
1
+ export declare const NGINX_CONFIG_ROOTS: readonly ["/etc/nginx", "/opt/homebrew/etc/nginx"];
2
+ export type Directive = {
3
+ readonly words: readonly string[];
4
+ readonly children: readonly Directive[];
5
+ };
6
+ export declare function parseConfig(text: string): Directive[];
7
+ export declare function readNginxConfig(file: string, roots?: readonly string[]): Promise<string>;
8
+ export declare function expandNginxDump(text: string): string;
@@ -0,0 +1,132 @@
1
+ import { readFile, readdir, realpath } from "node:fs/promises";
2
+ import { dirname, resolve, sep } from "node:path";
3
+ import { CollectionError } from "./command.js";
4
+ import { inside } from "./sources.js";
5
+ export const NGINX_CONFIG_ROOTS = ["/etc/nginx", "/opt/homebrew/etc/nginx"];
6
+ export function parseConfig(text) {
7
+ const tokens = text.match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|#[^\n]*|[{};]|[^\s{};"'#]+/g) ?? [];
8
+ let index = 0;
9
+ function block(nested) {
10
+ const directives = [];
11
+ let words = [];
12
+ while (index < tokens.length) {
13
+ const token = tokens[index++];
14
+ if (!token || token.startsWith("#"))
15
+ continue;
16
+ if (token === "}") {
17
+ if (!nested || words.length)
18
+ throw new CollectionError("internal");
19
+ return directives;
20
+ }
21
+ if (token === "{" || token === ";") {
22
+ directives.push({ words, children: token === "{" ? block(true) : [] });
23
+ words = [];
24
+ }
25
+ else
26
+ words.push(token.replace(/^(["'])([\s\S]*)\1$/, "$2"));
27
+ }
28
+ if (nested || words.length)
29
+ throw new CollectionError("internal");
30
+ return directives;
31
+ }
32
+ return block(false);
33
+ }
34
+ function globRegex(pattern) {
35
+ return new RegExp(`^${pattern.replace(/[.+^${}()|\\]/g, "\\$&").replace(/\[!/g, "[^").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`);
36
+ }
37
+ export async function readNginxConfig(file, roots = NGINX_CONFIG_ROOTS) {
38
+ const canonicalRoots = await Promise.all(roots.map(async (root) => {
39
+ try {
40
+ return await realpath(root);
41
+ }
42
+ catch (error) {
43
+ if (error instanceof Error && "code" in error && error.code === "ENOENT")
44
+ return resolve(root);
45
+ throw error;
46
+ }
47
+ }));
48
+ async function checked(path) {
49
+ if (!roots.some((root) => inside(resolve(path), resolve(root))))
50
+ throw new CollectionError("permission_denied");
51
+ const canonical = await realpath(path);
52
+ if (!canonicalRoots.some((root) => inside(canonical, root)))
53
+ throw new CollectionError("permission_denied");
54
+ return canonical;
55
+ }
56
+ async function expand(pattern) {
57
+ const segments = resolve(pattern).split(sep).filter(Boolean);
58
+ let paths = [sep];
59
+ for (const segment of segments) {
60
+ if (!/[?*\[]/.test(segment)) {
61
+ paths = paths.map((path) => resolve(path, segment));
62
+ continue;
63
+ }
64
+ const expanded = [];
65
+ for (const path of paths) {
66
+ const parent = await checked(path);
67
+ for (const name of (await readdir(parent)).sort())
68
+ if (globRegex(segment).test(name))
69
+ expanded.push(resolve(path, name));
70
+ }
71
+ paths = expanded;
72
+ }
73
+ return paths;
74
+ }
75
+ const prefix = dirname(file);
76
+ async function load(path, depth) {
77
+ if (depth > 3)
78
+ throw new CollectionError("unsupported");
79
+ const text = await readFile(await checked(path), "utf8");
80
+ const directives = parseConfig(text);
81
+ async function render(entries) {
82
+ const output = [];
83
+ for (const entry of entries) {
84
+ if (entry.words[0] === "include" && entry.words[1]) {
85
+ for (const child of await expand(resolve(prefix, entry.words[1])))
86
+ output.push(await load(child, depth + 1));
87
+ }
88
+ else {
89
+ const words = entry.words.map((word) => JSON.stringify(word)).join(" ");
90
+ output.push(entry.children.length ? `${words} { ${await render(entry.children)} }` : `${words};`);
91
+ }
92
+ }
93
+ return output.join("\n");
94
+ }
95
+ return render(directives);
96
+ }
97
+ return load(file, 0);
98
+ }
99
+ export function expandNginxDump(text) {
100
+ const sections = [...text.matchAll(/^# configuration file (.+):\r?\n/gm)];
101
+ if (!sections.length)
102
+ return text;
103
+ const files = new Map();
104
+ for (const [index, section] of sections.entries()) {
105
+ const path = section[1];
106
+ if (path) {
107
+ if (!NGINX_CONFIG_ROOTS.some((root) => inside(resolve(path), root)))
108
+ throw new CollectionError("permission_denied");
109
+ files.set(path, text.slice((section.index ?? 0) + section[0].length, sections[index + 1]?.index ?? text.length));
110
+ }
111
+ }
112
+ const first = sections[0]?.[1];
113
+ if (!first)
114
+ throw new CollectionError("internal");
115
+ const prefix = dirname(first);
116
+ function render(entries, depth) {
117
+ if (depth > 3)
118
+ throw new CollectionError("unsupported");
119
+ return entries.map((entry) => {
120
+ if (entry.words[0] === "include" && entry.words[1]) {
121
+ const include = resolve(prefix, entry.words[1]);
122
+ if (!NGINX_CONFIG_ROOTS.some((root) => inside(include, root)))
123
+ throw new CollectionError("permission_denied");
124
+ const pattern = globRegex(include);
125
+ return [...files].filter(([path]) => pattern.test(path)).map(([, content]) => render(parseConfig(content), depth + 1)).join("\n");
126
+ }
127
+ const words = entry.words.map((word) => JSON.stringify(word)).join(" ");
128
+ return entry.children.length ? `${words} { ${render(entry.children, depth)} }` : `${words};`;
129
+ }).join("\n");
130
+ }
131
+ return render(parseConfig(files.get(first) ?? ""), 0);
132
+ }
@@ -0,0 +1,8 @@
1
+ import type { CapabilityState, Source } from "../protocol.js";
2
+ import type { SourceRegistry } from "./sources.js";
3
+ export type NginxCollection = {
4
+ readonly capability: CapabilityState;
5
+ readonly sources: Source[];
6
+ };
7
+ export declare function mapNginx(text: string, registry: SourceRegistry, prefix?: string): NginxCollection;
8
+ export declare function collectNginx(nginxEnabled: boolean, registry: SourceRegistry, signal?: AbortSignal): Promise<NginxCollection>;
@@ -0,0 +1,78 @@
1
+ import { isAbsolute, resolve } from "node:path";
2
+ import { capabilityError, CollectionError, locate, run } from "./command.js";
3
+ import { expandNginxDump, parseConfig, readNginxConfig } from "./nginx-config.js";
4
+ export function mapNginx(text, registry, prefix = "/usr/local/nginx") {
5
+ const sources = [];
6
+ for (const http of parseConfig(text).filter((entry) => entry.words[0] === "http")) {
7
+ for (const server of http.children.filter((entry) => entry.words[0] === "server")) {
8
+ const name = server.children.find((entry) => entry.words[0] === "server_name")?.words[1] ?? "_";
9
+ for (const [directive, kind] of [["access_log", "nginx_access"], ["error_log", "nginx_error"]]) {
10
+ const find = (entries) => entries.find((entry) => entry.words[0] === directive);
11
+ const path = (find(server.children) ?? find(http.children))?.words[1];
12
+ if (path && path !== "off" && !path.startsWith("syslog:") && !path.includes("$")) {
13
+ sources.push(registry.registerFile(kind, name, resolve(prefix, path), name, "Nginx"));
14
+ }
15
+ }
16
+ }
17
+ }
18
+ return { capability: "available", sources: [...new Map(sources.map((source) => [source.id, source])).values()] };
19
+ }
20
+ export async function collectNginx(nginxEnabled, registry, signal) {
21
+ registry.clearKinds(["nginx_access", "nginx_error"]);
22
+ if (!nginxEnabled)
23
+ return { capability: "unsupported", sources: [] };
24
+ try {
25
+ const binary = await locate("nginx");
26
+ if (!binary)
27
+ return { capability: "not_installed", sources: [] };
28
+ let text;
29
+ try {
30
+ const { stdout } = await run(binary, ["-T"], { timeout: 5000, ...(signal ? { signal } : {}) });
31
+ text = expandNginxDump(stdout);
32
+ }
33
+ catch (error) {
34
+ if (!(error instanceof Error))
35
+ throw error;
36
+ signal?.throwIfAborted();
37
+ try {
38
+ text = await readNginxConfig("/etc/nginx/nginx.conf");
39
+ }
40
+ catch (fallbackError) {
41
+ if (!(fallbackError instanceof Error))
42
+ throw fallbackError;
43
+ try {
44
+ text = await readNginxConfig("/opt/homebrew/etc/nginx/nginx.conf");
45
+ }
46
+ catch (homebrewError) {
47
+ if (!(homebrewError instanceof Error))
48
+ throw homebrewError;
49
+ throw new CollectionError("permission_denied");
50
+ }
51
+ }
52
+ }
53
+ const pending = [...parseConfig(text)];
54
+ let relativeLogs = false;
55
+ while (pending.length) {
56
+ const entry = pending.pop();
57
+ if (!entry)
58
+ break;
59
+ pending.push(...entry.children);
60
+ const path = entry.words[1];
61
+ if ((entry.words[0] === "access_log" || entry.words[0] === "error_log") && path
62
+ && path !== "off" && !path.startsWith("syslog:") && !path.includes("$") && !isAbsolute(path))
63
+ relativeLogs = true;
64
+ }
65
+ let prefix = "/usr/local/nginx";
66
+ if (relativeLogs) {
67
+ const version = await run(binary, ["-V"], { timeout: 5000, ...(signal ? { signal } : {}) });
68
+ const configured = `${version.stdout}\n${version.stderr}`.match(/--prefix=(?:"([^"]+)"|'([^']+)'|(\S+))/);
69
+ prefix = configured?.[1] ?? configured?.[2] ?? configured?.[3] ?? prefix;
70
+ if (!isAbsolute(prefix))
71
+ throw new CollectionError("permission_denied");
72
+ }
73
+ return mapNginx(text, registry, prefix);
74
+ }
75
+ catch (error) {
76
+ return { capability: capabilityError(error), sources: [] };
77
+ }
78
+ }
@@ -0,0 +1,12 @@
1
+ import type { CapabilityState, Process, Source } from "../protocol.js";
2
+ import type { SourceRegistry } from "./sources.js";
3
+ export type Pm2Collection = {
4
+ readonly capability: CapabilityState;
5
+ readonly processes: Process[];
6
+ readonly sources: Source[];
7
+ };
8
+ export declare function mapPm2(text: string, registry: SourceRegistry, now?: number): Pm2Collection;
9
+ export declare function collectPm2(registry: SourceRegistry, options?: {
10
+ readonly pm2Home?: string;
11
+ readonly signal?: AbortSignal;
12
+ }): Promise<Pm2Collection>;
@@ -0,0 +1,50 @@
1
+ import { homedir } from "node:os";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { z } from "zod";
4
+ import { capabilityError, locate, run } from "./command.js";
5
+ const jlist = z.array(z.object({
6
+ name: z.string(), pm_id: z.number(),
7
+ monit: z.object({ cpu: z.number().default(0), memory: z.number().default(0) }),
8
+ pm2_env: z.object({
9
+ status: z.string(), restart_time: z.number().default(0), unstable_restarts: z.number().default(0),
10
+ pm_uptime: z.number().nullish(), version: z.string().nullish(), exec_mode: z.string().nullish(),
11
+ pm_out_log_path: z.string().optional(), pm_err_log_path: z.string().optional(),
12
+ }),
13
+ }));
14
+ export function mapPm2(text, registry, now = Date.now()) {
15
+ const entries = jlist.parse(JSON.parse(text)).slice(0, 100);
16
+ const sources = [];
17
+ const processes = entries.map((entry) => {
18
+ const environment = entry.pm2_env;
19
+ if (environment.pm_out_log_path && isAbsolute(environment.pm_out_log_path)) {
20
+ sources.push(registry.registerFile("pm2_out", entry.name, environment.pm_out_log_path, `${entry.name} stdout`, "PM2"));
21
+ }
22
+ if (environment.pm_err_log_path && isAbsolute(environment.pm_err_log_path)) {
23
+ sources.push(registry.registerFile("pm2_err", entry.name, environment.pm_err_log_path, `${entry.name} stderr`, "PM2"));
24
+ }
25
+ return {
26
+ name: entry.name, pm_id: entry.pm_id, status: environment.status, cpu_pct: entry.monit.cpu,
27
+ memory_bytes: entry.monit.memory, restarts: environment.restart_time, unstable_restarts: environment.unstable_restarts,
28
+ uptime_ms: environment.pm_uptime ? Math.max(0, now - environment.pm_uptime) : null,
29
+ version: environment.version ?? null, exec_mode: environment.exec_mode ?? null,
30
+ };
31
+ });
32
+ return { capability: "available", processes, sources: [...new Map(sources.map((source) => [source.id, source])).values()] };
33
+ }
34
+ export async function collectPm2(registry, options = {}) {
35
+ registry.clearKinds(["pm2_out", "pm2_err"]);
36
+ try {
37
+ const binary = process.env["PM2_BIN"] || await locate("pm2");
38
+ if (!binary)
39
+ return { capability: "not_installed", processes: [], sources: [] };
40
+ const { stdout } = await run(binary, ["jlist"], {
41
+ timeout: 8000, maxBuffer: 8 * 1024 * 1024,
42
+ env: { ...process.env, PM2_HOME: options.pm2Home ?? process.env["PM2_HOME"] ?? join(homedir(), ".pm2") },
43
+ ...(options.signal ? { signal: options.signal } : {}),
44
+ });
45
+ return mapPm2(stdout, registry);
46
+ }
47
+ catch (error) {
48
+ return { capability: capabilityError(error), processes: [], sources: [] };
49
+ }
50
+ }
@@ -0,0 +1,12 @@
1
+ import type { LogsTailPayload, Source, SourceKind } from "../protocol.js";
2
+ type FileKind = Exclude<SourceKind, "systemd_journal">;
3
+ export declare function inside(path: string, root: string): boolean;
4
+ export declare class SourceRegistry {
5
+ #private;
6
+ constructor(pm2Home?: string);
7
+ registerFile(kind: FileKind, name: string, path: string, label: string, group: string): Source;
8
+ registerJournal(unit: string): Source;
9
+ clearKinds(kinds: readonly SourceKind[]): void;
10
+ tail(sourceId: string, lines: number, signal?: AbortSignal): Promise<LogsTailPayload>;
11
+ }
12
+ export {};
@@ -0,0 +1,110 @@
1
+ import { constants } from "node:fs";
2
+ import { open, realpath, stat } from "node:fs/promises";
3
+ import { homedir, platform } from "node:os";
4
+ import { isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { LOG_MAX_BYTES, makeSourceId } from "../protocol.js";
6
+ import { CollectionError, responseError, run } from "./command.js";
7
+ export function inside(path, root) {
8
+ return path === root || path.startsWith(root + sep);
9
+ }
10
+ function payload(sourceId, input, lines, clipped) {
11
+ let start = Math.max(0, input.length - LOG_MAX_BYTES);
12
+ while (start < input.length && ((input[start] ?? 0) & 0xc0) === 0x80)
13
+ start++;
14
+ const text = input.subarray(start).toString("utf8").replace(/\n$/, "");
15
+ const selected = input.length ? text.split("\n").slice(-lines) : [];
16
+ const encoded = Buffer.from(selected.join("\n"));
17
+ let offset = Math.max(0, encoded.length - LOG_MAX_BYTES);
18
+ while (offset < encoded.length && ((encoded[offset] ?? 0) & 0xc0) === 0x80)
19
+ offset++;
20
+ const bounded = encoded.subarray(offset).toString("utf8");
21
+ return { source_id: sourceId, lines: selected.length ? bounded.split("\n") : [], bytes: Buffer.byteLength(bounded),
22
+ truncated: clipped || input.length > LOG_MAX_BYTES || offset > 0 };
23
+ }
24
+ export class SourceRegistry {
25
+ #targets = new Map();
26
+ #roots;
27
+ constructor(pm2Home = process.env["PM2_HOME"] ?? join(homedir(), ".pm2")) {
28
+ this.#roots = ["/var/log/nginx", "/opt/homebrew/var/log/nginx", join(pm2Home, "logs"), join(homedir(), ".pm2/logs")];
29
+ }
30
+ registerFile(kind, name, path, label, group) {
31
+ if (!isAbsolute(path))
32
+ throw new CollectionError("permission_denied");
33
+ const id = makeSourceId(kind, name);
34
+ this.#targets.set(id, { kind, path: resolve(path) });
35
+ return { id, kind, label, group };
36
+ }
37
+ registerJournal(unit) {
38
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_.@:\\-]*$/.test(unit))
39
+ throw new CollectionError("permission_denied");
40
+ const kind = "systemd_journal";
41
+ const id = makeSourceId(kind, unit);
42
+ this.#targets.set(id, { kind, unit });
43
+ return { id, kind, label: unit, group: "서비스" };
44
+ }
45
+ clearKinds(kinds) {
46
+ for (const [id, target] of this.#targets)
47
+ if (kinds.includes(target.kind))
48
+ this.#targets.delete(id);
49
+ }
50
+ async tail(sourceId, lines, signal) {
51
+ const target = this.#targets.get(sourceId);
52
+ if (!target)
53
+ throw new CollectionError("not_found");
54
+ if (!Number.isInteger(lines) || lines < 1 || lines > 500)
55
+ throw new CollectionError("unsupported");
56
+ try {
57
+ if (target.kind === "systemd_journal") {
58
+ const result = await run("journalctl", ["-u", target.unit, "-n", String(lines), "--no-pager", "-o", "short-iso"], { ...(signal ? { signal } : {}), maxBuffer: 8 * 1024 * 1024 });
59
+ return payload(sourceId, Buffer.from(result.stdout), lines, false);
60
+ }
61
+ const canonical = await realpath(target.path);
62
+ let advertised = target.path;
63
+ if (platform() === "darwin") {
64
+ const alias = ["/var", "/tmp", "/etc"].find((root) => inside(target.path, root));
65
+ if (alias)
66
+ advertised = resolve(await realpath(alias), relative(alias, target.path));
67
+ }
68
+ const roots = await Promise.all(this.#roots.map(async (root) => {
69
+ try {
70
+ return await realpath(root);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "EACCES"))
74
+ return resolve(root);
75
+ throw error;
76
+ }
77
+ }));
78
+ if (canonical !== advertised && !roots.some((root) => inside(canonical, root)))
79
+ throw new CollectionError("permission_denied");
80
+ const handle = await open(canonical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
81
+ try {
82
+ const metadata = await handle.stat();
83
+ const current = await stat(canonical);
84
+ if (!metadata.isFile() || metadata.dev !== current.dev || metadata.ino !== current.ino || await realpath(target.path) !== canonical) {
85
+ throw new CollectionError("permission_denied");
86
+ }
87
+ let position = metadata.size;
88
+ let content = Buffer.alloc(0);
89
+ while (position > 0 && content.length < LOG_MAX_BYTES + 1) {
90
+ signal?.throwIfAborted();
91
+ const size = Math.min(16384, position, LOG_MAX_BYTES + 1 - content.length);
92
+ position -= size;
93
+ const block = Buffer.alloc(size);
94
+ const { bytesRead } = await handle.read(block, 0, size, position);
95
+ content = Buffer.concat([block.subarray(0, bytesRead), content]);
96
+ const lineCount = content.toString("utf8").split("\n").length - (content.at(-1) === 10 ? 1 : 0);
97
+ if (lineCount > lines)
98
+ break;
99
+ }
100
+ return payload(sourceId, content, lines, content.length > LOG_MAX_BYTES);
101
+ }
102
+ finally {
103
+ await handle.close();
104
+ }
105
+ }
106
+ catch (error) {
107
+ throw responseError(error);
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,11 @@
1
+ import type { CapabilityState, Service, Source } from "../protocol.js";
2
+ import type { SourceRegistry } from "./sources.js";
3
+ export type SystemdCollection = {
4
+ readonly capability: CapabilityState;
5
+ readonly services: Service[];
6
+ readonly sources: Source[];
7
+ };
8
+ export declare function collectSystemd(watchedServices: readonly string[], registry: SourceRegistry, options?: {
9
+ readonly platform?: string;
10
+ readonly signal?: AbortSignal;
11
+ }): Promise<SystemdCollection>;
@@ -0,0 +1,28 @@
1
+ import { platform } from "node:os";
2
+ import { capabilityError, locate, run } from "./command.js";
3
+ export async function collectSystemd(watchedServices, registry, options = {}) {
4
+ registry.clearKinds(["systemd_journal"]);
5
+ if ((options.platform ?? platform()) !== "linux")
6
+ return { capability: "unsupported", services: [], sources: [] };
7
+ try {
8
+ const binary = await locate("systemctl");
9
+ if (!binary)
10
+ return { capability: "not_installed", services: [], sources: [] };
11
+ const services = [];
12
+ const sources = [];
13
+ for (const unit of [...new Set(watchedServices)].slice(0, 50)) {
14
+ sources.push(registry.registerJournal(unit));
15
+ const { stdout } = await run(binary, ["show", unit, "--property=ActiveState,SubState,MainPID,NRestarts", "--no-pager"], options.signal ? { signal: options.signal } : {});
16
+ const properties = new Map(stdout.split("\n").map((line) => {
17
+ const split = line.indexOf("=");
18
+ return [line.slice(0, split), line.slice(split + 1)];
19
+ }));
20
+ services.push({ unit, active_state: properties.get("ActiveState") ?? "unknown", sub_state: properties.get("SubState") ?? "unknown" });
21
+ }
22
+ return { capability: "available", services, sources };
23
+ }
24
+ catch (error) {
25
+ registry.clearKinds(["systemd_journal"]);
26
+ return { capability: capabilityError(error), services: [], sources: [] };
27
+ }
28
+ }
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ export declare const SYSTEM_CONFIG_DIR = "/etc/theseeker-agent";
3
+ export declare const CONFIG_FILE_NAME = "config.json";
4
+ export declare const CONFIG_FILE_MODE = 384;
5
+ export declare const CONFIG_DIR_MODE = 488;
6
+ export declare const ENDPOINT_ENV = "THESEEKER_AGENT_ENDPOINT";
7
+ export declare const TOKEN_ENV = "THESEEKER_AGENT_TOKEN";
8
+ export declare const AGENT_VERSION: string;
9
+ export declare const LogLevelSchema: z.ZodEnum<{
10
+ error: "error";
11
+ debug: "debug";
12
+ info: "info";
13
+ warn: "warn";
14
+ }>;
15
+ export type LogLevel = z.infer<typeof LogLevelSchema>;
16
+ export declare const ConfigFileSchema: z.ZodObject<{
17
+ endpoint: z.ZodOptional<z.ZodString>;
18
+ token: z.ZodOptional<z.ZodString>;
19
+ pm2Home: z.ZodOptional<z.ZodString>;
20
+ logLevel: z.ZodOptional<z.ZodEnum<{
21
+ error: "error";
22
+ debug: "debug";
23
+ info: "info";
24
+ warn: "warn";
25
+ }>>;
26
+ }, z.core.$strip>;
27
+ export type ConfigFile = z.infer<typeof ConfigFileSchema>;
28
+ export interface AgentConfig {
29
+ readonly endpoint: string;
30
+ readonly token: string;
31
+ readonly pm2Home?: string | undefined;
32
+ readonly logLevel: LogLevel;
33
+ }
34
+ export interface ConfigEnvironment {
35
+ readonly platform?: string | undefined;
36
+ readonly env?: NodeJS.ProcessEnv | undefined;
37
+ readonly home?: string | undefined;
38
+ }
39
+ export declare class AgentConfigError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ export declare function systemConfigPath(): string;
43
+ export declare function userConfigPath(home?: string): string;
44
+ /** First existing readable config file, or `null` when the agent is env-only. */
45
+ export declare function readableConfigPath(options?: ConfigEnvironment): Promise<string | null>;
46
+ /** Where `install` should write: the system path on linux when its directory is writable, else the user path. */
47
+ export declare function writableConfigPath(options?: ConfigEnvironment): Promise<string>;
48
+ export declare function writeConfigFile(config: ConfigFile, path: string): Promise<void>;
49
+ export declare function readConfigFile(path: string): Promise<ConfigFile>;
50
+ /** Config file merged with the `THESEEKER_AGENT_*` overrides. Never echoes the token back to the caller's logs. */
51
+ export declare function loadConfig(options?: ConfigEnvironment): Promise<AgentConfig>;
52
+ export type LogFields = Readonly<Record<string, string | number | boolean | null | undefined>>;
53
+ export type Logger = (level: LogLevel, event: string, fields?: LogFields) => void;
54
+ /** Structured stdout-only logger. Callers pass event names and scalar fields; frame bodies and tokens are never accepted. */
55
+ export declare function createLogger(level?: LogLevel, write?: (line: string) => void): Logger;