@agi-cli/sdk 0.1.157 → 0.1.158

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.157",
3
+ "version": "0.1.158",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "nitishxyz",
6
6
  "license": "MIT",
@@ -3,6 +3,7 @@ import type { PtyOptions } from './bun-pty.ts';
3
3
  import { spawn as spawnPty } from './bun-pty.ts';
4
4
  import { Terminal } from './terminal.ts';
5
5
  import { logger } from '../utils/logger.ts';
6
+ import { getAugmentedPath } from '../tools/bin-manager.ts';
6
7
 
7
8
  const MAX_TERMINALS = 10;
8
9
  const CLEANUP_DELAY_MS = 5 * 60 * 1000;
@@ -41,7 +42,10 @@ export class TerminalManager {
41
42
  cols: 80,
42
43
  rows: 30,
43
44
  cwd: options.cwd,
44
- env: process.env as Record<string, string>,
45
+ env: { ...process.env, PATH: getAugmentedPath() } as Record<
46
+ string,
47
+ string
48
+ >,
45
49
  };
46
50
 
47
51
  const pty = spawnPty(options.command, options.args || [], ptyOptions);
@@ -0,0 +1,197 @@
1
+ import { join } from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+ import { spawn } from 'node:child_process';
4
+
5
+ const AGI_BIN_DIR_NAME = 'bin';
6
+
7
+ let cachedBinDir: string | null = null;
8
+ const resolvedPaths = new Map<string, string>();
9
+
10
+ function getConfigHome(): string {
11
+ const cfgHome = process.env.XDG_CONFIG_HOME;
12
+ if (cfgHome?.trim()) return cfgHome.replace(/\\/g, '/');
13
+ const home = process.env.HOME || process.env.USERPROFILE || '';
14
+ return join(home, '.config');
15
+ }
16
+
17
+ export function getAgiBinDir(): string {
18
+ if (cachedBinDir) return cachedBinDir;
19
+ cachedBinDir = join(getConfigHome(), 'agi', AGI_BIN_DIR_NAME);
20
+ return cachedBinDir;
21
+ }
22
+
23
+ function getPlatformKey(): string {
24
+ const platform = process.platform;
25
+ const arch = process.arch;
26
+ const os =
27
+ platform === 'darwin'
28
+ ? 'darwin'
29
+ : platform === 'win32'
30
+ ? 'windows'
31
+ : 'linux';
32
+ const cpu = arch === 'arm64' ? 'arm64' : 'x64';
33
+ return `${os}-${cpu}`;
34
+ }
35
+
36
+ function getBinaryFileName(name: string): string {
37
+ if (process.platform === 'win32') return `${name}.exe`;
38
+ return name;
39
+ }
40
+
41
+ async function ensureDir(dir: string): Promise<void> {
42
+ await fs.mkdir(dir, { recursive: true });
43
+ }
44
+
45
+ async function fileExists(p: string): Promise<boolean> {
46
+ try {
47
+ await fs.access(p);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ async function isExecutable(p: string): Promise<boolean> {
55
+ try {
56
+ await fs.access(p, 0o1);
57
+ return true;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ async function makeExecutable(p: string): Promise<void> {
64
+ if (process.platform === 'win32') return;
65
+ try {
66
+ await fs.chmod(p, 0o755);
67
+ } catch {}
68
+ }
69
+
70
+ async function whichBinary(name: string): Promise<string | null> {
71
+ const cmd = process.platform === 'win32' ? 'where' : 'which';
72
+ return new Promise((resolve) => {
73
+ const proc = spawn(cmd, [name], { stdio: ['ignore', 'pipe', 'ignore'] });
74
+ let stdout = '';
75
+ proc.stdout.on('data', (d) => {
76
+ stdout += d.toString();
77
+ });
78
+ proc.on('close', (code) => {
79
+ if (code === 0 && stdout.trim()) resolve(stdout.trim().split('\n')[0]);
80
+ else resolve(null);
81
+ });
82
+ proc.on('error', () => resolve(null));
83
+ });
84
+ }
85
+
86
+ function getVendorSearchPaths(binaryName: string): string[] {
87
+ const platformKey = getPlatformKey();
88
+ const paths: string[] = [];
89
+
90
+ const tauriResource = process.env.TAURI_RESOURCE_DIR;
91
+ if (tauriResource) {
92
+ paths.push(join(tauriResource, 'vendor', 'bin', platformKey, binaryName));
93
+ paths.push(join(tauriResource, 'vendor', 'bin', binaryName));
94
+ }
95
+
96
+ try {
97
+ const exePath = process.execPath;
98
+ if (exePath) {
99
+ const exeDir = join(exePath, '..');
100
+ paths.push(join(exeDir, 'vendor', 'bin', platformKey, binaryName));
101
+ paths.push(
102
+ join(
103
+ exeDir,
104
+ '..',
105
+ 'Resources',
106
+ 'vendor',
107
+ 'bin',
108
+ platformKey,
109
+ binaryName,
110
+ ),
111
+ );
112
+ }
113
+ } catch {}
114
+
115
+ if (process.env.CARGO_MANIFEST_DIR) {
116
+ paths.push(
117
+ join(
118
+ process.env.CARGO_MANIFEST_DIR,
119
+ 'resources',
120
+ 'vendor',
121
+ 'bin',
122
+ platformKey,
123
+ binaryName,
124
+ ),
125
+ );
126
+ }
127
+
128
+ const cwd = process.cwd();
129
+ paths.push(join(cwd, 'vendor', 'bin', platformKey, binaryName));
130
+
131
+ return paths;
132
+ }
133
+
134
+ async function extractFromVendor(name: string): Promise<string | null> {
135
+ const binaryName = getBinaryFileName(name);
136
+ const binDir = getAgiBinDir();
137
+ const targetPath = join(binDir, binaryName);
138
+
139
+ if ((await fileExists(targetPath)) && (await isExecutable(targetPath))) {
140
+ return targetPath;
141
+ }
142
+
143
+ const searchPaths = getVendorSearchPaths(binaryName);
144
+
145
+ for (const src of searchPaths) {
146
+ if (await fileExists(src)) {
147
+ await ensureDir(binDir);
148
+ await fs.copyFile(src, targetPath);
149
+ await makeExecutable(targetPath);
150
+ return targetPath;
151
+ }
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ export async function resolveBinary(name: string): Promise<string> {
158
+ const cached = resolvedPaths.get(name);
159
+ if (cached) return cached;
160
+
161
+ const binaryName = getBinaryFileName(name);
162
+ const binDir = getAgiBinDir();
163
+ const installedPath = join(binDir, binaryName);
164
+ if (
165
+ (await fileExists(installedPath)) &&
166
+ (await isExecutable(installedPath))
167
+ ) {
168
+ resolvedPaths.set(name, installedPath);
169
+ return installedPath;
170
+ }
171
+
172
+ const vendorPath = await extractFromVendor(name);
173
+ if (vendorPath) {
174
+ resolvedPaths.set(name, vendorPath);
175
+ return vendorPath;
176
+ }
177
+
178
+ const systemPath = await whichBinary(binaryName);
179
+ if (systemPath) {
180
+ resolvedPaths.set(name, systemPath);
181
+ return systemPath;
182
+ }
183
+
184
+ return binaryName;
185
+ }
186
+
187
+ export function clearBinaryCache(): void {
188
+ resolvedPaths.clear();
189
+ }
190
+
191
+ export function getAugmentedPath(): string {
192
+ const binDir = getAgiBinDir();
193
+ const current = process.env.PATH || '';
194
+ const commonPaths = ['/opt/homebrew/bin', '/usr/local/bin'];
195
+ const parts = [binDir, ...commonPaths, current];
196
+ return parts.join(process.platform === 'win32' ? ';' : ':');
197
+ }
@@ -3,6 +3,7 @@ import { z } from 'zod/v3';
3
3
  import { spawn } from 'node:child_process';
4
4
  import DESCRIPTION from './bash.txt' with { type: 'text' };
5
5
  import { createToolError, type ToolResponse } from '../error.ts';
6
+ import { getAugmentedPath } from '../bin-manager.ts';
6
7
 
7
8
  function normalizePath(p: string) {
8
9
  const parts = p.replace(/\\/g, '/').split('/');
@@ -73,6 +74,7 @@ export function buildBashTool(projectRoot: string): {
73
74
  cwd: absCwd,
74
75
  shell: true,
75
76
  stdio: ['ignore', 'pipe', 'pipe'],
77
+ env: { ...process.env, PATH: getAugmentedPath() },
76
78
  });
77
79
 
78
80
  let stdout = '';
@@ -1,16 +1,11 @@
1
1
  import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
- import { exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
3
+ import { promises as fs } from 'node:fs';
5
4
  import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
6
5
  import DESCRIPTION from './ls.txt' with { type: 'text' };
7
6
  import { toIgnoredBasenames } from '../ignore.ts';
8
7
  import { createToolError, type ToolResponse } from '../../error.ts';
9
8
 
10
- const execAsync = promisify(exec);
11
-
12
- // description imported above
13
-
14
9
  export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
15
10
  const ls = tool({
16
11
  description: DESCRIPTION,
@@ -45,32 +40,29 @@ export function buildLsTool(projectRoot: string): { name: string; tool: Tool } {
45
40
  const ignored = toIgnoredBasenames(ignore);
46
41
 
47
42
  try {
48
- const { stdout } = await execAsync('ls -1p', {
49
- cwd: abs,
50
- maxBuffer: 10 * 1024 * 1024,
51
- });
52
- const entries = stdout
53
- .split('\n')
54
- .map((line) => line.trim())
55
- .filter((line) => line.length > 0 && !line.startsWith('.'))
56
- .map((line) => ({
57
- name: line.replace(/\/$/, ''),
58
- type: line.endsWith('/') ? 'dir' : 'file',
43
+ const dirents = await fs.readdir(abs, { withFileTypes: true });
44
+ const entries = dirents
45
+ .filter((d) => !String(d.name).startsWith('.'))
46
+ .map((d) => ({
47
+ name: String(d.name),
48
+ type: d.isDirectory() ? 'dir' : 'file',
59
49
  }))
60
- .filter(
61
- (entry) => !(entry.type === 'dir' && ignored.has(entry.name)),
62
- );
50
+ .filter((entry) => !(entry.type === 'dir' && ignored.has(entry.name)))
51
+ .sort((a, b) => a.name.localeCompare(b.name));
63
52
  return { ok: true, path: req, entries };
64
53
  } catch (error: unknown) {
65
- const err = error as { stderr?: string; stdout?: string };
66
- const message = (err.stderr || err.stdout || 'ls failed').trim();
54
+ const err = error as { code?: string; message?: string };
55
+ const message = err.message || 'ls failed';
67
56
  return createToolError(
68
57
  `ls failed for ${req}: ${message}`,
69
- 'execution',
58
+ err.code === 'ENOENT' ? 'not_found' : 'execution',
70
59
  {
71
60
  parameter: 'path',
72
61
  value: req,
73
- suggestion: 'Check if the directory exists and is accessible',
62
+ suggestion:
63
+ err.code === 'ENOENT'
64
+ ? 'Check if the directory exists'
65
+ : 'Check if the directory exists and is accessible',
74
66
  },
75
67
  );
76
68
  }
@@ -1,15 +1,72 @@
1
1
  import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
- import { exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
3
+ import { promises as fs } from 'node:fs';
4
+ import { join } from 'node:path';
5
5
  import { expandTilde, isAbsoluteLike, resolveSafePath } from './util.ts';
6
6
  import DESCRIPTION from './tree.txt' with { type: 'text' };
7
7
  import { toIgnoredBasenames } from '../ignore.ts';
8
8
  import { createToolError, type ToolResponse } from '../../error.ts';
9
9
 
10
- const execAsync = promisify(exec);
10
+ async function walkTree(
11
+ dir: string,
12
+ ignored: Set<string>,
13
+ maxDepth: number | null,
14
+ currentDepth: number,
15
+ prefix: string,
16
+ ): Promise<{ lines: string[]; dirs: number; files: number }> {
17
+ let dirs = 0;
18
+ let files = 0;
19
+ const lines: string[] = [];
11
20
 
12
- // description imported above
21
+ if (maxDepth !== null && currentDepth >= maxDepth)
22
+ return { lines, dirs, files };
23
+
24
+ try {
25
+ const rawEntries = await fs.readdir(dir, { withFileTypes: true });
26
+ const entries = rawEntries.map((e) => ({
27
+ name: String(e.name),
28
+ isDir: e.isDirectory(),
29
+ }));
30
+
31
+ const filtered = entries
32
+ .filter((e) => !e.name.startsWith('.'))
33
+ .filter((e) => !(e.isDir && ignored.has(e.name)))
34
+ .sort((a, b) => {
35
+ if (a.isDir && !b.isDir) return -1;
36
+ if (!a.isDir && b.isDir) return 1;
37
+ return a.name.localeCompare(b.name);
38
+ });
39
+
40
+ for (let i = 0; i < filtered.length; i++) {
41
+ const entry = filtered[i];
42
+ const isLast = i === filtered.length - 1;
43
+ const connector = isLast ? '└── ' : '├── ';
44
+ const childPrefix = isLast ? ' ' : '│ ';
45
+
46
+ if (entry.isDir) {
47
+ dirs++;
48
+ lines.push(`${prefix}${connector}${entry.name}`);
49
+ const sub = await walkTree(
50
+ join(dir, entry.name),
51
+ ignored,
52
+ maxDepth,
53
+ currentDepth + 1,
54
+ `${prefix}${childPrefix}`,
55
+ );
56
+ lines.push(...sub.lines);
57
+ dirs += sub.dirs;
58
+ files += sub.files;
59
+ } else {
60
+ files++;
61
+ lines.push(`${prefix}${connector}${entry.name}`);
62
+ }
63
+ }
64
+ } catch {
65
+ return { lines, dirs, files };
66
+ }
67
+
68
+ return { lines, dirs, files };
69
+ }
13
70
 
14
71
  export function buildTreeTool(projectRoot: string): {
15
72
  name: string;
@@ -48,32 +105,35 @@ export function buildTreeTool(projectRoot: string): {
48
105
  : resolveSafePath(projectRoot, req || '.');
49
106
  const ignored = toIgnoredBasenames(ignore);
50
107
 
51
- let cmd = 'tree';
52
- if (typeof depth === 'number') cmd += ` -L ${depth}`;
53
- if (ignored.size) {
54
- const pattern = Array.from(ignored).join('|');
55
- cmd += ` -I '${pattern.replace(/'/g, "'\\''")}'`;
108
+ try {
109
+ await fs.access(start);
110
+ } catch {
111
+ return createToolError(
112
+ `tree failed for ${req}: directory not found`,
113
+ 'not_found',
114
+ {
115
+ parameter: 'path',
116
+ value: req,
117
+ suggestion: 'Check if the directory exists',
118
+ },
119
+ );
56
120
  }
57
- cmd += ' .';
58
121
 
59
122
  try {
60
- const { stdout } = await execAsync(cmd, {
61
- cwd: start,
62
- maxBuffer: 10 * 1024 * 1024,
63
- });
64
- const output = stdout.trimEnd();
123
+ const result = await walkTree(start, ignored, depth ?? null, 0, '');
124
+ const header = '.';
125
+ const summary = `\n${result.dirs} director${result.dirs === 1 ? 'y' : 'ies'}, ${result.files} file${result.files === 1 ? '' : 's'}`;
126
+ const output = [header, ...result.lines, summary].join('\n');
65
127
  return { ok: true, path: req, depth: depth ?? null, tree: output };
66
128
  } catch (error: unknown) {
67
- const err = error as { stderr?: string; stdout?: string };
68
- const message = (err.stderr || err.stdout || 'tree failed').trim();
129
+ const err = error as { message?: string };
69
130
  return createToolError(
70
- `tree failed for ${req}: ${message}`,
131
+ `tree failed for ${req}: ${err.message || 'unknown error'}`,
71
132
  'execution',
72
133
  {
73
134
  parameter: 'path',
74
135
  value: req,
75
- suggestion:
76
- 'Check if the directory exists and tree command is installed',
136
+ suggestion: 'Check if the directory exists and is accessible',
77
137
  },
78
138
  );
79
139
  }
@@ -1,13 +1,11 @@
1
1
  import { tool, type Tool } from 'ai';
2
2
  import { z } from 'zod/v3';
3
- import { exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
3
+ import { spawn } from 'node:child_process';
5
4
  import { join } from 'node:path';
6
5
  import DESCRIPTION from './grep.txt' with { type: 'text' };
7
6
  import { defaultIgnoreGlobs } from './ignore.ts';
8
7
  import { createToolError, type ToolResponse } from '../error.ts';
9
-
10
- const execAsync = promisify(exec);
8
+ import { resolveBinary } from '../bin-manager.ts';
11
9
 
12
10
  function expandTilde(p: string) {
13
11
  const home = process.env.HOME || process.env.USERPROFILE || '';
@@ -63,35 +61,44 @@ export function buildGrepTool(projectRoot: string): {
63
61
  const isAbs = p.startsWith('/') || /^[A-Za-z]:[\\/]/.test(p);
64
62
  const searchPath = p ? (isAbs ? p : join(projectRoot, p)) : projectRoot;
65
63
 
66
- let cmd = `rg -n --color never`;
64
+ const rgBin = await resolveBinary('rg');
65
+ const args: string[] = ['-n', '--color', 'never'];
67
66
  for (const g of defaultIgnoreGlobs(params.ignore)) {
68
- cmd += ` --glob "${g.replace(/"/g, '\\"')}"`;
67
+ args.push('--glob', g);
69
68
  }
70
69
  if (params.include) {
71
- cmd += ` --glob "${params.include.replace(/"/g, '\\"')}"`;
70
+ args.push('--glob', params.include);
72
71
  }
73
- cmd += ` "${pattern.replace(/"/g, '\\"')}" "${searchPath}"`;
72
+ args.push(pattern, searchPath);
74
73
 
75
74
  let output = '';
76
75
  try {
77
- const result = await execAsync(cmd, { maxBuffer: 10 * 1024 * 1024 });
78
- output = result.stdout;
76
+ output = await new Promise<string>((resolve, reject) => {
77
+ const proc = spawn(rgBin, args, { cwd: projectRoot });
78
+ let stdout = '';
79
+ let stderr = '';
80
+ proc.stdout.on('data', (d) => {
81
+ stdout += d.toString();
82
+ });
83
+ proc.stderr.on('data', (d) => {
84
+ stderr += d.toString();
85
+ });
86
+ proc.on('close', (code) => {
87
+ if (code === 1) resolve('');
88
+ else if (code !== 0)
89
+ reject(new Error(stderr.trim() || 'ripgrep failed'));
90
+ else resolve(stdout);
91
+ });
92
+ proc.on('error', reject);
93
+ });
79
94
  } catch (error: unknown) {
80
- const err = error as { code?: number; stderr?: string };
81
- if (err.code === 1) {
82
- return { ok: true, count: 0, matches: [] };
83
- }
84
- const err2 = error as { stderr?: string; message?: string };
85
- return createToolError(
86
- `ripgrep failed: ${err2.stderr || err2.message}`,
87
- 'execution',
88
- {
89
- parameter: 'pattern',
90
- value: pattern,
91
- suggestion:
92
- 'Check if ripgrep (rg) is installed and the pattern is valid',
93
- },
94
- );
95
+ const err2 = error as { message?: string };
96
+ return createToolError(`ripgrep failed: ${err2.message}`, 'execution', {
97
+ parameter: 'pattern',
98
+ value: pattern,
99
+ suggestion:
100
+ 'Check if ripgrep (rg) is installed and the pattern is valid',
101
+ });
95
102
  }
96
103
 
97
104
  const lines = output.trim().split('\n');
@@ -4,6 +4,7 @@ import { spawn } from 'node:child_process';
4
4
  import { join } from 'node:path';
5
5
  import DESCRIPTION from './ripgrep.txt' with { type: 'text' };
6
6
  import { createToolError, type ToolResponse } from '../error.ts';
7
+ import { resolveBinary } from '../bin-manager.ts';
7
8
 
8
9
  export function buildRipgrepTool(projectRoot: string): {
9
10
  name: string;
@@ -61,8 +62,9 @@ export function buildRipgrepTool(projectRoot: string): {
61
62
  args.push(target);
62
63
 
63
64
  try {
65
+ const rgBin = await resolveBinary('rg');
64
66
  return await new Promise((resolve) => {
65
- const proc = spawn('rg', args, { cwd: projectRoot });
67
+ const proc = spawn(rgBin, args, { cwd: projectRoot });
66
68
  let stdout = '';
67
69
  let stderr = '';
68
70
 
@@ -3,8 +3,8 @@ You are an interactive CLI tool that helps users with software engineering tasks
3
3
  IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
4
4
 
5
5
  If the user asks for help or wants to give feedback inform them of the following:
6
- - /help: Get help with using opencode
7
- - To give feedback, users should report the issue at https://github.com/sst/opencode/issues
6
+ - /help: Get help with using agi
7
+ - To give feedback, users should report the issue at https://github.com/nitishxyz/agi/issues
8
8
 
9
9
  # Tone and style
10
10
  You should be concise, direct, and to the point.