copyrun 0.1.0 → 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.
package/dist/live.js CHANGED
@@ -118,14 +118,14 @@ function parseManaged(v) {
118
118
  if (!SLUG.test(s)) problems.push("org must be an org slug");
119
119
  else config.org = s;
120
120
  }
121
- for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableAutopilot", "copyLink"]) bool(k);
121
+ for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableReceipts", "disableAutopilot", "copyLink", "disableAsk", "disableMission", "disableCopy"]) bool(k);
122
122
  if (o.autopilotOrg !== void 0) {
123
123
  const s = typeof o.autopilotOrg === "string" ? o.autopilotOrg.trim().toLowerCase() : "";
124
124
  if (!SLUG.test(s)) problems.push("autopilotOrg must be an org slug");
125
125
  else config.autopilotOrg = s;
126
126
  }
127
127
  if (o.autopilotContent !== void 0) {
128
- if (o.autopilotContent !== "full" && o.autopilotContent !== "actions") problems.push('autopilotContent must be "full" or "actions"');
128
+ if (o.autopilotContent !== "full" && o.autopilotContent !== "actions" && o.autopilotContent !== "commands") problems.push('autopilotContent must be "full", "actions" or "commands"');
129
129
  else config.autopilotContent = o.autopilotContent;
130
130
  }
131
131
  if (o.allowedCloneSources !== void 0) {
@@ -158,6 +158,20 @@ function parseManaged(v) {
158
158
  if (o.restrictMode !== "block-all-uploads") problems.push('restrictMode must be "block-all-uploads"');
159
159
  else config.restrictMode = o.restrictMode;
160
160
  }
161
+ if (o.askBefore !== void 0) {
162
+ if (!Array.isArray(o.askBefore) || !o.askBefore.every((t) => typeof t === "string")) problems.push("askBefore must be a list of change areas and path patterns");
163
+ else config.askBefore = [...new Set(o.askBefore.map((t) => t.trim()).filter((t) => t && t.length <= 200))].slice(0, 50);
164
+ }
165
+ if (o.askTimeoutMinutes !== void 0) {
166
+ const n = o.askTimeoutMinutes;
167
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 1 || n > 15) problems.push("askTimeoutMinutes must be a number from 1 to 15");
168
+ else config.askTimeoutMinutes = Math.round(n);
169
+ }
170
+ if (o.askOrg !== void 0) {
171
+ const s = typeof o.askOrg === "string" ? o.askOrg.trim().toLowerCase() : "";
172
+ if (!SLUG.test(s)) problems.push("askOrg must be an org slug");
173
+ else config.askOrg = s;
174
+ }
161
175
  if (o.allowCloneYesWith !== void 0) {
162
176
  const l = o.allowCloneYesWith;
163
177
  if (!Array.isArray(l) || !l.every((x) => GATED_CAPABILITIES.includes(x))) problems.push('allowCloneYesWith must list only "shell", "install", "network" and/or "settings"');
@@ -217,6 +231,7 @@ function loadManaged(deps = {}) {
217
231
  }
218
232
  if (state.config.allowedCloneSources === "org-playbook" && !state.config.org) state.errors.push('allowedCloneSources "org-playbook" needs org');
219
233
  if (state.config.autopilotOrg && state.config.org && state.config.autopilotOrg !== state.config.org) state.errors.push("autopilotOrg must be the same org as org");
234
+ if (state.config.askOrg && state.config.org && state.config.askOrg !== state.config.org) state.errors.push("askOrg must be the same org as org");
220
235
  return state;
221
236
  }
222
237
  var cached;
@@ -0,0 +1,541 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/mission-watch.ts
4
+ import { execFile as execFile2 } from "child_process";
5
+ import { readFileSync as readFileSync3, readlinkSync, unlinkSync, writeFileSync } from "fs";
6
+ import { hostname } from "os";
7
+ import { basename, join as join4 } from "path";
8
+ import { promisify as promisify2 } from "util";
9
+
10
+ // src/config.ts
11
+ import { randomUUID } from "crypto";
12
+ import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
13
+ import { homedir } from "os";
14
+ import { join as join2 } from "path";
15
+
16
+ // src/managed.ts
17
+ import { execFileSync } from "child_process";
18
+ import { existsSync, readFileSync } from "fs";
19
+ import { join } from "path";
20
+ var GATED_CAPABILITIES = ["shell", "install", "network", "settings"];
21
+ var SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
22
+ var HANDLE = /^[a-z0-9][a-z0-9-]{0,38}$/;
23
+ function parseManaged(v) {
24
+ const problems = [];
25
+ const config = {};
26
+ if (!v || typeof v !== "object" || Array.isArray(v)) return { config, problems: ["not a JSON object"] };
27
+ const o = v;
28
+ const bool = (k) => {
29
+ if (o[k] === void 0) return;
30
+ if (typeof o[k] !== "boolean") problems.push(`${k} must be true or false`);
31
+ else config[k] = o[k];
32
+ };
33
+ if (o.apiHost !== void 0) {
34
+ try {
35
+ const u = new URL(String(o.apiHost));
36
+ const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
37
+ if (u.protocol !== "https:" && !(local && u.protocol === "http:")) problems.push("apiHost must be an https URL");
38
+ else config.apiHost = u.origin;
39
+ } catch {
40
+ problems.push("apiHost must be a URL");
41
+ }
42
+ }
43
+ if (o.org !== void 0) {
44
+ const s = typeof o.org === "string" ? o.org.trim().toLowerCase() : "";
45
+ if (!SLUG.test(s)) problems.push("org must be an org slug");
46
+ else config.org = s;
47
+ }
48
+ for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableReceipts", "disableAutopilot", "copyLink", "disableAsk", "disableMission", "disableCopy"]) bool(k);
49
+ if (o.autopilotOrg !== void 0) {
50
+ const s = typeof o.autopilotOrg === "string" ? o.autopilotOrg.trim().toLowerCase() : "";
51
+ if (!SLUG.test(s)) problems.push("autopilotOrg must be an org slug");
52
+ else config.autopilotOrg = s;
53
+ }
54
+ if (o.autopilotContent !== void 0) {
55
+ if (o.autopilotContent !== "full" && o.autopilotContent !== "actions" && o.autopilotContent !== "commands") problems.push('autopilotContent must be "full", "actions" or "commands"');
56
+ else config.autopilotContent = o.autopilotContent;
57
+ }
58
+ if (o.allowedCloneSources !== void 0) {
59
+ const a = o.allowedCloneSources;
60
+ if (a === "any" || a === "org-playbook") config.allowedCloneSources = a;
61
+ else if (Array.isArray(a) && a.every((x) => typeof x === "string" && HANDLE.test(x.trim().replace(/^@/, "").toLowerCase())))
62
+ config.allowedCloneSources = [...new Set(a.map((x) => x.trim().replace(/^@/, "").toLowerCase()))];
63
+ else problems.push('allowedCloneSources must be "any", "org-playbook" or a list of handles and org slugs');
64
+ }
65
+ if (o.extraRedactTerms !== void 0) {
66
+ if (!Array.isArray(o.extraRedactTerms) || !o.extraRedactTerms.every((t) => typeof t === "string")) problems.push("extraRedactTerms must be a list of strings");
67
+ else config.extraRedactTerms = [...new Set(o.extraRedactTerms.map((t) => t.trim()).filter((t) => t.length >= 2 && t.length <= 100))].slice(0, 200);
68
+ }
69
+ if (o.profile !== void 0) {
70
+ if (o.profile !== "finance" && o.profile !== "hipaa") problems.push('profile must be "finance" or "hipaa"');
71
+ else config.profile = o.profile;
72
+ }
73
+ if (o.archive !== void 0) {
74
+ const a = o.archive;
75
+ const org = a && typeof a.org === "string" ? a.org.trim().toLowerCase() : "";
76
+ if (!a || typeof a !== "object" || !SLUG.test(org) || a.required !== void 0 && typeof a.required !== "boolean") problems.push("archive must be { org: <slug>, required: true|false }");
77
+ else config.archive = { org, required: a.required === true };
78
+ }
79
+ if (o.restrictOrgs !== void 0) {
80
+ const list = typeof o.restrictOrgs === "string" ? o.restrictOrgs.split(",") : o.restrictOrgs;
81
+ if (!Array.isArray(list) || !list.every((x) => typeof x === "string" && SLUG.test(x.trim().toLowerCase()))) problems.push("restrictOrgs must be a list of org slugs");
82
+ else config.restrictOrgs = [...new Set(list.map((x) => x.trim().toLowerCase()))];
83
+ }
84
+ if (o.restrictMode !== void 0) {
85
+ if (o.restrictMode !== "block-all-uploads") problems.push('restrictMode must be "block-all-uploads"');
86
+ else config.restrictMode = o.restrictMode;
87
+ }
88
+ if (o.askBefore !== void 0) {
89
+ if (!Array.isArray(o.askBefore) || !o.askBefore.every((t) => typeof t === "string")) problems.push("askBefore must be a list of change areas and path patterns");
90
+ else config.askBefore = [...new Set(o.askBefore.map((t) => t.trim()).filter((t) => t && t.length <= 200))].slice(0, 50);
91
+ }
92
+ if (o.askTimeoutMinutes !== void 0) {
93
+ const n = o.askTimeoutMinutes;
94
+ if (typeof n !== "number" || !Number.isFinite(n) || n < 1 || n > 15) problems.push("askTimeoutMinutes must be a number from 1 to 15");
95
+ else config.askTimeoutMinutes = Math.round(n);
96
+ }
97
+ if (o.askOrg !== void 0) {
98
+ const s = typeof o.askOrg === "string" ? o.askOrg.trim().toLowerCase() : "";
99
+ if (!SLUG.test(s)) problems.push("askOrg must be an org slug");
100
+ else config.askOrg = s;
101
+ }
102
+ if (o.allowCloneYesWith !== void 0) {
103
+ const l = o.allowCloneYesWith;
104
+ if (!Array.isArray(l) || !l.every((x) => GATED_CAPABILITIES.includes(x))) problems.push('allowCloneYesWith must list only "shell", "install", "network" and/or "settings"');
105
+ else config.allowCloneYesWith = [...new Set(l)];
106
+ }
107
+ return { config, problems };
108
+ }
109
+ function systemManagedPaths(platform = process.platform, env = process.env) {
110
+ if (platform === "darwin")
111
+ return [
112
+ { path: "/Library/Application Support/copyrun/managed.json", kind: "json" },
113
+ // a configuration profile (Jamf, Kandji, Intune for Mac) lands here; it wins over the file
114
+ { path: "/Library/Managed Preferences/ai.copyrun.cli.plist", kind: "plist" }
115
+ ];
116
+ if (platform === "win32") return [{ path: join(env.ProgramData || env.PROGRAMDATA || "C:\\ProgramData", "copyrun", "managed.json"), kind: "json" }];
117
+ return [{ path: "/etc/copyrun/managed.json", kind: "json" }];
118
+ }
119
+ function plutil(p) {
120
+ try {
121
+ return execFileSync("/usr/bin/plutil", ["-convert", "json", "-o", "-", p], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
122
+ } catch {
123
+ return void 0;
124
+ }
125
+ }
126
+ function loadManaged(deps = {}) {
127
+ const env = deps.env ?? process.env;
128
+ const exists = deps.exists ?? existsSync;
129
+ const read = deps.read ?? ((p) => readFileSync(p, "utf8"));
130
+ const plist = deps.plistToJson ?? plutil;
131
+ const system = systemManagedPaths(deps.platform ?? process.platform, env).filter((f) => exists(f.path));
132
+ const files = system.length ? system : env.COPYRUN_MANAGED_CONFIG ? [{ path: env.COPYRUN_MANAGED_CONFIG, kind: "json" }] : [];
133
+ const state = { config: {}, sources: [], errors: [] };
134
+ for (const f of files) {
135
+ let text;
136
+ try {
137
+ text = f.kind === "plist" ? plist(f.path) : read(f.path);
138
+ } catch (e) {
139
+ state.errors.push(`${f.path}: can't be read (${e.message})`);
140
+ continue;
141
+ }
142
+ if (text === void 0) {
143
+ if (f.kind === "plist" && files.some((x) => x.kind === "json")) continue;
144
+ state.errors.push(`${f.path}: can't be read (plutil unavailable)`);
145
+ continue;
146
+ }
147
+ let json;
148
+ try {
149
+ json = JSON.parse(text);
150
+ } catch {
151
+ state.errors.push(`${f.path}: not valid JSON`);
152
+ continue;
153
+ }
154
+ const { config, problems } = parseManaged(json);
155
+ if (problems.length) state.errors.push(`${f.path}: ${problems.join("; ")}`);
156
+ state.config = { ...state.config, ...config };
157
+ state.sources.push(f.path);
158
+ }
159
+ if (state.config.allowedCloneSources === "org-playbook" && !state.config.org) state.errors.push('allowedCloneSources "org-playbook" needs org');
160
+ if (state.config.autopilotOrg && state.config.org && state.config.autopilotOrg !== state.config.org) state.errors.push("autopilotOrg must be the same org as org");
161
+ if (state.config.askOrg && state.config.org && state.config.askOrg !== state.config.org) state.errors.push("askOrg must be the same org as org");
162
+ return state;
163
+ }
164
+ var cached;
165
+ function managed() {
166
+ return cached ??= loadManaged();
167
+ }
168
+
169
+ // src/config.ts
170
+ var VERSION = "0.2.0";
171
+ var DIR = process.env.COPYRUN_HOME ?? join2(homedir(), ".copyrun");
172
+ function isLoopbackHost(hostname2) {
173
+ const h = hostname2.toLowerCase();
174
+ return h === "localhost" || h.endsWith(".localhost") || h === "127.0.0.1" || /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h) || h === "[::1]" || h === "::1";
175
+ }
176
+ function apiProblem(url) {
177
+ let u;
178
+ try {
179
+ u = new URL(url);
180
+ } catch {
181
+ return `"${url}" isn't a URL (--api / COPYRUN_API).`;
182
+ }
183
+ if (u.protocol === "https:" || u.protocol === "http:" && isLoopbackHost(u.hostname)) return void 0;
184
+ return `copyrun won't talk to ${u.origin}: the server must be https (plain http only for localhost). Your device token and runs would travel unencrypted.`;
185
+ }
186
+ function apiBase(flag) {
187
+ const pinned = managed().config.apiHost;
188
+ if (pinned) return pinned;
189
+ const base = (flag ?? process.env.COPYRUN_API ?? "https://copyrun.ai").replace(/\/$/, "");
190
+ const problem = apiProblem(base);
191
+ if (problem) throw new Error(problem);
192
+ return base;
193
+ }
194
+
195
+ // src/live-logic.ts
196
+ import { createHash } from "crypto";
197
+ function gateHeaders(env) {
198
+ const h = {};
199
+ const prelaunch = env.COPYRUN_PRELAUNCH_KEY?.trim();
200
+ if (prelaunch) h["x-copyrun-prelaunch"] = prelaunch;
201
+ const beta = env.COPYRUN_BETA?.trim();
202
+ if (beta && beta.includes(":")) h["x-copyrun-beta"] = beta;
203
+ return h;
204
+ }
205
+
206
+ // src/local-project.ts
207
+ import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
208
+ import { dirname, isAbsolute, join as join3, resolve } from "path";
209
+ var gitRootOf = (cwd) => {
210
+ try {
211
+ return existsSync2(cwd) ? gitRoot(cwd)?.root : void 0;
212
+ } catch {
213
+ return void 0;
214
+ }
215
+ };
216
+ function gitRoot(cwd) {
217
+ let dir = resolve(cwd);
218
+ for (let i = 0; i < 40; i++) {
219
+ const g = join3(dir, ".git");
220
+ try {
221
+ const st = statSync(g);
222
+ if (st.isDirectory()) return { root: dir, gitDir: g };
223
+ if (st.isFile()) {
224
+ const m = readFileSync2(g, "utf8").match(/^gitdir:\s*(.+)$/m);
225
+ if (m) {
226
+ const wt = isAbsolute(m[1].trim()) ? m[1].trim() : resolve(dir, m[1].trim());
227
+ let common = wt;
228
+ try {
229
+ const c = readFileSync2(join3(wt, "commondir"), "utf8").trim();
230
+ common = isAbsolute(c) ? c : resolve(wt, c);
231
+ } catch {
232
+ }
233
+ return { root: dir, gitDir: common };
234
+ }
235
+ }
236
+ } catch {
237
+ }
238
+ const up = dirname(dir);
239
+ if (up === dir) return void 0;
240
+ dir = up;
241
+ }
242
+ return void 0;
243
+ }
244
+
245
+ // src/mission-logic.ts
246
+ import { createHash as createHash2 } from "crypto";
247
+ var BEAT_TIMEOUT_MS = 1500;
248
+ var TOKENS_MAX_BYTES = 64 * 1024 * 1024;
249
+ var WATCHED = {
250
+ gemini: { agent: "gemini" },
251
+ opencode: { agent: "opencode" },
252
+ openclaw: { agent: "openclaw" },
253
+ copilot: { agent: "copilot" },
254
+ aider: { agent: "aider" },
255
+ goose: { agent: "goose" },
256
+ amp: { agent: "amp" },
257
+ crush: { agent: "agent", label: "Crush" },
258
+ qwen: { agent: "agent", label: "Qwen Code" },
259
+ kimi: { agent: "agent", label: "Kimi CLI" },
260
+ droid: { agent: "agent", label: "Factory Droid" },
261
+ "kiro-cli": { agent: "agent", label: "Kiro CLI" }
262
+ };
263
+ var WATCH_SETTLE_MS = 1e4;
264
+ var WATCH_TICK_MS = 5e3;
265
+ var WATCH_ALIVE_MS = 2e4;
266
+ var BUSY_CPU = 1;
267
+ var RUNTIMES = /^(node|nodejs|bun|deno|python[\d.]*|pypy[\d.]*|ruby)$/;
268
+ var basenameOf = (p) => p.replace(/^["']|["']$/g, "").split(/[\\/]/).pop() ?? "";
269
+ var GENERIC_SCRIPT = /^(index|main|cli|bin|app|start|run|entry|dist)$/i;
270
+ var PACKAGE_NAMES = {
271
+ "@google/gemini-cli": "gemini",
272
+ "opencode-ai": "opencode",
273
+ openclaw: "openclaw",
274
+ "@github/copilot": "copilot",
275
+ "@sourcegraph/amp": "amp",
276
+ "@qwen-code/qwen-code": "qwen"
277
+ };
278
+ function programName(command) {
279
+ const parts = command.trim().match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
280
+ let name = basenameOf(parts[0] ?? "");
281
+ if (RUNTIMES.test(name.replace(/\.exe$/i, ""))) {
282
+ const script = parts.slice(1).find((a) => !a.startsWith("-"));
283
+ if (script) {
284
+ name = basenameOf(script);
285
+ if (GENERIC_SCRIPT.test(name.replace(/\.(m?js|cjs|ts)$/i, ""))) {
286
+ const m = /node_modules[\\/]((?:@[^\\/"']+[\\/])?[^\\/"']+)/i.exec(script);
287
+ const pkg = m?.[1]?.replace(/\\/g, "/").toLowerCase();
288
+ if (pkg && PACKAGE_NAMES[pkg]) name = PACKAGE_NAMES[pkg];
289
+ }
290
+ }
291
+ }
292
+ return name.replace(/\.(m?js|cjs|ts|py|exe)$/i, "").toLowerCase();
293
+ }
294
+ var WINDOWS_PS_ARGS = ["-NoProfile", "-NonInteractive", "-Command", "Get-CimInstance Win32_Process | Select-Object ProcessId,ParentProcessId,Name,CommandLine,KernelModeTime,UserModeTime | ConvertTo-Json -Compress"];
295
+ function parseWindowsProcs(json) {
296
+ let rows;
297
+ try {
298
+ rows = JSON.parse(json);
299
+ } catch {
300
+ return [];
301
+ }
302
+ const list = Array.isArray(rows) ? rows : rows && typeof rows === "object" ? [rows] : [];
303
+ const out = [];
304
+ for (const r of list) {
305
+ const pid = Number(r.ProcessId);
306
+ if (!Number.isInteger(pid) || pid <= 0) continue;
307
+ const line = typeof r.CommandLine === "string" && r.CommandLine.trim() ? r.CommandLine : typeof r.Name === "string" ? r.Name : "";
308
+ const t = (Number(r.KernelModeTime) || 0) + (Number(r.UserModeTime) || 0);
309
+ out.push({ pid, ppid: Number(r.ParentProcessId) || 0, cpu: 0, cpuTime: t / 1e7, name: programName(line) });
310
+ }
311
+ return out;
312
+ }
313
+ function cpuFromTimes(procs, prev, now) {
314
+ const next = /* @__PURE__ */ new Map();
315
+ const out = [];
316
+ for (const p of procs) {
317
+ const was = prev.get(p.pid);
318
+ const dt = was ? (now - was.at) / 1e3 : 0;
319
+ const cpu = was && dt > 0 && p.cpuTime >= was.cpuTime ? Math.min(100 * (p.cpuTime - was.cpuTime) / dt, 100 * 64) : 0;
320
+ next.set(p.pid, { cpuTime: p.cpuTime, at: now });
321
+ out.push({ pid: p.pid, ppid: p.ppid, cpu, name: p.name });
322
+ }
323
+ prev.clear();
324
+ for (const [k, v] of next) prev.set(k, v);
325
+ return out;
326
+ }
327
+ function parsePs(out) {
328
+ const procs = [];
329
+ for (const line of out.split("\n")) {
330
+ const m = /^\s*(\d+)\s+(\d+)\s+([\d.,]+)\s+(.+)$/.exec(line);
331
+ if (!m) continue;
332
+ procs.push({ pid: Number(m[1]), ppid: Number(m[2]), cpu: Number(m[3].replace(",", ".")) || 0, name: programName(m[4]) });
333
+ }
334
+ return procs;
335
+ }
336
+ function agentProcs(procs, selfPid) {
337
+ const byPid = new Map(procs.map((p) => [p.pid, p]));
338
+ const kids = /* @__PURE__ */ new Map();
339
+ for (const p of procs) kids.set(p.ppid, [...kids.get(p.ppid) ?? [], p]);
340
+ const treeCpu = (p, depth = 0) => p.cpu + (depth > 8 ? 0 : (kids.get(p.pid) ?? []).reduce((a, k) => a + treeCpu(k, depth + 1), 0));
341
+ const out = [];
342
+ for (const p of procs) {
343
+ const w = WATCHED[p.name];
344
+ if (!w || p.pid === selfPid) continue;
345
+ let up = byPid.get(p.ppid);
346
+ let nested = false;
347
+ for (let i = 0; up && i < 32; i++, up = byPid.get(up.ppid)) {
348
+ if (up.name === p.name) {
349
+ nested = true;
350
+ break;
351
+ }
352
+ }
353
+ if (!nested) out.push({ ...p, ...w, cpuTree: treeCpu(p) });
354
+ }
355
+ return out;
356
+ }
357
+ var watchKey = (host, pid, name) => createHash2("sha256").update(`copyrun-mission-watch:${host}:${pid}:${name}`).digest("hex").slice(0, 32);
358
+ function parseLsofCwd(out) {
359
+ const m = /* @__PURE__ */ new Map();
360
+ let pid = 0;
361
+ for (const line of out.split("\n")) {
362
+ if (line.startsWith("p")) pid = Number(line.slice(1));
363
+ else if (line.startsWith("n") && pid) m.set(pid, line.slice(1));
364
+ }
365
+ return m;
366
+ }
367
+
368
+ // src/procs.ts
369
+ import { execFile } from "child_process";
370
+ import { promisify } from "util";
371
+ var run = promisify(execFile);
372
+ var times = /* @__PURE__ */ new Map();
373
+ async function listProcs() {
374
+ if (process.platform === "win32") {
375
+ const { stdout: stdout2 } = await run("powershell", WINDOWS_PS_ARGS, { timeout: 15e3, maxBuffer: 64 << 20, windowsHide: true });
376
+ return cpuFromTimes(parseWindowsProcs(stdout2), times, Date.now());
377
+ }
378
+ const { stdout } = await run("ps", ["-axww", "-o", "pid=,ppid=,pcpu=,command="], { timeout: 4e3, maxBuffer: 16 << 20 });
379
+ return parsePs(stdout);
380
+ }
381
+ async function watchedAgents() {
382
+ return agentProcs(await listProcs(), process.pid);
383
+ }
384
+ var alive = (pid) => {
385
+ try {
386
+ process.kill(pid, 0);
387
+ return true;
388
+ } catch {
389
+ return false;
390
+ }
391
+ };
392
+ function endProcess(pid) {
393
+ if (process.platform === "win32") {
394
+ run("taskkill", ["/PID", String(pid), "/T"], { windowsHide: true }).catch(() => void 0);
395
+ setTimeout(() => run("taskkill", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }).catch(() => void 0), 4e3).unref();
396
+ return;
397
+ }
398
+ try {
399
+ process.kill(pid, "SIGINT");
400
+ } catch {
401
+ }
402
+ setTimeout(() => {
403
+ if (alive(pid))
404
+ try {
405
+ process.kill(pid, "SIGTERM");
406
+ } catch {
407
+ }
408
+ }, 4e3).unref();
409
+ }
410
+
411
+ // src/mission-watch.ts
412
+ var run2 = promisify2(execFile2);
413
+ var PID_FILE = join4(DIR, "mission-watch.pid");
414
+ var tracked = /* @__PURE__ */ new Map();
415
+ function readJson(path) {
416
+ try {
417
+ return JSON.parse(readFileSync3(path, "utf8"));
418
+ } catch {
419
+ return void 0;
420
+ }
421
+ }
422
+ function alive2(pid) {
423
+ try {
424
+ process.kill(pid, 0);
425
+ return true;
426
+ } catch {
427
+ return false;
428
+ }
429
+ }
430
+ function claimPidFile() {
431
+ const other = Number(String(readJson(PID_FILE) ?? ""));
432
+ if (other && other !== process.pid && alive2(other)) return false;
433
+ try {
434
+ writeFileSync(PID_FILE, String(process.pid), { mode: 384 });
435
+ } catch {
436
+ }
437
+ return true;
438
+ }
439
+ async function folders(pids) {
440
+ if (!pids.length || process.platform === "win32") return /* @__PURE__ */ new Map();
441
+ if (process.platform === "linux") {
442
+ const m = /* @__PURE__ */ new Map();
443
+ for (const p of pids) {
444
+ try {
445
+ m.set(p, readlinkSync(`/proc/${p}/cwd`));
446
+ } catch {
447
+ }
448
+ }
449
+ return m;
450
+ }
451
+ try {
452
+ const { stdout } = await run2("lsof", ["-a", "-d", "cwd", "-Fn", "-p", pids.join(",")], { timeout: 4e3, maxBuffer: 1 << 20 });
453
+ return parseLsofCwd(stdout);
454
+ } catch (e) {
455
+ const out = e.stdout;
456
+ return out ? parseLsofCwd(out) : /* @__PURE__ */ new Map();
457
+ }
458
+ }
459
+ function credentials() {
460
+ if (managed().config.disableMission) return void 0;
461
+ const cfg = readJson(join4(DIR, "config.json")) ?? {};
462
+ if (cfg.mission !== true) return void 0;
463
+ const api = apiBase();
464
+ const token = cfg.device && cfg.device.api === api && typeof cfg.device.token === "string" ? cfg.device.token : void 0;
465
+ return token ? { api, token } : void 0;
466
+ }
467
+ async function send(c, beat) {
468
+ try {
469
+ const res = await fetch(`${c.api}/api/v1/me/mission/beat`, {
470
+ method: "POST",
471
+ headers: { ...gateHeaders(process.env), authorization: `Bearer ${c.token}`, "content-type": "application/json", "user-agent": `copyrun-cli/${VERSION} mission-watch` },
472
+ body: JSON.stringify(beat),
473
+ signal: AbortSignal.timeout(BEAT_TIMEOUT_MS * 3)
474
+ });
475
+ if (!res.ok) return {};
476
+ const body = await res.json().catch(() => ({}));
477
+ return { stop: body.stop === true };
478
+ } catch {
479
+ return {};
480
+ }
481
+ }
482
+ async function tick(host) {
483
+ const c = credentials();
484
+ if (!c) return;
485
+ const found = await watchedAgents();
486
+ const now = Date.now();
487
+ const seen = new Set(found.map((p) => p.pid));
488
+ const fresh = found.filter((p) => !tracked.has(p.pid));
489
+ const where = await folders(fresh.map((p) => p.pid));
490
+ for (const p of fresh) {
491
+ const cwd = where.get(p.pid);
492
+ const project = cwd ? basename(gitRootOf(cwd) ?? cwd) : void 0;
493
+ tracked.set(p.pid, { key: watchKey(host, p.pid, p.name), agent: p.agent, ...p.label ? { label: p.label } : {}, ...project && project !== "/" ? { project } : {}, firstSeen: now, started: false, lastBeat: 0 });
494
+ }
495
+ const base = (t) => ({ session: t.key, agent: t.agent, via: "watch", ...t.label ? { label: t.label } : {}, ...t.project ? { project: t.project } : {} });
496
+ for (const p of found) {
497
+ const t = tracked.get(p.pid);
498
+ if (t.stopping) continue;
499
+ if (now - t.firstSeen < WATCH_SETTLE_MS) continue;
500
+ const idle = p.cpuTree < BUSY_CPU;
501
+ if (!t.started) {
502
+ t.started = true;
503
+ t.lastBeat = now;
504
+ t.idle = idle;
505
+ await send(c, { ...base(t), event: "start" });
506
+ continue;
507
+ }
508
+ if (now - t.lastBeat < WATCH_ALIVE_MS && idle === t.idle) continue;
509
+ t.lastBeat = now;
510
+ t.idle = idle;
511
+ const r = await send(c, { ...base(t), event: "alive", ...idle ? { idle: true } : {} });
512
+ if (r.stop) {
513
+ t.stopping = now;
514
+ endProcess(p.pid);
515
+ }
516
+ }
517
+ for (const [pid, t] of tracked) {
518
+ if (seen.has(pid)) continue;
519
+ tracked.delete(pid);
520
+ if (t.started) await send(c, { ...base(t), event: "end" });
521
+ }
522
+ }
523
+ async function main() {
524
+ if (!claimPidFile()) return;
525
+ const host = hostname();
526
+ const cleanup = () => {
527
+ try {
528
+ if (String(readJson(PID_FILE)) === String(process.pid)) unlinkSync(PID_FILE);
529
+ } catch {
530
+ }
531
+ process.exit(0);
532
+ };
533
+ process.on("SIGINT", cleanup);
534
+ process.on("SIGTERM", cleanup);
535
+ for (; ; ) {
536
+ await tick(host).catch(() => {
537
+ });
538
+ await new Promise((r) => setTimeout(r, WATCH_TICK_MS));
539
+ }
540
+ }
541
+ main().catch(() => process.exit(0));