@yeaft/webchat-agent 1.0.415 → 1.0.416

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,189 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+
5
+ export const DEFAULT_AGENT_IMAGE = 'ghcr.io/yeaft/yeaft-web-code-agent-agent:dev';
6
+ const NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}$/;
7
+
8
+ export class ContainerAgentError extends Error {
9
+ constructor(code, message = code) {
10
+ super(message);
11
+ this.name = 'ContainerAgentError';
12
+ this.code = code;
13
+ }
14
+ }
15
+
16
+ export function normalizeContainerAgentName(name) {
17
+ const value = String(name || '').trim();
18
+ if (!NAME_PATTERN.test(value)) throw new ContainerAgentError('CONTAINER_AGENT_INVALID_NAME');
19
+ return value;
20
+ }
21
+
22
+ export function containerNameForAgent(name) {
23
+ return `yeaft-agent-${normalizeContainerAgentName(name)}`;
24
+ }
25
+
26
+ export async function runDocker(args, { spawnImpl = spawn, allowFailure = false, stdout = 'pipe' } = {}) {
27
+ return new Promise((resolvePromise, reject) => {
28
+ const child = spawnImpl('docker', args, {
29
+ stdio: ['ignore', stdout, 'pipe'],
30
+ windowsHide: true,
31
+ });
32
+ const output = [];
33
+ const errors = [];
34
+ child.stdout?.on('data', chunk => output.push(chunk));
35
+ child.stderr?.on('data', chunk => errors.push(chunk));
36
+ child.once('error', error => reject(new ContainerAgentError('CONTAINER_AGENT_DOCKER_UNAVAILABLE', error.message)));
37
+ child.once('close', code => {
38
+ const result = {
39
+ code: code ?? 1,
40
+ stdout: Buffer.concat(output).toString('utf8').trim(),
41
+ stderr: Buffer.concat(errors).toString('utf8').trim(),
42
+ };
43
+ if (result.code === 0 || allowFailure) resolvePromise(result);
44
+ else reject(new ContainerAgentError('CONTAINER_AGENT_DOCKER_FAILED', result.stderr || `docker ${args[0]} failed`));
45
+ });
46
+ });
47
+ }
48
+
49
+ export async function writeAgentSecretFile(path, secret) {
50
+ const value = String(secret || '').trim();
51
+ if (!value) throw new ContainerAgentError('CONTAINER_AGENT_SECRET_REQUIRED');
52
+ const absolute = resolve(path);
53
+ await mkdir(dirname(absolute), { recursive: true, mode: 0o700 });
54
+ await writeFile(absolute, `${value}\n`, { mode: 0o600 });
55
+ await chmod(absolute, 0o600);
56
+ return absolute;
57
+ }
58
+
59
+ export function buildCreateArgs({
60
+ name,
61
+ serverUrl,
62
+ secretFile,
63
+ image = DEFAULT_AGENT_IMAGE,
64
+ dataVolume,
65
+ workspaceVolume,
66
+ restart = 'unless-stopped',
67
+ }) {
68
+ const agentName = normalizeContainerAgentName(name);
69
+ if (!String(serverUrl || '').match(/^wss?:\/\//)) {
70
+ throw new ContainerAgentError('CONTAINER_AGENT_INVALID_SERVER_URL');
71
+ }
72
+ if (!secretFile) throw new ContainerAgentError('CONTAINER_AGENT_SECRET_REQUIRED');
73
+ const containerName = containerNameForAgent(agentName);
74
+ const safeImage = String(image || '').trim();
75
+ if (!safeImage || safeImage.startsWith('-')) throw new ContainerAgentError('CONTAINER_AGENT_INVALID_IMAGE');
76
+ return [
77
+ 'create', '--name', containerName,
78
+ '--label', 'io.yeaft.container-agent=true',
79
+ '--label', `io.yeaft.agent-name=${agentName}`,
80
+ '--restart', restart,
81
+ '--init',
82
+ '--mount', `type=volume,src=${dataVolume || `${containerName}-data`},dst=/home/yeaft/.yeaft`,
83
+ '--mount', `type=volume,src=${workspaceVolume || `${containerName}-workspace`},dst=/workspace`,
84
+ '--mount', `type=bind,src=${resolve(secretFile)},dst=/run/yeaft-host-secret,readonly`,
85
+ '--env', `SERVER_URL=${serverUrl}`,
86
+ '--env', `AGENT_NAME=${agentName}`,
87
+ '--env', 'AGENT_SECRET_FILE=/run/yeaft-host-secret',
88
+ '--env', 'YEAFT_DIR=/home/yeaft/.yeaft',
89
+ '--env', 'WORK_DIR=/workspace',
90
+ safeImage,
91
+ ];
92
+ }
93
+
94
+ /**
95
+ * Verify that the Docker client can reach a daemon before the Server advertises
96
+ * container Agent lifecycle support.
97
+ *
98
+ * @param {object} options runDocker overrides used by tests and alternate runtimes
99
+ * @returns {Promise<{serverVersion: string|null}>}
100
+ */
101
+ export async function checkContainerAgentRuntime(options = {}) {
102
+ const result = await runDocker(['version', '--format', '{{.Server.Version}}'], options);
103
+ return { serverVersion: result.stdout || null };
104
+ }
105
+
106
+ export async function inspectContainerAgent(name, options = {}) {
107
+ const result = await runDocker([
108
+ 'inspect', '--format', '{{json .State}}', containerNameForAgent(name),
109
+ ], { ...options, allowFailure: true });
110
+ if (result.code !== 0) {
111
+ if (/no such (object|container)/i.test(result.stderr)) {
112
+ return { exists: false, status: 'absent', running: false };
113
+ }
114
+ throw new ContainerAgentError('CONTAINER_AGENT_DOCKER_FAILED', result.stderr || 'docker inspect failed');
115
+ }
116
+ try {
117
+ const state = JSON.parse(result.stdout);
118
+ return {
119
+ exists: true,
120
+ status: state.Status || 'unknown',
121
+ running: state.Running === true,
122
+ startedAt: state.StartedAt || null,
123
+ error: state.Error || null,
124
+ };
125
+ } catch {
126
+ throw new ContainerAgentError('CONTAINER_AGENT_INVALID_DOCKER_RESPONSE');
127
+ }
128
+ }
129
+
130
+ export async function createContainerAgent(options, runtime = {}) {
131
+ const current = await inspectContainerAgent(options.name, runtime);
132
+ if (current.exists) throw new ContainerAgentError('CONTAINER_AGENT_ALREADY_EXISTS');
133
+ const containerName = containerNameForAgent(options.name);
134
+ await runDocker(buildCreateArgs(options), runtime);
135
+ try {
136
+ await runDocker(['start', containerName], runtime);
137
+ } catch (error) {
138
+ await runDocker(['rm', '-f', containerName], { ...runtime, allowFailure: true });
139
+ throw error;
140
+ }
141
+ return inspectContainerAgent(options.name, runtime);
142
+ }
143
+
144
+ export async function startContainerAgent(name, runtime = {}) {
145
+ await runDocker(['start', containerNameForAgent(name)], runtime);
146
+ return inspectContainerAgent(name, runtime);
147
+ }
148
+
149
+ export async function stopContainerAgent(name, runtime = {}) {
150
+ await runDocker(['stop', '--time', '10', containerNameForAgent(name)], runtime);
151
+ return inspectContainerAgent(name, runtime);
152
+ }
153
+
154
+ function isMissingDockerVolume(stderr) {
155
+ return /no such volume/i.test(String(stderr || ''));
156
+ }
157
+
158
+ export async function removeContainerAgent(name, { removeVolumes = true, ...runtime } = {}) {
159
+ const containerName = containerNameForAgent(name);
160
+ const current = await inspectContainerAgent(name, runtime);
161
+ if (current.exists) await runDocker(['rm', '-f', containerName], runtime);
162
+ if (removeVolumes) {
163
+ for (const volume of [`${containerName}-data`, `${containerName}-workspace`]) {
164
+ const result = await runDocker(['volume', 'rm', volume], {
165
+ ...runtime,
166
+ allowFailure: true,
167
+ });
168
+ if (result.code !== 0 && !isMissingDockerVolume(result.stderr)) {
169
+ throw new ContainerAgentError(
170
+ 'CONTAINER_AGENT_DOCKER_FAILED',
171
+ result.stderr || `docker volume rm ${volume} failed`,
172
+ );
173
+ }
174
+ }
175
+ }
176
+ return { exists: false, status: 'absent', running: false };
177
+ }
178
+
179
+ export async function logsContainerAgent(name, { follow = false, ...runtime } = {}) {
180
+ const args = ['logs'];
181
+ if (follow) args.push('--follow');
182
+ args.push(containerNameForAgent(name));
183
+ return runDocker(args, { ...runtime, stdout: follow ? 'inherit' : 'pipe' });
184
+ }
185
+
186
+ export async function readSecretInput({ secret, secretFile }) {
187
+ if (secretFile) return (await readFile(resolve(secretFile), 'utf8')).trim();
188
+ return String(secret || '').trim();
189
+ }
@@ -1 +1 @@
1
- {"version":"1.0.415"}
1
+ {"version":"1.0.416"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.415",
3
+ "version": "1.0.416",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,25 +1,44 @@
1
1
  import { cpSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
2
- import { dirname, join } from 'path';
2
+ import { dirname, join, resolve } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
 
5
- const agentDir = join(dirname(fileURLToPath(import.meta.url)), '..');
6
- const rootDir = join(agentDir, '..');
7
- const runtimeDir = join(agentDir, 'local-runtime');
5
+ const scriptPath = fileURLToPath(import.meta.url);
6
+ const defaultAgentDir = join(dirname(scriptPath), '..');
8
7
 
9
- rmSync(runtimeDir, { recursive: true, force: true });
10
- mkdirSync(runtimeDir, { recursive: true });
11
- const excludedServerPaths = new Set([
12
- join(rootDir, 'server', 'node_modules'),
13
- join(rootDir, 'server', 'data'),
14
- join(rootDir, 'server', '.env'),
15
- join(rootDir, 'server', 'user.json'),
16
- join(rootDir, 'server', 'users.json'),
17
- ]);
18
- cpSync(join(rootDir, 'server'), join(runtimeDir, 'server'), {
19
- recursive: true,
20
- filter: source => !excludedServerPaths.has(source),
21
- });
22
- cpSync(join(rootDir, 'web', 'dist'), join(runtimeDir, 'web'), { recursive: true });
8
+ /**
9
+ * Build the Server and Web payload embedded in the published Agent package.
10
+ * Server imports that cross into agent/ must be copied explicitly so the
11
+ * packaged local runtime has the same module boundary as the source tree.
12
+ */
13
+ export function prepareLocalRuntime({
14
+ agentDir = defaultAgentDir,
15
+ rootDir = join(agentDir, '..'),
16
+ runtimeDir = join(agentDir, 'local-runtime'),
17
+ } = {}) {
18
+ rmSync(runtimeDir, { recursive: true, force: true });
19
+ mkdirSync(runtimeDir, { recursive: true });
20
+ const excludedServerPaths = new Set([
21
+ join(rootDir, 'server', 'node_modules'),
22
+ join(rootDir, 'server', 'data'),
23
+ join(rootDir, 'server', '.env'),
24
+ join(rootDir, 'server', 'user.json'),
25
+ join(rootDir, 'server', 'users.json'),
26
+ ]);
27
+ cpSync(join(rootDir, 'server'), join(runtimeDir, 'server'), {
28
+ recursive: true,
29
+ filter: source => !excludedServerPaths.has(source),
30
+ });
31
+ cpSync(join(rootDir, 'web', 'dist'), join(runtimeDir, 'web'), { recursive: true });
23
32
 
24
- const { version } = JSON.parse(readFileSync(join(agentDir, 'package.json'), 'utf8'));
25
- writeFileSync(join(runtimeDir, 'version.json'), `${JSON.stringify({ version })}\n`);
33
+ const runtimeAgentDir = join(runtimeDir, 'agent');
34
+ mkdirSync(runtimeAgentDir, { recursive: true });
35
+ cpSync(join(agentDir, 'container-manager.js'), join(runtimeAgentDir, 'container-manager.js'));
36
+
37
+ const { version } = JSON.parse(readFileSync(join(agentDir, 'package.json'), 'utf8'));
38
+ writeFileSync(join(runtimeDir, 'version.json'), `${JSON.stringify({ version })}\n`);
39
+ return runtimeDir;
40
+ }
41
+
42
+ if (process.argv[1] && resolve(process.argv[1]) === resolve(scriptPath)) {
43
+ prepareLocalRuntime();
44
+ }