@webmux/agent 0.1.3 → 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 +671 -97
  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 } 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,18 +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 WebSocket from "ws";
202
-
203
- // src/terminal.ts
204
- import { spawn } from "node-pty";
205
-
206
- // ../shared/src/contracts.ts
207
- var DEFAULT_TERMINAL_SIZE = {
208
- cols: 120,
209
- rows: 36
210
- };
211
-
212
450
  // src/terminal.ts
213
451
  async function createTerminalBridge(options) {
214
452
  const {
@@ -252,28 +490,265 @@ async function createTerminalBridge(options) {
252
490
  };
253
491
  }
254
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
+
255
712
  // src/connection.ts
256
713
  var HEARTBEAT_INTERVAL_MS = 3e4;
257
714
  var SESSION_SYNC_INTERVAL_MS = 15e3;
258
715
  var INITIAL_RECONNECT_DELAY_MS = 1e3;
259
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
+ };
260
732
  var AgentConnection = class {
261
733
  serverUrl;
262
734
  agentId;
263
735
  agentSecret;
264
736
  tmux;
737
+ runtime;
265
738
  ws = null;
266
739
  heartbeatTimer = null;
267
740
  sessionSyncTimer = null;
268
741
  reconnectTimer = null;
269
742
  reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
270
743
  bridges = /* @__PURE__ */ new Map();
744
+ runs = /* @__PURE__ */ new Map();
271
745
  stopped = false;
272
- constructor(serverUrl, agentId, agentSecret, tmux) {
746
+ constructor(serverUrl, agentId, agentSecret, tmux, runtime = defaultAgentRuntime) {
273
747
  this.serverUrl = serverUrl;
274
748
  this.agentId = agentId;
275
749
  this.agentSecret = agentSecret;
276
750
  this.tmux = tmux;
751
+ this.runtime = runtime;
277
752
  }
278
753
  start() {
279
754
  this.stopped = false;
@@ -288,6 +763,7 @@ var AgentConnection = class {
288
763
  this.stopHeartbeat();
289
764
  this.stopSessionSync();
290
765
  this.disposeAllBridges();
766
+ this.disposeAllRuns();
291
767
  if (this.ws) {
292
768
  this.ws.close(1e3, "agent shutting down");
293
769
  this.ws = null;
@@ -301,7 +777,12 @@ var AgentConnection = class {
301
777
  ws.on("open", () => {
302
778
  console.log("[agent] WebSocket connected, authenticating...");
303
779
  this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
304
- this.sendMessage({ type: "auth", agentId: this.agentId, agentSecret: this.agentSecret });
780
+ this.sendMessage({
781
+ type: "auth",
782
+ agentId: this.agentId,
783
+ agentSecret: this.agentSecret,
784
+ version: this.runtime.version
785
+ });
305
786
  });
306
787
  ws.on("message", (raw) => {
307
788
  let msg;
@@ -325,6 +806,9 @@ var AgentConnection = class {
325
806
  switch (msg.type) {
326
807
  case "auth-ok":
327
808
  console.log("[agent] Authenticated successfully");
809
+ if (this.applyRecommendedUpgrade(msg.upgradePolicy)) {
810
+ return;
811
+ }
328
812
  this.startHeartbeat();
329
813
  this.startSessionSync();
330
814
  this.syncSessions();
@@ -336,7 +820,7 @@ var AgentConnection = class {
336
820
  this.ws.close();
337
821
  this.ws = null;
338
822
  }
339
- process.exit(1);
823
+ this.runtime.exit(1);
340
824
  break;
341
825
  case "sessions-list":
342
826
  this.syncSessions();
@@ -359,6 +843,21 @@ var AgentConnection = class {
359
843
  case "session-kill":
360
844
  this.handleSessionKill(msg.requestId, msg.name);
361
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;
362
861
  default:
363
862
  console.warn("[agent] Unknown message type:", msg.type);
364
863
  }
@@ -455,6 +954,43 @@ var AgentConnection = class {
455
954
  this.ws.send(JSON.stringify(msg));
456
955
  }
457
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
+ }
978
+ try {
979
+ this.runtime.applyServiceUpgrade({
980
+ packageName: upgradePolicy.packageName || AGENT_PACKAGE_NAME,
981
+ targetVersion
982
+ });
983
+ console.log(`[agent] Managed service switched to ${targetVersion}. Restarting...`);
984
+ } catch (err) {
985
+ const message = err instanceof Error ? err.message : String(err);
986
+ console.error(`[agent] Failed to apply managed upgrade: ${message}`);
987
+ console.log("[agent] Continuing with current version");
988
+ return false;
989
+ }
990
+ this.stop();
991
+ this.runtime.exit(0);
992
+ return true;
993
+ }
458
994
  startHeartbeat() {
459
995
  this.stopHeartbeat();
460
996
  this.heartbeatTimer = setInterval(() => {
@@ -485,6 +1021,76 @@ var AgentConnection = class {
485
1021
  this.bridges.delete(browserId);
486
1022
  }
487
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
+ }
488
1094
  onDisconnect() {
489
1095
  this.stopHeartbeat();
490
1096
  this.stopSessionSync();
@@ -508,12 +1114,11 @@ function buildWsUrl(serverUrl) {
508
1114
  }
509
1115
 
510
1116
  // src/cli.ts
511
- var SERVICE_NAME = "webmux-agent";
512
1117
  var program = new Command();
513
- 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);
514
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) => {
515
1120
  const serverUrl = opts.server.replace(/\/+$/, "");
516
- const agentName = opts.name ?? os2.hostname();
1121
+ const agentName = opts.name ?? os3.hostname();
517
1122
  console.log(`[agent] Registering with server ${serverUrl}...`);
518
1123
  console.log(`[agent] Agent name: ${agentName}`);
519
1124
  const body = {
@@ -555,8 +1160,8 @@ program.command("register").description("Register this agent with a webmux serve
555
1160
  console.log(`[agent] Credentials saved to ${credentialsPath()}`);
556
1161
  console.log(``);
557
1162
  console.log(`Next steps:`);
558
- console.log(` npx @webmux/agent start # run once`);
559
- 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`);
560
1165
  });
561
1166
  program.command("start").description("Start the agent and connect to the server").action(() => {
562
1167
  const creds = loadCredentials();
@@ -571,7 +1176,7 @@ program.command("start").description("Start the agent and connect to the server"
571
1176
  console.log(`[agent] Agent ID: ${creds.agentId}`);
572
1177
  const tmux = new TmuxClient({
573
1178
  socketName: "webmux",
574
- workspaceRoot: os2.homedir()
1179
+ workspaceRoot: os3.homedir()
575
1180
  });
576
1181
  const connection = new AgentConnection(
577
1182
  creds.serverUrl,
@@ -595,119 +1200,88 @@ program.command("status").description("Show agent status and credentials info").
595
1200
  process.exit(0);
596
1201
  }
597
1202
  console.log(`Agent Name: ${creds.name}`);
1203
+ console.log(`Agent Version: ${AGENT_VERSION}`);
598
1204
  console.log(`Server URL: ${creds.serverUrl}`);
599
1205
  console.log(`Agent ID: ${creds.agentId}`);
600
1206
  console.log(`Credentials File: ${credentialsPath()}`);
1207
+ const installedService = readInstalledServiceConfig();
601
1208
  try {
602
- const result = execSync(`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();
603
1210
  console.log(`Service: ${result}`);
604
1211
  } catch {
605
1212
  console.log(`Service: not installed`);
606
1213
  }
1214
+ if (installedService?.version) {
1215
+ console.log(`Service Version: ${installedService.version}`);
1216
+ }
607
1217
  });
608
1218
  var service = program.command("service").description("Manage the systemd service");
609
- 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) => {
610
1220
  const creds = loadCredentials();
611
1221
  if (!creds) {
612
1222
  console.error(`[agent] Not registered. Run "npx @webmux/agent register" first.`);
613
1223
  process.exit(1);
614
1224
  }
615
- const npxPath = findBinary("npx");
616
- if (!npxPath) {
617
- console.error(`[agent] Cannot find npx. Make sure Node.js is installed.`);
618
- process.exit(1);
619
- }
620
- const serviceDir = path2.join(os2.homedir(), ".config", "systemd", "user");
621
- const servicePath = path2.join(serviceDir, `${SERVICE_NAME}.service`);
622
- const npmPath = findBinary("npm") ?? "npm";
623
- const unit = `[Unit]
624
- Description=Webmux Agent (${creds.name})
625
- After=network-online.target
626
- Wants=network-online.target
627
-
628
- [Service]
629
- Type=simple
630
- ExecStartPre=${npmPath} install -g @webmux/agent@latest
631
- ExecStart=${findBinary("webmux-agent") ?? `${npxPath} -y @webmux/agent`} start
632
- Restart=always
633
- RestartSec=10
634
- Environment=HOME=${os2.homedir()}
635
- Environment=PATH=${process.env.PATH}
636
- WorkingDirectory=${os2.homedir()}
637
-
638
- [Install]
639
- WantedBy=default.target
640
- `;
641
- fs2.mkdirSync(serviceDir, { recursive: true });
642
- fs2.writeFileSync(servicePath, unit);
643
- console.log(`[agent] Service file created: ${servicePath}`);
644
1225
  try {
645
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
646
- execSync(`systemctl --user enable ${SERVICE_NAME}`, { stdio: "inherit" });
647
- execSync(`systemctl --user start ${SERVICE_NAME}`, { stdio: "inherit" });
648
- execSync(`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
+ });
649
1232
  console.log(``);
650
1233
  console.log(`[agent] Service installed and started!`);
1234
+ console.log(`[agent] Managed version: ${AGENT_VERSION}`);
651
1235
  console.log(`[agent] It will auto-start on boot.`);
652
1236
  console.log(``);
653
1237
  console.log(`Useful commands:`);
654
1238
  console.log(` systemctl --user status ${SERVICE_NAME}`);
655
1239
  console.log(` journalctl --user -u ${SERVICE_NAME} -f`);
656
- 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`);
657
1242
  } catch (err) {
658
1243
  const message = err instanceof Error ? err.message : String(err);
659
- console.error(`[agent] Failed to enable service: ${message}`);
660
- console.error(`[agent] Service file was written to ${servicePath}`);
661
- 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()}`);
662
1246
  process.exit(1);
663
1247
  }
664
1248
  });
665
1249
  service.command("uninstall").description("Stop and remove the systemd user service").action(() => {
666
- const servicePath = path2.join(os2.homedir(), ".config", "systemd", "user", `${SERVICE_NAME}.service`);
667
1250
  try {
668
- execSync(`systemctl --user stop ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
669
- execSync(`systemctl --user disable ${SERVICE_NAME} 2>/dev/null`, { stdio: "inherit" });
670
- } catch {
671
- }
672
- if (fs2.existsSync(servicePath)) {
673
- fs2.unlinkSync(servicePath);
674
- console.log(`[agent] Service file removed: ${servicePath}`);
675
- }
676
- try {
677
- execSync("systemctl --user daemon-reload", { stdio: "inherit" });
678
- } 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);
679
1258
  }
680
- console.log(`[agent] Service uninstalled.`);
681
1259
  });
682
1260
  service.command("status").description("Show systemd service status").action(() => {
683
1261
  try {
684
- execSync(`systemctl --user status ${SERVICE_NAME}`, { stdio: "inherit" });
1262
+ execFileSync2("systemctl", ["--user", "status", SERVICE_NAME], { stdio: "inherit" });
685
1263
  } catch {
686
1264
  console.log(`[agent] Service is not installed or not running.`);
687
1265
  }
688
1266
  });
689
- service.command("upgrade").description("Upgrade agent to latest version and restart service").action(() => {
690
- console.log("[agent] Upgrading @webmux/agent to latest...");
691
- try {
692
- execSync("npm install -g @webmux/agent@latest", { stdio: "inherit" });
693
- } catch {
694
- 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.`);
695
1271
  process.exit(1);
696
1272
  }
697
- console.log("[agent] Restarting service...");
1273
+ console.log(`[agent] Switching managed service to ${opts.to}...`);
698
1274
  try {
699
- execSync(`systemctl --user restart ${SERVICE_NAME}`, { stdio: "inherit" });
700
- console.log("[agent] Upgrade complete!");
701
- } catch {
702
- console.log("[agent] Package upgraded. Service not installed or restart failed.");
703
- 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);
704
1285
  }
705
1286
  });
706
- function findBinary(name) {
707
- try {
708
- return execSync(`which ${name} 2>/dev/null`, { encoding: "utf-8" }).trim();
709
- } catch {
710
- return null;
711
- }
712
- }
713
1287
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmux/agent",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "webmux-agent": "./dist/cli.js"