panopticon-cli 0.4.4 → 0.4.6

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 (68) hide show
  1. package/README.md +84 -2695
  2. package/dist/{agents-B5NRTVHK.js → agents-54LDKMHR.js} +8 -3
  3. package/dist/chunk-44EOY2ZL.js +58 -0
  4. package/dist/chunk-44EOY2ZL.js.map +1 -0
  5. package/dist/chunk-BWGFN44T.js +224 -0
  6. package/dist/chunk-BWGFN44T.js.map +1 -0
  7. package/dist/chunk-F7NQZD6H.js +49 -0
  8. package/dist/chunk-F7NQZD6H.js.map +1 -0
  9. package/dist/chunk-HCTJFIJJ.js +159 -0
  10. package/dist/chunk-HCTJFIJJ.js.map +1 -0
  11. package/dist/chunk-JM6V62LT.js +650 -0
  12. package/dist/chunk-JM6V62LT.js.map +1 -0
  13. package/dist/chunk-K45YD6A3.js +254 -0
  14. package/dist/chunk-K45YD6A3.js.map +1 -0
  15. package/dist/chunk-KGPRXDMX.js +137 -0
  16. package/dist/chunk-KGPRXDMX.js.map +1 -0
  17. package/dist/chunk-KQAEUOML.js +278 -0
  18. package/dist/chunk-KQAEUOML.js.map +1 -0
  19. package/dist/chunk-NYVQC3D7.js +90 -0
  20. package/dist/chunk-NYVQC3D7.js.map +1 -0
  21. package/dist/chunk-PUR532O7.js +1556 -0
  22. package/dist/chunk-PUR532O7.js.map +1 -0
  23. package/dist/chunk-VTDDVLCK.js +1977 -0
  24. package/dist/chunk-VTDDVLCK.js.map +1 -0
  25. package/dist/chunk-Z24TY3XN.js +916 -0
  26. package/dist/chunk-Z24TY3XN.js.map +1 -0
  27. package/dist/chunk-ZHC57RCV.js +44 -0
  28. package/dist/chunk-ZHC57RCV.js.map +1 -0
  29. package/dist/{chunk-ITI4IC5A.js → chunk-ZZ3477GY.js} +69 -100
  30. package/dist/chunk-ZZ3477GY.js.map +1 -0
  31. package/dist/cli/index.js +4664 -2912
  32. package/dist/cli/index.js.map +1 -1
  33. package/dist/dashboard/public/assets/index-CRqsEkmn.css +32 -0
  34. package/dist/dashboard/public/assets/index-DPSUbu4A.js +645 -0
  35. package/dist/dashboard/public/index.html +15 -3
  36. package/dist/dashboard/server.js +45663 -17860
  37. package/dist/dns-L3L2BB27.js +30 -0
  38. package/dist/dns-L3L2BB27.js.map +1 -0
  39. package/dist/index.d.ts +63 -3
  40. package/dist/index.js +42 -18
  41. package/dist/index.js.map +1 -1
  42. package/dist/projects-ESIB34QQ.js +43 -0
  43. package/dist/projects-ESIB34QQ.js.map +1 -0
  44. package/dist/remote-agents-Z3R2A5BN.js +25 -0
  45. package/dist/remote-agents-Z3R2A5BN.js.map +1 -0
  46. package/dist/remote-workspace-HI4VML6H.js +179 -0
  47. package/dist/remote-workspace-HI4VML6H.js.map +1 -0
  48. package/dist/specialist-context-SNCJ7O7G.js +256 -0
  49. package/dist/specialist-context-SNCJ7O7G.js.map +1 -0
  50. package/dist/specialist-logs-A7ODEK2T.js +43 -0
  51. package/dist/specialist-logs-A7ODEK2T.js.map +1 -0
  52. package/dist/specialists-C7XLNSXQ.js +121 -0
  53. package/dist/specialists-C7XLNSXQ.js.map +1 -0
  54. package/dist/traefik-WI3KSRGG.js +12 -0
  55. package/dist/traefik-WI3KSRGG.js.map +1 -0
  56. package/package.json +1 -1
  57. package/templates/traefik/docker-compose.yml +1 -1
  58. package/templates/traefik/dynamic/panopticon.yml.template +41 -0
  59. package/templates/traefik/traefik.yml +8 -0
  60. package/dist/chunk-7HHDVXBM.js +0 -349
  61. package/dist/chunk-7HHDVXBM.js.map +0 -1
  62. package/dist/chunk-H45CLB7E.js +0 -2044
  63. package/dist/chunk-H45CLB7E.js.map +0 -1
  64. package/dist/chunk-ITI4IC5A.js.map +0 -1
  65. package/dist/dashboard/public/assets/index-BDd8hGYb.css +0 -32
  66. package/dist/dashboard/public/assets/index-sFwLPko-.js +0 -556
  67. package/templates/traefik/dynamic/panopticon.yml +0 -51
  68. /package/dist/{agents-B5NRTVHK.js.map → agents-54LDKMHR.js.map} +0 -0
@@ -7,6 +7,7 @@ import {
7
7
  getAgentRuntimeState,
8
8
  getAgentState,
9
9
  getSessionId,
10
+ init_agents,
10
11
  listRunningAgents,
11
12
  messageAgent,
12
13
  recoverAgent,
@@ -16,8 +17,12 @@ import {
16
17
  saveSessionId,
17
18
  spawnAgent,
18
19
  stopAgent
19
- } from "./chunk-H45CLB7E.js";
20
- import "./chunk-7HHDVXBM.js";
20
+ } from "./chunk-Z24TY3XN.js";
21
+ import "./chunk-PUR532O7.js";
22
+ import "./chunk-KQAEUOML.js";
23
+ import "./chunk-KGPRXDMX.js";
24
+ import "./chunk-ZHC57RCV.js";
25
+ init_agents();
21
26
  export {
22
27
  appendActivity,
23
28
  autoRecoverAgents,
@@ -37,4 +42,4 @@ export {
37
42
  spawnAgent,
38
43
  stopAgent
39
44
  };
40
- //# sourceMappingURL=agents-B5NRTVHK.js.map
45
+ //# sourceMappingURL=agents-54LDKMHR.js.map
@@ -0,0 +1,58 @@
1
+ import {
2
+ __require,
3
+ init_esm_shims
4
+ } from "./chunk-ZHC57RCV.js";
5
+
6
+ // src/lib/remote/workspace-metadata.ts
7
+ init_esm_shims();
8
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from "fs";
9
+ import { join } from "path";
10
+ import { homedir } from "os";
11
+ import { parse, stringify } from "yaml";
12
+ var WORKSPACES_DIR = join(homedir(), ".panopticon", "workspaces");
13
+ function saveWorkspaceMetadata(metadata) {
14
+ if (!existsSync(WORKSPACES_DIR)) {
15
+ mkdirSync(WORKSPACES_DIR, { recursive: true });
16
+ }
17
+ const filename = join(WORKSPACES_DIR, `${metadata.id}.yaml`);
18
+ writeFileSync(filename, stringify(metadata), "utf-8");
19
+ }
20
+ function loadWorkspaceMetadata(issueId) {
21
+ const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
22
+ const filename = join(WORKSPACES_DIR, `${normalizedId}.yaml`);
23
+ if (!existsSync(filename)) {
24
+ return null;
25
+ }
26
+ try {
27
+ const content = readFileSync(filename, "utf-8");
28
+ return parse(content);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function findRemoteWorkspaceMetadata(issueId) {
34
+ return loadWorkspaceMetadata(issueId);
35
+ }
36
+ function deleteWorkspaceMetadata(issueId) {
37
+ const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
38
+ const filename = join(WORKSPACES_DIR, `${normalizedId}.yaml`);
39
+ if (!existsSync(filename)) {
40
+ return false;
41
+ }
42
+ try {
43
+ const { unlinkSync } = __require("fs");
44
+ unlinkSync(filename);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+
51
+ export {
52
+ WORKSPACES_DIR,
53
+ saveWorkspaceMetadata,
54
+ loadWorkspaceMetadata,
55
+ findRemoteWorkspaceMetadata,
56
+ deleteWorkspaceMetadata
57
+ };
58
+ //# sourceMappingURL=chunk-44EOY2ZL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/remote/workspace-metadata.ts"],"sourcesContent":["/**\n * Workspace Metadata Management\n *\n * Shared module for loading, saving, and listing workspace metadata.\n * Used by both workspace.ts and work/issue.ts for remote workspace support.\n */\n\nimport { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { parse, stringify } from 'yaml';\nimport type { RemoteWorkspaceMetadata } from './interface.js';\n\n// Path for workspace metadata\nexport const WORKSPACES_DIR = join(homedir(), '.panopticon', 'workspaces');\n\n/**\n * Save workspace metadata to ~/.panopticon/workspaces/{issueId}.yaml\n */\nexport function saveWorkspaceMetadata(metadata: RemoteWorkspaceMetadata): void {\n if (!existsSync(WORKSPACES_DIR)) {\n mkdirSync(WORKSPACES_DIR, { recursive: true });\n }\n\n const filename = join(WORKSPACES_DIR, `${metadata.id}.yaml`);\n writeFileSync(filename, stringify(metadata), 'utf-8');\n}\n\n/**\n * Load workspace metadata from ~/.panopticon/workspaces/{issueId}.yaml\n */\nexport function loadWorkspaceMetadata(issueId: string): RemoteWorkspaceMetadata | null {\n const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n const filename = join(WORKSPACES_DIR, `${normalizedId}.yaml`);\n\n if (!existsSync(filename)) {\n return null;\n }\n\n try {\n const content = readFileSync(filename, 'utf-8');\n return parse(content) as RemoteWorkspaceMetadata;\n } catch {\n return null;\n }\n}\n\n/**\n * List all workspace metadata files\n */\nexport function listWorkspaceMetadata(): RemoteWorkspaceMetadata[] {\n if (!existsSync(WORKSPACES_DIR)) {\n return [];\n }\n\n const files = readdirSync(WORKSPACES_DIR).filter(f => f.endsWith('.yaml'));\n const workspaces: RemoteWorkspaceMetadata[] = [];\n\n for (const file of files) {\n try {\n const content = readFileSync(join(WORKSPACES_DIR, file), 'utf-8');\n workspaces.push(parse(content) as RemoteWorkspaceMetadata);\n } catch {\n // Skip invalid files\n }\n }\n\n return workspaces;\n}\n\n/**\n * Check if a workspace exists (local or remote)\n * Returns metadata if remote workspace exists, null otherwise\n */\nexport function findRemoteWorkspaceMetadata(issueId: string): RemoteWorkspaceMetadata | null {\n return loadWorkspaceMetadata(issueId);\n}\n\n/**\n * Delete workspace metadata\n */\nexport function deleteWorkspaceMetadata(issueId: string): boolean {\n const normalizedId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, '-');\n const filename = join(WORKSPACES_DIR, `${normalizedId}.yaml`);\n\n if (!existsSync(filename)) {\n return false;\n }\n\n try {\n const { unlinkSync } = require('fs');\n unlinkSync(filename);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":";;;;;;AAAA;AAOA,SAAS,YAAY,WAAW,eAAe,aAAa,oBAAoB;AAChF,SAAS,YAAY;AACrB,SAAS,eAAe;AACxB,SAAS,OAAO,iBAAiB;AAI1B,IAAM,iBAAiB,KAAK,QAAQ,GAAG,eAAe,YAAY;AAKlE,SAAS,sBAAsB,UAAyC;AAC7E,MAAI,CAAC,WAAW,cAAc,GAAG;AAC/B,cAAU,gBAAgB,EAAE,WAAW,KAAK,CAAC;AAAA,EAC/C;AAEA,QAAM,WAAW,KAAK,gBAAgB,GAAG,SAAS,EAAE,OAAO;AAC3D,gBAAc,UAAU,UAAU,QAAQ,GAAG,OAAO;AACtD;AAKO,SAAS,sBAAsB,SAAiD;AACrF,QAAM,eAAe,QAAQ,YAAY,EAAE,QAAQ,eAAe,GAAG;AACrE,QAAM,WAAW,KAAK,gBAAgB,GAAG,YAAY,OAAO;AAE5D,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,WAAO,MAAM,OAAO;AAAA,EACtB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AA6BO,SAAS,4BAA4B,SAAiD;AAC3F,SAAO,sBAAsB,OAAO;AACtC;AAKO,SAAS,wBAAwB,SAA0B;AAChE,QAAM,eAAe,QAAQ,YAAY,EAAE,QAAQ,eAAe,GAAG;AACrE,QAAM,WAAW,KAAK,gBAAgB,GAAG,YAAY,OAAO;AAE5D,MAAI,CAAC,WAAW,QAAQ,GAAG;AACzB,WAAO;AAAA,EACT;AAEA,MAAI;AACF,UAAM,EAAE,WAAW,IAAI,UAAQ,IAAI;AACnC,eAAW,QAAQ;AACnB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,224 @@
1
+ import {
2
+ init_esm_shims
3
+ } from "./chunk-ZHC57RCV.js";
4
+
5
+ // src/lib/dns.ts
6
+ init_esm_shims();
7
+ import { existsSync, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import { exec, execSync } from "child_process";
11
+ import { promisify } from "util";
12
+
13
+ // src/lib/platform.ts
14
+ init_esm_shims();
15
+ import { readFileSync } from "fs";
16
+ import { platform } from "os";
17
+ function detectPlatform() {
18
+ const os = platform();
19
+ if (os === "linux") {
20
+ try {
21
+ const release = readFileSync("/proc/version", "utf8").toLowerCase();
22
+ if (release.includes("microsoft") || release.includes("wsl")) {
23
+ return "wsl";
24
+ }
25
+ } catch {
26
+ }
27
+ return "linux";
28
+ }
29
+ return os;
30
+ }
31
+
32
+ // src/lib/dns.ts
33
+ var execAsync = promisify(exec);
34
+ function detectDnsSyncMethod() {
35
+ const plat = detectPlatform();
36
+ switch (plat) {
37
+ case "wsl":
38
+ return "wsl2hosts";
39
+ case "darwin":
40
+ return isDnsmasqInstalled() ? "dnsmasq" : "hosts_file";
41
+ case "linux":
42
+ return isDnsmasqInstalled() ? "dnsmasq" : "hosts_file";
43
+ default:
44
+ return "hosts_file";
45
+ }
46
+ }
47
+ function isDnsmasqInstalled() {
48
+ try {
49
+ execSync("which dnsmasq", { stdio: "pipe" });
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ function addWsl2HostEntry(hostname) {
56
+ const wsl2hostsPath = join(homedir(), ".wsl2hosts");
57
+ try {
58
+ let content = "";
59
+ if (existsSync(wsl2hostsPath)) {
60
+ content = readFileSync2(wsl2hostsPath, "utf-8");
61
+ }
62
+ if (!content.includes(hostname)) {
63
+ writeFileSync(wsl2hostsPath, content + (content.endsWith("\n") ? "" : "\n") + hostname + "\n");
64
+ }
65
+ return true;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ function removeWsl2HostEntry(hostname) {
71
+ const wsl2hostsPath = join(homedir(), ".wsl2hosts");
72
+ try {
73
+ if (!existsSync(wsl2hostsPath)) return true;
74
+ const content = readFileSync2(wsl2hostsPath, "utf-8");
75
+ const lines = content.split("\n").filter((line) => line.trim() !== hostname);
76
+ writeFileSync(wsl2hostsPath, lines.join("\n"));
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+ async function syncDnsToWindows() {
83
+ try {
84
+ await execAsync(`powershell.exe -Command "Start-ScheduledTask -TaskName 'PanopticonWsl2HostsSync'"`);
85
+ return true;
86
+ } catch {
87
+ try {
88
+ await execAsync(`powershell.exe -Command "Start-ScheduledTask -TaskName 'SyncMynHosts'"`);
89
+ return true;
90
+ } catch {
91
+ return false;
92
+ }
93
+ }
94
+ }
95
+ var HOSTS_FILE = "/etc/hosts";
96
+ var MARKER_START = "# BEGIN panopticon managed entries";
97
+ var MARKER_END = "# END panopticon managed entries";
98
+ function addHostsFileEntry(hostname, ip = "127.0.0.1") {
99
+ try {
100
+ let content = existsSync(HOSTS_FILE) ? readFileSync2(HOSTS_FILE, "utf-8") : "";
101
+ const entry = `${ip} ${hostname}`;
102
+ if (content.includes(` ${hostname}`) || content.includes(` ${hostname}`)) return true;
103
+ const startIdx = content.indexOf(MARKER_START);
104
+ const endIdx = content.indexOf(MARKER_END);
105
+ if (startIdx !== -1 && endIdx !== -1) {
106
+ const before = content.substring(0, endIdx);
107
+ const after = content.substring(endIdx);
108
+ content = before + entry + "\n" + after;
109
+ } else {
110
+ content = content.trimEnd() + "\n\n" + MARKER_START + "\n" + entry + "\n" + MARKER_END + "\n";
111
+ }
112
+ writeFileSync(HOSTS_FILE, content);
113
+ return true;
114
+ } catch {
115
+ return false;
116
+ }
117
+ }
118
+ function removeHostsFileEntry(hostname) {
119
+ try {
120
+ if (!existsSync(HOSTS_FILE)) return true;
121
+ const content = readFileSync2(HOSTS_FILE, "utf-8");
122
+ const lines = content.split("\n").filter((line) => {
123
+ const parts = line.trim().split(/\s+/);
124
+ return parts[1] !== hostname;
125
+ });
126
+ writeFileSync(HOSTS_FILE, lines.join("\n"));
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ function getDnsmasqConfigDir() {
133
+ const plat = detectPlatform();
134
+ if (plat === "darwin") {
135
+ const brewPrefix = existsSync("/opt/homebrew") ? "/opt/homebrew" : "/usr/local";
136
+ return `${brewPrefix}/etc/dnsmasq.d`;
137
+ }
138
+ return "/etc/dnsmasq.d";
139
+ }
140
+ var PANOPTICON_DNSMASQ_CONF = "panopticon.conf";
141
+ function addDnsmasqEntry(hostname, ip = "127.0.0.1") {
142
+ try {
143
+ const configDir = getDnsmasqConfigDir();
144
+ mkdirSync(configDir, { recursive: true });
145
+ const confPath = join(configDir, PANOPTICON_DNSMASQ_CONF);
146
+ let content = "";
147
+ if (existsSync(confPath)) {
148
+ content = readFileSync2(confPath, "utf-8");
149
+ }
150
+ const entry = `address=/${hostname}/${ip}`;
151
+ if (content.includes(entry)) return true;
152
+ content = content.trimEnd() + (content.length > 0 ? "\n" : "") + entry + "\n";
153
+ writeFileSync(confPath, content);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
158
+ }
159
+ function removeDnsmasqEntry(hostname) {
160
+ try {
161
+ const configDir = getDnsmasqConfigDir();
162
+ const confPath = join(configDir, PANOPTICON_DNSMASQ_CONF);
163
+ if (!existsSync(confPath)) return true;
164
+ const content = readFileSync2(confPath, "utf-8");
165
+ const lines = content.split("\n").filter((line) => !line.includes(`/${hostname}/`));
166
+ writeFileSync(confPath, lines.join("\n"));
167
+ return true;
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+ async function restartDnsmasq() {
173
+ const plat = detectPlatform();
174
+ try {
175
+ if (plat === "darwin") {
176
+ await execAsync("brew services restart dnsmasq");
177
+ } else {
178
+ await execAsync("sudo systemctl restart dnsmasq");
179
+ }
180
+ return true;
181
+ } catch {
182
+ return false;
183
+ }
184
+ }
185
+ function addDnsEntry(method, hostname) {
186
+ switch (method) {
187
+ case "wsl2hosts":
188
+ return addWsl2HostEntry(hostname);
189
+ case "hosts_file":
190
+ return addHostsFileEntry(hostname);
191
+ case "dnsmasq":
192
+ return addDnsmasqEntry(hostname);
193
+ }
194
+ }
195
+ function removeDnsEntry(method, hostname) {
196
+ switch (method) {
197
+ case "wsl2hosts":
198
+ return removeWsl2HostEntry(hostname);
199
+ case "hosts_file":
200
+ return removeHostsFileEntry(hostname);
201
+ case "dnsmasq":
202
+ return removeDnsmasqEntry(hostname);
203
+ }
204
+ }
205
+ function ensureBaseDomain(method, domain = "pan.localhost") {
206
+ return addDnsEntry(method, domain);
207
+ }
208
+
209
+ export {
210
+ detectPlatform,
211
+ detectDnsSyncMethod,
212
+ addWsl2HostEntry,
213
+ removeWsl2HostEntry,
214
+ syncDnsToWindows,
215
+ addHostsFileEntry,
216
+ removeHostsFileEntry,
217
+ addDnsmasqEntry,
218
+ removeDnsmasqEntry,
219
+ restartDnsmasq,
220
+ addDnsEntry,
221
+ removeDnsEntry,
222
+ ensureBaseDomain
223
+ };
224
+ //# sourceMappingURL=chunk-BWGFN44T.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/dns.ts","../src/lib/platform.ts"],"sourcesContent":["/**\n * DNS Management\n *\n * Centralized DNS entry management for Panopticon.\n * Supports three sync methods:\n * - wsl2hosts: WSL2 → Windows hosts file sync via PowerShell scheduled task\n * - hosts_file: Direct /etc/hosts manipulation with managed block markers\n * - dnsmasq: System-wide dnsmasq configuration (Linux/macOS)\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport { exec, execSync } from 'child_process';\nimport { promisify } from 'util';\nimport { detectPlatform } from './platform.js';\n\nconst execAsync = promisify(exec);\n\nexport type DnsSyncMethod = 'wsl2hosts' | 'hosts_file' | 'dnsmasq';\n\n// ---- Detection ----\n\n/**\n * Detect the best DNS sync method for the current platform.\n */\nexport function detectDnsSyncMethod(): DnsSyncMethod {\n const plat = detectPlatform();\n switch (plat) {\n case 'wsl':\n return 'wsl2hosts';\n case 'darwin':\n return isDnsmasqInstalled() ? 'dnsmasq' : 'hosts_file';\n case 'linux':\n return isDnsmasqInstalled() ? 'dnsmasq' : 'hosts_file';\n default:\n return 'hosts_file';\n }\n}\n\n/**\n * Check if dnsmasq is installed.\n * Note: Uses execSync intentionally — this only runs in CLI context (pan install/up),\n * never from the dashboard server.\n */\nfunction isDnsmasqInstalled(): boolean {\n try {\n execSync('which dnsmasq', { stdio: 'pipe' });\n return true;\n } catch {\n return false;\n }\n}\n\n// ---- wsl2hosts method ----\n\nexport function addWsl2HostEntry(hostname: string): boolean {\n const wsl2hostsPath = join(homedir(), '.wsl2hosts');\n\n try {\n let content = '';\n if (existsSync(wsl2hostsPath)) {\n content = readFileSync(wsl2hostsPath, 'utf-8');\n }\n\n if (!content.includes(hostname)) {\n writeFileSync(wsl2hostsPath, content + (content.endsWith('\\n') ? '' : '\\n') + hostname + '\\n');\n }\n return true;\n } catch {\n return false;\n }\n}\n\nexport function removeWsl2HostEntry(hostname: string): boolean {\n const wsl2hostsPath = join(homedir(), '.wsl2hosts');\n\n try {\n if (!existsSync(wsl2hostsPath)) return true;\n\n const content = readFileSync(wsl2hostsPath, 'utf-8');\n const lines = content.split('\\n').filter(line => line.trim() !== hostname);\n writeFileSync(wsl2hostsPath, lines.join('\\n'));\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function syncDnsToWindows(): Promise<boolean> {\n try {\n await execAsync('powershell.exe -Command \"Start-ScheduledTask -TaskName \\'PanopticonWsl2HostsSync\\'\"');\n return true;\n } catch {\n // Fall back to legacy task name\n try {\n await execAsync('powershell.exe -Command \"Start-ScheduledTask -TaskName \\'SyncMynHosts\\'\"');\n return true;\n } catch {\n return false;\n }\n }\n}\n\n// ---- hosts_file method ----\n\nconst HOSTS_FILE = '/etc/hosts';\nconst MARKER_START = '# BEGIN panopticon managed entries';\nconst MARKER_END = '# END panopticon managed entries';\n\nexport function addHostsFileEntry(hostname: string, ip: string = '127.0.0.1'): boolean {\n try {\n let content = existsSync(HOSTS_FILE) ? readFileSync(HOSTS_FILE, 'utf-8') : '';\n const entry = `${ip}\\t${hostname}`;\n\n // Already present anywhere in file\n if (content.includes(`\\t${hostname}`) || content.includes(` ${hostname}`)) return true;\n\n const startIdx = content.indexOf(MARKER_START);\n const endIdx = content.indexOf(MARKER_END);\n\n if (startIdx !== -1 && endIdx !== -1) {\n // Managed block exists — insert entry before MARKER_END\n const before = content.substring(0, endIdx);\n const after = content.substring(endIdx);\n content = before + entry + '\\n' + after;\n } else {\n // Create managed block at end\n content = content.trimEnd() + '\\n\\n' + MARKER_START + '\\n' + entry + '\\n' + MARKER_END + '\\n';\n }\n\n writeFileSync(HOSTS_FILE, content);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function removeHostsFileEntry(hostname: string): boolean {\n try {\n if (!existsSync(HOSTS_FILE)) return true;\n\n const content = readFileSync(HOSTS_FILE, 'utf-8');\n const lines = content.split('\\n').filter(line => {\n const parts = line.trim().split(/\\s+/);\n return parts[1] !== hostname;\n });\n writeFileSync(HOSTS_FILE, lines.join('\\n'));\n return true;\n } catch {\n return false;\n }\n}\n\n// ---- dnsmasq method ----\n\nfunction getDnsmasqConfigDir(): string {\n const plat = detectPlatform();\n if (plat === 'darwin') {\n // Homebrew Intel location; Apple Silicon uses /opt/homebrew/etc/dnsmasq.d\n const brewPrefix = existsSync('/opt/homebrew') ? '/opt/homebrew' : '/usr/local';\n return `${brewPrefix}/etc/dnsmasq.d`;\n }\n return '/etc/dnsmasq.d';\n}\n\nconst PANOPTICON_DNSMASQ_CONF = 'panopticon.conf';\n\nexport function addDnsmasqEntry(hostname: string, ip: string = '127.0.0.1'): boolean {\n try {\n const configDir = getDnsmasqConfigDir();\n mkdirSync(configDir, { recursive: true });\n const confPath = join(configDir, PANOPTICON_DNSMASQ_CONF);\n\n let content = '';\n if (existsSync(confPath)) {\n content = readFileSync(confPath, 'utf-8');\n }\n\n const entry = `address=/${hostname}/${ip}`;\n if (content.includes(entry)) return true;\n\n content = content.trimEnd() + (content.length > 0 ? '\\n' : '') + entry + '\\n';\n writeFileSync(confPath, content);\n return true;\n } catch {\n return false;\n }\n}\n\nexport function removeDnsmasqEntry(hostname: string): boolean {\n try {\n const configDir = getDnsmasqConfigDir();\n const confPath = join(configDir, PANOPTICON_DNSMASQ_CONF);\n if (!existsSync(confPath)) return true;\n\n const content = readFileSync(confPath, 'utf-8');\n const lines = content.split('\\n').filter(line => !line.includes(`/${hostname}/`));\n writeFileSync(confPath, lines.join('\\n'));\n return true;\n } catch {\n return false;\n }\n}\n\nexport async function restartDnsmasq(): Promise<boolean> {\n const plat = detectPlatform();\n try {\n if (plat === 'darwin') {\n await execAsync('brew services restart dnsmasq');\n } else {\n await execAsync('sudo systemctl restart dnsmasq');\n }\n return true;\n } catch {\n return false;\n }\n}\n\n// ---- Unified interface ----\n\n/**\n * Add a DNS entry using the specified sync method.\n */\nexport function addDnsEntry(method: DnsSyncMethod, hostname: string): boolean {\n switch (method) {\n case 'wsl2hosts':\n return addWsl2HostEntry(hostname);\n case 'hosts_file':\n return addHostsFileEntry(hostname);\n case 'dnsmasq':\n return addDnsmasqEntry(hostname);\n }\n}\n\n/**\n * Remove a DNS entry using the specified sync method.\n */\nexport function removeDnsEntry(method: DnsSyncMethod, hostname: string): boolean {\n switch (method) {\n case 'wsl2hosts':\n return removeWsl2HostEntry(hostname);\n case 'hosts_file':\n return removeHostsFileEntry(hostname);\n case 'dnsmasq':\n return removeDnsmasqEntry(hostname);\n }\n}\n\n/**\n * Ensure the base Panopticon domain is resolvable.\n * Called during `pan install` and `pan up`.\n */\nexport function ensureBaseDomain(method: DnsSyncMethod, domain: string = 'pan.localhost'): boolean {\n return addDnsEntry(method, domain);\n}\n","/**\n * Platform Detection\n *\n * Shared platform detection utility. Distinguishes between\n * native Linux, macOS, Windows, and WSL2.\n */\n\nimport { readFileSync } from 'fs';\nimport { platform } from 'os';\n\nexport type Platform = 'linux' | 'darwin' | 'win32' | 'wsl';\n\nexport function detectPlatform(): Platform {\n const os = platform();\n if (os === 'linux') {\n // Check for WSL\n try {\n const release = readFileSync('/proc/version', 'utf8').toLowerCase();\n if (release.includes('microsoft') || release.includes('wsl')) {\n return 'wsl';\n }\n } catch {}\n return 'linux';\n }\n return os as 'darwin' | 'win32';\n}\n"],"mappings":"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","names":["readFileSync","readFileSync"]}
@@ -0,0 +1,49 @@
1
+ import {
2
+ loadConfig
3
+ } from "./chunk-NYVQC3D7.js";
4
+ import {
5
+ SOURCE_TRAEFIK_TEMPLATES,
6
+ TRAEFIK_DYNAMIC_DIR,
7
+ init_paths
8
+ } from "./chunk-KGPRXDMX.js";
9
+ import {
10
+ init_esm_shims
11
+ } from "./chunk-ZHC57RCV.js";
12
+
13
+ // src/lib/traefik.ts
14
+ init_esm_shims();
15
+ init_paths();
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from "fs";
17
+ import { join } from "path";
18
+ function generatePanopticonTraefikConfig() {
19
+ const templatePath = join(SOURCE_TRAEFIK_TEMPLATES, "dynamic", "panopticon.yml.template");
20
+ if (!existsSync(templatePath)) {
21
+ return false;
22
+ }
23
+ const config = loadConfig();
24
+ const placeholders = {
25
+ TRAEFIK_DOMAIN: config.traefik?.domain || "pan.localhost",
26
+ DASHBOARD_PORT: String(config.dashboard.port),
27
+ DASHBOARD_API_PORT: String(config.dashboard.api_port)
28
+ };
29
+ let content = readFileSync(templatePath, "utf-8");
30
+ for (const [key, value] of Object.entries(placeholders)) {
31
+ content = content.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
32
+ }
33
+ mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });
34
+ const outputPath = join(TRAEFIK_DYNAMIC_DIR, "panopticon.yml");
35
+ writeFileSync(outputPath, content, "utf-8");
36
+ return true;
37
+ }
38
+ function cleanupTemplateFiles() {
39
+ const copiedTemplate = join(TRAEFIK_DYNAMIC_DIR, "panopticon.yml.template");
40
+ if (existsSync(copiedTemplate)) {
41
+ unlinkSync(copiedTemplate);
42
+ }
43
+ }
44
+
45
+ export {
46
+ generatePanopticonTraefikConfig,
47
+ cleanupTemplateFiles
48
+ };
49
+ //# sourceMappingURL=chunk-F7NQZD6H.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/traefik.ts"],"sourcesContent":["/**\n * Traefik Configuration Generator\n *\n * Generates the Panopticon dashboard Traefik routing config\n * from a template, substituting values from config.toml.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';\nimport { join } from 'path';\nimport { TRAEFIK_DYNAMIC_DIR, SOURCE_TRAEFIK_TEMPLATES } from './paths.js';\nimport { loadConfig } from './config.js';\n\n/**\n * Generate panopticon.yml from template using current config values.\n * Safe to call multiple times (idempotent).\n * Returns true if file was written, false if template not found.\n */\nexport function generatePanopticonTraefikConfig(): boolean {\n const templatePath = join(SOURCE_TRAEFIK_TEMPLATES, 'dynamic', 'panopticon.yml.template');\n if (!existsSync(templatePath)) {\n return false;\n }\n\n const config = loadConfig();\n const placeholders: Record<string, string> = {\n TRAEFIK_DOMAIN: config.traefik?.domain || 'pan.localhost',\n DASHBOARD_PORT: String(config.dashboard.port),\n DASHBOARD_API_PORT: String(config.dashboard.api_port),\n };\n\n let content = readFileSync(templatePath, 'utf-8');\n for (const [key, value] of Object.entries(placeholders)) {\n content = content.replace(new RegExp(`\\\\{\\\\{${key}\\\\}\\\\}`, 'g'), value);\n }\n\n mkdirSync(TRAEFIK_DYNAMIC_DIR, { recursive: true });\n const outputPath = join(TRAEFIK_DYNAMIC_DIR, 'panopticon.yml');\n writeFileSync(outputPath, content, 'utf-8');\n return true;\n}\n\n/**\n * Remove any accidentally-copied .template files from the runtime Traefik dir.\n * Called after copyDirectoryRecursive in pan install.\n */\nexport function cleanupTemplateFiles(): void {\n const copiedTemplate = join(TRAEFIK_DYNAMIC_DIR, 'panopticon.yml.template');\n if (existsSync(copiedTemplate)) {\n unlinkSync(copiedTemplate);\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAAA;AASA;AAFA,SAAS,YAAY,cAAc,eAAe,WAAW,kBAAkB;AAC/E,SAAS,YAAY;AASd,SAAS,kCAA2C;AACzD,QAAM,eAAe,KAAK,0BAA0B,WAAW,yBAAyB;AACxF,MAAI,CAAC,WAAW,YAAY,GAAG;AAC7B,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,WAAW;AAC1B,QAAM,eAAuC;AAAA,IAC3C,gBAAgB,OAAO,SAAS,UAAU;AAAA,IAC1C,gBAAgB,OAAO,OAAO,UAAU,IAAI;AAAA,IAC5C,oBAAoB,OAAO,OAAO,UAAU,QAAQ;AAAA,EACtD;AAEA,MAAI,UAAU,aAAa,cAAc,OAAO;AAChD,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,YAAY,GAAG;AACvD,cAAU,QAAQ,QAAQ,IAAI,OAAO,SAAS,GAAG,UAAU,GAAG,GAAG,KAAK;AAAA,EACxE;AAEA,YAAU,qBAAqB,EAAE,WAAW,KAAK,CAAC;AAClD,QAAM,aAAa,KAAK,qBAAqB,gBAAgB;AAC7D,gBAAc,YAAY,SAAS,OAAO;AAC1C,SAAO;AACT;AAMO,SAAS,uBAA6B;AAC3C,QAAM,iBAAiB,KAAK,qBAAqB,yBAAyB;AAC1E,MAAI,WAAW,cAAc,GAAG;AAC9B,eAAW,cAAc;AAAA,EAC3B;AACF;","names":[]}
@@ -0,0 +1,159 @@
1
+ import {
2
+ createExeProvider,
3
+ init_exe_provider
4
+ } from "./chunk-JM6V62LT.js";
5
+ import {
6
+ __esm,
7
+ init_esm_shims
8
+ } from "./chunk-ZHC57RCV.js";
9
+
10
+ // src/lib/remote/remote-agents.ts
11
+ import { join } from "path";
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
13
+ import { homedir } from "os";
14
+ function getRemoteAgentStateFile(agentId) {
15
+ return join(AGENTS_DIR, agentId, "remote-state.json");
16
+ }
17
+ function saveRemoteAgentState(state) {
18
+ const dir = join(AGENTS_DIR, state.id);
19
+ if (!existsSync(dir)) {
20
+ mkdirSync(dir, { recursive: true });
21
+ }
22
+ writeFileSync(getRemoteAgentStateFile(state.id), JSON.stringify(state, null, 2));
23
+ }
24
+ function loadRemoteAgentState(agentId) {
25
+ const file = getRemoteAgentStateFile(agentId);
26
+ if (!existsSync(file)) return null;
27
+ try {
28
+ return JSON.parse(readFileSync(file, "utf-8"));
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ async function remoteSessionExists(exe, vmName, sessionName) {
34
+ const result = await exe.ssh(vmName, `tmux has-session -t ${sessionName} 2>/dev/null && echo exists || echo not-found`);
35
+ return result.stdout.trim() === "exists";
36
+ }
37
+ async function spawnRemoteAgent(options) {
38
+ const { issueId, workspace, model = "claude-sonnet-4-5", prompt } = options;
39
+ const agentId = `agent-${issueId.toLowerCase()}`;
40
+ const vmName = workspace.vmName;
41
+ const exe = createExeProvider({ infraVm: workspace.infraVm });
42
+ const vmStatus = await exe.getStatus(vmName);
43
+ if (vmStatus !== "running") {
44
+ throw new Error(`VM ${vmName} is not running. Start it with: pan workspace start ${issueId}`);
45
+ }
46
+ if (await remoteSessionExists(exe, vmName, agentId)) {
47
+ throw new Error(`Agent ${agentId} already running on ${vmName}. Use 'pan work tell' to message it.`);
48
+ }
49
+ const state = {
50
+ id: agentId,
51
+ issueId,
52
+ vmName,
53
+ model,
54
+ status: "starting",
55
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
56
+ location: "remote"
57
+ };
58
+ saveRemoteAgentState(state);
59
+ let claudeCmd;
60
+ if (prompt) {
61
+ const promptFile = `/workspace/.panopticon/prompts/${agentId}.md`;
62
+ await exe.ssh(vmName, `mkdir -p /workspace/.panopticon/prompts`);
63
+ const promptBase64 = Buffer.from(prompt).toString("base64");
64
+ await exe.ssh(vmName, `echo '${promptBase64}' | base64 -d > ${promptFile}`);
65
+ const launcherScript = `/workspace/.panopticon/prompts/${agentId}-launcher.sh`;
66
+ const launcherContent = `#!/bin/bash
67
+ export PATH="/usr/local/bin:$PATH"
68
+ prompt=$(cat "${promptFile}")
69
+ exec claude --dangerously-skip-permissions --model ${model} "$prompt"
70
+ `;
71
+ const launcherBase64 = Buffer.from(launcherContent).toString("base64");
72
+ await exe.ssh(vmName, `echo '${launcherBase64}' | base64 -d > ${launcherScript} && chmod +x ${launcherScript}`);
73
+ claudeCmd = `bash ${launcherScript}`;
74
+ } else {
75
+ claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;
76
+ }
77
+ const tmuxCmd = `tmux new-session -d -s ${agentId} -c /workspace '${claudeCmd}'`;
78
+ const result = await exe.ssh(vmName, tmuxCmd);
79
+ if (result.exitCode !== 0) {
80
+ state.status = "error";
81
+ saveRemoteAgentState(state);
82
+ throw new Error(`Failed to start agent: ${result.stderr}`);
83
+ }
84
+ state.status = "running";
85
+ saveRemoteAgentState(state);
86
+ return state;
87
+ }
88
+ async function getRemoteAgentOutput(agentId, vmName, lines = 100) {
89
+ const exe = createExeProvider();
90
+ const result = await exe.ssh(vmName, `tmux capture-pane -t ${agentId} -p -S -${lines}`);
91
+ return result.stdout;
92
+ }
93
+ async function sendToRemoteAgent(agentId, vmName, message) {
94
+ const exe = createExeProvider();
95
+ const escapedMessage = message.replace(/'/g, "'\\''");
96
+ await exe.ssh(vmName, `tmux send-keys -t ${agentId} '${escapedMessage}'`);
97
+ await exe.ssh(vmName, `tmux send-keys -t ${agentId} C-m`);
98
+ }
99
+ async function isRemoteAgentRunning(agentId, vmName) {
100
+ const exe = createExeProvider();
101
+ return remoteSessionExists(exe, vmName, agentId);
102
+ }
103
+ async function killRemoteAgent(agentId, vmName) {
104
+ const exe = createExeProvider();
105
+ await exe.ssh(vmName, `tmux kill-session -t ${agentId} 2>/dev/null || true`);
106
+ const state = loadRemoteAgentState(agentId);
107
+ if (state) {
108
+ state.status = "stopped";
109
+ saveRemoteAgentState(state);
110
+ }
111
+ }
112
+ async function listRemoteAgents(vmName) {
113
+ const exe = createExeProvider();
114
+ const result = await exe.ssh(vmName, `tmux list-sessions -F "#{session_name}" 2>/dev/null | grep "^agent-" || true`);
115
+ if (!result.stdout.trim()) {
116
+ return [];
117
+ }
118
+ return result.stdout.trim().split("\n").filter(Boolean);
119
+ }
120
+ async function pollRemoteAgentStatus(agentId, vmName) {
121
+ const exe = createExeProvider();
122
+ const isRunning = await remoteSessionExists(exe, vmName, agentId);
123
+ if (!isRunning) {
124
+ return { isRunning: false, lastOutput: "", toolUses: [] };
125
+ }
126
+ const output = await getRemoteAgentOutput(agentId, vmName, 50);
127
+ const toolUses = [];
128
+ const toolPattern = /(?:Using|Calling|Running)\s+(\w+)\s+tool/gi;
129
+ let match;
130
+ while ((match = toolPattern.exec(output)) !== null) {
131
+ toolUses.push(match[1]);
132
+ }
133
+ return {
134
+ isRunning,
135
+ lastOutput: output,
136
+ toolUses
137
+ };
138
+ }
139
+ var AGENTS_DIR;
140
+ var init_remote_agents = __esm({
141
+ "src/lib/remote/remote-agents.ts"() {
142
+ init_esm_shims();
143
+ init_exe_provider();
144
+ AGENTS_DIR = join(homedir(), ".panopticon", "agents");
145
+ }
146
+ });
147
+
148
+ export {
149
+ loadRemoteAgentState,
150
+ spawnRemoteAgent,
151
+ getRemoteAgentOutput,
152
+ sendToRemoteAgent,
153
+ isRemoteAgentRunning,
154
+ killRemoteAgent,
155
+ listRemoteAgents,
156
+ pollRemoteAgentStatus,
157
+ init_remote_agents
158
+ };
159
+ //# sourceMappingURL=chunk-HCTJFIJJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/remote/remote-agents.ts"],"sourcesContent":["/**\n * Remote Agent Management\n *\n * Spawn and manage Claude agents on remote exe.dev VMs.\n * Agents run in tmux sessions for persistence and monitoring.\n */\n\nimport { ExeProvider, createExeProvider } from './exe-provider.js';\nimport type { RemoteWorkspaceMetadata } from './interface.js';\nimport { join } from 'path';\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';\nimport { homedir } from 'os';\n\nconst AGENTS_DIR = join(homedir(), '.panopticon', 'agents');\n\nexport interface RemoteAgentState {\n id: string;\n issueId: string;\n vmName: string;\n model: string;\n status: 'starting' | 'running' | 'stopped' | 'error';\n startedAt: string;\n lastActivity?: string;\n location: 'remote';\n}\n\n/**\n * Get agent state file path\n */\nfunction getRemoteAgentStateFile(agentId: string): string {\n return join(AGENTS_DIR, agentId, 'remote-state.json');\n}\n\n/**\n * Save remote agent state\n */\nfunction saveRemoteAgentState(state: RemoteAgentState): void {\n const dir = join(AGENTS_DIR, state.id);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n writeFileSync(getRemoteAgentStateFile(state.id), JSON.stringify(state, null, 2));\n}\n\n/**\n * Load remote agent state\n */\nexport function loadRemoteAgentState(agentId: string): RemoteAgentState | null {\n const file = getRemoteAgentStateFile(agentId);\n if (!existsSync(file)) return null;\n\n try {\n return JSON.parse(readFileSync(file, 'utf-8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Check if remote agent session exists\n */\nasync function remoteSessionExists(\n exe: ExeProvider,\n vmName: string,\n sessionName: string\n): Promise<boolean> {\n const result = await exe.ssh(vmName, `tmux has-session -t ${sessionName} 2>/dev/null && echo exists || echo not-found`);\n return result.stdout.trim() === 'exists';\n}\n\nexport interface SpawnRemoteAgentOptions {\n issueId: string;\n workspace: RemoteWorkspaceMetadata;\n model?: string;\n prompt?: string;\n phase?: string;\n}\n\n/**\n * Spawn a Claude agent on a remote VM\n */\nexport async function spawnRemoteAgent(options: SpawnRemoteAgentOptions): Promise<RemoteAgentState> {\n const { issueId, workspace, model = 'claude-sonnet-4-5', prompt } = options;\n\n const agentId = `agent-${issueId.toLowerCase()}`;\n const vmName = workspace.vmName;\n\n const exe = createExeProvider({ infraVm: workspace.infraVm });\n\n // Check if VM is running\n const vmStatus = await exe.getStatus(vmName);\n if (vmStatus !== 'running') {\n throw new Error(`VM ${vmName} is not running. Start it with: pan workspace start ${issueId}`);\n }\n\n // Check if agent already exists\n if (await remoteSessionExists(exe, vmName, agentId)) {\n throw new Error(`Agent ${agentId} already running on ${vmName}. Use 'pan work tell' to message it.`);\n }\n\n // Create agent state\n const state: RemoteAgentState = {\n id: agentId,\n issueId,\n vmName,\n model,\n status: 'starting',\n startedAt: new Date().toISOString(),\n location: 'remote',\n };\n\n saveRemoteAgentState(state);\n\n // Write prompt to file on remote VM if provided\n let claudeCmd: string;\n\n if (prompt) {\n // Write prompt to file on VM using base64 to avoid escaping issues\n const promptFile = `/workspace/.panopticon/prompts/${agentId}.md`;\n await exe.ssh(vmName, `mkdir -p /workspace/.panopticon/prompts`);\n const promptBase64 = Buffer.from(prompt).toString('base64');\n await exe.ssh(vmName, `echo '${promptBase64}' | base64 -d > ${promptFile}`);\n\n // Create launcher script using base64 to avoid shell interpretation\n const launcherScript = `/workspace/.panopticon/prompts/${agentId}-launcher.sh`;\n const launcherContent = `#!/bin/bash\nexport PATH=\"/usr/local/bin:\\$PATH\"\nprompt=\\$(cat \"${promptFile}\")\nexec claude --dangerously-skip-permissions --model ${model} \"\\$prompt\"\n`;\n const launcherBase64 = Buffer.from(launcherContent).toString('base64');\n await exe.ssh(vmName, `echo '${launcherBase64}' | base64 -d > ${launcherScript} && chmod +x ${launcherScript}`);\n\n claudeCmd = `bash ${launcherScript}`;\n } else {\n claudeCmd = `claude --dangerously-skip-permissions --model ${model}`;\n }\n\n // Create tmux session on remote VM\n const tmuxCmd = `tmux new-session -d -s ${agentId} -c /workspace '${claudeCmd}'`;\n const result = await exe.ssh(vmName, tmuxCmd);\n\n if (result.exitCode !== 0) {\n state.status = 'error';\n saveRemoteAgentState(state);\n throw new Error(`Failed to start agent: ${result.stderr}`);\n }\n\n // Update status\n state.status = 'running';\n saveRemoteAgentState(state);\n\n return state;\n}\n\n/**\n * Get remote agent output from tmux session\n */\nexport async function getRemoteAgentOutput(\n agentId: string,\n vmName: string,\n lines: number = 100\n): Promise<string> {\n const exe = createExeProvider();\n\n const result = await exe.ssh(vmName, `tmux capture-pane -t ${agentId} -p -S -${lines}`);\n return result.stdout;\n}\n\n/**\n * Send message to remote agent\n */\nexport async function sendToRemoteAgent(\n agentId: string,\n vmName: string,\n message: string\n): Promise<void> {\n const exe = createExeProvider();\n\n // Escape message for shell\n const escapedMessage = message.replace(/'/g, \"'\\\\''\");\n\n // Send keys to tmux session (send message then Enter)\n await exe.ssh(vmName, `tmux send-keys -t ${agentId} '${escapedMessage}'`);\n await exe.ssh(vmName, `tmux send-keys -t ${agentId} C-m`);\n}\n\n/**\n * Check if remote agent is still running\n */\nexport async function isRemoteAgentRunning(\n agentId: string,\n vmName: string\n): Promise<boolean> {\n const exe = createExeProvider();\n return remoteSessionExists(exe, vmName, agentId);\n}\n\n/**\n * Kill remote agent session\n */\nexport async function killRemoteAgent(\n agentId: string,\n vmName: string\n): Promise<void> {\n const exe = createExeProvider();\n await exe.ssh(vmName, `tmux kill-session -t ${agentId} 2>/dev/null || true`);\n\n // Update state\n const state = loadRemoteAgentState(agentId);\n if (state) {\n state.status = 'stopped';\n saveRemoteAgentState(state);\n }\n}\n\n/**\n * Get list of running remote agents on a VM\n */\nexport async function listRemoteAgents(vmName: string): Promise<string[]> {\n const exe = createExeProvider();\n\n const result = await exe.ssh(vmName, `tmux list-sessions -F \"#{session_name}\" 2>/dev/null | grep \"^agent-\" || true`);\n if (!result.stdout.trim()) {\n return [];\n }\n\n return result.stdout.trim().split('\\n').filter(Boolean);\n}\n\n/**\n * Poll remote agent for status updates\n * Returns parsed events from the agent output\n */\nexport async function pollRemoteAgentStatus(\n agentId: string,\n vmName: string\n): Promise<{\n isRunning: boolean;\n lastOutput: string;\n toolUses: string[];\n}> {\n const exe = createExeProvider();\n\n // Check if session exists\n const isRunning = await remoteSessionExists(exe, vmName, agentId);\n\n if (!isRunning) {\n return { isRunning: false, lastOutput: '', toolUses: [] };\n }\n\n // Get recent output\n const output = await getRemoteAgentOutput(agentId, vmName, 50);\n\n // Parse tool uses from output (simple pattern matching)\n const toolUses: string[] = [];\n const toolPattern = /(?:Using|Calling|Running)\\s+(\\w+)\\s+tool/gi;\n let match;\n while ((match = toolPattern.exec(output)) !== null) {\n toolUses.push(match[1]);\n }\n\n return {\n isRunning,\n lastOutput: output,\n toolUses,\n };\n}\n"],"mappings":"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","names":[]}