nexus-agentd 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.
package/dist/cli.js ADDED
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+ import path from 'node:path';
3
+ import { startAgentd } from './index.js';
4
+ const configPath = resolveConfigPath(process.argv.slice(2));
5
+ const runtime = await startAgentd(configPath);
6
+ const address = runtime.server.address();
7
+ const label = typeof address === 'object' && address
8
+ ? `${address.address}:${address.port}`
9
+ : String(address);
10
+ console.log(`nexus-agentd listening on ${label}`);
11
+ let closing = false;
12
+ const close = async () => {
13
+ if (closing)
14
+ return;
15
+ closing = true;
16
+ await runtime.close();
17
+ };
18
+ process.once('SIGINT', () => void close().finally(() => process.exit(0)));
19
+ process.once('SIGTERM', () => void close().finally(() => process.exit(0)));
20
+ function resolveConfigPath(args) {
21
+ const index = args.indexOf('--config');
22
+ const value = index >= 0 ? args[index + 1] : process.env.NEXUS_AGENTD_CONFIG;
23
+ return path.resolve(value || 'nexus-agentd.json');
24
+ }
@@ -0,0 +1,3 @@
1
+ import { type AgentdConfig } from './types.js';
2
+ export declare function loadAgentdConfig(filePath: string): Promise<AgentdConfig>;
3
+ export declare function resolveSecret(value: string): string;
package/dist/config.js ADDED
@@ -0,0 +1,115 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { agentdDriverKinds } from './types.js';
4
+ export async function loadAgentdConfig(filePath) {
5
+ const absolute = path.resolve(filePath);
6
+ const raw = await readFile(absolute, 'utf8');
7
+ let value;
8
+ try {
9
+ value = JSON.parse(raw);
10
+ }
11
+ catch (error) {
12
+ throw new Error(`Invalid nexus-agentd config JSON: ${error instanceof Error ? error.message : String(error)}`);
13
+ }
14
+ if (!isRecord(value))
15
+ throw new Error('nexus-agentd config root must be an object');
16
+ const listen = isRecord(value.listen) ? value.listen : {};
17
+ const host = stringValue(listen.host) || '127.0.0.1';
18
+ const port = numberValue(listen.port, 8787, 1, 65535);
19
+ const authToken = resolveSecret(stringValue(value.authToken));
20
+ if (!authToken)
21
+ throw new Error('nexus-agentd authToken is required');
22
+ const workspaceRoots = arrayOfStrings(value.workspaceRoots);
23
+ if (!workspaceRoots.length) {
24
+ throw new Error('nexus-agentd workspaceRoots must contain at least one path');
25
+ }
26
+ if (!isRecord(value.agents)) {
27
+ throw new Error('nexus-agentd agents must be an object');
28
+ }
29
+ const agents = {};
30
+ for (const [id, input] of Object.entries(value.agents)) {
31
+ if (!isRecord(input))
32
+ throw new Error(`Agent config must be an object: ${id}`);
33
+ const driver = stringValue(input.driver) || inferDriver(id);
34
+ if (!isAgentdDriverKind(driver)) {
35
+ throw new Error(`Unsupported nexus-agentd driver: ${driver || id}`);
36
+ }
37
+ const permissionPolicy = stringValue(input.permissionPolicy) || 'ask';
38
+ if (permissionPolicy !== 'ask' && permissionPolicy !== 'deny') {
39
+ throw new Error(`Invalid permissionPolicy for ${id}`);
40
+ }
41
+ agents[id] = {
42
+ driver,
43
+ name: optionalString(input.name),
44
+ description: optionalString(input.description),
45
+ enabled: input.enabled !== false,
46
+ command: optionalString(input.command),
47
+ args: input.args === undefined ? undefined : arrayOfStrings(input.args),
48
+ inheritEnv: input.inheritEnv === undefined
49
+ ? undefined
50
+ : arrayOfStrings(input.inheritEnv),
51
+ env: recordOfStrings(input.env),
52
+ permissionPolicy,
53
+ permissionTimeoutMs: numberValue(input.permissionTimeoutMs, 15 * 60 * 1000, 1000, 24 * 60 * 60 * 1000)
54
+ };
55
+ }
56
+ return {
57
+ listen: { host, port },
58
+ authToken,
59
+ workspaceRoots,
60
+ maxRequestBytes: numberValue(value.maxRequestBytes, 1024 * 1024, 1024, 16 * 1024 * 1024),
61
+ maxEventsPerSession: numberValue(value.maxEventsPerSession, 2048, 64, 100_000),
62
+ maxOutputChars: numberValue(value.maxOutputChars, 512 * 1024, 16 * 1024, 16 * 1024 * 1024),
63
+ sessionTtlMs: numberValue(value.sessionTtlMs, 24 * 60 * 60 * 1000, 60_000, 30 * 24 * 60 * 60 * 1000),
64
+ agents
65
+ };
66
+ }
67
+ export function resolveSecret(value) {
68
+ if (!value.startsWith('env:'))
69
+ return value;
70
+ const name = value.slice(4).trim();
71
+ if (!name)
72
+ throw new Error('Secret environment variable name is empty');
73
+ const secret = process.env[name];
74
+ if (!secret)
75
+ throw new Error(`Secret environment variable is missing: ${name}`);
76
+ return secret;
77
+ }
78
+ function inferDriver(id) {
79
+ const value = id.trim().toLowerCase();
80
+ return isAgentdDriverKind(value) ? value : '';
81
+ }
82
+ function isAgentdDriverKind(value) {
83
+ return agentdDriverKinds.includes(value);
84
+ }
85
+ function isRecord(value) {
86
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
87
+ }
88
+ function stringValue(value) {
89
+ return typeof value === 'string' ? value.trim() : '';
90
+ }
91
+ function optionalString(value) {
92
+ return stringValue(value) || undefined;
93
+ }
94
+ function arrayOfStrings(value) {
95
+ if (!Array.isArray(value))
96
+ return [];
97
+ return value.map(stringValue).filter(Boolean);
98
+ }
99
+ function recordOfStrings(value) {
100
+ if (!isRecord(value))
101
+ return undefined;
102
+ const result = {};
103
+ for (const [key, item] of Object.entries(value)) {
104
+ if (typeof item !== 'string')
105
+ continue;
106
+ result[key] = item;
107
+ }
108
+ return result;
109
+ }
110
+ function numberValue(value, fallback, min, max) {
111
+ const number = Number(value);
112
+ if (!Number.isFinite(number))
113
+ return fallback;
114
+ return Math.min(max, Math.max(min, Math.trunc(number)));
115
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ export declare function createClaudeDriver(id: string, config: AgentdDriverConfig): import("./types.js").AgentDriver;
@@ -0,0 +1,9 @@
1
+ import { createStdioAcpDriver } from './stdio.js';
2
+ export function createClaudeDriver(id, config) {
3
+ return createStdioAcpDriver(id, config, {
4
+ name: 'Claude Code',
5
+ description: 'Claude Code through the official Claude Agent ACP adapter',
6
+ command: 'claude-agent-acp',
7
+ args: []
8
+ });
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ export declare function createCodexDriver(id: string, config: AgentdDriverConfig): import("./types.js").AgentDriver;
@@ -0,0 +1,9 @@
1
+ import { createStdioAcpDriver } from './stdio.js';
2
+ export function createCodexDriver(id, config) {
3
+ return createStdioAcpDriver(id, config, {
4
+ name: 'Codex',
5
+ description: 'Codex CLI through the official Codex ACP adapter',
6
+ command: 'codex-acp',
7
+ args: []
8
+ });
9
+ }
@@ -0,0 +1,4 @@
1
+ import type { AgentdConfig } from '../types.js';
2
+ import type { AgentDriver } from './types.js';
3
+ export declare function createDriverRegistry(config: AgentdConfig): Map<string, AgentDriver>;
4
+ export type { AgentDriver } from './types.js';
@@ -0,0 +1,30 @@
1
+ import { createClaudeDriver } from './claude.js';
2
+ import { createCodexDriver } from './codex.js';
3
+ import { createOpenCodeDriver } from './opencode.js';
4
+ import { createOpenClawDriver } from './openclaw.js';
5
+ import { createPiDriver } from './pi.js';
6
+ export function createDriverRegistry(config) {
7
+ const drivers = new Map();
8
+ for (const [id, driver] of Object.entries(config.agents)) {
9
+ if (driver.enabled === false)
10
+ continue;
11
+ switch (driver.driver) {
12
+ case 'opencode':
13
+ drivers.set(id, createOpenCodeDriver(id, driver));
14
+ break;
15
+ case 'claude':
16
+ drivers.set(id, createClaudeDriver(id, driver));
17
+ break;
18
+ case 'codex':
19
+ drivers.set(id, createCodexDriver(id, driver));
20
+ break;
21
+ case 'pi':
22
+ drivers.set(id, createPiDriver(id, driver));
23
+ break;
24
+ case 'openclaw':
25
+ drivers.set(id, createOpenClawDriver(id, driver));
26
+ break;
27
+ }
28
+ }
29
+ return drivers;
30
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ export declare function createOpenClawDriver(id: string, config: AgentdDriverConfig): import("./types.js").AgentDriver;
@@ -0,0 +1,13 @@
1
+ import { createStdioAcpDriver } from './stdio.js';
2
+ export function createOpenClawDriver(id, config) {
3
+ return createStdioAcpDriver(id, config, {
4
+ name: 'OpenClaw',
5
+ description: 'OpenClaw Gateway through its native ACP stdio bridge',
6
+ command: 'openclaw',
7
+ args: ['acp'],
8
+ env: {
9
+ OPENCLAW_HIDE_BANNER: '1',
10
+ OPENCLAW_SUPPRESS_NOTES: '1'
11
+ }
12
+ });
13
+ }
@@ -0,0 +1,2 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ export declare function createOpenCodeDriver(id: string, config: AgentdDriverConfig): import("./types.js").AgentDriver;
@@ -0,0 +1,9 @@
1
+ import { createStdioAcpDriver } from './stdio.js';
2
+ export function createOpenCodeDriver(id, config) {
3
+ return createStdioAcpDriver(id, config, {
4
+ name: 'OpenCode',
5
+ description: 'OpenCode Coding Agent through its native ACP stdio server',
6
+ command: 'opencode',
7
+ args: ['acp']
8
+ });
9
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ export declare function createPiDriver(id: string, config: AgentdDriverConfig): {
3
+ probe(): Promise<import("../types.js").AgentdAgentView>;
4
+ id: string;
5
+ name: string;
6
+ description?: string;
7
+ command: string;
8
+ args: string[];
9
+ env: NodeJS.ProcessEnv;
10
+ permissionPolicy: import("../types.js").PermissionPolicy;
11
+ permissionTimeoutMs: number;
12
+ spawn(workspace: string): import("child_process").ChildProcessWithoutNullStreams;
13
+ };
@@ -0,0 +1,31 @@
1
+ import { createStdioAcpDriver, probeCommand } from './stdio.js';
2
+ export function createPiDriver(id, config) {
3
+ const driver = createStdioAcpDriver(id, config, {
4
+ name: 'Pi',
5
+ description: 'Pi coding agent through the pi-acp adapter',
6
+ command: 'pi-acp',
7
+ args: []
8
+ });
9
+ const probeAdapter = driver.probe.bind(driver);
10
+ return {
11
+ ...driver,
12
+ async probe() {
13
+ const adapter = await probeAdapter();
14
+ if (!adapter.ready)
15
+ return adapter;
16
+ const command = driver.env.PI_ACP_PI_COMMAND || 'pi';
17
+ try {
18
+ const version = await probeCommand(command, ['--version'], driver.env, 'Pi runtime');
19
+ return { ...adapter, version };
20
+ }
21
+ catch (error) {
22
+ return {
23
+ ...adapter,
24
+ ready: false,
25
+ version: undefined,
26
+ error: error instanceof Error ? error.message : String(error)
27
+ };
28
+ }
29
+ }
30
+ };
31
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentdDriverConfig } from '../types.js';
2
+ import type { AgentDriver } from './types.js';
3
+ export interface StdioAcpDriverDefaults {
4
+ name: string;
5
+ description: string;
6
+ command: string;
7
+ args: string[];
8
+ probeArgs?: string[];
9
+ env?: Record<string, string>;
10
+ }
11
+ export declare function createStdioAcpDriver(id: string, config: AgentdDriverConfig, defaults: StdioAcpDriverDefaults): AgentDriver;
12
+ export declare function buildEnvironment(config: AgentdDriverConfig, defaults?: Record<string, string>): NodeJS.ProcessEnv;
13
+ export declare function probeCommand(command: string, args: string[], env: NodeJS.ProcessEnv, label: string): Promise<string>;
@@ -0,0 +1,114 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { resolveSecret } from '../config.js';
3
+ const BASE_ENV_KEYS = [
4
+ 'PATH',
5
+ 'HOME',
6
+ 'USER',
7
+ 'XDG_CONFIG_HOME',
8
+ 'XDG_DATA_HOME',
9
+ 'SHELL',
10
+ 'TMP',
11
+ 'TMPDIR',
12
+ 'TEMP',
13
+ 'LANG',
14
+ 'LC_ALL'
15
+ ];
16
+ export function createStdioAcpDriver(id, config, defaults) {
17
+ const command = config.command || defaults.command;
18
+ const args = config.args === undefined ? [...defaults.args] : [...config.args];
19
+ const env = buildEnvironment(config, defaults.env);
20
+ const name = config.name || defaults.name;
21
+ const description = config.description || defaults.description;
22
+ return {
23
+ id,
24
+ name,
25
+ description,
26
+ command,
27
+ args,
28
+ env,
29
+ permissionPolicy: config.permissionPolicy || 'ask',
30
+ permissionTimeoutMs: config.permissionTimeoutMs || 15 * 60 * 1000,
31
+ async probe() {
32
+ try {
33
+ const version = await probeCommand(command, defaults.probeArgs || ['--version'], env, name);
34
+ return {
35
+ id,
36
+ name,
37
+ description,
38
+ protocol: 'acp',
39
+ ready: true,
40
+ version
41
+ };
42
+ }
43
+ catch (error) {
44
+ return {
45
+ id,
46
+ name,
47
+ description,
48
+ protocol: 'acp',
49
+ ready: false,
50
+ error: error instanceof Error ? error.message : String(error)
51
+ };
52
+ }
53
+ },
54
+ spawn(workspace) {
55
+ return spawn(command, args, {
56
+ cwd: workspace,
57
+ env,
58
+ stdio: ['pipe', 'pipe', 'pipe']
59
+ });
60
+ }
61
+ };
62
+ }
63
+ export function buildEnvironment(config, defaults = {}) {
64
+ const result = {};
65
+ for (const key of new Set([...BASE_ENV_KEYS, ...(config.inheritEnv || [])])) {
66
+ const value = process.env[key];
67
+ if (value !== undefined)
68
+ result[key] = value;
69
+ }
70
+ Object.assign(result, defaults);
71
+ for (const [key, value] of Object.entries(config.env || {})) {
72
+ result[key] = resolveSecret(value);
73
+ }
74
+ return result;
75
+ }
76
+ export function probeCommand(command, args, env, label) {
77
+ return new Promise((resolve, reject) => {
78
+ const child = spawn(command, args, {
79
+ env,
80
+ stdio: ['ignore', 'pipe', 'pipe']
81
+ });
82
+ let output = '';
83
+ let settled = false;
84
+ const finish = (error) => {
85
+ if (settled)
86
+ return;
87
+ settled = true;
88
+ clearTimeout(timer);
89
+ if (error)
90
+ reject(error);
91
+ else
92
+ resolve(output.trim().split(/\r?\n/)[0] || 'unknown');
93
+ };
94
+ const timer = setTimeout(() => {
95
+ child.kill();
96
+ finish(new Error(`${label} probe timed out`));
97
+ }, 5000);
98
+ child.stdout.on('data', (chunk) => {
99
+ output = `${output}${String(chunk)}`.slice(-8192);
100
+ });
101
+ child.stderr.on('data', (chunk) => {
102
+ output = `${output}${String(chunk)}`.slice(-8192);
103
+ });
104
+ child.once('error', (error) => finish(error));
105
+ child.once('exit', (code, signal) => {
106
+ if (code === 0)
107
+ finish();
108
+ else {
109
+ finish(new Error(output.trim() ||
110
+ `${label} probe exited with ${signal || code || 'unknown'}`));
111
+ }
112
+ });
113
+ });
114
+ }
@@ -0,0 +1,14 @@
1
+ import type { ChildProcessWithoutNullStreams } from 'node:child_process';
2
+ import type { AgentdAgentView, PermissionPolicy } from '../types.js';
3
+ export interface AgentDriver {
4
+ id: string;
5
+ name: string;
6
+ description?: string;
7
+ command: string;
8
+ args: string[];
9
+ env: NodeJS.ProcessEnv;
10
+ permissionPolicy: PermissionPolicy;
11
+ permissionTimeoutMs: number;
12
+ probe(): Promise<AgentdAgentView>;
13
+ spawn(workspace: string): ChildProcessWithoutNullStreams;
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { AgentdEvent, AgentdEventType } from './types.js';
2
+ export declare class SessionEventLog {
3
+ private readonly sessionId;
4
+ private readonly maxEvents;
5
+ private sequence;
6
+ private events;
7
+ private listeners;
8
+ constructor(sessionId: string, maxEvents: number);
9
+ append(type: AgentdEventType, data?: unknown): AgentdEvent;
10
+ after(id?: string): AgentdEvent[];
11
+ subscribe(listener: (event: AgentdEvent) => void): () => boolean;
12
+ get lastId(): string | undefined;
13
+ }
package/dist/events.js ADDED
@@ -0,0 +1,40 @@
1
+ export class SessionEventLog {
2
+ sessionId;
3
+ maxEvents;
4
+ sequence = 0;
5
+ events = [];
6
+ listeners = new Set();
7
+ constructor(sessionId, maxEvents) {
8
+ this.sessionId = sessionId;
9
+ this.maxEvents = maxEvents;
10
+ }
11
+ append(type, data) {
12
+ const event = {
13
+ id: String(++this.sequence),
14
+ sessionId: this.sessionId,
15
+ type,
16
+ timestamp: Date.now(),
17
+ data
18
+ };
19
+ this.events.push(event);
20
+ if (this.events.length > this.maxEvents) {
21
+ this.events.splice(0, this.events.length - this.maxEvents);
22
+ }
23
+ for (const listener of this.listeners)
24
+ listener(structuredClone(event));
25
+ return event;
26
+ }
27
+ after(id) {
28
+ const sequence = Number(id || 0);
29
+ return this.events
30
+ .filter((event) => Number(event.id) > sequence)
31
+ .map((event) => structuredClone(event));
32
+ }
33
+ subscribe(listener) {
34
+ this.listeners.add(listener);
35
+ return () => this.listeners.delete(listener);
36
+ }
37
+ get lastId() {
38
+ return this.events.at(-1)?.id;
39
+ }
40
+ }
@@ -0,0 +1,13 @@
1
+ import type { Server } from 'node:http';
2
+ import { SessionManager } from './session.js';
3
+ export declare function startAgentd(configPath: string): Promise<{
4
+ config: import("./types.js").AgentdConfig;
5
+ server: Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
6
+ sessions: SessionManager;
7
+ close(): Promise<void>;
8
+ }>;
9
+ export * from './config.js';
10
+ export * from './server.js';
11
+ export * from './session.js';
12
+ export * from './types.js';
13
+ export * from './workspace.js';
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { loadAgentdConfig } from './config.js';
2
+ import { createDriverRegistry } from './drivers/index.js';
3
+ import { createAgentdServer } from './server.js';
4
+ import { SessionManager } from './session.js';
5
+ import { WorkspacePolicy } from './workspace.js';
6
+ export async function startAgentd(configPath) {
7
+ const config = await loadAgentdConfig(configPath);
8
+ const workspacePolicy = await WorkspacePolicy.create(config.workspaceRoots);
9
+ const sessions = new SessionManager(config, workspacePolicy, createDriverRegistry(config));
10
+ sessions.startCleanup();
11
+ const server = createAgentdServer(config, sessions);
12
+ await listen(server, config.listen.port, config.listen.host);
13
+ return {
14
+ config,
15
+ server,
16
+ sessions,
17
+ async close() {
18
+ await closeServer(server);
19
+ await sessions.shutdown();
20
+ }
21
+ };
22
+ }
23
+ function listen(server, port, host) {
24
+ return new Promise((resolve, reject) => {
25
+ const error = (value) => {
26
+ server.off('listening', listening);
27
+ reject(value);
28
+ };
29
+ const listening = () => {
30
+ server.off('error', error);
31
+ resolve();
32
+ };
33
+ server.once('error', error);
34
+ server.once('listening', listening);
35
+ server.listen(port, host);
36
+ });
37
+ }
38
+ function closeServer(server) {
39
+ return new Promise((resolve, reject) => {
40
+ server.close((error) => (error ? reject(error) : resolve()));
41
+ server.closeIdleConnections?.();
42
+ });
43
+ }
44
+ export * from './config.js';
45
+ export * from './server.js';
46
+ export * from './session.js';
47
+ export * from './types.js';
48
+ export * from './workspace.js';
@@ -0,0 +1,4 @@
1
+ import http from 'node:http';
2
+ import type { AgentdConfig } from './types.js';
3
+ import { SessionManager } from './session.js';
4
+ export declare function createAgentdServer(config: AgentdConfig, sessions: SessionManager): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;