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.
Files changed (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +676 -34
  3. 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 readFileSync21 } from "node:fs";
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-commit, github workflow)
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 (pre-commit) | github (workflow)
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 = ["git", "github"];
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 GIT_HOOK = `#!/usr/bin/env bash
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 on commit; silent when nothing changed. Generated by \`calllint guard install\`.
10058
+ # surface ${when}; silent when nothing changed. Generated by \`calllint guard install\`.
9995
10059
  # CallLint is static and NEVER executes a scanned server.
9996
- npx -y calllint guard --no-emoji
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
- if (host === "git") {
10000
- return { path: join17(".git", "hooks", "pre-commit"), content: GIT_HOOK, label: "git pre-commit hook" };
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) => ` ${h2.padEnd(8)} \u2192 ${guardArtifact(h2).label}`).join("\n");
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 rel = flagStr(args.flags, "out") ?? art.path;
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 gitHook = existsSync12(join17(deps.cwd, ".git", "hooks", "pre-commit"));
10053
- const ghWorkflow = existsSync12(join17(deps.cwd, ".github", "workflows", "calllint.yml"));
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
- `git pre-commit hook: ${gitHook ? "installed" : "not installed"}`,
10070
- `GitHub workflow: ${ghWorkflow ? "installed" : "not installed"}`
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 readFileSync20 } from "node:fs";
11094
+ import { readFileSync as readFileSync21 } from "node:fs";
10455
11095
  import { fileURLToPath } from "node:url";
10456
- import { dirname as dirname7, join as join18 } from "node:path";
11096
+ import { dirname as dirname7, join as join19 } from "node:path";
10457
11097
  function resolveToolVersion() {
10458
11098
  try {
10459
- const pkgPath = join18(dirname7(fileURLToPath(import.meta.url)), "..", "package.json");
10460
- const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
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 readFileSync21(0, "utf8");
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.0",
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
  }