@primo-ai/tools 0.1.3 → 0.1.5

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,7 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const calculatorTool: Tool<{
3
+ expression: string;
4
+ }, {
5
+ result: number;
6
+ expression: string;
7
+ }>;
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+ export const calculatorTool = {
3
+ name: 'calculator',
4
+ description: 'Evaluate a mathematical expression. Supports +, -, *, /, **, %, parentheses, and Math functions.',
5
+ inputSchema: z.object({
6
+ expression: z
7
+ .string()
8
+ .describe('Math expression (e.g. "2 + 3 * 4", "Math.sqrt(144)", "3.14 * 10 ** 2")'),
9
+ }),
10
+ requireApproval: false,
11
+ async execute(input) {
12
+ const { expression } = input;
13
+ const sanitized = expression.replace(/\s+/g, ' ').trim();
14
+ if (/[^0-9+\-*/().%\s^,a-zA-Z.]/.test(sanitized.replace(/Math\.\w+/g, ''))) {
15
+ throw new Error(`Disallowed characters in expression: "${expression}"`);
16
+ }
17
+ const fn = new Function(`"use strict"; return (${sanitized});`);
18
+ const result = fn();
19
+ if (typeof result !== 'number' || !Number.isFinite(result)) {
20
+ throw new Error(`Expression did not evaluate to a finite number: ${result}`);
21
+ }
22
+ return { result, expression };
23
+ },
24
+ renderCall(input) {
25
+ return `calc: ${input.expression}`;
26
+ },
27
+ renderResult(output) {
28
+ return `${output.expression} = ${output.result}`;
29
+ },
30
+ };
@@ -0,0 +1,10 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const datetimeTool: Tool<{
3
+ format?: string;
4
+ timezone?: string;
5
+ }, {
6
+ iso: string;
7
+ formatted: string;
8
+ timezone: string;
9
+ unix: number;
10
+ }>;
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ export const datetimeTool = {
3
+ name: 'datetime',
4
+ description: 'Get the current date and time. Supports timezone and format options.',
5
+ inputSchema: z.object({
6
+ format: z
7
+ .enum(['iso', 'locale', 'unix', 'date', 'time'])
8
+ .optional()
9
+ .default('iso')
10
+ .describe('Output format'),
11
+ timezone: z
12
+ .string()
13
+ .optional()
14
+ .describe('IANA timezone (e.g. "Asia/Shanghai", "America/New_York")'),
15
+ }),
16
+ requireApproval: false,
17
+ async execute(input) {
18
+ const { format = 'iso', timezone } = input;
19
+ const now = new Date();
20
+ const tz = timezone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
21
+ const formatted = format === 'iso'
22
+ ? now.toISOString()
23
+ : format === 'unix'
24
+ ? String(Math.floor(now.getTime() / 1000))
25
+ : format === 'date'
26
+ ? now.toLocaleDateString('en-US', { timeZone: tz })
27
+ : format === 'time'
28
+ ? now.toLocaleTimeString('en-US', { timeZone: tz })
29
+ : now.toLocaleString('en-US', { timeZone: tz });
30
+ return {
31
+ iso: now.toISOString(),
32
+ formatted,
33
+ timezone: tz,
34
+ unix: Math.floor(now.getTime() / 1000),
35
+ };
36
+ },
37
+ renderCall(input) {
38
+ return `datetime (${input.timezone ?? 'local'}, ${input.format ?? 'iso'})`;
39
+ },
40
+ renderResult(output) {
41
+ return output.formatted;
42
+ },
43
+ };
@@ -0,0 +1,10 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const fileEditTool: Tool<{
3
+ path: string;
4
+ oldString: string;
5
+ newString: string;
6
+ replaceAll?: boolean;
7
+ }, {
8
+ path: string;
9
+ replacements: number;
10
+ }>;
@@ -0,0 +1,41 @@
1
+ import { z } from 'zod';
2
+ export const fileEditTool = {
3
+ name: 'fileEdit',
4
+ description: 'Perform exact string replacement in a file. Set replaceAll to replace all occurrences.',
5
+ inputSchema: z.object({
6
+ path: z.string().describe('Absolute file path'),
7
+ oldString: z.string().describe('Exact string to find'),
8
+ newString: z.string().describe('Replacement string'),
9
+ replaceAll: z
10
+ .boolean()
11
+ .optional()
12
+ .default(false)
13
+ .describe('Replace all occurrences'),
14
+ }),
15
+ requireApproval: true,
16
+ async execute(input) {
17
+ const fs = await import('node:fs/promises');
18
+ const { path, oldString, newString, replaceAll = false } = input;
19
+ const content = await fs.readFile(path, 'utf-8');
20
+ if (!content.includes(oldString)) {
21
+ throw new Error(`String not found in ${path}`);
22
+ }
23
+ if (!replaceAll && content.split(oldString).length - 1 > 1) {
24
+ throw new Error(`Multiple matches found in ${path}. Use replaceAll: true or provide more context.`);
25
+ }
26
+ const updated = replaceAll
27
+ ? content.replaceAll(oldString, newString)
28
+ : content.replace(oldString, newString);
29
+ await fs.writeFile(path, updated, 'utf-8');
30
+ const replacements = replaceAll
31
+ ? content.split(oldString).length - 1
32
+ : 1;
33
+ return { path, replacements };
34
+ },
35
+ renderCall(input) {
36
+ return `edit ${input.path}: "${input.oldString.slice(0, 40)}..." → "${input.newString.slice(0, 40)}..."`;
37
+ },
38
+ renderResult(output) {
39
+ return `${output.path} (${output.replacements} replacement(s))`;
40
+ },
41
+ };
@@ -0,0 +1,11 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const fileReadTool: Tool<{
3
+ path: string;
4
+ encoding?: string;
5
+ offset?: number;
6
+ limit?: number;
7
+ }, {
8
+ content: string;
9
+ lines: number;
10
+ path: string;
11
+ }>;
@@ -0,0 +1,39 @@
1
+ import { z } from 'zod';
2
+ export const fileReadTool = {
3
+ name: 'fileRead',
4
+ description: 'Read file contents from the local filesystem. Supports line range via offset/limit.',
5
+ inputSchema: z.object({
6
+ path: z.string().describe('Absolute file path'),
7
+ encoding: z
8
+ .enum(['utf-8', 'ascii', 'base64', 'hex', 'latin1'])
9
+ .optional()
10
+ .default('utf-8')
11
+ .describe('File encoding'),
12
+ offset: z.number().optional().describe('Start line (1-indexed)'),
13
+ limit: z.number().optional().describe('Max lines to read'),
14
+ }),
15
+ requireApproval: false,
16
+ async execute(input) {
17
+ const fs = await import('node:fs/promises');
18
+ const { path, encoding = 'utf-8', offset, limit } = input;
19
+ const raw = await fs.readFile(path, { encoding: encoding });
20
+ if (offset == null && limit == null) {
21
+ const lines = raw.split('\n').length;
22
+ return { content: raw, lines, path };
23
+ }
24
+ const allLines = raw.split('\n');
25
+ const start = (offset ?? 1) - 1;
26
+ const end = limit != null ? start + limit : allLines.length;
27
+ const sliced = allLines.slice(start, end);
28
+ const numbered = sliced
29
+ .map((line, i) => `${start + i + 1}\t${line}`)
30
+ .join('\n');
31
+ return { content: numbered, lines: allLines.length, path };
32
+ },
33
+ renderCall(input) {
34
+ return `read ${input.path}`;
35
+ },
36
+ renderResult(output) {
37
+ return `[${output.path}] ${output.lines} lines`;
38
+ },
39
+ };
@@ -0,0 +1,10 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const fileWriteTool: Tool<{
3
+ path: string;
4
+ content: string;
5
+ encoding?: string;
6
+ append?: boolean;
7
+ }, {
8
+ path: string;
9
+ bytes: number;
10
+ }>;
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ export const fileWriteTool = {
3
+ name: 'fileWrite',
4
+ description: 'Write content to a file on the local filesystem. Creates parent directories if needed.',
5
+ inputSchema: z.object({
6
+ path: z.string().describe('Absolute file path'),
7
+ content: z.string().describe('Content to write'),
8
+ encoding: z
9
+ .enum(['utf-8', 'ascii', 'base64', 'hex', 'latin1'])
10
+ .optional()
11
+ .default('utf-8')
12
+ .describe('File encoding'),
13
+ append: z
14
+ .boolean()
15
+ .optional()
16
+ .default(false)
17
+ .describe('Append to file instead of overwriting'),
18
+ }),
19
+ requireApproval: true,
20
+ async execute(input) {
21
+ const fs = await import('node:fs/promises');
22
+ const pathModule = await import('node:path');
23
+ const { path, content, encoding = 'utf-8', append = false } = input;
24
+ await fs.mkdir(pathModule.dirname(path), { recursive: true });
25
+ const flag = append ? 'a' : 'w';
26
+ await fs.writeFile(path, content, { encoding: encoding, flag });
27
+ const bytes = Buffer.byteLength(content, encoding);
28
+ return { path, bytes };
29
+ },
30
+ renderCall(input) {
31
+ return `${input.append ? 'append' : 'write'} ${input.path} (${input.content.length} chars)`;
32
+ },
33
+ renderResult(output) {
34
+ return `${output.path} (${output.bytes} bytes)`;
35
+ },
36
+ };
package/dist/glob.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const globTool: Tool<{
3
+ pattern: string;
4
+ path?: string;
5
+ }, {
6
+ files: string[];
7
+ count: number;
8
+ }>;
package/dist/glob.js ADDED
@@ -0,0 +1,25 @@
1
+ import { z } from 'zod';
2
+ export const globTool = {
3
+ name: 'glob',
4
+ description: 'Find files matching a glob pattern. Returns file paths sorted by modification time.',
5
+ inputSchema: z.object({
6
+ pattern: z.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.js")'),
7
+ path: z.string().optional().describe('Base directory to search in'),
8
+ }),
9
+ requireApproval: false,
10
+ async execute(input) {
11
+ const { glob } = await import('node:fs/promises');
12
+ const { pattern, path: cwd } = input;
13
+ const files = [];
14
+ for await (const entry of glob(pattern, { cwd })) {
15
+ files.push(entry);
16
+ }
17
+ return { files, count: files.length };
18
+ },
19
+ renderCall(input) {
20
+ return `glob ${input.pattern}`;
21
+ },
22
+ renderResult(output) {
23
+ return `${output.count} files matched`;
24
+ },
25
+ };
package/dist/grep.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const grepTool: Tool<{
3
+ pattern: string;
4
+ path?: string;
5
+ include?: string;
6
+ maxResults?: number;
7
+ }, {
8
+ matches: {
9
+ file: string;
10
+ line: number;
11
+ text: string;
12
+ }[];
13
+ count: number;
14
+ }>;
package/dist/grep.js ADDED
@@ -0,0 +1,70 @@
1
+ import { z } from 'zod';
2
+ export const grepTool = {
3
+ name: 'grep',
4
+ description: 'Search file contents by regex pattern. Returns matching lines with file paths and line numbers.',
5
+ inputSchema: z.object({
6
+ pattern: z.string().describe('Regex pattern to search for'),
7
+ path: z.string().optional().describe('Directory to search in'),
8
+ include: z.string().optional().describe('File glob filter (e.g. "*.ts")'),
9
+ maxResults: z.number().optional().default(100).describe('Max results'),
10
+ }),
11
+ requireApproval: false,
12
+ async execute(input) {
13
+ const { readFile, readdir } = await import('node:fs/promises');
14
+ const { join, relative } = await import('node:path');
15
+ const { pattern, path: cwd = '.', include, maxResults = 100 } = input;
16
+ const regex = new RegExp(pattern, 'i');
17
+ const matches = [];
18
+ const includeRegex = include
19
+ ? globToRegex(include)
20
+ : null;
21
+ async function walk(dir) {
22
+ if (matches.length >= maxResults)
23
+ return;
24
+ const entries = await readdir(dir, { withFileTypes: true });
25
+ for (const entry of entries) {
26
+ if (matches.length >= maxResults)
27
+ return;
28
+ if (entry.name.startsWith('.') || entry.name === 'node_modules')
29
+ continue;
30
+ const full = join(dir, entry.name);
31
+ if (entry.isDirectory()) {
32
+ await walk(full);
33
+ }
34
+ else if (entry.isFile()) {
35
+ if (includeRegex && !includeRegex.test(entry.name))
36
+ continue;
37
+ try {
38
+ const content = await readFile(full, 'utf-8');
39
+ const lines = content.split('\n');
40
+ for (let i = 0; i < lines.length; i++) {
41
+ if (matches.length >= maxResults)
42
+ return;
43
+ if (regex.test(lines[i])) {
44
+ matches.push({ file: relative(cwd, full), line: i + 1, text: lines[i].trim() });
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ // skip binary / unreadable files
50
+ }
51
+ }
52
+ }
53
+ }
54
+ await walk(cwd);
55
+ return { matches, count: matches.length };
56
+ },
57
+ renderCall(input) {
58
+ return `grep "${input.pattern}"`;
59
+ },
60
+ renderResult(output) {
61
+ return `${output.count} matches`;
62
+ },
63
+ };
64
+ function globToRegex(glob) {
65
+ const escaped = glob
66
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
67
+ .replace(/\*/g, '.*')
68
+ .replace(/\?/g, '.');
69
+ return new RegExp(`^${escaped}$`);
70
+ }
package/dist/http.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const httpTool: Tool<{
3
+ url: string;
4
+ method?: string;
5
+ headers?: Record<string, string>;
6
+ body?: string;
7
+ }, {
8
+ status: number;
9
+ headers: Record<string, string>;
10
+ body: string;
11
+ }>;
package/dist/http.js ADDED
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ export const httpTool = {
3
+ name: 'http',
4
+ description: 'Make HTTP requests. Supports GET, POST, PUT, PATCH, DELETE. Returns status, headers, and body.',
5
+ inputSchema: z.object({
6
+ url: z.string().describe('The URL to request'),
7
+ method: z
8
+ .enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
9
+ .optional()
10
+ .default('GET')
11
+ .describe('HTTP method'),
12
+ headers: z
13
+ .record(z.string(), z.string())
14
+ .optional()
15
+ .describe('Request headers'),
16
+ body: z.string().optional().describe('Request body (for POST/PUT/PATCH)'),
17
+ }),
18
+ outputSchema: z.object({
19
+ status: z.number(),
20
+ headers: z.record(z.string(), z.string()),
21
+ body: z.string(),
22
+ }),
23
+ requireApproval: false,
24
+ async execute(input) {
25
+ const { url, method = 'GET', headers = {}, body } = input;
26
+ const reqHeaders = new Headers();
27
+ reqHeaders.set('User-Agent', 'AgentForge/1.0');
28
+ for (const [k, v] of Object.entries(headers)) {
29
+ reqHeaders.set(k, v);
30
+ }
31
+ const res = await fetch(url, {
32
+ method,
33
+ headers: reqHeaders,
34
+ body: ['GET', 'DELETE'].includes(method) ? undefined : body,
35
+ });
36
+ const resHeaders = {};
37
+ res.headers.forEach((v, k) => {
38
+ resHeaders[k] = v;
39
+ });
40
+ const resBody = await res.text();
41
+ return { status: res.status, headers: resHeaders, body: resBody };
42
+ },
43
+ renderCall(input) {
44
+ return `${input.method ?? 'GET'} ${input.url}`;
45
+ },
46
+ renderResult(output) {
47
+ return `[${output.status}] ${output.body.slice(0, 200)}`;
48
+ },
49
+ };
package/dist/index.d.ts CHANGED
@@ -1 +1,11 @@
1
1
  export { echoTool } from './echo.js';
2
+ export { httpTool } from './http.js';
3
+ export { fileReadTool } from './file-read.js';
4
+ export { fileWriteTool } from './file-write.js';
5
+ export { fileEditTool } from './file-edit.js';
6
+ export { globTool } from './glob.js';
7
+ export { grepTool } from './grep.js';
8
+ export { shellTool } from './shell.js';
9
+ export { calculatorTool } from './calculator.js';
10
+ export { datetimeTool } from './datetime.js';
11
+ export { jsonTool } from './json.js';
package/dist/index.js CHANGED
@@ -1,2 +1,12 @@
1
1
  // @primo-ai/tools — Built-in tools
2
2
  export { echoTool } from './echo.js';
3
+ export { httpTool } from './http.js';
4
+ export { fileReadTool } from './file-read.js';
5
+ export { fileWriteTool } from './file-write.js';
6
+ export { fileEditTool } from './file-edit.js';
7
+ export { globTool } from './glob.js';
8
+ export { grepTool } from './grep.js';
9
+ export { shellTool } from './shell.js';
10
+ export { calculatorTool } from './calculator.js';
11
+ export { datetimeTool } from './datetime.js';
12
+ export { jsonTool } from './json.js';
package/dist/json.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const jsonTool: Tool<{
3
+ operation: 'parse' | 'stringify' | 'query';
4
+ data: string;
5
+ path?: string;
6
+ indent?: number;
7
+ }, {
8
+ result: string;
9
+ }>;
package/dist/json.js ADDED
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ export const jsonTool = {
3
+ name: 'json',
4
+ description: 'Parse, stringify, or query JSON data. Query uses dot-notation paths (e.g. "users.0.name").',
5
+ inputSchema: z.object({
6
+ operation: z
7
+ .enum(['parse', 'stringify', 'query'])
8
+ .describe('Operation to perform'),
9
+ data: z.string().describe('JSON string or value to process'),
10
+ path: z
11
+ .string()
12
+ .optional()
13
+ .describe('Dot-notation path for query (e.g. "users.0.name")'),
14
+ indent: z.number().optional().default(2).describe('Indentation for stringify'),
15
+ }),
16
+ requireApproval: false,
17
+ async execute(input) {
18
+ const { operation, data, path, indent = 2 } = input;
19
+ switch (operation) {
20
+ case 'parse': {
21
+ const parsed = JSON.parse(data);
22
+ return { result: JSON.stringify(parsed, null, indent) };
23
+ }
24
+ case 'stringify': {
25
+ const parsed = JSON.parse(data);
26
+ return { result: JSON.stringify(parsed, null, indent) };
27
+ }
28
+ case 'query': {
29
+ if (!path)
30
+ throw new Error('path is required for query operation');
31
+ const parsed = JSON.parse(data);
32
+ const result = path.split('.').reduce((obj, key) => {
33
+ if (obj == null)
34
+ return undefined;
35
+ return obj[key];
36
+ }, parsed);
37
+ return { result: JSON.stringify(result, null, indent) };
38
+ }
39
+ default:
40
+ throw new Error(`Unknown operation: ${operation}`);
41
+ }
42
+ },
43
+ renderCall(input) {
44
+ return `json ${input.operation}${input.path ? ` .${input.path}` : ''}`;
45
+ },
46
+ renderResult(output) {
47
+ return output.result.slice(0, 200);
48
+ },
49
+ };
@@ -0,0 +1,10 @@
1
+ import type { Tool } from '@primo-ai/sdk';
2
+ export declare const shellTool: Tool<{
3
+ command: string;
4
+ cwd?: string;
5
+ timeout?: number;
6
+ }, {
7
+ exitCode: number | null;
8
+ stdout: string;
9
+ stderr: string;
10
+ }>;
package/dist/shell.js ADDED
@@ -0,0 +1,38 @@
1
+ import { z } from 'zod';
2
+ export const shellTool = {
3
+ name: 'shell',
4
+ description: 'Execute a shell command. Returns stdout, stderr, and exit code. Supports timeout.',
5
+ inputSchema: z.object({
6
+ command: z.string().describe('Shell command to execute'),
7
+ cwd: z.string().optional().describe('Working directory'),
8
+ timeout: z
9
+ .number()
10
+ .optional()
11
+ .default(30_000)
12
+ .describe('Timeout in milliseconds (default 30s)'),
13
+ }),
14
+ requireApproval: true,
15
+ async execute(input) {
16
+ const { exec } = await import('node:child_process');
17
+ const { command, cwd, timeout = 30_000 } = input;
18
+ return new Promise((resolve) => {
19
+ const child = exec(command, { cwd, timeout, maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => {
20
+ resolve({
21
+ exitCode: error ? 1 : 0,
22
+ stdout: stdout ?? '',
23
+ stderr: stderr ?? '',
24
+ });
25
+ });
26
+ child.on('error', (err) => {
27
+ resolve({ exitCode: 1, stdout: '', stderr: err.message });
28
+ });
29
+ });
30
+ },
31
+ renderCall(input) {
32
+ return `$ ${input.command}`;
33
+ },
34
+ renderResult(output) {
35
+ const out = output.stdout.slice(0, 200);
36
+ return `[exit ${output.exitCode}] ${out}`;
37
+ },
38
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primo-ai/tools",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -14,9 +14,10 @@
14
14
  }
15
15
  },
16
16
  "devDependencies": {
17
+ "@types/node": "^22.15.0",
17
18
  "typescript": "^5.7.3",
18
19
  "vitest": "^3.0.5",
19
- "@primo-ai/sdk": "0.1.3"
20
+ "@primo-ai/sdk": "0.1.5"
20
21
  },
21
22
  "dependencies": {
22
23
  "zod": "^4.4.3"