wtt-connect 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,510 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { normalizeAdapterName } from './adapters/index.js';
7
+
8
+ const DEFAULT_BASE_URL = 'https://www.waxbyte.com';
9
+ const DEFAULT_MODE = 'full-auto';
10
+ const VALID_MODES = new Set(['suggest', 'auto-edit', 'full-auto', 'yolo']);
11
+
12
+ export function resolveProfileEnvFile(profile) {
13
+ if (!profile) return '';
14
+ return profileEnvFile(sanitizeProfile(profile));
15
+ }
16
+
17
+ export async function up(argv) {
18
+ const [agentType, agentId, token] = argv._ || [];
19
+ const adapter = normalizeAdapterName(argv.adapter || agentType || '');
20
+ if (!adapter || !agentId || !token) {
21
+ throw new Error('usage: wtt-connect up <agenttype> <agent_id> <token> [--mode full-auto|auto-edit|suggest|yolo]');
22
+ }
23
+
24
+ const profile = sanitizeProfile(argv.profile || argv.name || `${agentId}-${adapter}`);
25
+ const mode = argv.mode || process.env.WTT_CONNECT_MODE || DEFAULT_MODE;
26
+ if (!VALID_MODES.has(mode)) throw new Error(`invalid --mode: ${mode}`);
27
+ if (mode === 'yolo' && !argv.allowYolo && !argv.yes) {
28
+ throw new Error('--mode yolo requires --allow-yolo or --yes');
29
+ }
30
+
31
+ const configDir = profilesDir();
32
+ const stateDir = path.resolve(argv.stateDir || defaultStateDir(profile));
33
+ const envFile = path.resolve(argv.envFile || profileEnvFile(profile));
34
+ const root = packageRoot();
35
+ const workDir = path.resolve(argv.workdir || argv.workDir || process.cwd());
36
+ const baseUrl = argv.baseUrl || process.env.WTT_BASE_URL || DEFAULT_BASE_URL;
37
+ const nodeBin = argv.nodeBin || process.execPath;
38
+ const codexBin = argv.codexBin || findBinary('codex');
39
+ const claudeBin = argv.claudeBin || findBinary('claude');
40
+
41
+ ensureSupportedPlatform();
42
+ ensureDir(configDir);
43
+ ensureDir(path.dirname(envFile));
44
+ ensureDir(stateDir);
45
+
46
+ const profileValues = {
47
+ WTT_BASE_URL: baseUrl,
48
+ WTT_AGENT_ID: agentId,
49
+ WTT_TOKEN: token,
50
+ WTT_CONNECT_ADAPTER: adapter,
51
+ WTT_CONNECT_ADAPTERS: adapter,
52
+ WTT_CONNECT_WORKDIR: workDir,
53
+ WTT_CONNECT_MODE: mode,
54
+ WTT_CONNECT_ALLOW_YOLO: mode === 'yolo' || argv.allowYolo || argv.yes ? '1' : '',
55
+ WTT_CONNECT_ENABLE_CHAT: '1',
56
+ WTT_CONNECT_PUBLISH_PROGRESS: argv.publishProgress ? '1' : '0',
57
+ WTT_CONNECT_TASK_TIMEOUT_SECONDS: String(argv.timeoutSeconds || (argv.timeout ? Math.round(argv.timeout / 1000) : 3600)),
58
+ WTT_CONNECT_STATE_DIR: stateDir,
59
+ WTT_REQUIRE_COMMIT_PUSH: '1',
60
+ WTT_CODEX_BIN: codexBin,
61
+ WTT_CLAUDE_BIN: claudeBin,
62
+ };
63
+ writeProfileEnv(envFile, {
64
+ ...profileValues,
65
+ ...passthroughEnv(process.env, new Set(Object.keys(profileValues))),
66
+ });
67
+
68
+ const service = installService({ profile, envFile, nodeBin, root, stateDir });
69
+ if (!argv.noStart) startService(profile);
70
+ if (argv.enableLinger && process.platform === 'linux') enableLinger();
71
+ if (argv.enableLinger && process.platform === 'darwin') {
72
+ console.warn('warning: --enable-linger is Linux-only; macOS LaunchAgents start at user login');
73
+ }
74
+
75
+ console.log(`profile: ${profile}`);
76
+ console.log(`env file: ${envFile}`);
77
+ console.log(`state dir: ${stateDir}`);
78
+ console.log(`service: ${service.name}`);
79
+ console.log(`${service.kind} file: ${service.path}`);
80
+ if (!argv.noStart) status({ _: [profile] });
81
+ }
82
+
83
+ export function listProfiles() {
84
+ const dir = profilesDir();
85
+ if (!fs.existsSync(dir)) {
86
+ console.log('no profiles');
87
+ return;
88
+ }
89
+ const files = fs.readdirSync(dir).filter((f) => f.endsWith('.env')).sort();
90
+ if (!files.length) {
91
+ console.log('no profiles');
92
+ return;
93
+ }
94
+ for (const file of files) {
95
+ const profile = file.slice(0, -4);
96
+ const env = readEnv(path.join(dir, file));
97
+ console.log(`${profile}\t${env.WTT_CONNECT_ADAPTER || ''}\t${env.WTT_AGENT_ID || ''}`);
98
+ }
99
+ }
100
+
101
+ export function status(argv) {
102
+ const profiles = targetProfiles(argv);
103
+ if (!profiles.length) {
104
+ console.log('no profiles');
105
+ return;
106
+ }
107
+ for (const profile of profiles) {
108
+ console.log(`${profile}: ${serviceStatus(profile)}`);
109
+ }
110
+ }
111
+
112
+ export function restart(argv) {
113
+ const profiles = targetProfiles(argv);
114
+ for (const profile of profiles) restartService(profile);
115
+ status({ _: profiles });
116
+ }
117
+
118
+ export function logs(argv) {
119
+ const [profile = ''] = argv._ || [];
120
+ if (!profile) throw new Error('usage: wtt-connect logs <profile> [--lines 100]');
121
+ const lines = String(argv.lines || 100);
122
+ if (process.platform === 'linux') {
123
+ const result = spawnSync('journalctl', ['--user', '-u', unitName(sanitizeProfile(profile)), '-n', lines, '--no-pager'], {
124
+ stdio: 'inherit',
125
+ });
126
+ if (result.status !== 0) process.exitCode = result.status || 1;
127
+ return;
128
+ }
129
+ if (process.platform === 'darwin') {
130
+ const safe = sanitizeProfile(profile);
131
+ const files = [launchdLogPath(safe, 'out'), launchdLogPath(safe, 'err')].filter((f) => fs.existsSync(f));
132
+ if (!files.length) {
133
+ console.log(`no launchd logs found for ${safe}`);
134
+ return;
135
+ }
136
+ const result = spawnSync('tail', ['-n', lines, ...files], { stdio: 'inherit' });
137
+ if (result.status !== 0) process.exitCode = result.status || 1;
138
+ return;
139
+ }
140
+ throw new Error(`unsupported platform: ${process.platform}`);
141
+ }
142
+
143
+ export function down(argv) {
144
+ const [profile = ''] = argv._ || [];
145
+ if (!profile) throw new Error('usage: wtt-connect down <profile>');
146
+ const safe = sanitizeProfile(profile);
147
+ stopAndDisableService(safe);
148
+ const servicePath = serviceFilePath(safe);
149
+ if (fs.existsSync(servicePath)) fs.rmSync(servicePath);
150
+ if (process.platform === 'linux') runSystemctl(['daemon-reload']);
151
+ console.log(`removed service: ${serviceName(safe)}`);
152
+ console.log(`kept profile env: ${profileEnvFile(safe)}`);
153
+ console.log(`kept state dir: ${defaultStateDir(safe)}`);
154
+ }
155
+
156
+ function targetProfiles(argv) {
157
+ const requested = argv._ || [];
158
+ if (requested.length && requested[0] !== 'all') return requested.map(sanitizeProfile);
159
+ const profiles = discoverProfiles();
160
+ if (!profiles.length && requested[0] === 'all') return [];
161
+ if (!profiles.length) throw new Error('no wtt-connect profiles found');
162
+ return profiles;
163
+ }
164
+
165
+ function discoverProfiles() {
166
+ const dir = profilesDir();
167
+ if (!fs.existsSync(dir)) return [];
168
+ return fs.readdirSync(dir).filter((f) => f.endsWith('.env')).map((f) => sanitizeProfile(f.slice(0, -4))).sort();
169
+ }
170
+
171
+ function writeProfileEnv(file, values) {
172
+ const lines = [
173
+ '# wtt-connect profile',
174
+ '# Keep this file local; it contains agent secrets.',
175
+ ];
176
+ for (const [key, value] of Object.entries(values)) {
177
+ if (value === undefined || value === null || value === '') continue;
178
+ lines.push(`${key}=${quoteEnv(value)}`);
179
+ }
180
+ fs.writeFileSync(file, `${lines.join('\n')}\n`, { mode: 0o600 });
181
+ fs.chmodSync(file, 0o600);
182
+ }
183
+
184
+ function installService(options) {
185
+ if (process.platform === 'linux') return installSystemdService(options);
186
+ if (process.platform === 'darwin') return installLaunchdService(options);
187
+ throw new Error(`unsupported platform: ${process.platform}`);
188
+ }
189
+
190
+ function startService(profile) {
191
+ if (process.platform === 'linux') {
192
+ runSystemctl(['daemon-reload']);
193
+ runSystemctl(['enable', unitName(profile)]);
194
+ runSystemctl(['restart', unitName(profile)]);
195
+ return;
196
+ }
197
+ if (process.platform === 'darwin') {
198
+ const plist = launchdPlistPath(profile);
199
+ const label = launchdLabel(profile);
200
+ runLaunchctl(['bootout', launchdTarget(), plist], { check: false });
201
+ runLaunchctl(['bootstrap', launchdTarget(), plist]);
202
+ runLaunchctl(['enable', `${launchdTarget()}/${label}`], { check: false });
203
+ runLaunchctl(['kickstart', '-k', `${launchdTarget()}/${label}`], { check: false });
204
+ return;
205
+ }
206
+ throw new Error(`unsupported platform: ${process.platform}`);
207
+ }
208
+
209
+ function restartService(profile) {
210
+ if (process.platform === 'linux') {
211
+ runSystemctl(['restart', unitName(profile)]);
212
+ return;
213
+ }
214
+ if (process.platform === 'darwin') {
215
+ const label = launchdLabel(profile);
216
+ const kick = runLaunchctl(['kickstart', '-k', `${launchdTarget()}/${label}`], { check: false });
217
+ if (kick.status !== 0) startService(profile);
218
+ return;
219
+ }
220
+ throw new Error(`unsupported platform: ${process.platform}`);
221
+ }
222
+
223
+ function stopAndDisableService(profile) {
224
+ if (process.platform === 'linux') {
225
+ runSystemctl(['disable', '--now', unitName(profile)], { check: false });
226
+ return;
227
+ }
228
+ if (process.platform === 'darwin') {
229
+ const plist = launchdPlistPath(profile);
230
+ const label = launchdLabel(profile);
231
+ runLaunchctl(['disable', `${launchdTarget()}/${label}`], { check: false });
232
+ runLaunchctl(['bootout', launchdTarget(), plist], { check: false });
233
+ runLaunchctl(['remove', label], { check: false });
234
+ return;
235
+ }
236
+ throw new Error(`unsupported platform: ${process.platform}`);
237
+ }
238
+
239
+ function serviceStatus(profile) {
240
+ if (process.platform === 'linux') {
241
+ const result = runSystemctl(['is-active', unitName(profile)], { check: false });
242
+ return String(result.stdout || result.stderr).trim() || 'unknown';
243
+ }
244
+ if (process.platform === 'darwin') {
245
+ const result = runLaunchctl(['print', `${launchdTarget()}/${launchdLabel(profile)}`], { check: false });
246
+ if (result.status !== 0) return 'inactive';
247
+ const output = result.stdout || '';
248
+ const state = output.match(/\bstate = ([^\n]+)/)?.[1]?.trim();
249
+ const pid = output.match(/\bpid = (\d+)/)?.[1]?.trim();
250
+ return pid ? `${state || 'running'} pid=${pid}` : (state || 'loaded');
251
+ }
252
+ throw new Error(`unsupported platform: ${process.platform}`);
253
+ }
254
+
255
+ function installSystemdService({ profile, envFile, nodeBin, root }) {
256
+ const servicePath = systemdServicePath(profile);
257
+ ensureDir(path.dirname(servicePath));
258
+ const nodeArgs = nodeMajor(nodeBin) < 22 ? ' --experimental-websocket' : '';
259
+ const bin = path.join(root, 'bin', 'wtt-connect.js');
260
+ const pathEnv = runtimePath();
261
+ const content = `[Unit]
262
+ Description=WTT Connect Agent (${profile})
263
+ After=network-online.target
264
+ Wants=network-online.target
265
+
266
+ [Service]
267
+ Type=simple
268
+ WorkingDirectory=${root}
269
+ Environment=PATH=${pathEnv}
270
+ EnvironmentFile=${envFile}
271
+ ExecStart=${nodeBin}${nodeArgs} ${bin} start --profile ${profile}
272
+ Restart=always
273
+ RestartSec=5
274
+
275
+ [Install]
276
+ WantedBy=default.target
277
+ `;
278
+ fs.writeFileSync(servicePath, content, { mode: 0o644 });
279
+ return { kind: 'systemd', name: unitName(profile), path: servicePath };
280
+ }
281
+
282
+ function installLaunchdService({ profile, nodeBin, root, stateDir }) {
283
+ const plistPath = launchdPlistPath(profile);
284
+ ensureDir(path.dirname(plistPath));
285
+ ensureDir(path.dirname(launchdLogPath(profile, 'out')));
286
+ const nodeArgs = nodeMajor(nodeBin) < 22 ? ['--experimental-websocket'] : [];
287
+ const bin = path.join(root, 'bin', 'wtt-connect.js');
288
+ const args = [nodeBin, ...nodeArgs, bin, 'start', '--profile', profile];
289
+ const content = `<?xml version="1.0" encoding="UTF-8"?>
290
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
291
+ <plist version="1.0">
292
+ <dict>
293
+ <key>Label</key>
294
+ <string>${xmlEscape(launchdLabel(profile))}</string>
295
+ <key>ProgramArguments</key>
296
+ <array>
297
+ ${args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n')}
298
+ </array>
299
+ <key>WorkingDirectory</key>
300
+ <string>${xmlEscape(root)}</string>
301
+ <key>EnvironmentVariables</key>
302
+ <dict>
303
+ <key>PATH</key>
304
+ <string>${xmlEscape(runtimePath())}</string>
305
+ </dict>
306
+ <key>RunAtLoad</key>
307
+ <true/>
308
+ <key>KeepAlive</key>
309
+ <true/>
310
+ <key>StandardOutPath</key>
311
+ <string>${xmlEscape(launchdLogPath(profile, 'out'))}</string>
312
+ <key>StandardErrorPath</key>
313
+ <string>${xmlEscape(launchdLogPath(profile, 'err'))}</string>
314
+ </dict>
315
+ </plist>
316
+ `;
317
+ ensureDir(stateDir);
318
+ fs.writeFileSync(plistPath, content, { mode: 0o644 });
319
+ return { kind: 'launchd', name: launchdLabel(profile), path: plistPath };
320
+ }
321
+
322
+ function ensureSupportedPlatform() {
323
+ if (process.platform === 'linux') {
324
+ const result = spawnSync('systemctl', ['--user', 'is-system-running'], { encoding: 'utf8' });
325
+ if (result.error) throw new Error('systemctl --user is required but was not found');
326
+ return;
327
+ }
328
+ if (process.platform === 'darwin') {
329
+ const result = spawnSync('launchctl', ['version'], { encoding: 'utf8' });
330
+ if (result.error) throw new Error('launchctl is required but was not found');
331
+ return;
332
+ }
333
+ throw new Error(`unsupported platform: ${process.platform}`);
334
+ }
335
+
336
+ function runSystemctl(args, options = {}) {
337
+ const result = spawnSync('systemctl', ['--user', ...args], { encoding: 'utf8' });
338
+ if (result.status !== 0 && options.check !== false) {
339
+ throw new Error(`systemctl --user ${args.join(' ')} failed: ${result.stderr || result.stdout}`);
340
+ }
341
+ return result;
342
+ }
343
+
344
+ function runLaunchctl(args, options = {}) {
345
+ const result = spawnSync('launchctl', args, { encoding: 'utf8' });
346
+ if (result.status !== 0 && options.check !== false) {
347
+ throw new Error(`launchctl ${args.join(' ')} failed: ${result.stderr || result.stdout}`);
348
+ }
349
+ return result;
350
+ }
351
+
352
+ function enableLinger() {
353
+ const result = spawnSync('loginctl', ['enable-linger', os.userInfo().username], { encoding: 'utf8' });
354
+ if (result.status !== 0) console.warn(`warning: failed to enable linger: ${result.stderr || result.stdout}`);
355
+ }
356
+
357
+ function readEnv(file) {
358
+ const env = {};
359
+ if (!fs.existsSync(file)) return env;
360
+ for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
361
+ const line = raw.trim();
362
+ if (!line || line.startsWith('#') || !line.includes('=')) continue;
363
+ const idx = line.indexOf('=');
364
+ env[line.slice(0, idx)] = line.slice(idx + 1).replace(/^['"]|['"]$/g, '');
365
+ }
366
+ return env;
367
+ }
368
+
369
+ function quoteEnv(value) {
370
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\$/g, '\\$')}"`;
371
+ }
372
+
373
+ function passthroughEnv(env, existingKeys) {
374
+ const out = {};
375
+ for (const [key, value] of Object.entries(env)) {
376
+ if (!value || existingKeys.has(key)) continue;
377
+ if (shouldPreserveEnvKey(key)) out[key] = value;
378
+ }
379
+ return out;
380
+ }
381
+
382
+ function shouldPreserveEnvKey(key) {
383
+ return (
384
+ key === 'HTTP_PROXY' ||
385
+ key === 'HTTPS_PROXY' ||
386
+ key === 'ALL_PROXY' ||
387
+ key === 'NO_PROXY' ||
388
+ key === 'DISABLE_TELEMETRY' ||
389
+ key === 'DISABLE_COST_WARNINGS' ||
390
+ key === 'API_TIMEOUT_MS' ||
391
+ key.startsWith('WTT_CONNECT_AGENT_') ||
392
+ key.startsWith('WTT_CONNECT_STT_') ||
393
+ key.startsWith('WTT_CONNECT_TTS_') ||
394
+ key.startsWith('WTT_CONNECT_CLAUDE_') ||
395
+ key.startsWith('WTT_CONNECT_CODEX_') ||
396
+ key.startsWith('ANTHROPIC_') ||
397
+ key.startsWith('OPENAI_') ||
398
+ key.startsWith('CLAUDE_CODE_')
399
+ );
400
+ }
401
+
402
+ function sanitizeProfile(value) {
403
+ const safe = String(value || '').trim().replace(/[^A-Za-z0-9_.@-]+/g, '-').replace(/^-+|-+$/g, '');
404
+ if (!safe) throw new Error('invalid profile name');
405
+ return safe;
406
+ }
407
+
408
+ function profilesDir() {
409
+ return path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'wtt-connect', 'profiles');
410
+ }
411
+
412
+ function profileEnvFile(profile) {
413
+ return path.join(profilesDir(), `${profile}.env`);
414
+ }
415
+
416
+ function defaultStateDir(profile) {
417
+ return path.join(process.env.XDG_STATE_HOME || path.join(os.homedir(), '.local', 'state'), 'wtt-connect', profile);
418
+ }
419
+
420
+ function systemdServicePath(profile) {
421
+ return path.join(os.homedir(), '.config', 'systemd', 'user', unitName(profile));
422
+ }
423
+
424
+ function unitName(profile) {
425
+ return `wtt-connect@${profile}.service`;
426
+ }
427
+
428
+ function serviceName(profile) {
429
+ if (process.platform === 'darwin') return launchdLabel(profile);
430
+ return unitName(profile);
431
+ }
432
+
433
+ function serviceFilePath(profile) {
434
+ if (process.platform === 'darwin') return launchdPlistPath(profile);
435
+ return systemdServicePath(profile);
436
+ }
437
+
438
+ function launchdLabel(profile) {
439
+ return `com.wtt.connect.${profile}`;
440
+ }
441
+
442
+ function launchdPlistPath(profile) {
443
+ return path.join(os.homedir(), 'Library', 'LaunchAgents', `${launchdLabel(profile)}.plist`);
444
+ }
445
+
446
+ function launchdLogPath(profile, stream) {
447
+ return path.join(defaultStateDir(profile), 'logs', `${stream}.log`);
448
+ }
449
+
450
+ function launchdTarget() {
451
+ return `gui/${process.getuid()}`;
452
+ }
453
+
454
+ function packageRoot() {
455
+ return path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
456
+ }
457
+
458
+ function ensureDir(dir) {
459
+ fs.mkdirSync(dir, { recursive: true });
460
+ }
461
+
462
+ function findBinary(name) {
463
+ const result = spawnSync('which', [name], { encoding: 'utf8', env: { ...process.env, PATH: runtimePath() } });
464
+ return result.status === 0 ? result.stdout.trim() : '';
465
+ }
466
+
467
+ function runtimePath() {
468
+ const home = os.homedir();
469
+ return unique([
470
+ ...(process.env.PATH || '').split(path.delimiter),
471
+ path.join(home, '.local', 'bin'),
472
+ path.join(home, '.npm-global', 'bin'),
473
+ path.join(home, '.bun', 'bin'),
474
+ path.join(home, '.volta', 'bin'),
475
+ path.join(home, '.asdf', 'shims'),
476
+ path.join(home, '.local', 'share', 'pnpm'),
477
+ '/opt/homebrew/bin',
478
+ '/opt/homebrew/sbin',
479
+ '/usr/local/bin',
480
+ '/usr/bin',
481
+ '/bin',
482
+ '/usr/sbin',
483
+ '/sbin',
484
+ ].filter(Boolean)).join(path.delimiter);
485
+ }
486
+
487
+ function unique(values) {
488
+ const seen = new Set();
489
+ const out = [];
490
+ for (const value of values) {
491
+ if (!value || seen.has(value)) continue;
492
+ seen.add(value);
493
+ out.push(value);
494
+ }
495
+ return out;
496
+ }
497
+
498
+ function nodeMajor(nodeBin) {
499
+ const result = spawnSync(nodeBin, ['-p', 'Number(process.versions.node.split(".")[0])'], { encoding: 'utf8' });
500
+ return Number.parseInt(result.stdout, 10) || 0;
501
+ }
502
+
503
+ function xmlEscape(value) {
504
+ return String(value)
505
+ .replace(/&/g, '&amp;')
506
+ .replace(/</g, '&lt;')
507
+ .replace(/>/g, '&gt;')
508
+ .replace(/"/g, '&quot;')
509
+ .replace(/'/g, '&apos;');
510
+ }
@@ -0,0 +1,31 @@
1
+ import { log } from './logger.js';
2
+
3
+ export class SessionManager {
4
+ constructor(adapter) {
5
+ this.adapter = adapter;
6
+ this.sessions = new Map();
7
+ }
8
+
9
+ enqueue(sessionKey, job) {
10
+ let state = this.sessions.get(sessionKey);
11
+ if (!state) {
12
+ state = { queue: [], running: false };
13
+ this.sessions.set(sessionKey, state);
14
+ }
15
+ state.queue.push(job);
16
+ if (!state.running) this.drain(sessionKey, state).catch((err) => log('error', 'session drain failed', { sessionKey, error: err.message }));
17
+ return state.queue.length;
18
+ }
19
+
20
+ async drain(sessionKey, state) {
21
+ state.running = true;
22
+ try {
23
+ while (state.queue.length) {
24
+ const job = state.queue.shift();
25
+ await job();
26
+ }
27
+ } finally {
28
+ state.running = false;
29
+ }
30
+ }
31
+ }
package/src/setup.js ADDED
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { WTTApi } from './wtt-api.js';
5
+ import { redact } from './logger.js';
6
+
7
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
8
+ const ENV_PATH = path.join(ROOT, '.env');
9
+
10
+ export async function setup(config, argv = {}) {
11
+ const env = readEnv(ENV_PATH);
12
+ let agentId = argv.agentId || config.agentId || env.WTT_AGENT_ID || '';
13
+ let token = argv.token || config.token || env.WTT_TOKEN || env.WTT_AGENT_TOKEN || '';
14
+ const api = new WTTApi({ ...config, httpToken: config.httpToken || '' });
15
+ if ((!agentId || !token) && !argv.noRegister) {
16
+ const registered = await api.registerAgent(argv.displayName || config.setupDisplayName || 'wtt-connect');
17
+ agentId = registered.agent_id;
18
+ token = registered.agent_token;
19
+ }
20
+ if (!agentId || !token) throw new Error('agent id/token missing. Provide WTT_AGENT_ID/WTT_TOKEN or omit --no-register.');
21
+ const next = {
22
+ ...env,
23
+ WTT_BASE_URL: config.wttBaseUrl,
24
+ WTT_AGENT_ID: agentId,
25
+ WTT_TOKEN: token,
26
+ WTT_CONNECT_ADAPTERS: env.WTT_CONNECT_ADAPTERS || config.adapters.join(','),
27
+ WTT_CONNECT_WORKDIR: env.WTT_CONNECT_WORKDIR || config.workDir,
28
+ WTT_CONNECT_MODE: env.WTT_CONNECT_MODE || config.mode,
29
+ WTT_CONNECT_ENABLE_CHAT: env.WTT_CONNECT_ENABLE_CHAT || '1',
30
+ WTT_REQUIRE_COMMIT_PUSH: env.WTT_REQUIRE_COMMIT_PUSH || '1',
31
+ };
32
+ writeEnv(ENV_PATH, next);
33
+ console.log('✅ wtt-connect configured');
34
+ console.log(`env: ${ENV_PATH}`);
35
+ console.log(`agent_id: ${agentId}`);
36
+ console.log(`agent_token: ${redact(token)} (saved only in .env)`);
37
+ if (argv.claimCode) {
38
+ const code = await new WTTApi({ ...config, agentId, token }).claimCode(agentId, token);
39
+ console.log('claim_code:');
40
+ console.log(code.claim_code || code.code || JSON.stringify(code));
41
+ }
42
+ }
43
+
44
+ export function readEnv(file = ENV_PATH) {
45
+ const data = {};
46
+ if (!fs.existsSync(file)) return data;
47
+ for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
48
+ const line = raw.trim();
49
+ if (!line || line.startsWith('#') || !line.includes('=')) continue;
50
+ const idx = line.indexOf('=');
51
+ data[line.slice(0, idx).trim()] = line.slice(idx + 1).trim().replace(/^['"]|['"]$/g, '');
52
+ }
53
+ return data;
54
+ }
55
+
56
+ export function writeEnv(file, data) {
57
+ const ordered = [
58
+ 'WTT_BASE_URL', 'WTT_AGENT_ID', 'WTT_TOKEN', 'WTT_HTTP_TOKEN',
59
+ 'WTT_CONNECT_ADAPTERS', 'WTT_CONNECT_ADAPTER', 'WTT_CONNECT_WORKDIR', 'WTT_CONNECT_MODE', 'WTT_CONNECT_ALLOW_YOLO',
60
+ 'WTT_CONNECT_REASONING_EFFORT', 'WTT_CONNECT_ENABLE_CHAT', 'WTT_CONNECT_PUBLISH_PROGRESS',
61
+ 'WTT_CONNECT_TASK_TIMEOUT_SECONDS', 'WTT_CONNECT_STATE_DIR', 'WTT_CONNECT_UPLOAD_ARTIFACTS',
62
+ 'WTT_CONNECT_TTS_PROVIDER', 'WTT_CONNECT_TTS_VOICE', 'WTT_CONNECT_STT_PROVIDER', 'WTT_CONNECT_STT_COMMAND',
63
+ 'WTT_REQUIRE_COMMIT_PUSH', 'WTT_SMOKE_SENDER_AGENT_ID', 'WTT_SMOKE_SENDER_TOKEN',
64
+ ];
65
+ const lines = ['# wtt-connect config', '# Keep local; contains agent secrets.'];
66
+ for (const k of ordered) if (data[k] !== undefined && data[k] !== '') lines.push(`${k}=${data[k]}`);
67
+ for (const k of Object.keys(data).sort()) if (!ordered.includes(k) && data[k] !== undefined && data[k] !== '') lines.push(`${k}=${data[k]}`);
68
+ fs.mkdirSync(path.dirname(file), { recursive: true });
69
+ fs.writeFileSync(file, `${lines.join('\n')}\n`);
70
+ try { fs.chmodSync(file, 0o600); } catch {}
71
+ }