@oked/sdk 0.1.6 → 0.1.7
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/classify.d.ts +8 -8
- package/dist/classify.js +80 -72
- package/dist/describe.js +31 -21
- package/package.json +1 -1
package/dist/classify.d.ts
CHANGED
|
@@ -8,14 +8,14 @@ export interface ShellWriteOp {
|
|
|
8
8
|
content?: string;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Removes heredoc *bodies*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* or
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* Removes heredoc *bodies* unless they're fed to an interpreter/DB/shell that
|
|
12
|
+
* executes them. The default is to strip: `cat >> file <<'EOF'`, `git commit -F
|
|
13
|
+
* - <<'MSG'`, `gh pr create --body "$(cat <<'BODY')"`, `mail <<'EOF'` and the
|
|
14
|
+
* like all treat the body as literal DATA, which must not be parsed as shell
|
|
15
|
+
* (a commit message with `->` or a PR body mentioning "TRUNCATE"/"rm -rf" would
|
|
16
|
+
* otherwise wreck classification). Only heredocs whose opener line invokes an
|
|
17
|
+
* interpreter (`psql <<EOF`, `node - <<EOF`, `cat <<EOF | bash`) keep their
|
|
18
|
+
* body, so SQL/code detection still runs. The opener line is always preserved.
|
|
19
19
|
*/
|
|
20
20
|
export declare function stripHeredocBodies(command: string): string;
|
|
21
21
|
export declare function extractShellWriteOps(command: string): ShellWriteOp[];
|
package/dist/classify.js
CHANGED
|
@@ -63,7 +63,8 @@ const SAFE_COMMANDS = [
|
|
|
63
63
|
/^node\s+(-v|--version)/,
|
|
64
64
|
/^npm\s+(list|ls|--version|-v|view|info|outdated|audit)\b/,
|
|
65
65
|
/^npx\s+-v/,
|
|
66
|
-
/^git\s+(status|log|diff|branch|remote|show|tag|stash
|
|
66
|
+
/^git\s+(status|log|diff|branch|remote|show|tag|stash\s+list|ls-files|ls-tree|ls-remote|check-ignore|check-attr|rev-parse|rev-list|describe|cat-file|blame|shortlog|reflog|name-rev|whatchanged|for-each-ref|symbolic-ref|merge-base|var|grep|count-objects|show-ref|cherry|verify-commit|fsck)\b/,
|
|
67
|
+
/^git\s+config\s+(--get|--get-all|--get-regexp|--list|-l)\b/,
|
|
67
68
|
/^docker\s+(ps|images|inspect|logs)\b/,
|
|
68
69
|
/^docker\s+compose\s+(ps|logs)\b/,
|
|
69
70
|
/^tree\b/,
|
|
@@ -178,56 +179,73 @@ const WARNING_COMMANDS = [
|
|
|
178
179
|
/^npm\s+run\b/,
|
|
179
180
|
/^bun\b/,
|
|
180
181
|
/^deno\b/,
|
|
182
|
+
// Package installs run dependency postinstall scripts (code execution), so
|
|
183
|
+
// they're not "safe" — but they're a constant part of the dev loop, so log
|
|
184
|
+
// (warning) rather than prompt.
|
|
185
|
+
/^npm\s+(install|ci|i|update|upgrade|rebuild|prune|dedupe)\b/,
|
|
186
|
+
/^(pnpm|yarn)\s+(install|add|ci|up|upgrade)\b/,
|
|
181
187
|
];
|
|
182
188
|
// Ephemeral filesystem locations. Writes here have no lasting effect on
|
|
183
189
|
// their own — what matters is whatever subsequent command CONSUMES the file
|
|
184
190
|
// (e.g. `himalaya message send < /tmp/draft.eml`). Without this carve-out,
|
|
185
191
|
// every multi-step skill that drafts a temp file generates two approval
|
|
186
192
|
// prompts (the temp write + the real send) instead of one.
|
|
187
|
-
const EPHEMERAL_PATH_RE = /^(?:\/tmp\/|\/var\/tmp\/|\/private\/tmp\/|[A-Za-z]:[\\/](?:Windows[\\/]Temp|Users[\\/][^\\/]+[\\/]AppData[\\/]Local[\\/]Temp)[\\/])/i;
|
|
193
|
+
const EPHEMERAL_PATH_RE = /^(?:\/tmp\/|\/var\/tmp\/|\/var\/folders\/|\/private\/tmp\/|\/private\/var\/folders\/|[A-Za-z]:[\\/](?:Windows[\\/]Temp|Users[\\/][^\\/]+[\\/]AppData[\\/]Local[\\/]Temp)[\\/])/i;
|
|
194
|
+
// A temp-dir env var (the conventional output of `mktemp -d` etc.): $TMPDIR,
|
|
195
|
+
// $TMP, $TEMP, ${TMPDIR}, and paths beneath them. Treated as ephemeral since
|
|
196
|
+
// we can't resolve the value but the intent is unambiguous.
|
|
197
|
+
const TEMP_VAR_RE = /^\$\{?(?:TMPDIR|TMP|TEMP)\}?(?:\/|$)/;
|
|
188
198
|
function isEphemeralPath(filePath) {
|
|
189
199
|
if (!filePath)
|
|
190
200
|
return false;
|
|
191
|
-
return EPHEMERAL_PATH_RE.test(filePath);
|
|
201
|
+
return TEMP_VAR_RE.test(filePath) || EPHEMERAL_PATH_RE.test(filePath);
|
|
192
202
|
}
|
|
193
|
-
//
|
|
194
|
-
//
|
|
195
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
path.join(".claude", "plans"),
|
|
201
|
-
path.join(".claude", "todos"),
|
|
202
|
-
];
|
|
203
|
-
function isAgentScratchPath(filePath) {
|
|
203
|
+
// Paths where a write/edit is genuinely dangerous and must stay `review`:
|
|
204
|
+
// system directories, credential/secret stores, shell startup files (a
|
|
205
|
+
// persistence vector), and OKed's own config (so an agent can't disable its
|
|
206
|
+
// guardrails). Everything else — project files, sibling repos, scratch — is
|
|
207
|
+
// treated as `warning` (a file write can't act on its own; whatever later
|
|
208
|
+
// executes it is classified separately).
|
|
209
|
+
function isSensitiveWritePath(filePath) {
|
|
204
210
|
if (!filePath)
|
|
205
|
-
return
|
|
211
|
+
return true; // unknown target → err toward review
|
|
212
|
+
let resolved;
|
|
206
213
|
try {
|
|
207
|
-
|
|
208
|
-
const home = os.homedir();
|
|
209
|
-
return AGENT_SCRATCH_DIRS.some((dir) => {
|
|
210
|
-
const base = path.join(home, dir);
|
|
211
|
-
const relative = path.relative(base, resolved);
|
|
212
|
-
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
213
|
-
});
|
|
214
|
+
resolved = path.resolve(filePath);
|
|
214
215
|
}
|
|
215
216
|
catch {
|
|
216
|
-
return
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
function isInsideProject(filePath) {
|
|
220
|
-
if (!filePath)
|
|
221
|
-
return false;
|
|
222
|
-
try {
|
|
223
|
-
const projectRoot = path.resolve(process.cwd());
|
|
224
|
-
const resolved = path.resolve(filePath);
|
|
225
|
-
const relative = path.relative(projectRoot, resolved);
|
|
226
|
-
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
|
217
|
+
return true;
|
|
227
218
|
}
|
|
228
|
-
|
|
229
|
-
|
|
219
|
+
const home = os.homedir();
|
|
220
|
+
const underHome = (rel) => {
|
|
221
|
+
const base = path.join(home, rel);
|
|
222
|
+
return resolved === base || resolved.startsWith(base + path.sep);
|
|
223
|
+
};
|
|
224
|
+
// OKed self-config — never let an agent edit its own hook config silently.
|
|
225
|
+
if (resolved === path.join(home, ".claude", "settings.json") ||
|
|
226
|
+
resolved === path.join(home, ".claude", "settings.local.json"))
|
|
227
|
+
return true;
|
|
228
|
+
// Credential / secret stores.
|
|
229
|
+
for (const d of [".ssh", ".aws", ".gnupg", ".kube", ".docker", path.join(".config", "gcloud")]) {
|
|
230
|
+
if (underHome(d))
|
|
231
|
+
return true;
|
|
230
232
|
}
|
|
233
|
+
// Sensitive dotfiles directly in $HOME (creds + shell startup persistence).
|
|
234
|
+
const sensitiveHomeFiles = new Set([
|
|
235
|
+
".netrc", ".npmrc", ".pypirc", ".git-credentials", ".bash_history", ".zsh_history",
|
|
236
|
+
".bashrc", ".zshrc", ".bash_profile", ".zprofile", ".profile", ".zshenv", ".zlogin",
|
|
237
|
+
]);
|
|
238
|
+
if (path.dirname(resolved) === home && sensitiveHomeFiles.has(path.basename(resolved)))
|
|
239
|
+
return true;
|
|
240
|
+
// System directories.
|
|
241
|
+
if (/^\/(etc|usr|bin|sbin|boot|sys|proc|opt|Library|System)(\/|$)/.test(resolved))
|
|
242
|
+
return true;
|
|
243
|
+
if (/^\/private\/etc(\/|$)/.test(resolved))
|
|
244
|
+
return true;
|
|
245
|
+
// /var, except the ephemeral temp subtrees.
|
|
246
|
+
if (/^\/var(\/|$)/.test(resolved) && !/^\/var\/(tmp|folders)(\/|$)/.test(resolved))
|
|
247
|
+
return true;
|
|
248
|
+
return false;
|
|
231
249
|
}
|
|
232
250
|
export function classify(toolName, toolInput) {
|
|
233
251
|
// Check tool-level classification first
|
|
@@ -239,14 +257,13 @@ export function classify(toolName, toolInput) {
|
|
|
239
257
|
return "high_stakes";
|
|
240
258
|
if (REVIEW_TOOLS.has(toolName))
|
|
241
259
|
return "review";
|
|
242
|
-
// File-editing tools:
|
|
243
|
-
//
|
|
244
|
-
//
|
|
260
|
+
// File-editing tools: a write/edit can't act on its own — whatever later
|
|
261
|
+
// executes it is classified separately — so it's `warning` (logged, no
|
|
262
|
+
// prompt) everywhere EXCEPT sensitive targets (system dirs, secret stores,
|
|
263
|
+
// shell startup files, OKed's own config), which stay `review`.
|
|
245
264
|
if (toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
|
|
246
265
|
const filePath = toolInput.file_path;
|
|
247
|
-
|
|
248
|
-
return "warning";
|
|
249
|
-
return "review";
|
|
266
|
+
return isSensitiveWritePath(filePath) ? "review" : "warning";
|
|
250
267
|
}
|
|
251
268
|
// Agent tool - safe. Launching a sub-agent is not itself a side effect, and
|
|
252
269
|
// the sub-agent's own tool calls (Bash/Write/Edit/MCP) each fire their own
|
|
@@ -276,9 +293,7 @@ export function classify(toolName, toolInput) {
|
|
|
276
293
|
const writePath = (toolInput.file_path ?? toolInput.path);
|
|
277
294
|
const writeContent = (toolInput.content ?? toolInput.data ?? toolInput.body);
|
|
278
295
|
if (typeof writePath === "string" && typeof writeContent === "string") {
|
|
279
|
-
|
|
280
|
-
return "warning";
|
|
281
|
-
return "review";
|
|
296
|
+
return isSensitiveWritePath(writePath) ? "review" : "warning";
|
|
282
297
|
}
|
|
283
298
|
// Unknown tool - default to review (require approval)
|
|
284
299
|
return "review";
|
|
@@ -406,16 +421,15 @@ function classifyBashCommand(command) {
|
|
|
406
421
|
return classifySqlSeverity(sql);
|
|
407
422
|
// File-mutating shell patterns. Content-creation idioms (echo > X, tee,
|
|
408
423
|
// dd of=, touch, sed -i) are the bypass route from a denied Write, so they're
|
|
409
|
-
// classified like the Write/Edit tool:
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
424
|
+
// classified exactly like the Write/Edit tool: `warning` unless a target is a
|
|
425
|
+
// sensitive path (system dir, secret store, shell rc, OKed config), which
|
|
426
|
+
// stays `review`. cp/mv (which can clobber/relocate existing files) stay
|
|
427
|
+
// review.
|
|
413
428
|
const ops = extractShellWriteOps(trimmed);
|
|
414
429
|
if (ops.length > 0) {
|
|
415
430
|
const creates = ops.filter((o) => o.kind !== "copy" && o.kind !== "move");
|
|
416
431
|
if (creates.length > 0) {
|
|
417
|
-
|
|
418
|
-
return allLocal ? "warning" : "review";
|
|
432
|
+
return creates.some((o) => isSensitiveWritePath(o.target)) ? "review" : "warning";
|
|
419
433
|
}
|
|
420
434
|
return "review";
|
|
421
435
|
}
|
|
@@ -461,29 +475,23 @@ function classifySqlSeverity(sql) {
|
|
|
461
475
|
*
|
|
462
476
|
* Skips /dev/null and bare-digit FD duplicates (2>&1).
|
|
463
477
|
*/
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
for (const m of line.matchAll(/(?:^|[^>])([12]?>>?|&>>?)\s*([^\s>|&;]+)/g)) {
|
|
472
|
-
const target = unquote(m[2]);
|
|
473
|
-
if (target && !/^\d+$/.test(target) && !isDevNullish(target))
|
|
474
|
-
return true;
|
|
475
|
-
}
|
|
476
|
-
return false;
|
|
478
|
+
// Commands that EXECUTE a heredoc body fed to their stdin — a SQL CLI, a code
|
|
479
|
+
// interpreter, or a shell. Their heredoc bodies are code/SQL and must stay
|
|
480
|
+
// scannable. Detected as a command word at the start of the opener line or
|
|
481
|
+
// after a pipe / `&&` / `;` / `$(` / backtick.
|
|
482
|
+
const HEREDOC_INTERPRETER_RE = /(?:^|\||&&|;|\$\(|`)\s*(?:\w+=\S+\s+)*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlite3?|node|python\d?|ruby|perl|deno|bun|bash|sh|zsh|ksh|fish)\b/;
|
|
483
|
+
function openerFeedsInterpreter(line) {
|
|
484
|
+
return HEREDOC_INTERPRETER_RE.test(line);
|
|
477
485
|
}
|
|
478
486
|
/**
|
|
479
|
-
* Removes heredoc *bodies*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
* or
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
487
|
+
* Removes heredoc *bodies* unless they're fed to an interpreter/DB/shell that
|
|
488
|
+
* executes them. The default is to strip: `cat >> file <<'EOF'`, `git commit -F
|
|
489
|
+
* - <<'MSG'`, `gh pr create --body "$(cat <<'BODY')"`, `mail <<'EOF'` and the
|
|
490
|
+
* like all treat the body as literal DATA, which must not be parsed as shell
|
|
491
|
+
* (a commit message with `->` or a PR body mentioning "TRUNCATE"/"rm -rf" would
|
|
492
|
+
* otherwise wreck classification). Only heredocs whose opener line invokes an
|
|
493
|
+
* interpreter (`psql <<EOF`, `node - <<EOF`, `cat <<EOF | bash`) keep their
|
|
494
|
+
* body, so SQL/code detection still runs. The opener line is always preserved.
|
|
487
495
|
*/
|
|
488
496
|
export function stripHeredocBodies(command) {
|
|
489
497
|
const lines = command.split("\n");
|
|
@@ -496,7 +504,7 @@ export function stripHeredocBodies(command) {
|
|
|
496
504
|
// space). Require a word-char delimiter so numeric left-shifts ($((1<<2)))
|
|
497
505
|
// don't match. Use the last opener on the line as the active delimiter.
|
|
498
506
|
const openers = [...line.matchAll(/<<-?\s*(["']?)([A-Za-z_][A-Za-z0-9_]*)\1/g)];
|
|
499
|
-
if (openers.length > 0 &&
|
|
507
|
+
if (openers.length > 0 && !openerFeedsInterpreter(line)) {
|
|
500
508
|
const delim = openers[openers.length - 1][2];
|
|
501
509
|
i++;
|
|
502
510
|
while (i < lines.length && lines[i].trim() !== delim)
|
package/dist/describe.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* from the `fields` payload. `describe()` returns just the title for
|
|
8
8
|
* backwards-compatible single-line consumers (audit logs, SMS).
|
|
9
9
|
*/
|
|
10
|
-
import { extractShellWriteOps
|
|
10
|
+
import { extractShellWriteOps } from "./classify.js";
|
|
11
11
|
const BODY_PREVIEW_MAX = 200;
|
|
12
12
|
const COMMAND_INLINE_MAX = 50;
|
|
13
13
|
const DIFF_HUNK_MAX_LINES = 10;
|
|
@@ -84,9 +84,7 @@ function summarize(toolName, toolInput) {
|
|
|
84
84
|
// Bash / shell — semantic rerendering
|
|
85
85
|
// ───────────────────────────────────────────────────────────────────────
|
|
86
86
|
function summarizeBash(command, sizeBytes) {
|
|
87
|
-
|
|
88
|
-
// (and don't bloat the card) — the redirect/target on the opener line stays.
|
|
89
|
-
const cmd = stripHeredocBodies(command || "").trim();
|
|
87
|
+
const cmd = (command || "").trim();
|
|
90
88
|
if (!cmd)
|
|
91
89
|
return { title: "Run empty command", kind: "unknown_bash" };
|
|
92
90
|
// SQL detection runs before shell-write detection so that inline-interpreter
|
|
@@ -123,13 +121,17 @@ function summarizeBash(command, sizeBytes) {
|
|
|
123
121
|
};
|
|
124
122
|
}
|
|
125
123
|
// git
|
|
126
|
-
if (/\bgit\s+push\s+(?:--force|-f)\b/.test(cmd)) {
|
|
127
|
-
const m = cmd.match(/git\s+push\s+(?:--force|-f)\s+(\S+)\s+(\S+)/);
|
|
128
|
-
return { title: "Force push", target: m ? `${m[2]} → ${m[1]}` : "current branch", kind: "git_force_push" };
|
|
129
|
-
}
|
|
130
124
|
if (/\bgit\s+push\b/.test(cmd)) {
|
|
131
|
-
|
|
132
|
-
|
|
125
|
+
// Parse the remote + branch ignoring flags (-u, --force, --set-upstream, …)
|
|
126
|
+
// so `git push -u origin feat` renders "feat → origin", not "origin → -u".
|
|
127
|
+
const after = cmd.match(/\bgit\s+push\b(.*)$/s)?.[1] ?? "";
|
|
128
|
+
const args = after.split(/\s+/).filter((a) => a && !a.startsWith("-"));
|
|
129
|
+
const [remote, branch] = args;
|
|
130
|
+
const target = remote && branch ? `${branch} → ${remote}` : remote || "current branch";
|
|
131
|
+
const forced = /\bgit\s+push\b[^\n]*\s(?:--force(?:-with-lease)?|-f)\b/.test(cmd);
|
|
132
|
+
return forced
|
|
133
|
+
? { title: "Force push", target, kind: "git_force_push" }
|
|
134
|
+
: { title: "Push", target, kind: "git_push" };
|
|
133
135
|
}
|
|
134
136
|
if (/\bgit\s+reset\s+--hard\b/.test(cmd))
|
|
135
137
|
return { title: "Hard reset — discard all local changes", kind: "git_reset_hard" };
|
|
@@ -291,12 +293,16 @@ function extractSqlFromScriptBody(body) {
|
|
|
291
293
|
}
|
|
292
294
|
return sqls.length > 0 ? sqls.join("\n") : null;
|
|
293
295
|
}
|
|
296
|
+
// A SQL CLI or code interpreter as a command word — at the start of the command
|
|
297
|
+
// (allowing env=val / sudo prefixes) or after a pipe / `&&` / `;` / `$(`. Used
|
|
298
|
+
// to gate SQL extraction so SQL words appearing in *argument data* (a `gh pr
|
|
299
|
+
// create --body` mentioning "TRUNCATE", a path like `better-sqlite3`) don't get
|
|
300
|
+
// misread as a statement to run.
|
|
301
|
+
const SQL_CONSUMER_RE = /(?:^|\||&&|;|\$\(|`)\s*(?:\w+=\S+\s+)*(?:sudo\s+)?(?:psql|mysql|mariadb|sqlite3?|node|python\d?|ruby|perl|deno|bun)\b/;
|
|
294
302
|
export function findSqlInCommand(cmd) {
|
|
295
|
-
// Inline interpreter flags: node -e, python -c, ruby -e, perl -e.
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
// `require('better-sqlite3')`) and would extract the wrong fragment.
|
|
299
|
-
const inline = cmd.match(/\b(?:node|python\d?|ruby|perl|deno|bun)\s+-[ec]\s+(?:"([\s\S]+?)"|'([\s\S]+?)')\s*$/);
|
|
303
|
+
// Inline interpreter flags: node -e, python -c, ruby -e, perl -e. Anchored to
|
|
304
|
+
// a command position so a SQL-looking string elsewhere doesn't match.
|
|
305
|
+
const inline = cmd.match(/(?:^|\||&&|;|\$\()\s*(?:\w+=\S+\s+)*(?:sudo\s+)?(?:node|python\d?|ruby|perl|deno|bun)\s+-[ec]\s+(?:"([\s\S]+?)"|'([\s\S]+?)')\s*$/);
|
|
300
306
|
if (inline) {
|
|
301
307
|
const body = inline[1] ?? inline[2];
|
|
302
308
|
if (body && SQL_KEYWORDS_RE.test(body)) {
|
|
@@ -304,19 +310,23 @@ export function findSqlInCommand(cmd) {
|
|
|
304
310
|
}
|
|
305
311
|
}
|
|
306
312
|
// psql -c "..." / mysql -e "..." / sqlite3 db "..." — outer-quoted statement.
|
|
307
|
-
|
|
313
|
+
// Anchored to the start of the command so "psql"/"mysql" appearing inside an
|
|
314
|
+
// argument (another tool's --body, etc.) can't trigger a false SQL match.
|
|
315
|
+
const dq = cmd.match(/^(?:\s*\w+=\S+\s+)*(?:sudo\s+)?(?:psql|mysql|sqlite3?|mariadb)\b[^"]*"([\s\S]+?)"\s*$/i);
|
|
308
316
|
if (dq)
|
|
309
317
|
return dq[1];
|
|
310
|
-
const sq = cmd.match(
|
|
318
|
+
const sq = cmd.match(/^(?:\s*\w+=\S+\s+)*(?:sudo\s+)?(?:psql|mysql|sqlite3?|mariadb)\b[^']*'([\s\S]+?)'\s*$/i);
|
|
311
319
|
if (sq)
|
|
312
320
|
return sq[1];
|
|
313
|
-
// Heredoc-piped script: <<EOF / <<'EOF' / <<"EOF" / <<-EOF.
|
|
314
|
-
//
|
|
315
|
-
//
|
|
321
|
+
// Heredoc-piped script: <<EOF / <<'EOF' / <<"EOF" / <<-EOF. Only when the
|
|
322
|
+
// command (outside the body) actually feeds the heredoc to a SQL CLI or
|
|
323
|
+
// interpreter — otherwise a `gh`/`cat`/`mail` heredoc whose body merely
|
|
324
|
+
// mentions SQL words would be misclassified as a statement to run.
|
|
316
325
|
const hd = cmd.match(/<<-?\s*['"]?(\w+)['"]?[ \t]*\r?\n([\s\S]*?)\r?\n[ \t]*\1\b/);
|
|
317
326
|
if (hd) {
|
|
318
327
|
const body = hd[2];
|
|
319
|
-
|
|
328
|
+
const context = cmd.slice(0, hd.index) + cmd.slice((hd.index ?? 0) + hd[0].length);
|
|
329
|
+
if (body && SQL_CONSUMER_RE.test(context) && SQL_KEYWORDS_RE.test(body)) {
|
|
320
330
|
return extractSqlFromScriptBody(body) ?? body;
|
|
321
331
|
}
|
|
322
332
|
}
|