@humanlayer/agentlayer-justbash 0.0.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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +630 -0
- package/dist/index.js.map +22 -0
- package/dist/prompts/index.d.ts +29 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +123 -0
- package/dist/prompts/index.js.map +10 -0
- package/dist/tools/apply-patch.d.ts +9 -0
- package/dist/tools/apply-patch.d.ts.map +1 -0
- package/dist/tools/bash.d.ts +8 -0
- package/dist/tools/bash.d.ts.map +1 -0
- package/dist/tools/code-search.d.ts +12 -0
- package/dist/tools/code-search.d.ts.map +1 -0
- package/dist/tools/edit.d.ts +12 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/glob.d.ts +6 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/grep.d.ts +8 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +510 -0
- package/dist/tools/index.js.map +21 -0
- package/dist/tools/list.d.ts +7 -0
- package/dist/tools/list.d.ts.map +1 -0
- package/dist/tools/read.d.ts +7 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/skill.d.ts +10 -0
- package/dist/tools/skill.d.ts.map +1 -0
- package/dist/tools/web-fetch.d.ts +7 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-search.d.ts +16 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/write.d.ts +6 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/package.json +59 -0
- package/src/index.ts +2 -0
- package/src/prompts/index.ts +161 -0
- package/src/tools/apply-patch.ts +121 -0
- package/src/tools/apply-patch.txt +35 -0
- package/src/tools/bash.ts +20 -0
- package/src/tools/bash.txt +114 -0
- package/src/tools/code-search.ts +119 -0
- package/src/tools/edit.ts +58 -0
- package/src/tools/edit.txt +10 -0
- package/src/tools/glob.ts +33 -0
- package/src/tools/glob.txt +6 -0
- package/src/tools/grep.ts +53 -0
- package/src/tools/grep.txt +8 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/list.ts +41 -0
- package/src/tools/list.txt +5 -0
- package/src/tools/read.ts +16 -0
- package/src/tools/read.txt +14 -0
- package/src/tools/skill.ts +49 -0
- package/src/tools/web-fetch.ts +97 -0
- package/src/tools/web-fetch.txt +10 -0
- package/src/tools/web-search.ts +59 -0
- package/src/tools/write.ts +26 -0
- package/src/tools/write.txt +8 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { CodeSearchInput } from '@humanlayer/agentlayer-core/interfaces'
|
|
2
|
+
import { CodeSearchTool } from '@humanlayer/agentlayer-core/interfaces'
|
|
3
|
+
import { CODE_SEARCH_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
4
|
+
import type { Bash } from 'just-bash'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_SEC = 30
|
|
7
|
+
const CONTEXT7_BASE_URL = 'https://context7.com'
|
|
8
|
+
|
|
9
|
+
export interface JustBashCodeSearchOptions {
|
|
10
|
+
exaApiKey?: string
|
|
11
|
+
context7ApiKey?: string
|
|
12
|
+
timeoutSec?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function fetchExaViaBash(
|
|
16
|
+
bash: Bash,
|
|
17
|
+
input: CodeSearchInput,
|
|
18
|
+
apiKey: string,
|
|
19
|
+
timeoutSec: number,
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
try {
|
|
22
|
+
const query = `${input.query} -- for ${input.packageName} in ${input.language}`
|
|
23
|
+
const payload = JSON.stringify({ query, tokensNum: 5000 })
|
|
24
|
+
const escapedPayload = payload.replace(/'/g, "'\\''")
|
|
25
|
+
|
|
26
|
+
const result = await bash.exec(
|
|
27
|
+
`curl -s --max-time ${timeoutSec} ` +
|
|
28
|
+
`-H "Content-Type: application/json" ` +
|
|
29
|
+
`-H "x-api-key: ${apiKey}" ` +
|
|
30
|
+
`-d '${escapedPayload}' ` +
|
|
31
|
+
`https://api.exa.ai/context`,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if (result.exitCode !== 0) return null
|
|
35
|
+
|
|
36
|
+
const data = JSON.parse(result.stdout) as { response?: string }
|
|
37
|
+
return data.response ?? null
|
|
38
|
+
} catch {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function fetchContext7ViaBash(
|
|
44
|
+
bash: Bash,
|
|
45
|
+
input: CodeSearchInput,
|
|
46
|
+
apiKey: string,
|
|
47
|
+
timeoutSec: number,
|
|
48
|
+
): Promise<string | null> {
|
|
49
|
+
try {
|
|
50
|
+
// Step 1: search for the library
|
|
51
|
+
const searchQuery = encodeURIComponent(input.query)
|
|
52
|
+
const libName = encodeURIComponent(input.packageName)
|
|
53
|
+
|
|
54
|
+
const searchResult = await bash.exec(
|
|
55
|
+
`curl -s --max-time ${timeoutSec} ` +
|
|
56
|
+
`-H "Authorization: Bearer ${apiKey}" ` +
|
|
57
|
+
`"${CONTEXT7_BASE_URL}/api/v2/libs/search?query=${searchQuery}&libraryName=${libName}"`,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if (searchResult.exitCode !== 0) return null
|
|
61
|
+
|
|
62
|
+
const searchData = JSON.parse(searchResult.stdout) as {
|
|
63
|
+
results?: Array<{ id: string; title: string; trustScore?: number }>
|
|
64
|
+
}
|
|
65
|
+
const libraries = searchData.results ?? []
|
|
66
|
+
if (libraries.length === 0) return null
|
|
67
|
+
|
|
68
|
+
const best = libraries.reduce((a, b) => ((b.trustScore ?? 0) > (a.trustScore ?? 0) ? b : a))
|
|
69
|
+
|
|
70
|
+
// Step 2: fetch context for the selected library
|
|
71
|
+
const contextQuery = encodeURIComponent(input.query)
|
|
72
|
+
const libId = encodeURIComponent(best.id)
|
|
73
|
+
|
|
74
|
+
const contextResult = await bash.exec(
|
|
75
|
+
`curl -s --max-time ${timeoutSec} ` +
|
|
76
|
+
`-H "Authorization: Bearer ${apiKey}" ` +
|
|
77
|
+
`"${CONTEXT7_BASE_URL}/api/v2/context?query=${contextQuery}&libraryId=${libId}"`,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (contextResult.exitCode !== 0) return null
|
|
81
|
+
return contextResult.stdout
|
|
82
|
+
} catch {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createCodeSearchTool(bash: Bash, opts: JustBashCodeSearchOptions) {
|
|
88
|
+
if (!opts.exaApiKey && !opts.context7ApiKey) {
|
|
89
|
+
throw new Error('At least one API key (exaApiKey or context7ApiKey) is required')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC
|
|
93
|
+
|
|
94
|
+
return CodeSearchTool.define(
|
|
95
|
+
async (input: CodeSearchInput): Promise<string> => {
|
|
96
|
+
const [exaResult, c7Result] = await Promise.all([
|
|
97
|
+
opts.exaApiKey ? fetchExaViaBash(bash, input, opts.exaApiKey, timeoutSec) : Promise.resolve(null),
|
|
98
|
+
opts.context7ApiKey
|
|
99
|
+
? fetchContext7ViaBash(bash, input, opts.context7ApiKey, timeoutSec)
|
|
100
|
+
: Promise.resolve(null),
|
|
101
|
+
])
|
|
102
|
+
|
|
103
|
+
const parts: string[] = []
|
|
104
|
+
if (c7Result) {
|
|
105
|
+
parts.push(`## Context7 Documentation\n\n${c7Result}`)
|
|
106
|
+
}
|
|
107
|
+
if (exaResult) {
|
|
108
|
+
parts.push(`## Exa Search Results\n\n${exaResult}`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (parts.length === 0) {
|
|
112
|
+
return `No documentation found for "${input.packageName}" with query: ${input.query}`
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return parts.join('\n\n---\n\n')
|
|
116
|
+
},
|
|
117
|
+
{ description: CODE_SEARCH_DESCRIPTION },
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EditTool } from '@humanlayer/agentlayer-core/interfaces'
|
|
2
|
+
import { EDIT_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
3
|
+
import type { Bash } from 'just-bash'
|
|
4
|
+
|
|
5
|
+
export function createEditTool(bash: Bash) {
|
|
6
|
+
return EditTool.define(
|
|
7
|
+
async (input) => {
|
|
8
|
+
// Read the file content
|
|
9
|
+
const catResult = await bash.exec(`cat "${input.file_path}"`)
|
|
10
|
+
if (catResult.exitCode !== 0) {
|
|
11
|
+
throw new Error(`File ${input.file_path} not found`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const content = catResult.stdout
|
|
15
|
+
|
|
16
|
+
if (!content.includes(input.old_string)) {
|
|
17
|
+
return { content, matchCount: 0 }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let updated: string
|
|
21
|
+
let matchCount: number
|
|
22
|
+
|
|
23
|
+
if (input.replace_all) {
|
|
24
|
+
let count = 0
|
|
25
|
+
let pos = 0
|
|
26
|
+
while (true) {
|
|
27
|
+
const idx = content.indexOf(input.old_string, pos)
|
|
28
|
+
if (idx === -1) break
|
|
29
|
+
count++
|
|
30
|
+
pos = idx + input.old_string.length
|
|
31
|
+
}
|
|
32
|
+
updated = content.split(input.old_string).join(input.new_string)
|
|
33
|
+
matchCount = count
|
|
34
|
+
} else {
|
|
35
|
+
// Single replacement — check for multiple matches
|
|
36
|
+
const firstIdx = content.indexOf(input.old_string)
|
|
37
|
+
const lastIdx = content.lastIndexOf(input.old_string)
|
|
38
|
+
if (firstIdx !== lastIdx) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
'Found multiple matches for old_string. Provide more surrounding context to make the match unique.',
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
updated = content.replace(input.old_string, input.new_string)
|
|
44
|
+
matchCount = 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Write the updated content back using a heredoc
|
|
48
|
+
const DELIM = 'EDITEOF_8f3a2b1c'
|
|
49
|
+
const writeResult = await bash.exec(`cat > "${input.file_path}" <<'${DELIM}'\n${updated}\n${DELIM}`)
|
|
50
|
+
if (writeResult.exitCode !== 0) {
|
|
51
|
+
throw new Error(`Failed to write file ${input.file_path}: ${writeResult.stderr}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { content: updated, matchCount }
|
|
55
|
+
},
|
|
56
|
+
{ description: EDIT_DESCRIPTION },
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Performs exact string replacements in files.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
5
|
+
- When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + arrow. Everything after that arrow is the actual file content to match. Never include any part of the line number prefix in the oldString or newString.
|
|
6
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
7
|
+
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
8
|
+
- The edit will FAIL if `oldString` is not found in the file. Either provide a larger string with more surrounding context to make it unique or use `replaceAll` to change every instance of `oldString`.
|
|
9
|
+
- The edit will FAIL if `oldString` matches multiple locations and `replaceAll` is not set.
|
|
10
|
+
- Use `replaceAll` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { GlobTool } from '@humanlayer/agentlayer-core/interfaces'
|
|
2
|
+
import { GLOB_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
3
|
+
import type { Bash } from 'just-bash'
|
|
4
|
+
|
|
5
|
+
export function createGlobTool(bash: Bash) {
|
|
6
|
+
return GlobTool.define(
|
|
7
|
+
async (input) => {
|
|
8
|
+
const searchPath = input.path ?? '.'
|
|
9
|
+
// Use rg --files with glob filter — rg is typically available in just-bash environments
|
|
10
|
+
const result = await bash.exec(`rg --files -g "${input.pattern}" "${searchPath}" 2>/dev/null`)
|
|
11
|
+
|
|
12
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
13
|
+
// Non-zero / non-empty exit may mean rg not available — fall back to find
|
|
14
|
+
const findResult = await bash.exec(
|
|
15
|
+
`find "${searchPath}" -type f -name "${input.pattern}" 2>/dev/null | head -100`,
|
|
16
|
+
)
|
|
17
|
+
if (findResult.exitCode !== 0) {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
return findResult.stdout
|
|
21
|
+
.split('\n')
|
|
22
|
+
.map((l) => l.trim())
|
|
23
|
+
.filter(Boolean)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return result.stdout
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((l) => l.trim())
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
},
|
|
31
|
+
{ description: GLOB_DESCRIPTION },
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- Fast file pattern matching tool that works with any codebase size
|
|
2
|
+
- Supports glob patterns like "**/*.js" or "src/**/*.ts"
|
|
3
|
+
- Returns matching file paths sorted by modification time
|
|
4
|
+
- Use this tool when you need to find files by name patterns
|
|
5
|
+
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
|
6
|
+
- You have the capability to call multiple tools in a single response. It is always better to speculatively perform multiple searches as a batch that are potentially useful.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { GrepMatch } from '@humanlayer/agentlayer-core/interfaces'
|
|
2
|
+
import { GrepTool } from '@humanlayer/agentlayer-core/interfaces'
|
|
3
|
+
import { GREP_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
4
|
+
import type { Bash } from 'just-bash'
|
|
5
|
+
|
|
6
|
+
const MAX_MATCHES = 100
|
|
7
|
+
|
|
8
|
+
export function createGrepTool(bash: Bash) {
|
|
9
|
+
return GrepTool.define(
|
|
10
|
+
async (input) => {
|
|
11
|
+
const searchPath = input.path ?? '.'
|
|
12
|
+
let cmd = `rg -nH --hidden --no-messages --regexp "${input.pattern}"`
|
|
13
|
+
if (input.include) {
|
|
14
|
+
cmd += ` --glob "${input.include}"`
|
|
15
|
+
}
|
|
16
|
+
cmd += ` "${searchPath}"`
|
|
17
|
+
|
|
18
|
+
const result = await bash.exec(cmd)
|
|
19
|
+
|
|
20
|
+
// Exit code 1 = no matches
|
|
21
|
+
if (result.exitCode === 1) {
|
|
22
|
+
return []
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (result.exitCode !== 0) {
|
|
26
|
+
throw new Error(`grep failed with exit code ${result.exitCode}: ${result.stderr}`)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Parse `file:line:content` output lines
|
|
30
|
+
const matches: GrepMatch[] = []
|
|
31
|
+
for (const line of result.stdout.split('\n')) {
|
|
32
|
+
if (!line) continue
|
|
33
|
+
const colonIdx = line.indexOf(':')
|
|
34
|
+
if (colonIdx === -1) continue
|
|
35
|
+
const afterFile = line.indexOf(':', colonIdx + 1)
|
|
36
|
+
if (afterFile === -1) continue
|
|
37
|
+
|
|
38
|
+
const file = line.slice(0, colonIdx)
|
|
39
|
+
const lineNum = Number.parseInt(line.slice(colonIdx + 1, afterFile), 10)
|
|
40
|
+
const content = line.slice(afterFile + 1)
|
|
41
|
+
|
|
42
|
+
if (!Number.isNaN(lineNum)) {
|
|
43
|
+
matches.push({ file, line: lineNum, content })
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (matches.length >= MAX_MATCHES) break
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return matches
|
|
50
|
+
},
|
|
51
|
+
{ description: GREP_DESCRIPTION },
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
- Fast content search tool that works with any codebase size
|
|
2
|
+
- Searches file contents using regular expressions
|
|
3
|
+
- Supports full regex syntax (eg. "log.*Error", "function\s+\w+", etc.)
|
|
4
|
+
- Filter files by pattern with the include parameter (eg. "*.js", "*.{ts,tsx}")
|
|
5
|
+
- Returns file paths and line numbers with at least one match sorted by modification time
|
|
6
|
+
- Use this tool when you need to find files containing specific patterns
|
|
7
|
+
- If you need to identify/count the number of matches within files, use the Bash tool with `rg` (ripgrep) directly. Do NOT use `grep`.
|
|
8
|
+
- When you are doing an open-ended search that may require multiple rounds of globbing and grepping, use the Task tool instead
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { createApplyPatchTool } from './apply-patch'
|
|
2
|
+
export { createJustBashTool } from './bash'
|
|
3
|
+
export { createCodeSearchTool, type JustBashCodeSearchOptions } from './code-search'
|
|
4
|
+
export { createEditTool } from './edit'
|
|
5
|
+
export { createGlobTool } from './glob'
|
|
6
|
+
export { createGrepTool } from './grep'
|
|
7
|
+
export { createListTool } from './list'
|
|
8
|
+
export { createJustBashReadTool } from './read'
|
|
9
|
+
export { createSkillToolFromVFS } from './skill'
|
|
10
|
+
export { createWebFetchTool } from './web-fetch'
|
|
11
|
+
export { createWebSearchTool, type JustBashWebSearchOptions } from './web-search'
|
|
12
|
+
export { createWriteTool } from './write'
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ListEntry } from '@humanlayer/agentlayer-core/interfaces'
|
|
2
|
+
import { ListTool } from '@humanlayer/agentlayer-core/interfaces'
|
|
3
|
+
import { LIST_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
4
|
+
import type { Bash } from 'just-bash'
|
|
5
|
+
export function createListTool(bash: Bash) {
|
|
6
|
+
return ListTool.define(
|
|
7
|
+
async (input) => {
|
|
8
|
+
const dirPath = input.path ?? '.'
|
|
9
|
+
|
|
10
|
+
// Use ls -1F: appends '/' to directories, nothing to files
|
|
11
|
+
const result = await bash.exec(`ls -1F "${dirPath}" 2>/dev/null`)
|
|
12
|
+
|
|
13
|
+
if (result.exitCode !== 0) {
|
|
14
|
+
throw new Error(`Failed to list directory ${dirPath}: ${result.stderr}`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ignorePatterns = new Set<string>(['node_modules', '.git', 'dist', 'build', ...(input.ignore ?? [])])
|
|
18
|
+
|
|
19
|
+
const entries: ListEntry[] = []
|
|
20
|
+
for (const raw of result.stdout.split('\n')) {
|
|
21
|
+
const line = raw.trim()
|
|
22
|
+
if (!line) continue
|
|
23
|
+
|
|
24
|
+
if (line.endsWith('/')) {
|
|
25
|
+
// Directory
|
|
26
|
+
const name = line.slice(0, -1)
|
|
27
|
+
if (ignorePatterns.has(name)) continue
|
|
28
|
+
entries.push({ name, type: 'directory' })
|
|
29
|
+
} else {
|
|
30
|
+
// File (strip any trailing decorator: *, @, |, =, >)
|
|
31
|
+
const name = line.replace(/[*@|=>]$/, '')
|
|
32
|
+
if (ignorePatterns.has(name)) continue
|
|
33
|
+
entries.push({ name, type: 'file' })
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return entries
|
|
38
|
+
},
|
|
39
|
+
{ description: LIST_DESCRIPTION },
|
|
40
|
+
)
|
|
41
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Lists files and directories in a given path.
|
|
2
|
+
|
|
3
|
+
- The path parameter must be absolute; omit it to use the current workspace directory.
|
|
4
|
+
- You can optionally provide an array of glob patterns to ignore with the ignore parameter.
|
|
5
|
+
- You should generally prefer the Glob and Grep tools if you know which directories to search.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ReadTool } from '@humanlayer/agentlayer-core/interfaces'
|
|
2
|
+
import { READ_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
3
|
+
import type { Bash } from 'just-bash'
|
|
4
|
+
|
|
5
|
+
export function createJustBashReadTool(bash: Bash) {
|
|
6
|
+
return ReadTool.define(
|
|
7
|
+
async (input) => {
|
|
8
|
+
const result = await bash.exec(`cat "${input.file_path}"`)
|
|
9
|
+
if (result.exitCode !== 0) {
|
|
10
|
+
throw new Error(`File not found: ${input.file_path}`)
|
|
11
|
+
}
|
|
12
|
+
return result.stdout
|
|
13
|
+
},
|
|
14
|
+
{ description: READ_DESCRIPTION },
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Read a file or directory from the local filesystem. If the path does not exist, an error is returned.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- The filePath parameter should be an absolute path.
|
|
5
|
+
- By default, this tool returns up to 2000 lines from the start of the file.
|
|
6
|
+
- The offset parameter is the line number to start from (1-indexed).
|
|
7
|
+
- To read later sections, call this tool again with a larger offset.
|
|
8
|
+
- Use the grep tool to find specific content in large files or files with long lines.
|
|
9
|
+
- If you are unsure of the correct file path, use the glob tool to look up filenames by glob pattern.
|
|
10
|
+
- Contents are returned with each line prefixed by its line number as `<line>: <content>`. For example, if a file has contents "foo\n", you will receive "1: foo\n". For directories, entries are returned one per line (without line numbers) with a trailing `/` for subdirectories.
|
|
11
|
+
- Any line longer than 2000 characters is truncated.
|
|
12
|
+
- Call this tool in parallel when you know there are multiple files you want to read.
|
|
13
|
+
- Avoid tiny repeated slices (30 line chunks). If you need more context, read a larger window.
|
|
14
|
+
- This tool can read image files and PDFs and return them as file attachments.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createSkillTool } from '@humanlayer/agentlayer-core'
|
|
2
|
+
import type { Skill } from '@humanlayer/agentlayer-core/interfaces'
|
|
3
|
+
import type { Bash } from 'just-bash'
|
|
4
|
+
|
|
5
|
+
function parseFrontmatterDescription(content: string): string | null {
|
|
6
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/)
|
|
7
|
+
if (!match) return null
|
|
8
|
+
const fmMatch = match[1]?.match(/description:\s*(.+)/)
|
|
9
|
+
return fmMatch?.[1]?.trim() ?? null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseFirstHeading(content: string): string | null {
|
|
13
|
+
const match = content.match(/^#\s+(.+)/m)
|
|
14
|
+
return match?.[1]?.trim() ?? null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function createSkillToolFromVFS(bash: Bash, opts: { dirs: string | string[]; skills?: Skill[] }) {
|
|
18
|
+
const directories = Array.isArray(opts.dirs) ? opts.dirs : [opts.dirs]
|
|
19
|
+
const resolved: Skill[] = []
|
|
20
|
+
|
|
21
|
+
for (const dir of directories) {
|
|
22
|
+
let lsResult: { stdout: string; exitCode: number }
|
|
23
|
+
try {
|
|
24
|
+
lsResult = await bash.exec(`ls "${dir}"/*.md 2>/dev/null`)
|
|
25
|
+
} catch {
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
if (lsResult.exitCode !== 0) continue
|
|
29
|
+
|
|
30
|
+
const files = lsResult.stdout.trim().split('\n').filter(Boolean)
|
|
31
|
+
for (const filePath of files) {
|
|
32
|
+
const catResult = await bash.exec(`cat "${filePath}"`)
|
|
33
|
+
if (catResult.exitCode !== 0) continue
|
|
34
|
+
|
|
35
|
+
const content = catResult.stdout
|
|
36
|
+
const name = filePath.split('/').pop()?.replace('.md', '') ?? filePath
|
|
37
|
+
const description = parseFrontmatterDescription(content) ?? parseFirstHeading(content) ?? name
|
|
38
|
+
|
|
39
|
+
resolved.push({ name, description, content })
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const mergedMap = new Map(resolved.map((s) => [s.name, s]))
|
|
44
|
+
for (const skill of opts.skills ?? []) {
|
|
45
|
+
mergedMap.set(skill.name, skill)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return createSkillTool({ skills: [...mergedMap.values()] })
|
|
49
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { WebFetchTool } from '@humanlayer/agentlayer-core'
|
|
2
|
+
import { WEB_FETCH_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
3
|
+
import type { Bash } from 'just-bash'
|
|
4
|
+
import TurndownService from 'turndown'
|
|
5
|
+
|
|
6
|
+
const MAX_RESPONSE_SIZE = 5 * 1024 * 1024 // 5MB in bytes
|
|
7
|
+
const MAX_TIMEOUT_MS = 120_000
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Strip HTML tags from a string to produce plain text.
|
|
11
|
+
*/
|
|
12
|
+
function stripHtmlTags(html: string): string {
|
|
13
|
+
return html
|
|
14
|
+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
15
|
+
.replace(/<style\b[^<]*(?:(?!<\/style>)<[^<]*)*<\/style>/gi, '')
|
|
16
|
+
.replace(/<[^>]+>/g, '')
|
|
17
|
+
.replace(/&/g, '&')
|
|
18
|
+
.replace(/</g, '<')
|
|
19
|
+
.replace(/>/g, '>')
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, "'")
|
|
22
|
+
.replace(/ /g, ' ')
|
|
23
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
24
|
+
.trim()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createWebFetchTool(bash: Bash) {
|
|
28
|
+
const turndown = new TurndownService({
|
|
29
|
+
headingStyle: 'atx',
|
|
30
|
+
codeBlockStyle: 'fenced',
|
|
31
|
+
})
|
|
32
|
+
turndown.remove(['script', 'style', 'meta', 'link', 'noscript', 'iframe'])
|
|
33
|
+
|
|
34
|
+
return WebFetchTool.define(
|
|
35
|
+
async (input) => {
|
|
36
|
+
if (!input.url.startsWith('http://') && !input.url.startsWith('https://')) {
|
|
37
|
+
throw new Error('URL must start with http:// or https://')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const timeoutSec = Math.min(input.timeout, MAX_TIMEOUT_MS) / 1000
|
|
41
|
+
const maxSizeBytes = MAX_RESPONSE_SIZE
|
|
42
|
+
|
|
43
|
+
// curl flags:
|
|
44
|
+
// -s: silent, -L: follow redirects
|
|
45
|
+
// --max-filesize: reject oversized responses
|
|
46
|
+
// --max-time: timeout
|
|
47
|
+
// -A: User-Agent
|
|
48
|
+
// -w '\n%{http_code}': append status code on last line
|
|
49
|
+
const result = await bash.exec(
|
|
50
|
+
`curl -sL --max-filesize ${maxSizeBytes} --max-time ${timeoutSec} ` +
|
|
51
|
+
`-A "Mozilla/5.0 (compatible; agent/1.0)" ` +
|
|
52
|
+
`-w '\\n%{http_code}' "${input.url}"`,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if (result.exitCode !== 0) {
|
|
56
|
+
// curl exit code 28 = timeout, 63 = file too large
|
|
57
|
+
if (result.exitCode === 28) {
|
|
58
|
+
throw new Error(`Request timed out after ${input.timeout}ms`)
|
|
59
|
+
}
|
|
60
|
+
if (result.exitCode === 63) {
|
|
61
|
+
throw new Error('Response too large (exceeds 5MB limit)')
|
|
62
|
+
}
|
|
63
|
+
throw new Error(`curl failed (exit ${result.exitCode}): ${result.stderr}`)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Split off the status code appended by -w
|
|
67
|
+
const lines = result.stdout.split('\n')
|
|
68
|
+
const statusLine = lines[lines.length - 1]?.trim() ?? ''
|
|
69
|
+
const statusCode = Number.parseInt(statusLine, 10)
|
|
70
|
+
const body = lines.slice(0, -1).join('\n')
|
|
71
|
+
|
|
72
|
+
if (!Number.isNaN(statusCode) && statusCode >= 400) {
|
|
73
|
+
throw new Error(`Request failed with status code: ${statusCode}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (input.format === 'html') {
|
|
77
|
+
return body
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const isHtml = body.trimStart().startsWith('<!') || body.trimStart().toLowerCase().startsWith('<html')
|
|
81
|
+
|
|
82
|
+
if (input.format === 'text') {
|
|
83
|
+
if (isHtml) {
|
|
84
|
+
return stripHtmlTags(body)
|
|
85
|
+
}
|
|
86
|
+
return body
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// format === 'markdown' (default)
|
|
90
|
+
if (isHtml) {
|
|
91
|
+
return turndown.turndown(body)
|
|
92
|
+
}
|
|
93
|
+
return body
|
|
94
|
+
},
|
|
95
|
+
{ description: WEB_FETCH_DESCRIPTION },
|
|
96
|
+
)
|
|
97
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Fetches content from a URL and returns it in the requested format.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- Provide a full URL starting with http:// or https://
|
|
5
|
+
- The format parameter controls output: "markdown" (default) converts HTML to readable markdown, "text" strips all HTML tags, "html" returns the raw HTML
|
|
6
|
+
- Default timeout is 30 seconds; maximum is 120 seconds
|
|
7
|
+
- Responses larger than 5MB are rejected
|
|
8
|
+
- Use markdown format for reading documentation, articles, or web pages
|
|
9
|
+
- Use html format only when you need the raw HTML structure
|
|
10
|
+
- Use text format when you need plain text without any markup
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { WebSearchInput, WebSearchResult } from '@humanlayer/agentlayer-core'
|
|
2
|
+
import { WebSearchTool } from '@humanlayer/agentlayer-core'
|
|
3
|
+
import { WEB_SEARCH_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
4
|
+
import type { Bash } from 'just-bash'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_SEC = 25
|
|
7
|
+
|
|
8
|
+
export interface JustBashWebSearchOptions {
|
|
9
|
+
exaApiKey: string
|
|
10
|
+
timeoutSec?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createWebSearchTool(bash: Bash, opts: JustBashWebSearchOptions) {
|
|
14
|
+
const timeoutSec = opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC
|
|
15
|
+
|
|
16
|
+
return WebSearchTool.define(
|
|
17
|
+
async (input: WebSearchInput): Promise<WebSearchResult> => {
|
|
18
|
+
const payload = JSON.stringify({
|
|
19
|
+
query: input.query,
|
|
20
|
+
numResults: input.numResults,
|
|
21
|
+
contents: { text: { maxCharacters: 500 } },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
// Escape single quotes in payload for shell safety
|
|
25
|
+
const escapedPayload = payload.replace(/'/g, "'\\''")
|
|
26
|
+
|
|
27
|
+
const result = await bash.exec(
|
|
28
|
+
`curl -s --max-time ${timeoutSec} ` +
|
|
29
|
+
`-H "Content-Type: application/json" ` +
|
|
30
|
+
`-H "x-api-key: ${opts.exaApiKey}" ` +
|
|
31
|
+
`-d '${escapedPayload}' ` +
|
|
32
|
+
`https://api.exa.ai/search`,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (result.exitCode !== 0) {
|
|
36
|
+
if (result.exitCode === 28) {
|
|
37
|
+
throw new Error('Search request timed out')
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`curl failed (exit ${result.exitCode}): ${result.stderr}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let data: { results?: Array<{ title?: string; url?: string; text?: string; snippet?: string }> }
|
|
43
|
+
try {
|
|
44
|
+
data = JSON.parse(result.stdout)
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error(`Failed to parse search response: ${result.stdout.slice(0, 200)}`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const results = (data.results ?? []).map((r) => ({
|
|
50
|
+
title: r.title ?? '',
|
|
51
|
+
url: r.url ?? '',
|
|
52
|
+
snippet: r.text ?? r.snippet ?? '',
|
|
53
|
+
}))
|
|
54
|
+
|
|
55
|
+
return { results }
|
|
56
|
+
},
|
|
57
|
+
{ description: WEB_SEARCH_DESCRIPTION },
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WriteTool } from '@humanlayer/agentlayer-core'
|
|
2
|
+
import { WRITE_DESCRIPTION } from '@humanlayer/agentlayer-core/prompts'
|
|
3
|
+
import type { Bash } from 'just-bash'
|
|
4
|
+
|
|
5
|
+
export function createWriteTool(bash: Bash) {
|
|
6
|
+
return WriteTool.define(
|
|
7
|
+
async (input) => {
|
|
8
|
+
// Ensure parent directory exists
|
|
9
|
+
const dirResult = await bash.exec(`mkdir -p "$(dirname "${input.file_path}")"`)
|
|
10
|
+
if (dirResult.exitCode !== 0) {
|
|
11
|
+
throw new Error(`Failed to create parent directory for ${input.file_path}: ${dirResult.stderr}`)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Write content using a heredoc to handle special characters safely
|
|
15
|
+
// We use a unique delimiter to avoid conflicts with content
|
|
16
|
+
const DELIM = 'WRITEOF_8f3a2b1c'
|
|
17
|
+
const writeResult = await bash.exec(`cat > "${input.file_path}" <<'${DELIM}'\n${input.content}\n${DELIM}`)
|
|
18
|
+
if (writeResult.exitCode !== 0) {
|
|
19
|
+
throw new Error(`Failed to write file ${input.file_path}: ${writeResult.stderr}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return `Successfully wrote to ${input.file_path}`
|
|
23
|
+
},
|
|
24
|
+
{ description: WRITE_DESCRIPTION },
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Writes a file to the local filesystem.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
- This tool will overwrite the existing file if there is one at the provided path.
|
|
5
|
+
- If this is an existing file, you MUST use the Read tool first to read the file's contents. This tool will fail if you did not read the file first.
|
|
6
|
+
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
7
|
+
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
|
|
8
|
+
- Only use emojis if the user explicitly requests it. Avoid writing emojis to files unless asked.
|