@animus-labs/cortex 0.2.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.
- package/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive command detection for Bash.
|
|
3
|
+
*
|
|
4
|
+
* Catches commands that would block waiting for TTY input (editors,
|
|
5
|
+
* pagers, REPLs, interactive DB clients) and rejects them with a
|
|
6
|
+
* concrete non-interactive suggestion. Prevents the agent from burning
|
|
7
|
+
* its entire timeout budget on a hung `vim` or `psql` invocation.
|
|
8
|
+
*
|
|
9
|
+
* This is a UX gate, not a security gate: it sits at the end of the
|
|
10
|
+
* safety cascade after all security layers have passed. Security checks
|
|
11
|
+
* always come first.
|
|
12
|
+
*
|
|
13
|
+
* The module is pure — no I/O, no global state — so the rule set can be
|
|
14
|
+
* exhaustively unit-tested without a shell.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { splitOnShellOperators, type SafetyCheckResult } from './safety.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Rule definition
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
interface InteractiveRule {
|
|
24
|
+
/** Exact basename of the program to match (e.g. 'vim', not '/usr/bin/vim'). */
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether the invocation is interactive. `args` is the token
|
|
28
|
+
* list AFTER the program (flags and positional args). Return a
|
|
29
|
+
* user-facing suggestion string when the invocation is interactive,
|
|
30
|
+
* or `null` when a non-interactive form has been detected.
|
|
31
|
+
*/
|
|
32
|
+
check: (args: string[]) => string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Rule groups
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
const EDITORS = ['vim', 'vi', 'nvim', 'emacs', 'emacsclient', 'nano', 'pico', 'ed', 'joe'];
|
|
40
|
+
const MONITORS = ['top', 'htop', 'atop', 'btop', 'watch'];
|
|
41
|
+
const PAGERS = ['less', 'more', 'most'];
|
|
42
|
+
|
|
43
|
+
function alwaysInteractive(name: string, suggestion: string): InteractiveRule {
|
|
44
|
+
return { name, check: () => suggestion };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const RULES: InteractiveRule[] = [
|
|
48
|
+
...EDITORS.map((name) =>
|
|
49
|
+
alwaysInteractive(
|
|
50
|
+
name,
|
|
51
|
+
`${name} is a terminal editor and will block waiting for input. Use the Edit or Write tools to modify files instead.`,
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
...MONITORS.map((name) =>
|
|
55
|
+
alwaysInteractive(
|
|
56
|
+
name,
|
|
57
|
+
`${name} runs continuously and blocks the shell. Use a one-shot alternative (e.g. \`ps aux | head\`, \`uptime\`, \`df -h\`).`,
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
// Pagers block even when piped — they paginate on keypress.
|
|
61
|
+
...PAGERS.map((name) =>
|
|
62
|
+
alwaysInteractive(
|
|
63
|
+
name,
|
|
64
|
+
`${name} paginates and will block waiting for a keypress. Use \`cat\`, \`head\`, or \`tail\` instead.`,
|
|
65
|
+
),
|
|
66
|
+
),
|
|
67
|
+
// Python: interactive unless given a script file or -c/-m.
|
|
68
|
+
...['python', 'python2', 'python3'].map((name): InteractiveRule => ({
|
|
69
|
+
name,
|
|
70
|
+
check: (args) => pythonCheck(name, args),
|
|
71
|
+
})),
|
|
72
|
+
{ name: 'node', check: nodeCheck },
|
|
73
|
+
{ name: 'ruby', check: rubyCheck },
|
|
74
|
+
alwaysInteractive(
|
|
75
|
+
'irb',
|
|
76
|
+
'irb opens an interactive Ruby shell. Use `ruby -e "..."` for one-off code.',
|
|
77
|
+
),
|
|
78
|
+
{ name: 'mongo', check: (args) => mongoCheck('mongo', args) },
|
|
79
|
+
{ name: 'mongosh', check: (args) => mongoCheck('mongosh', args) },
|
|
80
|
+
{ name: 'sqlite3', check: sqliteCheck },
|
|
81
|
+
{ name: 'psql', check: psqlCheck },
|
|
82
|
+
{ name: 'mysql', check: mysqlCheck },
|
|
83
|
+
{ name: 'mariadb', check: mysqlCheck },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Fast lookup by basename.
|
|
87
|
+
const RULES_BY_NAME = new Map(RULES.map((r) => [r.name, r]));
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Public entry point
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check a full shell command for interactive invocations. Splits on
|
|
95
|
+
* shell operators (`;`, `&&`, `||`, `|`) and inspects each sub-command
|
|
96
|
+
* independently — `cat file | less` is rejected because the `less`
|
|
97
|
+
* sub-command is interactive, even though `cat` is not.
|
|
98
|
+
*
|
|
99
|
+
* Returns the first interactive sub-command's rejection; if all
|
|
100
|
+
* sub-commands are non-interactive, returns `{ allowed: true }`.
|
|
101
|
+
*/
|
|
102
|
+
export function checkInteractive(command: string): SafetyCheckResult {
|
|
103
|
+
const subs = splitOnShellOperators(command);
|
|
104
|
+
for (const sub of subs) {
|
|
105
|
+
const tokens = tokenize(sub);
|
|
106
|
+
const program = findProgram(tokens);
|
|
107
|
+
if (!program) continue;
|
|
108
|
+
const rule = RULES_BY_NAME.get(program.name);
|
|
109
|
+
if (!rule) continue;
|
|
110
|
+
const suggestion = rule.check(program.args);
|
|
111
|
+
if (suggestion !== null) {
|
|
112
|
+
return {
|
|
113
|
+
allowed: false,
|
|
114
|
+
reason: `Interactive command detected. ${suggestion}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { allowed: true };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Tokenization
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Minimal shell-aware tokenizer: splits on unquoted whitespace, keeps
|
|
127
|
+
* single/double-quoted regions intact (quotes themselves are stripped),
|
|
128
|
+
* and honors backslash escapes. Sufficient for identifying the program
|
|
129
|
+
* token and distinguishing flags from positional args; not a complete
|
|
130
|
+
* POSIX shell parser.
|
|
131
|
+
*/
|
|
132
|
+
export function tokenize(command: string): string[] {
|
|
133
|
+
const tokens: string[] = [];
|
|
134
|
+
let current = '';
|
|
135
|
+
let inSingle = false;
|
|
136
|
+
let inDouble = false;
|
|
137
|
+
for (let i = 0; i < command.length; i++) {
|
|
138
|
+
const ch = command[i]!;
|
|
139
|
+
if (!inSingle && ch === '\\' && i + 1 < command.length) {
|
|
140
|
+
current += command[i + 1]!;
|
|
141
|
+
i++;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (!inDouble && ch === "'") {
|
|
145
|
+
inSingle = !inSingle;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (!inSingle && ch === '"') {
|
|
149
|
+
inDouble = !inDouble;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!inSingle && !inDouble && /\s/u.test(ch)) {
|
|
153
|
+
if (current.length > 0) {
|
|
154
|
+
tokens.push(current);
|
|
155
|
+
current = '';
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
current += ch;
|
|
160
|
+
}
|
|
161
|
+
if (current.length > 0) tokens.push(current);
|
|
162
|
+
return tokens;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Find the effective program name and argument list, accounting for:
|
|
167
|
+
* 1. Leading `KEY=VALUE` env var prefixes (`FOO=bar vim file`)
|
|
168
|
+
* 2. The `env` wrapper (`env FOO=bar vim file` or `env -u BAR vim`)
|
|
169
|
+
*
|
|
170
|
+
* Returns the program's basename (last path segment) and the remaining
|
|
171
|
+
* arg tokens, or `null` if no program token is present.
|
|
172
|
+
*/
|
|
173
|
+
export function findProgram(
|
|
174
|
+
tokens: string[],
|
|
175
|
+
): { name: string; args: string[] } | null {
|
|
176
|
+
let i = 0;
|
|
177
|
+
while (i < tokens.length && isEnvAssignment(tokens[i]!)) i++;
|
|
178
|
+
|
|
179
|
+
if (i < tokens.length && tokens[i] === 'env') {
|
|
180
|
+
i++;
|
|
181
|
+
// `env`'s flags that take a subsequent argument. We need to skip
|
|
182
|
+
// both the flag and its value so a value that doesn't look like a
|
|
183
|
+
// flag (e.g. `env -u OLD vim`) isn't mistaken for the program.
|
|
184
|
+
const ARG_FLAGS_SHORT = new Set(['-u', '-C', '-S']);
|
|
185
|
+
const ARG_FLAGS_LONG = new Set(['--unset', '--chdir', '--split-string']);
|
|
186
|
+
while (i < tokens.length) {
|
|
187
|
+
const t = tokens[i]!;
|
|
188
|
+
if (isEnvAssignment(t)) {
|
|
189
|
+
i++;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (!t.startsWith('-')) break;
|
|
193
|
+
if (ARG_FLAGS_SHORT.has(t) || ARG_FLAGS_LONG.has(t)) {
|
|
194
|
+
i += 2; // consume flag + value
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// --long=value form or flags with no arg (-i, -0, --verbose, etc.)
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (i >= tokens.length) return null;
|
|
203
|
+
const prog = tokens[i]!;
|
|
204
|
+
const basename = prog.split('/').pop() ?? prog;
|
|
205
|
+
return { name: basename, args: tokens.slice(i + 1) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isEnvAssignment(token: string): boolean {
|
|
209
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/u.test(token);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Per-program predicates
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
function pythonCheck(name: string, args: string[]): string | null {
|
|
217
|
+
for (let i = 0; i < args.length; i++) {
|
|
218
|
+
const arg = args[i]!;
|
|
219
|
+
// -c / -m consume the next arg but the presence alone is enough.
|
|
220
|
+
if (arg === '-c' || arg === '-m') return null;
|
|
221
|
+
if (arg === '-V' || arg === '--version') return null;
|
|
222
|
+
if (arg === '-h' || arg === '--help') return null;
|
|
223
|
+
// Script file.
|
|
224
|
+
if (/\.py[ocw]?$/u.test(arg) && !arg.startsWith('-')) return null;
|
|
225
|
+
// Any positional arg is treated as a script (matches how Python's argv
|
|
226
|
+
// parser works).
|
|
227
|
+
if (!arg.startsWith('-')) return null;
|
|
228
|
+
}
|
|
229
|
+
return `${name} without a script or \`-c\`/\`-m\` starts an interactive REPL and will block. Provide a script file or use \`${name} -c "..."\`.`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function nodeCheck(args: string[]): string | null {
|
|
233
|
+
for (const arg of args) {
|
|
234
|
+
if (
|
|
235
|
+
arg === '-e' ||
|
|
236
|
+
arg === '--eval' ||
|
|
237
|
+
arg === '-p' ||
|
|
238
|
+
arg === '--print' ||
|
|
239
|
+
arg === '-v' ||
|
|
240
|
+
arg === '--version' ||
|
|
241
|
+
arg === '-h' ||
|
|
242
|
+
arg === '--help'
|
|
243
|
+
) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (!arg.startsWith('-')) return null; // script file or --file
|
|
247
|
+
}
|
|
248
|
+
return 'node without a script or `-e` starts an interactive REPL and will block. Provide a script file or use `node -e "..."`.';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function rubyCheck(args: string[]): string | null {
|
|
252
|
+
for (const arg of args) {
|
|
253
|
+
if (arg === '-e' || arg === '-v' || arg === '--version' || arg === '-h' || arg === '--help') {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
if (!arg.startsWith('-')) return null;
|
|
257
|
+
}
|
|
258
|
+
return 'ruby without a script or `-e` reads from stdin and will block. Provide a `.rb` file or use `ruby -e "..."`.';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function mongoCheck(name: string, args: string[]): string | null {
|
|
262
|
+
if (args.includes('--eval') || args.includes('-e') || args.includes('--version') || args.includes('--help')) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
// Executing a script file is also non-interactive.
|
|
266
|
+
if (args.some((a) => /\.js$/u.test(a) && !a.startsWith('-'))) return null;
|
|
267
|
+
return `${name} without \`--eval\` or a script file opens an interactive shell. Use \`${name} --eval "..."\`.`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function sqliteCheck(args: string[]): string | null {
|
|
271
|
+
if (args.includes('-cmd') || args.includes('-batch') || args.includes('-version') || args.includes('--help')) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
// sqlite3 <db> "<sql>" — two or more non-flag args means the second is a query.
|
|
275
|
+
const nonFlag = args.filter((a) => !a.startsWith('-'));
|
|
276
|
+
if (nonFlag.length >= 2) return null;
|
|
277
|
+
return 'sqlite3 without a SQL argument opens an interactive prompt. Use `sqlite3 <db> "<sql>"` or pass `-cmd "..."`.';
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function psqlCheck(args: string[]): string | null {
|
|
281
|
+
// -c / --command: inline SQL. -f / --file: script file. -l / --list: list DBs.
|
|
282
|
+
// --version / -V: version. All are single-shot, non-interactive.
|
|
283
|
+
// NOTE: -h means "host" in psql, NOT help. We do not treat it as safe.
|
|
284
|
+
const safeFlags = new Set([
|
|
285
|
+
'-c', '--command', '-f', '--file', '-l', '--list', '--version', '-V', '--help',
|
|
286
|
+
]);
|
|
287
|
+
for (const arg of args) {
|
|
288
|
+
if (safeFlags.has(arg)) return null;
|
|
289
|
+
// Flags written as --command=VALUE.
|
|
290
|
+
if (arg.startsWith('--command=') || arg.startsWith('--file=')) return null;
|
|
291
|
+
}
|
|
292
|
+
return 'psql without `-c` or `-f` opens an interactive prompt. Use `psql -c "SELECT ..."` or `psql -f script.sql`.';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function mysqlCheck(args: string[]): string | null {
|
|
296
|
+
const safeFlags = new Set(['-e', '--execute', '--version', '-V', '--help', '-?']);
|
|
297
|
+
for (const arg of args) {
|
|
298
|
+
if (safeFlags.has(arg)) return null;
|
|
299
|
+
if (arg.startsWith('--execute=')) return null;
|
|
300
|
+
}
|
|
301
|
+
return 'mysql without `-e` opens an interactive prompt. Use `mysql -e "SELECT ..."`.';
|
|
302
|
+
}
|