@kill-switch/cli 0.2.0 → 0.3.1
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 +49 -4
- package/dist/commands/accounts.d.ts +2 -1
- package/dist/commands/accounts.js +44 -27
- package/dist/commands/activity.d.ts +3 -0
- package/dist/commands/activity.js +80 -0
- package/dist/commands/agent-guard.d.ts +10 -0
- package/dist/commands/agent-guard.js +175 -0
- package/dist/commands/alerts.d.ts +2 -1
- package/dist/commands/alerts.js +112 -12
- package/dist/commands/analytics.d.ts +2 -1
- package/dist/commands/analytics.js +36 -14
- package/dist/commands/auth.d.ts +2 -1
- package/dist/commands/auth.js +86 -46
- package/dist/commands/check.d.ts +2 -1
- package/dist/commands/check.js +28 -15
- package/dist/commands/config-cmd.js +20 -5
- package/dist/commands/kill.d.ts +2 -1
- package/dist/commands/kill.js +52 -37
- package/dist/commands/onboard.d.ts +2 -17
- package/dist/commands/onboard.js +244 -61
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +192 -0
- package/dist/commands/providers.d.ts +3 -0
- package/dist/commands/providers.js +82 -0
- package/dist/commands/rules.d.ts +2 -1
- package/dist/commands/rules.js +51 -28
- package/dist/commands/shield.d.ts +2 -1
- package/dist/commands/shield.js +36 -17
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +100 -0
- package/dist/commands/watch.d.ts +3 -0
- package/dist/commands/watch.js +68 -0
- package/dist/config.d.ts +4 -1
- package/dist/config.js +19 -2
- package/dist/device-flow.d.ts +33 -0
- package/dist/device-flow.js +91 -0
- package/dist/index.js +38 -11
- package/dist/output.d.ts +26 -4
- package/dist/output.js +101 -12
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +24 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +29 -7
package/README.md
CHANGED
|
@@ -40,7 +40,7 @@ ks onboard --provider cloudflare \
|
|
|
40
40
|
--token YOUR_CF_API_TOKEN \
|
|
41
41
|
--name "Production" \
|
|
42
42
|
--shields cost-runaway,ddos \
|
|
43
|
-
--alert-
|
|
43
|
+
--alert-pagerduty YOUR_ROUTING_KEY
|
|
44
44
|
|
|
45
45
|
# AWS
|
|
46
46
|
ks onboard --provider aws \
|
|
@@ -85,6 +85,40 @@ ks shield error-storm # Scale down on sustained high error rate
|
|
|
85
85
|
ks shield --list
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
## Alert Channels
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
# List configured channels
|
|
92
|
+
ks alerts list
|
|
93
|
+
|
|
94
|
+
# Add PagerDuty (recommended — get routing key from PagerDuty > Service > Integrations)
|
|
95
|
+
ks alerts add --type pagerduty --routing-key YOUR_ROUTING_KEY
|
|
96
|
+
|
|
97
|
+
# Add Slack
|
|
98
|
+
ks alerts add --type slack --webhook-url https://hooks.slack.com/...
|
|
99
|
+
|
|
100
|
+
# Add Discord
|
|
101
|
+
ks alerts add --type discord --webhook-url https://discord.com/api/webhooks/...
|
|
102
|
+
|
|
103
|
+
# Add GitHub AI remediation (triggers Claude Code to open a fix PR on extreme violations)
|
|
104
|
+
ks alerts add --type github \
|
|
105
|
+
--token ghp_YOUR_PAT \
|
|
106
|
+
--repo-owner YOUR_ORG \
|
|
107
|
+
--repo-name YOUR_REPO \
|
|
108
|
+
--workflow kill-switch-remediate.yml \
|
|
109
|
+
--branch main
|
|
110
|
+
|
|
111
|
+
# Add email or generic webhook
|
|
112
|
+
ks alerts add --type email --email you@example.com
|
|
113
|
+
ks alerts add --type webhook --webhook-url https://your-service.example.com/webhook
|
|
114
|
+
|
|
115
|
+
# Remove a channel by name
|
|
116
|
+
ks alerts remove "PagerDuty"
|
|
117
|
+
|
|
118
|
+
# Send a test alert to all channels
|
|
119
|
+
ks alerts test
|
|
120
|
+
```
|
|
121
|
+
|
|
88
122
|
## Commands
|
|
89
123
|
|
|
90
124
|
| Command | Description |
|
|
@@ -92,14 +126,18 @@ ks shield --list
|
|
|
92
126
|
| `ks onboard` | One-command setup: connect + shields + alerts |
|
|
93
127
|
| `ks auth login` | Authenticate with API key |
|
|
94
128
|
| `ks auth status` | Show auth status |
|
|
129
|
+
| `ks status` | Dashboard: accounts, alerts, 30-day spend summary |
|
|
95
130
|
| `ks accounts list` | List connected cloud accounts |
|
|
96
131
|
| `ks accounts add` | Connect a cloud provider |
|
|
97
132
|
| `ks accounts check <id>` | Run manual check on an account |
|
|
98
|
-
| `ks check` | Check all accounts |
|
|
133
|
+
| `ks check` | Check all accounts (shows violations with multiplier column e.g. 60x) |
|
|
99
134
|
| `ks shield <preset>` | Apply a protection preset |
|
|
100
135
|
| `ks rules list` | List active rules |
|
|
101
136
|
| `ks alerts list` | List alert channels |
|
|
102
|
-
| `ks
|
|
137
|
+
| `ks alerts add` | Add an alert channel |
|
|
138
|
+
| `ks alerts remove <name>` | Remove an alert channel by name |
|
|
139
|
+
| `ks alerts test` | Send a test alert to all channels |
|
|
140
|
+
| `ks analytics` | Cost analytics: spend summary, 7-day table, per-account breakdown |
|
|
103
141
|
| `ks config list` | Show configuration |
|
|
104
142
|
|
|
105
143
|
## AI Agent Usage
|
|
@@ -117,8 +155,15 @@ ks onboard \
|
|
|
117
155
|
--token CF_API_TOKEN \
|
|
118
156
|
--name "Production" \
|
|
119
157
|
--shields cost-runaway,ddos \
|
|
158
|
+
--alert-pagerduty KEY \
|
|
120
159
|
--json
|
|
121
160
|
|
|
161
|
+
# Add PagerDuty alerts
|
|
162
|
+
ks alerts add --type pagerduty --routing-key KEY --json
|
|
163
|
+
|
|
164
|
+
# Full status dashboard
|
|
165
|
+
ks status --json
|
|
166
|
+
|
|
122
167
|
# All commands support --json for machine-readable output
|
|
123
168
|
ks accounts list --json
|
|
124
169
|
ks check --json
|
|
@@ -171,7 +216,7 @@ The CLI uses personal API keys (prefixed with `ks_live_`). Create one from [app.
|
|
|
171
216
|
- [Dashboard](https://app.kill-switch.net)
|
|
172
217
|
- [API Docs](https://kill-switch.net/docs)
|
|
173
218
|
- [CLI Docs](https://kill-switch.net/docs/cli.html)
|
|
174
|
-
- [GitHub](https://github.com/
|
|
219
|
+
- [GitHub](https://github.com/divinci-ai/kill-switch)
|
|
175
220
|
|
|
176
221
|
## License
|
|
177
222
|
|
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
export function registerAccountCommands(program) {
|
|
1
|
+
import { outputJson, formatTable, formatObject, handleError, spinner, success } from "../output.js";
|
|
2
|
+
import { confirm } from "../prompts.js";
|
|
3
|
+
export function registerAccountCommands(program, createClient) {
|
|
4
4
|
const accounts = program.command("accounts").description("Manage cloud accounts");
|
|
5
5
|
accounts
|
|
6
6
|
.command("list")
|
|
7
7
|
.alias("ls")
|
|
8
8
|
.description("List connected cloud accounts")
|
|
9
|
-
.
|
|
9
|
+
.option("--provider <provider>", "Filter by provider (cloudflare, gcp, aws, runpod)")
|
|
10
|
+
.option("--status <status>", "Filter by status (active, paused, disconnected)")
|
|
11
|
+
.action(async (opts) => {
|
|
10
12
|
const json = program.opts().json;
|
|
11
13
|
try {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
+
const client = createClient();
|
|
15
|
+
let list = await client.accounts.list();
|
|
16
|
+
if (opts.provider)
|
|
17
|
+
list = list.filter((a) => a.provider === opts.provider);
|
|
18
|
+
if (opts.status)
|
|
19
|
+
list = list.filter((a) => a.status === opts.status);
|
|
14
20
|
if (json) {
|
|
15
21
|
outputJson(list);
|
|
16
22
|
}
|
|
17
23
|
else {
|
|
18
|
-
formatTable(
|
|
19
|
-
{ key: "
|
|
24
|
+
formatTable(list, [
|
|
25
|
+
{ key: "id", header: "ID" },
|
|
20
26
|
{ key: "provider", header: "Provider" },
|
|
21
27
|
{ key: "name", header: "Name" },
|
|
22
28
|
{ key: "status", header: "Status" },
|
|
@@ -24,8 +30,7 @@ export function registerAccountCommands(program) {
|
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
catch (err) {
|
|
27
|
-
|
|
28
|
-
process.exit(1);
|
|
33
|
+
handleError(err, json);
|
|
29
34
|
}
|
|
30
35
|
});
|
|
31
36
|
accounts
|
|
@@ -34,7 +39,8 @@ export function registerAccountCommands(program) {
|
|
|
34
39
|
.action(async (id) => {
|
|
35
40
|
const json = program.opts().json;
|
|
36
41
|
try {
|
|
37
|
-
const
|
|
42
|
+
const client = createClient();
|
|
43
|
+
const data = await client.accounts.get(id);
|
|
38
44
|
if (json) {
|
|
39
45
|
outputJson(data);
|
|
40
46
|
}
|
|
@@ -43,8 +49,7 @@ export function registerAccountCommands(program) {
|
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
catch (err) {
|
|
46
|
-
|
|
47
|
-
process.exit(1);
|
|
52
|
+
handleError(err, json);
|
|
48
53
|
}
|
|
49
54
|
});
|
|
50
55
|
accounts
|
|
@@ -66,21 +71,25 @@ export function registerAccountCommands(program) {
|
|
|
66
71
|
credential.projectId = opts.projectId;
|
|
67
72
|
if (opts.serviceAccount)
|
|
68
73
|
credential.serviceAccountJson = opts.serviceAccount;
|
|
74
|
+
const s = json ? null : spinner(`Connecting ${provider}...`).start();
|
|
69
75
|
try {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
const client = createClient();
|
|
77
|
+
const data = await client.accounts.create({
|
|
78
|
+
provider: provider,
|
|
79
|
+
name: opts.name,
|
|
80
|
+
credential: credential,
|
|
73
81
|
});
|
|
82
|
+
s?.stop();
|
|
74
83
|
if (json) {
|
|
75
84
|
outputJson(data);
|
|
76
85
|
}
|
|
77
86
|
else {
|
|
78
|
-
|
|
87
|
+
success(`Connected ${provider} account: ${data.name || data.id}`);
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
catch (err) {
|
|
82
|
-
|
|
83
|
-
|
|
91
|
+
s?.stop();
|
|
92
|
+
handleError(err, json);
|
|
84
93
|
}
|
|
85
94
|
});
|
|
86
95
|
accounts
|
|
@@ -88,19 +97,24 @@ export function registerAccountCommands(program) {
|
|
|
88
97
|
.alias("rm")
|
|
89
98
|
.description("Disconnect and delete a cloud account")
|
|
90
99
|
.action(async (id) => {
|
|
91
|
-
const json = program.opts()
|
|
100
|
+
const { json, yes } = program.opts();
|
|
92
101
|
try {
|
|
93
|
-
await
|
|
102
|
+
const ok = await confirm(`Are you sure you want to disconnect account ${id}?`, { yes, json });
|
|
103
|
+
if (!ok) {
|
|
104
|
+
console.log("Aborted.");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const client = createClient();
|
|
108
|
+
await client.accounts.delete(id);
|
|
94
109
|
if (json) {
|
|
95
110
|
outputJson({ deleted: true, id });
|
|
96
111
|
}
|
|
97
112
|
else {
|
|
98
|
-
|
|
113
|
+
success(`Account ${id} disconnected.`);
|
|
99
114
|
}
|
|
100
115
|
}
|
|
101
116
|
catch (err) {
|
|
102
|
-
|
|
103
|
-
process.exit(1);
|
|
117
|
+
handleError(err, json);
|
|
104
118
|
}
|
|
105
119
|
});
|
|
106
120
|
accounts
|
|
@@ -108,8 +122,11 @@ export function registerAccountCommands(program) {
|
|
|
108
122
|
.description("Run manual monitoring check on an account")
|
|
109
123
|
.action(async (id) => {
|
|
110
124
|
const json = program.opts().json;
|
|
125
|
+
const s = json ? null : spinner("Running check...").start();
|
|
111
126
|
try {
|
|
112
|
-
const
|
|
127
|
+
const client = createClient();
|
|
128
|
+
const data = await client.accounts.check(id);
|
|
129
|
+
s?.stop();
|
|
113
130
|
if (json) {
|
|
114
131
|
outputJson(data);
|
|
115
132
|
}
|
|
@@ -126,8 +143,8 @@ export function registerAccountCommands(program) {
|
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
145
|
catch (err) {
|
|
129
|
-
|
|
130
|
-
|
|
146
|
+
s?.stop();
|
|
147
|
+
handleError(err, json);
|
|
131
148
|
}
|
|
132
149
|
});
|
|
133
150
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { outputJson, formatTable, handleError } from "../output.js";
|
|
2
|
+
export function registerActivityCommands(program, createClient) {
|
|
3
|
+
const activity = program.command("activity").description("View audit trail and activity logs");
|
|
4
|
+
activity
|
|
5
|
+
.command("list")
|
|
6
|
+
.alias("ls")
|
|
7
|
+
.description("Query activity log (owner/admin only)")
|
|
8
|
+
.option("--page <n>", "Page number", "1")
|
|
9
|
+
.option("--limit <n>", "Results per page (max 100)", "25")
|
|
10
|
+
.option("--action <prefix>", "Filter by action prefix (e.g., cloud_account, rule, team, kill_switch)")
|
|
11
|
+
.option("--resource-type <type>", "Filter by resource type")
|
|
12
|
+
.option("--actor <userId>", "Filter by actor user ID")
|
|
13
|
+
.option("--from <date>", "Start date (ISO format)")
|
|
14
|
+
.option("--to <date>", "End date (ISO format)")
|
|
15
|
+
.action(async (opts) => {
|
|
16
|
+
const json = program.opts().json;
|
|
17
|
+
try {
|
|
18
|
+
const client = createClient();
|
|
19
|
+
const data = await client.activity.list({
|
|
20
|
+
page: parseInt(opts.page) || undefined,
|
|
21
|
+
limit: parseInt(opts.limit) || undefined,
|
|
22
|
+
action: opts.action,
|
|
23
|
+
resourceType: opts.resourceType,
|
|
24
|
+
actorUserId: opts.actor,
|
|
25
|
+
from: opts.from,
|
|
26
|
+
to: opts.to,
|
|
27
|
+
});
|
|
28
|
+
if (json) {
|
|
29
|
+
outputJson(data);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(`Activity Log — Page ${data.page} (${data.total} total entries)\n`);
|
|
33
|
+
formatTable((data.entries || []).map((e) => ({
|
|
34
|
+
time: new Date(e.created_at).toLocaleString(),
|
|
35
|
+
actor: e.actor_email || e.actor_user_id?.substring(0, 12) || "—",
|
|
36
|
+
action: e.action,
|
|
37
|
+
resource: `${e.resource_type}${e.resource_id ? `:${e.resource_id.substring(0, 8)}` : ""}`,
|
|
38
|
+
})), [
|
|
39
|
+
{ key: "time", header: "Time" },
|
|
40
|
+
{ key: "actor", header: "Actor" },
|
|
41
|
+
{ key: "action", header: "Action" },
|
|
42
|
+
{ key: "resource", header: "Resource" },
|
|
43
|
+
]);
|
|
44
|
+
const totalPages = Math.ceil(data.total / data.limit);
|
|
45
|
+
if (totalPages > 1) {
|
|
46
|
+
console.log(`\nPage ${data.page} of ${totalPages}. Use --page ${data.page + 1} for next.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
handleError(err, json);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
// Shorthand: `ks activity` without subcommand shows recent
|
|
55
|
+
activity
|
|
56
|
+
.action(async () => {
|
|
57
|
+
const json = program.opts().json;
|
|
58
|
+
try {
|
|
59
|
+
const client = createClient();
|
|
60
|
+
const data = await client.activity.list({ limit: 10 });
|
|
61
|
+
if (json) {
|
|
62
|
+
outputJson(data);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log("Recent Activity (last 10):\n");
|
|
66
|
+
for (const e of data.entries || []) {
|
|
67
|
+
const time = new Date(e.created_at).toLocaleTimeString();
|
|
68
|
+
const actor = e.actor_email || e.actor_user_id?.substring(0, 12) || "?";
|
|
69
|
+
console.log(` ${time} ${actor} ${e.action} ${e.resourceType || ""}`);
|
|
70
|
+
}
|
|
71
|
+
if (data.total > 10) {
|
|
72
|
+
console.log(`\n ... ${data.total - 10} more. Use 'ks activity list' for full log.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
handleError(err, json);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ks guard` — the agent-guard surface inside the main Kill Switch CLI.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over @kill-switch/agent-guard's exported ops so users get one
|
|
5
|
+
* tool name. Local-only: these read/write the on-disk ledger + config and
|
|
6
|
+
* Claude Code settings; they do NOT call the Guardian API (the hook/proxy report
|
|
7
|
+
* breaches to the API on their own).
|
|
8
|
+
*/
|
|
9
|
+
import { Command } from "commander";
|
|
10
|
+
export declare function registerAgentGuardCommands(program: Command): void;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ks guard` — the agent-guard surface inside the main Kill Switch CLI.
|
|
3
|
+
*
|
|
4
|
+
* Thin wrapper over @kill-switch/agent-guard's exported ops so users get one
|
|
5
|
+
* tool name. Local-only: these read/write the on-disk ledger + config and
|
|
6
|
+
* Claude Code settings; they do NOT call the Guardian API (the hook/proxy report
|
|
7
|
+
* breaches to the API on their own).
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from "node:module";
|
|
10
|
+
import { buildStatusReport, installHook, setBudget, resetLedger, writePause, clearPause, configPath, pausePath, startProxy, resolveUpstream, fmtUSD, } from "@kill-switch/agent-guard";
|
|
11
|
+
import { outputJson, colors as c } from "../output.js";
|
|
12
|
+
/** Resolve the absolute path to the agent-guard hook entry (its dist/cli.js). */
|
|
13
|
+
function agentGuardCliPath() {
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
// Resolve the package entry, then point at the sibling cli.js the hook uses.
|
|
16
|
+
const pkgMain = require.resolve("@kill-switch/agent-guard");
|
|
17
|
+
return pkgMain.replace(/index\.js$/, "cli.js");
|
|
18
|
+
}
|
|
19
|
+
function bar(spent, hard) {
|
|
20
|
+
const pct = hard > 0 ? Math.min(100, Math.round((spent / hard) * 100)) : 0;
|
|
21
|
+
const filled = Math.round(pct / 5);
|
|
22
|
+
return `[${"█".repeat(filled)}${"░".repeat(20 - filled)}] ${pct}%`;
|
|
23
|
+
}
|
|
24
|
+
export function registerAgentGuardCommands(program) {
|
|
25
|
+
const guard = program
|
|
26
|
+
.command("guard")
|
|
27
|
+
.description("Kill Switch for coding agents — cap Claude Code / Cursor / Aider spend");
|
|
28
|
+
// ks guard status
|
|
29
|
+
guard
|
|
30
|
+
.command("status")
|
|
31
|
+
.description("Show current session + daily agent spend against the budget")
|
|
32
|
+
.action(() => {
|
|
33
|
+
const json = program.opts().json;
|
|
34
|
+
const report = buildStatusReport();
|
|
35
|
+
if (json) {
|
|
36
|
+
outputJson(report);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const icon = report.paused ? "⏸ " : report.verdict === "block" ? "🛑" : report.verdict === "warn" ? "⚠️ " : "✅";
|
|
40
|
+
console.log(`\n${icon} ${c.bold("Agent Guard")} — ${report.paused ? "PAUSED (enforcement off)" : report.verdict.toUpperCase()}`);
|
|
41
|
+
if (report.paused) {
|
|
42
|
+
console.log(report.pauseUntil
|
|
43
|
+
? c.dim(` resumes ${new Date(report.pauseUntil).toLocaleString()}`)
|
|
44
|
+
: c.dim(" paused indefinitely — `ks guard resume` to re-arm"));
|
|
45
|
+
}
|
|
46
|
+
console.log("");
|
|
47
|
+
console.log(` ${c.bold("Daily (24h):")} ${fmtUSD(report.dailyUSD)} / ${fmtUSD(report.budget.dailyHardUSD)} ${bar(report.dailyUSD, report.budget.dailyHardUSD)}`);
|
|
48
|
+
console.log("");
|
|
49
|
+
if (report.sessions.length === 0) {
|
|
50
|
+
console.log(c.dim(" No active sessions in the last 24h."));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
console.log(c.bold(" Active sessions (24h):"));
|
|
54
|
+
for (const s of report.sessions.slice(0, 8)) {
|
|
55
|
+
console.log(` ${fmtUSD(s.costUSD).padStart(9)} / ${fmtUSD(report.budget.sessionHardUSD)} ${bar(s.costUSD, report.budget.sessionHardUSD)} ${c.dim(s.id)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
for (const r of report.reasons)
|
|
59
|
+
console.log(` ${c.yellow("•")} ${r}`);
|
|
60
|
+
console.log("");
|
|
61
|
+
});
|
|
62
|
+
// ks guard install
|
|
63
|
+
guard
|
|
64
|
+
.command("install")
|
|
65
|
+
.description("Wire the agent-guard hook into Claude Code settings")
|
|
66
|
+
.option("--global", "Install into ~/.claude/settings.json (default: ./.claude/settings.json)")
|
|
67
|
+
.action((opts) => {
|
|
68
|
+
const json = program.opts().json;
|
|
69
|
+
const { settingsPath, command, added } = installHook(agentGuardCliPath(), process.execPath, { global: opts.global });
|
|
70
|
+
if (json) {
|
|
71
|
+
outputJson({ settingsPath, command, added });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(`✅ Hook installed → ${settingsPath}`);
|
|
75
|
+
console.log(` Events: ${added.length ? added.join(", ") : "(already present — no change)"}`);
|
|
76
|
+
console.log(c.dim(` Command: ${command}`));
|
|
77
|
+
console.log(c.dim(` Set caps with: ks guard config --session-hard 30 --daily-hard 150`));
|
|
78
|
+
});
|
|
79
|
+
// ks guard config
|
|
80
|
+
guard
|
|
81
|
+
.command("config")
|
|
82
|
+
.description("View or set agent-guard budget caps")
|
|
83
|
+
.option("--session-soft <usd>", "Per-session soft cap (warn)")
|
|
84
|
+
.option("--session-hard <usd>", "Per-session hard cap (block)")
|
|
85
|
+
.option("--daily-soft <usd>", "Daily rolling soft cap (warn)")
|
|
86
|
+
.option("--daily-hard <usd>", "Daily rolling hard cap (block)")
|
|
87
|
+
.option("--slack-webhook <url>", "Slack incoming-webhook for breach alerts")
|
|
88
|
+
.action((opts) => {
|
|
89
|
+
const json = program.opts().json;
|
|
90
|
+
const anySet = ["sessionSoft", "sessionHard", "dailySoft", "dailyHard", "slackWebhook"].some((k) => opts[k] !== undefined);
|
|
91
|
+
if (!anySet) {
|
|
92
|
+
const report = buildStatusReport();
|
|
93
|
+
if (json)
|
|
94
|
+
return outputJson({ budget: report.budget, configPath: configPath() });
|
|
95
|
+
console.log(JSON.stringify(report.budget, null, 2));
|
|
96
|
+
console.log(c.dim(`\nConfig file: ${configPath()}`));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const num = (v) => (v !== undefined ? Number(v) : undefined);
|
|
100
|
+
const budget = setBudget({
|
|
101
|
+
sessionSoftUSD: num(opts.sessionSoft),
|
|
102
|
+
sessionHardUSD: num(opts.sessionHard),
|
|
103
|
+
dailySoftUSD: num(opts.dailySoft),
|
|
104
|
+
dailyHardUSD: num(opts.dailyHard),
|
|
105
|
+
slackWebhook: opts.slackWebhook,
|
|
106
|
+
});
|
|
107
|
+
if (json)
|
|
108
|
+
return outputJson({ budget, saved: true });
|
|
109
|
+
console.log(`✅ Saved → ${configPath()}`);
|
|
110
|
+
console.log(JSON.stringify(budget, null, 2));
|
|
111
|
+
});
|
|
112
|
+
// ks guard pause
|
|
113
|
+
guard
|
|
114
|
+
.command("pause")
|
|
115
|
+
.description("Temporarily disable enforcement (escape hatch)")
|
|
116
|
+
.option("--minutes <n>", "Auto-resume after N minutes (default: indefinite)")
|
|
117
|
+
.action((opts) => {
|
|
118
|
+
const json = program.opts().json;
|
|
119
|
+
const mins = opts.minutes !== undefined ? Number(opts.minutes) : NaN;
|
|
120
|
+
if (opts.minutes !== undefined && Number.isFinite(mins)) {
|
|
121
|
+
const until = Date.now() + mins * 60_000;
|
|
122
|
+
writePause(until);
|
|
123
|
+
if (json)
|
|
124
|
+
return outputJson({ paused: true, until });
|
|
125
|
+
console.log(`⏸ Enforcement paused until ${new Date(until).toLocaleString()} (${mins} min).`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
writePause();
|
|
129
|
+
if (json)
|
|
130
|
+
return outputJson({ paused: true, until: null });
|
|
131
|
+
console.log("⏸ Enforcement paused indefinitely. Re-arm with `ks guard resume`.");
|
|
132
|
+
}
|
|
133
|
+
console.log(c.dim(` Sentinel: ${pausePath()}`));
|
|
134
|
+
});
|
|
135
|
+
// ks guard resume
|
|
136
|
+
guard
|
|
137
|
+
.command("resume")
|
|
138
|
+
.description("Re-arm enforcement after a pause")
|
|
139
|
+
.action(() => {
|
|
140
|
+
const json = program.opts().json;
|
|
141
|
+
clearPause();
|
|
142
|
+
if (json)
|
|
143
|
+
return outputJson({ paused: false });
|
|
144
|
+
console.log("✅ Enforcement re-armed.");
|
|
145
|
+
});
|
|
146
|
+
// ks guard reset
|
|
147
|
+
guard
|
|
148
|
+
.command("reset")
|
|
149
|
+
.description("Clear the agent spend ledger")
|
|
150
|
+
.option("--all", "Wipe all sessions")
|
|
151
|
+
.option("--session <id>", "Clear a single session")
|
|
152
|
+
.option("--today", "Clear sessions active today")
|
|
153
|
+
.action((opts) => {
|
|
154
|
+
const json = program.opts().json;
|
|
155
|
+
const msg = resetLedger({ all: opts.all, session: opts.session, today: opts.today });
|
|
156
|
+
if (json)
|
|
157
|
+
return outputJson({ message: msg });
|
|
158
|
+
console.log(`✅ ${msg}`);
|
|
159
|
+
});
|
|
160
|
+
// ks guard proxy
|
|
161
|
+
guard
|
|
162
|
+
.command("proxy")
|
|
163
|
+
.description("Start the token-metering proxy (HTTP 402 at the hard cap) for non-Claude-Code agents")
|
|
164
|
+
.option("--port <n>", "Port to listen on", "8787")
|
|
165
|
+
.option("--flavor <name>", "API flavor: anthropic | openai", "anthropic")
|
|
166
|
+
.option("--upstream <url>", "Upstream origin (default: api.anthropic.com / api.openai.com)")
|
|
167
|
+
.action((opts) => {
|
|
168
|
+
const flavor = opts.flavor === "openai" ? "openai" : "anthropic";
|
|
169
|
+
startProxy({
|
|
170
|
+
port: parseInt(opts.port, 10) || 8787,
|
|
171
|
+
flavor,
|
|
172
|
+
upstream: resolveUpstream(flavor, opts.upstream),
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|