@agent-beam/beam 0.2.18 → 0.2.21
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 +19 -0
- package/dist/account.js +1 -1
- package/dist/account.js.map +1 -1
- package/dist/agents.d.ts +8 -2
- package/dist/agents.js +7 -7
- package/dist/agents.js.map +1 -1
- package/dist/blocked-targets.d.ts +8 -0
- package/dist/blocked-targets.js +83 -0
- package/dist/blocked-targets.js.map +1 -0
- package/dist/cli.js +208 -42
- package/dist/cli.js.map +1 -1
- package/dist/client.js +67 -11
- package/dist/client.js.map +1 -1
- package/dist/collector-health.d.ts +1 -0
- package/dist/collector-health.js +20 -0
- package/dist/collector-health.js.map +1 -0
- package/dist/connect.js +8 -4
- package/dist/connect.js.map +1 -1
- package/dist/enroll.d.ts +0 -4
- package/dist/enroll.js +2 -60
- package/dist/enroll.js.map +1 -1
- package/dist/forward.d.ts +5 -1
- package/dist/forward.js +23 -7
- package/dist/forward.js.map +1 -1
- package/dist/install.d.ts +2 -0
- package/dist/install.js +58 -6
- package/dist/install.js.map +1 -1
- package/dist/policy.d.ts +1 -0
- package/dist/policy.js +11 -3
- package/dist/policy.js.map +1 -1
- package/dist/serve.js +25 -2
- package/dist/serve.js.map +1 -1
- package/dist/service.d.ts +2 -0
- package/dist/service.js +16 -4
- package/dist/service.js.map +1 -1
- package/dist/setup.js +9 -4
- package/dist/setup.js.map +1 -1
- package/dist/tamper.d.ts +9 -0
- package/dist/tamper.js +116 -0
- package/dist/tamper.js.map +1 -0
- package/dist/trusted-paths.d.ts +5 -0
- package/dist/trusted-paths.js +59 -0
- package/dist/trusted-paths.js.map +1 -0
- package/dist/tty-prompt.d.ts +5 -0
- package/dist/tty-prompt.js +55 -0
- package/dist/tty-prompt.js.map +1 -0
- package/dist/update.d.ts +7 -0
- package/dist/update.js +31 -0
- package/dist/update.js.map +1 -0
- package/package.json +8 -1
package/dist/tamper.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Tamper detection: beam's hook install/service can be removed two ways -- through beam itself
|
|
2
|
+
// ('beam uninstall', 'beam agent install-all', ...), which is authorized and already tells the
|
|
3
|
+
// workspace via revokeDevice()/reportControlEvent() below, or by hand (editing an agent's config
|
|
4
|
+
// file directly, deleting it, unloading the launchd/systemd job) -- which today leaves no record
|
|
5
|
+
// anywhere. This module gives the collector a baseline of what *should* be installed and reports
|
|
6
|
+
// the difference as a critical finding the moment it notices something vanished outside beam's
|
|
7
|
+
// own code path. It cannot detect the collector process itself being killed -- a dead process
|
|
8
|
+
// can't self-report -- that has to be inferred workspace-side from a heartbeat/last-seen gap;
|
|
9
|
+
// this module only covers "the collector is still alive but something under it was tampered with."
|
|
10
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
11
|
+
import { homedir, hostname } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { getDataDirectory } from "./config.js";
|
|
14
|
+
import { findAgent } from "./agents.js";
|
|
15
|
+
import { installedHookAgentIds } from "./install.js";
|
|
16
|
+
import { normalize } from "./core.js";
|
|
17
|
+
import { forwardEvents } from "./forward.js";
|
|
18
|
+
function tamperStatePath() {
|
|
19
|
+
return join(getDataDirectory(), "tamper-state.json");
|
|
20
|
+
}
|
|
21
|
+
export async function readTamperState() {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(await readFile(tamperStatePath(), "utf8"));
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function writeTamperState(state) {
|
|
30
|
+
const path = tamperStatePath();
|
|
31
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
32
|
+
await writeFile(path, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
33
|
+
}
|
|
34
|
+
// Snapshots which agents currently have beam's hook installed and remembers it as the expected
|
|
35
|
+
// baseline. Call this right after any *authorized* change to hook state (installHook,
|
|
36
|
+
// uninstallHook, installAllDetectedHooks, uninstallAllHooks, and every 'beam start') so the
|
|
37
|
+
// baseline always reflects the last state a beam command itself produced. checkForTampering()
|
|
38
|
+
// below then only flags a gap between that baseline and reality -- i.e. a change nobody made
|
|
39
|
+
// through beam.
|
|
40
|
+
export async function recordExpectedHooks(home = homedir()) {
|
|
41
|
+
const expectedHooks = await installedHookAgentIds(home);
|
|
42
|
+
await writeTamperState({ expectedHooks, updatedAt: new Date().toISOString() });
|
|
43
|
+
}
|
|
44
|
+
function selfMonitorRecord(fields, severity) {
|
|
45
|
+
return {
|
|
46
|
+
record_type: "finding",
|
|
47
|
+
source_agent: "beam",
|
|
48
|
+
source_type: "self-monitor",
|
|
49
|
+
severity,
|
|
50
|
+
hostname: hostname(),
|
|
51
|
+
timestamp: new Date().toISOString(),
|
|
52
|
+
...fields,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function tamperFindingRecord(agentId) {
|
|
56
|
+
const name = findAgent(agentId)?.name ?? agentId;
|
|
57
|
+
return selfMonitorRecord({
|
|
58
|
+
event_type: "beam.tamper",
|
|
59
|
+
rule_id: "beam.tamper.hook_removed",
|
|
60
|
+
title: `Beam monitoring hook removed from ${name}`,
|
|
61
|
+
summary: `${name}'s beam hook disappeared from its config on ${hostname()} outside of any 'beam uninstall' or 'beam agent' command. This agent is no longer being observed or policy-enforced by beam.`,
|
|
62
|
+
}, "critical");
|
|
63
|
+
}
|
|
64
|
+
// Compares the live on-disk hook set against the last known-good baseline and returns one
|
|
65
|
+
// critical finding Event per hook that vanished without going through beam. The caller (serve.ts)
|
|
66
|
+
// stores these locally and forwards them to the workspace exactly like any other captured event,
|
|
67
|
+
// so they land on the dashboard's existing Activity/Security risks views with no new backend
|
|
68
|
+
// route required. Never throws -- a failure here must not take down the collector's poll loop.
|
|
69
|
+
export async function checkForTampering(home = homedir()) {
|
|
70
|
+
try {
|
|
71
|
+
const state = await readTamperState();
|
|
72
|
+
const current = await installedHookAgentIds(home);
|
|
73
|
+
if (!state) {
|
|
74
|
+
await recordExpectedHooks(home);
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const missing = state.expectedHooks.filter(id => !current.includes(id));
|
|
78
|
+
const changed = missing.length > 0
|
|
79
|
+
|| current.length !== state.expectedHooks.length
|
|
80
|
+
|| current.some(id => !state.expectedHooks.includes(id));
|
|
81
|
+
if (changed)
|
|
82
|
+
await writeTamperState({ expectedHooks: current, updatedAt: new Date().toISOString() });
|
|
83
|
+
if (!missing.length)
|
|
84
|
+
return [];
|
|
85
|
+
const events = [];
|
|
86
|
+
for (const agentId of missing) {
|
|
87
|
+
try {
|
|
88
|
+
events.push(normalize(tamperFindingRecord(agentId)));
|
|
89
|
+
}
|
|
90
|
+
catch { /* skip a record normalize() can't accept */ }
|
|
91
|
+
}
|
|
92
|
+
return events;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Explicit, non-tamper notice for an authorized stop/uninstall/logout, so the dashboard can tell
|
|
99
|
+
// "an admin or device owner deliberately turned this off" apart from the silent disappearance
|
|
100
|
+
// checkForTampering() exists to catch. Sent directly (not through the local collector, which may
|
|
101
|
+
// already be shutting down as part of the same command) using whatever identity is still on disk
|
|
102
|
+
// at the moment the caller invokes this -- callers must call it *before* clearing that identity.
|
|
103
|
+
// Best-effort: never blocks the command that calls it.
|
|
104
|
+
export async function reportControlEvent(ruleSuffix, title, detail) {
|
|
105
|
+
try {
|
|
106
|
+
const event = normalize(selfMonitorRecord({
|
|
107
|
+
event_type: "beam.control",
|
|
108
|
+
rule_id: `beam.control.${ruleSuffix}`,
|
|
109
|
+
title,
|
|
110
|
+
summary: detail,
|
|
111
|
+
}, "info"));
|
|
112
|
+
await forwardEvents(event);
|
|
113
|
+
}
|
|
114
|
+
catch { /* best-effort only */ }
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=tamper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tamper.js","sourceRoot":"","sources":["../src/tamper.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function trustedPathsFile(): string;
|
|
2
|
+
export declare function readTrustedPaths(): Promise<string[]>;
|
|
3
|
+
export declare function addTrustedPath(path: string): Promise<string[]>;
|
|
4
|
+
export declare function removeTrustedPath(path: string): Promise<string[]>;
|
|
5
|
+
export declare function isPathTrusted(candidate: string, trusted: string[]): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { getDataDirectory } from "./config.js";
|
|
4
|
+
// A global, human-maintained allowlist of outside-workspace paths/globs that should stop
|
|
5
|
+
// triggering Beam's "This action accesses a path outside the current workspace" ask. Lives
|
|
6
|
+
// under the data directory -- the same directory isBeamSelfProtectionTarget() already protects
|
|
7
|
+
// from agent writes -- so only a human running `beam trust` from a real shell can add to it, not
|
|
8
|
+
// an agent trying to approve its own escape.
|
|
9
|
+
export function trustedPathsFile() { return join(getDataDirectory(), "trusted-paths.json"); }
|
|
10
|
+
export async function readTrustedPaths() {
|
|
11
|
+
try {
|
|
12
|
+
const parsed = JSON.parse(await readFile(trustedPathsFile(), "utf8"));
|
|
13
|
+
return Array.isArray(parsed) ? parsed.filter((p) => typeof p === "string" && p.trim().length > 0) : [];
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
if (e.code === "ENOENT")
|
|
17
|
+
return [];
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function writeTrustedPaths(paths) {
|
|
22
|
+
const dir = getDataDirectory();
|
|
23
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
const file = trustedPathsFile();
|
|
25
|
+
const temp = `${file}.tmp`;
|
|
26
|
+
await writeFile(temp, JSON.stringify(paths, null, 2) + "\n", { mode: 0o600 });
|
|
27
|
+
await rename(temp, file);
|
|
28
|
+
}
|
|
29
|
+
export async function addTrustedPath(path) {
|
|
30
|
+
const resolved = resolve(path);
|
|
31
|
+
const current = await readTrustedPaths();
|
|
32
|
+
if (current.includes(resolved))
|
|
33
|
+
return current;
|
|
34
|
+
const next = [...current, resolved];
|
|
35
|
+
await writeTrustedPaths(next);
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
38
|
+
export async function removeTrustedPath(path) {
|
|
39
|
+
const resolved = resolve(path);
|
|
40
|
+
const next = (await readTrustedPaths()).filter(p => p !== resolved);
|
|
41
|
+
await writeTrustedPaths(next);
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
// A candidate path is trusted if it equals a trusted entry or sits under one (directory trust).
|
|
45
|
+
// Entries may also be globs (containing `*`) matched against the candidate.
|
|
46
|
+
export function isPathTrusted(candidate, trusted) {
|
|
47
|
+
const target = resolve(candidate);
|
|
48
|
+
return trusted.some(entry => {
|
|
49
|
+
if (entry.includes("*")) {
|
|
50
|
+
const escaped = entry.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "§§").replace(/\*/g, "[^/]*").replace(/§§/g, ".*");
|
|
51
|
+
return new RegExp(`^${escaped}$`).test(target);
|
|
52
|
+
}
|
|
53
|
+
if (target === entry)
|
|
54
|
+
return true;
|
|
55
|
+
const rel = relative(entry, target);
|
|
56
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=trusted-paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trusted-paths.js","sourceRoot":"","sources":["../src/trusted-paths.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { closeSync, createReadStream, createWriteStream, openSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
const PROMPT_TIMEOUT_MS = 120_000;
|
|
4
|
+
// Reads a choice directly from the controlling terminal (/dev/tty), bypassing stdin/stdout --
|
|
5
|
+
// those carry the hook's JSON protocol with the host agent (Claude Code, Codex, ...) and must
|
|
6
|
+
// never be touched here. Returns null -- "could not prompt" -- when there's no TTY attached
|
|
7
|
+
// (a headless run, CI, a detached process) or the prompt times out; callers must have a safe
|
|
8
|
+
// fallback for that case, never assume a choice was made.
|
|
9
|
+
export async function promptTty(message, choices) {
|
|
10
|
+
if (process.platform === "win32")
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
closeSync(openSync("/dev/tty", "r+"));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return new Promise(resolvePromise => {
|
|
19
|
+
let settled = false;
|
|
20
|
+
let input;
|
|
21
|
+
let output;
|
|
22
|
+
try {
|
|
23
|
+
input = createReadStream("/dev/tty");
|
|
24
|
+
output = createWriteStream("/dev/tty");
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
resolvePromise(null);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const rl = createInterface({ input, output, terminal: true });
|
|
31
|
+
const finish = (value) => {
|
|
32
|
+
if (settled)
|
|
33
|
+
return;
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
rl.close();
|
|
37
|
+
input.destroy();
|
|
38
|
+
output.destroy();
|
|
39
|
+
resolvePromise(value);
|
|
40
|
+
};
|
|
41
|
+
const timer = setTimeout(() => finish(null), PROMPT_TIMEOUT_MS);
|
|
42
|
+
output.write(`\n${message}\n`);
|
|
43
|
+
for (const c of choices)
|
|
44
|
+
output.write(` [${c.key}] ${c.label}\n`);
|
|
45
|
+
rl.question("Choice: ", answer => {
|
|
46
|
+
const normalized = answer.trim().toLowerCase();
|
|
47
|
+
const match = choices.find(c => c.key.toLowerCase() === normalized)
|
|
48
|
+
?? (normalized.length > 0 ? choices.find(c => c.label.toLowerCase().startsWith(normalized)) : undefined);
|
|
49
|
+
finish(match ? match.key : null);
|
|
50
|
+
});
|
|
51
|
+
rl.on("error", () => finish(null));
|
|
52
|
+
input.on("error", () => finish(null));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=tty-prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tty-prompt.js","sourceRoot":"","sources":["../src/tty-prompt.ts"],"names":[],"mappings":"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"}
|
package/dist/update.d.ts
ADDED
package/dist/update.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { runWithSudoFallback } from "./elevate.js";
|
|
4
|
+
const run = promisify(execFile);
|
|
5
|
+
const PACKAGE = "@agent-beam/beam";
|
|
6
|
+
// Plain numeric semver compare (major.minor.patch, ignoring any prerelease suffix) -- good
|
|
7
|
+
// enough for comparing against npm's published "latest" dist-tag. Returns >0 when `a` is newer.
|
|
8
|
+
function compareVersions(a, b) {
|
|
9
|
+
const pa = a.split(/[.-]/).map(n => Number(n) || 0);
|
|
10
|
+
const pb = b.split(/[.-]/).map(n => Number(n) || 0);
|
|
11
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
12
|
+
const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
|
|
13
|
+
if (diff !== 0)
|
|
14
|
+
return diff;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
export async function checkForUpdate(current) {
|
|
19
|
+
const { stdout } = await run("npm", ["view", PACKAGE, "version"], { timeout: 15_000 });
|
|
20
|
+
const latest = stdout.trim();
|
|
21
|
+
// Not just !==: a locally-run, unreleased build can be ahead of what's published -- that's
|
|
22
|
+
// "up to date" too, not something to offer downgrading.
|
|
23
|
+
return { current, latest, upToDate: compareVersions(latest, current) <= 0 };
|
|
24
|
+
}
|
|
25
|
+
// Installs the latest published version globally, falling back to sudo the same way
|
|
26
|
+
// enterprise-install.ts and 'beam uninstall' do when the global node_modules isn't
|
|
27
|
+
// user-writable (e.g. a system Node install).
|
|
28
|
+
export async function installUpdate(version) {
|
|
29
|
+
await runWithSudoFallback("npm", ["install", "-g", `${PACKAGE}@${version}`], { timeout: 120_000 });
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.js","sourceRoot":"","sources":["../src/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAEnD,MAAM,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAChC,MAAM,OAAO,GAAG,kBAAkB,CAAC;AAInC,2FAA2F;AAC3F,gGAAgG;AAChG,SAAS,eAAe,CAAC,CAAS,EAAE,CAAS;IAC3C,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,MAAM,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACzC,IAAI,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAC9B,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAe;IAClD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IACvF,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;IAC7B,2FAA2F;IAC3F,wDAAwD;IACxD,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;AAC9E,CAAC;AAED,oFAAoF;AACpF,mFAAmF;AACnF,8CAA8C;AAC9C,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe;IACjD,MAAM,mBAAmB,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,IAAI,EAAE,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACrG,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-beam/beam",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "Local-first observation and heuristic risk scanning for AI coding agents (Claude Code, Codex, Cursor, Copilot CLI, Gemini). Redacts secrets, flags risky tool calls, scans skills and MCP configs offline.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai agent security",
|
|
@@ -39,9 +39,15 @@
|
|
|
39
39
|
"dist",
|
|
40
40
|
"Skills"
|
|
41
41
|
],
|
|
42
|
+
"pkg": {
|
|
43
|
+
"assets": [
|
|
44
|
+
"package.json"
|
|
45
|
+
]
|
|
46
|
+
},
|
|
42
47
|
"scripts": {
|
|
43
48
|
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
44
49
|
"build:watch": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
|
50
|
+
"build:binaries": "npm run build && node scripts/build-binaries.mjs",
|
|
45
51
|
"dev": "tsx src/cli.ts",
|
|
46
52
|
"test": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --runInBand",
|
|
47
53
|
"test:watch": "node --no-warnings --experimental-vm-modules node_modules/jest/bin/jest.js --watch",
|
|
@@ -63,6 +69,7 @@
|
|
|
63
69
|
"devDependencies": {
|
|
64
70
|
"@types/jest": "^30.0.0",
|
|
65
71
|
"@types/node": "^22.20.2",
|
|
72
|
+
"@yao-pkg/pkg": "^6.22.0",
|
|
66
73
|
"jest": "^30.2.0",
|
|
67
74
|
"ts-jest": "^29.4.5",
|
|
68
75
|
"tsx": "^4.19.2",
|