@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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
70
|
-
var SKILL_FILE =
|
|
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 (!
|
|
1086
|
+
if (!existsSync2(SKILL_FILE)) return void 0;
|
|
975
1087
|
try {
|
|
976
|
-
const raw =
|
|
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
|
|
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
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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:
|
|
1137
|
+
status: overallStatus,
|
|
1023
1138
|
path: SKILL_FILE,
|
|
1024
|
-
|
|
1139
|
+
skill: { status: skillStatus, path: SKILL_FILE },
|
|
1140
|
+
completions: completionResult,
|
|
1141
|
+
loginHook: loginHookResult
|
|
1025
1142
|
})
|
|
1026
1143
|
);
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1165
|
+
`Installed ${completionResult.shell} completions at ${completionResult.path}`
|
|
1031
1166
|
);
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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({
|
|
1183
|
+
JSON.stringify({
|
|
1184
|
+
status: removedAnything ? "uninstalled" : "not-installed",
|
|
1185
|
+
path: SKILL_FILE,
|
|
1186
|
+
removedCompletions,
|
|
1187
|
+
loginHook: loginHookResult
|
|
1188
|
+
})
|
|
1058
1189
|
);
|
|
1059
|
-
|
|
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
|
-
|
|
1062
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
1160
|
+
const { uninstallSkill } = await import("./setup-skill-J7PZYVCE.js");
|
|
1157
1161
|
uninstallSkill({ json: jsonMode });
|
|
1158
1162
|
break;
|
|
1159
1163
|
}
|
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.
|
|
5
|
+
"version": "0.7.0",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://chainpatrol.com/docs/cli",
|
|
8
8
|
"keywords": [
|