calllint 1.7.0 → 1.7.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 +3 -0
- package/dist/index.js +676 -34
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -64,6 +64,9 @@ will do, it says so and never silently upgrades to `SAFE`.
|
|
|
64
64
|
- Self-contained HTML report: `calllint scan <config> --html > report.html`
|
|
65
65
|
- Drift / rug-pull detection: `calllint baseline <config>` then `calllint verify <config> --ci`
|
|
66
66
|
- Policy-as-code: `calllint policy init`
|
|
67
|
+
- Continuous Guard: `calllint guard` re-decides the approved authority surface (silent when unchanged); `calllint guard install --host <git|git-pre-push|github|claude-code|copilot|gemini|vscode>` adds a session-start / commit / CI hook that only shells out to `calllint guard` — never a per-call blocker
|
|
68
|
+
- Install the preflight into your hosts: `calllint integrate` prints a reversible install plan (writes nothing); `--apply --approve <digest>` is the only writer and reuses the audited atomic-write-and-rollback engine
|
|
69
|
+
- Claude Code plugin: a `PreToolUse` hook that *recommends* scanning before an agent-tool config edit — advisory, non-blocking, always exits 0, runs no scan itself
|
|
67
70
|
|
|
68
71
|
CallLint is a heuristic, evidence-backed pre-flight check, **not a proof of
|
|
69
72
|
safety**. `No blockers observed` ≠ guaranteed safe. Full docs, security model,
|
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 readFileSync22 } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -71,11 +71,12 @@ COMMANDS
|
|
|
71
71
|
inbox inspect <f> Preflight a normalized agent inbox event
|
|
72
72
|
evidence import <f> Import a third-party scanner report as evidence (no re-scoring)
|
|
73
73
|
trust prepare <target> Read-only Trust Gateway preview: resolve to a digest-pinned identity
|
|
74
|
+
integrate Install the CallLint preflight server into detected hosts (plan-only; --apply writes)
|
|
74
75
|
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
75
76
|
baseline [target] Record the approved risk surface as a baseline
|
|
76
77
|
approve Record the repo-wide capability surface as approved state (L4)
|
|
77
78
|
guard Continuous Guard: re-decide on authority change; silent when unchanged
|
|
78
|
-
guard install --host <h> Install a guard hook (git pre-
|
|
79
|
+
guard install --host <h> Install a guard hook (git, git-pre-push, github, claude-code, copilot, gemini, vscode)
|
|
79
80
|
guard status Show baseline / disable / installed-hook state
|
|
80
81
|
guard disable Turn Continuous Guard off (.calllint/guard.json)
|
|
81
82
|
receipt verify <f> Validate a calllint.receipt.v0 (structure + signature if present)
|
|
@@ -127,7 +128,8 @@ VERIFY OPTIONS
|
|
|
127
128
|
--json Emit the drift report JSON
|
|
128
129
|
|
|
129
130
|
GUARD OPTIONS
|
|
130
|
-
--host <h> guard install target: git
|
|
131
|
+
--host <h> guard install target: git | git-pre-push | github | claude-code | copilot | gemini | vscode
|
|
132
|
+
(session-start hooks, non-blocking; shared-config hosts print a fragment to merge)
|
|
131
133
|
--approved [file] Approved baseline to diff against (default: .calllint/approved.json)
|
|
132
134
|
--json Emit the guard assessment / status JSON
|
|
133
135
|
(exit) silent/note=0, REVIEW=10, UNKNOWN=20, BLOCK=30; guard self-failure is non-zero
|
|
@@ -5508,7 +5510,7 @@ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true
|
|
|
5508
5510
|
if (err2) return { stdout: "", stderr: err2, exitCode: EXIT.ERROR };
|
|
5509
5511
|
}
|
|
5510
5512
|
const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
|
|
5511
|
-
return { stdout, exitCode };
|
|
5513
|
+
return { stdout, exitCode, telemetry: { verdict: summary.verdict } };
|
|
5512
5514
|
}
|
|
5513
5515
|
function writeReceiptFile(summary, text, configPath, policy, args, deps) {
|
|
5514
5516
|
const toolVersion = deps.toolVersion ?? "0.0.0-dev";
|
|
@@ -8983,11 +8985,63 @@ function windsurfServerEntry(server) {
|
|
|
8983
8985
|
return entry;
|
|
8984
8986
|
}
|
|
8985
8987
|
|
|
8988
|
+
// ../../packages/install-planner/src/adapters/claudeDesktop.ts
|
|
8989
|
+
var CLAUDE_DESKTOP_HOST_ID = "claude-desktop";
|
|
8990
|
+
var CLAUDE_DESKTOP_TIER = "A";
|
|
8991
|
+
var claudeDesktopAdapter = {
|
|
8992
|
+
id: CLAUDE_DESKTOP_HOST_ID,
|
|
8993
|
+
tier: CLAUDE_DESKTOP_TIER,
|
|
8994
|
+
createPlan(ctx, upstream) {
|
|
8995
|
+
return buildInstallPlan({ ...ctx, host: CLAUDE_DESKTOP_HOST_ID, tier: CLAUDE_DESKTOP_TIER }, upstream);
|
|
8996
|
+
},
|
|
8997
|
+
validatePlan(plan) {
|
|
8998
|
+
return validatePlan(plan);
|
|
8999
|
+
},
|
|
9000
|
+
applyPlan(plan, ctx) {
|
|
9001
|
+
return applyPlan({
|
|
9002
|
+
plan,
|
|
9003
|
+
approvalDigest: ctx.approvalDigest,
|
|
9004
|
+
configPath: ctx.configPath,
|
|
9005
|
+
backupPath: ctx.backupPath,
|
|
9006
|
+
lockPath: ctx.lockPath,
|
|
9007
|
+
fs: ctx.fs,
|
|
9008
|
+
now: ctx.now
|
|
9009
|
+
});
|
|
9010
|
+
}
|
|
9011
|
+
};
|
|
9012
|
+
|
|
9013
|
+
// ../../packages/install-planner/src/adapters/vscode.ts
|
|
9014
|
+
var VSCODE_HOST_ID = "vscode";
|
|
9015
|
+
var VSCODE_TIER = "A";
|
|
9016
|
+
var vscodeAdapter = {
|
|
9017
|
+
id: VSCODE_HOST_ID,
|
|
9018
|
+
tier: VSCODE_TIER,
|
|
9019
|
+
createPlan(ctx, upstream) {
|
|
9020
|
+
return buildInstallPlan({ ...ctx, host: VSCODE_HOST_ID, tier: VSCODE_TIER }, upstream);
|
|
9021
|
+
},
|
|
9022
|
+
validatePlan(plan) {
|
|
9023
|
+
return validatePlan(plan);
|
|
9024
|
+
},
|
|
9025
|
+
applyPlan(plan, ctx) {
|
|
9026
|
+
return applyPlan({
|
|
9027
|
+
plan,
|
|
9028
|
+
approvalDigest: ctx.approvalDigest,
|
|
9029
|
+
configPath: ctx.configPath,
|
|
9030
|
+
backupPath: ctx.backupPath,
|
|
9031
|
+
lockPath: ctx.lockPath,
|
|
9032
|
+
fs: ctx.fs,
|
|
9033
|
+
now: ctx.now
|
|
9034
|
+
});
|
|
9035
|
+
}
|
|
9036
|
+
};
|
|
9037
|
+
|
|
8986
9038
|
// ../../packages/install-planner/src/index.ts
|
|
8987
9039
|
var HOST_ADAPTERS = {
|
|
8988
9040
|
[claudeCodeAdapter.id]: claudeCodeAdapter,
|
|
8989
9041
|
[cursorAdapter.id]: cursorAdapter,
|
|
8990
|
-
[windsurfAdapter.id]: windsurfAdapter
|
|
9042
|
+
[windsurfAdapter.id]: windsurfAdapter,
|
|
9043
|
+
[claudeDesktopAdapter.id]: claudeDesktopAdapter,
|
|
9044
|
+
[vscodeAdapter.id]: vscodeAdapter
|
|
8991
9045
|
};
|
|
8992
9046
|
function getHostAdapter(host) {
|
|
8993
9047
|
return HOST_ADAPTERS[host] ?? null;
|
|
@@ -9893,7 +9947,15 @@ SEE ALSO
|
|
|
9893
9947
|
// src/commands/guard.ts
|
|
9894
9948
|
import { existsSync as existsSync12, readFileSync as readFileSync19, writeFileSync as writeFileSync11, mkdirSync as mkdirSync6 } from "node:fs";
|
|
9895
9949
|
import { dirname as dirname6, join as join17, resolve as resolve10 } from "node:path";
|
|
9896
|
-
var GUARD_HOSTS = [
|
|
9950
|
+
var GUARD_HOSTS = [
|
|
9951
|
+
"git",
|
|
9952
|
+
"git-pre-push",
|
|
9953
|
+
"github",
|
|
9954
|
+
"claude-code",
|
|
9955
|
+
"copilot",
|
|
9956
|
+
"gemini",
|
|
9957
|
+
"vscode"
|
|
9958
|
+
];
|
|
9897
9959
|
function guardConfigPath(cwd) {
|
|
9898
9960
|
return join17(cwd, ".calllint", "guard.json");
|
|
9899
9961
|
}
|
|
@@ -9989,31 +10051,86 @@ function guardRun(args, deps, env) {
|
|
|
9989
10051
|
function isGuardHost(v) {
|
|
9990
10052
|
return v !== void 0 && GUARD_HOSTS.includes(v);
|
|
9991
10053
|
}
|
|
9992
|
-
var
|
|
10054
|
+
var GUARD_CMD = "npx -y calllint guard --no-emoji";
|
|
10055
|
+
function gitHookBody(when) {
|
|
10056
|
+
return `#!/usr/bin/env bash
|
|
9993
10057
|
# CallLint Continuous Guard (ADR 0045). Re-decides the agent-tool authority
|
|
9994
|
-
# surface
|
|
10058
|
+
# surface ${when}; silent when nothing changed. Generated by \`calllint guard install\`.
|
|
9995
10059
|
# CallLint is static and NEVER executes a scanned server.
|
|
9996
|
-
|
|
10060
|
+
${GUARD_CMD}
|
|
9997
10061
|
`;
|
|
10062
|
+
}
|
|
10063
|
+
var CLAUDE_CODE_HOOK = JSON.stringify(
|
|
10064
|
+
{ hooks: { SessionStart: [{ hooks: [{ type: "command", command: GUARD_CMD }] }] } },
|
|
10065
|
+
null,
|
|
10066
|
+
2
|
|
10067
|
+
) + "\n";
|
|
10068
|
+
var GEMINI_HOOK = JSON.stringify(
|
|
10069
|
+
{
|
|
10070
|
+
hooks: {
|
|
10071
|
+
SessionStart: [
|
|
10072
|
+
{ hooks: [{ type: "command", command: GUARD_CMD, name: "calllint-guard" }] }
|
|
10073
|
+
]
|
|
10074
|
+
}
|
|
10075
|
+
},
|
|
10076
|
+
null,
|
|
10077
|
+
2
|
|
10078
|
+
) + "\n";
|
|
10079
|
+
var COPILOT_HOOK = JSON.stringify(
|
|
10080
|
+
{ version: 1, hooks: { sessionStart: [{ type: "command", command: GUARD_CMD }] } },
|
|
10081
|
+
null,
|
|
10082
|
+
2
|
|
10083
|
+
) + "\n";
|
|
10084
|
+
var VSCODE_TASK = JSON.stringify(
|
|
10085
|
+
{
|
|
10086
|
+
version: "2.0.0",
|
|
10087
|
+
tasks: [
|
|
10088
|
+
{
|
|
10089
|
+
label: "CallLint Continuous Guard",
|
|
10090
|
+
type: "shell",
|
|
10091
|
+
command: GUARD_CMD,
|
|
10092
|
+
runOptions: { runOn: "folderOpen" },
|
|
10093
|
+
presentation: { reveal: "silent", panel: "shared" },
|
|
10094
|
+
problemMatcher: []
|
|
10095
|
+
}
|
|
10096
|
+
]
|
|
10097
|
+
},
|
|
10098
|
+
null,
|
|
10099
|
+
2
|
|
10100
|
+
) + "\n";
|
|
9998
10101
|
function guardArtifact(host) {
|
|
9999
|
-
|
|
10000
|
-
|
|
10102
|
+
switch (host) {
|
|
10103
|
+
case "git":
|
|
10104
|
+
return { path: join17(".git", "hooks", "pre-commit"), content: gitHookBody("on commit"), label: "git pre-commit hook", posture: "dedicated" };
|
|
10105
|
+
case "git-pre-push":
|
|
10106
|
+
return { path: join17(".git", "hooks", "pre-push"), content: gitHookBody("on push"), label: "git pre-push hook", posture: "dedicated" };
|
|
10107
|
+
case "github":
|
|
10108
|
+
return { path: join17(".github", "workflows", "calllint.yml"), content: renderCiGate({ mode: "drift" }), label: "GitHub Actions drift-gate workflow", posture: "dedicated" };
|
|
10109
|
+
case "copilot":
|
|
10110
|
+
return { path: join17(".github", "hooks", "calllint.json"), content: COPILOT_HOOK, label: "Copilot CLI sessionStart hook", posture: "dedicated" };
|
|
10111
|
+
case "claude-code":
|
|
10112
|
+
return { path: join17(".claude", "settings.json"), content: CLAUDE_CODE_HOOK, label: "Claude Code SessionStart hook", posture: "shared" };
|
|
10113
|
+
case "gemini":
|
|
10114
|
+
return { path: join17(".gemini", "settings.json"), content: GEMINI_HOOK, label: "Gemini CLI SessionStart hook", posture: "shared" };
|
|
10115
|
+
case "vscode":
|
|
10116
|
+
return { path: join17(".vscode", "tasks.json"), content: VSCODE_TASK, label: "VS Code folderOpen guard task", posture: "shared" };
|
|
10001
10117
|
}
|
|
10002
|
-
return {
|
|
10003
|
-
path: join17(".github", "workflows", "calllint.yml"),
|
|
10004
|
-
content: renderCiGate({ mode: "drift" }),
|
|
10005
|
-
label: "GitHub Actions drift-gate workflow"
|
|
10006
|
-
};
|
|
10007
10118
|
}
|
|
10008
10119
|
function guardInstall(args, deps) {
|
|
10009
10120
|
const host = flagStr(args.flags, "host");
|
|
10010
10121
|
if (!host) {
|
|
10011
|
-
const list = GUARD_HOSTS.map((h2) =>
|
|
10122
|
+
const list = GUARD_HOSTS.map((h2) => {
|
|
10123
|
+
const a = guardArtifact(h2);
|
|
10124
|
+
const tag = a.posture === "shared" ? " (fragment)" : "";
|
|
10125
|
+
return ` ${h2.padEnd(13)} \u2192 ${a.label}${tag}`;
|
|
10126
|
+
}).join("\n");
|
|
10012
10127
|
return {
|
|
10013
10128
|
stdout: `Usage: calllint guard install --host <host>
|
|
10014
10129
|
|
|
10015
10130
|
Hosts:
|
|
10016
|
-
${list}
|
|
10131
|
+
${list}
|
|
10132
|
+
|
|
10133
|
+
(fragment) hosts live inside a shared config file; install prints a snippet to merge and never overwrites it.`,
|
|
10017
10134
|
exitCode: EXIT.OK
|
|
10018
10135
|
};
|
|
10019
10136
|
}
|
|
@@ -10026,7 +10143,39 @@ Run \`calllint guard install\` to list hosts.`,
|
|
|
10026
10143
|
};
|
|
10027
10144
|
}
|
|
10028
10145
|
const art = guardArtifact(host);
|
|
10029
|
-
const
|
|
10146
|
+
const outFlag = flagStr(args.flags, "out");
|
|
10147
|
+
const rel = outFlag ?? art.path;
|
|
10148
|
+
if (art.posture === "shared") {
|
|
10149
|
+
if (outFlag) {
|
|
10150
|
+
const abs2 = resolve10(deps.cwd, rel);
|
|
10151
|
+
if (existsSync12(abs2)) {
|
|
10152
|
+
return {
|
|
10153
|
+
stdout: "",
|
|
10154
|
+
stderr: `Refusing to overwrite ${rel} \u2014 CallLint will not clobber a shared config file.
|
|
10155
|
+
Merge this fragment into it instead:
|
|
10156
|
+
|
|
10157
|
+
${art.content}`,
|
|
10158
|
+
exitCode: EXIT.USAGE
|
|
10159
|
+
};
|
|
10160
|
+
}
|
|
10161
|
+
if (deps.writeFile === false) {
|
|
10162
|
+
return { stdout: `Would write ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10163
|
+
}
|
|
10164
|
+
try {
|
|
10165
|
+
mkdirSync6(dirname6(abs2), { recursive: true });
|
|
10166
|
+
writeFileSync11(abs2, art.content, "utf8");
|
|
10167
|
+
} catch (e) {
|
|
10168
|
+
return { stdout: "", stderr: `Failed to write ${rel}: ${e instanceof Error ? e.message : String(e)}`, exitCode: EXIT.ERROR };
|
|
10169
|
+
}
|
|
10170
|
+
return { stdout: `Wrote ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10171
|
+
}
|
|
10172
|
+
return {
|
|
10173
|
+
stdout: `${art.label} \u2014 merge this into ${rel} (a shared config file CallLint will not overwrite):
|
|
10174
|
+
|
|
10175
|
+
${art.content}`,
|
|
10176
|
+
exitCode: EXIT.OK
|
|
10177
|
+
};
|
|
10178
|
+
}
|
|
10030
10179
|
if (deps.writeFile === false) {
|
|
10031
10180
|
return { stdout: `Would write ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10032
10181
|
}
|
|
@@ -10041,7 +10190,7 @@ Run \`calllint guard install\` to list hosts.`,
|
|
|
10041
10190
|
exitCode: EXIT.ERROR
|
|
10042
10191
|
};
|
|
10043
10192
|
}
|
|
10044
|
-
const note = host === "git" ? `
|
|
10193
|
+
const note = host === "git" || host === "git-pre-push" ? `
|
|
10045
10194
|
Make it executable: chmod +x ${rel}` : "";
|
|
10046
10195
|
return { stdout: `Wrote ${rel} (${art.label})${note}`, exitCode: EXIT.OK };
|
|
10047
10196
|
}
|
|
@@ -10049,16 +10198,26 @@ function guardStatus(args, deps, env) {
|
|
|
10049
10198
|
const approvedPath = flagStr(args.flags, "approved") ?? defaultApprovedPath(deps.cwd);
|
|
10050
10199
|
const hasBaseline = existsSync12(approvedPath);
|
|
10051
10200
|
const disabled = isDisabled(deps.cwd, env);
|
|
10052
|
-
const
|
|
10053
|
-
|
|
10201
|
+
const HOST_STATUS_KEY = {
|
|
10202
|
+
git: "git:pre-commit",
|
|
10203
|
+
"git-pre-push": "git:pre-push",
|
|
10204
|
+
github: "github:workflow",
|
|
10205
|
+
copilot: "copilot:sessionStart",
|
|
10206
|
+
"claude-code": "claude-code:sessionStart",
|
|
10207
|
+
gemini: "gemini:sessionStart",
|
|
10208
|
+
vscode: "vscode:folderOpen"
|
|
10209
|
+
};
|
|
10210
|
+
const installedHooks = {};
|
|
10211
|
+
for (const host of GUARD_HOSTS) {
|
|
10212
|
+
const art = guardArtifact(host);
|
|
10213
|
+
const abs = join17(deps.cwd, art.path);
|
|
10214
|
+
installedHooks[HOST_STATUS_KEY[host]] = hookInstalled(abs, art.posture);
|
|
10215
|
+
}
|
|
10054
10216
|
const status = {
|
|
10055
10217
|
enabled: !disabled,
|
|
10056
10218
|
disabledBy: disabled ? env["CALLLINT_GUARD"] === "0" ? "env:CALLLINT_GUARD=0" : "flag:.calllint/guard.json" : null,
|
|
10057
10219
|
approvedBaseline: hasBaseline ? approvedPath : null,
|
|
10058
|
-
installedHooks
|
|
10059
|
-
"git:pre-commit": gitHook,
|
|
10060
|
-
"github:workflow": ghWorkflow
|
|
10061
|
-
}
|
|
10220
|
+
installedHooks
|
|
10062
10221
|
};
|
|
10063
10222
|
if (flagBool(args.flags, "json")) {
|
|
10064
10223
|
return { stdout: JSON.stringify(status), exitCode: EXIT.OK };
|
|
@@ -10066,11 +10225,21 @@ function guardStatus(args, deps, env) {
|
|
|
10066
10225
|
const lines = [
|
|
10067
10226
|
`Continuous Guard: ${status.enabled ? "enabled" : `disabled (${status.disabledBy})`}`,
|
|
10068
10227
|
`Approved baseline: ${hasBaseline ? approvedPath : "none \u2014 run `calllint approve`"}`,
|
|
10069
|
-
|
|
10070
|
-
|
|
10228
|
+
...GUARD_HOSTS.map(
|
|
10229
|
+
(h2) => `${HOST_STATUS_KEY[h2]}: ${installedHooks[HOST_STATUS_KEY[h2]] ? "installed" : "not installed"}`
|
|
10230
|
+
)
|
|
10071
10231
|
];
|
|
10072
10232
|
return { stdout: lines.join("\n"), exitCode: EXIT.OK };
|
|
10073
10233
|
}
|
|
10234
|
+
function hookInstalled(abs, posture) {
|
|
10235
|
+
if (!existsSync12(abs)) return false;
|
|
10236
|
+
if (posture === "dedicated") return true;
|
|
10237
|
+
try {
|
|
10238
|
+
return readFileSync19(abs, "utf8").includes("calllint guard");
|
|
10239
|
+
} catch {
|
|
10240
|
+
return false;
|
|
10241
|
+
}
|
|
10242
|
+
}
|
|
10074
10243
|
function guardSetEnabled(deps, enabled) {
|
|
10075
10244
|
const cfg = { schemaVersion: "calllint.guard-config.v0", enabled };
|
|
10076
10245
|
if (deps.writeFile === false) {
|
|
@@ -10093,8 +10262,477 @@ function guardSetEnabled(deps, enabled) {
|
|
|
10093
10262
|
};
|
|
10094
10263
|
}
|
|
10095
10264
|
|
|
10265
|
+
// src/commands/integrate.ts
|
|
10266
|
+
import { existsSync as existsSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync12, mkdirSync as mkdirSync7 } from "node:fs";
|
|
10267
|
+
import { homedir as homedir2 } from "node:os";
|
|
10268
|
+
import { join as join18, resolve as resolve11 } from "node:path";
|
|
10269
|
+
|
|
10270
|
+
// ../../packages/agent-triggers/src/overlays.ts
|
|
10271
|
+
var PLATFORM_OVERLAYS = {
|
|
10272
|
+
"claude-code": {
|
|
10273
|
+
host: "claude-code",
|
|
10274
|
+
displayName: "Claude Code",
|
|
10275
|
+
channels: ["plugin-hook", "cli-recommend"],
|
|
10276
|
+
supportsRuntimeHook: true
|
|
10277
|
+
},
|
|
10278
|
+
cursor: {
|
|
10279
|
+
host: "cursor",
|
|
10280
|
+
displayName: "Cursor",
|
|
10281
|
+
channels: ["cli-recommend", "ide-diagnostic"],
|
|
10282
|
+
supportsRuntimeHook: false
|
|
10283
|
+
},
|
|
10284
|
+
windsurf: {
|
|
10285
|
+
host: "windsurf",
|
|
10286
|
+
displayName: "Windsurf",
|
|
10287
|
+
channels: ["cli-recommend", "ide-diagnostic"],
|
|
10288
|
+
supportsRuntimeHook: false
|
|
10289
|
+
},
|
|
10290
|
+
"claude-desktop": {
|
|
10291
|
+
host: "claude-desktop",
|
|
10292
|
+
displayName: "Claude Desktop",
|
|
10293
|
+
channels: ["cli-recommend"],
|
|
10294
|
+
supportsRuntimeHook: false
|
|
10295
|
+
},
|
|
10296
|
+
vscode: {
|
|
10297
|
+
host: "vscode",
|
|
10298
|
+
displayName: "VS Code",
|
|
10299
|
+
channels: ["cli-recommend", "ide-diagnostic"],
|
|
10300
|
+
supportsRuntimeHook: false
|
|
10301
|
+
}
|
|
10302
|
+
};
|
|
10303
|
+
function overlayForHost(host) {
|
|
10304
|
+
return PLATFORM_OVERLAYS[host] ?? null;
|
|
10305
|
+
}
|
|
10306
|
+
|
|
10307
|
+
// src/commands/integrate.ts
|
|
10308
|
+
var CALLLINT_MCP_SERVER_NAME = "calllint";
|
|
10309
|
+
function calllintMcpEntry() {
|
|
10310
|
+
return { command: "npx", args: ["-y", "calllint-mcp"] };
|
|
10311
|
+
}
|
|
10312
|
+
function integrableHosts() {
|
|
10313
|
+
return Object.values(HOST_ADAPTERS).filter((a) => typeof a.applyPlan === "function").map((a) => a.id).sort();
|
|
10314
|
+
}
|
|
10315
|
+
function decideForIntegrate(authority) {
|
|
10316
|
+
const policy = loadPolicyOrDefault();
|
|
10317
|
+
const flows = buildFlows([authority]);
|
|
10318
|
+
const flowReasons = foldFlowsIntoReasons(flows);
|
|
10319
|
+
return decideOverAuthority({ authority, policy, flowReasons });
|
|
10320
|
+
}
|
|
10321
|
+
function integrateCommand(args, deps) {
|
|
10322
|
+
const sub = args.positionals[0];
|
|
10323
|
+
if (sub === "help") return integrateHelp();
|
|
10324
|
+
if (sub !== void 0) {
|
|
10325
|
+
return usage(`Unexpected argument "${sub}". Run \`calllint integrate help\`.`);
|
|
10326
|
+
}
|
|
10327
|
+
const apply = flagBool(args.flags, "apply");
|
|
10328
|
+
return apply ? integrateApply(args, deps) : integratePlan(args, deps);
|
|
10329
|
+
}
|
|
10330
|
+
function integratePlan(args, deps) {
|
|
10331
|
+
const only = flagStr(args.flags, "host");
|
|
10332
|
+
const hosts = only ? [only] : integrableHosts();
|
|
10333
|
+
const detected = detectHosts(deps.cwd, hosts);
|
|
10334
|
+
const hostPlans = hosts.map((h2) => planForHost(h2, detected, deps));
|
|
10335
|
+
const written = /* @__PURE__ */ new Map();
|
|
10336
|
+
if (flagBool(args.flags, "write-plan") && deps.write !== false) {
|
|
10337
|
+
for (const hp of hostPlans) {
|
|
10338
|
+
if (hp.plan) written.set(hp.host, writePlanFile2(hp.plan, deps.cwd));
|
|
10339
|
+
}
|
|
10340
|
+
}
|
|
10341
|
+
if (flagBool(args.flags, "json")) {
|
|
10342
|
+
const payload = {
|
|
10343
|
+
schema: "calllint.integrate-plan.v0",
|
|
10344
|
+
generatedAt: deps.generatedAt,
|
|
10345
|
+
hosts: hostPlans.map((hp) => ({
|
|
10346
|
+
host: hp.host,
|
|
10347
|
+
configPath: hp.configPath,
|
|
10348
|
+
planDigest: hp.plan?.planDigest ?? null,
|
|
10349
|
+
operations: hp.plan?.operations.length ?? 0,
|
|
10350
|
+
planFile: written.get(hp.host) ?? null,
|
|
10351
|
+
note: hp.note
|
|
10352
|
+
}))
|
|
10353
|
+
};
|
|
10354
|
+
return { stdout: JSON.stringify(payload, null, 2), stderr: "", exitCode: EXIT.OK };
|
|
10355
|
+
}
|
|
10356
|
+
return { stdout: renderPlans(hostPlans, written), stderr: "", exitCode: EXIT.OK };
|
|
10357
|
+
}
|
|
10358
|
+
function writePlanFile2(plan, cwd) {
|
|
10359
|
+
const planDir = join18(cwd, ".calllint", "plans");
|
|
10360
|
+
mkdirSync7(planDir, { recursive: true });
|
|
10361
|
+
const file = join18(planDir, `${plan.planId}.json`);
|
|
10362
|
+
writeFileSync12(file, JSON.stringify(plan, null, 2) + "\n", "utf8");
|
|
10363
|
+
return file;
|
|
10364
|
+
}
|
|
10365
|
+
function detectHosts(cwd, hosts) {
|
|
10366
|
+
const result = discoverConfigs({ cwd, agentTypes: hosts, includeMissing: true });
|
|
10367
|
+
const cwdPrefix = resolve11(cwd);
|
|
10368
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
10369
|
+
for (const d of result.discovered) {
|
|
10370
|
+
if (!resolve11(d.configPath).startsWith(cwdPrefix)) continue;
|
|
10371
|
+
const prior = byHost.get(d.agentType);
|
|
10372
|
+
if (!prior || !prior.exists && d.exists) byHost.set(d.agentType, d);
|
|
10373
|
+
}
|
|
10374
|
+
return byHost;
|
|
10375
|
+
}
|
|
10376
|
+
function planForHost(host, detected, deps) {
|
|
10377
|
+
const adapter = getHostAdapter(host);
|
|
10378
|
+
const overlay = overlayForHost(host);
|
|
10379
|
+
const discovered = detected.get(host);
|
|
10380
|
+
const configFromDiscovery = discovered?.configPath;
|
|
10381
|
+
const configPath = configFromDiscovery ?? "(no default config path)";
|
|
10382
|
+
if (!adapter || !adapter.applyPlan) {
|
|
10383
|
+
return { host, configPath, plan: null, note: `host "${host}" has no audited writer (plan-only elsewhere)` };
|
|
10384
|
+
}
|
|
10385
|
+
if (!configFromDiscovery || !discovered?.exists) {
|
|
10386
|
+
return { host, configPath, plan: null, note: `host "${host}" not detected on this machine` };
|
|
10387
|
+
}
|
|
10388
|
+
let currentConfig = null;
|
|
10389
|
+
let configDigest = "absent";
|
|
10390
|
+
if (existsSync13(configFromDiscovery)) {
|
|
10391
|
+
try {
|
|
10392
|
+
const bytes = readFileSync20(configFromDiscovery, "utf8");
|
|
10393
|
+
configDigest = hashJson(bytes);
|
|
10394
|
+
currentConfig = JSON.parse(bytes);
|
|
10395
|
+
} catch {
|
|
10396
|
+
return { host, configPath, plan: null, note: `host config exists but is not valid JSON: ${configFromDiscovery}` };
|
|
10397
|
+
}
|
|
10398
|
+
}
|
|
10399
|
+
if (serverAlreadyPresent(currentConfig)) {
|
|
10400
|
+
return { host, configPath, plan: null, note: "already integrated (calllint server present) \u2014 no change" };
|
|
10401
|
+
}
|
|
10402
|
+
const calllintServer = {
|
|
10403
|
+
name: CALLLINT_MCP_SERVER_NAME,
|
|
10404
|
+
sourceConfigPath: configFromDiscovery,
|
|
10405
|
+
transport: "stdio",
|
|
10406
|
+
command: "npx",
|
|
10407
|
+
args: ["-y", "calllint-mcp"],
|
|
10408
|
+
envKeys: [],
|
|
10409
|
+
env: {},
|
|
10410
|
+
providedTools: [],
|
|
10411
|
+
raw: calllintMcpEntry()
|
|
10412
|
+
};
|
|
10413
|
+
const authority = buildAuthorityManifest({ artifactDigest: null, servers: [calllintServer], surfaces: [] });
|
|
10414
|
+
const decision = decideForIntegrate(authority);
|
|
10415
|
+
const backupPath = `${configFromDiscovery}.calllint-backup`;
|
|
10416
|
+
const expiresAt = planExpiry2(deps.generatedAt);
|
|
10417
|
+
const ctx = {
|
|
10418
|
+
host,
|
|
10419
|
+
tier: adapter.tier,
|
|
10420
|
+
configPath: configFromDiscovery,
|
|
10421
|
+
configDigest,
|
|
10422
|
+
currentConfig,
|
|
10423
|
+
servers: [{ name: CALLLINT_MCP_SERVER_NAME, entry: calllintMcpEntry() }],
|
|
10424
|
+
backupPath,
|
|
10425
|
+
expiresAt
|
|
10426
|
+
};
|
|
10427
|
+
const plan = adapter.createPlan(ctx, { artifactDigest: null, authority, decision });
|
|
10428
|
+
return { host, configPath, plan, note: null };
|
|
10429
|
+
}
|
|
10430
|
+
function serverAlreadyPresent(config) {
|
|
10431
|
+
if (!config || typeof config !== "object") return false;
|
|
10432
|
+
const servers = config.mcpServers;
|
|
10433
|
+
if (!servers || typeof servers !== "object") return false;
|
|
10434
|
+
return Object.prototype.hasOwnProperty.call(servers, CALLLINT_MCP_SERVER_NAME);
|
|
10435
|
+
}
|
|
10436
|
+
function integrateApply(args, deps) {
|
|
10437
|
+
const planFile = flagStr(args.flags, "plan");
|
|
10438
|
+
const approve = flagStr(args.flags, "approve");
|
|
10439
|
+
if (!planFile) return usage("Missing --plan <file>\nUsage: calllint integrate --apply --plan <plan.json> --approve <plan-digest>");
|
|
10440
|
+
if (!approve) return usage("Missing --approve <plan-digest>\nApproval must name the exact plan digest you reviewed.");
|
|
10441
|
+
let plan;
|
|
10442
|
+
try {
|
|
10443
|
+
plan = JSON.parse(readFileSync20(resolve11(deps.cwd, planFile), "utf8"));
|
|
10444
|
+
} catch (err2) {
|
|
10445
|
+
const e = err2;
|
|
10446
|
+
return { stdout: "", stderr: e.code === "ENOENT" ? `Plan file not found: ${planFile}` : `Plan file is not valid JSON: ${e.message}`, exitCode: EXIT.ERROR };
|
|
10447
|
+
}
|
|
10448
|
+
if (plan?.schema !== "calllint.install-plan.v1") {
|
|
10449
|
+
return { stdout: "", stderr: `Not a calllint.install-plan.v1 document: ${planFile}`, exitCode: EXIT.ERROR };
|
|
10450
|
+
}
|
|
10451
|
+
if (!verifyPlanDigest(plan)) {
|
|
10452
|
+
return { stdout: "", stderr: "Plan digest does not match its contents (tampered or hand-edited). Re-run `calllint integrate` to regenerate.", exitCode: EXIT.ERROR };
|
|
10453
|
+
}
|
|
10454
|
+
const adapter = getHostAdapter(plan.host);
|
|
10455
|
+
if (!adapter) return usage(`Unknown host "${plan.host}".`);
|
|
10456
|
+
if (!adapter.applyPlan) return usage(`Host "${plan.host}" is tier ${adapter.tier} \u2014 plan-only; cannot apply.`);
|
|
10457
|
+
const rawTarget = plan.operations[0]?.target;
|
|
10458
|
+
if (!rawTarget) return usage("Plan has no operations to apply.");
|
|
10459
|
+
let configPath;
|
|
10460
|
+
try {
|
|
10461
|
+
configPath = safeConfigPath(rawTarget, { cwd: deps.cwd, home: homedir2() });
|
|
10462
|
+
} catch (err2) {
|
|
10463
|
+
if (err2 instanceof PathSafetyError) return { stdout: "", stderr: `Unsafe target path in plan: ${err2.message}`, exitCode: EXIT.ERROR };
|
|
10464
|
+
throw err2;
|
|
10465
|
+
}
|
|
10466
|
+
const rid = shortReceiptId(plan.planDigest, deps.generatedAt);
|
|
10467
|
+
const backupPath = `${configPath}.calllint-backup-${rid}`;
|
|
10468
|
+
const lockPath = join18(deps.cwd, ".calllint", "locks", `${hashJson(configPath).slice("sha256:".length, "sha256:".length + 16)}.lock`);
|
|
10469
|
+
const result = adapter.applyPlan(plan, {
|
|
10470
|
+
approvalDigest: approve,
|
|
10471
|
+
configPath,
|
|
10472
|
+
backupPath,
|
|
10473
|
+
lockPath,
|
|
10474
|
+
fs: nodeFsPort(),
|
|
10475
|
+
now: deps.generatedAt
|
|
10476
|
+
});
|
|
10477
|
+
if (flagBool(args.flags, "json")) {
|
|
10478
|
+
return { stdout: JSON.stringify(result, null, 2), stderr: "", exitCode: applyExitCode2(result) };
|
|
10479
|
+
}
|
|
10480
|
+
return { stdout: renderApply(result), stderr: "", exitCode: applyExitCode2(result) };
|
|
10481
|
+
}
|
|
10482
|
+
function planExpiry2(generatedAt) {
|
|
10483
|
+
const ms = Date.parse(generatedAt);
|
|
10484
|
+
if (Number.isNaN(ms)) return generatedAt;
|
|
10485
|
+
return new Date(ms + 30 * 24 * 60 * 60 * 1e3).toISOString();
|
|
10486
|
+
}
|
|
10487
|
+
function shortReceiptId(planDigest, generatedAt) {
|
|
10488
|
+
return hashJson({ planDigest, generatedAt }).slice("sha256:".length, "sha256:".length + 12);
|
|
10489
|
+
}
|
|
10490
|
+
function applyExitCode2(result) {
|
|
10491
|
+
switch (result.outcome) {
|
|
10492
|
+
case "applied":
|
|
10493
|
+
case "already_applied":
|
|
10494
|
+
return EXIT.OK;
|
|
10495
|
+
case "stale":
|
|
10496
|
+
case "conflict":
|
|
10497
|
+
return EXIT.ERROR;
|
|
10498
|
+
case "rolled_back":
|
|
10499
|
+
case "rollback_failed":
|
|
10500
|
+
return EXIT.ERROR;
|
|
10501
|
+
}
|
|
10502
|
+
}
|
|
10503
|
+
function renderPlans(hostPlans, written) {
|
|
10504
|
+
const lines = ["CallLint integrate \u2014 preflight install plan (read-only; nothing written)\n"];
|
|
10505
|
+
for (const hp of hostPlans) {
|
|
10506
|
+
if (hp.plan) {
|
|
10507
|
+
lines.push(` ${hp.host}: ${hp.plan.operations.length} op(s) \u2192 ${hp.configPath}`);
|
|
10508
|
+
lines.push(` plan digest: ${hp.plan.planDigest}`);
|
|
10509
|
+
const planRef = written.get(hp.host) ?? "<saved-plan.json>";
|
|
10510
|
+
lines.push(` apply with: calllint integrate --apply --plan ${planRef} --approve ${hp.plan.planDigest}`);
|
|
10511
|
+
} else {
|
|
10512
|
+
lines.push(` ${hp.host}: no change \u2014 ${hp.note}`);
|
|
10513
|
+
}
|
|
10514
|
+
}
|
|
10515
|
+
lines.push("\nInstalling the CallLint preflight server does not block your agent (ADR 0051); it recommends. CallLint never executes the servers it judges.");
|
|
10516
|
+
return lines.join("\n") + "\n";
|
|
10517
|
+
}
|
|
10518
|
+
function renderApply(result) {
|
|
10519
|
+
const head = `integrate apply: ${result.outcome} (${result.state}) \u2014 host ${result.host}`;
|
|
10520
|
+
const trail = result.notes.map((n) => ` ${n}`).join("\n");
|
|
10521
|
+
const rb = result.rolledBack ? "\n rolled back to the original config (verify failed)." : "";
|
|
10522
|
+
return `${head}
|
|
10523
|
+
${trail}${rb}
|
|
10524
|
+
`;
|
|
10525
|
+
}
|
|
10526
|
+
function integrateHelp() {
|
|
10527
|
+
return {
|
|
10528
|
+
stdout: [
|
|
10529
|
+
"calllint integrate \u2014 install the CallLint preflight server into your agent hosts",
|
|
10530
|
+
"",
|
|
10531
|
+
" calllint integrate detect hosts + print an install plan (read-only)",
|
|
10532
|
+
" calllint integrate --host cursor plan for one host only",
|
|
10533
|
+
" calllint integrate --write-plan persist each plan to .calllint/plans/<id>.json",
|
|
10534
|
+
" calllint integrate --json machine-readable plan",
|
|
10535
|
+
" calllint integrate --apply --plan <p.json> --approve <digest>",
|
|
10536
|
+
" apply an approved plan (atomic, verified, auto-rollback)",
|
|
10537
|
+
"",
|
|
10538
|
+
"Idempotent: a host that already has the calllint server yields no change.",
|
|
10539
|
+
"Plan-only by default; --apply is the only writer and reuses the audited",
|
|
10540
|
+
"trust-apply engine. Installing the preflight does not block your agent (ADR 0051)."
|
|
10541
|
+
].join("\n") + "\n",
|
|
10542
|
+
stderr: "",
|
|
10543
|
+
exitCode: EXIT.OK
|
|
10544
|
+
};
|
|
10545
|
+
}
|
|
10546
|
+
function usage(msg) {
|
|
10547
|
+
return { stdout: "", stderr: msg, exitCode: EXIT.USAGE };
|
|
10548
|
+
}
|
|
10549
|
+
|
|
10550
|
+
// ../../packages/telemetry-contract/src/events.ts
|
|
10551
|
+
var ALLOWED_EVENTS = [
|
|
10552
|
+
"install_completed",
|
|
10553
|
+
"preflight_completed",
|
|
10554
|
+
"decision_safe",
|
|
10555
|
+
"decision_review",
|
|
10556
|
+
"decision_block",
|
|
10557
|
+
"decision_unknown",
|
|
10558
|
+
"approval_created",
|
|
10559
|
+
"apply_completed",
|
|
10560
|
+
"verify_completed",
|
|
10561
|
+
"rollback_completed",
|
|
10562
|
+
"guard_drift_detected",
|
|
10563
|
+
"trust_page_viewed",
|
|
10564
|
+
"trust_page_to_install",
|
|
10565
|
+
"partner_api_called",
|
|
10566
|
+
"badge_rendered"
|
|
10567
|
+
];
|
|
10568
|
+
var FORBIDDEN_FIELDS = [
|
|
10569
|
+
"rawConfig",
|
|
10570
|
+
"command",
|
|
10571
|
+
"environmentValue",
|
|
10572
|
+
"secret",
|
|
10573
|
+
"fileContents",
|
|
10574
|
+
"privateRepository",
|
|
10575
|
+
"userPrompt",
|
|
10576
|
+
"findingEvidenceText"
|
|
10577
|
+
];
|
|
10578
|
+
var SOURCES = ["cli", "ci", "server", "install"];
|
|
10579
|
+
var RESULTS2 = ["SAFE", "REVIEW", "BLOCK", "UNKNOWN"];
|
|
10580
|
+
var EVENT_VERSION = "1.0.0";
|
|
10581
|
+
|
|
10582
|
+
// ../../packages/telemetry-contract/src/tiers.ts
|
|
10583
|
+
var TIER_POLICY = {
|
|
10584
|
+
server: {
|
|
10585
|
+
source: "server",
|
|
10586
|
+
defaultEnabled: true,
|
|
10587
|
+
requiresNotice: false,
|
|
10588
|
+
rationale: "Own web/API property; no user machine involved (funnel top)."
|
|
10589
|
+
},
|
|
10590
|
+
install: {
|
|
10591
|
+
source: "install",
|
|
10592
|
+
defaultEnabled: true,
|
|
10593
|
+
requiresNotice: false,
|
|
10594
|
+
rationale: "Attributed at the click/redirect surface, not on the machine."
|
|
10595
|
+
},
|
|
10596
|
+
ci: {
|
|
10597
|
+
source: "ci",
|
|
10598
|
+
defaultEnabled: true,
|
|
10599
|
+
requiresNotice: true,
|
|
10600
|
+
rationale: "The installing org is the controller; standard for CI tooling; disableable."
|
|
10601
|
+
},
|
|
10602
|
+
cli: {
|
|
10603
|
+
source: "cli",
|
|
10604
|
+
defaultEnabled: false,
|
|
10605
|
+
requiresNotice: true,
|
|
10606
|
+
rationale: "Personal machine \u2192 consent-first (opt-in, default off, first-run consent)."
|
|
10607
|
+
}
|
|
10608
|
+
};
|
|
10609
|
+
function isEnabledByDefault(source) {
|
|
10610
|
+
return TIER_POLICY[source].defaultEnabled;
|
|
10611
|
+
}
|
|
10612
|
+
|
|
10613
|
+
// ../../packages/telemetry-contract/src/sanitize.ts
|
|
10614
|
+
function bucketDuration(ms) {
|
|
10615
|
+
if (ms == null || !Number.isFinite(ms) || ms < 0) return void 0;
|
|
10616
|
+
if (ms < 100) return "<100ms";
|
|
10617
|
+
if (ms < 500) return "100-500ms";
|
|
10618
|
+
if (ms < 2e3) return "500-2000ms";
|
|
10619
|
+
return ">2000ms";
|
|
10620
|
+
}
|
|
10621
|
+
var isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
|
|
10622
|
+
function sanitizeEvent(input) {
|
|
10623
|
+
for (const f of FORBIDDEN_FIELDS) {
|
|
10624
|
+
if (f in input) {
|
|
10625
|
+
throw new Error(`telemetry: forbidden field "${f}" present on event \u2014 refusing to sanitize`);
|
|
10626
|
+
}
|
|
10627
|
+
}
|
|
10628
|
+
if (!ALLOWED_EVENTS.includes(input.eventName)) {
|
|
10629
|
+
throw new Error(`telemetry: unknown eventName "${input.eventName}"`);
|
|
10630
|
+
}
|
|
10631
|
+
if (!SOURCES.includes(input.source)) {
|
|
10632
|
+
throw new Error(`telemetry: unknown source "${input.source}"`);
|
|
10633
|
+
}
|
|
10634
|
+
if (input.result != null && !RESULTS2.includes(input.result)) {
|
|
10635
|
+
throw new Error(`telemetry: unknown result "${input.result}"`);
|
|
10636
|
+
}
|
|
10637
|
+
const out = {
|
|
10638
|
+
eventVersion: EVENT_VERSION,
|
|
10639
|
+
eventName: input.eventName,
|
|
10640
|
+
timestamp: isNonEmptyString(input.timestamp) ? input.timestamp : "",
|
|
10641
|
+
source: input.source
|
|
10642
|
+
};
|
|
10643
|
+
if (isNonEmptyString(input.hostFamily)) out.hostFamily = input.hostFamily;
|
|
10644
|
+
if (input.result != null) out.result = input.result;
|
|
10645
|
+
const bucket = bucketDuration(input.durationMs);
|
|
10646
|
+
if (bucket) out.durationBucket = bucket;
|
|
10647
|
+
if (isNonEmptyString(input.inputKind)) out.inputKind = input.inputKind;
|
|
10648
|
+
if (isNonEmptyString(input.anonymousInstallationId)) {
|
|
10649
|
+
out.anonymousInstallationId = input.anonymousInstallationId;
|
|
10650
|
+
}
|
|
10651
|
+
if (isNonEmptyString(input.productVersion)) out.productVersion = input.productVersion;
|
|
10652
|
+
return out;
|
|
10653
|
+
}
|
|
10654
|
+
|
|
10655
|
+
// ../../packages/telemetry-emit/src/gate.ts
|
|
10656
|
+
var DISABLED_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
|
|
10657
|
+
function isTelemetryDisabledByEnv(env) {
|
|
10658
|
+
const v = env["CALLLINT_TELEMETRY"];
|
|
10659
|
+
return v != null && DISABLED_VALUES.has(v.trim().toLowerCase());
|
|
10660
|
+
}
|
|
10661
|
+
function shouldEmit(source, state = {}) {
|
|
10662
|
+
const env = state.env ?? {};
|
|
10663
|
+
if (isTelemetryDisabledByEnv(env)) return false;
|
|
10664
|
+
if (source === "cli") return state.consented === true;
|
|
10665
|
+
return isEnabledByDefault(source);
|
|
10666
|
+
}
|
|
10667
|
+
|
|
10668
|
+
// ../../packages/telemetry-emit/src/sink.ts
|
|
10669
|
+
var noopSink = {
|
|
10670
|
+
kind: "noop",
|
|
10671
|
+
write() {
|
|
10672
|
+
}
|
|
10673
|
+
};
|
|
10674
|
+
|
|
10675
|
+
// ../../packages/telemetry-emit/src/emitter.ts
|
|
10676
|
+
function createEmitter(config) {
|
|
10677
|
+
const sink = config.sink ?? noopSink;
|
|
10678
|
+
const gate = { consented: config.consented, env: config.env };
|
|
10679
|
+
return {
|
|
10680
|
+
source: config.source,
|
|
10681
|
+
emit(input) {
|
|
10682
|
+
if (!shouldEmit(config.source, gate)) {
|
|
10683
|
+
return { status: "gated", reason: "disabled-or-no-consent" };
|
|
10684
|
+
}
|
|
10685
|
+
let event;
|
|
10686
|
+
try {
|
|
10687
|
+
event = sanitizeEvent({ ...input, source: config.source });
|
|
10688
|
+
} catch (err2) {
|
|
10689
|
+
return { status: "dropped", reason: err2 instanceof Error ? err2.message : String(err2) };
|
|
10690
|
+
}
|
|
10691
|
+
sink.write(event);
|
|
10692
|
+
return { status: "emitted", event };
|
|
10693
|
+
}
|
|
10694
|
+
};
|
|
10695
|
+
}
|
|
10696
|
+
|
|
10697
|
+
// src/telemetry.ts
|
|
10698
|
+
var VERDICT_EVENT = {
|
|
10699
|
+
SAFE: "decision_safe",
|
|
10700
|
+
REVIEW: "decision_review",
|
|
10701
|
+
BLOCK: "decision_block",
|
|
10702
|
+
UNKNOWN: "decision_unknown"
|
|
10703
|
+
};
|
|
10704
|
+
function eventFor(signal) {
|
|
10705
|
+
if (signal.event) return signal.event;
|
|
10706
|
+
if (signal.verdict) return VERDICT_EVENT[signal.verdict];
|
|
10707
|
+
return null;
|
|
10708
|
+
}
|
|
10709
|
+
function buildCliEmitter(env, opts = {}) {
|
|
10710
|
+
return createEmitter({ source: "cli", env, sink: opts.sink, consented: opts.consented });
|
|
10711
|
+
}
|
|
10712
|
+
function emitCommandSignal(emitter, signal, productVersion) {
|
|
10713
|
+
if (!emitter || !signal) return;
|
|
10714
|
+
try {
|
|
10715
|
+
const eventName = eventFor(signal);
|
|
10716
|
+
if (!eventName) return;
|
|
10717
|
+
const input = {
|
|
10718
|
+
eventName,
|
|
10719
|
+
...signal.verdict ? { result: signal.verdict } : {},
|
|
10720
|
+
...signal.hostFamily ? { hostFamily: signal.hostFamily } : {},
|
|
10721
|
+
...signal.inputKind ? { inputKind: signal.inputKind } : {},
|
|
10722
|
+
...productVersion ? { productVersion } : {}
|
|
10723
|
+
};
|
|
10724
|
+
emitter.emit(input);
|
|
10725
|
+
} catch {
|
|
10726
|
+
}
|
|
10727
|
+
}
|
|
10728
|
+
|
|
10096
10729
|
// src/run.ts
|
|
10097
10730
|
function run(argv, deps) {
|
|
10731
|
+
const result = dispatch(argv, deps);
|
|
10732
|
+
emitCommandSignal(deps.emitter, result.telemetry, deps.toolVersion);
|
|
10733
|
+
return result;
|
|
10734
|
+
}
|
|
10735
|
+
function dispatch(argv, deps) {
|
|
10098
10736
|
const args = parseArgs(argv);
|
|
10099
10737
|
const cmd = args.command;
|
|
10100
10738
|
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
@@ -10171,6 +10809,8 @@ function run(argv, deps) {
|
|
|
10171
10809
|
return evidenceCommand(args, { cwd: deps.cwd });
|
|
10172
10810
|
case "trust":
|
|
10173
10811
|
return trustCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
|
|
10812
|
+
case "integrate":
|
|
10813
|
+
return integrateCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
|
|
10174
10814
|
case "guard":
|
|
10175
10815
|
return guardCommand(args, {
|
|
10176
10816
|
cwd: deps.cwd,
|
|
@@ -10451,13 +11091,13 @@ async function breathe(argv, deps = {}) {
|
|
|
10451
11091
|
}
|
|
10452
11092
|
|
|
10453
11093
|
// src/version.ts
|
|
10454
|
-
import { readFileSync as
|
|
11094
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
10455
11095
|
import { fileURLToPath } from "node:url";
|
|
10456
|
-
import { dirname as dirname7, join as
|
|
11096
|
+
import { dirname as dirname7, join as join19 } from "node:path";
|
|
10457
11097
|
function resolveToolVersion() {
|
|
10458
11098
|
try {
|
|
10459
|
-
const pkgPath =
|
|
10460
|
-
const pkg = JSON.parse(
|
|
11099
|
+
const pkgPath = join19(dirname7(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
11100
|
+
const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
|
|
10461
11101
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
10462
11102
|
} catch {
|
|
10463
11103
|
return "unknown";
|
|
@@ -10467,7 +11107,7 @@ function resolveToolVersion() {
|
|
|
10467
11107
|
// src/index.ts
|
|
10468
11108
|
function readStdin() {
|
|
10469
11109
|
try {
|
|
10470
|
-
return
|
|
11110
|
+
return readFileSync22(0, "utf8");
|
|
10471
11111
|
} catch {
|
|
10472
11112
|
return "";
|
|
10473
11113
|
}
|
|
@@ -10510,6 +11150,7 @@ async function main() {
|
|
|
10510
11150
|
await breathe(argv);
|
|
10511
11151
|
} catch {
|
|
10512
11152
|
}
|
|
11153
|
+
const emitter = buildCliEmitter(process.env);
|
|
10513
11154
|
const result = run(argv, {
|
|
10514
11155
|
cwd: process.cwd(),
|
|
10515
11156
|
readStdin,
|
|
@@ -10517,7 +11158,8 @@ async function main() {
|
|
|
10517
11158
|
generatedAt,
|
|
10518
11159
|
online,
|
|
10519
11160
|
toolVersion: resolveToolVersion(),
|
|
10520
|
-
getChangedFilesDiff: () => gitChangedFiles(process.cwd())
|
|
11161
|
+
getChangedFilesDiff: () => gitChangedFiles(process.cwd()),
|
|
11162
|
+
emitter
|
|
10521
11163
|
});
|
|
10522
11164
|
if (result.stdout) process.stdout.write(result.stdout + "\n");
|
|
10523
11165
|
if (result.stderr) process.stderr.write(result.stderr + "\n");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.2",
|
|
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",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"dependencies": {},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@calllint/action-analyzer": "workspace:*",
|
|
53
|
+
"@calllint/agent-triggers": "workspace:*",
|
|
53
54
|
"@calllint/core": "workspace:*",
|
|
54
55
|
"@calllint/discovery": "workspace:*",
|
|
55
56
|
"@calllint/evidence": "workspace:*",
|
|
@@ -62,6 +63,8 @@
|
|
|
62
63
|
"@calllint/report-renderer": "workspace:*",
|
|
63
64
|
"@calllint/resolver": "workspace:*",
|
|
64
65
|
"@calllint/signature": "workspace:*",
|
|
66
|
+
"@calllint/telemetry-contract": "workspace:*",
|
|
67
|
+
"@calllint/telemetry-emit": "workspace:*",
|
|
65
68
|
"@calllint/types": "workspace:*",
|
|
66
69
|
"esbuild": "^0.21.5"
|
|
67
70
|
}
|