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/README.md +25 -2
- package/dist/archive-hook.js +0 -0
- package/dist/archive-worker.js +1384 -126
- package/dist/ask.js +3919 -0
- package/dist/autopilot-worker.js +11 -3
- package/dist/autopilot.js +1187 -111
- package/dist/check.js +1754 -110
- package/dist/index.js +24470 -16065
- package/dist/live.js +17 -2
- package/dist/mission-watch.js +541 -0
- package/dist/mission.js +472 -0
- package/dist/nudge.js +17 -2
- package/dist/receipt.js +349 -0
- package/dist/seal-worker.js +0 -0
- package/package.json +11 -10
package/dist/mission.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/mission-hook.ts
|
|
4
|
+
import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
5
|
+
import { basename, join as join4 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { join as join2 } from "path";
|
|
12
|
+
|
|
13
|
+
// src/managed.ts
|
|
14
|
+
import { execFileSync } from "child_process";
|
|
15
|
+
import { existsSync, readFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
var GATED_CAPABILITIES = ["shell", "install", "network", "settings"];
|
|
18
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
19
|
+
var HANDLE = /^[a-z0-9][a-z0-9-]{0,38}$/;
|
|
20
|
+
function parseManaged(v) {
|
|
21
|
+
const problems = [];
|
|
22
|
+
const config = {};
|
|
23
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return { config, problems: ["not a JSON object"] };
|
|
24
|
+
const o = v;
|
|
25
|
+
const bool = (k) => {
|
|
26
|
+
if (o[k] === void 0) return;
|
|
27
|
+
if (typeof o[k] !== "boolean") problems.push(`${k} must be true or false`);
|
|
28
|
+
else config[k] = o[k];
|
|
29
|
+
};
|
|
30
|
+
if (o.apiHost !== void 0) {
|
|
31
|
+
try {
|
|
32
|
+
const u = new URL(String(o.apiHost));
|
|
33
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
34
|
+
if (u.protocol !== "https:" && !(local && u.protocol === "http:")) problems.push("apiHost must be an https URL");
|
|
35
|
+
else config.apiHost = u.origin;
|
|
36
|
+
} catch {
|
|
37
|
+
problems.push("apiHost must be a URL");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (o.org !== void 0) {
|
|
41
|
+
const s = typeof o.org === "string" ? o.org.trim().toLowerCase() : "";
|
|
42
|
+
if (!SLUG.test(s)) problems.push("org must be an org slug");
|
|
43
|
+
else config.org = s;
|
|
44
|
+
}
|
|
45
|
+
for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableReceipts", "disableAutopilot", "copyLink", "disableAsk", "disableMission", "disableCopy"]) bool(k);
|
|
46
|
+
if (o.autopilotOrg !== void 0) {
|
|
47
|
+
const s = typeof o.autopilotOrg === "string" ? o.autopilotOrg.trim().toLowerCase() : "";
|
|
48
|
+
if (!SLUG.test(s)) problems.push("autopilotOrg must be an org slug");
|
|
49
|
+
else config.autopilotOrg = s;
|
|
50
|
+
}
|
|
51
|
+
if (o.autopilotContent !== void 0) {
|
|
52
|
+
if (o.autopilotContent !== "full" && o.autopilotContent !== "actions" && o.autopilotContent !== "commands") problems.push('autopilotContent must be "full", "actions" or "commands"');
|
|
53
|
+
else config.autopilotContent = o.autopilotContent;
|
|
54
|
+
}
|
|
55
|
+
if (o.allowedCloneSources !== void 0) {
|
|
56
|
+
const a = o.allowedCloneSources;
|
|
57
|
+
if (a === "any" || a === "org-playbook") config.allowedCloneSources = a;
|
|
58
|
+
else if (Array.isArray(a) && a.every((x) => typeof x === "string" && HANDLE.test(x.trim().replace(/^@/, "").toLowerCase())))
|
|
59
|
+
config.allowedCloneSources = [...new Set(a.map((x) => x.trim().replace(/^@/, "").toLowerCase()))];
|
|
60
|
+
else problems.push('allowedCloneSources must be "any", "org-playbook" or a list of handles and org slugs');
|
|
61
|
+
}
|
|
62
|
+
if (o.extraRedactTerms !== void 0) {
|
|
63
|
+
if (!Array.isArray(o.extraRedactTerms) || !o.extraRedactTerms.every((t) => typeof t === "string")) problems.push("extraRedactTerms must be a list of strings");
|
|
64
|
+
else config.extraRedactTerms = [...new Set(o.extraRedactTerms.map((t) => t.trim()).filter((t) => t.length >= 2 && t.length <= 100))].slice(0, 200);
|
|
65
|
+
}
|
|
66
|
+
if (o.profile !== void 0) {
|
|
67
|
+
if (o.profile !== "finance" && o.profile !== "hipaa") problems.push('profile must be "finance" or "hipaa"');
|
|
68
|
+
else config.profile = o.profile;
|
|
69
|
+
}
|
|
70
|
+
if (o.archive !== void 0) {
|
|
71
|
+
const a = o.archive;
|
|
72
|
+
const org = a && typeof a.org === "string" ? a.org.trim().toLowerCase() : "";
|
|
73
|
+
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 }");
|
|
74
|
+
else config.archive = { org, required: a.required === true };
|
|
75
|
+
}
|
|
76
|
+
if (o.restrictOrgs !== void 0) {
|
|
77
|
+
const list = typeof o.restrictOrgs === "string" ? o.restrictOrgs.split(",") : o.restrictOrgs;
|
|
78
|
+
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");
|
|
79
|
+
else config.restrictOrgs = [...new Set(list.map((x) => x.trim().toLowerCase()))];
|
|
80
|
+
}
|
|
81
|
+
if (o.restrictMode !== void 0) {
|
|
82
|
+
if (o.restrictMode !== "block-all-uploads") problems.push('restrictMode must be "block-all-uploads"');
|
|
83
|
+
else config.restrictMode = o.restrictMode;
|
|
84
|
+
}
|
|
85
|
+
if (o.askBefore !== void 0) {
|
|
86
|
+
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");
|
|
87
|
+
else config.askBefore = [...new Set(o.askBefore.map((t) => t.trim()).filter((t) => t && t.length <= 200))].slice(0, 50);
|
|
88
|
+
}
|
|
89
|
+
if (o.askTimeoutMinutes !== void 0) {
|
|
90
|
+
const n = o.askTimeoutMinutes;
|
|
91
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < 1 || n > 15) problems.push("askTimeoutMinutes must be a number from 1 to 15");
|
|
92
|
+
else config.askTimeoutMinutes = Math.round(n);
|
|
93
|
+
}
|
|
94
|
+
if (o.askOrg !== void 0) {
|
|
95
|
+
const s = typeof o.askOrg === "string" ? o.askOrg.trim().toLowerCase() : "";
|
|
96
|
+
if (!SLUG.test(s)) problems.push("askOrg must be an org slug");
|
|
97
|
+
else config.askOrg = s;
|
|
98
|
+
}
|
|
99
|
+
if (o.allowCloneYesWith !== void 0) {
|
|
100
|
+
const l = o.allowCloneYesWith;
|
|
101
|
+
if (!Array.isArray(l) || !l.every((x) => GATED_CAPABILITIES.includes(x))) problems.push('allowCloneYesWith must list only "shell", "install", "network" and/or "settings"');
|
|
102
|
+
else config.allowCloneYesWith = [...new Set(l)];
|
|
103
|
+
}
|
|
104
|
+
return { config, problems };
|
|
105
|
+
}
|
|
106
|
+
function systemManagedPaths(platform = process.platform, env = process.env) {
|
|
107
|
+
if (platform === "darwin")
|
|
108
|
+
return [
|
|
109
|
+
{ path: "/Library/Application Support/copyrun/managed.json", kind: "json" },
|
|
110
|
+
// a configuration profile (Jamf, Kandji, Intune for Mac) lands here; it wins over the file
|
|
111
|
+
{ path: "/Library/Managed Preferences/ai.copyrun.cli.plist", kind: "plist" }
|
|
112
|
+
];
|
|
113
|
+
if (platform === "win32") return [{ path: join(env.ProgramData || env.PROGRAMDATA || "C:\\ProgramData", "copyrun", "managed.json"), kind: "json" }];
|
|
114
|
+
return [{ path: "/etc/copyrun/managed.json", kind: "json" }];
|
|
115
|
+
}
|
|
116
|
+
function plutil(p) {
|
|
117
|
+
try {
|
|
118
|
+
return execFileSync("/usr/bin/plutil", ["-convert", "json", "-o", "-", p], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
119
|
+
} catch {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function loadManaged(deps = {}) {
|
|
124
|
+
const env = deps.env ?? process.env;
|
|
125
|
+
const exists = deps.exists ?? existsSync;
|
|
126
|
+
const read = deps.read ?? ((p) => readFileSync(p, "utf8"));
|
|
127
|
+
const plist = deps.plistToJson ?? plutil;
|
|
128
|
+
const system = systemManagedPaths(deps.platform ?? process.platform, env).filter((f) => exists(f.path));
|
|
129
|
+
const files = system.length ? system : env.COPYRUN_MANAGED_CONFIG ? [{ path: env.COPYRUN_MANAGED_CONFIG, kind: "json" }] : [];
|
|
130
|
+
const state = { config: {}, sources: [], errors: [] };
|
|
131
|
+
for (const f of files) {
|
|
132
|
+
let text;
|
|
133
|
+
try {
|
|
134
|
+
text = f.kind === "plist" ? plist(f.path) : read(f.path);
|
|
135
|
+
} catch (e) {
|
|
136
|
+
state.errors.push(`${f.path}: can't be read (${e.message})`);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (text === void 0) {
|
|
140
|
+
if (f.kind === "plist" && files.some((x) => x.kind === "json")) continue;
|
|
141
|
+
state.errors.push(`${f.path}: can't be read (plutil unavailable)`);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
let json;
|
|
145
|
+
try {
|
|
146
|
+
json = JSON.parse(text);
|
|
147
|
+
} catch {
|
|
148
|
+
state.errors.push(`${f.path}: not valid JSON`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const { config, problems } = parseManaged(json);
|
|
152
|
+
if (problems.length) state.errors.push(`${f.path}: ${problems.join("; ")}`);
|
|
153
|
+
state.config = { ...state.config, ...config };
|
|
154
|
+
state.sources.push(f.path);
|
|
155
|
+
}
|
|
156
|
+
if (state.config.allowedCloneSources === "org-playbook" && !state.config.org) state.errors.push('allowedCloneSources "org-playbook" needs org');
|
|
157
|
+
if (state.config.autopilotOrg && state.config.org && state.config.autopilotOrg !== state.config.org) state.errors.push("autopilotOrg must be the same org as org");
|
|
158
|
+
if (state.config.askOrg && state.config.org && state.config.askOrg !== state.config.org) state.errors.push("askOrg must be the same org as org");
|
|
159
|
+
return state;
|
|
160
|
+
}
|
|
161
|
+
var cached;
|
|
162
|
+
function managed() {
|
|
163
|
+
return cached ??= loadManaged();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/config.ts
|
|
167
|
+
var VERSION = "0.2.0";
|
|
168
|
+
var DIR = process.env.COPYRUN_HOME ?? join2(homedir(), ".copyrun");
|
|
169
|
+
function isLoopbackHost(hostname) {
|
|
170
|
+
const h = hostname.toLowerCase();
|
|
171
|
+
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";
|
|
172
|
+
}
|
|
173
|
+
function apiProblem(url) {
|
|
174
|
+
let u;
|
|
175
|
+
try {
|
|
176
|
+
u = new URL(url);
|
|
177
|
+
} catch {
|
|
178
|
+
return `"${url}" isn't a URL (--api / COPYRUN_API).`;
|
|
179
|
+
}
|
|
180
|
+
if (u.protocol === "https:" || u.protocol === "http:" && isLoopbackHost(u.hostname)) return void 0;
|
|
181
|
+
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.`;
|
|
182
|
+
}
|
|
183
|
+
function apiBase(flag) {
|
|
184
|
+
const pinned = managed().config.apiHost;
|
|
185
|
+
if (pinned) return pinned;
|
|
186
|
+
const base = (flag ?? process.env.COPYRUN_API ?? "https://copyrun.ai").replace(/\/$/, "");
|
|
187
|
+
const problem = apiProblem(base);
|
|
188
|
+
if (problem) throw new Error(problem);
|
|
189
|
+
return base;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/live-logic.ts
|
|
193
|
+
import { createHash } from "crypto";
|
|
194
|
+
function gateHeaders(env) {
|
|
195
|
+
const h = {};
|
|
196
|
+
const prelaunch = env.COPYRUN_PRELAUNCH_KEY?.trim();
|
|
197
|
+
if (prelaunch) h["x-copyrun-prelaunch"] = prelaunch;
|
|
198
|
+
const beta = env.COPYRUN_BETA?.trim();
|
|
199
|
+
if (beta && beta.includes(":")) h["x-copyrun-beta"] = beta;
|
|
200
|
+
return h;
|
|
201
|
+
}
|
|
202
|
+
var TEST_RE = /(^|[\s;&|])(npm|pnpm|yarn|bun)\s+(run\s+)?test\b|(^|[\s;&|])(vitest|jest|pytest|mocha|cargo test|go test|node --test|rspec|phpunit|mix test|dotnet test|gradle test|mvn test|make test)\b/;
|
|
203
|
+
var FAILED_RE = /(\d+\s+(failed|failing)\b|FAIL\b|Tests:\s+\d+ failed|AssertionError|Traceback \(most recent call last\)|error TS\d+|npm ERR!)/;
|
|
204
|
+
var EXT_RE = /\.([a-z0-9]{1,12})$/i;
|
|
205
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
206
|
+
function extOf(path) {
|
|
207
|
+
if (!path) return void 0;
|
|
208
|
+
const base = path.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
209
|
+
const m = base.match(EXT_RE);
|
|
210
|
+
return m ? m[1].toLowerCase() : void 0;
|
|
211
|
+
}
|
|
212
|
+
function summarizeHook(input) {
|
|
213
|
+
if (input.hook_event_name === "Stop") return { kind: "stop" };
|
|
214
|
+
const name = input.tool_name;
|
|
215
|
+
if (!name) return void 0;
|
|
216
|
+
const ti = input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
|
|
217
|
+
const file = str(ti.file_path) ?? str(ti.notebook_path) ?? str(ti.path);
|
|
218
|
+
const ext = extOf(file);
|
|
219
|
+
switch (name) {
|
|
220
|
+
case "Read":
|
|
221
|
+
return { kind: "read", ...ext ? { ext } : {} };
|
|
222
|
+
case "Write":
|
|
223
|
+
return { kind: "write", ...ext ? { ext } : {} };
|
|
224
|
+
case "Edit":
|
|
225
|
+
case "MultiEdit":
|
|
226
|
+
case "NotebookEdit":
|
|
227
|
+
return { kind: "edit", ...ext ? { ext } : {} };
|
|
228
|
+
case "Bash": {
|
|
229
|
+
const command = str(ti.command) ?? "";
|
|
230
|
+
if (!TEST_RE.test(command)) return { kind: "bash" };
|
|
231
|
+
const out = testOutput(input.tool_response);
|
|
232
|
+
return { kind: "test", ...out === void 0 ? {} : { ok: !FAILED_RE.test(out) } };
|
|
233
|
+
}
|
|
234
|
+
case "Grep":
|
|
235
|
+
case "Glob":
|
|
236
|
+
case "LS":
|
|
237
|
+
return { kind: "search" };
|
|
238
|
+
case "WebFetch":
|
|
239
|
+
case "WebSearch":
|
|
240
|
+
return { kind: "web" };
|
|
241
|
+
case "Task":
|
|
242
|
+
case "Agent":
|
|
243
|
+
return { kind: "agent" };
|
|
244
|
+
case "Skill":
|
|
245
|
+
return { kind: "skill" };
|
|
246
|
+
default:
|
|
247
|
+
return { kind: "tool" };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function testOutput(resp) {
|
|
251
|
+
if (typeof resp === "string") return resp.slice(-4e3);
|
|
252
|
+
if (resp && typeof resp === "object") {
|
|
253
|
+
const r = resp;
|
|
254
|
+
if (r.interrupted === true) return "FAIL interrupted";
|
|
255
|
+
const parts = [str(r.stdout), str(r.stderr), str(r.output)].filter((s) => !!s);
|
|
256
|
+
if (parts.length) return parts.join("\n").slice(-4e3);
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/mission-logic.ts
|
|
262
|
+
import { createHash as createHash2 } from "crypto";
|
|
263
|
+
var BEAT_TIMEOUT_MS = 1500;
|
|
264
|
+
var missionKey = (sessionId) => createHash2("sha256").update(`copyrun-mission-v1:${sessionId}`).digest("hex").slice(0, 32);
|
|
265
|
+
function transcriptTokens(jsonl) {
|
|
266
|
+
const byId = /* @__PURE__ */ new Map();
|
|
267
|
+
let n = 0;
|
|
268
|
+
for (const line of jsonl.split("\n")) {
|
|
269
|
+
if (!line.includes('"usage"')) continue;
|
|
270
|
+
let o;
|
|
271
|
+
try {
|
|
272
|
+
o = JSON.parse(line);
|
|
273
|
+
} catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
const u = o.message?.usage;
|
|
277
|
+
if (o.type !== "assistant" || !u) continue;
|
|
278
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) && v > 0 ? v : 0;
|
|
279
|
+
const cached2 = num(u.cache_read_input_tokens);
|
|
280
|
+
const t = { input: num(u.input_tokens) + num(u.cache_creation_input_tokens) + cached2, cached: cached2, output: num(u.output_tokens) };
|
|
281
|
+
byId.set(typeof o.message?.id === "string" ? o.message.id : `#${n++}`, t);
|
|
282
|
+
}
|
|
283
|
+
if (!byId.size) return void 0;
|
|
284
|
+
const sum = { input: 0, cached: 0, output: 0 };
|
|
285
|
+
for (const t of byId.values()) {
|
|
286
|
+
sum.input += t.input;
|
|
287
|
+
sum.cached += t.cached;
|
|
288
|
+
sum.output += t.output;
|
|
289
|
+
}
|
|
290
|
+
return sum.input + sum.output > 0 ? sum : void 0;
|
|
291
|
+
}
|
|
292
|
+
var TOKENS_MAX_BYTES = 64 * 1024 * 1024;
|
|
293
|
+
var EVENT = { SessionStart: "start", UserPromptSubmit: "prompt", PreToolUse: "step", Notification: "waiting", Stop: "stop", SessionEnd: "end" };
|
|
294
|
+
function beatOf(input, agent2, project) {
|
|
295
|
+
const event = input.hook_event_name ? EVENT[input.hook_event_name] : void 0;
|
|
296
|
+
if (!event || !input.session_id) return void 0;
|
|
297
|
+
const step = event === "step" ? summarizeHook({ ...input, hook_event_name: "PreToolUse" }) : void 0;
|
|
298
|
+
const kind = step?.kind === "stop" || step?.kind === "start" ? void 0 : step?.kind;
|
|
299
|
+
const name = projectName(project);
|
|
300
|
+
return { session: missionKey(input.session_id), agent: agent2, event, ...name ? { project: name } : {}, ...kind ? { kind } : {}, ...step?.ext ? { ext: step.ext } : {}, ...event === "waiting" && needsPermission(input) ? { page: true } : {} };
|
|
301
|
+
}
|
|
302
|
+
var projectName = (p) => p?.replace(/[^\w .@()+-]/g, "").trim().slice(0, 80) || void 0;
|
|
303
|
+
function needsPermission(input) {
|
|
304
|
+
if (typeof input.notification_type === "string") return input.notification_type === "permission_prompt";
|
|
305
|
+
return typeof input.message === "string" && /\bpermission\b/i.test(input.message);
|
|
306
|
+
}
|
|
307
|
+
var SHELL_TEST = /(^|[\s;&|])(npm|pnpm|yarn|bun)\s+(run\s+)?test\b|(^|[\s;&|])(vitest|jest|pytest|mocha|cargo test|go test|node --test|rspec|phpunit|mix test|dotnet test|gradle test|mvn test|make test)\b/;
|
|
308
|
+
var extOfPath = (p) => {
|
|
309
|
+
const m = typeof p === "string" ? /\.([A-Za-z0-9]{1,12})$/.exec(p) : null;
|
|
310
|
+
return m ? m[1].toLowerCase() : void 0;
|
|
311
|
+
};
|
|
312
|
+
function cursorBeatOf(input, project) {
|
|
313
|
+
const id = typeof input.conversation_id === "string" ? input.conversation_id : typeof input.session_id === "string" ? input.session_id : void 0;
|
|
314
|
+
const name = typeof input.hook_event_name === "string" ? input.hook_event_name : void 0;
|
|
315
|
+
if (!id || !name) return void 0;
|
|
316
|
+
const base = { session: missionKey(`cursor:${id}`), agent: "cursor", ...projectName(project) ? { project: projectName(project) } : {} };
|
|
317
|
+
switch (name) {
|
|
318
|
+
case "beforeSubmitPrompt":
|
|
319
|
+
return { ...base, event: "prompt" };
|
|
320
|
+
case "beforeShellExecution":
|
|
321
|
+
return { ...base, event: "step", kind: typeof input.command === "string" && SHELL_TEST.test(input.command) ? "test" : "bash" };
|
|
322
|
+
case "beforeMCPExecution":
|
|
323
|
+
return { ...base, event: "step", kind: "tool" };
|
|
324
|
+
case "beforeReadFile": {
|
|
325
|
+
const ext = extOfPath(input.file_path);
|
|
326
|
+
return { ...base, event: "step", kind: "read", ...ext ? { ext } : {} };
|
|
327
|
+
}
|
|
328
|
+
case "afterFileEdit": {
|
|
329
|
+
const ext = extOfPath(input.file_path);
|
|
330
|
+
return { ...base, event: "step", kind: "edit", ...ext ? { ext } : {} };
|
|
331
|
+
}
|
|
332
|
+
case "stop":
|
|
333
|
+
return { ...base, event: "stop" };
|
|
334
|
+
default:
|
|
335
|
+
return void 0;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
var CURSOR_PERMISSION_EVENTS = ["beforeShellExecution", "beforeMCPExecution", "beforeReadFile"];
|
|
339
|
+
var isCursorPermissionEvent = (event) => CURSOR_PERMISSION_EVENTS.includes(event);
|
|
340
|
+
var cursorAllowOutput = (event) => isCursorPermissionEvent(event) ? JSON.stringify({ continue: true, permission: "allow" }) : void 0;
|
|
341
|
+
function cursorStopOutput(event, by) {
|
|
342
|
+
if (!isCursorPermissionEvent(event)) return void 0;
|
|
343
|
+
const who = by ? `@${by}` : "you";
|
|
344
|
+
const user = `Stopped from copyrun mission control by ${who}.`;
|
|
345
|
+
const agentMsg = `${user} Stop working now: don't retry or work around this, and wait for the person's next message.`;
|
|
346
|
+
return JSON.stringify({ continue: true, permission: "deny", user_message: user, agent_message: agentMsg });
|
|
347
|
+
}
|
|
348
|
+
function stopOutput(by, agent2) {
|
|
349
|
+
const who = by ? `@${by}` : "you";
|
|
350
|
+
const reason = `Stopped from copyrun mission control by ${who}. Stop working now: don't retry or work around this, and wait for the person's next message.`;
|
|
351
|
+
const deny = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } };
|
|
352
|
+
return JSON.stringify(agent2 === "codex" ? deny : { continue: false, stopReason: `Stopped from copyrun mission control by ${who}.`, ...deny });
|
|
353
|
+
}
|
|
354
|
+
var codexQuietOutput = (event) => event === "Stop" ? "{}" : void 0;
|
|
355
|
+
|
|
356
|
+
// src/local-project.ts
|
|
357
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
358
|
+
import { dirname, isAbsolute, join as join3, resolve } from "path";
|
|
359
|
+
var gitRootOf = (cwd) => {
|
|
360
|
+
try {
|
|
361
|
+
return existsSync2(cwd) ? gitRoot(cwd)?.root : void 0;
|
|
362
|
+
} catch {
|
|
363
|
+
return void 0;
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
function gitRoot(cwd) {
|
|
367
|
+
let dir = resolve(cwd);
|
|
368
|
+
for (let i = 0; i < 40; i++) {
|
|
369
|
+
const g = join3(dir, ".git");
|
|
370
|
+
try {
|
|
371
|
+
const st = statSync(g);
|
|
372
|
+
if (st.isDirectory()) return { root: dir, gitDir: g };
|
|
373
|
+
if (st.isFile()) {
|
|
374
|
+
const m = readFileSync2(g, "utf8").match(/^gitdir:\s*(.+)$/m);
|
|
375
|
+
if (m) {
|
|
376
|
+
const wt = isAbsolute(m[1].trim()) ? m[1].trim() : resolve(dir, m[1].trim());
|
|
377
|
+
let common = wt;
|
|
378
|
+
try {
|
|
379
|
+
const c = readFileSync2(join3(wt, "commondir"), "utf8").trim();
|
|
380
|
+
common = isAbsolute(c) ? c : resolve(wt, c);
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
return { root: dir, gitDir: common };
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
const up = dirname(dir);
|
|
389
|
+
if (up === dir) return void 0;
|
|
390
|
+
dir = up;
|
|
391
|
+
}
|
|
392
|
+
return void 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/mission-hook.ts
|
|
396
|
+
var agent = process.argv.includes("cursor") ? "cursor" : process.argv.includes("codex") ? "codex" : "claude-code";
|
|
397
|
+
var argEvent = process.argv[process.argv.indexOf("--event") + 1];
|
|
398
|
+
var eventName = process.argv.includes("--event") ? argEvent : void 0;
|
|
399
|
+
var answered = false;
|
|
400
|
+
function readJson(path) {
|
|
401
|
+
try {
|
|
402
|
+
return JSON.parse(readFileSync3(path, "utf8"));
|
|
403
|
+
} catch {
|
|
404
|
+
return void 0;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
function readStdin() {
|
|
408
|
+
return new Promise((resolve2) => {
|
|
409
|
+
let data = "";
|
|
410
|
+
const t = setTimeout(() => resolve2(data), 1e3);
|
|
411
|
+
process.stdin.setEncoding("utf8");
|
|
412
|
+
process.stdin.on("data", (d) => {
|
|
413
|
+
data += d;
|
|
414
|
+
if (data.length > 2e6) process.stdin.destroy();
|
|
415
|
+
});
|
|
416
|
+
process.stdin.on("end", () => (clearTimeout(t), resolve2(data)));
|
|
417
|
+
process.stdin.on("error", () => (clearTimeout(t), resolve2(data)));
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
async function main() {
|
|
421
|
+
if (process.env.COPYRUN_REPLAY || managed().config.disableMission) return;
|
|
422
|
+
const cfg = readJson(join4(DIR, "config.json")) ?? {};
|
|
423
|
+
if (cfg.mission !== true) return;
|
|
424
|
+
const api = apiBase();
|
|
425
|
+
const token = cfg.device && cfg.device.api === api && typeof cfg.device.token === "string" ? cfg.device.token : void 0;
|
|
426
|
+
if (!token) return;
|
|
427
|
+
const input = JSON.parse(await readStdin());
|
|
428
|
+
if (typeof input.hook_event_name === "string" && input.hook_event_name) eventName ??= input.hook_event_name;
|
|
429
|
+
else if (eventName) input.hook_event_name = eventName;
|
|
430
|
+
const roots = Array.isArray(input.workspace_roots) ? input.workspace_roots.filter((r) => typeof r === "string") : [];
|
|
431
|
+
const cwd = typeof input.cwd === "string" && input.cwd || roots[0] || process.cwd();
|
|
432
|
+
const project = basename(gitRootOf(cwd) ?? cwd);
|
|
433
|
+
const beat = agent === "cursor" ? cursorBeatOf(input, project) : beatOf(input, agent, project);
|
|
434
|
+
if (!beat) return;
|
|
435
|
+
if (agent === "codex") {
|
|
436
|
+
const quiet = codexQuietOutput(eventName);
|
|
437
|
+
if (quiet) {
|
|
438
|
+
answered = true;
|
|
439
|
+
process.stdout.write(quiet + "\n");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (agent === "claude-code" && (beat.event === "stop" || beat.event === "end") && typeof input.transcript_path === "string") {
|
|
443
|
+
try {
|
|
444
|
+
if (statSync2(input.transcript_path).size <= TOKENS_MAX_BYTES) {
|
|
445
|
+
const tokens = transcriptTokens(readFileSync3(input.transcript_path, "utf8"));
|
|
446
|
+
if (tokens) beat.tokens = tokens;
|
|
447
|
+
}
|
|
448
|
+
} catch {
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
const res = await fetch(`${api}/api/v1/me/mission/beat`, {
|
|
452
|
+
method: "POST",
|
|
453
|
+
headers: { ...gateHeaders(process.env), authorization: `Bearer ${token}`, "content-type": "application/json", "user-agent": `copyrun-cli/${VERSION} mission-hook` },
|
|
454
|
+
body: JSON.stringify(beat),
|
|
455
|
+
signal: AbortSignal.timeout(BEAT_TIMEOUT_MS)
|
|
456
|
+
});
|
|
457
|
+
if (!res.ok || beat.event !== "step") return;
|
|
458
|
+
const body = await res.json().catch(() => ({}));
|
|
459
|
+
if (body.stop !== true) return;
|
|
460
|
+
const by = typeof body.by === "string" ? body.by : void 0;
|
|
461
|
+
const out = agent === "cursor" ? cursorStopOutput(eventName ?? "", by) : stopOutput(by, agent);
|
|
462
|
+
if (out) {
|
|
463
|
+
answered = true;
|
|
464
|
+
process.stdout.write(out + "\n");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
main().catch(() => {
|
|
468
|
+
}).finally(() => {
|
|
469
|
+
const allow = agent === "cursor" && !answered ? cursorAllowOutput(eventName ?? "") : void 0;
|
|
470
|
+
if (allow) process.stdout.write(allow + "\n");
|
|
471
|
+
process.exit(0);
|
|
472
|
+
});
|
package/dist/nudge.js
CHANGED
|
@@ -125,14 +125,14 @@ function parseManaged(v) {
|
|
|
125
125
|
if (!SLUG.test(s)) problems.push("org must be an org slug");
|
|
126
126
|
else config.org = s;
|
|
127
127
|
}
|
|
128
|
-
for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableAutopilot", "copyLink"]) bool(k);
|
|
128
|
+
for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableReceipts", "disableAutopilot", "copyLink", "disableAsk", "disableMission", "disableCopy"]) bool(k);
|
|
129
129
|
if (o.autopilotOrg !== void 0) {
|
|
130
130
|
const s = typeof o.autopilotOrg === "string" ? o.autopilotOrg.trim().toLowerCase() : "";
|
|
131
131
|
if (!SLUG.test(s)) problems.push("autopilotOrg must be an org slug");
|
|
132
132
|
else config.autopilotOrg = s;
|
|
133
133
|
}
|
|
134
134
|
if (o.autopilotContent !== void 0) {
|
|
135
|
-
if (o.autopilotContent !== "full" && o.autopilotContent !== "actions") problems.push('autopilotContent must be "full" or "
|
|
135
|
+
if (o.autopilotContent !== "full" && o.autopilotContent !== "actions" && o.autopilotContent !== "commands") problems.push('autopilotContent must be "full", "actions" or "commands"');
|
|
136
136
|
else config.autopilotContent = o.autopilotContent;
|
|
137
137
|
}
|
|
138
138
|
if (o.allowedCloneSources !== void 0) {
|
|
@@ -165,6 +165,20 @@ function parseManaged(v) {
|
|
|
165
165
|
if (o.restrictMode !== "block-all-uploads") problems.push('restrictMode must be "block-all-uploads"');
|
|
166
166
|
else config.restrictMode = o.restrictMode;
|
|
167
167
|
}
|
|
168
|
+
if (o.askBefore !== void 0) {
|
|
169
|
+
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");
|
|
170
|
+
else config.askBefore = [...new Set(o.askBefore.map((t) => t.trim()).filter((t) => t && t.length <= 200))].slice(0, 50);
|
|
171
|
+
}
|
|
172
|
+
if (o.askTimeoutMinutes !== void 0) {
|
|
173
|
+
const n = o.askTimeoutMinutes;
|
|
174
|
+
if (typeof n !== "number" || !Number.isFinite(n) || n < 1 || n > 15) problems.push("askTimeoutMinutes must be a number from 1 to 15");
|
|
175
|
+
else config.askTimeoutMinutes = Math.round(n);
|
|
176
|
+
}
|
|
177
|
+
if (o.askOrg !== void 0) {
|
|
178
|
+
const s = typeof o.askOrg === "string" ? o.askOrg.trim().toLowerCase() : "";
|
|
179
|
+
if (!SLUG.test(s)) problems.push("askOrg must be an org slug");
|
|
180
|
+
else config.askOrg = s;
|
|
181
|
+
}
|
|
168
182
|
if (o.allowCloneYesWith !== void 0) {
|
|
169
183
|
const l = o.allowCloneYesWith;
|
|
170
184
|
if (!Array.isArray(l) || !l.every((x) => GATED_CAPABILITIES.includes(x))) problems.push('allowCloneYesWith must list only "shell", "install", "network" and/or "settings"');
|
|
@@ -224,6 +238,7 @@ function loadManaged(deps = {}) {
|
|
|
224
238
|
}
|
|
225
239
|
if (state.config.allowedCloneSources === "org-playbook" && !state.config.org) state.errors.push('allowedCloneSources "org-playbook" needs org');
|
|
226
240
|
if (state.config.autopilotOrg && state.config.org && state.config.autopilotOrg !== state.config.org) state.errors.push("autopilotOrg must be the same org as org");
|
|
241
|
+
if (state.config.askOrg && state.config.org && state.config.askOrg !== state.config.org) state.errors.push("askOrg must be the same org as org");
|
|
227
242
|
return state;
|
|
228
243
|
}
|
|
229
244
|
var cached;
|