gm-kilo 2.0.15 → 2.0.16
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/.editorconfig +12 -0
- package/.gitignore +13 -0
- package/CONTRIBUTING.md +26 -0
- package/cli.js +7 -1
- package/hooks/hooks.json +58 -0
- package/hooks/pre-tool-use-hook.js +94 -0
- package/hooks/prompt-submit-hook.js +87 -0
- package/hooks/session-start-hook.js +171 -0
- package/hooks/stop-hook-git.js +184 -0
- package/hooks/stop-hook.js +58 -0
- package/package.json +15 -3
- package/scripts/postinstall.js +138 -0
- package/skills/agent-browser/SKILL.md +257 -0
package/.editorconfig
ADDED
package/.gitignore
ADDED
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Please ensure all code follows the conventions established in this project.
|
|
4
|
+
|
|
5
|
+
## Before Committing
|
|
6
|
+
|
|
7
|
+
Run the build to verify everything is working:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm run build plugforge-starter [output-dir]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Platform Conventions
|
|
14
|
+
|
|
15
|
+
- Each platform adapter in `platforms/` extends PlatformAdapter or CLIAdapter
|
|
16
|
+
- File generation logic goes in `createFileStructure()`
|
|
17
|
+
- Use TemplateBuilder methods for shared generation logic
|
|
18
|
+
- Skills are auto-discovered from plugforge-starter/skills/
|
|
19
|
+
|
|
20
|
+
## Testing
|
|
21
|
+
|
|
22
|
+
Build all 9 platform outputs:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
node cli.js plugforge-starter /tmp/test-build
|
|
26
|
+
```
|
package/cli.js
CHANGED
|
@@ -19,12 +19,18 @@ try {
|
|
|
19
19
|
|
|
20
20
|
const filesToCopy = [
|
|
21
21
|
['agents', 'agents'],
|
|
22
|
+
['hooks', 'hooks'],
|
|
23
|
+
['skills', 'skills'],
|
|
22
24
|
['index.js', 'index.js'],
|
|
23
25
|
['gm.js', 'gm.js'],
|
|
24
26
|
['kilocode.json', 'kilocode.json'],
|
|
25
27
|
['package.json', 'package.json'],
|
|
26
28
|
['.mcp.json', '.mcp.json'],
|
|
27
|
-
['README.md', 'README.md']
|
|
29
|
+
['README.md', 'README.md'],
|
|
30
|
+
['LICENSE', 'LICENSE'],
|
|
31
|
+
['CONTRIBUTING.md', 'CONTRIBUTING.md'],
|
|
32
|
+
['.gitignore', '.gitignore'],
|
|
33
|
+
['.editorconfig', '.editorconfig']
|
|
28
34
|
];
|
|
29
35
|
|
|
30
36
|
function copyRecursive(src, dst) {
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Hooks for gm Kilo CLI extension",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"tool.execute.before": [
|
|
5
|
+
{
|
|
6
|
+
"matcher": "*",
|
|
7
|
+
"hooks": [
|
|
8
|
+
{
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node ${KILO_PLUGIN_ROOT}/hooks/pre-tool-use-hook.js",
|
|
11
|
+
"timeout": 3600
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
],
|
|
16
|
+
"session.created": [
|
|
17
|
+
{
|
|
18
|
+
"matcher": "*",
|
|
19
|
+
"hooks": [
|
|
20
|
+
{
|
|
21
|
+
"type": "command",
|
|
22
|
+
"command": "node ${KILO_PLUGIN_ROOT}/hooks/session-start-hook.js",
|
|
23
|
+
"timeout": 10000
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"message.updated": [
|
|
29
|
+
{
|
|
30
|
+
"matcher": "*",
|
|
31
|
+
"hooks": [
|
|
32
|
+
{
|
|
33
|
+
"type": "command",
|
|
34
|
+
"command": "node ${KILO_PLUGIN_ROOT}/hooks/prompt-submit-hook.js",
|
|
35
|
+
"timeout": 3600
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"session.closing": [
|
|
41
|
+
{
|
|
42
|
+
"matcher": "*",
|
|
43
|
+
"hooks": [
|
|
44
|
+
{
|
|
45
|
+
"type": "command",
|
|
46
|
+
"command": "node ${KILO_PLUGIN_ROOT}/hooks/stop-hook.js",
|
|
47
|
+
"timeout": 300000
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"type": "command",
|
|
51
|
+
"command": "node ${KILO_PLUGIN_ROOT}/hooks/stop-hook-git.js",
|
|
52
|
+
"timeout": 60000
|
|
53
|
+
}
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
7
|
+
|
|
8
|
+
const shellTools = ['Bash', 'run_shell_command'];
|
|
9
|
+
const writeTools = ['Write', 'write_file'];
|
|
10
|
+
const searchTools = ['Glob', 'Grep', 'glob', 'search_file_content', 'Search', 'search'];
|
|
11
|
+
const forbiddenTools = ['find', 'Find'];
|
|
12
|
+
|
|
13
|
+
const run = () => {
|
|
14
|
+
try {
|
|
15
|
+
const input = fs.readFileSync(0, 'utf-8');
|
|
16
|
+
const data = JSON.parse(input);
|
|
17
|
+
const { tool_name, tool_input } = data;
|
|
18
|
+
|
|
19
|
+
if (!tool_name) return { allow: true };
|
|
20
|
+
|
|
21
|
+
if (forbiddenTools.includes(tool_name)) {
|
|
22
|
+
return { block: true, reason: 'Use gm:code-search or plugin:gm:dev for semantic codebase search instead of filesystem find' };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (shellTools.includes(tool_name)) {
|
|
26
|
+
return { block: true, reason: 'Use dev execute instead for all command execution' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (writeTools.includes(tool_name)) {
|
|
30
|
+
const file_path = tool_input?.file_path || '';
|
|
31
|
+
const ext = path.extname(file_path);
|
|
32
|
+
const inSkillsDir = file_path.includes('/skills/');
|
|
33
|
+
const base = path.basename(file_path).toLowerCase();
|
|
34
|
+
if ((ext === '.md' || ext === '.txt' || base.startsWith('features_list')) &&
|
|
35
|
+
!base.startsWith('claude') && !base.startsWith('readme') && !inSkillsDir) {
|
|
36
|
+
return { block: true, reason: 'Cannot create documentation files. Only CLAUDE.md and readme.md are maintained.' };
|
|
37
|
+
}
|
|
38
|
+
if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/.test(base) ||
|
|
39
|
+
/^(jest|vitest|mocha|ava|jasmine|tap)\.(config|setup)/.test(base) ||
|
|
40
|
+
file_path.includes('/__tests__/') || file_path.includes('/test/') ||
|
|
41
|
+
file_path.includes('/tests/') || file_path.includes('/fixtures/') ||
|
|
42
|
+
file_path.includes('/test-data/') || file_path.includes('/__mocks__/') ||
|
|
43
|
+
/\.(snap|stub|mock|fixture)\.(js|ts|json)$/.test(base)) {
|
|
44
|
+
return { block: true, reason: 'Test files forbidden on disk. Use plugin:gm:dev with real services for all testing.' };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (searchTools.includes(tool_name)) {
|
|
49
|
+
return { block: true, reason: 'Code exploration must use: gm:code-search skill or plugin:gm:dev execute. This restriction enforces semantic search over filesystem patterns.' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (tool_name === 'Task') {
|
|
53
|
+
const subagentType = tool_input?.subagent_type || '';
|
|
54
|
+
if (subagentType === 'Explore') {
|
|
55
|
+
return { block: true, reason: 'Use gm:thorns-overview for codebase insight, then use gm:code-search or plugin:gm:dev' };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (tool_name === 'EnterPlanMode') {
|
|
60
|
+
return { block: true, reason: 'Plan mode is disabled. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) via gm:gm subagent instead.' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { allow: true };
|
|
64
|
+
} catch (error) {
|
|
65
|
+
return { allow: true };
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = run();
|
|
71
|
+
|
|
72
|
+
if (result.block) {
|
|
73
|
+
if (isGemini) {
|
|
74
|
+
console.log(JSON.stringify({ decision: 'deny', reason: result.reason }));
|
|
75
|
+
} else {
|
|
76
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: result.reason } }));
|
|
77
|
+
}
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isGemini) {
|
|
82
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
83
|
+
} else {
|
|
84
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } }));
|
|
85
|
+
}
|
|
86
|
+
process.exit(0);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (isGemini) {
|
|
89
|
+
console.log(JSON.stringify({ decision: 'allow' }));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'allow' } }));
|
|
92
|
+
}
|
|
93
|
+
process.exit(0);
|
|
94
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const { execSync } = require('child_process');
|
|
5
|
+
|
|
6
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || process.env.OC_PROJECT_DIR;
|
|
7
|
+
|
|
8
|
+
const COMPACT_CONTEXT = 'use gm agent | ref: TOOL_INVARIANTS | codesearch for exploration | plugin:gm:dev for execution';
|
|
9
|
+
|
|
10
|
+
const PLAN_MODE_BLOCK = 'DO NOT use EnterPlanMode or any plan mode tool. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) instead. Plan mode is blocked.';
|
|
11
|
+
|
|
12
|
+
const getBaseContext = (resetMsg = '') => {
|
|
13
|
+
let ctx = 'use gm agent';
|
|
14
|
+
if (resetMsg) ctx += ' - ' + resetMsg;
|
|
15
|
+
return ctx;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const readStdinPrompt = () => {
|
|
19
|
+
try {
|
|
20
|
+
const raw = fs.readFileSync(0, 'utf-8');
|
|
21
|
+
const data = JSON.parse(raw);
|
|
22
|
+
return data.prompt || '';
|
|
23
|
+
} catch (e) {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const runCodeSearch = (query, cwd) => {
|
|
29
|
+
if (!query || !cwd || !fs.existsSync(cwd)) return '';
|
|
30
|
+
try {
|
|
31
|
+
const escaped = query.replace(/"/g, '\\"').substring(0, 200);
|
|
32
|
+
let out;
|
|
33
|
+
try {
|
|
34
|
+
out = execSync(`bun x codebasesearch@latest "${escaped}"`, {
|
|
35
|
+
encoding: 'utf-8',
|
|
36
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
37
|
+
cwd,
|
|
38
|
+
timeout: 55000,
|
|
39
|
+
killSignal: 'SIGTERM'
|
|
40
|
+
});
|
|
41
|
+
} catch (bunErr) {
|
|
42
|
+
if (bunErr.killed) return '';
|
|
43
|
+
out = execSync(`npx -y codebasesearch@latest "${escaped}"`, {
|
|
44
|
+
encoding: 'utf-8',
|
|
45
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
46
|
+
cwd,
|
|
47
|
+
timeout: 55000,
|
|
48
|
+
killSignal: 'SIGTERM'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const lines = out.split('\n');
|
|
52
|
+
const resultStart = lines.findIndex(l => l.includes('Searching for:'));
|
|
53
|
+
return resultStart >= 0 ? lines.slice(resultStart).join('\n').trim() : out.trim();
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return '';
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const emit = (additionalContext) => {
|
|
60
|
+
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
61
|
+
const isOpenCode = process.env.OC_PLUGIN_ROOT !== undefined;
|
|
62
|
+
|
|
63
|
+
if (isGemini) {
|
|
64
|
+
console.log(JSON.stringify({ systemMessage: additionalContext }, null, 2));
|
|
65
|
+
} else if (isOpenCode) {
|
|
66
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'message.updated', additionalContext } }, null, 2));
|
|
67
|
+
} else {
|
|
68
|
+
console.log(JSON.stringify({ hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext } }, null, 2));
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const prompt = readStdinPrompt();
|
|
74
|
+
const parts = [getBaseContext() + ' | ' + COMPACT_CONTEXT + ' | ' + PLAN_MODE_BLOCK];
|
|
75
|
+
|
|
76
|
+
if (prompt && projectDir) {
|
|
77
|
+
const searchResults = runCodeSearch(prompt, projectDir);
|
|
78
|
+
if (searchResults) {
|
|
79
|
+
parts.push(`=== Semantic code search results for initial prompt ===\n${searchResults}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
emit(parts.join('\n\n'));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
emit(getBaseContext('hook error: ' + error.message) + ' | ' + COMPACT_CONTEXT);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT || process.env.GEMINI_PROJECT_DIR || process.env.OC_PLUGIN_ROOT;
|
|
8
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.env.GEMINI_PROJECT_DIR || process.env.OC_PROJECT_DIR;
|
|
9
|
+
|
|
10
|
+
const ensureGitignore = () => {
|
|
11
|
+
if (!projectDir) return;
|
|
12
|
+
const gitignorePath = path.join(projectDir, '.gitignore');
|
|
13
|
+
const entry = '.gm-stop-verified';
|
|
14
|
+
try {
|
|
15
|
+
let content = '';
|
|
16
|
+
if (fs.existsSync(gitignorePath)) {
|
|
17
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
18
|
+
}
|
|
19
|
+
if (!content.split('\n').some(line => line.trim() === entry)) {
|
|
20
|
+
const newContent = content.endsWith('\n') || content === ''
|
|
21
|
+
? content + entry + '\n'
|
|
22
|
+
: content + '\n' + entry + '\n';
|
|
23
|
+
fs.writeFileSync(gitignorePath, newContent);
|
|
24
|
+
}
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// Silently fail - not critical
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
ensureGitignore();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
let outputs = [];
|
|
34
|
+
|
|
35
|
+
// 1. Read ./start.md
|
|
36
|
+
if (pluginRoot) {
|
|
37
|
+
const startMdPath = path.join(pluginRoot, '/agents/gm.md');
|
|
38
|
+
try {
|
|
39
|
+
const startMdContent = fs.readFileSync(startMdPath, 'utf-8');
|
|
40
|
+
outputs.push(startMdContent);
|
|
41
|
+
} catch (e) {
|
|
42
|
+
// File may not exist in this context
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 2. Add semantic code-search explanation
|
|
47
|
+
const codeSearchContext = `## 🔍 Semantic Code Search Now Available
|
|
48
|
+
|
|
49
|
+
Your prompts will trigger **semantic code search** - intelligent, intent-based exploration of your codebase.
|
|
50
|
+
|
|
51
|
+
### How It Works
|
|
52
|
+
Describe what you need in plain language, and the search understands your intent:
|
|
53
|
+
- "Find authentication validation" → locates auth checks, guards, permission logic
|
|
54
|
+
- "Where is database initialization?" → finds connection setup, migrations, schemas
|
|
55
|
+
- "Show error handling patterns" → discovers try/catch patterns, error boundaries
|
|
56
|
+
|
|
57
|
+
NOT syntax-based regex matching - truly semantic understanding across files.
|
|
58
|
+
|
|
59
|
+
### Example
|
|
60
|
+
Instead of regex patterns, simply describe your intent:
|
|
61
|
+
"Find where API authorization is checked"
|
|
62
|
+
|
|
63
|
+
The search will find permission validations, role checks, authentication guards - however they're implemented.
|
|
64
|
+
|
|
65
|
+
### When to Use Code Search
|
|
66
|
+
When exploring unfamiliar code, finding similar patterns, understanding integrations, or locating feature implementations across your codebase.`;
|
|
67
|
+
outputs.push(codeSearchContext);
|
|
68
|
+
|
|
69
|
+
// 3. Run mcp-thorns (bun x with npx fallback)
|
|
70
|
+
if (projectDir && fs.existsSync(projectDir)) {
|
|
71
|
+
try {
|
|
72
|
+
let thornOutput;
|
|
73
|
+
try {
|
|
74
|
+
thornOutput = execSync(`bun x mcp-thorns@latest`, {
|
|
75
|
+
encoding: 'utf-8',
|
|
76
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
77
|
+
cwd: projectDir,
|
|
78
|
+
timeout: 180000,
|
|
79
|
+
killSignal: 'SIGTERM'
|
|
80
|
+
});
|
|
81
|
+
} catch (bunErr) {
|
|
82
|
+
if (bunErr.killed && bunErr.signal === 'SIGTERM') {
|
|
83
|
+
thornOutput = '=== mcp-thorns ===\nSkipped (3min timeout)';
|
|
84
|
+
} else {
|
|
85
|
+
try {
|
|
86
|
+
thornOutput = execSync(`npx -y mcp-thorns@latest`, {
|
|
87
|
+
encoding: 'utf-8',
|
|
88
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
89
|
+
cwd: projectDir,
|
|
90
|
+
timeout: 180000,
|
|
91
|
+
killSignal: 'SIGTERM'
|
|
92
|
+
});
|
|
93
|
+
} catch (npxErr) {
|
|
94
|
+
if (npxErr.killed && npxErr.signal === 'SIGTERM') {
|
|
95
|
+
thornOutput = '=== mcp-thorns ===\nSkipped (3min timeout)';
|
|
96
|
+
} else {
|
|
97
|
+
thornOutput = `=== mcp-thorns ===\nSkipped (error: ${bunErr.message.split('\n')[0]})`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
outputs.push(`=== This is your initial insight of the repository, look at every possible aspect of this for initial opinionation and to offset the need for code exploration ===\n${thornOutput}`);
|
|
103
|
+
} catch (e) {
|
|
104
|
+
if (e.killed && e.signal === 'SIGTERM') {
|
|
105
|
+
outputs.push(`=== mcp-thorns ===\nSkipped (3min timeout)`);
|
|
106
|
+
} else {
|
|
107
|
+
outputs.push(`=== mcp-thorns ===\nSkipped (error: ${e.message.split('\n')[0]})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
outputs.push('Use gm as a philosophy to coordinate all plans and the gm subagent to create and execute all plans');
|
|
112
|
+
const additionalContext = outputs.join('\n\n');
|
|
113
|
+
|
|
114
|
+
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
115
|
+
const isOpenCode = process.env.OC_PLUGIN_ROOT !== undefined;
|
|
116
|
+
|
|
117
|
+
if (isGemini) {
|
|
118
|
+
const result = {
|
|
119
|
+
systemMessage: additionalContext
|
|
120
|
+
};
|
|
121
|
+
console.log(JSON.stringify(result, null, 2));
|
|
122
|
+
} else if (isOpenCode) {
|
|
123
|
+
const result = {
|
|
124
|
+
hookSpecificOutput: {
|
|
125
|
+
hookEventName: 'session.created',
|
|
126
|
+
additionalContext
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
console.log(JSON.stringify(result, null, 2));
|
|
130
|
+
} else {
|
|
131
|
+
const result = {
|
|
132
|
+
hookSpecificOutput: {
|
|
133
|
+
hookEventName: 'SessionStart',
|
|
134
|
+
additionalContext
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
console.log(JSON.stringify(result, null, 2));
|
|
138
|
+
}
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
|
|
141
|
+
const isOpenCode = process.env.OC_PLUGIN_ROOT !== undefined;
|
|
142
|
+
|
|
143
|
+
if (isGemini) {
|
|
144
|
+
console.log(JSON.stringify({
|
|
145
|
+
systemMessage: `Error executing hook: ${error.message}`
|
|
146
|
+
}, null, 2));
|
|
147
|
+
} else if (isOpenCode) {
|
|
148
|
+
console.log(JSON.stringify({
|
|
149
|
+
hookSpecificOutput: {
|
|
150
|
+
hookEventName: 'session.created',
|
|
151
|
+
additionalContext: `Error executing hook: ${error.message}`
|
|
152
|
+
}
|
|
153
|
+
}, null, 2));
|
|
154
|
+
} else {
|
|
155
|
+
console.log(JSON.stringify({
|
|
156
|
+
hookSpecificOutput: {
|
|
157
|
+
hookEventName: 'SessionStart',
|
|
158
|
+
additionalContext: `Error executing hook: ${error.message}`
|
|
159
|
+
}
|
|
160
|
+
}, null, 2));
|
|
161
|
+
}
|
|
162
|
+
process.exit(0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
9
|
+
|
|
10
|
+
const getCounterPath = () => {
|
|
11
|
+
const hash = crypto.createHash('md5').update(projectDir).digest('hex');
|
|
12
|
+
return path.join('/tmp', `gm-git-block-counter-${hash}.json`);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const readCounter = () => {
|
|
16
|
+
try {
|
|
17
|
+
const counterPath = getCounterPath();
|
|
18
|
+
if (fs.existsSync(counterPath)) {
|
|
19
|
+
const data = fs.readFileSync(counterPath, 'utf-8');
|
|
20
|
+
return JSON.parse(data);
|
|
21
|
+
}
|
|
22
|
+
} catch (e) {}
|
|
23
|
+
return { count: 0, lastGitHash: null };
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const writeCounter = (data) => {
|
|
27
|
+
try {
|
|
28
|
+
const counterPath = getCounterPath();
|
|
29
|
+
fs.writeFileSync(counterPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
30
|
+
} catch (e) {}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getCurrentGitHash = () => {
|
|
34
|
+
try {
|
|
35
|
+
const hash = execSync('git rev-parse HEAD', {
|
|
36
|
+
cwd: projectDir,
|
|
37
|
+
stdio: 'pipe',
|
|
38
|
+
encoding: 'utf-8'
|
|
39
|
+
}).trim();
|
|
40
|
+
return hash;
|
|
41
|
+
} catch (e) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const resetCounterIfCommitted = (currentHash) => {
|
|
47
|
+
const counter = readCounter();
|
|
48
|
+
if (counter.lastGitHash && currentHash && counter.lastGitHash !== currentHash) {
|
|
49
|
+
counter.count = 0;
|
|
50
|
+
counter.lastGitHash = currentHash;
|
|
51
|
+
writeCounter(counter);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const incrementCounter = (currentHash) => {
|
|
58
|
+
const counter = readCounter();
|
|
59
|
+
counter.count = (counter.count || 0) + 1;
|
|
60
|
+
counter.lastGitHash = currentHash;
|
|
61
|
+
writeCounter(counter);
|
|
62
|
+
return counter.count;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const getGitStatus = () => {
|
|
66
|
+
try {
|
|
67
|
+
execSync('git rev-parse --git-dir', {
|
|
68
|
+
cwd: projectDir,
|
|
69
|
+
stdio: 'pipe'
|
|
70
|
+
});
|
|
71
|
+
} catch (e) {
|
|
72
|
+
return { isRepo: false };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const status = execSync('git status --porcelain', {
|
|
77
|
+
cwd: projectDir,
|
|
78
|
+
stdio: 'pipe',
|
|
79
|
+
encoding: 'utf-8'
|
|
80
|
+
}).trim();
|
|
81
|
+
|
|
82
|
+
const isDirty = status.length > 0;
|
|
83
|
+
|
|
84
|
+
let unpushedCount = 0;
|
|
85
|
+
try {
|
|
86
|
+
const unpushed = execSync('git rev-list --count @{u}..HEAD', {
|
|
87
|
+
cwd: projectDir,
|
|
88
|
+
stdio: 'pipe',
|
|
89
|
+
encoding: 'utf-8'
|
|
90
|
+
}).trim();
|
|
91
|
+
unpushedCount = parseInt(unpushed, 10) || 0;
|
|
92
|
+
} catch (e) {
|
|
93
|
+
unpushedCount = -1;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let behindCount = 0;
|
|
97
|
+
try {
|
|
98
|
+
const behind = execSync('git rev-list --count HEAD..@{u}', {
|
|
99
|
+
cwd: projectDir,
|
|
100
|
+
stdio: 'pipe',
|
|
101
|
+
encoding: 'utf-8'
|
|
102
|
+
}).trim();
|
|
103
|
+
behindCount = parseInt(behind, 10) || 0;
|
|
104
|
+
} catch (e) {}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
isRepo: true,
|
|
108
|
+
isDirty,
|
|
109
|
+
unpushedCount,
|
|
110
|
+
behindCount,
|
|
111
|
+
statusOutput: status
|
|
112
|
+
};
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { isRepo: true, isDirty: false, unpushedCount: 0, behindCount: 0 };
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const run = () => {
|
|
119
|
+
const gitStatus = getGitStatus();
|
|
120
|
+
if (!gitStatus.isRepo) return { ok: true };
|
|
121
|
+
|
|
122
|
+
const currentHash = getCurrentGitHash();
|
|
123
|
+
resetCounterIfCommitted(currentHash);
|
|
124
|
+
|
|
125
|
+
const issues = [];
|
|
126
|
+
if (gitStatus.isDirty) {
|
|
127
|
+
issues.push('Uncommitted changes exist');
|
|
128
|
+
}
|
|
129
|
+
if (gitStatus.unpushedCount > 0) {
|
|
130
|
+
issues.push(`${gitStatus.unpushedCount} commit(s) not pushed`);
|
|
131
|
+
}
|
|
132
|
+
if (gitStatus.unpushedCount === -1) {
|
|
133
|
+
issues.push('Unable to verify push status - may have unpushed commits');
|
|
134
|
+
}
|
|
135
|
+
if (gitStatus.behindCount > 0) {
|
|
136
|
+
issues.push(`${gitStatus.behindCount} upstream change(s) not pulled`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (issues.length > 0) {
|
|
140
|
+
const blockCount = incrementCounter(currentHash);
|
|
141
|
+
return {
|
|
142
|
+
ok: false,
|
|
143
|
+
reason: `Git: ${issues.join(', ')}, must push to remote`,
|
|
144
|
+
blockCount
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const counter = readCounter();
|
|
149
|
+
if (counter.count > 0) {
|
|
150
|
+
counter.count = 0;
|
|
151
|
+
writeCounter(counter);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: true };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const result = run();
|
|
159
|
+
if (!result.ok) {
|
|
160
|
+
if (result.blockCount === 1) {
|
|
161
|
+
console.log(JSON.stringify({
|
|
162
|
+
decision: 'block',
|
|
163
|
+
reason: `Git: ${result.reason} [First violation - blocks this session]`
|
|
164
|
+
}, null, 2));
|
|
165
|
+
process.exit(2);
|
|
166
|
+
} else if (result.blockCount > 1) {
|
|
167
|
+
console.log(JSON.stringify({
|
|
168
|
+
decision: 'approve',
|
|
169
|
+
reason: `⚠️ Git warning (attempt #${result.blockCount}): ${result.reason} - Please commit and push your changes.`
|
|
170
|
+
}, null, 2));
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
decision: 'approve'
|
|
176
|
+
}, null, 2));
|
|
177
|
+
process.exit(0);
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
console.log(JSON.stringify({
|
|
181
|
+
decision: 'approve'
|
|
182
|
+
}, null, 2));
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
// Always use current working directory for .prd location
|
|
7
|
+
// Explicitly resolve to ./.prd in the current folder
|
|
8
|
+
const projectDir = process.cwd();
|
|
9
|
+
const prdFile = path.resolve(projectDir, '.prd');
|
|
10
|
+
|
|
11
|
+
let aborted = false;
|
|
12
|
+
process.on('SIGTERM', () => { aborted = true; });
|
|
13
|
+
process.on('SIGINT', () => { aborted = true; });
|
|
14
|
+
|
|
15
|
+
const run = () => {
|
|
16
|
+
if (aborted) return { ok: true };
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
// Check if .prd file exists and has content
|
|
20
|
+
if (fs.existsSync(prdFile)) {
|
|
21
|
+
const prdContent = fs.readFileSync(prdFile, 'utf-8').trim();
|
|
22
|
+
if (prdContent.length > 0) {
|
|
23
|
+
// .prd has content, block stopping
|
|
24
|
+
return {
|
|
25
|
+
ok: false,
|
|
26
|
+
reason: `Work items remain in ${prdFile}. Remove completed items as they finish. Current items:\n\n${prdContent}`
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// .prd doesn't exist or is empty, allow stop
|
|
32
|
+
return { ok: true };
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { ok: true };
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = run();
|
|
40
|
+
|
|
41
|
+
if (!result.ok) {
|
|
42
|
+
console.log(JSON.stringify({
|
|
43
|
+
decision: 'block',
|
|
44
|
+
reason: result.reason
|
|
45
|
+
}, null, 2));
|
|
46
|
+
process.exit(2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(JSON.stringify({
|
|
50
|
+
decision: 'approve'
|
|
51
|
+
}, null, 2));
|
|
52
|
+
process.exit(0);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
console.log(JSON.stringify({
|
|
55
|
+
decision: 'approve'
|
|
56
|
+
}, null, 2));
|
|
57
|
+
process.exit(0);
|
|
58
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gm-kilo",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.16",
|
|
4
4
|
"description": "Advanced Claude Code plugin with WFGY integration, MCP tools, and automated hooks",
|
|
5
5
|
"author": "AnEntrypoint",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,14 +27,22 @@
|
|
|
27
27
|
"engines": {
|
|
28
28
|
"node": ">=16.0.0"
|
|
29
29
|
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"postinstall": "node scripts/postinstall.js"
|
|
32
|
+
},
|
|
30
33
|
"publishConfig": {
|
|
31
34
|
"access": "public"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
37
|
+
"mcp-gm": "latest",
|
|
38
|
+
"codebasesearch": "latest",
|
|
34
39
|
"mcp-thorns": "^4.1.0"
|
|
35
40
|
},
|
|
36
41
|
"files": [
|
|
37
42
|
"agents/",
|
|
43
|
+
"hooks/",
|
|
44
|
+
"skills/",
|
|
45
|
+
"scripts/",
|
|
38
46
|
"gm.js",
|
|
39
47
|
"index.js",
|
|
40
48
|
"kilocode.json",
|
|
@@ -42,6 +50,10 @@
|
|
|
42
50
|
".mcp.json",
|
|
43
51
|
"README.md",
|
|
44
52
|
"cli.js",
|
|
45
|
-
"install.js"
|
|
53
|
+
"install.js",
|
|
54
|
+
"LICENSE",
|
|
55
|
+
"CONTRIBUTING.md",
|
|
56
|
+
".gitignore",
|
|
57
|
+
".editorconfig"
|
|
46
58
|
]
|
|
47
|
-
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Postinstall script for gm-cc
|
|
8
|
+
* Implements Mode 1: Standalone .claude/ directory installation
|
|
9
|
+
*
|
|
10
|
+
* When installed via npm in a project:
|
|
11
|
+
* - Copies agents/, hooks/, .mcp.json to project's .claude/
|
|
12
|
+
* - Updates .gitignore with .gm-stop-verified
|
|
13
|
+
* - Runs silently, never breaks npm install
|
|
14
|
+
* - Safe to run multiple times (idempotent)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
function isInsideNodeModules() {
|
|
18
|
+
// Check if __dirname contains /node_modules/ in its path
|
|
19
|
+
// Example: /project/node_modules/gm-cc/scripts
|
|
20
|
+
return __dirname.includes(path.sep + 'node_modules' + path.sep);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function getProjectRoot() {
|
|
24
|
+
// From /project/node_modules/gm-cc/scripts
|
|
25
|
+
// Navigate to /project
|
|
26
|
+
if (!isInsideNodeModules()) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Find the node_modules parent (project root)
|
|
31
|
+
let current = __dirname;
|
|
32
|
+
while (current !== path.dirname(current)) { // While not at root
|
|
33
|
+
current = path.dirname(current);
|
|
34
|
+
const parent = path.dirname(current);
|
|
35
|
+
if (path.basename(current) === 'node_modules') {
|
|
36
|
+
return parent;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeCopyFile(src, dst) {
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(src, 'utf-8');
|
|
45
|
+
const dstDir = path.dirname(dst);
|
|
46
|
+
if (!fs.existsSync(dstDir)) {
|
|
47
|
+
fs.mkdirSync(dstDir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
fs.writeFileSync(dst, content, 'utf-8');
|
|
50
|
+
return true;
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// Silently skip errors
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function safeCopyDirectory(src, dst) {
|
|
58
|
+
try {
|
|
59
|
+
if (!fs.existsSync(src)) {
|
|
60
|
+
return false; // Source doesn't exist, skip
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
fs.mkdirSync(dst, { recursive: true });
|
|
64
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
65
|
+
|
|
66
|
+
entries.forEach(entry => {
|
|
67
|
+
const srcPath = path.join(src, entry.name);
|
|
68
|
+
const dstPath = path.join(dst, entry.name);
|
|
69
|
+
|
|
70
|
+
if (entry.isDirectory()) {
|
|
71
|
+
safeCopyDirectory(srcPath, dstPath);
|
|
72
|
+
} else if (entry.isFile()) {
|
|
73
|
+
safeCopyFile(srcPath, dstPath);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// Silently skip errors
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function updateGitignore(projectRoot) {
|
|
84
|
+
try {
|
|
85
|
+
const gitignorePath = path.join(projectRoot, '.gitignore');
|
|
86
|
+
const entry = '.gm-stop-verified';
|
|
87
|
+
|
|
88
|
+
// Read existing content
|
|
89
|
+
let content = '';
|
|
90
|
+
if (fs.existsSync(gitignorePath)) {
|
|
91
|
+
content = fs.readFileSync(gitignorePath, 'utf-8');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check if entry already exists
|
|
95
|
+
if (content.includes(entry)) {
|
|
96
|
+
return true; // Already there
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Append entry
|
|
100
|
+
if (content && !content.endsWith('\n')) {
|
|
101
|
+
content += '\n';
|
|
102
|
+
}
|
|
103
|
+
content += entry + '\n';
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(gitignorePath, content, 'utf-8');
|
|
106
|
+
return true;
|
|
107
|
+
} catch (err) {
|
|
108
|
+
// Silently skip errors
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function install() {
|
|
114
|
+
// Only run if inside node_modules
|
|
115
|
+
if (!isInsideNodeModules()) {
|
|
116
|
+
return; // Silent exit
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const projectRoot = getProjectRoot();
|
|
120
|
+
if (!projectRoot) {
|
|
121
|
+
return; // Silent exit
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const claudeDir = path.join(projectRoot, '.claude');
|
|
125
|
+
const sourceDir = __dirname.replace(/[\/]scripts$/, ''); // Remove /scripts
|
|
126
|
+
|
|
127
|
+
// Copy files
|
|
128
|
+
safeCopyDirectory(path.join(sourceDir, 'agents'), path.join(claudeDir, 'agents'));
|
|
129
|
+
safeCopyDirectory(path.join(sourceDir, 'hooks'), path.join(claudeDir, 'hooks'));
|
|
130
|
+
safeCopyFile(path.join(sourceDir, '.mcp.json'), path.join(claudeDir, '.mcp.json'));
|
|
131
|
+
|
|
132
|
+
// Update .gitignore
|
|
133
|
+
updateGitignore(projectRoot);
|
|
134
|
+
|
|
135
|
+
// Silent success
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
install();
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-browser
|
|
3
|
+
description: Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to "open a website", "fill out a form", "click a button", "take a screenshot", "scrape data from a page", "test this web app", "login to a site", "automate browser actions", or any task requiring programmatic web interaction.
|
|
4
|
+
allowed-tools: Bash(agent-browser:*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Browser Automation with agent-browser
|
|
8
|
+
|
|
9
|
+
## Core Workflow
|
|
10
|
+
|
|
11
|
+
Every browser automation follows this pattern:
|
|
12
|
+
|
|
13
|
+
1. **Navigate**: `agent-browser open <url>`
|
|
14
|
+
2. **Snapshot**: `agent-browser snapshot -i` (get element refs like `@e1`, `@e2`)
|
|
15
|
+
3. **Interact**: Use refs to click, fill, select
|
|
16
|
+
4. **Re-snapshot**: After navigation or DOM changes, get fresh refs
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
agent-browser open https://example.com/form
|
|
20
|
+
agent-browser snapshot -i
|
|
21
|
+
# Output: @e1 [input type="email"], @e2 [input type="password"], @e3 [button] "Submit"
|
|
22
|
+
|
|
23
|
+
agent-browser fill @e1 "user@example.com"
|
|
24
|
+
agent-browser fill @e2 "password123"
|
|
25
|
+
agent-browser click @e3
|
|
26
|
+
agent-browser wait --load networkidle
|
|
27
|
+
agent-browser snapshot -i # Check result
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Essential Commands
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Navigation
|
|
34
|
+
agent-browser open <url> # Navigate (aliases: goto, navigate)
|
|
35
|
+
agent-browser close # Close browser
|
|
36
|
+
|
|
37
|
+
# Snapshot
|
|
38
|
+
agent-browser snapshot -i # Interactive elements with refs (recommended)
|
|
39
|
+
agent-browser snapshot -i -C # Include cursor-interactive elements (divs with onclick, cursor:pointer)
|
|
40
|
+
agent-browser snapshot -s "#selector" # Scope to CSS selector
|
|
41
|
+
|
|
42
|
+
# Interaction (use @refs from snapshot)
|
|
43
|
+
agent-browser click @e1 # Click element
|
|
44
|
+
agent-browser fill @e2 "text" # Clear and type text
|
|
45
|
+
agent-browser type @e2 "text" # Type without clearing
|
|
46
|
+
agent-browser select @e1 "option" # Select dropdown option
|
|
47
|
+
agent-browser check @e1 # Check checkbox
|
|
48
|
+
agent-browser press Enter # Press key
|
|
49
|
+
agent-browser scroll down 500 # Scroll page
|
|
50
|
+
|
|
51
|
+
# Get information
|
|
52
|
+
agent-browser get text @e1 # Get element text
|
|
53
|
+
agent-browser get url # Get current URL
|
|
54
|
+
agent-browser get title # Get page title
|
|
55
|
+
|
|
56
|
+
# Wait
|
|
57
|
+
agent-browser wait @e1 # Wait for element
|
|
58
|
+
agent-browser wait --load networkidle # Wait for network idle
|
|
59
|
+
agent-browser wait --url "**/page" # Wait for URL pattern
|
|
60
|
+
agent-browser wait 2000 # Wait milliseconds
|
|
61
|
+
|
|
62
|
+
# Capture
|
|
63
|
+
agent-browser screenshot # Screenshot to temp dir
|
|
64
|
+
agent-browser screenshot --full # Full page screenshot
|
|
65
|
+
agent-browser pdf output.pdf # Save as PDF
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Common Patterns
|
|
69
|
+
|
|
70
|
+
### Form Submission
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
agent-browser open https://example.com/signup
|
|
74
|
+
agent-browser snapshot -i
|
|
75
|
+
agent-browser fill @e1 "Jane Doe"
|
|
76
|
+
agent-browser fill @e2 "jane@example.com"
|
|
77
|
+
agent-browser select @e3 "California"
|
|
78
|
+
agent-browser check @e4
|
|
79
|
+
agent-browser click @e5
|
|
80
|
+
agent-browser wait --load networkidle
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Authentication with State Persistence
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Login once and save state
|
|
87
|
+
agent-browser open https://app.example.com/login
|
|
88
|
+
agent-browser snapshot -i
|
|
89
|
+
agent-browser fill @e1 "$USERNAME"
|
|
90
|
+
agent-browser fill @e2 "$PASSWORD"
|
|
91
|
+
agent-browser click @e3
|
|
92
|
+
agent-browser wait --url "**/dashboard"
|
|
93
|
+
agent-browser state save auth.json
|
|
94
|
+
|
|
95
|
+
# Reuse in future sessions
|
|
96
|
+
agent-browser state load auth.json
|
|
97
|
+
agent-browser open https://app.example.com/dashboard
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Data Extraction
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
agent-browser open https://example.com/products
|
|
104
|
+
agent-browser snapshot -i
|
|
105
|
+
agent-browser get text @e5 # Get specific element text
|
|
106
|
+
agent-browser get text body > page.txt # Get all page text
|
|
107
|
+
|
|
108
|
+
# JSON output for parsing
|
|
109
|
+
agent-browser snapshot -i --json
|
|
110
|
+
agent-browser get text @e1 --json
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Parallel Sessions
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
agent-browser --session site1 open https://site-a.com
|
|
117
|
+
agent-browser --session site2 open https://site-b.com
|
|
118
|
+
|
|
119
|
+
agent-browser --session site1 snapshot -i
|
|
120
|
+
agent-browser --session site2 snapshot -i
|
|
121
|
+
|
|
122
|
+
agent-browser session list
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Connect to Existing Chrome
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
# Auto-discover running Chrome with remote debugging enabled
|
|
129
|
+
agent-browser --auto-connect open https://example.com
|
|
130
|
+
agent-browser --auto-connect snapshot
|
|
131
|
+
|
|
132
|
+
# Or with explicit CDP port
|
|
133
|
+
agent-browser --cdp 9222 snapshot
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Visual Browser (Debugging)
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
agent-browser --headed open https://example.com
|
|
140
|
+
agent-browser highlight @e1 # Highlight element
|
|
141
|
+
agent-browser record start demo.webm # Record session
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Local Files (PDFs, HTML)
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
# Open local files with file:// URLs
|
|
148
|
+
agent-browser --allow-file-access open file:///path/to/document.pdf
|
|
149
|
+
agent-browser --allow-file-access open file:///path/to/page.html
|
|
150
|
+
agent-browser screenshot output.png
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### iOS Simulator (Mobile Safari)
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# List available iOS simulators
|
|
157
|
+
agent-browser device list
|
|
158
|
+
|
|
159
|
+
# Launch Safari on a specific device
|
|
160
|
+
agent-browser -p ios --device "iPhone 16 Pro" open https://example.com
|
|
161
|
+
|
|
162
|
+
# Same workflow as desktop - snapshot, interact, re-snapshot
|
|
163
|
+
agent-browser -p ios snapshot -i
|
|
164
|
+
agent-browser -p ios tap @e1 # Tap (alias for click)
|
|
165
|
+
agent-browser -p ios fill @e2 "text"
|
|
166
|
+
agent-browser -p ios swipe up # Mobile-specific gesture
|
|
167
|
+
|
|
168
|
+
# Take screenshot
|
|
169
|
+
agent-browser -p ios screenshot mobile.png
|
|
170
|
+
|
|
171
|
+
# Close session (shuts down simulator)
|
|
172
|
+
agent-browser -p ios close
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Requirements:** macOS with Xcode, Appium (`npm install -g appium && appium driver install xcuitest`)
|
|
176
|
+
|
|
177
|
+
**Real devices:** Works with physical iOS devices if pre-configured. Use `--device "<UDID>"` where UDID is from `xcrun xctrace list devices`.
|
|
178
|
+
|
|
179
|
+
## Ref Lifecycle (Important)
|
|
180
|
+
|
|
181
|
+
Refs (`@e1`, `@e2`, etc.) are invalidated when the page changes. Always re-snapshot after:
|
|
182
|
+
|
|
183
|
+
- Clicking links or buttons that navigate
|
|
184
|
+
- Form submissions
|
|
185
|
+
- Dynamic content loading (dropdowns, modals)
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
agent-browser click @e5 # Navigates to new page
|
|
189
|
+
agent-browser snapshot -i # MUST re-snapshot
|
|
190
|
+
agent-browser click @e1 # Use new refs
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Semantic Locators (Alternative to Refs)
|
|
194
|
+
|
|
195
|
+
When refs are unavailable or unreliable, use semantic locators:
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
agent-browser find text "Sign In" click
|
|
199
|
+
agent-browser find label "Email" fill "user@test.com"
|
|
200
|
+
agent-browser find role button click --name "Submit"
|
|
201
|
+
agent-browser find placeholder "Search" type "query"
|
|
202
|
+
agent-browser find testid "submit-btn" click
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## JavaScript Evaluation (eval)
|
|
206
|
+
|
|
207
|
+
Use `eval` to run JavaScript in the browser context. **Shell quoting can corrupt complex expressions** -- use `--stdin` or `-b` to avoid issues.
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Simple expressions work with regular quoting
|
|
211
|
+
agent-browser eval 'document.title'
|
|
212
|
+
agent-browser eval 'document.querySelectorAll("img").length'
|
|
213
|
+
|
|
214
|
+
# Complex JS: use --stdin with heredoc (RECOMMENDED)
|
|
215
|
+
agent-browser eval --stdin <<'EVALEOF'
|
|
216
|
+
JSON.stringify(
|
|
217
|
+
Array.from(document.querySelectorAll("img"))
|
|
218
|
+
.filter(i => !i.alt)
|
|
219
|
+
.map(i => ({ src: i.src.split("/").pop(), width: i.width }))
|
|
220
|
+
)
|
|
221
|
+
EVALEOF
|
|
222
|
+
|
|
223
|
+
# Alternative: base64 encoding (avoids all shell escaping issues)
|
|
224
|
+
agent-browser eval -b "$(echo -n 'Array.from(document.querySelectorAll("a")).map(a => a.href)' | base64)"
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Why this matters:** When the shell processes your command, inner double quotes, `!` characters (history expansion), backticks, and `$()` can all corrupt the JavaScript before it reaches agent-browser. The `--stdin` and `-b` flags bypass shell interpretation entirely.
|
|
228
|
+
|
|
229
|
+
**Rules of thumb:**
|
|
230
|
+
- Single-line, no nested quotes -> regular `eval 'expression'` with single quotes is fine
|
|
231
|
+
- Nested quotes, arrow functions, template literals, or multiline -> use `eval --stdin <<'EVALEOF'`
|
|
232
|
+
- Programmatic/generated scripts -> use `eval -b` with base64
|
|
233
|
+
|
|
234
|
+
## Deep-Dive Documentation
|
|
235
|
+
|
|
236
|
+
| Reference | When to Use |
|
|
237
|
+
|-----------|-------------|
|
|
238
|
+
| [references/commands.md](references/commands.md) | Full command reference with all options |
|
|
239
|
+
| [references/snapshot-refs.md](references/snapshot-refs.md) | Ref lifecycle, invalidation rules, troubleshooting |
|
|
240
|
+
| [references/session-management.md](references/session-management.md) | Parallel sessions, state persistence, concurrent scraping |
|
|
241
|
+
| [references/authentication.md](references/authentication.md) | Login flows, OAuth, 2FA handling, state reuse |
|
|
242
|
+
| [references/video-recording.md](references/video-recording.md) | Recording workflows for debugging and documentation |
|
|
243
|
+
| [references/proxy-support.md](references/proxy-support.md) | Proxy configuration, geo-testing, rotating proxies |
|
|
244
|
+
|
|
245
|
+
## Ready-to-Use Templates
|
|
246
|
+
|
|
247
|
+
| Template | Description |
|
|
248
|
+
|----------|-------------|
|
|
249
|
+
| [templates/form-automation.sh](templates/form-automation.sh) | Form filling with validation |
|
|
250
|
+
| [templates/authenticated-session.sh](templates/authenticated-session.sh) | Login once, reuse state |
|
|
251
|
+
| [templates/capture-workflow.sh](templates/capture-workflow.sh) | Content extraction with screenshots |
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
./templates/form-automation.sh https://example.com/form
|
|
255
|
+
./templates/authenticated-session.sh https://app.example.com/login
|
|
256
|
+
./templates/capture-workflow.sh https://example.com ./output
|
|
257
|
+
```
|