@webmux/agent 0.1.4 → 0.2.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.
Files changed (2) hide show
  1. package/dist/cli.js +668 -117
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import fs2 from "fs";
5
- import os2 from "os";
6
- import path2 from "path";
7
- import { execSync as execSync2 } from "child_process";
4
+ import os3 from "os";
5
+ import { execFileSync as execFileSync2 } from "child_process";
8
6
  import { Command } from "commander";
9
7
 
10
8
  // src/credentials.ts
@@ -43,6 +41,258 @@ function saveCredentials(creds) {
43
41
  });
44
42
  }
45
43
 
44
+ // src/connection.ts
45
+ import WebSocket from "ws";
46
+
47
+ // ../shared/src/contracts.ts
48
+ var DEFAULT_TERMINAL_SIZE = {
49
+ cols: 120,
50
+ rows: 36
51
+ };
52
+
53
+ // ../shared/src/version.ts
54
+ var SEMVER_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
55
+ function parseSemanticVersion(version) {
56
+ const match = version.trim().match(SEMVER_PATTERN);
57
+ if (!match) {
58
+ return null;
59
+ }
60
+ return {
61
+ major: Number.parseInt(match[1], 10),
62
+ minor: Number.parseInt(match[2], 10),
63
+ patch: Number.parseInt(match[3], 10),
64
+ prerelease: match[4] ? match[4].split(".") : []
65
+ };
66
+ }
67
+ function compareSemanticVersions(left, right) {
68
+ const parsedLeft = parseSemanticVersion(left);
69
+ const parsedRight = parseSemanticVersion(right);
70
+ if (!parsedLeft || !parsedRight) {
71
+ throw new Error(`Invalid semantic version comparison: "${left}" vs "${right}"`);
72
+ }
73
+ if (parsedLeft.major !== parsedRight.major) {
74
+ return parsedLeft.major - parsedRight.major;
75
+ }
76
+ if (parsedLeft.minor !== parsedRight.minor) {
77
+ return parsedLeft.minor - parsedRight.minor;
78
+ }
79
+ if (parsedLeft.patch !== parsedRight.patch) {
80
+ return parsedLeft.patch - parsedRight.patch;
81
+ }
82
+ return comparePrerelease(parsedLeft.prerelease, parsedRight.prerelease);
83
+ }
84
+ function comparePrerelease(left, right) {
85
+ if (left.length === 0 && right.length === 0) {
86
+ return 0;
87
+ }
88
+ if (left.length === 0) {
89
+ return 1;
90
+ }
91
+ if (right.length === 0) {
92
+ return -1;
93
+ }
94
+ const maxLength = Math.max(left.length, right.length);
95
+ for (let index = 0; index < maxLength; index += 1) {
96
+ const leftIdentifier = left[index];
97
+ const rightIdentifier = right[index];
98
+ if (leftIdentifier === void 0) {
99
+ return -1;
100
+ }
101
+ if (rightIdentifier === void 0) {
102
+ return 1;
103
+ }
104
+ const numericLeft = Number.parseInt(leftIdentifier, 10);
105
+ const numericRight = Number.parseInt(rightIdentifier, 10);
106
+ const leftIsNumber = String(numericLeft) === leftIdentifier;
107
+ const rightIsNumber = String(numericRight) === rightIdentifier;
108
+ if (leftIsNumber && rightIsNumber && numericLeft !== numericRight) {
109
+ return numericLeft - numericRight;
110
+ }
111
+ if (leftIsNumber !== rightIsNumber) {
112
+ return leftIsNumber ? -1 : 1;
113
+ }
114
+ if (leftIdentifier !== rightIdentifier) {
115
+ return leftIdentifier < rightIdentifier ? -1 : 1;
116
+ }
117
+ }
118
+ return 0;
119
+ }
120
+
121
+ // src/service.ts
122
+ import fs2 from "fs";
123
+ import os2 from "os";
124
+ import path2 from "path";
125
+ import { execFileSync } from "child_process";
126
+ var SERVICE_NAME = "webmux-agent";
127
+ function renderServiceUnit(options) {
128
+ return `[Unit]
129
+ Description=Webmux Agent (${options.agentName})
130
+ After=network-online.target
131
+ Wants=network-online.target
132
+
133
+ [Service]
134
+ Type=simple
135
+ ExecStart=${options.nodePath} ${options.cliPath} start
136
+ Restart=always
137
+ RestartSec=10
138
+ Environment=WEBMUX_AGENT_SERVICE=1
139
+ Environment=WEBMUX_AGENT_AUTO_UPGRADE=${options.autoUpgrade ? "1" : "0"}
140
+ Environment=WEBMUX_AGENT_NAME=${options.agentName}
141
+ Environment=HOME=${options.homeDir}
142
+ Environment=PATH=${options.pathEnv}
143
+ WorkingDirectory=${options.homeDir}
144
+
145
+ [Install]
146
+ WantedBy=default.target
147
+ `;
148
+ }
149
+ function installService(options) {
150
+ const homeDir = options.homeDir ?? os2.homedir();
151
+ const autoUpgrade = options.autoUpgrade;
152
+ const release = installManagedRelease({
153
+ packageName: options.packageName,
154
+ version: options.version,
155
+ homeDir
156
+ });
157
+ writeServiceUnit({
158
+ agentName: options.agentName,
159
+ autoUpgrade,
160
+ cliPath: release.cliPath,
161
+ homeDir
162
+ });
163
+ runSystemctl(["--user", "daemon-reload"]);
164
+ runSystemctl(["--user", "enable", SERVICE_NAME]);
165
+ runSystemctl(["--user", "restart", SERVICE_NAME]);
166
+ runCommand("loginctl", ["enable-linger", os2.userInfo().username]);
167
+ }
168
+ function upgradeService(options) {
169
+ const homeDir = options.homeDir ?? os2.homedir();
170
+ const installedConfig = readInstalledServiceConfig(homeDir);
171
+ const autoUpgrade = options.autoUpgrade ?? installedConfig?.autoUpgrade ?? true;
172
+ const release = installManagedRelease({
173
+ packageName: options.packageName,
174
+ version: options.version,
175
+ homeDir
176
+ });
177
+ writeServiceUnit({
178
+ agentName: options.agentName,
179
+ autoUpgrade,
180
+ cliPath: release.cliPath,
181
+ homeDir
182
+ });
183
+ runSystemctl(["--user", "daemon-reload"]);
184
+ runSystemctl(["--user", "restart", SERVICE_NAME]);
185
+ }
186
+ function uninstallService(homeDir = os2.homedir()) {
187
+ const unitPath = servicePath(homeDir);
188
+ try {
189
+ runSystemctl(["--user", "stop", SERVICE_NAME]);
190
+ } catch {
191
+ }
192
+ try {
193
+ runSystemctl(["--user", "disable", SERVICE_NAME]);
194
+ } catch {
195
+ }
196
+ if (fs2.existsSync(unitPath)) {
197
+ fs2.unlinkSync(unitPath);
198
+ }
199
+ try {
200
+ runSystemctl(["--user", "daemon-reload"]);
201
+ } catch {
202
+ }
203
+ }
204
+ function readInstalledServiceConfig(homeDir = os2.homedir()) {
205
+ const unitPath = servicePath(homeDir);
206
+ if (!fs2.existsSync(unitPath)) {
207
+ return null;
208
+ }
209
+ const unit = fs2.readFileSync(unitPath, "utf-8");
210
+ const autoUpgradeMatch = unit.match(/^Environment=WEBMUX_AGENT_AUTO_UPGRADE=(\d)$/m);
211
+ const versionMatch = unit.match(/\/releases\/([^/\s]+)\/node_modules\//);
212
+ return {
213
+ autoUpgrade: autoUpgradeMatch?.[1] !== "0",
214
+ version: versionMatch?.[1] ?? null
215
+ };
216
+ }
217
+ function servicePath(homeDir = os2.homedir()) {
218
+ return path2.join(homeDir, ".config", "systemd", "user", `${SERVICE_NAME}.service`);
219
+ }
220
+ function writeServiceUnit(options) {
221
+ const serviceDir = path2.dirname(servicePath(options.homeDir));
222
+ fs2.mkdirSync(serviceDir, { recursive: true });
223
+ fs2.writeFileSync(
224
+ servicePath(options.homeDir),
225
+ renderServiceUnit({
226
+ agentName: options.agentName,
227
+ autoUpgrade: options.autoUpgrade,
228
+ cliPath: options.cliPath,
229
+ homeDir: options.homeDir,
230
+ nodePath: findBinary("node") ?? process.execPath,
231
+ pathEnv: process.env.PATH ?? ""
232
+ })
233
+ );
234
+ }
235
+ function installManagedRelease(options) {
236
+ const releaseDir = path2.join(options.homeDir, ".webmux", "releases", options.version);
237
+ const cliPath = path2.join(
238
+ releaseDir,
239
+ "node_modules",
240
+ ...options.packageName.split("/"),
241
+ "dist",
242
+ "cli.js"
243
+ );
244
+ if (fs2.existsSync(cliPath)) {
245
+ return { cliPath, releaseDir };
246
+ }
247
+ fs2.mkdirSync(releaseDir, { recursive: true });
248
+ ensureRuntimePackageJson(releaseDir);
249
+ const packageManager = findBinary("pnpm") ? "pnpm" : "npm";
250
+ if (packageManager === "pnpm") {
251
+ runCommand("pnpm", ["add", "--dir", releaseDir, `${options.packageName}@${options.version}`]);
252
+ } else {
253
+ if (!findBinary("npm")) {
254
+ throw new Error("Cannot find pnpm or npm. Install one package manager before installing the service.");
255
+ }
256
+ runCommand("npm", ["install", "--omit=dev", `${options.packageName}@${options.version}`], releaseDir);
257
+ }
258
+ if (!fs2.existsSync(cliPath)) {
259
+ throw new Error(`Managed release did not produce a CLI at ${cliPath}`);
260
+ }
261
+ return { cliPath, releaseDir };
262
+ }
263
+ function ensureRuntimePackageJson(releaseDir) {
264
+ const packageJsonPath = path2.join(releaseDir, "package.json");
265
+ if (fs2.existsSync(packageJsonPath)) {
266
+ return;
267
+ }
268
+ fs2.writeFileSync(
269
+ packageJsonPath,
270
+ JSON.stringify({
271
+ name: "webmux-agent-runtime",
272
+ private: true
273
+ }, null, 2) + "\n"
274
+ );
275
+ }
276
+ function runSystemctl(args) {
277
+ runCommand("systemctl", args);
278
+ }
279
+ function runCommand(command, args, cwd) {
280
+ execFileSync(command, args, {
281
+ cwd,
282
+ stdio: "inherit"
283
+ });
284
+ }
285
+ function findBinary(name) {
286
+ try {
287
+ return execFileSync("which", [name], { encoding: "utf-8" }).trim();
288
+ } catch {
289
+ return null;
290
+ }
291
+ }
292
+
293
+ // src/terminal.ts
294
+ import { spawn } from "node-pty";
295
+
46
296
  // src/tmux.ts
47
297
  import { execFile } from "child_process";
48
298
  import { promisify } from "util";
@@ -197,19 +447,6 @@ function isTmuxEmptyStateMessage(message) {
197
447
  return TMUX_EMPTY_STATE_MARKERS.some((marker) => message.includes(marker));
198
448
  }
199
449
 
200
- // src/connection.ts
201
- import { execSync } from "child_process";
202
- import WebSocket from "ws";
203
-
204
- // src/terminal.ts
205
- import { spawn } from "node-pty";
206
-
207
- // ../shared/src/contracts.ts
208
- var DEFAULT_TERMINAL_SIZE = {
209
- cols: 120,
210
- rows: 36
211
- };
212
-
213
450
  // src/terminal.ts
214
451
  async function createTerminalBridge(options) {
215
452
  const {
@@ -253,29 +490,265 @@ async function createTerminalBridge(options) {
253
490
  };
254
491
  }
255
492
 
493
+ // src/run-wrapper.ts
494
+ import { spawn as spawn2 } from "node-pty";
495
+ var STATUS_DEBOUNCE_MS = 300;
496
+ var OUTPUT_BUFFER_MAX_LINES = 20;
497
+ var CLAUDE_APPROVAL_PATTERNS = [
498
+ /do you want to/i,
499
+ /\ballow\b/i,
500
+ /\bdeny\b/i,
501
+ /\bpermission\b/i,
502
+ /proceed\?/i
503
+ ];
504
+ var CLAUDE_INPUT_PATTERNS = [
505
+ /^>\s*$/m,
506
+ /❯/,
507
+ /\$ $/m
508
+ ];
509
+ var CODEX_APPROVAL_PATTERNS = [
510
+ /apply changes/i,
511
+ /\[y\/n\]/i,
512
+ /\bapprove\b/i
513
+ ];
514
+ var CODEX_INPUT_PATTERNS = [
515
+ /what would you like/i,
516
+ /❯/,
517
+ /^>\s*$/m
518
+ ];
519
+ function matchesAny(text, patterns) {
520
+ return patterns.some((pattern) => pattern.test(text));
521
+ }
522
+ var RunWrapper = class {
523
+ runId;
524
+ tool;
525
+ repoPath;
526
+ prompt;
527
+ tmux;
528
+ onEvent;
529
+ onOutput;
530
+ ptyProcess = null;
531
+ currentStatus = "starting";
532
+ outputBuffer = [];
533
+ debounceTimer = null;
534
+ disposed = false;
535
+ sessionName;
536
+ constructor(options) {
537
+ this.runId = options.runId;
538
+ this.tool = options.tool;
539
+ this.repoPath = options.repoPath;
540
+ this.prompt = options.prompt;
541
+ this.tmux = options.tmux;
542
+ this.onEvent = options.onEvent;
543
+ this.onOutput = options.onOutput;
544
+ const shortId = this.runId.slice(0, 8);
545
+ this.sessionName = `run-${shortId}`;
546
+ }
547
+ async start() {
548
+ if (this.disposed) {
549
+ return;
550
+ }
551
+ this.emitStatus("starting");
552
+ await this.tmux.createSession(this.sessionName);
553
+ const command = this.buildCommand();
554
+ const ptyProcess = spawn2(
555
+ "tmux",
556
+ ["-L", this.tmux.socketName, "attach-session", "-t", this.sessionName],
557
+ {
558
+ cols: 120,
559
+ rows: 36,
560
+ cwd: this.repoPath,
561
+ env: {
562
+ ...process.env,
563
+ TERM: "xterm-256color"
564
+ },
565
+ name: "xterm-256color"
566
+ }
567
+ );
568
+ this.ptyProcess = ptyProcess;
569
+ ptyProcess.onData((data) => {
570
+ if (this.disposed) {
571
+ return;
572
+ }
573
+ this.onOutput(data);
574
+ this.appendToBuffer(data);
575
+ this.scheduleStatusDetection();
576
+ });
577
+ ptyProcess.onExit(({ exitCode }) => {
578
+ if (this.disposed) {
579
+ return;
580
+ }
581
+ if (this.debounceTimer) {
582
+ clearTimeout(this.debounceTimer);
583
+ this.debounceTimer = null;
584
+ }
585
+ if (exitCode === 0) {
586
+ this.emitStatus("success");
587
+ } else {
588
+ this.emitStatus("failed");
589
+ }
590
+ this.ptyProcess = null;
591
+ });
592
+ setTimeout(() => {
593
+ if (this.ptyProcess && !this.disposed) {
594
+ this.ptyProcess.write(command + "\n");
595
+ this.emitStatus("running");
596
+ }
597
+ }, 500);
598
+ }
599
+ sendInput(input) {
600
+ if (this.ptyProcess && !this.disposed) {
601
+ this.ptyProcess.write(input);
602
+ }
603
+ }
604
+ interrupt() {
605
+ if (this.ptyProcess && !this.disposed) {
606
+ this.ptyProcess.write("");
607
+ this.emitStatus("interrupted");
608
+ }
609
+ }
610
+ approve() {
611
+ if (this.ptyProcess && !this.disposed) {
612
+ this.ptyProcess.write("y\n");
613
+ }
614
+ }
615
+ reject() {
616
+ if (this.ptyProcess && !this.disposed) {
617
+ this.ptyProcess.write("n\n");
618
+ }
619
+ }
620
+ dispose() {
621
+ if (this.disposed) {
622
+ return;
623
+ }
624
+ this.disposed = true;
625
+ if (this.debounceTimer) {
626
+ clearTimeout(this.debounceTimer);
627
+ this.debounceTimer = null;
628
+ }
629
+ if (this.ptyProcess) {
630
+ this.ptyProcess.kill();
631
+ this.ptyProcess = null;
632
+ }
633
+ this.tmux.killSession(this.sessionName).catch(() => {
634
+ });
635
+ }
636
+ buildCommand() {
637
+ const escapedPrompt = this.prompt.replace(/'/g, "'\\''");
638
+ switch (this.tool) {
639
+ case "claude":
640
+ return `cd '${this.repoPath.replace(/'/g, "'\\''")}' && claude '${escapedPrompt}'`;
641
+ case "codex":
642
+ return `cd '${this.repoPath.replace(/'/g, "'\\''")}' && codex '${escapedPrompt}'`;
643
+ }
644
+ }
645
+ appendToBuffer(data) {
646
+ const newLines = data.split("\n");
647
+ this.outputBuffer.push(...newLines);
648
+ if (this.outputBuffer.length > OUTPUT_BUFFER_MAX_LINES) {
649
+ this.outputBuffer = this.outputBuffer.slice(-OUTPUT_BUFFER_MAX_LINES);
650
+ }
651
+ }
652
+ scheduleStatusDetection() {
653
+ if (this.debounceTimer) {
654
+ clearTimeout(this.debounceTimer);
655
+ }
656
+ this.debounceTimer = setTimeout(() => {
657
+ this.debounceTimer = null;
658
+ this.detectStatus();
659
+ }, STATUS_DEBOUNCE_MS);
660
+ }
661
+ detectStatus() {
662
+ if (this.disposed) {
663
+ return;
664
+ }
665
+ if (this.currentStatus === "success" || this.currentStatus === "failed" || this.currentStatus === "interrupted") {
666
+ return;
667
+ }
668
+ const recentText = this.outputBuffer.join("\n");
669
+ const detectedStatus = this.detectStatusFromText(recentText);
670
+ if (detectedStatus && detectedStatus !== this.currentStatus) {
671
+ this.emitStatus(detectedStatus);
672
+ }
673
+ }
674
+ detectStatusFromText(text) {
675
+ const approvalPatterns = this.tool === "claude" ? CLAUDE_APPROVAL_PATTERNS : CODEX_APPROVAL_PATTERNS;
676
+ if (matchesAny(text, approvalPatterns)) {
677
+ return "waiting_approval";
678
+ }
679
+ const inputPatterns = this.tool === "claude" ? CLAUDE_INPUT_PATTERNS : CODEX_INPUT_PATTERNS;
680
+ if (matchesAny(text, inputPatterns)) {
681
+ return "waiting_input";
682
+ }
683
+ if (text.trim().length > 0) {
684
+ return "running";
685
+ }
686
+ return null;
687
+ }
688
+ emitStatus(status, summary, hasDiff) {
689
+ this.currentStatus = status;
690
+ this.onEvent(status, summary, hasDiff);
691
+ }
692
+ };
693
+
694
+ // src/version.ts
695
+ import fs3 from "fs";
696
+ var packageMetadata = readAgentPackageMetadata();
697
+ var AGENT_PACKAGE_NAME = packageMetadata.name;
698
+ var AGENT_VERSION = packageMetadata.version;
699
+ function readAgentPackageMetadata() {
700
+ const packageJsonPath = new URL("../package.json", import.meta.url);
701
+ const raw = fs3.readFileSync(packageJsonPath, "utf-8");
702
+ const parsed = JSON.parse(raw);
703
+ if (!parsed.name || !parsed.version) {
704
+ throw new Error("Agent package metadata is missing name or version");
705
+ }
706
+ return {
707
+ name: parsed.name,
708
+ version: parsed.version
709
+ };
710
+ }
711
+
256
712
  // src/connection.ts
257
- var AGENT_VERSION = "0.1.4";
258
713
  var HEARTBEAT_INTERVAL_MS = 3e4;
259
714
  var SESSION_SYNC_INTERVAL_MS = 15e3;
260
715
  var INITIAL_RECONNECT_DELAY_MS = 1e3;
261
716
  var MAX_RECONNECT_DELAY_MS = 3e4;
717
+ var defaultAgentRuntime = {
718
+ version: AGENT_VERSION,
719
+ serviceMode: process.env.WEBMUX_AGENT_SERVICE === "1",
720
+ autoUpgrade: process.env.WEBMUX_AGENT_AUTO_UPGRADE !== "0",
721
+ applyServiceUpgrade: ({ packageName, targetVersion }) => {
722
+ upgradeService({
723
+ agentName: process.env.WEBMUX_AGENT_NAME ?? "webmux-agent",
724
+ packageName,
725
+ version: targetVersion
726
+ });
727
+ },
728
+ exit: (code) => {
729
+ process.exit(code);
730
+ }
731
+ };
262
732
  var AgentConnection = class {
263
733
  serverUrl;
264
734
  agentId;
265
735
  agentSecret;
266
736
  tmux;
737
+ runtime;
267
738
  ws = null;
268
739
  heartbeatTimer = null;
269
740
  sessionSyncTimer = null;
270
741
  reconnectTimer = null;
271
742
  reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
272
743
  bridges = /* @__PURE__ */ new Map();
744
+ runs = /* @__PURE__ */ new Map();
273
745
  stopped = false;
274
- constructor(serverUrl, agentId, agentSecret, tmux) {
746
+ constructor(serverUrl, agentId, agentSecret, tmux, runtime = defaultAgentRuntime) {
275
747
  this.serverUrl = serverUrl;
276
748
  this.agentId = agentId;
277
749
  this.agentSecret = agentSecret;
278
750
  this.tmux = tmux;
751
+ this.runtime = runtime;
279
752
  }
280
753
  start() {
281
754
  this.stopped = false;
@@ -290,6 +763,7 @@ var AgentConnection = class {
290
763
  this.stopHeartbeat();
291
764
  this.stopSessionSync();
292
765
  this.disposeAllBridges();
766
+ this.disposeAllRuns();
293
767
  if (this.ws) {
294
768
  this.ws.close(1e3, "agent shutting down");
295
769
  this.ws = null;
@@ -303,7 +777,12 @@ var AgentConnection = class {
303
777
  ws.on("open", () => {
304
778
  console.log("[agent] WebSocket connected, authenticating...");
305
779
  this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
306
- this.sendMessage({ type: "auth", agentId: this.agentId, agentSecret: this.agentSecret, version: AGENT_VERSION });
780
+ this.sendMessage({
781
+ type: "auth",
782
+ agentId: this.agentId,
783
+ agentSecret: this.agentSecret,
784
+ version: this.runtime.version
785
+ });
307
786
  });
308
787
  ws.on("message", (raw) => {
309
788
  let msg;
@@ -327,9 +806,7 @@ var AgentConnection = class {
327
806
  switch (msg.type) {
328
807
  case "auth-ok":
329
808
  console.log("[agent] Authenticated successfully");
330
- if (msg.latestVersion && msg.latestVersion !== AGENT_VERSION) {
331
- console.log(`[agent] Update available: ${AGENT_VERSION} \u2192 ${msg.latestVersion}`);
332
- this.selfUpdate(msg.latestVersion);
809
+ if (this.applyRecommendedUpgrade(msg.upgradePolicy)) {
333
810
  return;
334
811
  }
335
812
  this.startHeartbeat();
@@ -343,7 +820,7 @@ var AgentConnection = class {
343
820
  this.ws.close();
344
821
  this.ws = null;
345
822
  }
346
- process.exit(1);
823
+ this.runtime.exit(1);
347
824
  break;
348
825
  case "sessions-list":
349
826
  this.syncSessions();
@@ -366,6 +843,21 @@ var AgentConnection = class {
366
843
  case "session-kill":
367
844
  this.handleSessionKill(msg.requestId, msg.name);
368
845
  break;
846
+ case "run-start":
847
+ this.handleRunStart(msg.runId, msg.tool, msg.repoPath, msg.prompt);
848
+ break;
849
+ case "run-input":
850
+ this.handleRunInput(msg.runId, msg.input);
851
+ break;
852
+ case "run-interrupt":
853
+ this.handleRunInterrupt(msg.runId);
854
+ break;
855
+ case "run-approve":
856
+ this.handleRunApprove(msg.runId);
857
+ break;
858
+ case "run-reject":
859
+ this.handleRunReject(msg.runId);
860
+ break;
369
861
  default:
370
862
  console.warn("[agent] Unknown message type:", msg.type);
371
863
  }
@@ -457,26 +949,47 @@ var AgentConnection = class {
457
949
  this.sendMessage({ type: "command-result", requestId, ok: false, error: message });
458
950
  }
459
951
  }
460
- selfUpdate(targetVersion) {
461
- console.log(`[agent] Installing @webmux/agent@${targetVersion}...`);
952
+ sendMessage(msg) {
953
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
954
+ this.ws.send(JSON.stringify(msg));
955
+ }
956
+ }
957
+ applyRecommendedUpgrade(upgradePolicy) {
958
+ const targetVersion = upgradePolicy?.targetVersion;
959
+ if (!targetVersion) {
960
+ return false;
961
+ }
962
+ let comparison;
963
+ try {
964
+ comparison = compareSemanticVersions(this.runtime.version, targetVersion);
965
+ } catch {
966
+ console.warn("[agent] Skipping automatic upgrade because version parsing failed");
967
+ return false;
968
+ }
969
+ if (comparison >= 0) {
970
+ return false;
971
+ }
972
+ console.log(`[agent] Update available: ${this.runtime.version} \u2192 ${targetVersion}`);
973
+ if (!this.runtime.serviceMode || !this.runtime.autoUpgrade) {
974
+ console.log("[agent] Automatic upgrades are only applied for the managed systemd service");
975
+ console.log(`[agent] To upgrade manually, run: pnpm dlx @webmux/agent service upgrade --to ${targetVersion}`);
976
+ return false;
977
+ }
462
978
  try {
463
- execSync(`npm install -g @webmux/agent@${targetVersion}`, { stdio: "inherit" });
464
- console.log("[agent] Update installed. Restarting...");
979
+ this.runtime.applyServiceUpgrade({
980
+ packageName: upgradePolicy.packageName || AGENT_PACKAGE_NAME,
981
+ targetVersion
982
+ });
983
+ console.log(`[agent] Managed service switched to ${targetVersion}. Restarting...`);
465
984
  } catch (err) {
466
- console.error("[agent] Update failed:", err instanceof Error ? err.message : err);
985
+ const message = err instanceof Error ? err.message : String(err);
986
+ console.error(`[agent] Failed to apply managed upgrade: ${message}`);
467
987
  console.log("[agent] Continuing with current version");
468
- this.startHeartbeat();
469
- this.startSessionSync();
470
- this.syncSessions();
471
- return;
988
+ return false;
472
989
  }
473
990
  this.stop();
474
- process.exit(0);
475
- }
476
- sendMessage(msg) {
477
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
478
- this.ws.send(JSON.stringify(msg));
479
- }
991
+ this.runtime.exit(0);
992
+ return true;
480
993
  }
481
994
  startHeartbeat() {
482
995
  this.stopHeartbeat();
@@ -508,6 +1021,76 @@ var AgentConnection = class {
508
1021
  this.bridges.delete(browserId);
509
1022
  }
510
1023
  }
1024
+ disposeAllRuns() {
1025
+ for (const [runId, run] of this.runs) {
1026
+ run.dispose();
1027
+ this.runs.delete(runId);
1028
+ }
1029
+ }
1030
+ handleRunStart(runId, tool, repoPath, prompt) {
1031
+ const existing = this.runs.get(runId);
1032
+ if (existing) {
1033
+ existing.dispose();
1034
+ this.runs.delete(runId);
1035
+ }
1036
+ const run = new RunWrapper({
1037
+ runId,
1038
+ tool,
1039
+ repoPath,
1040
+ prompt,
1041
+ tmux: this.tmux,
1042
+ onEvent: (status, summary, hasDiff) => {
1043
+ this.sendMessage({ type: "run-event", runId, status, summary, hasDiff });
1044
+ },
1045
+ onOutput: (data) => {
1046
+ this.sendMessage({ type: "run-output", runId, data });
1047
+ }
1048
+ });
1049
+ this.runs.set(runId, run);
1050
+ run.start().catch((err) => {
1051
+ const message = err instanceof Error ? err.message : String(err);
1052
+ console.error(`[agent] Failed to start run ${runId}:`, message);
1053
+ this.sendMessage({
1054
+ type: "run-event",
1055
+ runId,
1056
+ status: "failed",
1057
+ summary: `Failed to start: ${message}`
1058
+ });
1059
+ this.runs.delete(runId);
1060
+ });
1061
+ }
1062
+ handleRunInput(runId, input) {
1063
+ const run = this.runs.get(runId);
1064
+ if (run) {
1065
+ run.sendInput(input);
1066
+ } else {
1067
+ console.warn(`[agent] run-input: no run found for ${runId}`);
1068
+ }
1069
+ }
1070
+ handleRunInterrupt(runId) {
1071
+ const run = this.runs.get(runId);
1072
+ if (run) {
1073
+ run.interrupt();
1074
+ } else {
1075
+ console.warn(`[agent] run-interrupt: no run found for ${runId}`);
1076
+ }
1077
+ }
1078
+ handleRunApprove(runId) {
1079
+ const run = this.runs.get(runId);
1080
+ if (run) {
1081
+ run.approve();
1082
+ } else {
1083
+ console.warn(`[agent] run-approve: no run found for ${runId}`);
1084
+ }
1085
+ }
1086
+ handleRunReject(runId) {
1087
+ const run = this.runs.get(runId);
1088
+ if (run) {
1089
+ run.reject();
1090
+ } else {
1091
+ console.warn(`[agent] run-reject: no run found for ${runId}`);
1092
+ }
1093
+ }
511
1094
  onDisconnect() {
512
1095
  this.stopHeartbeat();
513
1096
  this.stopSessionSync();
@@ -531,12 +1114,11 @@ function buildWsUrl(serverUrl) {
531
1114
  }
532
1115
 
533
1116
  // src/cli.ts
534
- var SERVICE_NAME = "webmux-agent";
535
1117
  var program = new Command();
536
- program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version("0.0.0");
1118
+ program.name("webmux-agent").description("Webmux agent \u2014 connects your machine to the webmux server").version(AGENT_VERSION);
537
1119
  program.command("register").description("Register this agent with a webmux server").requiredOption("--server <url>", "Server URL (e.g. https://webmux.example.com)").requiredOption("--token <token>", "One-time registration token from the server").option("--name <name>", "Display name for this agent (defaults to hostname)").action(async (opts) => {
538
1120
  const serverUrl = opts.server.replace(/\/+$/, "");
539
- const agentName = opts.name ?? os2.hostname();
1121
+ const agentName = opts.name ?? os3.hostname();
540
1122
  console.log(`[agent] Registering with server ${serverUrl}...`);
541
1123
  console.log(`[agent] Agent name: ${agentName}`);
542
1124
  const body = {
@@ -578,8 +1160,8 @@ program.command("register").description("Register this agent with a webmux serve
578
1160
  console.log(`[agent] Credentials saved to ${credentialsPath()}`);
579
1161
  console.log(``);
580
1162
  console.log(`Next steps:`);
581
- console.log(` npx @webmux/agent start # run once`);
582
- console.log(` npx @webmux/agent service install # install as systemd service`);
1163
+ console.log(` pnpm dlx @webmux/agent start # run once`);
1164
+ console.log(` pnpm dlx @webmux/agent service install # install as managed systemd service`);
583
1165
  });
584
1166
  program.command("start").description("Start the agent and connect to the server").action(() => {
585
1167
  const creds = loadCredentials();
@@ -594,7 +1176,7 @@ program.command("start").description("Start the agent and connect to the server"
594
1176
  console.log(`[agent] Agent ID: ${creds.agentId}`);
595
1177
  const tmux = new TmuxClient({
596
1178
  socketName: "webmux",
597
- workspaceRoot: os2.homedir()
1179
+ workspaceRoot: os3.homedir()
598
1180
  });
599
1181
  const connection = new AgentConnection(
600
1182
  creds.serverUrl,
@@ -618,119 +1200,88 @@ program.command("status").description("Show agent status and credentials info").
618
1200
  process.exit(0);
619
1201
  }
620
1202
  console.log(`Agent Name: ${creds.name}`);
1203
+ console.log(`Agent Version: ${AGENT_VERSION}`);
621
1204
  console.log(`Server URL: ${creds.serverUrl}`);
622
1205
  console.log(`Agent ID: ${creds.agentId}`);
623
1206
  console.log(`Credentials File: ${credentialsPath()}`);
1207
+ const installedService = readInstalledServiceConfig();
624
1208
  try {
625
- const result = execSync2(`systemctl --user is-active ${SERVICE_NAME} 2>/dev/null`, { encoding: "utf-8" }).trim();
1209
+ const result = execFileSync2("systemctl", ["--user", "is-active", SERVICE_NAME], { encoding: "utf-8" }).trim();
626
1210
  console.log(`Service: ${result}`);
627
1211
  } catch {
628
1212
  console.log(`Service: not installed`);
629
1213
  }
1214
+ if (installedService?.version) {
1215
+ console.log(`Service Version: ${installedService.version}`);
1216
+ }
630
1217
  });
631
1218
  var service = program.command("service").description("Manage the systemd service");
632
- service.command("install").description("Install and start the agent as a systemd user service").action(() => {
1219
+ service.command("install").description("Install and start the agent as a managed systemd user service").option("--no-auto-upgrade", "Disable automatic upgrades for the managed service").action((opts) => {
633
1220
  const creds = loadCredentials();
634
1221
  if (!creds) {
635
1222
  console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
636
1223
  process.exit(1);
637
1224
  }
638
- const npxPath = findBinary("npx");
639
- if (!npxPath) {
640
- console.error(`[agent] Cannot find npx. Make sure Node.js is installed.`);
641
- process.exit(1);
642
- }
643
- const serviceDir = path2.join(os2.homedir(), ".config", "systemd", "user");
644
- const servicePath = path2.join(serviceDir, `${SERVICE_NAME}.service`);
645
- const npmPath = findBinary("npm") ?? "npm";
646
- const unit = `[Unit]
647
- Description=Webmux Agent (${creds.name})
648
- After=network-online.target
649
- Wants=network-online.target
650
-
651
- [Service]
652
- Type=simple
653
- ExecStartPre=${npmPath} install -g @webmux/agent@latest
654
- ExecStart=${findBinary("webmux-agent") ?? `${npxPath} -y @webmux/agent`} start
655
- Restart=always
656
- RestartSec=10
657
- Environment=HOME=${os2.homedir()}
658
- Environment=PATH=${process.env.PATH}
659
- WorkingDirectory=${os2.homedir()}
660
-
661
- [Install]
662
- WantedBy=default.target
663
- `;
664
- fs2.mkdirSync(serviceDir, { recursive: true });
665
- fs2.writeFileSync(servicePath, unit);
666
- console.log(`[agent] Service file created: ${servicePath}`);
667
1225
  try {
668
- execSync2("systemctl --user daemon-reload", { stdio: "inherit" });
669
- execSync2(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "inherit" });
670
- execSync2(`systemctl --user start ${SERVICE_NAME}`, { stdio: "inherit" });
671
- execSync2(`loginctl enable-linger ${os2.userInfo().username}`, { stdio: "inherit" });
1226
+ installService({
1227
+ agentName: creds.name,
1228
+ packageName: AGENT_PACKAGE_NAME,
1229
+ version: AGENT_VERSION,
1230
+ autoUpgrade: opts.autoUpgrade
1231
+ });
672
1232
  console.log(``);
673
1233
  console.log(`[agent] Service installed and started!`);
1234
+ console.log(`[agent] Managed version: ${AGENT_VERSION}`);
674
1235
  console.log(`[agent] It will auto-start on boot.`);
675
1236
  console.log(``);
676
1237
  console.log(`Useful commands:`);
677
1238
  console.log(` systemctl --user status ${SERVICE_NAME}`);
678
1239
  console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
679
- console.log(` npx @webmux/agent service uninstall`);
1240
+ console.log(` pnpm dlx @webmux/agent service upgrade --to <version>`);
1241
+ console.log(` pnpm dlx @webmux/agent service uninstall`);
680
1242
  } catch (err) {
681
1243
  const message = err instanceof Error ? err.message : String(err);
682
- console.error(`[agent] Failed to enable service: ${message}`);
683
- console.error(`[agent] Service file was written to ${servicePath}`);
684
- console.error(`[agent] You can try manually: systemctl --user enable --now ${SERVICE_NAME}`);
1244
+ console.error(`[agent] Failed to install managed service: ${message}`);
1245
+ console.error(`[agent] Service file path: ${servicePath()}`);
685
1246
  process.exit(1);
686
1247
  }
687
1248
  });
688
1249
  service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
689
- const servicePath = path2.join(os2.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
690
1250
  try {
691
- execSync2(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
692
- execSync2(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
693
- } catch {
694
- }
695
- if (fs2.existsSync(servicePath)) {
696
- fs2.unlinkSync(servicePath);
697
- console.log(`[agent] Service file removed: ${servicePath}`);
698
- }
699
- try {
700
- execSync2("systemctl --user daemon-reload", { stdio: "inherit" });
701
- } catch {
1251
+ uninstallService();
1252
+ console.log(`[agent] Service file removed: ${servicePath()}`);
1253
+ console.log(`[agent] Service uninstalled.`);
1254
+ } catch (err) {
1255
+ const message = err instanceof Error ? err.message : String(err);
1256
+ console.error(`[agent] Failed to uninstall service: ${message}`);
1257
+ process.exit(1);
702
1258
  }
703
- console.log(`[agent] Service uninstalled.`);
704
1259
  });
705
1260
  service.command("status").description("Show systemd service status").action(() => {
706
1261
  try {
707
- execSync2(`systemctl --user status ${SERVICE_NAME}`, { stdio: "inherit" });
1262
+ execFileSync2("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
708
1263
  } catch {
709
1264
  console.log(`[agent] Service is not installed or not running.`);
710
1265
  }
711
1266
  });
712
- service.command("upgrade").description("Upgrade agent to latest version and restart service").action(() => {
713
- console.log("[agent] Upgrading @webmux/agent to latest...");
714
- try {
715
- execSync2("npm install -g @webmux/agent@latest", { stdio: "inherit" });
716
- } catch {
717
- console.error("[agent] Failed to upgrade. Try manually: npm install -g @webmux/agent@latest");
1267
+ service.command("upgrade").description("Switch the managed service to a specific agent version and restart it").requiredOption("--to <version>", "Target agent version (for example 0.1.5)").action((opts) => {
1268
+ const creds = loadCredentials();
1269
+ if (!creds) {
1270
+ console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
718
1271
  process.exit(1);
719
1272
  }
720
- console.log("[agent] Restarting service...");
1273
+ console.log(`[agent] Switching managed service to ${opts.to}...`);
721
1274
  try {
722
- execSync2(`systemctl --user restart ${SERVICE_NAME}`, { stdio: "inherit" });
723
- console.log("[agent] Upgrade complete!");
724
- } catch {
725
- console.log("[agent] Package upgraded. Service not installed or restart failed.");
726
- console.log("[agent] If running manually, restart with: npx @webmux/agent@latest start");
1275
+ upgradeService({
1276
+ agentName: creds.name,
1277
+ packageName: AGENT_PACKAGE_NAME,
1278
+ version: opts.to
1279
+ });
1280
+ console.log("[agent] Managed service updated successfully.");
1281
+ } catch (err) {
1282
+ const message = err instanceof Error ? err.message : String(err);
1283
+ console.error(`[agent] Failed to upgrade managed service: ${message}`);
1284
+ process.exit(1);
727
1285
  }
728
1286
  });
729
- function findBinary(name) {
730
- try {
731
- return execSync2(`which ${name} 2>/dev/null`, { encoding: "utf-8" }).trim();
732
- } catch {
733
- return null;
734
- }
735
- }
736
1287
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmux/agent",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmux-agent": "./dist/cli.js"