@sisu-ai/tool-terminal 1.1.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.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # @sisu-ai/tool-terminal
2
+
3
+ [![Tests](https://github.com/finger-gun/sisu/actions/workflows/tests.yml/badge.svg?branch=main)](https://github.com/finger-gun/sisu/actions/workflows/tests.yml)
4
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue)](https://github.com/finger-gun/sisu/blob/main/LICENSE)
5
+ [![Downloads](https://img.shields.io/npm/dm/%40sisu-ai%2Ftool-terminal)](https://www.npmjs.com/package/@sisu-ai/tool-terminal)
6
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/finger-gun/sisu/blob/main/CONTRIBUTING.md)
7
+
8
+ A secure terminal execution tool for Sisu agents. Provides sandboxed shell command execution with session support, command allow/deny lists, path scoping, timeouts and basic file helpers.
9
+
10
+ ## API
11
+
12
+ - `createTerminalTool(config?)` → returns an instance with:
13
+ - methods: `start_session`, `run_command`, `cd`, `read_file`
14
+ - `tools`: an array of Tool definitions (for models): `terminalRun`, `terminalCd`, `terminalReadFile`.
15
+
16
+ ### Defaults & Reuse
17
+ - Importable defaults to help you build policies/UI:
18
+ - `DEFAULT_CONFIG` — full default config object
19
+ - `TERMINAL_COMMANDS_ALLOW` — default allowlist array
20
+ - `TERMINAL_COMMANDS_DENY` — default denylist array
21
+ - `defaultTerminalConfig(partial)` — helper to merge your overrides with sensible defaults
22
+
23
+ ## Quick Start
24
+
25
+ ```ts
26
+ import 'dotenv/config';
27
+ import { Agent, SimpleTools, InMemoryKV, NullStream, createConsoleLogger, type Ctx } from '@sisu-ai/core';
28
+ import { openAIAdapter } from '@sisu-ai/adapter-openai';
29
+ import { registerTools } from '@sisu-ai/mw-register-tools';
30
+ import { inputToMessage, conversationBuffer } from '@sisu-ai/mw-conversation-buffer';
31
+ import { toolCalling } from '@sisu-ai/mw-tool-calling';
32
+ import { errorBoundary } from '@sisu-ai/mw-error-boundary';
33
+ import { traceViewer } from '@sisu-ai/mw-trace-viewer';
34
+ import { createTerminalTool } from '@sisu-ai/tool-terminal';
35
+
36
+ const terminal = createTerminalTool({ roots: [process.cwd()] });
37
+
38
+ const model = openAIAdapter({ model: process.env.MODEL || 'gpt-4o-mini' });
39
+ const ctx: Ctx = {
40
+ input: 'List files in the project root and show the first 10 lines of README.md.',
41
+ messages: [{ role: 'system', content: 'You are a helpful assistant.' }],
42
+ model,
43
+ tools: new SimpleTools(),
44
+ memory: new InMemoryKV(),
45
+ stream: new NullStream(),
46
+ state: {},
47
+ signal: new AbortController().signal,
48
+ log: createConsoleLogger({ level: (process.env.LOG_LEVEL as any) ?? 'info' }),
49
+ };
50
+
51
+ const app = new Agent()
52
+ .use(errorBoundary(async (err, c) => { c.log.error(err); }))
53
+ .use(traceViewer())
54
+ .use(registerTools(terminal.tools))
55
+ .use(inputToMessage)
56
+ .use(conversationBuffer({ window: 6 }))
57
+ .use(toolCalling);
58
+
59
+ await app.handler()(ctx);
60
+ console.log(ctx.messages.filter(m => m.role === 'assistant').pop()?.content);
61
+ ```
62
+
63
+ ## Logging & Traces
64
+ - Tools emit structured logs via `ctx.log` so runs are visible in traces:
65
+ - `terminalRun`: logs policy pre-check and final result (exit code, duration, bytes).
66
+ - `terminalCd`: logs requested and resolved paths and whether allowed.
67
+ - `terminalReadFile`: logs resolved path and byte size of returned contents.
68
+
69
+ ## Config (subset)
70
+
71
+ ```ts
72
+ type TerminalToolConfig = {
73
+ roots: string[]; // allowed path roots (required)
74
+ readOnlyRoots?: string[];
75
+ capabilities: { read: boolean; write: boolean; delete: boolean; exec: boolean };
76
+ commands: { allow: string[]; deny: string[] };
77
+ execution: { timeoutMs: number; maxStdoutBytes: number; maxStderrBytes: number; shell: 'direct'|'sh'|'bash'|'powershell'|'cmd' };
78
+ sessions: { enabled: boolean; ttlMs: number; maxPerAgent: number };
79
+ }
80
+ ```
81
+
82
+ Sensible defaults: `read: true`, `exec: true`, `write/delete: false`, timeout 10s, `roots: [process.cwd()]`, and a conservative allow/deny command policy. See `DEFAULT_CONFIG` in `src/index.ts` for full details.
83
+
84
+ ## Tool Schemas
85
+
86
+ - `terminalRun({ command, cwd?, env?, stdin?, sessionId? }) → { exitCode, stdout, stderr, durationMs, policy, cwd }`
87
+ - `terminalCd({ path, sessionId? }) → { cwd, sessionId? }` // creates a session if missing
88
+ - `terminalReadFile({ path, encoding?, sessionId? }) → { contents }`
89
+
90
+ Each tool is validated with zod and registered through the instance’s `tools` array. `start_session` is available as a method for advanced use but is not exposed as a tool by default.
91
+
92
+ ### When To Use `start_session`
93
+ - Persistent cwd across multiple calls: when you plan a sequence like “cd → run → read → run” and want a stable working directory without passing `cwd` every time.
94
+ - Pre-seeding env: when a short‑lived, limited env should apply to multiple runs (e.g., `PATH` tweak, `FOO_MODE=1`) without repeating it on each call.
95
+ - Deterministic bursts: when you want a TTL‑bounded context that will expire automatically after inactivity (defaults to 2 minutes), avoiding stale state.
96
+ - Lower friction than an initial `terminalCd`: prefer `start_session({ cwd, env })` if you already know the starting folder and env.
97
+
98
+ Example
99
+ ```ts
100
+ const term = createTerminalTool({ roots: [process.cwd()] });
101
+ const { sessionId } = term.start_session({ cwd: process.cwd(), env: { FOO_MODE: '1' } });
102
+ await term.run_command({ sessionId, command: 'ls -la' });
103
+ await term.run_command({ sessionId, command: 'grep -n "TODO" README.md' });
104
+ const file = await term.read_file({ sessionId, path: 'README.md' });
105
+ ```
106
+
107
+ ## Notes
108
+
109
+ - Non-interactive commands only.
110
+ - Network-accessing commands are denied by default via patterns (e.g., `curl *`, `wget *`).
111
+ - All paths are resolved and constrained to configured `roots`; write/delete under read-only roots are denied.
112
+ - Absolute path arguments outside `roots` are denied (e.g., `grep -r /`). Prefer setting `cwd` (via `terminalCd`) and using relative paths.
113
+
114
+ # Community & Support
115
+ - [Code of Conduct](https://github.com/finger-gun/sisu/blob/main/CODE_OF_CONDUCT.md)
116
+ - [Contributing Guide](https://github.com/finger-gun/sisu/blob/main/CONTRIBUTING.md)
117
+ - [License](https://github.com/finger-gun/sisu/blob/main/LICENSE)
118
+ - [Report a Bug](https://github.com/finger-gun/sisu/issues/new?template=bug_report.md)
119
+ - [Request a Feature](https://github.com/finger-gun/sisu/issues/new?template=feature_request.md)
@@ -0,0 +1,108 @@
1
+ import type { Tool } from '@sisu-ai/core';
2
+ export interface TerminalToolConfig {
3
+ roots: string[];
4
+ readOnlyRoots?: string[];
5
+ capabilities: {
6
+ read: boolean;
7
+ write: boolean;
8
+ delete: boolean;
9
+ exec: boolean;
10
+ };
11
+ commands: {
12
+ allow: string[];
13
+ deny: string[];
14
+ };
15
+ execution: {
16
+ timeoutMs: number;
17
+ maxStdoutBytes: number;
18
+ maxStderrBytes: number;
19
+ shell: 'sh' | 'bash' | 'powershell' | 'cmd' | 'direct';
20
+ };
21
+ sessions: {
22
+ enabled: boolean;
23
+ ttlMs: number;
24
+ maxPerAgent: number;
25
+ };
26
+ }
27
+ export declare const DEFAULT_CONFIG: TerminalToolConfig;
28
+ export declare const TERMINAL_COMMANDS_ALLOW: ReadonlyArray<string>;
29
+ export declare const TERMINAL_COMMANDS_DENY: ReadonlyArray<string>;
30
+ export declare function defaultTerminalConfig(overrides?: Partial<TerminalToolConfig>): TerminalToolConfig;
31
+ export declare function createTerminalTool(config?: Partial<TerminalToolConfig>): {
32
+ start_session: (args?: {
33
+ cwd?: string;
34
+ env?: Record<string, string>;
35
+ }) => {
36
+ sessionId: `${string}-${string}-${string}-${string}-${string}`;
37
+ expiresAt: string;
38
+ };
39
+ run_command: (args: {
40
+ command: string;
41
+ cwd?: string;
42
+ env?: Record<string, string>;
43
+ stdin?: string;
44
+ sessionId?: string;
45
+ }) => Promise<{
46
+ exitCode: number;
47
+ stdout: string;
48
+ stderr: string;
49
+ durationMs: number;
50
+ policy: {
51
+ allowed: boolean;
52
+ reason?: string;
53
+ };
54
+ cwd: string;
55
+ } | {
56
+ exitCode: number;
57
+ stdout: Buffer<ArrayBufferLike>;
58
+ stderr: Buffer<ArrayBufferLike>;
59
+ durationMs: number;
60
+ policy: {
61
+ allowed: boolean;
62
+ };
63
+ cwd: string;
64
+ } | {
65
+ exitCode: any;
66
+ stdout: string;
67
+ stderr: string;
68
+ durationMs: number;
69
+ policy: {
70
+ allowed: boolean;
71
+ };
72
+ cwd: string;
73
+ }>;
74
+ cd: (args: {
75
+ path: string;
76
+ sessionId?: string;
77
+ }) => {
78
+ cwd: string;
79
+ sessionId?: string;
80
+ };
81
+ read_file: (args: {
82
+ path: string;
83
+ encoding?: "utf8" | "base64";
84
+ sessionId?: string;
85
+ }) => Promise<{
86
+ contents: string;
87
+ }>;
88
+ tools: (Tool<{
89
+ command: string;
90
+ cwd?: string;
91
+ env?: Record<string, string>;
92
+ stdin?: string;
93
+ sessionId?: string;
94
+ }, any> | Tool<{
95
+ path: string;
96
+ sessionId?: string;
97
+ }, {
98
+ cwd: string;
99
+ sessionId?: string;
100
+ }> | Tool<{
101
+ path: string;
102
+ encoding?: "utf8" | "base64";
103
+ sessionId?: string;
104
+ }, {
105
+ contents: string;
106
+ }>)[];
107
+ };
108
+ export type TerminalTool = ReturnType<typeof createTerminalTool>;
package/dist/index.js ADDED
@@ -0,0 +1,291 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { exec as cpExec } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+ import { minimatch } from 'minimatch';
7
+ import { z } from 'zod';
8
+ const exec = promisify(cpExec);
9
+ export const DEFAULT_CONFIG = {
10
+ roots: [process.cwd()],
11
+ capabilities: { read: true, write: false, delete: false, exec: true },
12
+ commands: {
13
+ allow: [
14
+ 'pwd',
15
+ 'ls',
16
+ 'cat',
17
+ 'head',
18
+ 'tail',
19
+ 'stat',
20
+ 'wc',
21
+ 'grep',
22
+ 'find',
23
+ 'echo',
24
+ 'sed',
25
+ 'awk',
26
+ 'cut',
27
+ 'sort',
28
+ 'uniq',
29
+ 'xargs',
30
+ 'node',
31
+ 'npm',
32
+ 'pnpm',
33
+ 'yarn'
34
+ ],
35
+ deny: [
36
+ 'sudo',
37
+ 'chmod',
38
+ 'chown',
39
+ 'mount',
40
+ 'umount',
41
+ 'shutdown',
42
+ 'reboot',
43
+ 'dd',
44
+ 'mkfs*',
45
+ 'service',
46
+ 'systemctl',
47
+ 'iptables',
48
+ 'firewall*',
49
+ 'curl *',
50
+ 'wget *'
51
+ ]
52
+ },
53
+ execution: {
54
+ timeoutMs: 10_000,
55
+ maxStdoutBytes: 1_000_000,
56
+ maxStderrBytes: 250_000,
57
+ shell: 'direct'
58
+ },
59
+ sessions: { enabled: true, ttlMs: 120_000, maxPerAgent: 4 }
60
+ };
61
+ // Reusable exports for consumers who want to surface or extend policy
62
+ export const TERMINAL_COMMANDS_ALLOW = Object.freeze([...DEFAULT_CONFIG.commands.allow]);
63
+ export const TERMINAL_COMMANDS_DENY = Object.freeze([...DEFAULT_CONFIG.commands.deny]);
64
+ export function defaultTerminalConfig(overrides) {
65
+ return {
66
+ ...DEFAULT_CONFIG,
67
+ ...overrides,
68
+ capabilities: { ...DEFAULT_CONFIG.capabilities, ...(overrides?.capabilities ?? {}) },
69
+ commands: {
70
+ allow: overrides?.commands?.allow ?? DEFAULT_CONFIG.commands.allow,
71
+ deny: overrides?.commands?.deny ?? DEFAULT_CONFIG.commands.deny,
72
+ },
73
+ execution: { ...DEFAULT_CONFIG.execution, ...(overrides?.execution ?? {}) },
74
+ sessions: { ...DEFAULT_CONFIG.sessions, ...(overrides?.sessions ?? {}) },
75
+ };
76
+ }
77
+ function isCommandAllowed(cmd, policy) {
78
+ const normalized = cmd.trim().replace(/\s+/g, ' ');
79
+ const verb = normalized.split(' ')[0] ?? '';
80
+ const candidates = [normalized, verb];
81
+ const opts = { nocase: true, matchBase: true };
82
+ const denyHit = policy.deny.some(p => candidates.some(c => minimatch(c, p, opts)));
83
+ if (denyHit)
84
+ return false;
85
+ return policy.allow.some(p => candidates.some(c => minimatch(c, p, opts)));
86
+ }
87
+ function isPathAllowed(absPath, cfg, mode) {
88
+ const real = path.resolve(absPath);
89
+ const roots = cfg.roots.map(r => path.resolve(r));
90
+ const inside = roots.some(r => real === r || real.startsWith(r + path.sep));
91
+ if (!inside)
92
+ return false;
93
+ if (mode !== 'read' && cfg.readOnlyRoots) {
94
+ const ro = cfg.readOnlyRoots.map(r => path.resolve(r));
95
+ const inRo = ro.some(r => real === r || real.startsWith(r + path.sep));
96
+ if (inRo)
97
+ return false;
98
+ }
99
+ return true;
100
+ }
101
+ function extractAbsolutePaths(cmd) {
102
+ // Very simple token scan: find whitespace-delimited tokens that start with '/'
103
+ const matches = cmd.match(/(?:^|\s)(\/[\w\-./]+)(?=\s|$)/g) || [];
104
+ return matches.map(m => m.trim());
105
+ }
106
+ function commandPolicyCheck(args, cfg) {
107
+ if (!cfg.capabilities.exec)
108
+ return { allowed: false, reason: 'exec disabled' };
109
+ if (!isPathAllowed(args.cwd, cfg, 'exec'))
110
+ return { allowed: false, reason: 'cwd outside roots' };
111
+ if (!isCommandAllowed(args.command, cfg.commands))
112
+ return { allowed: false, reason: 'command denied' };
113
+ // Guard against absolute path escapes: deny any absolute path outside roots
114
+ const abs = extractAbsolutePaths(args.command);
115
+ for (const p of abs) {
116
+ if (!isPathAllowed(p, cfg, 'read')) {
117
+ return { allowed: false, reason: `absolute path outside roots: ${p}` };
118
+ }
119
+ }
120
+ return { allowed: true };
121
+ }
122
+ export function createTerminalTool(config) {
123
+ const cfg = defaultTerminalConfig(config);
124
+ const sessions = new Map();
125
+ function getSession(id) {
126
+ if (!id)
127
+ return undefined;
128
+ const s = sessions.get(id);
129
+ if (!s)
130
+ return undefined;
131
+ if (Date.now() > s.expiresAt) {
132
+ sessions.delete(id);
133
+ return undefined;
134
+ }
135
+ return s;
136
+ }
137
+ function start_session(args) {
138
+ const cwd = args?.cwd ? path.resolve(args.cwd) : cfg.roots[0];
139
+ if (!isPathAllowed(cwd, cfg, 'exec')) {
140
+ throw new Error('cwd outside allowed roots');
141
+ }
142
+ const sessionId = randomUUID();
143
+ const expiresAt = Date.now() + cfg.sessions.ttlMs;
144
+ sessions.set(sessionId, { cwd, env: { ...(args?.env ?? {}) }, expiresAt });
145
+ return { sessionId, expiresAt: new Date(expiresAt).toISOString() };
146
+ }
147
+ async function run_command(args) {
148
+ const session = getSession(args.sessionId);
149
+ const cwd = path.resolve(args.cwd ?? session?.cwd ?? cfg.roots[0]);
150
+ const commandStr = args.command;
151
+ const pre = commandPolicyCheck({ command: commandStr, cwd }, cfg);
152
+ if (!pre.allowed) {
153
+ return { exitCode: -1, stdout: '', stderr: '', durationMs: 0, policy: pre, cwd };
154
+ }
155
+ const start = Date.now();
156
+ try {
157
+ const { stdout: s, stderr: e } = await exec(commandStr, {
158
+ cwd,
159
+ env: { ...(session?.env ?? {}), ...(args.env ?? {}) },
160
+ timeout: cfg.execution.timeoutMs,
161
+ input: args.stdin
162
+ });
163
+ const dur = Date.now() - start;
164
+ let stdout = s ?? '';
165
+ let stderr = e ?? '';
166
+ if (Buffer.byteLength(stdout) > cfg.execution.maxStdoutBytes) {
167
+ stdout = stdout.slice(-cfg.execution.maxStdoutBytes);
168
+ }
169
+ if (Buffer.byteLength(stderr) > cfg.execution.maxStderrBytes) {
170
+ stderr = stderr.slice(-cfg.execution.maxStderrBytes);
171
+ }
172
+ if (session)
173
+ session.cwd = cwd;
174
+ return { exitCode: 0, stdout, stderr, durationMs: dur, policy: { allowed: true }, cwd };
175
+ }
176
+ catch (err) {
177
+ const dur = Date.now() - start;
178
+ const stdout = String(err.stdout ?? '');
179
+ const stderr = String(err.stderr ?? err.message ?? '');
180
+ const code = typeof err.code === 'number' ? err.code : -1;
181
+ return { exitCode: code, stdout, stderr, durationMs: dur, policy: { allowed: true }, cwd };
182
+ }
183
+ }
184
+ function cd(args) {
185
+ let session = getSession(args.sessionId);
186
+ // If no valid session is provided, create one anchored at the first root
187
+ if (!session) {
188
+ const cwd = cfg.roots[0];
189
+ const sessionId = randomUUID();
190
+ const expiresAt = Date.now() + cfg.sessions.ttlMs;
191
+ session = { cwd, env: {}, expiresAt };
192
+ sessions.set(sessionId, session);
193
+ // attach generated id on args for return below
194
+ args._createdSessionId = sessionId;
195
+ }
196
+ const newPath = path.resolve(session.cwd, args.path);
197
+ if (!isPathAllowed(newPath, cfg, 'exec')) {
198
+ throw new Error('path outside allowed roots');
199
+ }
200
+ session.cwd = newPath;
201
+ session.expiresAt = Date.now() + cfg.sessions.ttlMs;
202
+ return { cwd: session.cwd, sessionId: args._createdSessionId ?? args.sessionId };
203
+ }
204
+ async function read_file(args) {
205
+ if (!cfg.capabilities.read)
206
+ throw new Error('read disabled');
207
+ const session = getSession(args.sessionId);
208
+ const cwd = session?.cwd ?? cfg.roots[0];
209
+ const abs = path.resolve(cwd, args.path);
210
+ if (!isPathAllowed(abs, cfg, 'read'))
211
+ throw new Error('path outside allowed roots');
212
+ const buf = await fs.readFile(abs);
213
+ const encoding = args.encoding ?? 'utf8';
214
+ const contents = encoding === 'base64' ? buf.toString('base64') : buf.toString('utf8');
215
+ return { contents };
216
+ }
217
+ const runCommandTool = {
218
+ name: 'terminalRun',
219
+ description: [
220
+ `Run a non-interactive, sandboxed terminal command within allowed roots (${cfg.roots}).`,
221
+ `Use for listing files (ls), printing files (cat), simple text processing etc. Allowed commands are ${cfg.commands.allow.join(', ')}).`,
222
+ 'Always prefer passing a safe single command. Network and destructive commands are denied by policy.',
223
+ 'Tips: pass cwd to run in a specific folder; use terminalCd first to set a working directory for subsequent calls; prefer terminalReadFile when you only need file contents.'
224
+ ].join(' '),
225
+ schema: z.object({
226
+ command: z.string(),
227
+ cwd: z.string().optional(),
228
+ env: z.record(z.string()).optional(),
229
+ stdin: z.string().optional(),
230
+ sessionId: z.string().optional()
231
+ }),
232
+ handler: async (a, ctx) => {
233
+ const s = getSession(a.sessionId);
234
+ const effCwd = path.resolve(a.cwd ?? s?.cwd ?? cfg.roots[0]);
235
+ const policy = commandPolicyCheck({ command: a.command, cwd: effCwd }, cfg);
236
+ ctx?.log?.debug?.('[terminalRun] policy', { command: a.command, cwd: effCwd, policy });
237
+ const res = await run_command(a);
238
+ ctx?.log?.info?.('[terminalRun] result', {
239
+ command: a.command,
240
+ cwd: res.cwd,
241
+ exitCode: res.exitCode,
242
+ durationMs: res.durationMs,
243
+ stdoutBytes: Buffer.byteLength(res.stdout || ''),
244
+ stderrBytes: Buffer.byteLength(res.stderr || ''),
245
+ policy: res.policy,
246
+ });
247
+ return res;
248
+ }
249
+ };
250
+ const cdTool = {
251
+ name: 'terminalCd',
252
+ description: [
253
+ 'Change the working directory for subsequent terminal operations.',
254
+ 'Accepts a path relative to the current directory or absolute within the configured roots.',
255
+ 'If no session exists, creates one and returns sessionId.',
256
+ 'Use before terminalRun when you need to run multiple commands in the same folder.'
257
+ ].join(' '),
258
+ schema: z.object({ path: z.string(), sessionId: z.string().optional() }),
259
+ handler: async ({ path: relPath, sessionId }, ctx) => {
260
+ const s = getSession(sessionId);
261
+ const base = s?.cwd ?? cfg.roots[0];
262
+ const target = path.resolve(base, relPath);
263
+ const allowed = isPathAllowed(target, cfg, 'exec');
264
+ ctx?.log?.debug?.('[terminalCd] request', { base, path: relPath, target, allowed });
265
+ const res = cd({ path: relPath, sessionId });
266
+ ctx?.log?.info?.('[terminalCd] result', res);
267
+ return res;
268
+ }
269
+ };
270
+ const readFileTool = {
271
+ name: 'terminalReadFile',
272
+ description: [
273
+ 'Read a small text file from the sandboxed workspace.',
274
+ 'Prefer this instead of running `cat` when you only need file contents.',
275
+ 'Path must be inside allowed roots; returns UTF-8 text by default.'
276
+ ].join(' '),
277
+ schema: z.object({ path: z.string(), encoding: z.enum(['utf8', 'base64']).optional(), sessionId: z.string().optional() }),
278
+ handler: async (a, ctx) => {
279
+ const s = getSession(a.sessionId);
280
+ const base = s?.cwd ?? cfg.roots[0];
281
+ const abs = path.resolve(base, a.path);
282
+ const allowed = isPathAllowed(abs, cfg, 'read');
283
+ ctx?.log?.debug?.('[terminalReadFile] request', { base, path: a.path, abs, allowed });
284
+ const res = await read_file(a);
285
+ ctx?.log?.info?.('[terminalReadFile] result', { abs, bytes: Buffer.byteLength(res.contents || ''), encoding: a.encoding || 'utf8' });
286
+ return res;
287
+ }
288
+ };
289
+ // Do not expose start_session as a tool by default to keep the model API simple.
290
+ return { start_session, run_command, cd, read_file, tools: [runCommandTool, cdTool, readFileTool] };
291
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@sisu-ai/tool-terminal",
3
+ "version": "1.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist"
9
+ ],
10
+ "scripts": {
11
+ "build": "tsc -b"
12
+ },
13
+ "dependencies": {
14
+ "minimatch": "^9.0.3",
15
+ "zod": "^3.23.8"
16
+ },
17
+ "peerDependencies": {
18
+ "@sisu-ai/core": "1.0.2"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/finger-gun/sisu",
23
+ "directory": "packages/tools/terminal"
24
+ },
25
+ "homepage": "https://github.com/finger-gun/sisu#readme",
26
+ "bugs": {
27
+ "url": "https://github.com/finger-gun/sisu/issues"
28
+ }
29
+ }