@tetsuo-ai/agenc 0.1.0 → 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.
@@ -0,0 +1,246 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const DEFAULT_READY_TIMEOUT_MS = 2000;
9
+ const DEFAULT_POLL_MS = 25;
10
+ const READY_TIMEOUT_ENV = "AGENC_DAEMON_READY_TIMEOUT_MS";
11
+ const AUTOSTART_ENV = "AGENC_DAEMON_AUTOSTART";
12
+
13
+ const requireFromLauncher = createRequire(import.meta.url);
14
+
15
+ export function shouldAutostartDaemon(env = process.env) {
16
+ const raw = env[AUTOSTART_ENV]?.trim().toLowerCase();
17
+ if (raw === undefined || raw.length === 0) return true;
18
+ return raw !== "0" && raw !== "false" && raw !== "off";
19
+ }
20
+
21
+ export function resolveReadyTimeoutMs(env = process.env) {
22
+ const raw = env[READY_TIMEOUT_ENV]?.trim();
23
+ if (raw === undefined || raw.length === 0) return DEFAULT_READY_TIMEOUT_MS;
24
+ const parsed = Number.parseInt(raw, 10);
25
+ if (!Number.isInteger(parsed) || String(parsed) !== raw || parsed <= 0) {
26
+ throw new Error(`${READY_TIMEOUT_ENV} must be a positive integer`);
27
+ }
28
+ return parsed;
29
+ }
30
+
31
+ export function resolveAgenCHome(env = process.env, userHome = homedir()) {
32
+ const configured = env.AGENC_HOME?.trim();
33
+ return configured && configured.length > 0
34
+ ? configured
35
+ : join(userHome, ".agenc");
36
+ }
37
+
38
+ export function resolveDaemonPidPath(env = process.env, userHome = homedir()) {
39
+ return join(resolveAgenCHome(env, userHome), "daemon.pid");
40
+ }
41
+
42
+ export function resolveDaemonCookiePath(env = process.env, userHome = homedir()) {
43
+ return join(resolveAgenCHome(env, userHome), "daemon.cookie");
44
+ }
45
+
46
+ export async function readDaemonPid(pidPath, readText = readFile) {
47
+ try {
48
+ const raw = (await readText(pidPath, "utf8")).trim();
49
+ if (!/^\d+$/.test(raw)) return null;
50
+ const pid = Number.parseInt(raw, 10);
51
+ return pid > 0 ? pid : null;
52
+ } catch (error) {
53
+ if (error?.code === "ENOENT") return null;
54
+ throw error;
55
+ }
56
+ }
57
+
58
+ export function isPidRunning(pid, signalPid = process.kill) {
59
+ try {
60
+ signalPid(pid, 0);
61
+ return true;
62
+ } catch (error) {
63
+ return error?.code === "EPERM";
64
+ }
65
+ }
66
+
67
+ export function isDaemonCommand(argv) {
68
+ return argv[0] === "daemon";
69
+ }
70
+
71
+ // Dev path: the `file:`-linked @tetsuo-ai/runtime resolves locally. Returns
72
+ // null in a published install where runtime is NOT an npm dependency (it's the
73
+ // downloaded GitHub-Releases artifact instead).
74
+ export function resolveRuntimeBin(requireFn = requireFromLauncher) {
75
+ try {
76
+ const runtimeEntry = requireFn.resolve("@tetsuo-ai/runtime");
77
+ return resolve(dirname(runtimeEntry), "../bin/agenc");
78
+ } catch (error) {
79
+ if (
80
+ error?.code === "MODULE_NOT_FOUND" ||
81
+ error?.code === "ERR_MODULE_NOT_FOUND"
82
+ ) {
83
+ return null;
84
+ }
85
+ throw error;
86
+ }
87
+ }
88
+
89
+ // Resolve the runtime entry for either mode: prefer the dev file:-link, else
90
+ // ensure (download + verify + extract) the published per-platform runtime.
91
+ export async function resolveRuntimeBinAsync({
92
+ requireFn = requireFromLauncher,
93
+ ensureFn,
94
+ } = {}) {
95
+ const dev = resolveRuntimeBin(requireFn);
96
+ if (dev !== null) return dev;
97
+ const ensureRuntime =
98
+ ensureFn ?? (await import("../lib/runtime-manager.mjs")).ensureRuntime;
99
+ return ensureRuntime();
100
+ }
101
+
102
+ export async function spawnNodeScript(
103
+ scriptPath,
104
+ args,
105
+ {
106
+ env = process.env,
107
+ cwd = process.cwd(),
108
+ stdio = "inherit",
109
+ spawnFn = spawn,
110
+ } = {},
111
+ ) {
112
+ return new Promise((resolveExit, reject) => {
113
+ const child = spawnFn(process.execPath, [scriptPath, ...args], {
114
+ cwd,
115
+ env,
116
+ stdio,
117
+ });
118
+ child.once("error", reject);
119
+ child.once("exit", (code, signal) => {
120
+ if (signal !== null) {
121
+ reject(new Error(`agenc runtime exited from signal ${signal}`));
122
+ return;
123
+ }
124
+ resolveExit(code ?? 1);
125
+ });
126
+ });
127
+ }
128
+
129
+ export async function spawnDaemon(runtimeBin, options = {}) {
130
+ const exitCode = await spawnNodeScript(runtimeBin, ["daemon", "start"], {
131
+ ...options,
132
+ stdio: ["ignore", "ignore", "inherit"],
133
+ });
134
+ if (exitCode !== 0) {
135
+ throw new Error(`AgenC daemon start failed with exit code ${exitCode}`);
136
+ }
137
+ }
138
+
139
+ export async function isDaemonReady(
140
+ {
141
+ env = process.env,
142
+ userHome = homedir(),
143
+ readText = readFile,
144
+ signalPid = process.kill,
145
+ } = {},
146
+ ) {
147
+ const pidPath = resolveDaemonPidPath(env, userHome);
148
+ const cookiePath = resolveDaemonCookiePath(env, userHome);
149
+ const pid = await readDaemonPid(pidPath, readText);
150
+ if (pid === null || !isPidRunning(pid, signalPid)) return false;
151
+ try {
152
+ return (await readText(cookiePath, "utf8")).trim().length > 0;
153
+ } catch (error) {
154
+ if (error?.code === "ENOENT") return false;
155
+ throw error;
156
+ }
157
+ }
158
+
159
+ export async function waitForDaemonReady(options = {}) {
160
+ const timeoutMs = options.timeoutMs ?? resolveReadyTimeoutMs(options.env);
161
+ const pollMs = options.pollMs ?? DEFAULT_POLL_MS;
162
+ const startedAt = Date.now();
163
+ const sleep = options.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
164
+
165
+ while (Date.now() - startedAt < timeoutMs) {
166
+ if (await isDaemonReady(options)) return true;
167
+ await sleep(pollMs);
168
+ }
169
+ return isDaemonReady(options);
170
+ }
171
+
172
+ export async function ensureDaemonForLaunch({
173
+ argv = process.argv.slice(2),
174
+ env = process.env,
175
+ cwd = process.cwd(),
176
+ runtimeBin = resolveRuntimeBin(),
177
+ userHome = homedir(),
178
+ readText = readFile,
179
+ signalPid = process.kill,
180
+ spawnDaemonFn = spawnDaemon,
181
+ waitForReadyFn = waitForDaemonReady,
182
+ } = {}) {
183
+ if (isDaemonCommand(argv)) return { status: "skipped-daemon-command" };
184
+ if (!shouldAutostartDaemon(env)) return { status: "disabled" };
185
+
186
+ if (
187
+ await waitForReadyFn({
188
+ env,
189
+ userHome,
190
+ readText,
191
+ signalPid,
192
+ timeoutMs: 1,
193
+ pollMs: 1,
194
+ sleep: async () => {},
195
+ })
196
+ ) {
197
+ return { status: "already-running" };
198
+ }
199
+
200
+ await spawnDaemonFn(runtimeBin, { env, cwd });
201
+ const ready = await waitForReadyFn({ env, userHome, readText, signalPid });
202
+ if (!ready) {
203
+ throw new Error("AgenC daemon did not become ready before timeout");
204
+ }
205
+ return { status: "started" };
206
+ }
207
+
208
+ export async function main(
209
+ argv = process.argv.slice(2),
210
+ {
211
+ env = process.env,
212
+ cwd = process.cwd(),
213
+ runtimeBin,
214
+ userHome = homedir(),
215
+ } = {},
216
+ ) {
217
+ // Resolve (and, in a published install, download + verify) the runtime before
218
+ // anything tries to spawn it. Sync default is avoided: it returns null when
219
+ // runtime isn't an npm dep, which is the normal published case.
220
+ let resolvedBin = runtimeBin;
221
+ try {
222
+ resolvedBin ??= await resolveRuntimeBinAsync();
223
+ } catch (error) {
224
+ process.stderr.write(
225
+ `agenc: could not obtain runtime: ${
226
+ error instanceof Error ? error.message : String(error)
227
+ }\n`,
228
+ );
229
+ return 1;
230
+ }
231
+ try {
232
+ await ensureDaemonForLaunch({ argv, env, cwd, runtimeBin: resolvedBin, userHome });
233
+ } catch (error) {
234
+ process.stderr.write(
235
+ `agenc: daemon autostart failed: ${
236
+ error instanceof Error ? error.message : String(error)
237
+ }\n`,
238
+ );
239
+ return 1;
240
+ }
241
+ return spawnNodeScript(resolvedBin, argv, { env, cwd, stdio: "inherit" });
242
+ }
243
+
244
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
245
+ process.exitCode = await main();
246
+ }
package/README.md DELETED
@@ -1,90 +0,0 @@
1
- # agenc
2
-
3
- Public CLI and launcher for the AgenC framework.
4
-
5
- This package owns the user-facing global install surface:
6
-
7
- ```bash
8
- npm install -g @tetsuo-ai/agenc
9
- agenc onboard
10
- agenc start
11
- agenc
12
- agenc ui
13
- ```
14
-
15
- It does not expose the runtime source tree directly. Instead, it installs and
16
- launches the matching AgenC runtime artifact for the current supported
17
- platform.
18
-
19
- The supported npm install identity is `@tetsuo-ai/agenc`. The unscoped
20
- `agenc` package name is not part of the supported public release contract.
21
-
22
- Current public support is intentionally narrow:
23
-
24
- - Linux `x64`
25
- - Node `>=18.0.0`
26
-
27
- Current validated release-gate lanes:
28
-
29
- - Node `18` minimum floor
30
- - Node `20` mainline lane
31
-
32
- Production release channel:
33
-
34
- - npm package: `@tetsuo-ai/agenc`
35
- - runtime artifact host: GitHub Releases on `tetsuo-ai/agenc-core`
36
- - trust: embedded signed manifest + embedded public key + embedded trust policy
37
-
38
- After the matching runtime artifact is installed, `agenc` can continue to run
39
- offline against the local install. The npm package name is scoped, but the CLI
40
- binary remains `agenc`.
41
-
42
- `agenc` exposes two primary local operator surfaces against the same daemon:
43
-
44
- - `agenc` attaches the terminal operator console
45
- - `agenc ui` opens or prints the local dashboard URL on `/ui/`
46
-
47
- For automation or remote shells, use:
48
-
49
- ```bash
50
- agenc ui --no-open
51
- ```
52
-
53
- ## First-party connector lifecycle
54
-
55
- The first V1 connector is Telegram. It is managed through the same daemon as the
56
- CLI and dashboard; there is no separate connector service to start.
57
-
58
- ```bash
59
- agenc connector list
60
- agenc connector status telegram
61
- agenc connector add telegram --bot-token-env TELEGRAM_BOT_TOKEN
62
- agenc connector remove telegram
63
- ```
64
-
65
- For non-interactive shells or secret managers, you can pipe the bot token on
66
- stdin instead:
67
-
68
- ```bash
69
- printf '%s' "$TELEGRAM_BOT_TOKEN" | agenc connector add telegram --bot-token-stdin
70
- ```
71
-
72
- Connector health and pending-restart state are exposed through both:
73
-
74
- - `agenc connector status telegram`
75
- - `agenc ui` on the dashboard status view
76
-
77
- ## Wrapper-local runtime management
78
-
79
- ```bash
80
- agenc runtime where
81
- agenc runtime install
82
- agenc runtime update
83
- agenc runtime uninstall
84
- ```
85
-
86
- ## Development
87
-
88
- The embedded runtime manifest in `generated/` is produced by the `agenc-core`
89
- artifact preparation scripts. Local smoke tests use the same packaged manifest
90
- flow as publish/release preparation.
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runAgencRuntimeWrapper } from "../lib/cli.js";
3
-
4
- const exitCode = await runAgencRuntimeWrapper();
5
- process.exitCode = exitCode;
package/bin/agenc.js DELETED
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runAgencWrapper } from "../lib/cli.js";
3
-
4
- const exitCode = await runAgencWrapper();
5
- process.exitCode = exitCode;
@@ -1 +0,0 @@
1
- iFOPHEatEGfaMVQhLsENy9CEenablWaZLG6G5V2u37hN44hvIwTtgoK6ihiHQL3aIJXmBEM2VRdSPInlJBtMAg==
@@ -1,3 +0,0 @@
1
- -----BEGIN PUBLIC KEY-----
2
- MCowBQYDK2VwAyEAQSdfrCHY3kfLtuy5ncKApxvMz9VuNHLU9TWYYWdzedc=
3
- -----END PUBLIC KEY-----
@@ -1,9 +0,0 @@
1
- {
2
- "wrapperVersion": "0.1.0",
3
- "keyId": "local-dev",
4
- "releaseChannel": "github-releases",
5
- "releaseRepository": "tetsuo-ai/agenc-core",
6
- "releaseTag": "v0.1.0",
7
- "revokedManifestDigests": [],
8
- "revokedRuntimeVersions": []
9
- }
package/lib/cli.js DELETED
@@ -1,187 +0,0 @@
1
- import process from "node:process";
2
- import {
3
- describeRuntimeInstall,
4
- ensureRuntimeInstalled,
5
- RuntimeInstallError,
6
- spawnInstalledRuntimeBin,
7
- uninstallRuntime,
8
- } from "./runtime-manager.js";
9
-
10
- function writeLine(stream, value) {
11
- stream.write(`${value}\n`);
12
- }
13
-
14
- function buildAgencHelp() {
15
- return [
16
- "agenc [runtime-command] [options]",
17
- "agenc runtime <where|install|update|uninstall>",
18
- "",
19
- "Examples:",
20
- " agenc onboard",
21
- " agenc ui",
22
- " agenc start",
23
- " agenc status",
24
- " agenc logs",
25
- " agenc runtime where",
26
- ].join("\n");
27
- }
28
-
29
- function buildAgencRuntimeHelp() {
30
- return [
31
- "agenc-runtime [runtime-command] [options]",
32
- "",
33
- "Compatibility alias for the installed AgenC runtime CLI.",
34
- ].join("\n");
35
- }
36
-
37
- async function runRuntimeAdmin(argv, io, deps, runtimeOptions) {
38
- const command = argv[0];
39
- const force = argv.includes("--force");
40
- if (!command || command === "help" || command === "--help" || command === "-h") {
41
- writeLine(
42
- io.stdout,
43
- "agenc runtime <where|install|update|uninstall> [--force]",
44
- );
45
- return 0;
46
- }
47
-
48
- if (command === "where") {
49
- const description = await deps.describeRuntimeInstall(runtimeOptions);
50
- writeLine(
51
- io.stdout,
52
- JSON.stringify(
53
- {
54
- runtimeHome: description.runtimeHome,
55
- installed: description.installed,
56
- releaseDir: description.releaseDir,
57
- currentDir: description.currentDir,
58
- manifestSource: description.manifestSource,
59
- manifestDigest: description.manifestDigest,
60
- trustPolicy: description.trustPolicy
61
- ? {
62
- wrapperVersion: description.trustPolicy.wrapperVersion,
63
- keyId: description.trustPolicy.keyId,
64
- releaseChannel: description.trustPolicy.releaseChannel ?? null,
65
- releaseRepository:
66
- description.trustPolicy.releaseRepository ?? null,
67
- releaseTag: description.trustPolicy.releaseTag ?? null,
68
- revokedManifestDigests:
69
- description.trustPolicy.revokedManifestDigests,
70
- revokedRuntimeVersions:
71
- description.trustPolicy.revokedRuntimeVersions,
72
- }
73
- : null,
74
- selectedArtifact: description.selectedArtifact
75
- ? {
76
- runtimeVersion: description.selectedArtifact.runtimeVersion,
77
- platform: description.selectedArtifact.platform,
78
- arch: description.selectedArtifact.arch,
79
- nodeRange: description.selectedArtifact.nodeRange,
80
- }
81
- : null,
82
- },
83
- null,
84
- 2,
85
- ),
86
- );
87
- return 0;
88
- }
89
-
90
- if (command === "install" || command === "update") {
91
- const installation = await deps.ensureRuntimeInstalled({
92
- ...runtimeOptions,
93
- force: command === "update",
94
- });
95
- writeLine(
96
- io.stdout,
97
- `Installed AgenC runtime ${installation.selectedArtifact.runtimeVersion} at ${installation.releaseDir}`,
98
- );
99
- return 0;
100
- }
101
-
102
- if (command === "uninstall") {
103
- const result = await deps.uninstallRuntime({ ...runtimeOptions, force });
104
- writeLine(
105
- io.stdout,
106
- result.removed
107
- ? `Removed AgenC runtime at ${result.releaseDir}\nPreserved: ${result.preservedPaths.join(", ")}`
108
- : "No installed AgenC runtime found for the current platform",
109
- );
110
- return 0;
111
- }
112
-
113
- writeLine(io.stderr, `Unknown agenc runtime command: ${command}`);
114
- return 2;
115
- }
116
-
117
- const DEFAULT_DEPS = {
118
- ensureRuntimeInstalled,
119
- describeRuntimeInstall,
120
- uninstallRuntime,
121
- spawnInstalledRuntimeBin,
122
- };
123
-
124
- function formatRuntimeInstallError(error) {
125
- if (error instanceof RuntimeInstallError) {
126
- return error.message;
127
- }
128
- return error instanceof Error ? error.message : String(error);
129
- }
130
-
131
- export async function runAgencWrapper(options = {}, deps = DEFAULT_DEPS) {
132
- const argv = options.argv ?? process.argv.slice(2);
133
- const stdout = options.stdout ?? process.stdout;
134
- const stderr = options.stderr ?? process.stderr;
135
- const runtimeOptions = {
136
- env: options.env ?? process.env,
137
- cwd: options.cwd ?? process.cwd(),
138
- homeDir: options.homeDir,
139
- packageRoot: options.packageRoot,
140
- };
141
-
142
- try {
143
- if (argv[0] === "runtime") {
144
- return await runRuntimeAdmin(
145
- argv.slice(1),
146
- { stdout, stderr },
147
- deps,
148
- runtimeOptions,
149
- );
150
- }
151
- if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
152
- writeLine(stdout, buildAgencHelp());
153
- return 0;
154
- }
155
- return await deps.spawnInstalledRuntimeBin("agenc", argv, runtimeOptions);
156
- } catch (error) {
157
- writeLine(stderr, formatRuntimeInstallError(error));
158
- return 1;
159
- }
160
- }
161
-
162
- export async function runAgencRuntimeWrapper(options = {}, deps = DEFAULT_DEPS) {
163
- const argv = options.argv ?? process.argv.slice(2);
164
- const stdout = options.stdout ?? process.stdout;
165
- const stderr = options.stderr ?? process.stderr;
166
- const runtimeOptions = {
167
- env: options.env ?? process.env,
168
- cwd: options.cwd ?? process.cwd(),
169
- homeDir: options.homeDir,
170
- packageRoot: options.packageRoot,
171
- };
172
-
173
- try {
174
- if (argv[0] === "--help" || argv[0] === "-h" || argv[0] === "help") {
175
- writeLine(stdout, buildAgencRuntimeHelp());
176
- return 0;
177
- }
178
- return await deps.spawnInstalledRuntimeBin(
179
- "agenc-runtime",
180
- argv,
181
- runtimeOptions,
182
- );
183
- } catch (error) {
184
- writeLine(stderr, formatRuntimeInstallError(error));
185
- return 1;
186
- }
187
- }