calllint 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +389 -4
- 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 readFileSync11 } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -4472,6 +4472,389 @@ function receiptCommand(args, deps) {
|
|
|
4472
4472
|
return { stdout, exitCode: EXIT.OK };
|
|
4473
4473
|
}
|
|
4474
4474
|
|
|
4475
|
+
// src/commands/action.ts
|
|
4476
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4477
|
+
import { resolve as resolve5 } from "node:path";
|
|
4478
|
+
|
|
4479
|
+
// ../../packages/action-analyzer/src/types.ts
|
|
4480
|
+
var KIND_RISK_PROFILES = {
|
|
4481
|
+
"email.reply": ["ACTION", "SECRETS", "PROMPT"],
|
|
4482
|
+
"email.forward": ["ACTION", "SECRETS", "FILES", "PROMPT"],
|
|
4483
|
+
"message.post": ["ACTION", "SECRETS"],
|
|
4484
|
+
"a2a.delegate": ["ACTION", "NETWORK", "PROMPT"],
|
|
4485
|
+
"payment.authorize": ["MONEY", "SECRETS"],
|
|
4486
|
+
"account.register": ["ACTION", "SECRETS", "SUPPLY"],
|
|
4487
|
+
"github.write": ["ACTION", "SUPPLY"],
|
|
4488
|
+
"npm.publish": ["ACTION", "SUPPLY", "MONEY"],
|
|
4489
|
+
"cloud.modify": ["ACTION", "EXEC", "MONEY"]
|
|
4490
|
+
};
|
|
4491
|
+
|
|
4492
|
+
// ../../packages/action-analyzer/src/analyzeAction.ts
|
|
4493
|
+
function analyzeAction(descriptor) {
|
|
4494
|
+
const findings = [];
|
|
4495
|
+
if (descriptor.schema_version !== "calllint.action.v0") {
|
|
4496
|
+
findings.push({
|
|
4497
|
+
id: "action.unknown-schema-version",
|
|
4498
|
+
title: "Unknown action schema version",
|
|
4499
|
+
severity: "medium",
|
|
4500
|
+
blocker: false,
|
|
4501
|
+
symbol: "ACTION",
|
|
4502
|
+
riskClass: "S3",
|
|
4503
|
+
mode: "OBSERVED",
|
|
4504
|
+
confidence: "high",
|
|
4505
|
+
detectionMethod: "config-analysis",
|
|
4506
|
+
evidence: [{
|
|
4507
|
+
type: "config",
|
|
4508
|
+
path: "schema_version",
|
|
4509
|
+
value: descriptor.schema_version
|
|
4510
|
+
}],
|
|
4511
|
+
impact: `Unknown action schema version: ${descriptor.schema_version}`,
|
|
4512
|
+
fix: "Use calllint.action.v0 schema version"
|
|
4513
|
+
});
|
|
4514
|
+
return findings;
|
|
4515
|
+
}
|
|
4516
|
+
const riskProfile = KIND_RISK_PROFILES[descriptor.kind];
|
|
4517
|
+
if (!riskProfile) {
|
|
4518
|
+
findings.push({
|
|
4519
|
+
id: "action.unknown-kind",
|
|
4520
|
+
title: "Unknown action kind",
|
|
4521
|
+
severity: "medium",
|
|
4522
|
+
blocker: false,
|
|
4523
|
+
symbol: "ACTION",
|
|
4524
|
+
riskClass: "S3",
|
|
4525
|
+
mode: "OBSERVED",
|
|
4526
|
+
confidence: "high",
|
|
4527
|
+
detectionMethod: "config-analysis",
|
|
4528
|
+
evidence: [{
|
|
4529
|
+
type: "config",
|
|
4530
|
+
path: "kind",
|
|
4531
|
+
value: descriptor.kind
|
|
4532
|
+
}],
|
|
4533
|
+
impact: `Unknown action kind: ${descriptor.kind}`,
|
|
4534
|
+
fix: "Use one of the 9 defined action kinds (email.reply, email.forward, message.post, a2a.delegate, payment.authorize, account.register, github.write, npm.publish, cloud.modify)"
|
|
4535
|
+
});
|
|
4536
|
+
return findings;
|
|
4537
|
+
}
|
|
4538
|
+
switch (descriptor.kind) {
|
|
4539
|
+
case "email.reply":
|
|
4540
|
+
case "email.forward":
|
|
4541
|
+
findings.push(...analyzeEmail(descriptor));
|
|
4542
|
+
break;
|
|
4543
|
+
case "message.post":
|
|
4544
|
+
findings.push(...analyzeMessaging(descriptor));
|
|
4545
|
+
break;
|
|
4546
|
+
case "a2a.delegate":
|
|
4547
|
+
findings.push(...analyzeDelegate(descriptor));
|
|
4548
|
+
break;
|
|
4549
|
+
case "payment.authorize":
|
|
4550
|
+
findings.push(...analyzePayment(descriptor));
|
|
4551
|
+
break;
|
|
4552
|
+
case "account.register":
|
|
4553
|
+
findings.push(...analyzeRegistration(descriptor));
|
|
4554
|
+
break;
|
|
4555
|
+
case "github.write":
|
|
4556
|
+
findings.push(...analyzeGitHub(descriptor));
|
|
4557
|
+
break;
|
|
4558
|
+
case "npm.publish":
|
|
4559
|
+
findings.push(...analyzeNpmPublish(descriptor));
|
|
4560
|
+
break;
|
|
4561
|
+
case "cloud.modify":
|
|
4562
|
+
findings.push(...analyzeCloud(descriptor));
|
|
4563
|
+
break;
|
|
4564
|
+
}
|
|
4565
|
+
findings.push(...analyzeSecretShapedHeaders(descriptor));
|
|
4566
|
+
return findings;
|
|
4567
|
+
}
|
|
4568
|
+
function analyzeEmail(descriptor) {
|
|
4569
|
+
const findings = [];
|
|
4570
|
+
const metadata = descriptor.metadata || {};
|
|
4571
|
+
const hasAttachments = descriptor.parameters.has_attachments;
|
|
4572
|
+
const attachmentHashes = metadata.attachment_hashes || [];
|
|
4573
|
+
if (hasAttachments && attachmentHashes.length === 0) {
|
|
4574
|
+
findings.push({
|
|
4575
|
+
id: "action.unverified-attachment",
|
|
4576
|
+
title: "Unverified email attachments",
|
|
4577
|
+
severity: "high",
|
|
4578
|
+
blocker: false,
|
|
4579
|
+
symbol: "FILES",
|
|
4580
|
+
riskClass: "S2",
|
|
4581
|
+
mode: "OBSERVED",
|
|
4582
|
+
confidence: "high",
|
|
4583
|
+
detectionMethod: "config-analysis",
|
|
4584
|
+
evidence: [{
|
|
4585
|
+
type: "config",
|
|
4586
|
+
path: "parameters.has_attachments",
|
|
4587
|
+
value: String(hasAttachments)
|
|
4588
|
+
}],
|
|
4589
|
+
impact: "Email action has attachments but no attachment_hashes provided",
|
|
4590
|
+
fix: "Provide SHA-256 hashes for all attachments in metadata.attachment_hashes"
|
|
4591
|
+
});
|
|
4592
|
+
}
|
|
4593
|
+
return findings;
|
|
4594
|
+
}
|
|
4595
|
+
function analyzeMessaging(descriptor) {
|
|
4596
|
+
const findings = [];
|
|
4597
|
+
return findings;
|
|
4598
|
+
}
|
|
4599
|
+
function analyzeDelegate(descriptor) {
|
|
4600
|
+
const findings = [];
|
|
4601
|
+
const metadata = descriptor.metadata || {};
|
|
4602
|
+
const delegateTarget = metadata.delegate_target;
|
|
4603
|
+
if (!delegateTarget) {
|
|
4604
|
+
findings.push({
|
|
4605
|
+
id: "action.missing-delegate-target",
|
|
4606
|
+
title: "Missing delegate target",
|
|
4607
|
+
severity: "high",
|
|
4608
|
+
blocker: false,
|
|
4609
|
+
symbol: "NETWORK",
|
|
4610
|
+
riskClass: "S3",
|
|
4611
|
+
mode: "OBSERVED",
|
|
4612
|
+
confidence: "high",
|
|
4613
|
+
detectionMethod: "config-analysis",
|
|
4614
|
+
evidence: [{
|
|
4615
|
+
type: "config",
|
|
4616
|
+
path: "metadata.delegate_target",
|
|
4617
|
+
value: "null"
|
|
4618
|
+
}],
|
|
4619
|
+
impact: "Agent delegation missing delegate_target in metadata",
|
|
4620
|
+
fix: "Specify the delegate target (agent ID, API endpoint, or service name)"
|
|
4621
|
+
});
|
|
4622
|
+
} else if (typeof delegateTarget === "string" && delegateTarget.startsWith("http://")) {
|
|
4623
|
+
findings.push({
|
|
4624
|
+
id: "action.insecure-delegate-target",
|
|
4625
|
+
title: "Insecure delegate target",
|
|
4626
|
+
severity: "medium",
|
|
4627
|
+
blocker: false,
|
|
4628
|
+
symbol: "NETWORK",
|
|
4629
|
+
riskClass: "S3",
|
|
4630
|
+
mode: "OBSERVED",
|
|
4631
|
+
confidence: "high",
|
|
4632
|
+
detectionMethod: "config-analysis",
|
|
4633
|
+
evidence: [{
|
|
4634
|
+
type: "config",
|
|
4635
|
+
path: "metadata.delegate_target",
|
|
4636
|
+
value: delegateTarget
|
|
4637
|
+
}],
|
|
4638
|
+
impact: "Delegate target uses insecure HTTP",
|
|
4639
|
+
fix: "Use HTTPS for delegate targets"
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
return findings;
|
|
4643
|
+
}
|
|
4644
|
+
function analyzePayment(descriptor) {
|
|
4645
|
+
const findings = [];
|
|
4646
|
+
const metadata = descriptor.metadata || {};
|
|
4647
|
+
const amount = metadata.amount;
|
|
4648
|
+
if (typeof amount === "number" && amount > 0) {
|
|
4649
|
+
findings.push({
|
|
4650
|
+
id: "action.financial-observed",
|
|
4651
|
+
title: "Financial transaction observed",
|
|
4652
|
+
severity: "high",
|
|
4653
|
+
blocker: false,
|
|
4654
|
+
symbol: "MONEY",
|
|
4655
|
+
riskClass: "S5",
|
|
4656
|
+
mode: "OBSERVED",
|
|
4657
|
+
confidence: "high",
|
|
4658
|
+
detectionMethod: "config-analysis",
|
|
4659
|
+
evidence: [{
|
|
4660
|
+
type: "config",
|
|
4661
|
+
path: "metadata.amount",
|
|
4662
|
+
value: String(amount)
|
|
4663
|
+
}],
|
|
4664
|
+
impact: `Payment authorization for ${metadata.currency || "unknown currency"} ${amount}`,
|
|
4665
|
+
fix: "Verify payment amount and recipient before authorizing"
|
|
4666
|
+
});
|
|
4667
|
+
}
|
|
4668
|
+
return findings;
|
|
4669
|
+
}
|
|
4670
|
+
function analyzeRegistration(descriptor) {
|
|
4671
|
+
const findings = [];
|
|
4672
|
+
return findings;
|
|
4673
|
+
}
|
|
4674
|
+
function analyzeGitHub(descriptor) {
|
|
4675
|
+
const findings = [];
|
|
4676
|
+
return findings;
|
|
4677
|
+
}
|
|
4678
|
+
function analyzeNpmPublish(descriptor) {
|
|
4679
|
+
const findings = [];
|
|
4680
|
+
return findings;
|
|
4681
|
+
}
|
|
4682
|
+
function analyzeCloud(descriptor) {
|
|
4683
|
+
const findings = [];
|
|
4684
|
+
return findings;
|
|
4685
|
+
}
|
|
4686
|
+
function analyzeSecretShapedHeaders(descriptor) {
|
|
4687
|
+
const findings = [];
|
|
4688
|
+
const metadata = descriptor.metadata || {};
|
|
4689
|
+
const headerKeys = metadata.header_keys || [];
|
|
4690
|
+
const secretPatterns = ["authorization", "api-key", "api_key", "token", "secret", "password", "bearer"];
|
|
4691
|
+
for (const key of headerKeys) {
|
|
4692
|
+
const lowerKey = key.toLowerCase();
|
|
4693
|
+
if (secretPatterns.some((pattern) => lowerKey.includes(pattern))) {
|
|
4694
|
+
findings.push({
|
|
4695
|
+
id: "secrets.env-key",
|
|
4696
|
+
title: "Secret-shaped header key",
|
|
4697
|
+
severity: "medium",
|
|
4698
|
+
blocker: false,
|
|
4699
|
+
symbol: "SECRETS",
|
|
4700
|
+
riskClass: "S2",
|
|
4701
|
+
mode: "OBSERVED",
|
|
4702
|
+
confidence: "medium",
|
|
4703
|
+
detectionMethod: "config-analysis",
|
|
4704
|
+
evidence: [{
|
|
4705
|
+
type: "config",
|
|
4706
|
+
path: "metadata.header_keys",
|
|
4707
|
+
value: key
|
|
4708
|
+
}],
|
|
4709
|
+
impact: `Header key resembles a secret: ${key}`,
|
|
4710
|
+
fix: "Verify that secret headers are properly secured and not logged"
|
|
4711
|
+
});
|
|
4712
|
+
break;
|
|
4713
|
+
}
|
|
4714
|
+
}
|
|
4715
|
+
return findings;
|
|
4716
|
+
}
|
|
4717
|
+
|
|
4718
|
+
// src/commands/action.ts
|
|
4719
|
+
function actionCommand(args, deps) {
|
|
4720
|
+
const subcommand = args.positionals[0];
|
|
4721
|
+
if (!subcommand || subcommand === "help") {
|
|
4722
|
+
return {
|
|
4723
|
+
stdout: actionHelp(),
|
|
4724
|
+
stderr: "",
|
|
4725
|
+
exitCode: 0
|
|
4726
|
+
};
|
|
4727
|
+
}
|
|
4728
|
+
if (subcommand === "inspect") {
|
|
4729
|
+
return actionInspect(args, deps);
|
|
4730
|
+
}
|
|
4731
|
+
return {
|
|
4732
|
+
stdout: "",
|
|
4733
|
+
stderr: `Unknown action subcommand: ${subcommand}
|
|
4734
|
+
Run \`calllint action help\`.`,
|
|
4735
|
+
exitCode: 2
|
|
4736
|
+
};
|
|
4737
|
+
}
|
|
4738
|
+
function actionInspect(args, deps) {
|
|
4739
|
+
const actionFile = args.positionals[1];
|
|
4740
|
+
if (!actionFile) {
|
|
4741
|
+
return {
|
|
4742
|
+
stdout: "",
|
|
4743
|
+
stderr: "Error: Missing action file\nUsage: calllint action inspect <file.json>",
|
|
4744
|
+
exitCode: 2
|
|
4745
|
+
};
|
|
4746
|
+
}
|
|
4747
|
+
try {
|
|
4748
|
+
const absolutePath = resolve5(deps.cwd, actionFile);
|
|
4749
|
+
const content = readFileSync9(absolutePath, "utf-8");
|
|
4750
|
+
const descriptor = JSON.parse(content);
|
|
4751
|
+
if (descriptor.schema_version !== "calllint.action.v0") {
|
|
4752
|
+
return {
|
|
4753
|
+
stdout: "",
|
|
4754
|
+
stderr: `Error: Unsupported schema version: ${descriptor.schema_version}
|
|
4755
|
+
Expected: calllint.action.v0`,
|
|
4756
|
+
exitCode: 2
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
const policyPath = args.flags["policy"];
|
|
4760
|
+
const policy = loadPolicyOrDefault(policyPath ? resolve5(deps.cwd, policyPath) : void 0);
|
|
4761
|
+
const findings = analyzeAction(descriptor);
|
|
4762
|
+
const verdict = computeActionVerdict(findings, policy);
|
|
4763
|
+
let stdout = "";
|
|
4764
|
+
if (args.flags["json"]) {
|
|
4765
|
+
const report = {
|
|
4766
|
+
schema_version: "calllint.action-report.v0",
|
|
4767
|
+
verdict,
|
|
4768
|
+
findings,
|
|
4769
|
+
target: {
|
|
4770
|
+
type: "action",
|
|
4771
|
+
kind: descriptor.kind,
|
|
4772
|
+
schema_version: descriptor.schema_version
|
|
4773
|
+
},
|
|
4774
|
+
policy_applied: "default",
|
|
4775
|
+
scan_timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4776
|
+
};
|
|
4777
|
+
stdout = JSON.stringify(report, null, 2);
|
|
4778
|
+
} else {
|
|
4779
|
+
const header = `
|
|
4780
|
+
\u{1F50D} Action: ${descriptor.kind}
|
|
4781
|
+
`;
|
|
4782
|
+
const verdictEmoji = verdict === "SAFE" ? "\u2705" : verdict === "BLOCK" ? "\u26D4" : verdict === "REVIEW" ? "\u26A0\uFE0F" : "\u25C7";
|
|
4783
|
+
const verdictLine = `Verdict: ${verdictEmoji} ${verdict}
|
|
4784
|
+
`;
|
|
4785
|
+
const findingsLine = `Findings: ${findings.length}
|
|
4786
|
+
`;
|
|
4787
|
+
let body = verdictLine + findingsLine;
|
|
4788
|
+
if (findings.length > 0) {
|
|
4789
|
+
body += "\n";
|
|
4790
|
+
for (const f of findings) {
|
|
4791
|
+
body += ` \u2022 ${f.id}: ${f.impact}
|
|
4792
|
+
`;
|
|
4793
|
+
}
|
|
4794
|
+
}
|
|
4795
|
+
stdout = header + body;
|
|
4796
|
+
}
|
|
4797
|
+
return {
|
|
4798
|
+
stdout,
|
|
4799
|
+
stderr: "",
|
|
4800
|
+
exitCode: verdict === "SAFE" ? 0 : 1
|
|
4801
|
+
};
|
|
4802
|
+
} catch (error) {
|
|
4803
|
+
const err = error;
|
|
4804
|
+
let stderr = "";
|
|
4805
|
+
if (err.code === "ENOENT") {
|
|
4806
|
+
stderr = `Error: File not found: ${actionFile}`;
|
|
4807
|
+
} else if (error instanceof SyntaxError) {
|
|
4808
|
+
stderr = `Error: Invalid JSON in ${actionFile}
|
|
4809
|
+
${error.message}`;
|
|
4810
|
+
} else {
|
|
4811
|
+
stderr = `Error: ${err.message}`;
|
|
4812
|
+
}
|
|
4813
|
+
return {
|
|
4814
|
+
stdout: "",
|
|
4815
|
+
stderr,
|
|
4816
|
+
exitCode: 2
|
|
4817
|
+
};
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
function actionHelp() {
|
|
4821
|
+
return `calllint action \u2014 Inspect planned external actions
|
|
4822
|
+
|
|
4823
|
+
USAGE
|
|
4824
|
+
calllint action inspect <file.json>
|
|
4825
|
+
calllint action help
|
|
4826
|
+
|
|
4827
|
+
COMMANDS
|
|
4828
|
+
inspect <file> Analyze a calllint.action.v0 descriptor
|
|
4829
|
+
help Show this help
|
|
4830
|
+
|
|
4831
|
+
OPTIONS
|
|
4832
|
+
--json Output JSON report
|
|
4833
|
+
--policy <file> Use custom policy file
|
|
4834
|
+
--no-emoji Disable emoji in output
|
|
4835
|
+
|
|
4836
|
+
EXAMPLE
|
|
4837
|
+
calllint action inspect payment.json
|
|
4838
|
+
calllint action inspect email-reply.json --json
|
|
4839
|
+
|
|
4840
|
+
See: https://calllint.com/docs/action-inspect (ADR 0029)
|
|
4841
|
+
`;
|
|
4842
|
+
}
|
|
4843
|
+
function computeActionVerdict(findings, policy) {
|
|
4844
|
+
if (findings.length === 0) {
|
|
4845
|
+
return "SAFE";
|
|
4846
|
+
}
|
|
4847
|
+
const hasBlocker = findings.some((f) => f.blocker || f.severity === "critical");
|
|
4848
|
+
if (hasBlocker) {
|
|
4849
|
+
return "BLOCK";
|
|
4850
|
+
}
|
|
4851
|
+
const hasHigh = findings.some((f) => f.severity === "high");
|
|
4852
|
+
if (hasHigh) {
|
|
4853
|
+
return "REVIEW";
|
|
4854
|
+
}
|
|
4855
|
+
return "REVIEW";
|
|
4856
|
+
}
|
|
4857
|
+
|
|
4475
4858
|
// src/run.ts
|
|
4476
4859
|
function run(argv, deps) {
|
|
4477
4860
|
const args = parseArgs(argv);
|
|
@@ -4540,6 +4923,8 @@ function run(argv, deps) {
|
|
|
4540
4923
|
return explainCommand(args, { cwd: deps.cwd });
|
|
4541
4924
|
case "receipt":
|
|
4542
4925
|
return receiptCommand(args, { cwd: deps.cwd });
|
|
4926
|
+
case "action":
|
|
4927
|
+
return actionCommand(args, { cwd: deps.cwd });
|
|
4543
4928
|
case "gen-rule":
|
|
4544
4929
|
return genRuleCommand(args, { cwd: deps.cwd });
|
|
4545
4930
|
case "policy":
|
|
@@ -4813,13 +5198,13 @@ async function breathe(argv, deps = {}) {
|
|
|
4813
5198
|
}
|
|
4814
5199
|
|
|
4815
5200
|
// src/version.ts
|
|
4816
|
-
import { readFileSync as
|
|
5201
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4817
5202
|
import { fileURLToPath } from "node:url";
|
|
4818
5203
|
import { dirname as dirname4, join as join11 } from "node:path";
|
|
4819
5204
|
function resolveToolVersion() {
|
|
4820
5205
|
try {
|
|
4821
5206
|
const pkgPath = join11(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
4822
|
-
const pkg = JSON.parse(
|
|
5207
|
+
const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
|
|
4823
5208
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
4824
5209
|
} catch {
|
|
4825
5210
|
return "unknown";
|
|
@@ -4829,7 +5214,7 @@ function resolveToolVersion() {
|
|
|
4829
5214
|
// src/index.ts
|
|
4830
5215
|
function readStdin() {
|
|
4831
5216
|
try {
|
|
4832
|
-
return
|
|
5217
|
+
return readFileSync11(0, "utf8");
|
|
4833
5218
|
} catch {
|
|
4834
5219
|
return "";
|
|
4835
5220
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {},
|
|
51
51
|
"devDependencies": {
|
|
52
|
+
"@calllint/action-analyzer": "workspace:*",
|
|
52
53
|
"@calllint/core": "workspace:*",
|
|
53
54
|
"@calllint/fixtures": "workspace:*",
|
|
54
55
|
"@calllint/online": "workspace:*",
|