@contro1/claude-code 0.1.0 → 0.1.2
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 -2
- package/dist/config.js +3 -4
- package/dist/formatter.js +4 -1
- package/dist/hook.js +12 -6
- package/dist/types.d.ts +1 -1
- package/package.json +5 -3
- package/dist/centcomClient.d.ts +0 -33
- package/dist/centcomClient.js +0 -66
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ Set env vars (or use `.centcom.json` in working directory):
|
|
|
23
23
|
- `CENTCOM_SLA_MINUTES` (optional)
|
|
24
24
|
- `CENTCOM_REQUIRED_ROLE` (optional)
|
|
25
25
|
- `CENTCOM_FALLBACK` (`deny` default; supported: `deny`, `ask`, `allow`)
|
|
26
|
-
- `CENTCOM_CALLBACK_URL` (
|
|
26
|
+
- `CENTCOM_CALLBACK_URL` (optional; only set when you also want webhook callbacks)
|
|
27
27
|
|
|
28
28
|
## Claude Code Hook Setup
|
|
29
29
|
|
|
@@ -34,7 +34,7 @@ Add this in `.claude/settings.json`:
|
|
|
34
34
|
"hooks": {
|
|
35
35
|
"PreToolUse": [
|
|
36
36
|
{
|
|
37
|
-
"matcher": "",
|
|
37
|
+
"matcher": "Write|Edit|Bash",
|
|
38
38
|
"command": "centcom-claude-code",
|
|
39
39
|
"timeout": 310000
|
|
40
40
|
}
|
|
@@ -51,6 +51,18 @@ Add this in `.claude/settings.json`:
|
|
|
51
51
|
- Timeout -> request cancel attempt + `deny`.
|
|
52
52
|
- API errors -> fallback decision (`CENTCOM_FALLBACK`).
|
|
53
53
|
|
|
54
|
+
## Quick Verify
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npm view @contro1/claude-code version
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Related Packages
|
|
61
|
+
|
|
62
|
+
- [`centcom`](https://github.com/contro1-hq/centcom) for Python integrations
|
|
63
|
+
- [`@contro1/sdk`](https://github.com/contro1-hq/centcom-sdk) for Node/TypeScript SDK usage
|
|
64
|
+
- [`centcom-langgraph`](https://github.com/contro1-hq/centcom-langgraph) for LangGraph integrations
|
|
65
|
+
|
|
54
66
|
## Development
|
|
55
67
|
|
|
56
68
|
```bash
|
|
@@ -58,3 +70,8 @@ npm install
|
|
|
58
70
|
npm run build
|
|
59
71
|
npm pack
|
|
60
72
|
```
|
|
73
|
+
|
|
74
|
+
## Skill
|
|
75
|
+
|
|
76
|
+
This repo includes an integration skill:
|
|
77
|
+
- `skills/centcom-claude-code.md`
|
package/dist/config.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
const DEFAULT_BASE_URL = "https://contro1.com/api/centcom/v1";
|
|
3
|
+
const DEFAULT_BASE_URL = "https://api.contro1.com/api/centcom/v1";
|
|
4
4
|
const DEFAULT_TOOLS = "Write,Edit,Bash";
|
|
5
5
|
const DEFAULT_TIMEOUT_MS = 300_000;
|
|
6
6
|
const DEFAULT_POLL_INTERVAL_MS = 3_000;
|
|
7
7
|
const DEFAULT_PRIORITY = "urgent";
|
|
8
8
|
const DEFAULT_FALLBACK = "deny";
|
|
9
|
-
const DEFAULT_CALLBACK_URL = "https://centcom.local/claude-code";
|
|
10
9
|
function readLocalConfig() {
|
|
11
10
|
const path = join(process.cwd(), ".centcom.json");
|
|
12
11
|
if (!existsSync(path))
|
|
@@ -43,7 +42,7 @@ export function loadConfig() {
|
|
|
43
42
|
const slaValue = env.CENTCOM_SLA_MINUTES ?? file.CENTCOM_SLA_MINUTES;
|
|
44
43
|
const slaMinutes = slaValue !== undefined ? Number(slaValue) : undefined;
|
|
45
44
|
const requiredRole = env.CENTCOM_REQUIRED_ROLE ?? file.CENTCOM_REQUIRED_ROLE;
|
|
46
|
-
const callbackUrl = env.CENTCOM_CALLBACK_URL ?? file.CENTCOM_CALLBACK_URL
|
|
45
|
+
const callbackUrl = env.CENTCOM_CALLBACK_URL ?? file.CENTCOM_CALLBACK_URL;
|
|
47
46
|
return {
|
|
48
47
|
apiKey,
|
|
49
48
|
baseUrl,
|
|
@@ -54,6 +53,6 @@ export function loadConfig() {
|
|
|
54
53
|
slaMinutes: Number.isFinite(slaMinutes) ? slaMinutes : undefined,
|
|
55
54
|
requiredRole: requiredRole || undefined,
|
|
56
55
|
fallback,
|
|
57
|
-
callbackUrl,
|
|
56
|
+
callbackUrl: callbackUrl || undefined,
|
|
58
57
|
};
|
|
59
58
|
}
|
package/dist/formatter.js
CHANGED
|
@@ -4,7 +4,9 @@ function redactSecrets(text) {
|
|
|
4
4
|
return text
|
|
5
5
|
.replace(/(cc_(?:live|test)_[a-zA-Z0-9_-]{8,})/g, "[REDACTED_API_KEY]")
|
|
6
6
|
.replace(/(whsec_[a-zA-Z0-9_-]{8,})/g, "[REDACTED_WEBHOOK_SECRET]")
|
|
7
|
-
.replace(/(Bearer\s+)[^\s]+/gi, "$1[REDACTED_TOKEN]")
|
|
7
|
+
.replace(/(Bearer\s+)[^\s]+/gi, "$1[REDACTED_TOKEN]")
|
|
8
|
+
.replace(/("?(?:api[_-]?key|token|secret|password)"?\s*:\s*)"[^"]*"/gi, '$1"[REDACTED]"')
|
|
9
|
+
.replace(/((?:api[_-]?key|token|secret|password)\s*=\s*)[^\s]+/gi, "$1[REDACTED]");
|
|
8
10
|
}
|
|
9
11
|
function clip(text, size = MAX_PREVIEW) {
|
|
10
12
|
if (text.length <= size)
|
|
@@ -52,6 +54,7 @@ export function formatRequest(input) {
|
|
|
52
54
|
source: "claude-code",
|
|
53
55
|
session_id: input.session_id,
|
|
54
56
|
tool_name: tool,
|
|
57
|
+
tool_input_hash: createHash("sha256").update(stableStringify(toolInput)).digest("hex").slice(0, 24),
|
|
55
58
|
cwd: input.cwd ?? "",
|
|
56
59
|
},
|
|
57
60
|
};
|
package/dist/hook.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { stdin, stdout, stderr } from "node:process";
|
|
3
|
-
import { CentcomClient } from "
|
|
3
|
+
import { CentcomClient } from "@contro1/sdk";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { buildIdempotencyKey, formatRequest } from "./formatter.js";
|
|
6
6
|
let activeRequestId = null;
|
|
@@ -70,23 +70,29 @@ async function main() {
|
|
|
70
70
|
const client = new CentcomClient({
|
|
71
71
|
apiKey: config.apiKey,
|
|
72
72
|
baseUrl: config.baseUrl,
|
|
73
|
-
|
|
73
|
+
timeout: Math.min(config.timeoutMs, 30_000),
|
|
74
74
|
});
|
|
75
75
|
activeClient = client;
|
|
76
76
|
const formatted = formatRequest(input);
|
|
77
77
|
const idempotencyKey = buildIdempotencyKey(input);
|
|
78
78
|
try {
|
|
79
|
-
const
|
|
79
|
+
const requestPayload = {
|
|
80
80
|
type: "approval",
|
|
81
81
|
question: formatted.question,
|
|
82
82
|
context: formatted.context,
|
|
83
|
-
callback_url: config.callbackUrl,
|
|
84
83
|
priority: config.priority,
|
|
85
84
|
required_role: config.requiredRole,
|
|
86
|
-
metadata:
|
|
85
|
+
metadata: {
|
|
86
|
+
...formatted.metadata,
|
|
87
|
+
idempotency_key: idempotencyKey,
|
|
88
|
+
},
|
|
87
89
|
sla_minutes: config.slaMinutes,
|
|
88
90
|
idempotency_key: idempotencyKey,
|
|
89
|
-
}
|
|
91
|
+
};
|
|
92
|
+
if (config.callbackUrl) {
|
|
93
|
+
requestPayload.callback_url = config.callbackUrl;
|
|
94
|
+
}
|
|
95
|
+
const request = await client.createRequest(requestPayload);
|
|
90
96
|
activeRequestId = request.id;
|
|
91
97
|
const result = await client.waitForResponse(request.id, config.pollIntervalMs, config.timeoutMs);
|
|
92
98
|
const approved = responseApproved(result.response ?? null);
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contro1/claude-code",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "CENTCOM approval hook for Claude Code
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "CENTCOM approval hook for Claude Code PermissionRequest",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/hook.js",
|
|
7
7
|
"types": "dist/hook.d.ts",
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
-
"dependencies": {
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@contro1/sdk": "^1.0.0"
|
|
34
|
+
},
|
|
33
35
|
"devDependencies": {
|
|
34
36
|
"@types/node": "^20.0.0",
|
|
35
37
|
"typescript": "^5.4.0"
|
package/dist/centcomClient.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
interface CentcomClientConfig {
|
|
2
|
-
apiKey: string;
|
|
3
|
-
baseUrl: string;
|
|
4
|
-
timeoutMs: number;
|
|
5
|
-
}
|
|
6
|
-
interface CreateRequestParams {
|
|
7
|
-
type: "approval" | "yes_no" | "free_text";
|
|
8
|
-
context: string;
|
|
9
|
-
question: string;
|
|
10
|
-
callback_url: string;
|
|
11
|
-
priority: "normal" | "urgent";
|
|
12
|
-
required_role?: string;
|
|
13
|
-
metadata?: Record<string, unknown>;
|
|
14
|
-
sla_minutes?: number;
|
|
15
|
-
idempotency_key?: string;
|
|
16
|
-
}
|
|
17
|
-
export interface CentcomRequest {
|
|
18
|
-
id: string;
|
|
19
|
-
state: string;
|
|
20
|
-
response?: Record<string, unknown> | null;
|
|
21
|
-
}
|
|
22
|
-
export declare class CentcomClient {
|
|
23
|
-
private readonly apiKey;
|
|
24
|
-
private readonly baseUrl;
|
|
25
|
-
private readonly timeoutMs;
|
|
26
|
-
constructor(config: CentcomClientConfig);
|
|
27
|
-
private request;
|
|
28
|
-
createRequest(params: CreateRequestParams): Promise<CentcomRequest>;
|
|
29
|
-
getRequest(requestId: string): Promise<CentcomRequest>;
|
|
30
|
-
cancelRequest(requestId: string): Promise<void>;
|
|
31
|
-
waitForResponse(requestId: string, intervalMs: number, timeoutMs: number): Promise<CentcomRequest>;
|
|
32
|
-
}
|
|
33
|
-
export {};
|
package/dist/centcomClient.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
export class CentcomClient {
|
|
2
|
-
apiKey;
|
|
3
|
-
baseUrl;
|
|
4
|
-
timeoutMs;
|
|
5
|
-
constructor(config) {
|
|
6
|
-
this.apiKey = config.apiKey;
|
|
7
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
8
|
-
this.timeoutMs = config.timeoutMs;
|
|
9
|
-
}
|
|
10
|
-
async request(method, path, body, extraHeaders) {
|
|
11
|
-
const controller = new AbortController();
|
|
12
|
-
const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
13
|
-
try {
|
|
14
|
-
const res = await fetch(`${this.baseUrl}${path}`, {
|
|
15
|
-
method,
|
|
16
|
-
headers: {
|
|
17
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
18
|
-
"Content-Type": "application/json",
|
|
19
|
-
...(extraHeaders ?? {}),
|
|
20
|
-
},
|
|
21
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
22
|
-
signal: controller.signal,
|
|
23
|
-
});
|
|
24
|
-
const json = (await res.json());
|
|
25
|
-
if (!res.ok) {
|
|
26
|
-
throw new Error(json.message || `HTTP ${res.status}`);
|
|
27
|
-
}
|
|
28
|
-
return json;
|
|
29
|
-
}
|
|
30
|
-
finally {
|
|
31
|
-
clearTimeout(timeout);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
async createRequest(params) {
|
|
35
|
-
const { idempotency_key, ...body } = params;
|
|
36
|
-
const headers = {};
|
|
37
|
-
if (idempotency_key)
|
|
38
|
-
headers["Idempotency-Key"] = idempotency_key;
|
|
39
|
-
return this.request("POST", "/requests", body, headers);
|
|
40
|
-
}
|
|
41
|
-
async getRequest(requestId) {
|
|
42
|
-
return this.request("GET", `/requests/${requestId}`);
|
|
43
|
-
}
|
|
44
|
-
async cancelRequest(requestId) {
|
|
45
|
-
await this.request("DELETE", `/requests/${requestId}`);
|
|
46
|
-
}
|
|
47
|
-
async waitForResponse(requestId, intervalMs, timeoutMs) {
|
|
48
|
-
const deadline = Date.now() + timeoutMs;
|
|
49
|
-
const terminal = new Set([
|
|
50
|
-
"answered",
|
|
51
|
-
"callback_pending",
|
|
52
|
-
"callback_delivered",
|
|
53
|
-
"callback_failed",
|
|
54
|
-
"closed",
|
|
55
|
-
"expired",
|
|
56
|
-
"cancelled",
|
|
57
|
-
]);
|
|
58
|
-
while (Date.now() < deadline) {
|
|
59
|
-
const req = await this.getRequest(requestId);
|
|
60
|
-
if (terminal.has(req.state))
|
|
61
|
-
return req;
|
|
62
|
-
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
63
|
-
}
|
|
64
|
-
throw new Error(`Timeout waiting for response on request ${requestId}`);
|
|
65
|
-
}
|
|
66
|
-
}
|