calllint 1.0.1 → 1.1.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/dist/index.js +731 -45
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync17 } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -58,16 +58,23 @@ USAGE
|
|
|
58
58
|
|
|
59
59
|
COMMANDS
|
|
60
60
|
check [target] Compact safety decision for an MCP config or install snippet
|
|
61
|
+
inventory List all discovered agent configs (auto-discovery, no scan)
|
|
61
62
|
scan-all Scan every agent-tool surface in the repo (compact table)
|
|
62
63
|
explain <server> Explain the verdict for one server from the last scan
|
|
63
64
|
verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
|
|
64
65
|
|
|
65
66
|
Advanced:
|
|
66
67
|
scan [target] Full ScanReport for an MCP config / npm:<pkg> / github:<repo>
|
|
68
|
+
scan --auto Discover and scan all agent configs (auto-discovery)
|
|
69
|
+
scan --agent <type> Discover and scan a specific agent (cursor, claude-code, claude-desktop, vscode, windsurf)
|
|
70
|
+
action inspect <f> Preflight a planned external action (calllint.action.v0)
|
|
71
|
+
inbox inspect <f> Preflight a normalized agent inbox event
|
|
67
72
|
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
68
73
|
baseline [target] Record the approved risk surface as a baseline
|
|
69
74
|
approve Record the repo-wide capability surface as approved state (L4)
|
|
70
|
-
receipt verify <f>
|
|
75
|
+
receipt verify <f> Validate a calllint.receipt.v0 (structure + signature if present)
|
|
76
|
+
receipt sign <f> Sign a receipt with a local key (--key; development/testing)
|
|
77
|
+
receipt keygen Generate a local ed25519 keypair (--out; development/testing)
|
|
71
78
|
gen-rule --host <h> Emit the CallLint agent-safety rule for a host (CLAUDE.md, etc.)
|
|
72
79
|
policy init Write a default calllint.policy.json
|
|
73
80
|
policy explain Show the effective policy
|
|
@@ -85,6 +92,8 @@ TARGETS
|
|
|
85
92
|
github:<owner/repo>[@ref] A GitHub repo (requires --online)
|
|
86
93
|
|
|
87
94
|
SCAN OPTIONS
|
|
95
|
+
--auto Discover and scan all agent configs (P0+P1: Cursor, Claude Code, Claude Desktop, VS Code, Windsurf)
|
|
96
|
+
--agent <type> Discover and scan a specific agent type
|
|
88
97
|
--changed Scan only the agent-tool configs changed in the git diff
|
|
89
98
|
--json Emit the ScanReport JSON (stable, emoji-free)
|
|
90
99
|
--compact One line per server
|
|
@@ -93,7 +102,7 @@ SCAN OPTIONS
|
|
|
93
102
|
--markdown Emit Markdown for PR comments / GitHub Step Summary
|
|
94
103
|
--html Emit a self-contained HTML report
|
|
95
104
|
--badge Emit a shields.io endpoint badge JSON (SAFE/REVIEW/UNKNOWN/BLOCK)
|
|
96
|
-
--receipt
|
|
105
|
+
--receipt Generate a cryptographically-verifiable audit receipt (for CI/compliance)
|
|
97
106
|
--receipt-out <f> Receipt output path (default: calllint-receipt.json)
|
|
98
107
|
--policy <file> Use a policy file (default: built-in defaults)
|
|
99
108
|
--stdin Read config JSON from stdin
|
|
@@ -108,6 +117,9 @@ VERIFY OPTIONS
|
|
|
108
117
|
--json Emit the drift report JSON
|
|
109
118
|
|
|
110
119
|
EXAMPLES
|
|
120
|
+
calllint inventory # List discovered agent configs
|
|
121
|
+
calllint scan --auto # Discover and scan all agents
|
|
122
|
+
calllint scan --agent cursor # Scan only Cursor configs
|
|
111
123
|
calllint check .cursor/mcp.json
|
|
112
124
|
calllint check npm:mcp-weather@1.0.0
|
|
113
125
|
echo "npx -y demo-mcp@1.2.3" | calllint check --stdin
|
|
@@ -115,7 +127,10 @@ EXAMPLES
|
|
|
115
127
|
calllint check ./mcp.json --json
|
|
116
128
|
calllint scan .cursor/mcp.json --markdown
|
|
117
129
|
calllint scan .cursor/mcp.json --badge > calllint-badge.json
|
|
118
|
-
calllint scan .cursor/mcp.json --receipt
|
|
130
|
+
calllint scan .cursor/mcp.json --receipt # Generate audit receipt
|
|
131
|
+
calllint receipt verify calllint-receipt.json # Verify receipt integrity
|
|
132
|
+
calllint action inspect payment.json
|
|
133
|
+
calllint inbox inspect gmail-reply.normalized.json
|
|
119
134
|
calllint verify ./mcp.json --ci
|
|
120
135
|
calllint approve && calllint verify --approved --ci
|
|
121
136
|
calllint explain filesystem
|
|
@@ -125,7 +140,7 @@ function helpCommand() {
|
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
// src/commands/scan.ts
|
|
128
|
-
import { join as
|
|
143
|
+
import { join as join11, resolve as resolve3 } from "node:path";
|
|
129
144
|
|
|
130
145
|
// ../../packages/policy/src/defaultPolicy.ts
|
|
131
146
|
function defaultPolicy() {
|
|
@@ -3883,11 +3898,437 @@ function readDocumentSurfaces(dir) {
|
|
|
3883
3898
|
}
|
|
3884
3899
|
|
|
3885
3900
|
// src/commands/scan.ts
|
|
3886
|
-
import { readFileSync as
|
|
3901
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3902
|
+
|
|
3903
|
+
// ../../packages/discovery/src/registry.ts
|
|
3904
|
+
var ExtractorRegistry = class {
|
|
3905
|
+
extractors = /* @__PURE__ */ new Map();
|
|
3906
|
+
/**
|
|
3907
|
+
* Register an agent extractor.
|
|
3908
|
+
*/
|
|
3909
|
+
register(extractor) {
|
|
3910
|
+
if (this.extractors.has(extractor.agentType)) {
|
|
3911
|
+
throw new Error(`Extractor already registered for agent type: ${extractor.agentType}`);
|
|
3912
|
+
}
|
|
3913
|
+
this.extractors.set(extractor.agentType, extractor);
|
|
3914
|
+
}
|
|
3915
|
+
/**
|
|
3916
|
+
* Get extractor for a specific agent type.
|
|
3917
|
+
*/
|
|
3918
|
+
get(agentType) {
|
|
3919
|
+
return this.extractors.get(agentType);
|
|
3920
|
+
}
|
|
3921
|
+
/**
|
|
3922
|
+
* Get all registered extractors.
|
|
3923
|
+
*/
|
|
3924
|
+
getAll() {
|
|
3925
|
+
return Array.from(this.extractors.values());
|
|
3926
|
+
}
|
|
3927
|
+
/**
|
|
3928
|
+
* Get extractors filtered by agent types.
|
|
3929
|
+
*/
|
|
3930
|
+
getByTypes(agentTypes) {
|
|
3931
|
+
return agentTypes.map((type) => this.extractors.get(type)).filter((e) => e !== void 0);
|
|
3932
|
+
}
|
|
3933
|
+
/**
|
|
3934
|
+
* Get extractors filtered by priority tier.
|
|
3935
|
+
*/
|
|
3936
|
+
getByPriority(priority) {
|
|
3937
|
+
return this.getAll().filter((e) => e.priority === priority);
|
|
3938
|
+
}
|
|
3939
|
+
/**
|
|
3940
|
+
* Get all extractors sorted by priority (P0 first, P3 last).
|
|
3941
|
+
*/
|
|
3942
|
+
getAllSortedByPriority() {
|
|
3943
|
+
const priorityOrder = {
|
|
3944
|
+
P0: 0,
|
|
3945
|
+
P1: 1,
|
|
3946
|
+
P2: 2,
|
|
3947
|
+
P3: 3
|
|
3948
|
+
};
|
|
3949
|
+
return this.getAll().sort((a, b) => {
|
|
3950
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
3951
|
+
});
|
|
3952
|
+
}
|
|
3953
|
+
/**
|
|
3954
|
+
* Clear all registered extractors (for testing).
|
|
3955
|
+
*/
|
|
3956
|
+
clear() {
|
|
3957
|
+
this.extractors.clear();
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
var registry = new ExtractorRegistry();
|
|
3961
|
+
|
|
3962
|
+
// ../../packages/discovery/src/extractors/base.ts
|
|
3963
|
+
var BaseAgentExtractor = class {
|
|
3964
|
+
/**
|
|
3965
|
+
* Resolve home directory (~) to absolute path.
|
|
3966
|
+
*/
|
|
3967
|
+
resolveHome() {
|
|
3968
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
3969
|
+
if (!home) {
|
|
3970
|
+
throw new Error("Could not resolve home directory (HOME/USERPROFILE not set)");
|
|
3971
|
+
}
|
|
3972
|
+
return home;
|
|
3973
|
+
}
|
|
3974
|
+
/**
|
|
3975
|
+
* Get platform-specific application data directory.
|
|
3976
|
+
*
|
|
3977
|
+
* Windows: %APPDATA% (Roaming)
|
|
3978
|
+
* macOS: ~/Library/Application Support
|
|
3979
|
+
* Linux: ~/.config (XDG_CONFIG_HOME or default)
|
|
3980
|
+
*/
|
|
3981
|
+
getAppDataDir() {
|
|
3982
|
+
const platform = process.platform;
|
|
3983
|
+
if (platform === "win32") {
|
|
3984
|
+
const appData = process.env.APPDATA;
|
|
3985
|
+
if (!appData) {
|
|
3986
|
+
throw new Error("Could not resolve APPDATA directory on Windows");
|
|
3987
|
+
}
|
|
3988
|
+
return appData;
|
|
3989
|
+
}
|
|
3990
|
+
if (platform === "darwin") {
|
|
3991
|
+
return `${this.resolveHome()}/Library/Application Support`;
|
|
3992
|
+
}
|
|
3993
|
+
return process.env.XDG_CONFIG_HOME || `${this.resolveHome()}/.config`;
|
|
3994
|
+
}
|
|
3995
|
+
};
|
|
3996
|
+
|
|
3997
|
+
// ../../packages/discovery/src/path-resolver.ts
|
|
3998
|
+
import { resolve as resolve2, join as join6, isAbsolute } from "node:path";
|
|
3999
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "node:fs";
|
|
4000
|
+
function resolvePath(path, cwd) {
|
|
4001
|
+
if (path.startsWith("~/") || path === "~") {
|
|
4002
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
4003
|
+
if (!home) {
|
|
4004
|
+
throw new Error("Could not resolve home directory (HOME/USERPROFILE not set)");
|
|
4005
|
+
}
|
|
4006
|
+
return join6(home, path.slice(2));
|
|
4007
|
+
}
|
|
4008
|
+
if (isAbsolute(path)) {
|
|
4009
|
+
return path;
|
|
4010
|
+
}
|
|
4011
|
+
return resolve2(cwd, path);
|
|
4012
|
+
}
|
|
4013
|
+
function isRegularFile(path) {
|
|
4014
|
+
try {
|
|
4015
|
+
if (!existsSync6(path)) {
|
|
4016
|
+
return false;
|
|
4017
|
+
}
|
|
4018
|
+
const stats = statSync3(path);
|
|
4019
|
+
return stats.isFile();
|
|
4020
|
+
} catch {
|
|
4021
|
+
return false;
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
function isReasonableSize(path) {
|
|
4025
|
+
try {
|
|
4026
|
+
const stats = statSync3(path);
|
|
4027
|
+
const maxSize = 10 * 1024 * 1024;
|
|
4028
|
+
return stats.size < maxSize;
|
|
4029
|
+
} catch {
|
|
4030
|
+
return false;
|
|
4031
|
+
}
|
|
4032
|
+
}
|
|
4033
|
+
function validateConfigPath(path) {
|
|
4034
|
+
return isRegularFile(path) && isReasonableSize(path);
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
// ../../packages/discovery/src/extractors/cursor.ts
|
|
4038
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4039
|
+
var CursorExtractor = class extends BaseAgentExtractor {
|
|
4040
|
+
agentType = "cursor";
|
|
4041
|
+
priority = "P0";
|
|
4042
|
+
discover(cwd) {
|
|
4043
|
+
const configs = [];
|
|
4044
|
+
const projectPath = resolvePath(".cursor/mcp.json", cwd);
|
|
4045
|
+
configs.push(this.createConfig(projectPath));
|
|
4046
|
+
try {
|
|
4047
|
+
const home = this.resolveHome();
|
|
4048
|
+
const userPath = resolvePath(".cursor/mcp.json", home);
|
|
4049
|
+
if (userPath !== projectPath) {
|
|
4050
|
+
configs.push(this.createConfig(userPath));
|
|
4051
|
+
}
|
|
4052
|
+
} catch {
|
|
4053
|
+
}
|
|
4054
|
+
return configs;
|
|
4055
|
+
}
|
|
4056
|
+
createConfig(configPath) {
|
|
4057
|
+
const exists = this.isValidConfig(configPath);
|
|
4058
|
+
return {
|
|
4059
|
+
agentType: this.agentType,
|
|
4060
|
+
configPath,
|
|
4061
|
+
exists,
|
|
4062
|
+
kind: "cursor-mcp-config",
|
|
4063
|
+
priority: this.priority
|
|
4064
|
+
};
|
|
4065
|
+
}
|
|
4066
|
+
/**
|
|
4067
|
+
* Check if path is a valid Cursor config.
|
|
4068
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4069
|
+
*/
|
|
4070
|
+
isValidConfig(path) {
|
|
4071
|
+
if (!validateConfigPath(path)) {
|
|
4072
|
+
return false;
|
|
4073
|
+
}
|
|
4074
|
+
try {
|
|
4075
|
+
const content = readFileSync7(path, "utf8");
|
|
4076
|
+
const json = JSON.parse(content);
|
|
4077
|
+
return typeof json === "object" && json !== null && "mcpServers" in json;
|
|
4078
|
+
} catch {
|
|
4079
|
+
return false;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
|
|
4084
|
+
// ../../packages/discovery/src/extractors/claude-code.ts
|
|
4085
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4086
|
+
import { join as join7 } from "node:path";
|
|
4087
|
+
var ClaudeCodeExtractor = class extends BaseAgentExtractor {
|
|
4088
|
+
agentType = "claude-code";
|
|
4089
|
+
priority = "P0";
|
|
4090
|
+
discover(cwd) {
|
|
4091
|
+
const configs = [];
|
|
4092
|
+
const projectPath = resolvePath(".claude/settings.json", cwd);
|
|
4093
|
+
configs.push(this.createConfig(projectPath));
|
|
4094
|
+
try {
|
|
4095
|
+
const userPath = this.getUserSettingsPath();
|
|
4096
|
+
if (userPath !== projectPath) {
|
|
4097
|
+
configs.push(this.createConfig(userPath));
|
|
4098
|
+
}
|
|
4099
|
+
} catch {
|
|
4100
|
+
}
|
|
4101
|
+
return configs;
|
|
4102
|
+
}
|
|
4103
|
+
getUserSettingsPath() {
|
|
4104
|
+
const appDataDir = this.getAppDataDir();
|
|
4105
|
+
return join7(appDataDir, "Claude", "settings.json");
|
|
4106
|
+
}
|
|
4107
|
+
createConfig(configPath) {
|
|
4108
|
+
const exists = this.isValidConfig(configPath);
|
|
4109
|
+
return {
|
|
4110
|
+
agentType: this.agentType,
|
|
4111
|
+
configPath,
|
|
4112
|
+
exists,
|
|
4113
|
+
kind: "claude-settings",
|
|
4114
|
+
priority: this.priority
|
|
4115
|
+
};
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Check if path is a valid Claude Code config.
|
|
4119
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4120
|
+
*/
|
|
4121
|
+
isValidConfig(path) {
|
|
4122
|
+
if (!validateConfigPath(path)) {
|
|
4123
|
+
return false;
|
|
4124
|
+
}
|
|
4125
|
+
try {
|
|
4126
|
+
const content = readFileSync8(path, "utf8");
|
|
4127
|
+
const json = JSON.parse(content);
|
|
4128
|
+
return typeof json === "object" && json !== null && "mcpServers" in json;
|
|
4129
|
+
} catch {
|
|
4130
|
+
return false;
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
4133
|
+
};
|
|
4134
|
+
|
|
4135
|
+
// ../../packages/discovery/src/extractors/claude-desktop.ts
|
|
4136
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4137
|
+
import { join as join8 } from "node:path";
|
|
4138
|
+
var ClaudeDesktopExtractor = class extends BaseAgentExtractor {
|
|
4139
|
+
agentType = "claude-desktop";
|
|
4140
|
+
priority = "P0";
|
|
4141
|
+
discover(_cwd) {
|
|
4142
|
+
const configs = [];
|
|
4143
|
+
try {
|
|
4144
|
+
const userPath = this.getUserConfigPath();
|
|
4145
|
+
configs.push(this.createConfig(userPath));
|
|
4146
|
+
} catch {
|
|
4147
|
+
}
|
|
4148
|
+
return configs;
|
|
4149
|
+
}
|
|
4150
|
+
getUserConfigPath() {
|
|
4151
|
+
const appDataDir = this.getAppDataDir();
|
|
4152
|
+
return join8(appDataDir, "Claude", "claude_desktop_config.json");
|
|
4153
|
+
}
|
|
4154
|
+
createConfig(configPath) {
|
|
4155
|
+
const exists = this.isValidConfig(configPath);
|
|
4156
|
+
return {
|
|
4157
|
+
agentType: this.agentType,
|
|
4158
|
+
configPath,
|
|
4159
|
+
exists,
|
|
4160
|
+
kind: "claude-settings",
|
|
4161
|
+
priority: this.priority
|
|
4162
|
+
};
|
|
4163
|
+
}
|
|
4164
|
+
/**
|
|
4165
|
+
* Check if path is a valid Claude Desktop config.
|
|
4166
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4167
|
+
*/
|
|
4168
|
+
isValidConfig(path) {
|
|
4169
|
+
if (!validateConfigPath(path)) {
|
|
4170
|
+
return false;
|
|
4171
|
+
}
|
|
4172
|
+
try {
|
|
4173
|
+
const content = readFileSync9(path, "utf8");
|
|
4174
|
+
const json = JSON.parse(content);
|
|
4175
|
+
return typeof json === "object" && json !== null && "mcpServers" in json;
|
|
4176
|
+
} catch {
|
|
4177
|
+
return false;
|
|
4178
|
+
}
|
|
4179
|
+
}
|
|
4180
|
+
};
|
|
4181
|
+
|
|
4182
|
+
// ../../packages/discovery/src/extractors/vscode.ts
|
|
4183
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4184
|
+
import { join as join9 } from "node:path";
|
|
4185
|
+
var VSCodeExtractor = class extends BaseAgentExtractor {
|
|
4186
|
+
agentType = "vscode";
|
|
4187
|
+
priority = "P1";
|
|
4188
|
+
discover(cwd) {
|
|
4189
|
+
const configs = [];
|
|
4190
|
+
try {
|
|
4191
|
+
const userPath = this.getUserConfigPath();
|
|
4192
|
+
configs.push(this.createConfig(userPath));
|
|
4193
|
+
} catch {
|
|
4194
|
+
}
|
|
4195
|
+
return configs;
|
|
4196
|
+
}
|
|
4197
|
+
getUserConfigPath() {
|
|
4198
|
+
const appDataDir = this.getAppDataDir();
|
|
4199
|
+
return join9(appDataDir, "Code", "User", "mcp.json");
|
|
4200
|
+
}
|
|
4201
|
+
createConfig(configPath) {
|
|
4202
|
+
const exists = this.isValidConfig(configPath);
|
|
4203
|
+
return {
|
|
4204
|
+
agentType: this.agentType,
|
|
4205
|
+
configPath,
|
|
4206
|
+
exists,
|
|
4207
|
+
kind: "vscode-mcp-config",
|
|
4208
|
+
priority: this.priority
|
|
4209
|
+
};
|
|
4210
|
+
}
|
|
4211
|
+
/**
|
|
4212
|
+
* Check if path is a valid VS Code MCP config.
|
|
4213
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4214
|
+
*/
|
|
4215
|
+
isValidConfig(path) {
|
|
4216
|
+
if (!validateConfigPath(path)) {
|
|
4217
|
+
return false;
|
|
4218
|
+
}
|
|
4219
|
+
try {
|
|
4220
|
+
const content = readFileSync10(path, "utf8");
|
|
4221
|
+
const json = JSON.parse(content);
|
|
4222
|
+
return typeof json === "object" && json !== null && "mcpServers" in json && json.mcpServers !== null;
|
|
4223
|
+
} catch {
|
|
4224
|
+
return false;
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
};
|
|
4228
|
+
|
|
4229
|
+
// ../../packages/discovery/src/extractors/windsurf.ts
|
|
4230
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
4231
|
+
import { join as join10 } from "node:path";
|
|
4232
|
+
var WindsurfExtractor = class extends BaseAgentExtractor {
|
|
4233
|
+
agentType = "windsurf";
|
|
4234
|
+
priority = "P1";
|
|
4235
|
+
discover(cwd) {
|
|
4236
|
+
const configs = [];
|
|
4237
|
+
try {
|
|
4238
|
+
const userPath = this.getUserConfigPath();
|
|
4239
|
+
configs.push(this.createConfig(userPath));
|
|
4240
|
+
} catch {
|
|
4241
|
+
}
|
|
4242
|
+
return configs;
|
|
4243
|
+
}
|
|
4244
|
+
getUserConfigPath() {
|
|
4245
|
+
const appDataDir = this.getAppDataDir();
|
|
4246
|
+
return join10(appDataDir, "Windsurf", "mcp.json");
|
|
4247
|
+
}
|
|
4248
|
+
createConfig(configPath) {
|
|
4249
|
+
const exists = this.isValidConfig(configPath);
|
|
4250
|
+
return {
|
|
4251
|
+
agentType: this.agentType,
|
|
4252
|
+
configPath,
|
|
4253
|
+
exists,
|
|
4254
|
+
kind: "windsurf-mcp-config",
|
|
4255
|
+
priority: this.priority
|
|
4256
|
+
};
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* Check if path is a valid Windsurf MCP config.
|
|
4260
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4261
|
+
*/
|
|
4262
|
+
isValidConfig(path) {
|
|
4263
|
+
if (!validateConfigPath(path)) {
|
|
4264
|
+
return false;
|
|
4265
|
+
}
|
|
4266
|
+
try {
|
|
4267
|
+
const content = readFileSync11(path, "utf8");
|
|
4268
|
+
const json = JSON.parse(content);
|
|
4269
|
+
return typeof json === "object" && json !== null && "mcpServers" in json && json.mcpServers !== null;
|
|
4270
|
+
} catch {
|
|
4271
|
+
return false;
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
};
|
|
4275
|
+
|
|
4276
|
+
// ../../packages/discovery/src/bootstrap.ts
|
|
4277
|
+
function bootstrapExtractors() {
|
|
4278
|
+
registry.register(new CursorExtractor());
|
|
4279
|
+
registry.register(new ClaudeCodeExtractor());
|
|
4280
|
+
registry.register(new ClaudeDesktopExtractor());
|
|
4281
|
+
registry.register(new VSCodeExtractor());
|
|
4282
|
+
registry.register(new WindsurfExtractor());
|
|
4283
|
+
}
|
|
4284
|
+
bootstrapExtractors();
|
|
4285
|
+
|
|
4286
|
+
// ../../packages/discovery/src/discovery-engine.ts
|
|
4287
|
+
function discoverConfigs(options) {
|
|
4288
|
+
const { cwd, agentTypes, includeMissing = false } = options;
|
|
4289
|
+
const extractors = agentTypes ? registry.getByTypes(agentTypes) : registry.getAllSortedByPriority();
|
|
4290
|
+
const results = extractors.map((extractor) => {
|
|
4291
|
+
try {
|
|
4292
|
+
return extractor.discover(cwd);
|
|
4293
|
+
} catch (error) {
|
|
4294
|
+
console.error(`[discovery] Extractor ${extractor.agentType} failed:`, error);
|
|
4295
|
+
return [];
|
|
4296
|
+
}
|
|
4297
|
+
});
|
|
4298
|
+
const allDiscovered = results.flat();
|
|
4299
|
+
const discovered = includeMissing ? allDiscovered : allDiscovered.filter((config) => config.exists);
|
|
4300
|
+
const searchedPaths = allDiscovered.map((config) => config.configPath);
|
|
4301
|
+
return {
|
|
4302
|
+
cwd,
|
|
4303
|
+
discovered,
|
|
4304
|
+
searchedPaths
|
|
4305
|
+
};
|
|
4306
|
+
}
|
|
4307
|
+
function discoverAgent(agentType, options) {
|
|
4308
|
+
const extractor = registry.get(agentType);
|
|
4309
|
+
if (!extractor) {
|
|
4310
|
+
throw new Error(`Unknown agent type: ${agentType}`);
|
|
4311
|
+
}
|
|
4312
|
+
try {
|
|
4313
|
+
return extractor.discover(options.cwd);
|
|
4314
|
+
} catch (error) {
|
|
4315
|
+
console.error(`[discovery] Failed to discover ${agentType}:`, error);
|
|
4316
|
+
return [];
|
|
4317
|
+
}
|
|
4318
|
+
}
|
|
4319
|
+
|
|
4320
|
+
// src/commands/scan.ts
|
|
3887
4321
|
function scanCommand(args, deps) {
|
|
3888
4322
|
if (flagBool(args.flags, "changed")) {
|
|
3889
4323
|
return scanChangedCommand(args, deps);
|
|
3890
4324
|
}
|
|
4325
|
+
if (flagBool(args.flags, "auto")) {
|
|
4326
|
+
return scanAutoCommand(args, deps);
|
|
4327
|
+
}
|
|
4328
|
+
const agentType = flagStr(args.flags, "agent");
|
|
4329
|
+
if (agentType) {
|
|
4330
|
+
return scanAgentCommand(agentType, args, deps);
|
|
4331
|
+
}
|
|
3891
4332
|
const policyPath = flagStr(args.flags, "policy");
|
|
3892
4333
|
let policy;
|
|
3893
4334
|
try {
|
|
@@ -3908,7 +4349,7 @@ function scanCommand(args, deps) {
|
|
|
3908
4349
|
}
|
|
3909
4350
|
function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true) {
|
|
3910
4351
|
const surfaceDir = flagStr(args.flags, "surface-dir");
|
|
3911
|
-
const surfaces = surfaceDir ? readDocumentSurfaces(
|
|
4352
|
+
const surfaces = surfaceDir ? readDocumentSurfaces(resolve3(deps.cwd, surfaceDir)) : void 0;
|
|
3912
4353
|
let summary;
|
|
3913
4354
|
try {
|
|
3914
4355
|
summary = scanConfigText(text, configPath, {
|
|
@@ -3930,7 +4371,7 @@ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true
|
|
|
3930
4371
|
}
|
|
3931
4372
|
if (deps.writeCacheFile !== false) {
|
|
3932
4373
|
try {
|
|
3933
|
-
writeCache(summary,
|
|
4374
|
+
writeCache(summary, join11(deps.cwd, ".calllint", "last-scan.json"));
|
|
3934
4375
|
} catch {
|
|
3935
4376
|
}
|
|
3936
4377
|
}
|
|
@@ -3956,7 +4397,7 @@ function writeReceiptFile(summary, text, configPath, policy, args, deps) {
|
|
|
3956
4397
|
},
|
|
3957
4398
|
deps.generatedAt
|
|
3958
4399
|
);
|
|
3959
|
-
const outPath =
|
|
4400
|
+
const outPath = resolve3(
|
|
3960
4401
|
deps.cwd,
|
|
3961
4402
|
flagStr(args.flags, "receipt-out") ?? "calllint-receipt.json"
|
|
3962
4403
|
);
|
|
@@ -4004,7 +4445,7 @@ function scanChangedCommand(args, deps) {
|
|
|
4004
4445
|
};
|
|
4005
4446
|
}
|
|
4006
4447
|
const results = paths.map((p) => {
|
|
4007
|
-
const text =
|
|
4448
|
+
const text = readFileSync12(p, "utf8");
|
|
4008
4449
|
return scanOneConfig(text, p, policy, args, deps);
|
|
4009
4450
|
});
|
|
4010
4451
|
const exitCode = results.reduce((worst, r) => Math.max(worst, r.exitCode), EXIT.OK);
|
|
@@ -4017,6 +4458,95 @@ function scanChangedCommand(args, deps) {
|
|
|
4017
4458
|
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
4018
4459
|
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
4019
4460
|
}
|
|
4461
|
+
function scanAutoCommand(args, deps) {
|
|
4462
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
4463
|
+
let policy;
|
|
4464
|
+
try {
|
|
4465
|
+
policy = loadPolicyOrDefault(policyPath);
|
|
4466
|
+
} catch (err2) {
|
|
4467
|
+
return {
|
|
4468
|
+
stdout: "",
|
|
4469
|
+
stderr: err2 instanceof Error ? err2.message : String(err2),
|
|
4470
|
+
exitCode: EXIT.ERROR
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
const discovered = discoverConfigs({ cwd: deps.cwd });
|
|
4474
|
+
if (discovered.discovered.length === 0) {
|
|
4475
|
+
return {
|
|
4476
|
+
stdout: "",
|
|
4477
|
+
stderr: "No agent configs discovered. Try: calllint inventory\n",
|
|
4478
|
+
exitCode: EXIT.ERROR
|
|
4479
|
+
};
|
|
4480
|
+
}
|
|
4481
|
+
const results = [];
|
|
4482
|
+
for (const config of discovered.discovered) {
|
|
4483
|
+
let text;
|
|
4484
|
+
try {
|
|
4485
|
+
text = readFileSync12(config.configPath, "utf8");
|
|
4486
|
+
} catch (err2) {
|
|
4487
|
+
results.push({
|
|
4488
|
+
stdout: "",
|
|
4489
|
+
stderr: `Could not read ${config.configPath}: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
4490
|
+
exitCode: EXIT.ERROR
|
|
4491
|
+
});
|
|
4492
|
+
continue;
|
|
4493
|
+
}
|
|
4494
|
+
const result = scanOneConfig(text, config.configPath, policy, args, deps, false);
|
|
4495
|
+
results.push(result);
|
|
4496
|
+
}
|
|
4497
|
+
const isJson = flagBool(args.flags, "json");
|
|
4498
|
+
const stdout = isJson ? JSON.stringify(results.map((r) => JSON.parse(r.stdout)), null, 2) + "\n" : results.map((r) => r.stdout).join("\n---\n\n");
|
|
4499
|
+
const exitCode = flagBool(args.flags, "ci") ? Math.max(...results.map((r) => r.exitCode)) : EXIT.OK;
|
|
4500
|
+
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
4501
|
+
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
4502
|
+
}
|
|
4503
|
+
function scanAgentCommand(agentType, args, deps) {
|
|
4504
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
4505
|
+
let policy;
|
|
4506
|
+
try {
|
|
4507
|
+
policy = loadPolicyOrDefault(policyPath);
|
|
4508
|
+
} catch (err2) {
|
|
4509
|
+
return {
|
|
4510
|
+
stdout: "",
|
|
4511
|
+
stderr: err2 instanceof Error ? err2.message : String(err2),
|
|
4512
|
+
exitCode: EXIT.ERROR
|
|
4513
|
+
};
|
|
4514
|
+
}
|
|
4515
|
+
const discovered = discoverAgent(agentType, { cwd: deps.cwd });
|
|
4516
|
+
const existing = discovered.filter((c) => c.exists);
|
|
4517
|
+
if (existing.length === 0) {
|
|
4518
|
+
return {
|
|
4519
|
+
stdout: "",
|
|
4520
|
+
stderr: `No config found for agent '${agentType}'. Try: calllint inventory
|
|
4521
|
+
`,
|
|
4522
|
+
exitCode: EXIT.ERROR
|
|
4523
|
+
};
|
|
4524
|
+
}
|
|
4525
|
+
const results = [];
|
|
4526
|
+
for (const config of existing) {
|
|
4527
|
+
let text;
|
|
4528
|
+
try {
|
|
4529
|
+
text = readFileSync12(config.configPath, "utf8");
|
|
4530
|
+
} catch (err2) {
|
|
4531
|
+
results.push({
|
|
4532
|
+
stdout: "",
|
|
4533
|
+
stderr: `Could not read ${config.configPath}: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
4534
|
+
exitCode: EXIT.ERROR
|
|
4535
|
+
});
|
|
4536
|
+
continue;
|
|
4537
|
+
}
|
|
4538
|
+
const result = scanOneConfig(text, config.configPath, policy, args, deps, true);
|
|
4539
|
+
results.push(result);
|
|
4540
|
+
}
|
|
4541
|
+
if (results.length === 1) {
|
|
4542
|
+
return results[0];
|
|
4543
|
+
}
|
|
4544
|
+
const isJson = flagBool(args.flags, "json");
|
|
4545
|
+
const stdout = isJson ? JSON.stringify(results.map((r) => JSON.parse(r.stdout)), null, 2) + "\n" : results.map((r) => r.stdout).join("\n---\n\n");
|
|
4546
|
+
const exitCode = flagBool(args.flags, "ci") ? Math.max(...results.map((r) => r.exitCode)) : EXIT.OK;
|
|
4547
|
+
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
4548
|
+
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
4549
|
+
}
|
|
4020
4550
|
|
|
4021
4551
|
// src/commands/check.ts
|
|
4022
4552
|
function checkCommand(args, deps) {
|
|
@@ -4106,7 +4636,7 @@ function worstExit2(decisions) {
|
|
|
4106
4636
|
|
|
4107
4637
|
// src/commands/genRule.ts
|
|
4108
4638
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
4109
|
-
import { dirname as dirname3, join as
|
|
4639
|
+
import { dirname as dirname3, join as join12, resolve as resolve4 } from "node:path";
|
|
4110
4640
|
function isRuleHost(v) {
|
|
4111
4641
|
return v !== void 0 && RULE_HOSTS.includes(v);
|
|
4112
4642
|
}
|
|
@@ -4137,7 +4667,7 @@ Run \`calllint gen-rule\` to list hosts.`,
|
|
|
4137
4667
|
return { stdout: content, exitCode: EXIT.OK };
|
|
4138
4668
|
}
|
|
4139
4669
|
const rel = flagStr(args.flags, "out") ?? RULE_TARGETS[host].path;
|
|
4140
|
-
const abs =
|
|
4670
|
+
const abs = resolve4(deps.cwd, rel);
|
|
4141
4671
|
const write = deps.writeFile ?? defaultWrite;
|
|
4142
4672
|
try {
|
|
4143
4673
|
write(abs, content);
|
|
@@ -4203,7 +4733,7 @@ function diagnosticsCommand(args, deps) {
|
|
|
4203
4733
|
}
|
|
4204
4734
|
|
|
4205
4735
|
// src/commands/explain.ts
|
|
4206
|
-
import { join as
|
|
4736
|
+
import { join as join13 } from "node:path";
|
|
4207
4737
|
function explainCommand(args, deps) {
|
|
4208
4738
|
const serverName = args.positionals[0];
|
|
4209
4739
|
if (!serverName) {
|
|
@@ -4213,7 +4743,7 @@ function explainCommand(args, deps) {
|
|
|
4213
4743
|
exitCode: EXIT.USAGE
|
|
4214
4744
|
};
|
|
4215
4745
|
}
|
|
4216
|
-
const summary = readCache(
|
|
4746
|
+
const summary = readCache(join13(deps.cwd, ".calllint", "last-scan.json"));
|
|
4217
4747
|
if (!summary) {
|
|
4218
4748
|
return {
|
|
4219
4749
|
stdout: "",
|
|
@@ -4235,13 +4765,13 @@ function explainCommand(args, deps) {
|
|
|
4235
4765
|
}
|
|
4236
4766
|
|
|
4237
4767
|
// src/commands/policy.ts
|
|
4238
|
-
import { existsSync as
|
|
4239
|
-
import { join as
|
|
4768
|
+
import { existsSync as existsSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4769
|
+
import { join as join14 } from "node:path";
|
|
4240
4770
|
function policyCommand(args, deps) {
|
|
4241
4771
|
const sub = args.positionals[0];
|
|
4242
4772
|
if (sub === "init") {
|
|
4243
|
-
const path =
|
|
4244
|
-
if (
|
|
4773
|
+
const path = join14(deps.cwd, "calllint.policy.json");
|
|
4774
|
+
if (existsSync9(path) && !flagBool(args.flags, "force")) {
|
|
4245
4775
|
return {
|
|
4246
4776
|
stdout: "",
|
|
4247
4777
|
stderr: `${path} already exists. Use --force to overwrite.`,
|
|
@@ -4271,7 +4801,7 @@ function policyCommand(args, deps) {
|
|
|
4271
4801
|
}
|
|
4272
4802
|
|
|
4273
4803
|
// src/commands/verify.ts
|
|
4274
|
-
import { join as
|
|
4804
|
+
import { join as join15 } from "node:path";
|
|
4275
4805
|
function loadPolicy(args) {
|
|
4276
4806
|
try {
|
|
4277
4807
|
return loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
@@ -4280,7 +4810,7 @@ function loadPolicy(args) {
|
|
|
4280
4810
|
}
|
|
4281
4811
|
}
|
|
4282
4812
|
function baselinePathFor(args, cwd) {
|
|
4283
|
-
return flagStr(args.flags, "baseline") ??
|
|
4813
|
+
return flagStr(args.flags, "baseline") ?? join15(cwd, ".calllint", "baseline.json");
|
|
4284
4814
|
}
|
|
4285
4815
|
function baselineCommand(args, deps) {
|
|
4286
4816
|
const policy = loadPolicy(args);
|
|
@@ -4406,8 +4936,8 @@ function approveCommand(args, deps) {
|
|
|
4406
4936
|
}
|
|
4407
4937
|
|
|
4408
4938
|
// src/commands/receipt.ts
|
|
4409
|
-
import { readFileSync as
|
|
4410
|
-
import { resolve as
|
|
4939
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4940
|
+
import { resolve as resolve5 } from "node:path";
|
|
4411
4941
|
|
|
4412
4942
|
// ../../packages/signature/src/ed25519.ts
|
|
4413
4943
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -5016,7 +5546,7 @@ function receiptVerify(args, deps) {
|
|
|
5016
5546
|
}
|
|
5017
5547
|
let raw;
|
|
5018
5548
|
try {
|
|
5019
|
-
raw =
|
|
5549
|
+
raw = readFileSync13(resolve5(deps.cwd, file), "utf8");
|
|
5020
5550
|
} catch (e) {
|
|
5021
5551
|
return {
|
|
5022
5552
|
stdout: "",
|
|
@@ -5069,6 +5599,43 @@ function receiptVerify(args, deps) {
|
|
|
5069
5599
|
exitCode: EXIT.OK
|
|
5070
5600
|
};
|
|
5071
5601
|
}
|
|
5602
|
+
if (flagBool(args.flags, "verbose")) {
|
|
5603
|
+
const stdout2 = [
|
|
5604
|
+
"\u2713 CallLint receipt: valid",
|
|
5605
|
+
"",
|
|
5606
|
+
"Receipt Information:",
|
|
5607
|
+
` ID: ${receipt.receipt_id}`,
|
|
5608
|
+
` Schema: ${receipt.schema_version}`,
|
|
5609
|
+
` Created: ${receipt.created_at}`,
|
|
5610
|
+
"",
|
|
5611
|
+
"Tool:",
|
|
5612
|
+
` Name: ${receipt.tool.name}`,
|
|
5613
|
+
` Version: ${receipt.tool.version}`,
|
|
5614
|
+
"",
|
|
5615
|
+
"Subject:",
|
|
5616
|
+
` Type: ${receipt.subject.type}`,
|
|
5617
|
+
` Target: ${receipt.subject.target}`,
|
|
5618
|
+
"",
|
|
5619
|
+
`Verdict: ${receipt.verdict}`,
|
|
5620
|
+
"",
|
|
5621
|
+
"Hashes:",
|
|
5622
|
+
` Input: ${receipt.hashes.input_hash}`,
|
|
5623
|
+
` Policy: ${receipt.hashes.policy_hash}`,
|
|
5624
|
+
` Report: ${receipt.hashes.report_hash}`,
|
|
5625
|
+
` Ruleset: ${receipt.hashes.ruleset_hash}`,
|
|
5626
|
+
"",
|
|
5627
|
+
"Risk Summary:",
|
|
5628
|
+
` BLOCK: ${receipt.risk_counts.block ?? 0}`,
|
|
5629
|
+
` REVIEW: ${receipt.risk_counts.review ?? 0}`,
|
|
5630
|
+
` UNKNOWN: ${receipt.risk_counts.unknown ?? 0}`,
|
|
5631
|
+
` SAFE: ${receipt.risk_counts.safe ?? 0}`,
|
|
5632
|
+
"",
|
|
5633
|
+
`Network: ${receipt.trust_boundaries.network_used ? "Used" : "Not used"}`,
|
|
5634
|
+
"",
|
|
5635
|
+
"Signature: unsigned local receipt"
|
|
5636
|
+
].join("\n");
|
|
5637
|
+
return { stdout: stdout2, exitCode: EXIT.OK };
|
|
5638
|
+
}
|
|
5072
5639
|
const stdout = [
|
|
5073
5640
|
"\u2713 CallLint receipt: valid",
|
|
5074
5641
|
` Receipt ID: ${receipt.receipt_id}`,
|
|
@@ -5098,7 +5665,7 @@ function verifySignature(receipt, args, deps) {
|
|
|
5098
5665
|
}
|
|
5099
5666
|
let publicKey;
|
|
5100
5667
|
try {
|
|
5101
|
-
const keyJson = JSON.parse(
|
|
5668
|
+
const keyJson = JSON.parse(readFileSync13(resolve5(deps.cwd, publicKeyFlag), "utf8"));
|
|
5102
5669
|
publicKey = keyJson.public_key;
|
|
5103
5670
|
if (typeof publicKey !== "string") {
|
|
5104
5671
|
throw new Error("key file has no string 'public_key' field");
|
|
@@ -5143,6 +5710,47 @@ function verifySignature(receipt, args, deps) {
|
|
|
5143
5710
|
];
|
|
5144
5711
|
return { stdout: "", stderr: lines.join("\n"), exitCode: 1 };
|
|
5145
5712
|
}
|
|
5713
|
+
if (flagBool(args.flags, "verbose")) {
|
|
5714
|
+
const stdout2 = [
|
|
5715
|
+
"\u2713 CallLint receipt: valid",
|
|
5716
|
+
"",
|
|
5717
|
+
"Receipt Information:",
|
|
5718
|
+
` ID: ${receipt.receipt_id}`,
|
|
5719
|
+
` Schema: ${receipt.schema_version}`,
|
|
5720
|
+
` Created: ${receipt.created_at}`,
|
|
5721
|
+
"",
|
|
5722
|
+
"Tool:",
|
|
5723
|
+
` Name: ${receipt.tool.name}`,
|
|
5724
|
+
` Version: ${receipt.tool.version}`,
|
|
5725
|
+
"",
|
|
5726
|
+
"Subject:",
|
|
5727
|
+
` Type: ${receipt.subject.type}`,
|
|
5728
|
+
` Target: ${receipt.subject.target}`,
|
|
5729
|
+
"",
|
|
5730
|
+
`Verdict: ${receipt.verdict}`,
|
|
5731
|
+
"",
|
|
5732
|
+
"Hashes:",
|
|
5733
|
+
` Input: ${receipt.hashes.input_hash}`,
|
|
5734
|
+
` Policy: ${receipt.hashes.policy_hash}`,
|
|
5735
|
+
` Report: ${receipt.hashes.report_hash}`,
|
|
5736
|
+
` Ruleset: ${receipt.hashes.ruleset_hash}`,
|
|
5737
|
+
"",
|
|
5738
|
+
"Risk Summary:",
|
|
5739
|
+
` BLOCK: ${receipt.risk_counts.block ?? 0}`,
|
|
5740
|
+
` REVIEW: ${receipt.risk_counts.review ?? 0}`,
|
|
5741
|
+
` UNKNOWN: ${receipt.risk_counts.unknown ?? 0}`,
|
|
5742
|
+
` SAFE: ${receipt.risk_counts.safe ?? 0}`,
|
|
5743
|
+
"",
|
|
5744
|
+
`Network: ${receipt.trust_boundaries.network_used ? "Used" : "Not used"}`,
|
|
5745
|
+
"",
|
|
5746
|
+
"Signature:",
|
|
5747
|
+
` Status: valid`,
|
|
5748
|
+
` Key ID: ${sig.key_id}`,
|
|
5749
|
+
` Algorithm: ${sig.algorithm}`,
|
|
5750
|
+
` Signed at: ${sig.signed_at}`
|
|
5751
|
+
].join("\n");
|
|
5752
|
+
return { stdout: stdout2, exitCode: EXIT.OK };
|
|
5753
|
+
}
|
|
5146
5754
|
const stdout = [
|
|
5147
5755
|
"\u2713 CallLint receipt: valid",
|
|
5148
5756
|
` Receipt ID: ${receipt.receipt_id}`,
|
|
@@ -5171,7 +5779,7 @@ function receiptSign(args, deps) {
|
|
|
5171
5779
|
}
|
|
5172
5780
|
let unsignedReceipt;
|
|
5173
5781
|
try {
|
|
5174
|
-
unsignedReceipt = JSON.parse(
|
|
5782
|
+
unsignedReceipt = JSON.parse(readFileSync13(resolve5(deps.cwd, receiptFile), "utf8"));
|
|
5175
5783
|
} catch (e) {
|
|
5176
5784
|
return {
|
|
5177
5785
|
stdout: "",
|
|
@@ -5188,7 +5796,7 @@ function receiptSign(args, deps) {
|
|
|
5188
5796
|
}
|
|
5189
5797
|
let keypair;
|
|
5190
5798
|
try {
|
|
5191
|
-
const keyJson = JSON.parse(
|
|
5799
|
+
const keyJson = JSON.parse(readFileSync13(resolve5(deps.cwd, keyFile), "utf8"));
|
|
5192
5800
|
keypair = importKeypair(keyJson);
|
|
5193
5801
|
} catch (e) {
|
|
5194
5802
|
return {
|
|
@@ -5200,7 +5808,7 @@ function receiptSign(args, deps) {
|
|
|
5200
5808
|
try {
|
|
5201
5809
|
const signature = signReceipt(unsignedReceipt, keypair);
|
|
5202
5810
|
const signedReceipt = { ...unsignedReceipt, signature };
|
|
5203
|
-
writeFileSync6(
|
|
5811
|
+
writeFileSync6(resolve5(deps.cwd, outFile), JSON.stringify(signedReceipt, null, 2) + "\n", "utf8");
|
|
5204
5812
|
const stdout = [
|
|
5205
5813
|
"\u2713 Receipt signed successfully",
|
|
5206
5814
|
` Key ID: ${signature.key_id}`,
|
|
@@ -5235,7 +5843,7 @@ function receiptKeygen(args, deps) {
|
|
|
5235
5843
|
try {
|
|
5236
5844
|
const keypair = generateKeypair(keyId);
|
|
5237
5845
|
const exported = exportKeypair(keypair);
|
|
5238
|
-
writeFileSync6(
|
|
5846
|
+
writeFileSync6(resolve5(deps.cwd, outFile), JSON.stringify(exported, null, 2) + "\n", "utf8");
|
|
5239
5847
|
const stdout = [
|
|
5240
5848
|
"\u2713 Keypair generated successfully",
|
|
5241
5849
|
` Key ID: ${keyId}`,
|
|
@@ -5255,8 +5863,8 @@ function receiptKeygen(args, deps) {
|
|
|
5255
5863
|
}
|
|
5256
5864
|
|
|
5257
5865
|
// src/commands/action.ts
|
|
5258
|
-
import { readFileSync as
|
|
5259
|
-
import { resolve as
|
|
5866
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
5867
|
+
import { resolve as resolve6 } from "node:path";
|
|
5260
5868
|
|
|
5261
5869
|
// ../../packages/action-analyzer/src/types.ts
|
|
5262
5870
|
var KIND_RISK_PROFILES = {
|
|
@@ -5730,8 +6338,8 @@ function actionInspect(args, deps) {
|
|
|
5730
6338
|
};
|
|
5731
6339
|
}
|
|
5732
6340
|
try {
|
|
5733
|
-
const absolutePath =
|
|
5734
|
-
const content =
|
|
6341
|
+
const absolutePath = resolve6(deps.cwd, actionFile);
|
|
6342
|
+
const content = readFileSync14(absolutePath, "utf-8");
|
|
5735
6343
|
const descriptor = JSON.parse(content);
|
|
5736
6344
|
if (descriptor.schema_version !== "calllint.action.v0") {
|
|
5737
6345
|
return {
|
|
@@ -5742,7 +6350,7 @@ Expected: calllint.action.v0`,
|
|
|
5742
6350
|
};
|
|
5743
6351
|
}
|
|
5744
6352
|
const policyPath = args.flags["policy"];
|
|
5745
|
-
const policy = loadPolicyOrDefault(policyPath ?
|
|
6353
|
+
const policy = loadPolicyOrDefault(policyPath ? resolve6(deps.cwd, policyPath) : void 0);
|
|
5746
6354
|
const findings = analyzeAction(descriptor);
|
|
5747
6355
|
const verdict = computeActionVerdict(findings, policy);
|
|
5748
6356
|
const actionReport = {
|
|
@@ -5869,7 +6477,7 @@ function writeActionReceipt(descriptor, rawContent, actionReport, policy, args,
|
|
|
5869
6477
|
},
|
|
5870
6478
|
deps.generatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
5871
6479
|
);
|
|
5872
|
-
const outPath =
|
|
6480
|
+
const outPath = resolve6(
|
|
5873
6481
|
deps.cwd,
|
|
5874
6482
|
args.flags["receipt-out"] ?? "calllint-action-receipt.json"
|
|
5875
6483
|
);
|
|
@@ -5882,8 +6490,8 @@ function writeActionReceipt(descriptor, rawContent, actionReport, policy, args,
|
|
|
5882
6490
|
}
|
|
5883
6491
|
|
|
5884
6492
|
// src/commands/inbox.ts
|
|
5885
|
-
import { readFileSync as
|
|
5886
|
-
import { resolve as
|
|
6493
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
6494
|
+
import { resolve as resolve7 } from "node:path";
|
|
5887
6495
|
function inboxCommand(args, deps) {
|
|
5888
6496
|
const subcommand = args.positionals[0];
|
|
5889
6497
|
if (!subcommand || subcommand === "help") {
|
|
@@ -5913,8 +6521,8 @@ function inboxInspect(args, deps) {
|
|
|
5913
6521
|
};
|
|
5914
6522
|
}
|
|
5915
6523
|
try {
|
|
5916
|
-
const absolutePath =
|
|
5917
|
-
const content =
|
|
6524
|
+
const absolutePath = resolve7(deps.cwd, eventFile);
|
|
6525
|
+
const content = readFileSync15(absolutePath, "utf-8");
|
|
5918
6526
|
const event = JSON.parse(content);
|
|
5919
6527
|
if (event.schema_version !== "calllint.agent-inbox-event.v0") {
|
|
5920
6528
|
return {
|
|
@@ -5932,7 +6540,7 @@ Expected: calllint.agent-inbox-event.v0`,
|
|
|
5932
6540
|
};
|
|
5933
6541
|
}
|
|
5934
6542
|
const policyPath = args.flags["policy"];
|
|
5935
|
-
const policy = loadPolicyOrDefault(policyPath ?
|
|
6543
|
+
const policy = loadPolicyOrDefault(policyPath ? resolve7(deps.cwd, policyPath) : void 0);
|
|
5936
6544
|
const actionCandidate = event.action_candidate;
|
|
5937
6545
|
let verdict;
|
|
5938
6546
|
let findings;
|
|
@@ -6080,7 +6688,7 @@ function writeInboxReceipt(event, rawContent, verdict, findings, policy, args, d
|
|
|
6080
6688
|
deps.generatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
6081
6689
|
);
|
|
6082
6690
|
const receiptOutPath = args.flags["receipt-out"] || "calllint-inbox-receipt.json";
|
|
6083
|
-
const absoluteReceiptPath =
|
|
6691
|
+
const absoluteReceiptPath = resolve7(deps.cwd, receiptOutPath);
|
|
6084
6692
|
writeFileSync8(absoluteReceiptPath, JSON.stringify(receipt, null, 2) + "\n", "utf-8");
|
|
6085
6693
|
return null;
|
|
6086
6694
|
} catch (error) {
|
|
@@ -6135,6 +6743,82 @@ SEE ALSO
|
|
|
6135
6743
|
`;
|
|
6136
6744
|
}
|
|
6137
6745
|
|
|
6746
|
+
// src/commands/inventory.ts
|
|
6747
|
+
function inventoryCommand(args, deps) {
|
|
6748
|
+
try {
|
|
6749
|
+
const result = discoverConfigs({ cwd: deps.cwd });
|
|
6750
|
+
if (args.flags.json) {
|
|
6751
|
+
return {
|
|
6752
|
+
exitCode: 0,
|
|
6753
|
+
stdout: JSON.stringify(result, null, 2) + "\n",
|
|
6754
|
+
stderr: ""
|
|
6755
|
+
};
|
|
6756
|
+
}
|
|
6757
|
+
const lines = [];
|
|
6758
|
+
if (result.discovered.length === 0) {
|
|
6759
|
+
lines.push("No agent configs discovered.");
|
|
6760
|
+
lines.push("");
|
|
6761
|
+
lines.push("Searched agents: Cursor, Claude Code, Claude Desktop");
|
|
6762
|
+
lines.push("To scan a specific config: calllint scan --config <path>");
|
|
6763
|
+
} else {
|
|
6764
|
+
lines.push(`Discovered ${result.discovered.length} agent config(s):`);
|
|
6765
|
+
lines.push("");
|
|
6766
|
+
const byAgent = /* @__PURE__ */ new Map();
|
|
6767
|
+
for (const config of result.discovered) {
|
|
6768
|
+
const existing = byAgent.get(config.agentType) || [];
|
|
6769
|
+
existing.push(config);
|
|
6770
|
+
byAgent.set(config.agentType, existing);
|
|
6771
|
+
}
|
|
6772
|
+
const sortedAgents = Array.from(byAgent.keys()).sort((a, b) => {
|
|
6773
|
+
const priorityA = result.discovered.find((c) => c.agentType === a)?.priority || "P3";
|
|
6774
|
+
const priorityB = result.discovered.find((c) => c.agentType === b)?.priority || "P3";
|
|
6775
|
+
return priorityA.localeCompare(priorityB);
|
|
6776
|
+
});
|
|
6777
|
+
for (const agentType of sortedAgents) {
|
|
6778
|
+
const configs = byAgent.get(agentType);
|
|
6779
|
+
const priority = configs[0].priority;
|
|
6780
|
+
const agentLabel = formatAgentType(agentType);
|
|
6781
|
+
lines.push(`${agentLabel} (${priority}):`);
|
|
6782
|
+
for (const config of configs) {
|
|
6783
|
+
lines.push(` ${config.configPath}`);
|
|
6784
|
+
}
|
|
6785
|
+
lines.push("");
|
|
6786
|
+
}
|
|
6787
|
+
lines.push(`To scan all: calllint scan --auto`);
|
|
6788
|
+
lines.push(`To scan one: calllint scan --agent ${result.discovered[0].agentType}`);
|
|
6789
|
+
}
|
|
6790
|
+
return {
|
|
6791
|
+
exitCode: 0,
|
|
6792
|
+
stdout: lines.join("\n") + "\n",
|
|
6793
|
+
stderr: ""
|
|
6794
|
+
};
|
|
6795
|
+
} catch (error) {
|
|
6796
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6797
|
+
return {
|
|
6798
|
+
exitCode: 1,
|
|
6799
|
+
stdout: "",
|
|
6800
|
+
stderr: `Error discovering configs: ${message}
|
|
6801
|
+
`
|
|
6802
|
+
};
|
|
6803
|
+
}
|
|
6804
|
+
}
|
|
6805
|
+
function formatAgentType(agentType) {
|
|
6806
|
+
switch (agentType) {
|
|
6807
|
+
case "cursor":
|
|
6808
|
+
return "Cursor";
|
|
6809
|
+
case "claude-code":
|
|
6810
|
+
return "Claude Code";
|
|
6811
|
+
case "claude-desktop":
|
|
6812
|
+
return "Claude Desktop";
|
|
6813
|
+
case "vscode":
|
|
6814
|
+
return "VS Code";
|
|
6815
|
+
case "windsurf":
|
|
6816
|
+
return "Windsurf";
|
|
6817
|
+
default:
|
|
6818
|
+
return agentType;
|
|
6819
|
+
}
|
|
6820
|
+
}
|
|
6821
|
+
|
|
6138
6822
|
// src/run.ts
|
|
6139
6823
|
function run(argv, deps) {
|
|
6140
6824
|
const args = parseArgs(argv);
|
|
@@ -6207,6 +6891,8 @@ function run(argv, deps) {
|
|
|
6207
6891
|
return actionCommand(args, { cwd: deps.cwd, toolVersion: deps.toolVersion, generatedAt: deps.generatedAt });
|
|
6208
6892
|
case "inbox":
|
|
6209
6893
|
return inboxCommand(args, { cwd: deps.cwd, toolVersion: deps.toolVersion, generatedAt: deps.generatedAt });
|
|
6894
|
+
case "inventory":
|
|
6895
|
+
return inventoryCommand(args, { cwd: deps.cwd });
|
|
6210
6896
|
case "gen-rule":
|
|
6211
6897
|
return genRuleCommand(args, { cwd: deps.cwd });
|
|
6212
6898
|
case "policy":
|
|
@@ -6480,13 +7166,13 @@ async function breathe(argv, deps = {}) {
|
|
|
6480
7166
|
}
|
|
6481
7167
|
|
|
6482
7168
|
// src/version.ts
|
|
6483
|
-
import { readFileSync as
|
|
7169
|
+
import { readFileSync as readFileSync16 } from "node:fs";
|
|
6484
7170
|
import { fileURLToPath } from "node:url";
|
|
6485
|
-
import { dirname as dirname4, join as
|
|
7171
|
+
import { dirname as dirname4, join as join16 } from "node:path";
|
|
6486
7172
|
function resolveToolVersion() {
|
|
6487
7173
|
try {
|
|
6488
|
-
const pkgPath =
|
|
6489
|
-
const pkg = JSON.parse(
|
|
7174
|
+
const pkgPath = join16(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
7175
|
+
const pkg = JSON.parse(readFileSync16(pkgPath, "utf8"));
|
|
6490
7176
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
6491
7177
|
} catch {
|
|
6492
7178
|
return "unknown";
|
|
@@ -6496,7 +7182,7 @@ function resolveToolVersion() {
|
|
|
6496
7182
|
// src/index.ts
|
|
6497
7183
|
function readStdin() {
|
|
6498
7184
|
try {
|
|
6499
|
-
return
|
|
7185
|
+
return readFileSync17(0, "utf8");
|
|
6500
7186
|
} catch {
|
|
6501
7187
|
return "";
|
|
6502
7188
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Evidence-backed security verdicts for MCP servers and agent tools. Lint agent tool-call risk before tools run — SAFE / REVIEW / BLOCK / UNKNOWN, with evidence. Never executes the server it judges.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@calllint/action-analyzer": "workspace:*",
|
|
53
53
|
"@calllint/core": "workspace:*",
|
|
54
|
+
"@calllint/discovery": "workspace:*",
|
|
54
55
|
"@calllint/fixtures": "workspace:*",
|
|
55
56
|
"@calllint/online": "workspace:*",
|
|
56
57
|
"@calllint/policy": "workspace:*",
|