agentinel 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +44 -15
  2. package/dist/asen.js +217 -127
  3. package/package.json +3 -2
package/README.md CHANGED
@@ -42,11 +42,17 @@ Every other tool in this space guards your terminal. **Agentinel guards your age
42
42
 
43
43
  ## 🚀 Installation
44
44
 
45
- Install it as a dev-dependency in your repository:
45
+ For the best experience across all your projects and to use the short `asen` alias, install Agentinel globally:
46
46
 
47
+ ```sh
48
+ npm install -g agentinel
49
+ asen init
50
+ ```
51
+
52
+ Alternatively, you can install it as a dev-dependency per-project:
47
53
  ```sh
48
54
  npm install --save-dev agentinel
49
- npx asen init
55
+ npx agentinel init
50
56
  ```
51
57
  *(No account, no server, no complex configuration.)*
52
58
 
@@ -83,7 +89,9 @@ Agentinel provides an opt-in **PATH shim**. By default, running `npx asen init`
83
89
  npx asen init
84
90
  ```
85
91
 
86
- This puts a tiny, fail-open wrapper script earlier in your `PATH`. When you type `npm install`, the shim checks the package first. If it's safe, the real `npm` command runs instantly.
92
+ This puts a tiny, fail-open wrapper script earlier in your `PATH`. When you type `npm install <pkg>`, the shim checks the package first. If it's safe, the real `npm` command runs instantly.
93
+
94
+ As a bonus, if you just run a plain `npm install` with no arguments, the shim instantly checks your unstaged `package.json` for any newly added dependencies, ensuring that packages you pasted in are scanned before they resolve!
87
95
 
88
96
  **Terminal Example:**
89
97
  ```bash
@@ -151,22 +159,43 @@ Agentinel hard-blocks the install and returns an error payload to the agent. The
151
159
 
152
160
  ## 🧰 Command Reference
153
161
 
154
- Here are all the commands you can run via `npx asen <command>`:
162
+ You can run Agentinel using `npx agentinel <command>`.
163
+ If you have installed `agentinel` globally (`npm install -g agentinel`) or locally in your project, you can use the shorter alias: `npx asen <command>` (or just `asen <command>` if global).
164
+
165
+ Here are all the commands:
155
166
 
156
167
  ### `npx asen init [--no-shim]`
157
168
  Wires up agent hooks and git hooks in the current repo, and installs the global PATH shim for human terminal protection.
158
169
  ```bash
159
170
  $ npx asen init
160
- wrote .agentinel.json
161
- registered the Claude Code PreToolUse hook in .claude/settings.json
162
- installed the git pre-commit hook in .git/hooks
163
- wrote shims for npm, npx, pnpm, yarn, bun in /Users/user/.agentinel/bin
164
- added the shims to PATH in /Users/user/.zshrc
165
- Mode is warn, so a risky package typed at the terminal will be reported, not blocked.
166
- Open a new terminal, or run \`asen unshim\` to undo this.
167
171
 
168
- agentinel is set up. New npm packages will be checked before they land.
169
- Default mode is warn. Set "mode": "strict" in .agentinel.json to block instead.
172
+ ╭─ agentinel ──────────────────────────────╮
173
+ agentinel setup complete │
174
+ │ │
175
+ │ New npm packages will be checked before │
176
+ │ they land. │
177
+ │ │
178
+ │ ✔ wrote .agentinel.json │
179
+ │ ✔ registered the Claude Code PreToolUse │
180
+ │ hook in .claude/settings.json │
181
+ │ ✔ installed the git pre-commit hook in │
182
+ │ .git/hooks │
183
+ │ ✔ wrote shims for npm, npx, pnpm, yarn, │
184
+ │ bun in /Users/user/.agentinel/bin │
185
+ │ ✔ added the shims to PATH in │
186
+ │ /Users/user/.zshrc │
187
+ │ ✔ Open a new terminal, or run `asen │
188
+ │ unshim` to undo this. │
189
+ │ │
190
+ │ Default mode is strict. Set "mode": │
191
+ │ "warn" in .agentinel.json to only warn │
192
+ │ instead. │
193
+ ╰──────────────────────────────────────────╯
194
+
195
+ Performance Tip:
196
+ The hook runs on every command, and resolving through npx each time is slow.
197
+ For faster hooks, add it to the repo and run init again:
198
+ npm install --save-dev agentinel && npx asen init
170
199
  ```
171
200
 
172
201
  When used with `--no-shim`, it wires up hooks but skips installing the global PATH shim.
@@ -177,11 +206,11 @@ registered the Claude Code PreToolUse hook in .claude/settings.json
177
206
  installed the git pre-commit hook in .git/hooks
178
207
 
179
208
  agentinel is set up. New npm packages will be checked before they land.
180
- Default mode is warn. Set "mode": "strict" in .agentinel.json to block instead.
209
+ Default mode is strict. Set "mode": "warn" in .agentinel.json to only warn instead.
181
210
  ```
182
211
 
183
212
  ### `npx asen check [pkg...]`
184
- Scans the currently staged dependencies in your lockfile. Exits non-zero if flagged (great for CI/CD pipelines).
213
+ Scans the unstaged (or newly added) dependencies in your working tree, including the lockfile. Exits non-zero if flagged. (The Git pre-commit hook uses a strictly staged version of this check).
185
214
  ```bash
186
215
  $ npx asen check
187
216
  checked 142 package(s), nothing suspicious
package/dist/asen.js CHANGED
@@ -27,27 +27,68 @@ function readFlag(args, flag) {
27
27
 
28
28
  // src/checks/package-guard/staged-deps.ts
29
29
  import { execFileSync } from "child_process";
30
+ import { readFileSync, lstatSync } from "fs";
31
+ import { join, resolve, sep } from "path";
30
32
  var DEP_FIELDS = ["dependencies", "devDependencies", "optionalDependencies"];
31
33
  function newStagedDependencies(repoRoot) {
32
34
  const names = [];
33
- for (const path of stagedManifestPaths(repoRoot)) {
35
+ for (const path of changedManifestPaths(repoRoot, [
36
+ "diff",
37
+ "--cached",
38
+ "--name-only",
39
+ "-z",
40
+ "--diff-filter=ACMR"
41
+ ]) ?? []) {
34
42
  const staged = gitJson(repoRoot, `:${path}`);
35
- if (!staged) {
36
- continue;
37
- }
43
+ if (!staged) continue;
38
44
  const before = collectNames(gitJson(repoRoot, `HEAD:${path}`) ?? {});
39
45
  for (const name of collectNames(staged)) {
40
- if (!before.has(name) && !names.includes(name)) {
41
- names.push(name);
46
+ if (!before.has(name) && !names.includes(name)) names.push(name);
47
+ }
48
+ }
49
+ return names;
50
+ }
51
+ function newWorkingTreeDependencies(repoRoot) {
52
+ const names = [];
53
+ let paths = changedManifestPaths(repoRoot, [
54
+ "diff",
55
+ "HEAD",
56
+ "--name-only",
57
+ "-z",
58
+ "--diff-filter=ACMR"
59
+ ]);
60
+ if (paths === null) {
61
+ paths = changedManifestPaths(repoRoot, ["ls-files", "-z", "-c", "-o", "--exclude-standard"]) ?? [];
62
+ } else {
63
+ const untracked = changedManifestPaths(repoRoot, ["ls-files", "-z", "-o", "--exclude-standard"]) ?? [];
64
+ paths = Array.from(/* @__PURE__ */ new Set([...paths, ...untracked]));
65
+ }
66
+ for (const path of paths) {
67
+ let onDisk = null;
68
+ try {
69
+ const fullPath = resolve(join(repoRoot, path));
70
+ if (!fullPath.startsWith(resolve(repoRoot) + sep) && fullPath !== resolve(repoRoot)) {
71
+ continue;
42
72
  }
73
+ if (lstatSync(fullPath).isSymbolicLink()) {
74
+ continue;
75
+ }
76
+ onDisk = JSON.parse(readFileSync(fullPath, "utf8"));
77
+ } catch {
78
+ continue;
79
+ }
80
+ if (!onDisk) continue;
81
+ const before = collectNames(gitJson(repoRoot, `HEAD:${path}`) ?? {});
82
+ for (const name of collectNames(onDisk)) {
83
+ if (!before.has(name) && !names.includes(name)) names.push(name);
43
84
  }
44
85
  }
45
86
  return names;
46
87
  }
47
- function stagedManifestPaths(repoRoot) {
48
- const output = git(repoRoot, ["diff", "--cached", "--name-only", "-z", "--diff-filter=ACMR"]);
88
+ function changedManifestPaths(repoRoot, gitArgs) {
89
+ const output = git(repoRoot, gitArgs);
49
90
  if (output === null) {
50
- return [];
91
+ return null;
51
92
  }
52
93
  return output.split("\0").filter((path) => path === "package.json" || path.endsWith("/package.json")).filter((path) => !isInsideDependencies(path));
53
94
  }
@@ -96,14 +137,14 @@ function git(cwd, args) {
96
137
  }
97
138
 
98
139
  // src/config/load.ts
99
- import { readFileSync, writeFileSync, existsSync } from "fs";
100
- import { join } from "path";
140
+ import { readFileSync as readFileSync2, writeFileSync, existsSync } from "fs";
141
+ import { join as join2 } from "path";
101
142
 
102
143
  // src/config/schema.ts
103
144
  var CONFIG_FILENAME = ".agentinel.json";
104
145
  function defaultConfig() {
105
146
  return {
106
- mode: "warn",
147
+ mode: "strict",
107
148
  allow: [
108
149
  {
109
150
  name: "asen",
@@ -129,7 +170,7 @@ function parseConfig(raw, warn2 = () => {
129
170
  config.mode = source.mode;
130
171
  } else if (source.mode !== void 0) {
131
172
  warn2(
132
- `unknown mode ${JSON.stringify(source.mode)} in ${CONFIG_FILENAME}, falling back to "warn". Valid modes are "warn" and "strict".`
173
+ `unknown mode ${JSON.stringify(source.mode)} in ${CONFIG_FILENAME}, falling back to "strict". Valid modes are "warn" and "strict".`
133
174
  );
134
175
  }
135
176
  if (Array.isArray(source.allow)) {
@@ -155,7 +196,7 @@ function isAllowlisted(config, name) {
155
196
  var ConfigError = class extends Error {
156
197
  };
157
198
  function configPath(repoRoot) {
158
- return join(repoRoot, CONFIG_FILENAME);
199
+ return join2(repoRoot, CONFIG_FILENAME);
159
200
  }
160
201
  function loadConfig(repoRoot) {
161
202
  const path = configPath(repoRoot);
@@ -164,7 +205,7 @@ function loadConfig(repoRoot) {
164
205
  }
165
206
  let text;
166
207
  try {
167
- text = readFileSync(path, "utf8");
208
+ text = readFileSync2(path, "utf8");
168
209
  } catch (error) {
169
210
  throw new ConfigError(`could not read ${CONFIG_FILENAME}: ${describe(error)}`);
170
211
  }
@@ -304,8 +345,8 @@ function describeFailure(error) {
304
345
  }
305
346
 
306
347
  // src/checks/package-guard/malware.ts
307
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
308
- import { dirname, join as join2 } from "path";
348
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
349
+ import { dirname, join as join3 } from "path";
309
350
  import { fileURLToPath } from "url";
310
351
  import { gunzipSync } from "zlib";
311
352
  var cache = null;
@@ -331,7 +372,7 @@ function load() {
331
372
  return {};
332
373
  }
333
374
  try {
334
- const raw = gunzipSync(readFileSync2(path)).toString("utf8");
375
+ const raw = gunzipSync(readFileSync3(path)).toString("utf8");
335
376
  const parsed = JSON.parse(raw);
336
377
  return typeof parsed === "object" && parsed !== null ? parsed : {};
337
378
  } catch {
@@ -341,9 +382,9 @@ function load() {
341
382
  function listPath() {
342
383
  const here = dirname(fileURLToPath(import.meta.url));
343
384
  const candidates = [
344
- join2(here, "..", "data", "malware-names.json.gz"),
345
- join2(here, "..", "..", "data", "malware-names.json.gz"),
346
- join2(here, "..", "..", "..", "data", "malware-names.json.gz")
385
+ join3(here, "..", "data", "malware-names.json.gz"),
386
+ join3(here, "..", "..", "data", "malware-names.json.gz"),
387
+ join3(here, "..", "..", "..", "data", "malware-names.json.gz")
347
388
  ];
348
389
  return candidates.find((path) => existsSync2(path)) ?? null;
349
390
  }
@@ -735,15 +776,18 @@ function scanForKnownMalware(tree, config) {
735
776
 
736
777
  // src/checks/package-guard/lockfile.ts
737
778
  import { execFileSync as execFileSync2 } from "child_process";
738
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
739
- import { join as join3 } from "path";
779
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
780
+ import { join as join4 } from "path";
740
781
  function packagesInLockfile(repoRoot) {
741
- const path = join3(repoRoot, "package-lock.json");
782
+ const path = join4(repoRoot, "package-lock.json");
742
783
  if (!existsSync3(path)) {
743
784
  return [];
744
785
  }
745
786
  return packagesInLockText(readFileOrNull(path));
746
787
  }
788
+ function workingTreeLockfilePackages(repoRoot) {
789
+ return packagesInLockfile(repoRoot);
790
+ }
747
791
  function stagedLockfilePackages(repoRoot) {
748
792
  let text = null;
749
793
  try {
@@ -778,7 +822,7 @@ function packagesInLockText(text) {
778
822
  }
779
823
  function readFileOrNull(path) {
780
824
  try {
781
- return readFileSync3(path, "utf8");
825
+ return readFileSync4(path, "utf8");
782
826
  } catch {
783
827
  return null;
784
828
  }
@@ -818,7 +862,10 @@ function collectFromDependencies(lock, found) {
818
862
  var RESET = "\x1B[0m";
819
863
  var YELLOW = "\x1B[33m";
820
864
  var RED = "\x1B[31m";
865
+ var GREEN = "\x1B[32m";
866
+ var CYAN = "\x1B[36m";
821
867
  var DIM = "\x1B[2m";
868
+ var BOLD = "\x1B[1m";
822
869
  var WIDTH = 46;
823
870
  var TITLE = "agentinel";
824
871
  function supportsColor(stream = process.stdout) {
@@ -908,6 +955,39 @@ function draw(b, paint) {
908
955
  return b.action ? `${box}
909
956
  ${paint(DIM, b.action)}` : box;
910
957
  }
958
+ function drawSuccessBox(heading, body, stream = process.stdout) {
959
+ const inner = WIDTH - 4;
960
+ const text = inner - 1;
961
+ const hasColor = supportsColor(stream);
962
+ const color = hasColor ? GREEN : "";
963
+ const paint = (code, str) => hasColor ? `${code}${str}${RESET}` : str;
964
+ const titlePart = hasColor ? `${BOLD}${TITLE}${RESET}${color}` : TITLE;
965
+ const top = paint(color, `\u256D\u2500 ${titlePart} ${"\u2500".repeat(WIDTH - TITLE.length - 5)}\u256E`);
966
+ const bottom = paint(color, `\u2570${"\u2500".repeat(WIDTH - 2)}\u256F`);
967
+ const rows = [];
968
+ const push = (line, styled) => {
969
+ const edge = paint(color, "\u2502");
970
+ const plainLine = (styled ?? line).replace(/\x1b\[[0-9;]*m/g, "");
971
+ const pad = " ".repeat(Math.max(0, inner - plainLine.length));
972
+ rows.push(`${edge} ${styled ?? line}${pad}${edge}`);
973
+ };
974
+ push(heading, paint(BOLD, heading));
975
+ push("");
976
+ for (const line of body) {
977
+ if (line === "") {
978
+ push("");
979
+ continue;
980
+ }
981
+ if (line.includes("\x1B")) {
982
+ push(line, line);
983
+ } else {
984
+ for (const wrapped of wrap(line, text)) {
985
+ push(wrapped);
986
+ }
987
+ }
988
+ }
989
+ return [top, ...rows, bottom].join("\n");
990
+ }
911
991
  function wrap(line, width) {
912
992
  const lines = [];
913
993
  let current = "";
@@ -981,8 +1061,8 @@ async function runCheck(names) {
981
1061
  named = names;
982
1062
  tree = [];
983
1063
  } else {
984
- named = newStagedDependencies(repoRoot);
985
- tree = stagedLockfilePackages(repoRoot).filter((entry) => !named.includes(entry.name));
1064
+ named = newWorkingTreeDependencies(repoRoot);
1065
+ tree = workingTreeLockfilePackages(repoRoot).filter((entry) => !named.includes(entry.name));
986
1066
  }
987
1067
  if (named.length === 0 && tree.length === 0) {
988
1068
  console.log("no new packages to check");
@@ -1020,9 +1100,9 @@ async function runCheck(names) {
1020
1100
 
1021
1101
  // src/commands/init.ts
1022
1102
  import { execFileSync as execFileSync3 } from "child_process";
1023
- import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
1103
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
1024
1104
  import { homedir as homedir2 } from "os";
1025
- import { isAbsolute, join as join5 } from "path";
1105
+ import { isAbsolute, join as join6 } from "path";
1026
1106
 
1027
1107
  // src/platform.ts
1028
1108
  function isWindows() {
@@ -1033,9 +1113,9 @@ function npmCommand() {
1033
1113
  }
1034
1114
 
1035
1115
  // src/commands/shim.ts
1036
- import { chmodSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync4, rmSync, writeFileSync as writeFileSync2 } from "fs";
1116
+ import { chmodSync, existsSync as existsSync4, mkdirSync, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync2 } from "fs";
1037
1117
  import { homedir } from "os";
1038
- import { join as join4 } from "path";
1118
+ import { join as join5 } from "path";
1039
1119
  var CLIENTS = ["npm", "npx", "pnpm", "yarn", "bun"];
1040
1120
  var BLOCK_EXIT_CODE = 2;
1041
1121
  var RC_MARKER = "# agentinel";
@@ -1043,7 +1123,7 @@ function currentTarget() {
1043
1123
  return { home: homedir(), shell: process.env.SHELL ?? "", platform: process.platform };
1044
1124
  }
1045
1125
  function shimDirectory(home) {
1046
- return join4(home, ".agentinel", "bin");
1126
+ return join5(home, ".agentinel", "bin");
1047
1127
  }
1048
1128
  function onWindows(target) {
1049
1129
  return target.platform === "win32";
@@ -1053,33 +1133,29 @@ function shimFileName(client, target) {
1053
1133
  }
1054
1134
  function startupFilePath(target) {
1055
1135
  if (onWindows(target)) {
1056
- return join4(target.home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
1136
+ return join5(target.home, "Documents", "PowerShell", "Microsoft.PowerShell_profile.ps1");
1057
1137
  }
1058
1138
  if (target.shell.includes("zsh")) {
1059
- return join4(target.home, ".zshrc");
1139
+ return join5(target.home, ".zshrc");
1060
1140
  }
1061
1141
  if (target.shell.includes("bash")) {
1062
- return join4(target.home, ".bashrc");
1142
+ return join5(target.home, ".bashrc");
1063
1143
  }
1064
- return join4(target.home, ".profile");
1144
+ return join5(target.home, ".profile");
1065
1145
  }
1066
- function installShim(repoRoot, target = currentTarget()) {
1146
+ function installShim(repoRoot, target = currentTarget(), messages = []) {
1067
1147
  const dir = shimDirectory(target.home);
1068
1148
  mkdirSync(dir, { recursive: true });
1069
1149
  for (const client of CLIENTS) {
1070
- const path = join4(dir, shimFileName(client, target));
1150
+ const path = join5(dir, shimFileName(client, target));
1071
1151
  writeFileSync2(path, shimScript(client, target), "utf8");
1072
1152
  if (!onWindows(target)) {
1073
1153
  chmodSync(path, 493);
1074
1154
  }
1075
1155
  }
1076
- console.log(`wrote shims for ${CLIENTS.join(", ")} in ${dir}`);
1077
- addPathLine(target);
1078
- const mode = loadConfig(repoRoot).mode;
1079
- console.log(
1080
- mode === "strict" ? "Mode is strict, so a risky package typed at the terminal will be blocked." : "Mode is warn, so a risky package typed at the terminal will be reported, not blocked."
1081
- );
1082
- console.log("Open a new terminal, or run `asen unshim` to undo this.");
1156
+ messages.push(`wrote shims for ${CLIENTS.join(", ")} in ${dir}`);
1157
+ addPathLine(target, messages);
1158
+ messages.push("Open a new terminal, or run `asen unshim` to undo this.");
1083
1159
  return 0;
1084
1160
  }
1085
1161
  function removeShim(target = currentTarget()) {
@@ -1097,24 +1173,24 @@ function pathLine(target) {
1097
1173
  return `export PATH="$HOME/.agentinel/bin:$PATH" ${RC_MARKER}
1098
1174
  `;
1099
1175
  }
1100
- function addPathLine(target) {
1176
+ function addPathLine(target, messages) {
1101
1177
  const path = startupFilePath(target);
1102
- const existing = existsSync4(path) ? readFileSync4(path, "utf8") : "";
1178
+ const existing = existsSync4(path) ? readFileSync5(path, "utf8") : "";
1103
1179
  if (existing.includes(RC_MARKER)) {
1104
- console.log(`${path} already puts the shims on PATH, left alone`);
1180
+ messages.push(`${path} already puts the shims on PATH, left alone`);
1105
1181
  return;
1106
1182
  }
1107
- mkdirSync(join4(path, ".."), { recursive: true });
1183
+ mkdirSync(join5(path, ".."), { recursive: true });
1108
1184
  const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
1109
1185
  writeFileSync2(path, `${existing}${separator}${pathLine(target)}`, "utf8");
1110
- console.log(`added the shims to PATH in ${path}`);
1186
+ messages.push(`added the shims to PATH in ${path}`);
1111
1187
  }
1112
1188
  function removePathLine(target) {
1113
1189
  const path = startupFilePath(target);
1114
1190
  if (!existsSync4(path)) {
1115
1191
  return;
1116
1192
  }
1117
- const lines = readFileSync4(path, "utf8").split("\n");
1193
+ const lines = readFileSync5(path, "utf8").split("\n");
1118
1194
  const kept = lines.filter((line) => !line.includes(RC_MARKER));
1119
1195
  if (kept.length === lines.length) {
1120
1196
  return;
@@ -1224,12 +1300,15 @@ async function runCheckCommand(command) {
1224
1300
  if (!command) {
1225
1301
  return 0;
1226
1302
  }
1227
- const { installs, executes } = parseCommand(command);
1303
+ const { installs, executes, lockfile } = parseCommand(command);
1304
+ const repoRoot = repoRootOrCwd();
1228
1305
  const names = [.../* @__PURE__ */ new Set([...installs, ...executes])];
1229
1306
  if (names.length === 0) {
1230
- return 0;
1307
+ if (!lockfile) return 0;
1308
+ const workingTree = newWorkingTreeDependencies(repoRoot);
1309
+ if (workingTree.length === 0) return 0;
1310
+ names.push(...workingTree);
1231
1311
  }
1232
- const repoRoot = repoRootOrCwd();
1233
1312
  const config = loadConfig(repoRoot);
1234
1313
  const verdicts = await checkPackages(names, config);
1235
1314
  for (const verdict of verdicts) {
@@ -1253,24 +1332,35 @@ var HOOK_MARKER = "agentinel";
1253
1332
  var HOOK_SUBCOMMAND = "hook claude-code";
1254
1333
  function runInit(options = {}) {
1255
1334
  const repoRoot = repoRootOrCwd();
1256
- writeConfig(repoRoot);
1257
- wireClaudeCodeHook(repoRoot, claudeCodeCommand(repoRoot));
1258
- wireOtherAgents(repoRoot, agentHookCommand(repoRoot));
1259
- wirePreCommitHook(repoRoot, gitHookCommand(repoRoot));
1335
+ const messages = [];
1336
+ writeConfig(repoRoot, messages);
1337
+ wireClaudeCodeHook(repoRoot, claudeCodeCommand(repoRoot), messages);
1338
+ wireOtherAgents(repoRoot, agentHookCommand(repoRoot), messages);
1339
+ wirePreCommitHook(repoRoot, gitHookCommand(repoRoot), messages);
1260
1340
  if (options.shim) {
1261
- installShim(repoRoot, options.shimTarget);
1262
- }
1263
- console.log("\nagentinel is set up. New npm packages will be checked before they land.");
1264
- console.log('Default mode is warn. Set "mode": "strict" in .agentinel.json to block instead.');
1341
+ installShim(repoRoot, options.shimTarget, messages);
1342
+ }
1343
+ const successBox = drawSuccessBox("agentinel setup complete", [
1344
+ "New npm packages will be checked before they land.",
1345
+ "",
1346
+ ...messages.map((m) => `\u2714 ${m}`),
1347
+ "",
1348
+ `Default mode is ${GREEN}strict${RESET}.`,
1349
+ 'Set "mode": "warn" in .agentinel.json to only warn instead.'
1350
+ ]);
1351
+ console.log(`
1352
+ ${successBox}`);
1265
1353
  if (!hasLocalInstall(repoRoot)) {
1266
- console.log("\nThe hook runs on every command, and going through npx each time is slow.");
1267
- console.log("For faster hooks, add it to the repo and run init again:");
1268
- console.log(" npm install --save-dev agentinel && npx asen init");
1354
+ console.log(`
1355
+ ${CYAN}Performance Tip:${RESET}`);
1356
+ console.log(`The hook runs on every command, and resolving through npx each time is slow.`);
1357
+ console.log(`For faster hooks, add it to the repo and run init again:`);
1358
+ console.log(` ${DIM}npm install --save-dev agentinel && npx asen init${RESET}`);
1269
1359
  }
1270
1360
  return 0;
1271
1361
  }
1272
1362
  function hasLocalInstall(repoRoot) {
1273
- return existsSync5(join5(repoRoot, "node_modules", ".bin", "asen"));
1363
+ return existsSync5(join6(repoRoot, "node_modules", ".bin", "asen"));
1274
1364
  }
1275
1365
  function claudeCodeCommand(repoRoot) {
1276
1366
  if (isWindows()) {
@@ -1281,34 +1371,34 @@ function claudeCodeCommand(repoRoot) {
1281
1371
  function gitHookCommand(repoRoot) {
1282
1372
  return hasLocalInstall(repoRoot) ? "./node_modules/.bin/asen" : "npx agentinel";
1283
1373
  }
1284
- function writeConfig(repoRoot) {
1374
+ function writeConfig(repoRoot, messages) {
1285
1375
  const path = configPath(repoRoot);
1286
1376
  if (existsSync5(path)) {
1287
- console.log(".agentinel.json already exists, left alone");
1377
+ messages.push(".agentinel.json already exists, left alone");
1288
1378
  return;
1289
1379
  }
1290
1380
  saveConfig(repoRoot, defaultConfig());
1291
- console.log("wrote .agentinel.json");
1381
+ messages.push("wrote .agentinel.json");
1292
1382
  }
1293
- function wireClaudeCodeHook(repoRoot, command) {
1294
- const dir = join5(repoRoot, ".claude");
1295
- const path = join5(dir, "settings.json");
1383
+ function wireClaudeCodeHook(repoRoot, command, messages) {
1384
+ const dir = join6(repoRoot, ".claude");
1385
+ const path = join6(dir, "settings.json");
1296
1386
  let settings = {};
1297
1387
  if (existsSync5(path)) {
1298
1388
  try {
1299
- const parsed = JSON.parse(readFileSync5(path, "utf8"));
1389
+ const parsed = JSON.parse(readFileSync6(path, "utf8"));
1300
1390
  if (typeof parsed === "object" && parsed !== null) {
1301
1391
  settings = parsed;
1302
1392
  }
1303
1393
  } catch {
1304
- console.log(".claude/settings.json is not valid JSON, skipping the Claude Code hook");
1394
+ messages.push(".claude/settings.json is not valid JSON, skipping the Claude Code hook");
1305
1395
  return;
1306
1396
  }
1307
1397
  }
1308
1398
  const hooks = asRecord2(settings.hooks) ?? {};
1309
1399
  const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
1310
1400
  if (alreadyRegistered(preToolUse)) {
1311
- console.log("Claude Code hook already registered, left alone");
1401
+ messages.push("Claude Code hook already registered, left alone");
1312
1402
  return;
1313
1403
  }
1314
1404
  preToolUse.push({
@@ -1319,7 +1409,7 @@ function wireClaudeCodeHook(repoRoot, command) {
1319
1409
  settings.hooks = hooks;
1320
1410
  mkdirSync2(dir, { recursive: true });
1321
1411
  writeFileSync3(path, JSON.stringify(settings, null, 2) + "\n", "utf8");
1322
- console.log("registered the Claude Code PreToolUse hook in .claude/settings.json");
1412
+ messages.push("registered the Claude Code PreToolUse hook in .claude/settings.json");
1323
1413
  }
1324
1414
  function agentHookCommand(repoRoot) {
1325
1415
  if (isWindows()) {
@@ -1327,31 +1417,31 @@ function agentHookCommand(repoRoot) {
1327
1417
  }
1328
1418
  return hasLocalInstall(repoRoot) ? '"$(git rev-parse --show-toplevel)"/node_modules/.bin/asen' : "npx agentinel";
1329
1419
  }
1330
- function wireOtherAgents(repoRoot, command) {
1420
+ function wireOtherAgents(repoRoot, command, messages) {
1331
1421
  if (uses(repoRoot, ".codex")) {
1332
- wireCodexHook(repoRoot, command);
1422
+ wireCodexHook(repoRoot, command, messages);
1333
1423
  }
1334
- if (uses(repoRoot, ".copilot") || existsSync5(join5(repoRoot, ".github", "hooks"))) {
1335
- wireCopilotHook(repoRoot, command);
1424
+ if (uses(repoRoot, ".copilot") || existsSync5(join6(repoRoot, ".github", "hooks"))) {
1425
+ wireCopilotHook(repoRoot, command, messages);
1336
1426
  }
1337
1427
  if (uses(repoRoot, ".gemini")) {
1338
- wireGeminiHook(repoRoot, command);
1428
+ wireGeminiHook(repoRoot, command, messages);
1339
1429
  }
1340
1430
  }
1341
1431
  function uses(repoRoot, dir) {
1342
- return existsSync5(join5(repoRoot, dir)) || existsSync5(join5(homedir2(), dir));
1432
+ return existsSync5(join6(repoRoot, dir)) || existsSync5(join6(homedir2(), dir));
1343
1433
  }
1344
- function wireCodexHook(repoRoot, command) {
1345
- const path = join5(repoRoot, ".codex", "hooks.json");
1434
+ function wireCodexHook(repoRoot, command, messages) {
1435
+ const path = join6(repoRoot, ".codex", "hooks.json");
1346
1436
  const file = readJson(path);
1347
1437
  if (file === null) {
1348
- console.log(".codex/hooks.json is not valid JSON, skipping the Codex hook");
1438
+ messages.push(".codex/hooks.json is not valid JSON, skipping the Codex hook");
1349
1439
  return;
1350
1440
  }
1351
1441
  const hooks = asRecord2(file.hooks) ?? {};
1352
1442
  const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse : [];
1353
1443
  if (registers(preToolUse, "codex")) {
1354
- console.log("Codex hook already registered, left alone");
1444
+ messages.push("Codex hook already registered, left alone");
1355
1445
  return;
1356
1446
  }
1357
1447
  preToolUse.push({
@@ -1361,19 +1451,19 @@ function wireCodexHook(repoRoot, command) {
1361
1451
  hooks.PreToolUse = preToolUse;
1362
1452
  file.hooks = hooks;
1363
1453
  writeJson(path, file);
1364
- console.log("registered the Codex PreToolUse hook in .codex/hooks.json");
1454
+ messages.push("registered the Codex PreToolUse hook in .codex/hooks.json");
1365
1455
  }
1366
- function wireCopilotHook(repoRoot, command) {
1367
- const path = join5(repoRoot, ".github", "hooks", "agentinel.json");
1456
+ function wireCopilotHook(repoRoot, command, messages) {
1457
+ const path = join6(repoRoot, ".github", "hooks", "agentinel.json");
1368
1458
  const file = readJson(path);
1369
1459
  if (file === null) {
1370
- console.log(".github/hooks/agentinel.json is not valid JSON, skipping the Copilot hook");
1460
+ messages.push(".github/hooks/agentinel.json is not valid JSON, skipping the Copilot hook");
1371
1461
  return;
1372
1462
  }
1373
1463
  const hooks = asRecord2(file.hooks) ?? {};
1374
1464
  const preToolUse = Array.isArray(hooks.preToolUse) ? hooks.preToolUse : [];
1375
1465
  if (registers(preToolUse, "copilot")) {
1376
- console.log("Copilot hook already registered, left alone");
1466
+ messages.push("Copilot hook already registered, left alone");
1377
1467
  return;
1378
1468
  }
1379
1469
  preToolUse.push({ type: "command", matcher: "bash", bash: `${command} hook copilot` });
@@ -1381,19 +1471,19 @@ function wireCopilotHook(repoRoot, command) {
1381
1471
  file.version = 1;
1382
1472
  file.hooks = hooks;
1383
1473
  writeJson(path, file);
1384
- console.log("registered the Copilot preToolUse hook in .github/hooks/agentinel.json");
1474
+ messages.push("registered the Copilot preToolUse hook in .github/hooks/agentinel.json");
1385
1475
  }
1386
- function wireGeminiHook(repoRoot, command) {
1387
- const path = join5(repoRoot, ".gemini", "settings.json");
1476
+ function wireGeminiHook(repoRoot, command, messages) {
1477
+ const path = join6(repoRoot, ".gemini", "settings.json");
1388
1478
  const file = readJson(path);
1389
1479
  if (file === null) {
1390
- console.log(".gemini/settings.json is not valid JSON, skipping the Gemini hook");
1480
+ messages.push(".gemini/settings.json is not valid JSON, skipping the Gemini hook");
1391
1481
  return;
1392
1482
  }
1393
1483
  const hooks = asRecord2(file.hooks) ?? {};
1394
1484
  const beforeTool = Array.isArray(hooks.BeforeTool) ? hooks.BeforeTool : [];
1395
1485
  if (registers(beforeTool, "gemini")) {
1396
- console.log("Gemini hook already registered, left alone");
1486
+ messages.push("Gemini hook already registered, left alone");
1397
1487
  return;
1398
1488
  }
1399
1489
  beforeTool.push({
@@ -1403,20 +1493,20 @@ function wireGeminiHook(repoRoot, command) {
1403
1493
  hooks.BeforeTool = beforeTool;
1404
1494
  file.hooks = hooks;
1405
1495
  writeJson(path, file);
1406
- console.log("registered the Gemini BeforeTool hook in .gemini/settings.json");
1496
+ messages.push("registered the Gemini BeforeTool hook in .gemini/settings.json");
1407
1497
  }
1408
1498
  function readJson(path) {
1409
1499
  if (!existsSync5(path)) {
1410
1500
  return {};
1411
1501
  }
1412
1502
  try {
1413
- return asRecord2(JSON.parse(readFileSync5(path, "utf8"))) ?? {};
1503
+ return asRecord2(JSON.parse(readFileSync6(path, "utf8"))) ?? {};
1414
1504
  } catch {
1415
1505
  return null;
1416
1506
  }
1417
1507
  }
1418
1508
  function writeJson(path, value) {
1419
- mkdirSync2(join5(path, ".."), { recursive: true });
1509
+ mkdirSync2(join6(path, ".."), { recursive: true });
1420
1510
  writeFileSync3(path, JSON.stringify(value, null, 2) + "\n", "utf8");
1421
1511
  }
1422
1512
  function registers(entries, kind) {
@@ -1436,40 +1526,40 @@ function git2(repoRoot, args) {
1436
1526
  function hooksDirectory(repoRoot) {
1437
1527
  const configured = git2(repoRoot, ["config", "--get", "core.hooksPath"]);
1438
1528
  if (configured) {
1439
- return isAbsolute(configured) ? configured : join5(repoRoot, configured);
1529
+ return isAbsolute(configured) ? configured : join6(repoRoot, configured);
1440
1530
  }
1441
1531
  const path = git2(repoRoot, ["rev-parse", "--git-path", "hooks"]);
1442
1532
  if (path) {
1443
- return isAbsolute(path) ? path : join5(repoRoot, path);
1533
+ return isAbsolute(path) ? path : join6(repoRoot, path);
1444
1534
  }
1445
- return join5(repoRoot, ".git", "hooks");
1535
+ return join6(repoRoot, ".git", "hooks");
1446
1536
  }
1447
- function wirePreCommitHook(repoRoot, command) {
1448
- if (!existsSync5(join5(repoRoot, ".git"))) {
1449
- console.log("not a git repo, skipping the pre-commit hook");
1537
+ function wirePreCommitHook(repoRoot, command, messages) {
1538
+ if (!existsSync5(join6(repoRoot, ".git"))) {
1539
+ messages.push("not a git repo, skipping the pre-commit hook");
1450
1540
  return;
1451
1541
  }
1452
1542
  const dir = hooksDirectory(repoRoot);
1453
- const path = join5(dir, "pre-commit");
1543
+ const path = join6(dir, "pre-commit");
1454
1544
  const script = `#!/bin/sh
1455
1545
  # ${HOOK_MARKER}: check newly added npm packages before they get committed
1456
1546
  ${command} hook pre-commit
1457
1547
  `;
1458
1548
  if (existsSync5(path)) {
1459
- const existing = readFileSync5(path, "utf8");
1549
+ const existing = readFileSync6(path, "utf8");
1460
1550
  if (existing.includes(HOOK_MARKER)) {
1461
- console.log("pre-commit hook already installed, left alone");
1551
+ messages.push("pre-commit hook already installed, left alone");
1462
1552
  return;
1463
1553
  }
1464
- console.log("a pre-commit hook already exists, not overwriting it");
1465
- console.log(`add this line to ${path}:
1554
+ messages.push("a pre-commit hook already exists, not overwriting it");
1555
+ messages.push(`add this line to ${path}:
1466
1556
  ${command} hook pre-commit`);
1467
1557
  return;
1468
1558
  }
1469
1559
  mkdirSync2(dir, { recursive: true });
1470
1560
  writeFileSync3(path, script, "utf8");
1471
1561
  chmodSync2(path, 493);
1472
- console.log(`installed the git pre-commit hook in ${dir}`);
1562
+ messages.push(`installed the git pre-commit hook in ${dir}`);
1473
1563
  }
1474
1564
  function alreadyRegistered(preToolUse) {
1475
1565
  return JSON.stringify(preToolUse).includes(`${HOOK_SUBCOMMAND}`);
@@ -1497,8 +1587,8 @@ function runMode(targetMode) {
1497
1587
  }
1498
1588
 
1499
1589
  // src/commands/uninstall.ts
1500
- import { existsSync as existsSync6, readFileSync as readFileSync6, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
1501
- import { join as join6 } from "path";
1590
+ import { existsSync as existsSync6, readFileSync as readFileSync7, rmSync as rmSync2, writeFileSync as writeFileSync4 } from "fs";
1591
+ import { join as join7 } from "path";
1502
1592
  var HOOK_MARKER2 = "agentinel";
1503
1593
  var HOOK_SUBCOMMAND2 = "hook claude-code";
1504
1594
  function runUninstall() {
@@ -1513,7 +1603,7 @@ function runUninstall() {
1513
1603
  return 0;
1514
1604
  }
1515
1605
  function unwireClaudeCodeHook(repoRoot) {
1516
- const path = join6(repoRoot, ".claude", "settings.json");
1606
+ const path = join7(repoRoot, ".claude", "settings.json");
1517
1607
  if (!existsSync6(path)) return;
1518
1608
  const file = readJson2(path);
1519
1609
  if (file === null) return;
@@ -1531,7 +1621,7 @@ function unwireClaudeCodeHook(repoRoot) {
1531
1621
  console.log("removed Claude Code hook");
1532
1622
  }
1533
1623
  function unwireCodexHook(repoRoot) {
1534
- const path = join6(repoRoot, ".codex", "hooks.json");
1624
+ const path = join7(repoRoot, ".codex", "hooks.json");
1535
1625
  if (!existsSync6(path)) return;
1536
1626
  const file = readJson2(path);
1537
1627
  if (file === null) return;
@@ -1543,7 +1633,7 @@ function unwireCodexHook(repoRoot) {
1543
1633
  console.log("removed Codex hook");
1544
1634
  }
1545
1635
  function unwireCopilotHook(repoRoot) {
1546
- const path = join6(repoRoot, ".github", "hooks", "agentinel.json");
1636
+ const path = join7(repoRoot, ".github", "hooks", "agentinel.json");
1547
1637
  if (!existsSync6(path)) return;
1548
1638
  const file = readJson2(path);
1549
1639
  if (file === null) return;
@@ -1555,7 +1645,7 @@ function unwireCopilotHook(repoRoot) {
1555
1645
  console.log("removed Copilot hook");
1556
1646
  }
1557
1647
  function unwireGeminiHook(repoRoot) {
1558
- const path = join6(repoRoot, ".gemini", "settings.json");
1648
+ const path = join7(repoRoot, ".gemini", "settings.json");
1559
1649
  if (!existsSync6(path)) return;
1560
1650
  const file = readJson2(path);
1561
1651
  if (file === null) return;
@@ -1567,17 +1657,17 @@ function unwireGeminiHook(repoRoot) {
1567
1657
  console.log("removed Gemini hook");
1568
1658
  }
1569
1659
  function unwirePreCommitHook(repoRoot) {
1570
- if (!existsSync6(join6(repoRoot, ".git"))) return;
1571
- const path = join6(hooksDirectory(repoRoot), "pre-commit");
1660
+ if (!existsSync6(join7(repoRoot, ".git"))) return;
1661
+ const path = join7(hooksDirectory(repoRoot), "pre-commit");
1572
1662
  if (!existsSync6(path)) return;
1573
- const existing = readFileSync6(path, "utf8");
1663
+ const existing = readFileSync7(path, "utf8");
1574
1664
  if (!existing.includes(HOOK_MARKER2)) return;
1575
1665
  rmSync2(path);
1576
1666
  console.log("removed git pre-commit hook");
1577
1667
  }
1578
1668
  function readJson2(path) {
1579
1669
  try {
1580
- return asRecord3(JSON.parse(readFileSync6(path, "utf8"))) ?? {};
1670
+ return asRecord3(JSON.parse(readFileSync7(path, "utf8"))) ?? {};
1581
1671
  } catch {
1582
1672
  return null;
1583
1673
  }
@@ -1591,7 +1681,7 @@ function asRecord3(value) {
1591
1681
 
1592
1682
  // src/hooks/claude-code.ts
1593
1683
  import { existsSync as existsSync7 } from "fs";
1594
- import { join as join7 } from "path";
1684
+ import { join as join8 } from "path";
1595
1685
 
1596
1686
  // src/checks/package-guard/resolve.ts
1597
1687
  import { execFileSync as execFileSync4 } from "child_process";
@@ -1675,7 +1765,7 @@ function candidatesFor(command, repoRoot) {
1675
1765
  }
1676
1766
  function isLocalTool(repoRoot, name) {
1677
1767
  const binary = name.includes("/") ? name.split("/").pop() : name;
1678
- return existsSync7(join7(repoRoot, "node_modules", ".bin", binary));
1768
+ return existsSync7(join8(repoRoot, "node_modules", ".bin", binary));
1679
1769
  }
1680
1770
  function warn(verdicts) {
1681
1771
  const summary = plainSummary(verdicts);
@@ -1889,14 +1979,14 @@ async function runPreCommitHook() {
1889
1979
  }
1890
1980
 
1891
1981
  // bin/asen.ts
1892
- import { readFileSync as readFileSync7 } from "fs";
1893
- import { dirname as dirname2, join as join8 } from "path";
1982
+ import { readFileSync as readFileSync8 } from "fs";
1983
+ import { dirname as dirname2, join as join9 } from "path";
1894
1984
  import { fileURLToPath as fileURLToPath2 } from "url";
1895
1985
  function getVersion() {
1896
1986
  try {
1897
1987
  const file = fileURLToPath2(import.meta.url);
1898
- const pkgPath = join8(dirname2(file), "..", "package.json");
1899
- const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
1988
+ const pkgPath = join9(dirname2(file), "..", "package.json");
1989
+ const pkg = JSON.parse(readFileSync8(pkgPath, "utf8"));
1900
1990
  return typeof pkg.version === "string" ? pkg.version : "unknown";
1901
1991
  } catch {
1902
1992
  return "unknown";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentinel",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Catches AI hallucinated npm packages before an agent installs them",
5
5
  "license": "MIT",
6
6
  "author": "Aman Janwani",
@@ -14,7 +14,8 @@
14
14
  },
15
15
  "homepage": "https://github.com/aman-janwani/agentinel#readme",
16
16
  "bin": {
17
- "asen": "./dist/asen.js"
17
+ "asen": "./dist/asen.js",
18
+ "agentinel": "./dist/asen.js"
18
19
  },
19
20
  "files": [
20
21
  "dist",