@chainpatrol/cli 0.6.0 → 0.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c170343: Add `chainpatrol setup --cloud` flag that also installs a Claude Code
8
+ SessionStart hook at `~/.claude/hooks/chainpatrol-login.sh` and registers
9
+ it in `~/.claude/settings.json`. When the user has no chainpatrol
10
+ credentials, the hook emits SessionStart `additionalContext` so Claude
11
+ surfaces the device-code login URL on the first user turn — useful for
12
+ cloud Claude Code environments that can't run the interactive login from
13
+ a container startup script. The hook is a silent no-op once the user is
14
+ authenticated. Local installs (without `--cloud`) leave Claude Code
15
+ hooks untouched. `chainpatrol uninstall` always removes the hook if
16
+ present, without disturbing unrelated hooks or settings.
17
+
3
18
  ## 0.6.0
4
19
 
5
20
  ### Minor Changes
@@ -4,13 +4,125 @@ import {
4
4
  } from "./chunk-IUZB3DQW.js";
5
5
 
6
6
  // src/commands/setup-skill.ts
7
- import { mkdirSync, writeFileSync, existsSync, readFileSync as readFileSync2, rmSync } from "fs";
8
- import { join } from "path";
7
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
8
+ import { join as join2 } from "path";
9
+ import { homedir as homedir2 } from "os";
10
+
11
+ // src/commands/login-hook.ts
12
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, unlinkSync } from "fs";
9
13
  import { homedir } from "os";
14
+ import { join, dirname } from "path";
15
+ var CLAUDE_DIR = join(homedir(), ".claude");
16
+ var HOOK_DIR = join(CLAUDE_DIR, "hooks");
17
+ var HOOK_FILE = join(HOOK_DIR, "chainpatrol-login.sh");
18
+ var SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
19
+ var HOOK_SCRIPT = `#!/bin/bash
20
+ # chainpatrol auto-login hook \u2014 installed by \`chainpatrol setup\`.
21
+ # On Claude Code session start, if the user has no chainpatrol credentials,
22
+ # emit additionalContext so Claude surfaces the device-code login URL on
23
+ # the first user turn. Safe to run repeatedly: no-ops once authenticated.
24
+ set -uo pipefail
25
+
26
+ CONFIG_DIR="\${CHAINPATROL_CONFIG_DIR:-$HOME/.chainpatrol}"
27
+ if [ -s "$CONFIG_DIR/credentials.json" ]; then
28
+ exit 0
29
+ fi
30
+
31
+ # Only nudge when the CLI is reachable \u2014 otherwise we'd ask Claude to run
32
+ # a command that isn't installed.
33
+ if ! command -v chainpatrol >/dev/null 2>&1 \\
34
+ && [ ! -x /usr/local/bin/chainpatrol ] \\
35
+ && ! ls /opt/node*/bin/chainpatrol >/dev/null 2>&1; then
36
+ exit 0
37
+ fi
38
+
39
+ cat <<'JSON'
40
+ {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"chainpatrol auto-login: the user is not authenticated with ChainPatrol (no credentials.json found). If the user's task involves ChainPatrol or the chainpatrol CLI, start \`chainpatrol login\` in the background per the /chainpatrol skill's headless runbook (background + tail /tmp/cp-login.out) and surface the verification URL before doing anything else. If the user's task is unrelated, ignore this notice."}}
41
+ JSON
42
+ exit 0
43
+ `;
44
+ function installLoginHook() {
45
+ mkdirSync(HOOK_DIR, { recursive: true });
46
+ const hookWritten = !existsSync(HOOK_FILE) || readFileSync(HOOK_FILE, "utf-8") !== HOOK_SCRIPT;
47
+ if (hookWritten) {
48
+ writeFileSync(HOOK_FILE, HOOK_SCRIPT, { mode: 493 });
49
+ }
50
+ const settingsUpdated = registerHookInSettings();
51
+ return {
52
+ hookPath: HOOK_FILE,
53
+ hookWritten,
54
+ settingsPath: SETTINGS_FILE,
55
+ settingsUpdated
56
+ };
57
+ }
58
+ function uninstallLoginHook() {
59
+ let hookRemoved = false;
60
+ if (existsSync(HOOK_FILE)) {
61
+ unlinkSync(HOOK_FILE);
62
+ hookRemoved = true;
63
+ }
64
+ const settingsUpdated = unregisterHookFromSettings();
65
+ return { hookRemoved, settingsUpdated };
66
+ }
67
+ function readSettings() {
68
+ if (!existsSync(SETTINGS_FILE)) return {};
69
+ const raw = readFileSync(SETTINGS_FILE, "utf-8");
70
+ if (raw.trim() === "") return {};
71
+ return JSON.parse(raw);
72
+ }
73
+ function writeSettings(settings) {
74
+ mkdirSync(dirname(SETTINGS_FILE), { recursive: true });
75
+ writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + "\n", {
76
+ mode: 420
77
+ });
78
+ }
79
+ function hasHookEntry(settings) {
80
+ const matchers = settings.hooks?.SessionStart;
81
+ if (!Array.isArray(matchers)) return false;
82
+ return matchers.some(
83
+ (matcher) => Array.isArray(matcher?.hooks) && matcher.hooks.some((h) => h?.command === HOOK_FILE)
84
+ );
85
+ }
86
+ function registerHookInSettings() {
87
+ const settings = readSettings();
88
+ if (hasHookEntry(settings)) return false;
89
+ const hooks = settings.hooks ??= {};
90
+ const sessionStart = hooks.SessionStart ??= [];
91
+ sessionStart.push({
92
+ hooks: [{ type: "command", command: HOOK_FILE }]
93
+ });
94
+ writeSettings(settings);
95
+ return true;
96
+ }
97
+ function unregisterHookFromSettings() {
98
+ if (!existsSync(SETTINGS_FILE)) return false;
99
+ const settings = readSettings();
100
+ const sessionStart = settings.hooks?.SessionStart;
101
+ if (!Array.isArray(sessionStart)) return false;
102
+ let changed = false;
103
+ const filtered = sessionStart.map((matcher) => {
104
+ if (!Array.isArray(matcher?.hooks)) return matcher;
105
+ const remaining = matcher.hooks.filter((h) => h?.command !== HOOK_FILE);
106
+ if (remaining.length === matcher.hooks.length) return matcher;
107
+ changed = true;
108
+ return remaining.length > 0 ? { ...matcher, hooks: remaining } : null;
109
+ }).filter((m) => m !== null);
110
+ if (!changed) return false;
111
+ if (filtered.length === 0) {
112
+ delete settings.hooks.SessionStart;
113
+ if (Object.keys(settings.hooks).length === 0) {
114
+ delete settings.hooks;
115
+ }
116
+ } else {
117
+ settings.hooks.SessionStart = filtered;
118
+ }
119
+ writeSettings(settings);
120
+ return true;
121
+ }
10
122
 
11
123
  // src/lib/version.ts
12
- import { readFileSync } from "fs";
13
- import { dirname, resolve } from "path";
124
+ import { readFileSync as readFileSync2 } from "fs";
125
+ import { dirname as dirname2, resolve } from "path";
14
126
  import { fileURLToPath } from "url";
15
127
  var PACKAGE_NAME = "@chainpatrol/cli";
16
128
  var cached;
@@ -21,7 +133,7 @@ function getCliVersion() {
21
133
  }
22
134
  function resolveCliVersion() {
23
135
  try {
24
- const here = dirname(fileURLToPath(import.meta.url));
136
+ const here = dirname2(fileURLToPath(import.meta.url));
25
137
  const candidates = [
26
138
  resolve(here, "..", "package.json"),
27
139
  resolve(here, "..", "..", "package.json")
@@ -36,7 +148,7 @@ function resolveCliVersion() {
36
148
  }
37
149
  function tryReadVersion(path) {
38
150
  try {
39
- const raw = readFileSync(path, "utf-8");
151
+ const raw = readFileSync2(path, "utf-8");
40
152
  const pkg = JSON.parse(raw);
41
153
  if (pkg.name === PACKAGE_NAME && typeof pkg.version === "string") {
42
154
  return pkg.version;
@@ -66,8 +178,8 @@ function compareVersions(a, b) {
66
178
  }
67
179
 
68
180
  // src/commands/setup-skill.ts
69
- var SKILL_DIR = join(homedir(), ".claude", "skills", "chainpatrol");
70
- var SKILL_FILE = join(SKILL_DIR, "SKILL.md");
181
+ var SKILL_DIR = join2(homedir2(), ".claude", "skills", "chainpatrol");
182
+ var SKILL_FILE = join2(SKILL_DIR, "SKILL.md");
71
183
  function buildSkillContent(version) {
72
184
  return `---
73
185
  name: chainpatrol
@@ -971,16 +1083,16 @@ function getBundledSkillContent() {
971
1083
  return buildSkillContent(getCliVersion());
972
1084
  }
973
1085
  function readInstalledSkillVersion() {
974
- if (!existsSync(SKILL_FILE)) return void 0;
1086
+ if (!existsSync2(SKILL_FILE)) return void 0;
975
1087
  try {
976
- const raw = readFileSync2(SKILL_FILE, "utf-8");
1088
+ const raw = readFileSync3(SKILL_FILE, "utf-8");
977
1089
  return parseSkillVersion(raw);
978
1090
  } catch {
979
1091
  return void 0;
980
1092
  }
981
1093
  }
982
1094
  function isSkillInstalled() {
983
- return existsSync(SKILL_FILE);
1095
+ return existsSync2(SKILL_FILE);
984
1096
  }
985
1097
  function parseSkillVersion(content) {
986
1098
  const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
@@ -1001,66 +1113,94 @@ var LOGO = `
1001
1113
  `;
1002
1114
  function setupSkill(options) {
1003
1115
  const skillContent = buildSkillContent(getCliVersion());
1004
- const alreadyExists = existsSync(SKILL_FILE);
1005
- if (alreadyExists) {
1006
- const existing = readFileSync2(SKILL_FILE, "utf-8");
1007
- if (existing === skillContent) {
1008
- if (options.json) {
1009
- console.log(JSON.stringify({ status: "up-to-date", path: SKILL_FILE }));
1010
- } else {
1011
- console.log("Claude Code skill is already up to date.");
1012
- }
1013
- return;
1014
- }
1116
+ const skillAlreadyExists = existsSync2(SKILL_FILE);
1117
+ const skillUpToDate = skillAlreadyExists && readFileSync3(SKILL_FILE, "utf-8") === skillContent;
1118
+ let skillStatus;
1119
+ if (!skillAlreadyExists) {
1120
+ mkdirSync2(SKILL_DIR, { recursive: true });
1121
+ writeFileSync2(SKILL_FILE, skillContent, { mode: 420 });
1122
+ skillStatus = "installed";
1123
+ } else if (!skillUpToDate) {
1124
+ writeFileSync2(SKILL_FILE, skillContent, { mode: 420 });
1125
+ skillStatus = "updated";
1126
+ } else {
1127
+ skillStatus = "up-to-date";
1015
1128
  }
1016
- mkdirSync(SKILL_DIR, { recursive: true });
1017
- writeFileSync(SKILL_FILE, skillContent, { mode: 420 });
1018
1129
  const completionResult = installCompletions();
1130
+ const loginHookResult = options.cloud ? installLoginHook() : null;
1131
+ const completionsChanged = completionResult.installed || completionResult.configuredShellRc;
1132
+ const loginHookChanged = loginHookResult != null && (loginHookResult.hookWritten || loginHookResult.settingsUpdated);
1133
+ const overallStatus = skillStatus !== "up-to-date" ? skillStatus : completionsChanged || loginHookChanged ? "updated" : "up-to-date";
1019
1134
  if (options.json) {
1020
1135
  console.log(
1021
1136
  JSON.stringify({
1022
- status: alreadyExists ? "updated" : "installed",
1137
+ status: overallStatus,
1023
1138
  path: SKILL_FILE,
1024
- completions: completionResult
1139
+ skill: { status: skillStatus, path: SKILL_FILE },
1140
+ completions: completionResult,
1141
+ loginHook: loginHookResult
1025
1142
  })
1026
1143
  );
1027
- } else {
1028
- console.log(LOGO);
1144
+ return;
1145
+ }
1146
+ if (overallStatus === "up-to-date") {
1147
+ console.log("Claude Code skill, completions, and login hook are already up to date.");
1148
+ return;
1149
+ }
1150
+ console.log(LOGO);
1151
+ if (skillStatus === "installed") {
1152
+ console.log(`Installed Claude Code skill at ${SKILL_FILE}`);
1153
+ } else if (skillStatus === "updated") {
1154
+ console.log(`Updated Claude Code skill at ${SKILL_FILE}`);
1155
+ }
1156
+ console.log("You can now use /chainpatrol in Claude Code from any project.");
1157
+ if (loginHookChanged && loginHookResult) {
1158
+ console.log(`Installed auto-login hook at ${loginHookResult.hookPath}`);
1159
+ if (loginHookResult.settingsUpdated) {
1160
+ console.log(`Registered SessionStart hook in ${loginHookResult.settingsPath}`);
1161
+ }
1162
+ }
1163
+ if (completionResult.installed) {
1029
1164
  console.log(
1030
- alreadyExists ? `Updated Claude Code skill at ${SKILL_FILE}` : `Installed Claude Code skill at ${SKILL_FILE}`
1165
+ `Installed ${completionResult.shell} completions at ${completionResult.path}`
1031
1166
  );
1032
- console.log("You can now use /chainpatrol in Claude Code from any project.");
1033
- if (completionResult.installed) {
1034
- console.log(
1035
- `Installed ${completionResult.shell} completions at ${completionResult.path}`
1036
- );
1037
- if (completionResult.configuredShellRc) {
1038
- console.log("Added completion config to ~/.zshrc");
1039
- }
1040
- console.log('Run "exec zsh" or open a new terminal to enable tab completion.');
1167
+ if (completionResult.configuredShellRc) {
1168
+ console.log("Added completion config to ~/.zshrc");
1041
1169
  }
1170
+ console.log('Run "exec zsh" or open a new terminal to enable tab completion.');
1042
1171
  }
1043
1172
  }
1044
1173
  function uninstallSkill(options) {
1045
- if (!existsSync(SKILL_FILE)) {
1046
- if (options.json) {
1047
- console.log(JSON.stringify({ status: "not-installed" }));
1048
- } else {
1049
- console.log("Claude Code skill is not installed.");
1050
- }
1051
- return;
1174
+ const skillInstalled = existsSync2(SKILL_FILE);
1175
+ if (skillInstalled) {
1176
+ rmSync(SKILL_DIR, { recursive: true });
1052
1177
  }
1053
- rmSync(SKILL_DIR, { recursive: true });
1054
1178
  const removedCompletions = uninstallCompletions();
1179
+ const loginHookResult = uninstallLoginHook();
1180
+ const removedAnything = skillInstalled || removedCompletions || loginHookResult.hookRemoved || loginHookResult.settingsUpdated;
1055
1181
  if (options.json) {
1056
1182
  console.log(
1057
- JSON.stringify({ status: "uninstalled", path: SKILL_FILE, removedCompletions })
1183
+ JSON.stringify({
1184
+ status: removedAnything ? "uninstalled" : "not-installed",
1185
+ path: SKILL_FILE,
1186
+ removedCompletions,
1187
+ loginHook: loginHookResult
1188
+ })
1058
1189
  );
1059
- } else {
1190
+ return;
1191
+ }
1192
+ if (!removedAnything) {
1193
+ console.log("Claude Code skill is not installed.");
1194
+ return;
1195
+ }
1196
+ if (skillInstalled) {
1060
1197
  console.log("Removed Claude Code skill from " + SKILL_DIR);
1061
- if (removedCompletions) {
1062
- console.log("Removed shell completions.");
1063
- }
1198
+ }
1199
+ if (loginHookResult.hookRemoved || loginHookResult.settingsUpdated) {
1200
+ console.log("Removed chainpatrol auto-login hook.");
1201
+ }
1202
+ if (removedCompletions) {
1203
+ console.log("Removed shell completions.");
1064
1204
  }
1065
1205
  }
1066
1206
 
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-XOXQPUR6.js";
16
+ } from "./chunk-ZN3VMRWG.js";
17
17
  import "./chunk-IUZB3DQW.js";
18
18
  import {
19
19
  DateTime
@@ -354,7 +354,10 @@ var HELP = {
354
354
  },
355
355
  setup: {
356
356
  description: "Install Claude Code skill and shell completions.",
357
- usage: "chainpatrol setup"
357
+ usage: "chainpatrol setup [--cloud]",
358
+ options: [
359
+ "--cloud Also install a SessionStart hook that surfaces the device-code login URL when the user is not authenticated. Intended for Claude Code cloud / headless environments; omit for local installs."
360
+ ]
358
361
  },
359
362
  uninstall: {
360
363
  description: "Remove Claude Code skill and shell completions.",
@@ -651,7 +654,8 @@ ${getTopLevelHelp()}
651
654
  version: { type: "boolean", shortFlag: "V" },
652
655
  quiet: { type: "boolean", default: false, shortFlag: "q" },
653
656
  noInput: { type: "boolean", default: false },
654
- noColor: { type: "boolean", default: false }
657
+ noColor: { type: "boolean", default: false },
658
+ cloud: { type: "boolean", default: false }
655
659
  }
656
660
  });
657
661
  var [command, subcommand, action] = cli.input;
@@ -1148,12 +1152,12 @@ async function main() {
1148
1152
  case "setup":
1149
1153
  case "install":
1150
1154
  case "i": {
1151
- const { setupSkill } = await import("./setup-skill-BTR2IZ4E.js");
1152
- setupSkill({ json: jsonMode });
1155
+ const { setupSkill } = await import("./setup-skill-J7PZYVCE.js");
1156
+ setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1153
1157
  break;
1154
1158
  }
1155
1159
  case "uninstall": {
1156
- const { uninstallSkill } = await import("./setup-skill-BTR2IZ4E.js");
1160
+ const { uninstallSkill } = await import("./setup-skill-J7PZYVCE.js");
1157
1161
  uninstallSkill({ json: jsonMode });
1158
1162
  break;
1159
1163
  }
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-XOXQPUR6.js";
9
+ } from "./chunk-ZN3VMRWG.js";
10
10
  import "./chunk-IUZB3DQW.js";
11
11
  export {
12
12
  getBundledSkillContent,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@chainpatrol/cli",
3
3
  "description": "The official ChainPatrol CLI — terminal interface for threat detection",
4
4
  "author": "Umar Ahmed <umar@chainpatrol.io>",
5
- "version": "0.6.0",
5
+ "version": "0.7.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [