copyrun 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +105 -2
- package/dist/archive-hook.js +252 -0
- package/dist/archive-worker.js +7169 -0
- package/dist/ask.js +3919 -0
- package/dist/autopilot-worker.js +226 -0
- package/dist/autopilot.js +3501 -0
- package/dist/check.js +4722 -0
- package/dist/index.js +29678 -0
- package/dist/live.js +298 -0
- package/dist/mission-watch.js +541 -0
- package/dist/mission.js +472 -0
- package/dist/nudge.js +441 -0
- package/dist/receipt.js +349 -0
- package/dist/seal-worker.js +132 -0
- package/package.json +46 -4
- package/skills/copyrun/SKILL.md +97 -0
- package/skills/copyrun-router/SKILL.md +61 -0
package/dist/live.js
ADDED
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/live-hook.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { readFileSync as readFileSync2, renameSync, writeFileSync } from "fs";
|
|
6
|
+
import { basename, dirname, join as join2 } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
// src/live-logic.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
var LIVE_STATE_FILE = "live.json";
|
|
12
|
+
function gateHeaders(env) {
|
|
13
|
+
const h = {};
|
|
14
|
+
const prelaunch = env.COPYRUN_PRELAUNCH_KEY?.trim();
|
|
15
|
+
if (prelaunch) h["x-copyrun-prelaunch"] = prelaunch;
|
|
16
|
+
const beta = env.COPYRUN_BETA?.trim();
|
|
17
|
+
if (beta && beta.includes(":")) h["x-copyrun-beta"] = beta;
|
|
18
|
+
return h;
|
|
19
|
+
}
|
|
20
|
+
var liveId = (secret, sessionId) => createHash("sha256").update(`copyrun-live-v1:${secret}:${sessionId}`).digest("hex").slice(0, 24);
|
|
21
|
+
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/;
|
|
22
|
+
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!)/;
|
|
23
|
+
var EXT_RE = /\.([a-z0-9]{1,12})$/i;
|
|
24
|
+
var str = (v) => typeof v === "string" ? v : void 0;
|
|
25
|
+
function extOf(path) {
|
|
26
|
+
if (!path) return void 0;
|
|
27
|
+
const base = path.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
28
|
+
const m = base.match(EXT_RE);
|
|
29
|
+
return m ? m[1].toLowerCase() : void 0;
|
|
30
|
+
}
|
|
31
|
+
function summarizeHook(input) {
|
|
32
|
+
if (input.hook_event_name === "Stop") return { kind: "stop" };
|
|
33
|
+
const name = input.tool_name;
|
|
34
|
+
if (!name) return void 0;
|
|
35
|
+
const ti = input.tool_input && typeof input.tool_input === "object" ? input.tool_input : {};
|
|
36
|
+
const file = str(ti.file_path) ?? str(ti.notebook_path) ?? str(ti.path);
|
|
37
|
+
const ext = extOf(file);
|
|
38
|
+
switch (name) {
|
|
39
|
+
case "Read":
|
|
40
|
+
return { kind: "read", ...ext ? { ext } : {} };
|
|
41
|
+
case "Write":
|
|
42
|
+
return { kind: "write", ...ext ? { ext } : {} };
|
|
43
|
+
case "Edit":
|
|
44
|
+
case "MultiEdit":
|
|
45
|
+
case "NotebookEdit":
|
|
46
|
+
return { kind: "edit", ...ext ? { ext } : {} };
|
|
47
|
+
case "Bash": {
|
|
48
|
+
const command = str(ti.command) ?? "";
|
|
49
|
+
if (!TEST_RE.test(command)) return { kind: "bash" };
|
|
50
|
+
const out = testOutput(input.tool_response);
|
|
51
|
+
return { kind: "test", ...out === void 0 ? {} : { ok: !FAILED_RE.test(out) } };
|
|
52
|
+
}
|
|
53
|
+
case "Grep":
|
|
54
|
+
case "Glob":
|
|
55
|
+
case "LS":
|
|
56
|
+
return { kind: "search" };
|
|
57
|
+
case "WebFetch":
|
|
58
|
+
case "WebSearch":
|
|
59
|
+
return { kind: "web" };
|
|
60
|
+
case "Task":
|
|
61
|
+
case "Agent":
|
|
62
|
+
return { kind: "agent" };
|
|
63
|
+
case "Skill":
|
|
64
|
+
return { kind: "skill" };
|
|
65
|
+
default:
|
|
66
|
+
return { kind: "tool" };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function testOutput(resp) {
|
|
70
|
+
if (typeof resp === "string") return resp.slice(-4e3);
|
|
71
|
+
if (resp && typeof resp === "object") {
|
|
72
|
+
const r = resp;
|
|
73
|
+
if (r.interrupted === true) return "FAIL interrupted";
|
|
74
|
+
const parts = [str(r.stdout), str(r.stderr), str(r.output)].filter((s) => !!s);
|
|
75
|
+
if (parts.length) return parts.join("\n").slice(-4e3);
|
|
76
|
+
}
|
|
77
|
+
return void 0;
|
|
78
|
+
}
|
|
79
|
+
var streamMessage = (url) => `\u{1F534} Streaming this session's steps (kinds and extensions only) at ${url} \xB7 npx copyrun live off stops it`;
|
|
80
|
+
function recordStep(state, sessionId, id, now, project) {
|
|
81
|
+
const sessions = { ...state.sessions ?? {} };
|
|
82
|
+
const cur = sessions[sessionId];
|
|
83
|
+
const first = !cur;
|
|
84
|
+
sessions[sessionId] = cur ? { ...cur, lastAt: now, steps: cur.steps + 1 } : { liveId: id, startedAt: now, lastAt: now, steps: 1, ...project ? { project } : {} };
|
|
85
|
+
const keep = Object.entries(sessions).sort((a, b) => b[1].lastAt.localeCompare(a[1].lastAt)).slice(0, 50);
|
|
86
|
+
return { state: { ...state, sessions: Object.fromEntries(keep) }, first };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/managed.ts
|
|
90
|
+
import { execFileSync } from "child_process";
|
|
91
|
+
import { existsSync, readFileSync } from "fs";
|
|
92
|
+
import { join } from "path";
|
|
93
|
+
var GATED_CAPABILITIES = ["shell", "install", "network", "settings"];
|
|
94
|
+
var SLUG = /^[a-z0-9][a-z0-9-]{0,39}$/;
|
|
95
|
+
var HANDLE = /^[a-z0-9][a-z0-9-]{0,38}$/;
|
|
96
|
+
function parseManaged(v) {
|
|
97
|
+
const problems = [];
|
|
98
|
+
const config = {};
|
|
99
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) return { config, problems: ["not a JSON object"] };
|
|
100
|
+
const o = v;
|
|
101
|
+
const bool = (k) => {
|
|
102
|
+
if (o[k] === void 0) return;
|
|
103
|
+
if (typeof o[k] !== "boolean") problems.push(`${k} must be true or false`);
|
|
104
|
+
else config[k] = o[k];
|
|
105
|
+
};
|
|
106
|
+
if (o.apiHost !== void 0) {
|
|
107
|
+
try {
|
|
108
|
+
const u = new URL(String(o.apiHost));
|
|
109
|
+
const local = u.hostname === "localhost" || u.hostname === "127.0.0.1";
|
|
110
|
+
if (u.protocol !== "https:" && !(local && u.protocol === "http:")) problems.push("apiHost must be an https URL");
|
|
111
|
+
else config.apiHost = u.origin;
|
|
112
|
+
} catch {
|
|
113
|
+
problems.push("apiHost must be a URL");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (o.org !== void 0) {
|
|
117
|
+
const s = typeof o.org === "string" ? o.org.trim().toLowerCase() : "";
|
|
118
|
+
if (!SLUG.test(s)) problems.push("org must be an org slug");
|
|
119
|
+
else config.org = s;
|
|
120
|
+
}
|
|
121
|
+
for (const k of ["forbidPublic", "forbidAnonymous", "forbidUnlisted", "requireShape", "disableNudge", "disableLive", "disableReceipts", "disableAutopilot", "copyLink", "disableAsk", "disableMission", "disableCopy"]) bool(k);
|
|
122
|
+
if (o.autopilotOrg !== void 0) {
|
|
123
|
+
const s = typeof o.autopilotOrg === "string" ? o.autopilotOrg.trim().toLowerCase() : "";
|
|
124
|
+
if (!SLUG.test(s)) problems.push("autopilotOrg must be an org slug");
|
|
125
|
+
else config.autopilotOrg = s;
|
|
126
|
+
}
|
|
127
|
+
if (o.autopilotContent !== void 0) {
|
|
128
|
+
if (o.autopilotContent !== "full" && o.autopilotContent !== "actions" && o.autopilotContent !== "commands") problems.push('autopilotContent must be "full", "actions" or "commands"');
|
|
129
|
+
else config.autopilotContent = o.autopilotContent;
|
|
130
|
+
}
|
|
131
|
+
if (o.allowedCloneSources !== void 0) {
|
|
132
|
+
const a = o.allowedCloneSources;
|
|
133
|
+
if (a === "any" || a === "org-playbook") config.allowedCloneSources = a;
|
|
134
|
+
else if (Array.isArray(a) && a.every((x) => typeof x === "string" && HANDLE.test(x.trim().replace(/^@/, "").toLowerCase())))
|
|
135
|
+
config.allowedCloneSources = [...new Set(a.map((x) => x.trim().replace(/^@/, "").toLowerCase()))];
|
|
136
|
+
else problems.push('allowedCloneSources must be "any", "org-playbook" or a list of handles and org slugs');
|
|
137
|
+
}
|
|
138
|
+
if (o.extraRedactTerms !== void 0) {
|
|
139
|
+
if (!Array.isArray(o.extraRedactTerms) || !o.extraRedactTerms.every((t) => typeof t === "string")) problems.push("extraRedactTerms must be a list of strings");
|
|
140
|
+
else config.extraRedactTerms = [...new Set(o.extraRedactTerms.map((t) => t.trim()).filter((t) => t.length >= 2 && t.length <= 100))].slice(0, 200);
|
|
141
|
+
}
|
|
142
|
+
if (o.profile !== void 0) {
|
|
143
|
+
if (o.profile !== "finance" && o.profile !== "hipaa") problems.push('profile must be "finance" or "hipaa"');
|
|
144
|
+
else config.profile = o.profile;
|
|
145
|
+
}
|
|
146
|
+
if (o.archive !== void 0) {
|
|
147
|
+
const a = o.archive;
|
|
148
|
+
const org = a && typeof a.org === "string" ? a.org.trim().toLowerCase() : "";
|
|
149
|
+
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 }");
|
|
150
|
+
else config.archive = { org, required: a.required === true };
|
|
151
|
+
}
|
|
152
|
+
if (o.restrictOrgs !== void 0) {
|
|
153
|
+
const list = typeof o.restrictOrgs === "string" ? o.restrictOrgs.split(",") : o.restrictOrgs;
|
|
154
|
+
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");
|
|
155
|
+
else config.restrictOrgs = [...new Set(list.map((x) => x.trim().toLowerCase()))];
|
|
156
|
+
}
|
|
157
|
+
if (o.restrictMode !== void 0) {
|
|
158
|
+
if (o.restrictMode !== "block-all-uploads") problems.push('restrictMode must be "block-all-uploads"');
|
|
159
|
+
else config.restrictMode = o.restrictMode;
|
|
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
|
+
}
|
|
175
|
+
if (o.allowCloneYesWith !== void 0) {
|
|
176
|
+
const l = o.allowCloneYesWith;
|
|
177
|
+
if (!Array.isArray(l) || !l.every((x) => GATED_CAPABILITIES.includes(x))) problems.push('allowCloneYesWith must list only "shell", "install", "network" and/or "settings"');
|
|
178
|
+
else config.allowCloneYesWith = [...new Set(l)];
|
|
179
|
+
}
|
|
180
|
+
return { config, problems };
|
|
181
|
+
}
|
|
182
|
+
function systemManagedPaths(platform = process.platform, env = process.env) {
|
|
183
|
+
if (platform === "darwin")
|
|
184
|
+
return [
|
|
185
|
+
{ path: "/Library/Application Support/copyrun/managed.json", kind: "json" },
|
|
186
|
+
// a configuration profile (Jamf, Kandji, Intune for Mac) lands here; it wins over the file
|
|
187
|
+
{ path: "/Library/Managed Preferences/ai.copyrun.cli.plist", kind: "plist" }
|
|
188
|
+
];
|
|
189
|
+
if (platform === "win32") return [{ path: join(env.ProgramData || env.PROGRAMDATA || "C:\\ProgramData", "copyrun", "managed.json"), kind: "json" }];
|
|
190
|
+
return [{ path: "/etc/copyrun/managed.json", kind: "json" }];
|
|
191
|
+
}
|
|
192
|
+
function plutil(p) {
|
|
193
|
+
try {
|
|
194
|
+
return execFileSync("/usr/bin/plutil", ["-convert", "json", "-o", "-", p], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 5e3 });
|
|
195
|
+
} catch {
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function loadManaged(deps = {}) {
|
|
200
|
+
const env = deps.env ?? process.env;
|
|
201
|
+
const exists = deps.exists ?? existsSync;
|
|
202
|
+
const read = deps.read ?? ((p) => readFileSync(p, "utf8"));
|
|
203
|
+
const plist = deps.plistToJson ?? plutil;
|
|
204
|
+
const system = systemManagedPaths(deps.platform ?? process.platform, env).filter((f) => exists(f.path));
|
|
205
|
+
const files = system.length ? system : env.COPYRUN_MANAGED_CONFIG ? [{ path: env.COPYRUN_MANAGED_CONFIG, kind: "json" }] : [];
|
|
206
|
+
const state = { config: {}, sources: [], errors: [] };
|
|
207
|
+
for (const f of files) {
|
|
208
|
+
let text;
|
|
209
|
+
try {
|
|
210
|
+
text = f.kind === "plist" ? plist(f.path) : read(f.path);
|
|
211
|
+
} catch (e) {
|
|
212
|
+
state.errors.push(`${f.path}: can't be read (${e.message})`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (text === void 0) {
|
|
216
|
+
if (f.kind === "plist" && files.some((x) => x.kind === "json")) continue;
|
|
217
|
+
state.errors.push(`${f.path}: can't be read (plutil unavailable)`);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
let json;
|
|
221
|
+
try {
|
|
222
|
+
json = JSON.parse(text);
|
|
223
|
+
} catch {
|
|
224
|
+
state.errors.push(`${f.path}: not valid JSON`);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const { config, problems } = parseManaged(json);
|
|
228
|
+
if (problems.length) state.errors.push(`${f.path}: ${problems.join("; ")}`);
|
|
229
|
+
state.config = { ...state.config, ...config };
|
|
230
|
+
state.sources.push(f.path);
|
|
231
|
+
}
|
|
232
|
+
if (state.config.allowedCloneSources === "org-playbook" && !state.config.org) state.errors.push('allowedCloneSources "org-playbook" needs org');
|
|
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");
|
|
235
|
+
return state;
|
|
236
|
+
}
|
|
237
|
+
var cached;
|
|
238
|
+
function managed() {
|
|
239
|
+
return cached ??= loadManaged();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/live-hook.ts
|
|
243
|
+
var DIR = dirname(fileURLToPath(import.meta.url));
|
|
244
|
+
var SELF = fileURLToPath(import.meta.url);
|
|
245
|
+
function readJson(path) {
|
|
246
|
+
try {
|
|
247
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
248
|
+
} catch {
|
|
249
|
+
return void 0;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function readStdin() {
|
|
253
|
+
return new Promise((resolve) => {
|
|
254
|
+
let data = "";
|
|
255
|
+
const t = setTimeout(() => resolve(data), 1500);
|
|
256
|
+
process.stdin.setEncoding("utf8");
|
|
257
|
+
process.stdin.on("data", (d) => {
|
|
258
|
+
data += d;
|
|
259
|
+
if (data.length > 1e6) process.stdin.destroy();
|
|
260
|
+
});
|
|
261
|
+
process.stdin.on("end", () => (clearTimeout(t), resolve(data)));
|
|
262
|
+
process.stdin.on("error", () => (clearTimeout(t), resolve(data)));
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function send(api, payload, headers) {
|
|
266
|
+
try {
|
|
267
|
+
const gate = headers ? JSON.parse(headers) : {};
|
|
268
|
+
await fetch(`${api}/api/v1/live`, { method: "POST", headers: { ...gate, "content-type": "application/json" }, body: payload, signal: AbortSignal.timeout(6e3) });
|
|
269
|
+
} catch {
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function main() {
|
|
273
|
+
if (process.argv[2] === "--send") return send(process.argv[3], process.argv[4], process.argv[5]);
|
|
274
|
+
const config = readJson(join2(DIR, "config.json"));
|
|
275
|
+
if (config?.live !== true || managed().config.disableLive) return;
|
|
276
|
+
const statePath = join2(DIR, LIVE_STATE_FILE);
|
|
277
|
+
const state = readJson(statePath) ?? {};
|
|
278
|
+
if (!state.secret || !state.api) return;
|
|
279
|
+
const input = JSON.parse(await readStdin());
|
|
280
|
+
if (!input.session_id) return;
|
|
281
|
+
const draft = summarizeHook(input);
|
|
282
|
+
if (!draft) return;
|
|
283
|
+
const id = liveId(state.secret, input.session_id);
|
|
284
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
285
|
+
const { state: next, first } = recordStep(state, input.session_id, id, now, input.cwd ? basename(input.cwd) : void 0);
|
|
286
|
+
const tmp = `${statePath}.${process.pid}.tmp`;
|
|
287
|
+
writeFileSync(tmp, JSON.stringify(next), { mode: 384 });
|
|
288
|
+
renameSync(tmp, statePath);
|
|
289
|
+
const payload = JSON.stringify({ liveId: id, tool: "claude-code", kind: draft.kind, ...draft.ext ? { ext: draft.ext } : {}, ...draft.ok === void 0 ? {} : { ok: draft.ok } });
|
|
290
|
+
const gate = { ...state.gate, ...gateHeaders(process.env) };
|
|
291
|
+
const child = spawn(process.execPath, [SELF, "--send", state.api, payload, ...Object.keys(gate).length ? [JSON.stringify(gate)] : []], { detached: true, stdio: "ignore", windowsHide: true });
|
|
292
|
+
child.on("error", () => {
|
|
293
|
+
});
|
|
294
|
+
child.unref();
|
|
295
|
+
if (first) process.stdout.write(JSON.stringify({ systemMessage: streamMessage(`${state.api}/live/${id}`) }) + "\n");
|
|
296
|
+
}
|
|
297
|
+
main().catch(() => {
|
|
298
|
+
}).finally(() => process.exit(0));
|