@khanglvm/outline-cli 0.1.2 → 0.1.4
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/CHANGELOG.md +10 -0
- package/README.md +44 -22
- package/bin/outline-cli.js +14 -0
- package/docs/TOOL_CONTRACTS.md +8 -0
- package/package.json +1 -1
- package/src/agent-skills.js +160 -23
- package/src/cli.js +207 -63
- package/src/config-store.js +86 -6
- package/src/entry-integrity-manifest.generated.js +15 -11
- package/src/entry-integrity.js +3 -0
- package/src/summary-redaction.js +37 -0
- package/src/tool-arg-schemas.js +266 -10
- package/src/tools.extended.js +123 -16
- package/src/tools.js +277 -21
- package/src/tools.mutation.js +2 -1
- package/src/tools.navigation.js +3 -2
- package/test/agent-skills.unit.test.js +64 -1
- package/test/config-store.unit.test.js +32 -0
- package/test/hardening.unit.test.js +26 -1
- package/test/live.integration.test.js +20 -24
- package/test/profile-selection.unit.test.js +14 -4
- package/test/tool-resolution.unit.test.js +333 -0
- package/test/version.unit.test.js +21 -0
package/src/config-store.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import fsSync from "node:fs";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { CliError } from "./errors.js";
|
|
5
6
|
|
|
6
7
|
export const CONFIG_VERSION = 1;
|
|
7
8
|
|
|
@@ -479,6 +480,45 @@ export function suggestProfiles(config, query, options = {}) {
|
|
|
479
480
|
};
|
|
480
481
|
}
|
|
481
482
|
|
|
483
|
+
function isHighConfidenceProfileMatch(top, second, options = {}) {
|
|
484
|
+
if (!top) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const minScore = Number.isFinite(Number(options.minScore)) ? Number(options.minScore) : 1.7;
|
|
489
|
+
const minGap = Number.isFinite(Number(options.minGap)) ? Number(options.minGap) : 0.55;
|
|
490
|
+
const exactSignals = new Set(["id_exact", "name_exact", "keyword_exact", "host"]);
|
|
491
|
+
const hasExactSignal = Array.isArray(top.matchedOn) && top.matchedOn.some((item) => exactSignals.has(item));
|
|
492
|
+
const gap = top.score - Number(second?.score || 0);
|
|
493
|
+
|
|
494
|
+
if (top.score >= 3.2) {
|
|
495
|
+
return true;
|
|
496
|
+
}
|
|
497
|
+
if (top.score >= minScore && gap >= minGap) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
if (hasExactSignal && top.score >= 1.2 && gap >= 0.35) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function formatProfileSelectionError(config, message, options = {}) {
|
|
508
|
+
const availableProfiles = Object.keys(config?.profiles || {});
|
|
509
|
+
const query = String(options.query || "").trim();
|
|
510
|
+
const suggestionLimit = Number.isFinite(Number(options.suggestionLimit))
|
|
511
|
+
? Math.max(1, Number(options.suggestionLimit))
|
|
512
|
+
: 3;
|
|
513
|
+
const suggestions = query ? suggestProfiles(config, query, { limit: suggestionLimit }).matches : [];
|
|
514
|
+
|
|
515
|
+
return new CliError(message, {
|
|
516
|
+
code: "PROFILE_SELECTION_REQUIRED",
|
|
517
|
+
availableProfiles,
|
|
518
|
+
...(query ? { query, suggestions } : {}),
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
482
522
|
export function suggestProfileMetadata(input = {}, options = {}) {
|
|
483
523
|
const id = String(input.id || "").trim();
|
|
484
524
|
const name = String(input.name || id || "").trim();
|
|
@@ -566,13 +606,17 @@ export function listProfiles(config) {
|
|
|
566
606
|
}));
|
|
567
607
|
}
|
|
568
608
|
|
|
569
|
-
export function getProfile(config, explicitId) {
|
|
609
|
+
export function getProfile(config, explicitId, options = {}) {
|
|
570
610
|
const profiles = config?.profiles || {};
|
|
571
611
|
|
|
572
612
|
if (explicitId) {
|
|
573
613
|
const profile = profiles[explicitId];
|
|
574
614
|
if (!profile) {
|
|
575
|
-
throw new
|
|
615
|
+
throw new CliError(`Profile not found: ${explicitId}`, {
|
|
616
|
+
code: "PROFILE_NOT_FOUND",
|
|
617
|
+
profileId: explicitId,
|
|
618
|
+
availableProfiles: Object.keys(profiles),
|
|
619
|
+
});
|
|
576
620
|
}
|
|
577
621
|
return {
|
|
578
622
|
id: explicitId,
|
|
@@ -583,7 +627,11 @@ export function getProfile(config, explicitId) {
|
|
|
583
627
|
if (config.defaultProfile) {
|
|
584
628
|
const profile = profiles[config.defaultProfile];
|
|
585
629
|
if (!profile) {
|
|
586
|
-
throw new
|
|
630
|
+
throw new CliError(`Profile not found: ${config.defaultProfile}`, {
|
|
631
|
+
code: "PROFILE_NOT_FOUND",
|
|
632
|
+
profileId: config.defaultProfile,
|
|
633
|
+
availableProfiles: Object.keys(profiles),
|
|
634
|
+
});
|
|
587
635
|
}
|
|
588
636
|
return {
|
|
589
637
|
id: config.defaultProfile,
|
|
@@ -601,12 +649,44 @@ export function getProfile(config, explicitId) {
|
|
|
601
649
|
}
|
|
602
650
|
|
|
603
651
|
if (profileIds.length > 1) {
|
|
604
|
-
|
|
605
|
-
|
|
652
|
+
const query = String(options.query || "").trim();
|
|
653
|
+
const allowAutoSelect = options.allowAutoSelect !== false;
|
|
654
|
+
const suggestionLimit = Number.isFinite(Number(options.suggestionLimit))
|
|
655
|
+
? Math.max(1, Number(options.suggestionLimit))
|
|
656
|
+
: 3;
|
|
657
|
+
|
|
658
|
+
if (allowAutoSelect && query) {
|
|
659
|
+
const suggestions = suggestProfiles(config, query, { limit: suggestionLimit }).matches;
|
|
660
|
+
const top = suggestions[0];
|
|
661
|
+
const second = suggestions[1];
|
|
662
|
+
|
|
663
|
+
if (isHighConfidenceProfileMatch(top, second, options)) {
|
|
664
|
+
return {
|
|
665
|
+
id: top.id,
|
|
666
|
+
...profiles[top.id],
|
|
667
|
+
selection: {
|
|
668
|
+
autoSelected: true,
|
|
669
|
+
query,
|
|
670
|
+
score: top.score,
|
|
671
|
+
matchedOn: top.matchedOn,
|
|
672
|
+
},
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
throw formatProfileSelectionError(
|
|
678
|
+
config,
|
|
679
|
+
"Profile selection required: multiple profiles are saved and no default profile is set. Use --profile <id> or `outline-cli profile use <id>`.",
|
|
680
|
+
{
|
|
681
|
+
query,
|
|
682
|
+
suggestionLimit,
|
|
683
|
+
}
|
|
606
684
|
);
|
|
607
685
|
}
|
|
608
686
|
|
|
609
|
-
throw new
|
|
687
|
+
throw new CliError("No profiles configured. Use `outline-cli profile add <id> ...` first.", {
|
|
688
|
+
code: "PROFILE_NOT_CONFIGURED",
|
|
689
|
+
});
|
|
610
690
|
}
|
|
611
691
|
|
|
612
692
|
export function redactProfile(profile) {
|
|
@@ -3,8 +3,8 @@ export const ENTRY_INTEGRITY_MANIFEST = Object.freeze({
|
|
|
3
3
|
version: 1,
|
|
4
4
|
algorithm: "sha256",
|
|
5
5
|
signatureAlgorithm: "sha256-salted-manifest-v1",
|
|
6
|
-
signature: "
|
|
7
|
-
generatedAt: "2026-03-
|
|
6
|
+
signature: "c69d41948b78c8ae35c0223377149ece8df5588863a010adf92a0b6ad11876d3",
|
|
7
|
+
generatedAt: "2026-03-07T12:16:27.340Z",
|
|
8
8
|
files: [
|
|
9
9
|
{
|
|
10
10
|
"path": "src/action-gate.js",
|
|
@@ -12,19 +12,19 @@ export const ENTRY_INTEGRITY_MANIFEST = Object.freeze({
|
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
14
|
"path": "src/agent-skills.js",
|
|
15
|
-
"sha256": "
|
|
15
|
+
"sha256": "d40e94a4b61922d3ee8a43ed89d5e9ce1562a2cdffd37e2f396c2a18b9a481fc"
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
"path": "src/cli.js",
|
|
19
|
-
"sha256": "
|
|
19
|
+
"sha256": "2b6b17a71c15954c2663397d57afde2b8825ed368bb7f71833f6191662eff079"
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
"path": "src/config-store.js",
|
|
23
|
-
"sha256": "
|
|
23
|
+
"sha256": "3773d59d74502c75b7375c194b4f0e2c39fb43e95f739e6e4878aa1b0957a511"
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
"path": "src/entry-integrity.js",
|
|
27
|
-
"sha256": "
|
|
27
|
+
"sha256": "5f66c29e57f53f11fc1751c9a8bd41c318001e3fb6562ee6f3a593f31849e45e"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "src/errors.js",
|
|
@@ -42,25 +42,29 @@ export const ENTRY_INTEGRITY_MANIFEST = Object.freeze({
|
|
|
42
42
|
"path": "src/secure-keyring.js",
|
|
43
43
|
"sha256": "370d904265733f7d6b0f47b3213c7f026c95e59bc18d364dff96be90dbd7f479"
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
"path": "src/summary-redaction.js",
|
|
47
|
+
"sha256": "ddd3562334e704de5d9f6068726e311a98564e7e46bcdb56d9aade7e63b65bd3"
|
|
48
|
+
},
|
|
45
49
|
{
|
|
46
50
|
"path": "src/tool-arg-schemas.js",
|
|
47
|
-
"sha256": "
|
|
51
|
+
"sha256": "51f8727255e5b2b0a4cce78495cfaed9bb1869a7ef104b22dbc170604f878feb"
|
|
48
52
|
},
|
|
49
53
|
{
|
|
50
54
|
"path": "src/tools.extended.js",
|
|
51
|
-
"sha256": "
|
|
55
|
+
"sha256": "0c5317ca0b28a0b7110d9e6c81d8ca6ccd705e9b303de2f29e03f847b31df310"
|
|
52
56
|
},
|
|
53
57
|
{
|
|
54
58
|
"path": "src/tools.js",
|
|
55
|
-
"sha256": "
|
|
59
|
+
"sha256": "96cfbb14cac2baa5221dc4e9294a0520f81c3e22a0c69ee648cd6c2354e1512e"
|
|
56
60
|
},
|
|
57
61
|
{
|
|
58
62
|
"path": "src/tools.mutation.js",
|
|
59
|
-
"sha256": "
|
|
63
|
+
"sha256": "15fa3d146d99f55bc16d814ffd9360127b99eca2f9271cf751c6e5c7e4234317"
|
|
60
64
|
},
|
|
61
65
|
{
|
|
62
66
|
"path": "src/tools.navigation.js",
|
|
63
|
-
"sha256": "
|
|
67
|
+
"sha256": "f7118e91c2edd0e6b57591d41fe7707c68eef51cde77918495bb4ee166139339"
|
|
64
68
|
},
|
|
65
69
|
{
|
|
66
70
|
"path": "src/tools.platform.js",
|
package/src/entry-integrity.js
CHANGED
|
@@ -52,6 +52,7 @@ export async function assertEntryIntegrity(rootDir = REPO_ROOT) {
|
|
|
52
52
|
if (files.length === 0) {
|
|
53
53
|
throw new CliError("Entry integrity manifest is empty", {
|
|
54
54
|
code: "ENTRY_INTEGRITY_MANIFEST_EMPTY",
|
|
55
|
+
hint: "Run `npm run integrity:refresh` to regenerate local integrity metadata.",
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
|
|
@@ -62,6 +63,7 @@ export async function assertEntryIntegrity(rootDir = REPO_ROOT) {
|
|
|
62
63
|
expected: manifest.signature,
|
|
63
64
|
actual: computedSignature,
|
|
64
65
|
keyId: ENTRY_INTEGRITY_BINDING.keyId,
|
|
66
|
+
hint: "Run `npm run integrity:refresh` after local source edits, or set `OUTLINE_CLI_SKIP_INTEGRITY_CHECK=1` for local smoke runs.",
|
|
65
67
|
});
|
|
66
68
|
}
|
|
67
69
|
|
|
@@ -93,6 +95,7 @@ export async function assertEntryIntegrity(rootDir = REPO_ROOT) {
|
|
|
93
95
|
code: "ENTRY_SUBMODULE_INTEGRITY_FAILED",
|
|
94
96
|
mismatchCount: mismatches.length,
|
|
95
97
|
mismatches,
|
|
98
|
+
hint: "Run `npm run integrity:refresh` after local source edits, or set `OUTLINE_CLI_SKIP_INTEGRITY_CHECK=1` for local smoke runs.",
|
|
96
99
|
});
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const SUMMARY_SECRET_LINE_PATTERNS = [
|
|
2
|
+
/(^|\n)([ \t>*+\-]*(?:[*_`~]+\s*)*(?:user(?:name)?\s*\/\s*pass(?:word)?|username\s*\/\s*password|credentials?|api[ _-]*key|access[ _-]*token|refresh[ _-]*token|client[ _-]*secret|secret|password|pass|pwd|authorization|bearer)(?:\s*[*_`~]+)*\s*:\s*)([^\n]+)/gi,
|
|
3
|
+
/(^|\n)([ \t>*+\-]*(?:[*_`~]+\s*)*(?:user(?:name)?|email|login)(?:\s*[*_`~]+)*\s*:\s*)([^\n]+)(\n[ \t>*+\-]*(?:[*_`~]+\s*)*(?:pass(?:word)?|pwd)(?:\s*[*_`~]+)*\s*:\s*)([^\n]+)/gi,
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
export function redactSensitiveSummaryText(text) {
|
|
7
|
+
if (typeof text !== "string" || text.length === 0) {
|
|
8
|
+
return text;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let redacted = text;
|
|
12
|
+
for (const pattern of SUMMARY_SECRET_LINE_PATTERNS) {
|
|
13
|
+
redacted = redacted.replace(pattern, (...parts) => {
|
|
14
|
+
const leading = parts[1] || "";
|
|
15
|
+
const prefix = parts[2] || "";
|
|
16
|
+
const secondPrefix = parts[4];
|
|
17
|
+
if (typeof secondPrefix === "string") {
|
|
18
|
+
return `${leading}${prefix}[REDACTED]${secondPrefix}[REDACTED]`;
|
|
19
|
+
}
|
|
20
|
+
return `${leading}${prefix}[REDACTED]`;
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
redacted = redacted.replace(/\bol_api_[A-Za-z0-9]+\b/g, "ol_api_[REDACTED]");
|
|
25
|
+
redacted = redacted.replace(/\b(?:ghp|gho|ghu|ghs|github_pat|sk|pk)_[A-Za-z0-9_]+\b/g, "[REDACTED_TOKEN]");
|
|
26
|
+
redacted = redacted.replace(/(https?:\/\/)([^\s:@/]+):([^\s@/]+)@/gi, "$1[REDACTED]@");
|
|
27
|
+
|
|
28
|
+
return redacted;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function summarizeSafeText(text, maxChars) {
|
|
32
|
+
const redacted = redactSensitiveSummaryText(text);
|
|
33
|
+
if (typeof redacted !== "string") {
|
|
34
|
+
return redacted;
|
|
35
|
+
}
|
|
36
|
+
return redacted.length > maxChars ? `${redacted.slice(0, maxChars)}...` : redacted;
|
|
37
|
+
}
|
package/src/tool-arg-schemas.js
CHANGED
|
@@ -11,12 +11,8 @@ const TYPES = {
|
|
|
11
11
|
"string|string[]": (v) => typeof v === "string" || (Array.isArray(v) && v.every((x) => typeof x === "string")),
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
function fail(tool, issues) {
|
|
15
|
-
throw new CliError(`Invalid args for ${tool}`,
|
|
16
|
-
code: "ARG_VALIDATION_FAILED",
|
|
17
|
-
tool,
|
|
18
|
-
issues,
|
|
19
|
-
});
|
|
14
|
+
function fail(tool, issues, spec = {}, args = undefined) {
|
|
15
|
+
throw new CliError(`Invalid args for ${tool}`, buildValidationDetails(tool, spec, issues, args));
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
function ensureObject(tool, args) {
|
|
@@ -25,7 +21,248 @@ function ensureObject(tool, args) {
|
|
|
25
21
|
}
|
|
26
22
|
}
|
|
27
23
|
|
|
28
|
-
function
|
|
24
|
+
function levenshteinDistance(a, b) {
|
|
25
|
+
const left = String(a || "");
|
|
26
|
+
const right = String(b || "");
|
|
27
|
+
const rows = left.length + 1;
|
|
28
|
+
const cols = right.length + 1;
|
|
29
|
+
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < rows; i += 1) {
|
|
32
|
+
matrix[i][0] = i;
|
|
33
|
+
}
|
|
34
|
+
for (let j = 0; j < cols; j += 1) {
|
|
35
|
+
matrix[0][j] = j;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 1; i < rows; i += 1) {
|
|
39
|
+
for (let j = 1; j < cols; j += 1) {
|
|
40
|
+
const cost = left[i - 1] === right[j - 1] ? 0 : 1;
|
|
41
|
+
matrix[i][j] = Math.min(
|
|
42
|
+
matrix[i - 1][j] + 1,
|
|
43
|
+
matrix[i][j - 1] + 1,
|
|
44
|
+
matrix[i - 1][j - 1] + cost
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return matrix[rows - 1][cols - 1];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function buildAcceptedArgList(spec = {}) {
|
|
53
|
+
const accepted = new Set(Object.keys(spec.properties || {}));
|
|
54
|
+
accepted.add("compact");
|
|
55
|
+
return [...accepted].sort();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function suggestClosestArgNames(key, acceptedArgs) {
|
|
59
|
+
const raw = String(key || "").trim();
|
|
60
|
+
if (!raw) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return acceptedArgs
|
|
65
|
+
.map((candidate) => ({
|
|
66
|
+
candidate,
|
|
67
|
+
distance: levenshteinDistance(raw.toLowerCase(), String(candidate).toLowerCase()),
|
|
68
|
+
}))
|
|
69
|
+
.filter((row) => row.distance <= 3)
|
|
70
|
+
.sort((a, b) => a.distance - b.distance || a.candidate.localeCompare(b.candidate))
|
|
71
|
+
.slice(0, 3)
|
|
72
|
+
.map((row) => row.candidate);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function enrichIssuesWithArgSuggestions(issues, acceptedArgs) {
|
|
76
|
+
return issues.map((issue) => {
|
|
77
|
+
if (!issue || issue.message !== "is not allowed" || typeof issue.path !== "string") {
|
|
78
|
+
return issue;
|
|
79
|
+
}
|
|
80
|
+
const key = issue.path.startsWith("args.") ? issue.path.slice(5) : issue.path;
|
|
81
|
+
const suggestions = suggestClosestArgNames(key, acceptedArgs);
|
|
82
|
+
return suggestions.length > 0 ? { ...issue, suggestions } : issue;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractArgKeyFromIssue(issue) {
|
|
87
|
+
if (!issue || typeof issue.path !== "string") {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
return issue.path.startsWith("args.") ? issue.path.slice(5) : issue.path;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function applySuggestedArgFixes(args, issues) {
|
|
94
|
+
if (!args || typeof args !== "object" || Array.isArray(args)) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const next = { ...args };
|
|
99
|
+
let changed = false;
|
|
100
|
+
|
|
101
|
+
for (const issue of issues) {
|
|
102
|
+
if (!issue || issue.message !== "is not allowed") {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const key = extractArgKeyFromIssue(issue);
|
|
106
|
+
const target = Array.isArray(issue.suggestions) && issue.suggestions.length > 0
|
|
107
|
+
? issue.suggestions[0]
|
|
108
|
+
: null;
|
|
109
|
+
if (!key || !target || !(key in next) || key === target) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (target in next && next[target] !== next[key]) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!(target in next)) {
|
|
116
|
+
next[target] = next[key];
|
|
117
|
+
}
|
|
118
|
+
delete next[key];
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return changed ? next : undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function buildSuggestedArgs(spec, args, enrichedIssues) {
|
|
126
|
+
const candidate = applySuggestedArgFixes(args, enrichedIssues);
|
|
127
|
+
if (!candidate) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const normalized = normalizeArgsForSpec(candidate, spec);
|
|
132
|
+
const remainingIssues = collectValidationIssues(normalized, spec);
|
|
133
|
+
return remainingIssues.length === 0 ? normalized : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function buildValidationDetails(tool, spec, issues, args = undefined) {
|
|
137
|
+
const acceptedArgs = buildAcceptedArgList(spec);
|
|
138
|
+
const enrichedIssues = enrichIssuesWithArgSuggestions(issues, acceptedArgs);
|
|
139
|
+
const requiredArgs = [...new Set(spec.required || [])].sort();
|
|
140
|
+
const unknownArgs = enrichedIssues
|
|
141
|
+
.filter((issue) => issue?.message === "is not allowed" && typeof issue.path === "string")
|
|
142
|
+
.map((issue) => issue.path.replace(/^args\./, ""));
|
|
143
|
+
const suggestedArgs = buildSuggestedArgs(spec, args, enrichedIssues);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
code: "ARG_VALIDATION_FAILED",
|
|
147
|
+
tool,
|
|
148
|
+
issues: enrichedIssues,
|
|
149
|
+
acceptedArgs,
|
|
150
|
+
requiredArgs,
|
|
151
|
+
unknownArgs,
|
|
152
|
+
suggestedArgs,
|
|
153
|
+
validationHint:
|
|
154
|
+
acceptedArgs.length > 0 ? `Accepted args: ${acceptedArgs.join(", ")}` : undefined,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function looksNumeric(value) {
|
|
159
|
+
return /^-?(?:\d+|\d+\.\d+)$/.test(String(value || "").trim());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function coerceScalarValue(types, value) {
|
|
163
|
+
if (typeof value !== "string") {
|
|
164
|
+
return value;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const trimmed = value.trim();
|
|
168
|
+
if (!trimmed) {
|
|
169
|
+
return value;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (types.includes("boolean") && /^(true|false)$/i.test(trimmed)) {
|
|
173
|
+
return trimmed.toLowerCase() === "true";
|
|
174
|
+
}
|
|
175
|
+
if (types.includes("null") && /^null$/i.test(trimmed)) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (types.includes("number") && looksNumeric(trimmed)) {
|
|
179
|
+
return Number(trimmed);
|
|
180
|
+
}
|
|
181
|
+
if (types.includes("object") && /^[{]/.test(trimmed)) {
|
|
182
|
+
try {
|
|
183
|
+
return JSON.parse(trimmed);
|
|
184
|
+
} catch {
|
|
185
|
+
return value;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (types.includes("array") && /^[[]/.test(trimmed)) {
|
|
189
|
+
try {
|
|
190
|
+
return JSON.parse(trimmed);
|
|
191
|
+
} catch {
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function coerceArrayValue(types, value) {
|
|
200
|
+
if (!Array.isArray(value)) {
|
|
201
|
+
if (types.includes("string[]") && typeof value === "string") {
|
|
202
|
+
return [value];
|
|
203
|
+
}
|
|
204
|
+
return value;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (types.includes("string[]")) {
|
|
208
|
+
return value.map((item) => (typeof item === "string" ? item : String(item)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function normalizeCrossFieldAliases(args, properties) {
|
|
215
|
+
const next = { ...args };
|
|
216
|
+
|
|
217
|
+
if (Array.isArray(next.query) && properties.queries && next.queries === undefined) {
|
|
218
|
+
next.queries = next.query.map((item) => String(item));
|
|
219
|
+
delete next.query;
|
|
220
|
+
}
|
|
221
|
+
if (Array.isArray(next.question) && properties.questions && next.questions === undefined) {
|
|
222
|
+
next.questions = next.question.map((item) => String(item));
|
|
223
|
+
delete next.question;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(next.id) && properties.ids && next.ids === undefined) {
|
|
226
|
+
next.ids = next.id.map((item) => String(item));
|
|
227
|
+
delete next.id;
|
|
228
|
+
}
|
|
229
|
+
if (typeof next.ids === "string" && properties.id && !properties.ids && next.id === undefined) {
|
|
230
|
+
next.id = next.ids;
|
|
231
|
+
delete next.ids;
|
|
232
|
+
}
|
|
233
|
+
if (typeof next.queries === "string" && properties.query && !properties.queries && next.query === undefined) {
|
|
234
|
+
next.query = next.queries;
|
|
235
|
+
delete next.queries;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return next;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeArgsForSpec(args, spec) {
|
|
242
|
+
const properties = spec.properties || {};
|
|
243
|
+
const next = normalizeCrossFieldAliases(args, properties);
|
|
244
|
+
|
|
245
|
+
for (const [key, rule] of Object.entries(properties)) {
|
|
246
|
+
if (!(key in next)) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const types = Array.isArray(rule.type) ? rule.type : [rule.type];
|
|
251
|
+
let value = next[key];
|
|
252
|
+
value = coerceArrayValue(types, value);
|
|
253
|
+
value = coerceScalarValue(types, value);
|
|
254
|
+
|
|
255
|
+
if (Array.isArray(value) && types.includes("string[]")) {
|
|
256
|
+
value = value.map((item) => (typeof item === "string" ? item : String(item)));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
next[key] = value;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return next;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function collectValidationIssues(args, spec) {
|
|
29
266
|
const issues = [];
|
|
30
267
|
const properties = spec.properties || {};
|
|
31
268
|
|
|
@@ -81,8 +318,14 @@ function validateSpec(tool, args, spec) {
|
|
|
81
318
|
spec.custom(args, issues);
|
|
82
319
|
}
|
|
83
320
|
|
|
321
|
+
return issues;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function validateSpec(tool, args, spec) {
|
|
325
|
+
const issues = collectValidationIssues(args, spec);
|
|
326
|
+
|
|
84
327
|
if (issues.length > 0) {
|
|
85
|
-
fail(tool, issues);
|
|
328
|
+
fail(tool, issues, spec, args);
|
|
86
329
|
}
|
|
87
330
|
}
|
|
88
331
|
|
|
@@ -151,10 +394,19 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
151
394
|
sort: { type: "string" },
|
|
152
395
|
direction: { type: "string", enum: ["ASC", "DESC"] },
|
|
153
396
|
parentDocumentId: { type: ["string", "null"] },
|
|
397
|
+
rootOnly: { type: "boolean" },
|
|
154
398
|
backlinkDocumentId: { type: "string" },
|
|
155
399
|
includePolicies: { type: "boolean" },
|
|
156
400
|
...SHARED_DOC_COMMON,
|
|
157
401
|
},
|
|
402
|
+
custom(args, issues) {
|
|
403
|
+
if (args.rootOnly === true && Object.prototype.hasOwnProperty.call(args, "parentDocumentId") && args.parentDocumentId !== null) {
|
|
404
|
+
issues.push({
|
|
405
|
+
path: "args.rootOnly",
|
|
406
|
+
message: "cannot be combined with a non-null args.parentDocumentId",
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
},
|
|
158
410
|
},
|
|
159
411
|
"documents.backlinks": {
|
|
160
412
|
required: ["id"],
|
|
@@ -2245,6 +2497,7 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
2245
2497
|
dateFilter: { type: "string", enum: ["day", "week", "month", "year"] },
|
|
2246
2498
|
includePolicies: { type: "boolean" },
|
|
2247
2499
|
includeEvidenceDocs: { type: "boolean" },
|
|
2500
|
+
limit: { type: "number", min: 1 },
|
|
2248
2501
|
view: { type: "string", enum: ["summary", "full"] },
|
|
2249
2502
|
maxAttempts: { type: "number", min: 1 },
|
|
2250
2503
|
},
|
|
@@ -2267,6 +2520,7 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
2267
2520
|
dateFilter: { type: "string", enum: ["day", "week", "month", "year"] },
|
|
2268
2521
|
includePolicies: { type: "boolean" },
|
|
2269
2522
|
includeEvidenceDocs: { type: "boolean" },
|
|
2523
|
+
limit: { type: "number", min: 1 },
|
|
2270
2524
|
view: { type: "string", enum: ["summary", "full"] },
|
|
2271
2525
|
concurrency: { type: "number", min: 1 },
|
|
2272
2526
|
maxAttempts: { type: "number", min: 1 },
|
|
@@ -2338,9 +2592,11 @@ export const TOOL_ARG_SCHEMAS = {
|
|
|
2338
2592
|
export function validateToolArgs(toolName, args = {}) {
|
|
2339
2593
|
const spec = TOOL_ARG_SCHEMAS[toolName];
|
|
2340
2594
|
if (!spec) {
|
|
2341
|
-
return;
|
|
2595
|
+
return args;
|
|
2342
2596
|
}
|
|
2343
2597
|
|
|
2344
2598
|
ensureObject(toolName, args);
|
|
2345
|
-
|
|
2599
|
+
const normalizedArgs = normalizeArgsForSpec(args, spec);
|
|
2600
|
+
validateSpec(toolName, normalizedArgs, spec);
|
|
2601
|
+
return normalizedArgs;
|
|
2346
2602
|
}
|