tiger-agent 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.
@@ -0,0 +1,169 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execFile } = require('child_process');
4
+ const { promisify } = require('util');
5
+
6
+ const execFileAsync = promisify(execFile);
7
+ const localClawhubBin = path.resolve(process.cwd(), 'node_modules', '.bin', 'clawhub');
8
+
9
+ function listSkills(baseDir) {
10
+ if (!fs.existsSync(baseDir)) return [];
11
+ return fs
12
+ .readdirSync(baseDir, { withFileTypes: true })
13
+ .filter((d) => d.isDirectory())
14
+ .map((d) => d.name)
15
+ .sort();
16
+ }
17
+
18
+ function loadSkill(baseDir, skillName) {
19
+ const skillDir = path.join(baseDir, skillName);
20
+ const skillFile = path.join(skillDir, 'SKILL.md');
21
+ if (!fs.existsSync(skillFile)) {
22
+ throw new Error(`Skill not found: ${skillName}`);
23
+ }
24
+ return fs.readFileSync(skillFile, 'utf8');
25
+ }
26
+
27
+ function isValidSlug(slug) {
28
+ return /^[a-z0-9][a-z0-9-]*$/.test(String(slug || ''));
29
+ }
30
+
31
+ async function ensureClawhubCli() {
32
+ const candidates = ['clawhub'];
33
+ if (fs.existsSync(localClawhubBin)) {
34
+ candidates.unshift(localClawhubBin);
35
+ }
36
+
37
+ for (const bin of candidates) {
38
+ try {
39
+ const { stdout, stderr } = await execFileAsync(bin, ['--cli-version'], {
40
+ timeout: 10000,
41
+ maxBuffer: 256 * 1024
42
+ });
43
+ return { ok: true, bin, version: String(stdout || stderr || '').trim() };
44
+ } catch (err) {
45
+ // try next candidate
46
+ }
47
+ }
48
+
49
+ return {
50
+ ok: false,
51
+ error:
52
+ 'clawhub CLI is not available. Install it with: npm i -g clawhub or npm i clawhub in this project.'
53
+ };
54
+ }
55
+
56
+ async function runClawhub(argv, opts = {}) {
57
+ const cli = await ensureClawhubCli();
58
+ if (!cli.ok) return cli;
59
+ try {
60
+ const { stdout, stderr } = await execFileAsync(cli.bin, argv, {
61
+ timeout: Number(opts.timeout || 30000),
62
+ maxBuffer: Number(opts.maxBuffer || 1024 * 1024)
63
+ });
64
+ return {
65
+ ok: true,
66
+ bin: cli.bin,
67
+ version: cli.version,
68
+ stdout: String(stdout || '').trim(),
69
+ stderr: String(stderr || '').trim()
70
+ };
71
+ } catch (err) {
72
+ return {
73
+ ok: false,
74
+ bin: cli.bin,
75
+ version: cli.version,
76
+ error: err.message,
77
+ stdout: String(err.stdout || '').trim(),
78
+ stderr: String(err.stderr || '').trim()
79
+ };
80
+ }
81
+ }
82
+
83
+ async function clawhubSearch(args = {}) {
84
+ const query = String(args.query || '').trim();
85
+ if (!query) return { ok: false, error: 'Missing query.' };
86
+
87
+ const limit = Math.max(1, Math.min(50, Number(args.limit || 10)));
88
+ const workdir = path.resolve(String(args.workdir || process.cwd()));
89
+ const dir = String(args.dir || 'skills').trim() || 'skills';
90
+
91
+ const argv = ['search', query, '--limit', String(limit), '--no-input', '--workdir', workdir, '--dir', dir];
92
+ const res = await runClawhub(argv, {
93
+ timeout: Number(args.timeout_ms || 30000),
94
+ maxBuffer: 1024 * 1024
95
+ });
96
+ if (res.ok) {
97
+ return {
98
+ ok: true,
99
+ bin: res.bin,
100
+ query,
101
+ limit,
102
+ workdir,
103
+ dir,
104
+ output: res.stdout,
105
+ warning: res.stderr
106
+ };
107
+ }
108
+ return {
109
+ ok: false,
110
+ bin: res.bin,
111
+ query,
112
+ workdir,
113
+ dir,
114
+ error: res.error,
115
+ output: res.stdout || '',
116
+ warning: res.stderr || ''
117
+ };
118
+ }
119
+
120
+ async function clawhubInstall(args = {}) {
121
+ const slug = String(args.slug || '').trim();
122
+ if (!slug) return { ok: false, error: 'Missing slug.' };
123
+ if (!isValidSlug(slug)) {
124
+ return { ok: false, error: 'Invalid slug format. Use lowercase letters, numbers, and hyphens only.' };
125
+ }
126
+
127
+ const workdir = path.resolve(String(args.workdir || process.cwd()));
128
+ const dir = String(args.dir || 'skills').trim() || 'skills';
129
+ const version = String(args.version || '').trim();
130
+ const force = Boolean(args.force);
131
+
132
+ const argv = ['install', slug, '--no-input', '--workdir', workdir, '--dir', dir];
133
+ if (version) argv.push('--version', version);
134
+ if (force) argv.push('--force');
135
+
136
+ const res = await runClawhub(argv, {
137
+ timeout: Number(args.timeout_ms || 120000),
138
+ maxBuffer: 1024 * 1024
139
+ });
140
+ if (res.ok) {
141
+ const skillPath = path.join(workdir, dir, slug, 'SKILL.md');
142
+ return {
143
+ ok: true,
144
+ bin: res.bin,
145
+ slug,
146
+ version: version || 'latest',
147
+ installed_path: skillPath,
148
+ skill_exists: fs.existsSync(skillPath),
149
+ output: res.stdout,
150
+ warning: res.stderr
151
+ };
152
+ }
153
+ return {
154
+ ok: false,
155
+ bin: res.bin,
156
+ slug,
157
+ version: version || 'latest',
158
+ error: res.error,
159
+ output: res.stdout || '',
160
+ warning: res.stderr || ''
161
+ };
162
+ }
163
+
164
+ module.exports = {
165
+ listSkills,
166
+ loadSkill,
167
+ clawhubSearch,
168
+ clawhubInstall
169
+ };
@@ -0,0 +1,39 @@
1
+ const { chatCompletion } = require('../llmClient');
2
+
3
+ async function runSubAgent(task, contextText) {
4
+ const system = [
5
+ 'You are a focused sub-agent.',
6
+ 'Complete only the assigned task.',
7
+ 'Return concise findings, key facts, and action items.'
8
+ ].join(' ');
9
+
10
+ const message = [
11
+ `Task: ${task}`,
12
+ contextText ? `Context:\n${contextText}` : ''
13
+ ]
14
+ .filter(Boolean)
15
+ .join('\n\n');
16
+
17
+ const result = await chatCompletion([
18
+ { role: 'system', content: system },
19
+ { role: 'user', content: message }
20
+ ]);
21
+
22
+ return result.content || '';
23
+ }
24
+
25
+ async function runSubAgentBatch(tasks, contextText) {
26
+ const safeTasks = Array.isArray(tasks) ? tasks : [];
27
+ const out = await Promise.all(
28
+ safeTasks.map(async (task) => {
29
+ const answer = await runSubAgent(task, contextText);
30
+ return { task, answer };
31
+ })
32
+ );
33
+ return out;
34
+ }
35
+
36
+ module.exports = {
37
+ runSubAgent,
38
+ runSubAgentBatch
39
+ };
@@ -0,0 +1,291 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { exec } = require('child_process');
4
+ const { promisify } = require('util');
5
+ const { allowShell } = require('../config');
6
+ const { listSkills, loadSkill, clawhubSearch, clawhubInstall } = require('./skills');
7
+ const { runSubAgentBatch } = require('./subAgent');
8
+
9
+ const execAsync = promisify(exec);
10
+ const skillsDir = path.resolve('./skills');
11
+
12
+ function toAbsolutePath(inputPath) {
13
+ return path.resolve(String(inputPath || '.'));
14
+ }
15
+
16
+ function listFiles(args = {}) {
17
+ const target = toAbsolutePath(args.path || '.');
18
+ const recursive = Boolean(args.recursive);
19
+ const limit = Number(args.limit || 200);
20
+ const out = [];
21
+
22
+ function walk(dir) {
23
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
24
+ for (const entry of entries) {
25
+ const full = path.join(dir, entry.name);
26
+ out.push({
27
+ path: full,
28
+ type: entry.isDirectory() ? 'dir' : 'file'
29
+ });
30
+ if (out.length >= limit) return;
31
+ if (recursive && entry.isDirectory()) {
32
+ walk(full);
33
+ if (out.length >= limit) return;
34
+ }
35
+ }
36
+ }
37
+
38
+ walk(target);
39
+ return { root: target, items: out, truncated: out.length >= limit };
40
+ }
41
+
42
+ function readFile(args = {}) {
43
+ const target = toAbsolutePath(args.path);
44
+ const maxChars = Number(args.max_chars || 16000);
45
+ const content = fs.readFileSync(target, 'utf8');
46
+ return {
47
+ path: target,
48
+ content: content.slice(0, maxChars),
49
+ truncated: content.length > maxChars
50
+ };
51
+ }
52
+
53
+ function writeFile(args = {}) {
54
+ const target = toAbsolutePath(args.path);
55
+ const append = Boolean(args.append);
56
+ const content = String(args.content || '');
57
+ fs.mkdirSync(path.dirname(target), { recursive: true });
58
+ if (append) {
59
+ fs.appendFileSync(target, content, 'utf8');
60
+ } else {
61
+ fs.writeFileSync(target, content, 'utf8');
62
+ }
63
+ return { path: target, bytes: Buffer.byteLength(content, 'utf8'), append };
64
+ }
65
+
66
+ async function runShell(args = {}) {
67
+ if (!allowShell) {
68
+ return { ok: false, error: 'Shell tool disabled. Set ALLOW_SHELL=true to enable.' };
69
+ }
70
+ const command = String(args.command || '').trim();
71
+ if (!command) {
72
+ return { ok: false, error: 'Missing command.' };
73
+ }
74
+ const cwd = toAbsolutePath(args.cwd || process.cwd());
75
+ try {
76
+ const { stdout, stderr } = await execAsync(command, {
77
+ cwd,
78
+ timeout: Number(args.timeout_ms || 15000),
79
+ maxBuffer: 1024 * 1024
80
+ });
81
+ return { ok: true, cwd, stdout, stderr };
82
+ } catch (err) {
83
+ return {
84
+ ok: false,
85
+ cwd,
86
+ error: err.message,
87
+ stdout: err.stdout || '',
88
+ stderr: err.stderr || ''
89
+ };
90
+ }
91
+ }
92
+
93
+ async function runSubAgentsTool(args = {}) {
94
+ const tasks = Array.isArray(args.tasks) ? args.tasks.map(String) : [];
95
+ const context = String(args.context || '');
96
+ const results = await runSubAgentBatch(tasks, context);
97
+ return { count: results.length, results };
98
+ }
99
+
100
+ function listSkillsTool() {
101
+ const skills = listSkills(skillsDir);
102
+ return { skills, skills_dir: skillsDir };
103
+ }
104
+
105
+ function loadSkillTool(args = {}) {
106
+ const skill = String(args.skill || '').trim();
107
+ if (!skill) return { ok: false, error: 'Missing skill name.' };
108
+ const content = loadSkill(skillsDir, skill);
109
+ return { ok: true, skill, content };
110
+ }
111
+
112
+ async function clawhubSearchTool(args = {}) {
113
+ return clawhubSearch({
114
+ query: args.query,
115
+ limit: args.limit,
116
+ workdir: args.workdir || process.cwd(),
117
+ dir: args.dir || 'skills',
118
+ timeout_ms: args.timeout_ms
119
+ });
120
+ }
121
+
122
+ async function clawhubInstallTool(args = {}) {
123
+ return clawhubInstall({
124
+ slug: args.slug,
125
+ version: args.version,
126
+ force: args.force,
127
+ workdir: args.workdir || process.cwd(),
128
+ dir: args.dir || 'skills',
129
+ timeout_ms: args.timeout_ms
130
+ });
131
+ }
132
+
133
+ const tools = [
134
+ {
135
+ type: 'function',
136
+ function: {
137
+ name: 'list_files',
138
+ description: 'List files/directories from a path. Supports recursive mode.',
139
+ parameters: {
140
+ type: 'object',
141
+ properties: {
142
+ path: { type: 'string' },
143
+ recursive: { type: 'boolean' },
144
+ limit: { type: 'integer' }
145
+ }
146
+ }
147
+ }
148
+ },
149
+ {
150
+ type: 'function',
151
+ function: {
152
+ name: 'read_file',
153
+ description: 'Read text file content from disk.',
154
+ parameters: {
155
+ type: 'object',
156
+ properties: {
157
+ path: { type: 'string' },
158
+ max_chars: { type: 'integer' }
159
+ },
160
+ required: ['path']
161
+ }
162
+ }
163
+ },
164
+ {
165
+ type: 'function',
166
+ function: {
167
+ name: 'write_file',
168
+ description: 'Write or append text content to a file on disk.',
169
+ parameters: {
170
+ type: 'object',
171
+ properties: {
172
+ path: { type: 'string' },
173
+ content: { type: 'string' },
174
+ append: { type: 'boolean' }
175
+ },
176
+ required: ['path', 'content']
177
+ }
178
+ }
179
+ },
180
+ {
181
+ type: 'function',
182
+ function: {
183
+ name: 'run_shell',
184
+ description: 'Execute shell command on the computer if enabled by ALLOW_SHELL=true.',
185
+ parameters: {
186
+ type: 'object',
187
+ properties: {
188
+ command: { type: 'string' },
189
+ cwd: { type: 'string' },
190
+ timeout_ms: { type: 'integer' }
191
+ },
192
+ required: ['command']
193
+ }
194
+ }
195
+ },
196
+ {
197
+ type: 'function',
198
+ function: {
199
+ name: 'list_skills',
200
+ description: 'List available local skills.',
201
+ parameters: { type: 'object', properties: {} }
202
+ }
203
+ },
204
+ {
205
+ type: 'function',
206
+ function: {
207
+ name: 'load_skill',
208
+ description: 'Load full SKILL.md content for a specific local skill.',
209
+ parameters: {
210
+ type: 'object',
211
+ properties: {
212
+ skill: { type: 'string' }
213
+ },
214
+ required: ['skill']
215
+ }
216
+ }
217
+ },
218
+ {
219
+ type: 'function',
220
+ function: {
221
+ name: 'clawhub_search',
222
+ description: 'Search ClawHub skills using the clawhub CLI.',
223
+ parameters: {
224
+ type: 'object',
225
+ properties: {
226
+ query: { type: 'string' },
227
+ limit: { type: 'integer' },
228
+ workdir: { type: 'string' },
229
+ dir: { type: 'string' },
230
+ timeout_ms: { type: 'integer' }
231
+ },
232
+ required: ['query']
233
+ }
234
+ }
235
+ },
236
+ {
237
+ type: 'function',
238
+ function: {
239
+ name: 'clawhub_install',
240
+ description: 'Install a ClawHub skill by slug into the local skills directory.',
241
+ parameters: {
242
+ type: 'object',
243
+ properties: {
244
+ slug: { type: 'string' },
245
+ version: { type: 'string' },
246
+ force: { type: 'boolean' },
247
+ workdir: { type: 'string' },
248
+ dir: { type: 'string' },
249
+ timeout_ms: { type: 'integer' }
250
+ },
251
+ required: ['slug']
252
+ }
253
+ }
254
+ },
255
+ {
256
+ type: 'function',
257
+ function: {
258
+ name: 'run_sub_agents',
259
+ description: 'Run multiple focused sub-agents and return all outputs for orchestration.',
260
+ parameters: {
261
+ type: 'object',
262
+ properties: {
263
+ tasks: {
264
+ type: 'array',
265
+ items: { type: 'string' }
266
+ },
267
+ context: { type: 'string' }
268
+ },
269
+ required: ['tasks']
270
+ }
271
+ }
272
+ }
273
+ ];
274
+
275
+ async function callTool(name, args) {
276
+ if (name === 'list_files') return listFiles(args);
277
+ if (name === 'read_file') return readFile(args);
278
+ if (name === 'write_file') return writeFile(args);
279
+ if (name === 'run_shell') return runShell(args);
280
+ if (name === 'list_skills') return listSkillsTool();
281
+ if (name === 'load_skill') return loadSkillTool(args);
282
+ if (name === 'clawhub_search') return clawhubSearchTool(args);
283
+ if (name === 'clawhub_install') return clawhubInstallTool(args);
284
+ if (name === 'run_sub_agents') return runSubAgentsTool(args);
285
+ return { ok: false, error: `Unknown tool: ${name}` };
286
+ }
287
+
288
+ module.exports = {
289
+ tools,
290
+ callTool
291
+ };