@blogic-cz/agent-tools 1.10.1 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +21 -44
  2. package/dist/credential-guard/bash.d.ts +3 -0
  3. package/dist/credential-guard/bash.d.ts.map +1 -0
  4. package/dist/credential-guard/claude-hook.d.ts.map +1 -1
  5. package/dist/credential-guard/engine.test.d.ts +5 -0
  6. package/dist/credential-guard/engine.test.d.ts.map +1 -0
  7. package/dist/credential-guard/index.d.ts +8 -35
  8. package/dist/credential-guard/index.d.ts.map +1 -1
  9. package/dist/credential-guard/ir.d.ts +150 -0
  10. package/dist/credential-guard/ir.d.ts.map +1 -0
  11. package/dist/credential-guard/literal-output.d.ts +2 -0
  12. package/dist/credential-guard/literal-output.d.ts.map +1 -0
  13. package/dist/credential-guard/passive-awk.d.ts +2 -0
  14. package/dist/credential-guard/passive-awk.d.ts.map +1 -0
  15. package/dist/credential-guard/passive-jq.d.ts.map +1 -1
  16. package/dist/credential-guard/passive-sed.d.ts +2 -0
  17. package/dist/credential-guard/passive-sed.d.ts.map +1 -0
  18. package/dist/credential-guard/passive.test.d.ts +2 -0
  19. package/dist/credential-guard/passive.test.d.ts.map +1 -0
  20. package/dist/credential-guard/paths.d.ts +14 -0
  21. package/dist/credential-guard/paths.d.ts.map +1 -0
  22. package/dist/credential-guard/policy.d.ts +47 -0
  23. package/dist/credential-guard/policy.d.ts.map +1 -0
  24. package/dist/credential-guard/powershell.d.ts +3 -0
  25. package/dist/credential-guard/powershell.d.ts.map +1 -0
  26. package/dist/credential-guard/registry.d.ts +89 -0
  27. package/dist/credential-guard/registry.d.ts.map +1 -0
  28. package/dist/credential-guard/review4.test.d.ts +2 -0
  29. package/dist/credential-guard/review4.test.d.ts.map +1 -0
  30. package/dist/credential-guard/review7.test.d.ts +2 -0
  31. package/dist/credential-guard/review7.test.d.ts.map +1 -0
  32. package/dist/credential-guard/shell-oracle.d.ts +2 -0
  33. package/dist/credential-guard/shell-oracle.d.ts.map +1 -0
  34. package/dist/credential-guard/xargs.d.ts +7 -0
  35. package/dist/credential-guard/xargs.d.ts.map +1 -0
  36. package/dist/db-tool/service.d.ts +2 -1
  37. package/dist/db-tool/service.d.ts.map +1 -1
  38. package/dist/db-tool/sql-client.d.ts +1 -0
  39. package/dist/db-tool/sql-client.d.ts.map +1 -1
  40. package/dist/gh-tool/pr/commands.d.ts.map +1 -1
  41. package/dist/gh-tool/read/api.d.ts +8 -0
  42. package/dist/gh-tool/read/api.d.ts.map +1 -1
  43. package/dist/gh-tool/read/cache.d.ts.map +1 -1
  44. package/dist/gh-tool/read/commands.d.ts +5 -0
  45. package/dist/gh-tool/read/commands.d.ts.map +1 -1
  46. package/dist/gh-tool/read/reads.d.ts +38 -0
  47. package/dist/gh-tool/read/reads.d.ts.map +1 -1
  48. package/dist/gh-tool/read/telemetry-command.d.ts +7 -0
  49. package/dist/gh-tool/read/telemetry-command.d.ts.map +1 -0
  50. package/dist/gh-tool/read/telemetry-store.d.ts +3 -0
  51. package/dist/gh-tool/read/telemetry-store.d.ts.map +1 -0
  52. package/dist/gh-tool/read/telemetry.d.ts +59 -0
  53. package/dist/gh-tool/read/telemetry.d.ts.map +1 -0
  54. package/dist/gh-tool/read/transport.d.ts.map +1 -1
  55. package/dist/observability-tool/logs.d.ts +1 -1
  56. package/dist/observability-tool/logs.d.ts.map +1 -1
  57. package/dist/observability-tool/metrics.d.ts +1 -1
  58. package/dist/observability-tool/metrics.d.ts.map +1 -1
  59. package/dist/observability-tool/shared.d.ts +2 -1
  60. package/dist/observability-tool/shared.d.ts.map +1 -1
  61. package/dist/observability-tool/trace.d.ts +1 -1
  62. package/dist/observability-tool/trace.d.ts.map +1 -1
  63. package/dist/shared/content-security.d.ts +3 -1
  64. package/dist/shared/content-security.d.ts.map +1 -1
  65. package/dist/shared/secret-env.d.ts +22 -0
  66. package/dist/shared/secret-env.d.ts.map +1 -0
  67. package/package.json +2 -1
  68. package/schemas/agent-tools.schema.json +1 -1
  69. package/src/credential-guard/bash.ts +769 -0
  70. package/src/credential-guard/claude-hook.ts +14 -14
  71. package/src/credential-guard/engine.test.ts +2050 -0
  72. package/src/credential-guard/index.ts +285 -4268
  73. package/src/credential-guard/ir.ts +139 -0
  74. package/src/credential-guard/literal-output.ts +8 -0
  75. package/src/credential-guard/passive-awk.ts +239 -0
  76. package/src/credential-guard/passive-jq.ts +183 -24
  77. package/src/credential-guard/passive-sed.ts +78 -0
  78. package/src/credential-guard/passive.test.ts +141 -0
  79. package/src/credential-guard/paths.ts +108 -0
  80. package/src/credential-guard/policy.ts +1576 -0
  81. package/src/credential-guard/powershell.ts +220 -0
  82. package/src/credential-guard/registry.ts +4720 -0
  83. package/src/credential-guard/review4.test.ts +83 -0
  84. package/src/credential-guard/review7.test.ts +51 -0
  85. package/src/credential-guard/rg-types.json +389 -0
  86. package/src/credential-guard/shell-oracle.ts +187 -0
  87. package/src/credential-guard/xargs.ts +65 -0
  88. package/src/db-tool/main.ts +2 -0
  89. package/src/db-tool/service.ts +122 -140
  90. package/src/db-tool/sql-client.ts +7 -4
  91. package/src/gh-tool/main.ts +30 -6
  92. package/src/gh-tool/pr/commands.ts +9 -2
  93. package/src/gh-tool/pr/core.ts +2 -2
  94. package/src/gh-tool/read/api.ts +73 -6
  95. package/src/gh-tool/read/cache.ts +13 -4
  96. package/src/gh-tool/read/commands.ts +17 -1
  97. package/src/gh-tool/read/reads.ts +84 -1
  98. package/src/gh-tool/read/telemetry-command.ts +45 -0
  99. package/src/gh-tool/read/telemetry-store.ts +85 -0
  100. package/src/gh-tool/read/telemetry.ts +191 -0
  101. package/src/gh-tool/read/transport.ts +10 -5
  102. package/src/gh-tool/workflow.ts +2 -2
  103. package/src/observability-tool/main.ts +7 -1
  104. package/src/observability-tool/shared.ts +20 -14
  105. package/src/shared/content-security.ts +7 -2
  106. package/src/shared/secret-env.ts +230 -0
@@ -0,0 +1,139 @@
1
+ export type Span = { readonly start: number; readonly end: number };
2
+ export type Script = {
3
+ readonly kind: "script";
4
+ readonly dialect: "bash" | "powershell";
5
+ readonly source: string;
6
+ readonly body: List;
7
+ readonly span: Span;
8
+ };
9
+ export type List = { readonly items: readonly AndOr[] };
10
+ export type AndOr = {
11
+ readonly first: Pipeline;
12
+ readonly background: boolean;
13
+ readonly rest: readonly { readonly op: "&&" | "||"; readonly pipeline: Pipeline }[];
14
+ };
15
+ export type Pipeline = {
16
+ readonly negated: boolean;
17
+ readonly stderr: boolean;
18
+ readonly commands: readonly Command[];
19
+ };
20
+ export type Command =
21
+ | SimpleCommand
22
+ | {
23
+ readonly kind: "group";
24
+ readonly subshell: boolean;
25
+ readonly body: List;
26
+ readonly redirects: readonly Redirect[];
27
+ readonly span: Span;
28
+ }
29
+ | {
30
+ readonly kind: "for";
31
+ readonly name: string;
32
+ readonly items: readonly Word[];
33
+ readonly body: List;
34
+ readonly redirects: readonly Redirect[];
35
+ readonly span: Span;
36
+ }
37
+ | {
38
+ readonly kind: "while";
39
+ readonly condition: List;
40
+ readonly body: List;
41
+ readonly redirects: readonly Redirect[];
42
+ readonly span: Span;
43
+ }
44
+ | {
45
+ readonly kind: "until";
46
+ readonly condition: List;
47
+ readonly body: List;
48
+ readonly redirects: readonly Redirect[];
49
+ readonly span: Span;
50
+ }
51
+ | {
52
+ readonly kind: "if";
53
+ readonly branches: readonly { readonly condition: List; readonly body: List }[];
54
+ readonly otherwise: List | null;
55
+ readonly redirects: readonly Redirect[];
56
+ readonly span: Span;
57
+ }
58
+ | {
59
+ readonly kind: "case";
60
+ readonly subject: Word;
61
+ readonly arms: readonly { readonly patterns: readonly Word[]; readonly body: List }[];
62
+ readonly redirects: readonly Redirect[];
63
+ readonly span: Span;
64
+ }
65
+ | { readonly kind: "test"; readonly words: readonly Word[]; readonly span: Span };
66
+ export type SimpleCommand = {
67
+ readonly kind: "simple";
68
+ readonly assignments: readonly Assignment[];
69
+ readonly argv: readonly Word[];
70
+ readonly redirects: readonly Redirect[];
71
+ readonly span: Span;
72
+ };
73
+ export type Assignment = {
74
+ readonly operation: "replace" | "opaque";
75
+ readonly name: string;
76
+ readonly value: Word;
77
+ readonly exported: boolean;
78
+ };
79
+ export type Word = {
80
+ readonly split: boolean;
81
+ readonly parts: readonly WordPart[];
82
+ readonly span: Span;
83
+ };
84
+ export type WordPart =
85
+ | { readonly kind: "literal"; readonly text: string; readonly quoted: boolean }
86
+ | { readonly kind: "glob"; readonly text: string }
87
+ | { readonly kind: "brace"; readonly alternatives: readonly string[] }
88
+ | {
89
+ readonly kind: "parameter";
90
+ readonly name: string;
91
+ readonly ambient: boolean;
92
+ readonly operator: "plain" | "default" | "alternate" | "length";
93
+ readonly operand: Word | null;
94
+ readonly span: Span;
95
+ }
96
+ | { readonly kind: "special"; readonly name: string }
97
+ | { readonly kind: "command-substitution"; readonly body: Script }
98
+ | { readonly kind: "process-substitution"; readonly body: Script };
99
+ export type Redirect = (
100
+ | {
101
+ readonly kind: "file";
102
+ readonly op: "<" | ">" | ">>" | "<>" | ">|" | "&>" | "&>>";
103
+ readonly target: Word;
104
+ }
105
+ | { readonly kind: "duplicate"; readonly input: boolean; readonly target: string }
106
+ | { readonly kind: "heredoc"; readonly body: Word; readonly literal: boolean }
107
+ | { readonly kind: "herestring"; readonly body: Word }
108
+ ) & { readonly fd: number | null };
109
+ export type ParseErrorCode =
110
+ | "syntax"
111
+ | "unterminated-quote"
112
+ | "unterminated-heredoc"
113
+ | "unterminated-substitution"
114
+ | "function-definition"
115
+ | "unsupported-keyword"
116
+ | "indirect-expansion"
117
+ | "unsupported-parameter-operator"
118
+ | "depth-limit"
119
+ | "size-limit"
120
+ | "work-limit";
121
+ export type ParseError = {
122
+ readonly kind: "parse-error";
123
+ readonly code: ParseErrorCode;
124
+ readonly span: Span;
125
+ };
126
+ export const MAX_ANALYSIS_UNITS = 4096;
127
+ export const MAX_PARSE_DEPTH = 8;
128
+ export const MAX_SCRIPT_BYTES = 262144;
129
+ export function literalWord(text: string, span: Span = { start: 0, end: 0 }): Word {
130
+ return { split: false, parts: [{ kind: "literal", text, quoted: true }], span };
131
+ }
132
+ export function wordText(word: Word): string | undefined {
133
+ let text = "";
134
+ for (const part of word.parts) {
135
+ if (part.kind !== "literal") return undefined;
136
+ text += part.text;
137
+ }
138
+ return text;
139
+ }
@@ -0,0 +1,8 @@
1
+ export function formatLiteralPrintf(argv: readonly string[]): string | null {
2
+ const start = argv[0] === "--" ? 1 : 0;
3
+ const format = argv[start];
4
+ if (format !== "%s" && format !== "%s\\n" && format !== "%s ") return null;
5
+ const suffix = format === "%s\\n" ? "\n" : format === "%s " ? " " : "";
6
+ const args = argv.slice(start + 1);
7
+ return (args.length ? args : [""]).map((word) => word + suffix).join("");
8
+ }
@@ -0,0 +1,239 @@
1
+ export function isPassiveAwkProgram(text: string): boolean {
2
+ if (text.length > 65_536) return false;
3
+ const tokens: string[] = [];
4
+ const reserved = new Set([
5
+ "BEGIN",
6
+ "END",
7
+ "if",
8
+ "else",
9
+ "for",
10
+ "while",
11
+ "do",
12
+ "in",
13
+ "print",
14
+ "printf",
15
+ "next",
16
+ "nextfile",
17
+ "exit",
18
+ "break",
19
+ "continue",
20
+ "delete",
21
+ "function",
22
+ "getline",
23
+ "system",
24
+ "close",
25
+ "fflush",
26
+ ]);
27
+ const variables = new Set([
28
+ "NR",
29
+ "FNR",
30
+ "NF",
31
+ "FILENAME",
32
+ "RSTART",
33
+ "RLENGTH",
34
+ "SUBSEP",
35
+ "FS",
36
+ "RS",
37
+ "OFS",
38
+ "ORS",
39
+ "OFMT",
40
+ "CONVFMT",
41
+ ]);
42
+ const functions = new Set([
43
+ "length",
44
+ "substr",
45
+ "index",
46
+ "int",
47
+ "tolower",
48
+ "toupper",
49
+ "split",
50
+ "match",
51
+ "sub",
52
+ "gsub",
53
+ "sprintf",
54
+ "sqrt",
55
+ "log",
56
+ "exp",
57
+ ]);
58
+ const local = (token: string): boolean =>
59
+ /^[a-z_][A-Za-z0-9_]*$/.test(token) && !reserved.has(token) && !functions.has(token);
60
+ const constant = (token: string): boolean =>
61
+ /^["0-9]/.test(token) || (token.startsWith("/") && token.length > 1);
62
+ const lexer =
63
+ /[ \t\r]+|\n|"(?:[^"\\\n]|\\[^\n])*"|[0-9]+(?:\.[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|&&|\|\||==|!=|<=|>=|!~|\+\+|--|[+*%/-]=|[${}[\](),;!~+*%<>?:=-]/y;
64
+ let offset = 0;
65
+ let parentheses = 0;
66
+ while (offset < text.length) {
67
+ if (text[offset] === "/") {
68
+ const previous = tokens.at(-1) ?? "";
69
+ const division =
70
+ constant(previous) ||
71
+ variables.has(previous) ||
72
+ functions.has(previous) ||
73
+ local(previous) ||
74
+ [")", "]", "++", "--"].includes(previous);
75
+ if (division) {
76
+ tokens.push(text[offset + 1] === "=" ? "/=" : "/");
77
+ offset += text[offset + 1] === "=" ? 2 : 1;
78
+ } else {
79
+ const start = offset++;
80
+ let closed = false;
81
+ while (offset < text.length) {
82
+ const ch = text[offset++];
83
+ if (ch === "\\") {
84
+ if (offset === text.length || text[offset] === "\n") return false;
85
+ offset++;
86
+ } else if (ch === "\n") return false;
87
+ else if (ch === "/") {
88
+ closed = true;
89
+ break;
90
+ }
91
+ }
92
+ if (!closed) return false;
93
+ tokens.push(text.slice(start, offset));
94
+ }
95
+ } else {
96
+ lexer.lastIndex = offset;
97
+ const match = lexer.exec(text);
98
+ if (!match) return false;
99
+ offset = lexer.lastIndex;
100
+ const token = match[0];
101
+ if (token === "(") parentheses++;
102
+ if (token === ")") parentheses--;
103
+ if (token === "\n") {
104
+ if (parentheses === 0) tokens.push(";");
105
+ } else if (!/^\s/.test(token)) tokens.push(token);
106
+ }
107
+ if (tokens.length > 4096) return false;
108
+ }
109
+ let position = 0;
110
+ const take = (token: string): boolean => {
111
+ if (tokens[position] !== token) return false;
112
+ position++;
113
+ return true;
114
+ };
115
+ const operators = new Set([
116
+ "&&",
117
+ "||",
118
+ "==",
119
+ "!=",
120
+ "<",
121
+ "<=",
122
+ ">",
123
+ ">=",
124
+ "~",
125
+ "!~",
126
+ "+",
127
+ "-",
128
+ "*",
129
+ "/",
130
+ "%",
131
+ "in",
132
+ "=",
133
+ "+=",
134
+ "-=",
135
+ "*=",
136
+ "/=",
137
+ "%=",
138
+ ]);
139
+ const startsAtom = (): boolean => {
140
+ const token = tokens[position] ?? "";
141
+ return (
142
+ constant(token) ||
143
+ ["$", "("].includes(token) ||
144
+ variables.has(token) ||
145
+ local(token) ||
146
+ functions.has(token)
147
+ );
148
+ };
149
+ const atom = (depth: number): boolean => {
150
+ if (depth > 64) return false;
151
+ if (take("!") || take("-") || take("+") || take("++") || take("--")) return atom(depth + 1);
152
+ if (take("$")) return atom(depth + 1);
153
+ if (take("(")) return expression(depth + 1) && take(")");
154
+ const token = tokens[position] ?? "";
155
+ if (constant(token)) {
156
+ position++;
157
+ return true;
158
+ }
159
+ if (variables.has(token) || local(token)) {
160
+ position++;
161
+ if (tokens[position] === "(") return false;
162
+ if (take("[")) {
163
+ if (!expression(depth + 1)) return false;
164
+ while (take(",")) if (!expression(depth + 1)) return false;
165
+ if (!take("]")) return false;
166
+ }
167
+ if (!take("++")) take("--");
168
+ return true;
169
+ }
170
+ if (!functions.has(token)) return false;
171
+ position++;
172
+ if (!take("(")) return token === "length";
173
+ if (take(")")) return token === "length";
174
+ if (!expression(depth + 1)) return false;
175
+ while (take(",")) if (!expression(depth + 1)) return false;
176
+ return take(")");
177
+ };
178
+ const expression = (depth: number, printing = false): boolean => {
179
+ if (!atom(depth)) return false;
180
+ while (true) {
181
+ const operator = tokens[position] ?? "";
182
+ if (operators.has(operator)) {
183
+ if (printing && operator === ">") return false;
184
+ position++;
185
+ if (!atom(depth)) return false;
186
+ } else if (take("?")) {
187
+ return expression(depth + 1, printing) && take(":") && expression(depth + 1, printing);
188
+ } else if (startsAtom()) {
189
+ if (!atom(depth)) return false;
190
+ } else return true;
191
+ }
192
+ };
193
+ const block = (depth: number): boolean => {
194
+ if (depth > 64 || !take("{")) return false;
195
+ while (!take("}")) {
196
+ if (take(";")) continue;
197
+ if (!statement(depth + 1)) return false;
198
+ }
199
+ return true;
200
+ };
201
+ const statement = (depth: number): boolean => {
202
+ if (depth > 64) return false;
203
+ if (tokens[position] === "{") return block(depth + 1);
204
+ if (take("if")) {
205
+ if (!take("(") || !expression(depth + 1) || !take(")") || !statement(depth + 1)) return false;
206
+ return !take("else") || statement(depth + 1);
207
+ }
208
+ if (take("for")) {
209
+ if (!take("(") || !expression(depth + 1)) return false;
210
+ if (take(";")) {
211
+ if (!expression(depth + 1) || !take(";") || !expression(depth + 1)) return false;
212
+ }
213
+ return take(")") && statement(depth + 1);
214
+ }
215
+ if (take("print") || take("printf")) {
216
+ if (![";", "}"].includes(tokens[position] ?? "")) {
217
+ if (!expression(depth + 1, true)) return false;
218
+ while (take(",")) if (!expression(depth + 1, true)) return false;
219
+ }
220
+ } else if (take("exit")) {
221
+ if (![";", "}"].includes(tokens[position] ?? "") && !expression(depth + 1)) return false;
222
+ } else if (!take("next") && !take("break") && !take("continue") && !expression(depth + 1))
223
+ return false;
224
+ return take(";") || tokens[position] === "}" || tokens[position] === "else";
225
+ };
226
+ let rules = 0;
227
+ while (position < tokens.length) {
228
+ if (take(";")) continue;
229
+ if (tokens[position] !== "{") {
230
+ if (!take("BEGIN") && !take("END") && !expression(0)) return false;
231
+ if (take(",") && !expression(0)) return false;
232
+ }
233
+ rules++;
234
+ if (tokens[position] === "{") {
235
+ if (!block(0)) return false;
236
+ } else if (position < tokens.length && !take(";")) return false;
237
+ }
238
+ return rules > 0;
239
+ }
@@ -1,12 +1,30 @@
1
1
  /** Closed admission grammar, not an evaluator. Unknown syntax never acquires a proof. */
2
2
  export function isPassiveJqFilter(filter: string): boolean {
3
+ return admitFilter(filter, 0, new Set());
4
+ }
5
+
6
+ function admitFilter(
7
+ filter: string,
8
+ interpolationDepth: number,
9
+ inherited: ReadonlySet<string>,
10
+ ): boolean {
11
+ const bindings = new Set(inherited);
12
+ if (interpolationDepth > 32) return false;
3
13
  if (filter.length > 65_536) return false;
4
14
  if (new TextEncoder().encode(filter).byteLength > 65_536) return false;
5
15
  const tokens: string[] = [];
6
16
  const lexer =
7
- /\s+|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[A-Za-z_][A-Za-z0-9_]*|\/\/|==|!=|<=|>=|[.{}[\]():,?|+*/%<>-]/y; // eslint-disable-line eslint/no-control-regex -- JSON strings exclude literal control bytes.
17
+ /\s+|"(?:[^"\\\x00-\x1f]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4}))*"|(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|[$@]?[A-Za-z_][A-Za-z0-9_]*|\.[A-Za-z_][A-Za-z0-9_]*|\.\.|\/\/=|[|+*/%-]=|\/\/|==|!=|<=|>=|=|[.{}[\]():,;?|+*/%<>-]/y; // eslint-disable-line eslint/no-control-regex -- JSON strings exclude literal control bytes.
8
18
  let offset = 0;
9
19
  while (offset < filter.length) {
20
+ if (filter[offset] === '"') {
21
+ const quoted = readString(filter, offset, 0);
22
+ if (!quoted) return false;
23
+ tokens.push(filter.slice(offset, quoted.end));
24
+ if (tokens.length > 4096) return false;
25
+ offset = quoted.end;
26
+ continue;
27
+ }
10
28
  lexer.lastIndex = offset;
11
29
  const match = lexer.exec(filter);
12
30
  if (!match) return false;
@@ -22,7 +40,18 @@ export function isPassiveJqFilter(filter: string): boolean {
22
40
  };
23
41
  const identifier = (token: string | undefined): boolean =>
24
42
  /^[A-Za-z_][A-Za-z0-9_]*$/.test(token ?? "");
25
- const string = (token: string | undefined): boolean => token?.startsWith('"') === true;
43
+ const string = (token: string | undefined): boolean => {
44
+ if (!token?.startsWith('"')) return false;
45
+ const quoted = readString(token, 0, 0);
46
+ return (
47
+ quoted !== undefined &&
48
+ quoted.interpolations.every(([start, end]) =>
49
+ admitFilter(token.slice(start, end), interpolationDepth + 1, bindings),
50
+ )
51
+ );
52
+ };
53
+ const variable = (token: string | undefined): token is string =>
54
+ /^\$[A-Za-z_][A-Za-z0-9_]*$/.test(token ?? "") && token !== "$ENV";
26
55
  const number = (token: string | undefined): boolean => /^[0-9]/.test(token ?? "");
27
56
  const noArgs = new Set([
28
57
  "length",
@@ -30,6 +59,16 @@ export function isPassiveJqFilter(filter: string): boolean {
30
59
  "keys_unsorted",
31
60
  "type",
32
61
  "values",
62
+ "objects",
63
+ "arrays",
64
+ "strings",
65
+ "numbers",
66
+ "booleans",
67
+ "nulls",
68
+ "scalars",
69
+ "iterables",
70
+ "first",
71
+ "last",
33
72
  "empty",
34
73
  "add",
35
74
  "min",
@@ -41,6 +80,26 @@ export function isPassiveJqFilter(filter: string): boolean {
41
80
  "reverse",
42
81
  "to_entries",
43
82
  "from_entries",
83
+ "tostring",
84
+ "tonumber",
85
+ "tojson",
86
+ "fromjson",
87
+ "ascii_downcase",
88
+ "ascii_upcase",
89
+ "floor",
90
+ "ceil",
91
+ "round",
92
+ "abs",
93
+ "utf8bytelength",
94
+ "@text",
95
+ "@json",
96
+ "@html",
97
+ "@uri",
98
+ "@csv",
99
+ "@tsv",
100
+ "@sh",
101
+ "@base64",
102
+ "@base64d",
44
103
  ]);
45
104
  const oneArg = new Set([
46
105
  "map",
@@ -54,8 +113,37 @@ export function isPassiveJqFilter(filter: string): boolean {
54
113
  "any",
55
114
  "all",
56
115
  "join",
116
+ "split",
117
+ "startswith",
118
+ "endswith",
119
+ "ltrimstr",
120
+ "rtrimstr",
121
+ "test",
122
+ "index",
123
+ "rindex",
124
+ "indices",
125
+ "min_by",
126
+ "max_by",
127
+ "del",
128
+ "with_entries",
129
+ "getpath",
130
+ ]);
131
+ const multipleArgs = new Map<string, readonly number[]>([
132
+ ["test", [1, 2]],
133
+ ["match", [1, 2]],
134
+ ["capture", [1, 2]],
135
+ ["sub", [2, 3]],
136
+ ["gsub", [2, 3]],
57
137
  ]);
58
138
  const operators = new Set([
139
+ "=",
140
+ "|=",
141
+ "+=",
142
+ "-=",
143
+ "*=",
144
+ "/=",
145
+ "%=",
146
+ "//=",
59
147
  "|",
60
148
  "//",
61
149
  "==",
@@ -74,18 +162,32 @@ export function isPassiveJqFilter(filter: string): boolean {
74
162
  ]);
75
163
  const expression = (depth: number, commas = false): boolean => {
76
164
  if (depth > 64 || !term(depth)) return false;
77
- const nextOperator = () =>
78
- operators.has(tokens[position] ?? "") || (commas && tokens[position] === ",");
79
- while (nextOperator()) {
80
- position++;
165
+ while (true) {
166
+ if (take("as")) {
167
+ const name = tokens[position++];
168
+ if (!variable(name) || !take("|")) return false;
169
+ bindings.add(name);
170
+ } else if (operators.has(tokens[position] ?? "") || (commas && tokens[position] === ",")) {
171
+ position++;
172
+ } else break;
81
173
  if (!term(depth)) return false;
82
174
  }
83
175
  return true;
84
176
  };
177
+ const field = (): boolean => /^\.[A-Za-z_]/.test(tokens[position] ?? "");
85
178
  const term = (depth: number): boolean => {
86
179
  if (take("-")) return number(tokens[position]) && (position++, true);
87
- if (take(".")) {
88
- if (identifier(tokens[position]) || string(tokens[position])) position++;
180
+ if (take("if")) {
181
+ do {
182
+ if (!expression(depth + 1) || !take("then") || !expression(depth + 1)) return false;
183
+ } while (take("elif"));
184
+ if (!take("else") || !expression(depth + 1) || !take("end")) return false;
185
+ } else if (field()) {
186
+ position++;
187
+ } else if (take("..")) {
188
+ // Recursive descent only selects values from the current input.
189
+ } else if (take(".")) {
190
+ if (string(tokens[position])) position++;
89
191
  } else if (take("(")) {
90
192
  if (!expression(depth + 1, true) || !take(")")) return false;
91
193
  } else if (take("[")) {
@@ -93,37 +195,56 @@ export function isPassiveJqFilter(filter: string): boolean {
93
195
  } else if (take("{")) {
94
196
  if (!take("}")) {
95
197
  do {
96
- const key = tokens[position];
97
- if (!identifier(key) && !string(key)) return false;
98
- position++;
99
- if (take(":")) {
100
- if (!expression(depth + 1)) return false;
101
- } else if (!identifier(key)) return false;
198
+ if (take("(")) {
199
+ if (!expression(depth + 1) || !take(")") || !take(":") || !expression(depth + 1))
200
+ return false;
201
+ } else {
202
+ const key = tokens[position];
203
+ if (!identifier(key) && !string(key)) return false;
204
+ position++;
205
+ if (take(":")) {
206
+ if (!expression(depth + 1)) return false;
207
+ } else if (!identifier(key)) return false;
208
+ }
102
209
  } while (take(","));
103
210
  if (!take("}")) return false;
104
211
  }
105
212
  } else {
106
213
  const token = tokens[position];
107
- if (string(token) || number(token) || ["true", "false", "null"].includes(token ?? ""))
214
+ if (
215
+ (variable(token) && bindings.has(token)) ||
216
+ string(token) ||
217
+ number(token) ||
218
+ ["true", "false", "null"].includes(token ?? "")
219
+ )
108
220
  position++;
109
- else if (noArgs.has(token ?? "")) position++;
110
- else if (oneArg.has(token ?? "")) {
221
+ else if (noArgs.has(token ?? "")) {
111
222
  position++;
112
- if (!take("(") || !expression(depth + 1) || !take(")")) return false;
223
+ if (token?.startsWith("@") && string(tokens[position])) position++;
224
+ } else if (oneArg.has(token ?? "") || multipleArgs.has(token ?? "")) {
225
+ position++;
226
+ if (!take("(") || !expression(depth + 1)) return false;
227
+ let count = 1;
228
+ while (take(";")) {
229
+ count++;
230
+ if (!expression(depth + 1)) return false;
231
+ }
232
+ if (!(multipleArgs.get(token ?? "") ?? [1]).includes(count) || !take(")")) return false;
113
233
  } else return false;
114
234
  }
115
235
  while (true) {
116
236
  if (take("?")) continue;
237
+ if (field()) {
238
+ position++;
239
+ continue;
240
+ }
117
241
  if (take(".")) {
118
- if (!identifier(tokens[position]) && !string(tokens[position])) return false;
242
+ if (!string(tokens[position])) return false;
119
243
  position++;
120
244
  } else if (take("[")) {
121
245
  if (!take("]")) {
122
- if (take("-")) {
123
- if (!number(tokens[position])) return false;
124
- position++;
125
- } else if (string(tokens[position]) || number(tokens[position])) position++;
126
- else return false;
246
+ if (tokens[position] !== ":" && !expression(depth + 1, true)) return false;
247
+ if (take(":") && tokens[position] !== "]" && !expression(depth + 1)) return false;
127
248
  if (!take("]")) return false;
128
249
  }
129
250
  } else break;
@@ -133,6 +254,44 @@ export function isPassiveJqFilter(filter: string): boolean {
133
254
  return expression(0, true) && position === tokens.length;
134
255
  }
135
256
 
257
+ function readString(
258
+ text: string,
259
+ start: number,
260
+ depth: number,
261
+ ): { end: number; interpolations: [number, number][] } | undefined {
262
+ if (depth > 32) return undefined;
263
+ const interpolations: [number, number][] = [];
264
+ let offset = start + 1;
265
+ while (offset < text.length) {
266
+ const character = text[offset++];
267
+ if (character === '"') return { end: offset, interpolations };
268
+ if (!character || character.charCodeAt(0) < 32) return undefined;
269
+ if (character !== "\\") continue;
270
+ const escape = text[offset++];
271
+ if (escape === "(") {
272
+ const expressionStart = offset;
273
+ let parentheses = 1;
274
+ while (offset < text.length && parentheses > 0) {
275
+ if (text[offset] === '"') {
276
+ const nested = readString(text, offset, depth + 1);
277
+ if (!nested) return undefined;
278
+ offset = nested.end;
279
+ } else {
280
+ const token = text[offset++];
281
+ if (token === "(") parentheses++;
282
+ if (token === ")") parentheses--;
283
+ }
284
+ }
285
+ if (parentheses !== 0) return undefined;
286
+ interpolations.push([expressionStart, offset - 1]);
287
+ } else if (escape === "u") {
288
+ if (!/^[0-9a-fA-F]{4}$/.test(text.slice(offset, offset + 4))) return undefined;
289
+ offset += 4;
290
+ } else if (!escape || !'"\\/bfnrt'.includes(escape)) return undefined;
291
+ }
292
+ return undefined;
293
+ }
294
+
136
295
  /** Only known formatting/input flags and one literal program, followed by file operands. */
137
296
  export function passiveJqProgramIndex(argv: string[]): number | undefined {
138
297
  if (argv[0]?.split("/").at(-1) !== "jq") return undefined;