pi-tool-discipline 0.1.5 → 0.1.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/README.md +26 -16
- package/extensions/index.ts +111 -39
- package/extensions/search.ts +142 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,18 +21,22 @@ tool, not bash").
|
|
|
21
21
|
|
|
22
22
|
Two mechanisms, applied automatically in every session:
|
|
23
23
|
|
|
24
|
-
1. **
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
`
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
1. **Activate search tools (root fix).** pi 0.84+ ships real `grep` / `find` /
|
|
25
|
+
`ls` built-in tool definitions but only activates `read`/`bash`/`edit`/
|
|
26
|
+
`write` by default. This extension activates the built-ins, so pi's
|
|
27
|
+
`hasGrep`/`hasFind`/`hasLs` check passes and the conflicting
|
|
28
|
+
`Use bash for file operations like ls, rg, find` guideline is **never
|
|
29
|
+
generated** — the model is never told to use bash for searching. On older
|
|
30
|
+
pi versions without these built-ins, fs-based fallbacks
|
|
31
|
+
(`extensions/search.ts`) are registered instead (visible only when
|
|
32
|
+
`ffgrep`/`fffind` from pi-fff are absent).
|
|
33
|
+
2. **System-prompt injection (rules).** On every agent start, appends an
|
|
34
|
+
idempotent "Tool Discipline" section to the system prompt: content search
|
|
35
|
+
with `ffgrep`, path search with `fffind`, file reads with `read`
|
|
36
|
+
(offset/limit), no bash `grep`/`rg`/`find`/`ls`/`cat`/`sed`/`head`/`tail`/
|
|
37
|
+
`which` for searching, bash reserved for pipelines/git/npm/network, `rg`
|
|
38
|
+
(never `grep`) as the last resort. Also strips the bash guideline text as
|
|
39
|
+
a belt-and-suspenders fallback.
|
|
36
40
|
|
|
37
41
|
## Install
|
|
38
42
|
|
|
@@ -56,10 +60,16 @@ You can also inspect the system prompt: the string
|
|
|
56
60
|
|
|
57
61
|
## Security
|
|
58
62
|
|
|
59
|
-
This extension runs with full system access like any pi extension.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
This extension runs with full system access like any pi extension. What it does:
|
|
64
|
+
- Activates pi's built-in `grep`/`find`/`ls` tools (read-only file search).
|
|
65
|
+
- Injects text into the system prompt (discipline rules).
|
|
66
|
+
- On pi versions without built-in search tools, registers read-only fs-based
|
|
67
|
+
fallback implementations that read file contents under the searched path.
|
|
68
|
+
|
|
69
|
+
It never writes files, executes commands, or touches the network. Note that
|
|
70
|
+
any installed tool, including this one, can be invoked by the model; the
|
|
71
|
+
fallback search tools only read. Review the source in `extensions/` before
|
|
72
|
+
installing.
|
|
63
73
|
|
|
64
74
|
## License
|
|
65
75
|
|
package/extensions/index.ts
CHANGED
|
@@ -13,24 +13,30 @@
|
|
|
13
13
|
* rules that say to use the grep tool instead.
|
|
14
14
|
*
|
|
15
15
|
* How it works:
|
|
16
|
-
* A.
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* conflicting bash guideline is never generated
|
|
16
|
+
* A. Ensures `grep` / `find` / `ls` tools are ACTIVE in the session. pi 0.84+
|
|
17
|
+
* ships real built-in definitions (createAllToolDefinitions) but only
|
|
18
|
+
* activates read/bash/edit/write by default — this extension activates
|
|
19
|
+
* the built-ins, so pi's hasGrep/hasFind/hasLs check passes and the
|
|
20
|
+
* conflicting bash guideline is never generated, and the model always has
|
|
21
|
+
* search tools. On older pi versions without the built-ins, fs-based
|
|
22
|
+
* fallbacks (search.ts) are registered instead (visible only when the
|
|
23
|
+
* FFF counterpart ffgrep/fffind is absent).
|
|
21
24
|
* B. On before_agent_start, appends the tool-discipline rules to the system
|
|
22
|
-
* prompt (idempotent) and strips the bash guideline text as a fallback
|
|
23
|
-
* for environments where the placeholder registration is disabled.
|
|
25
|
+
* prompt (idempotent) and strips the bash guideline text as a fallback.
|
|
24
26
|
*/
|
|
25
27
|
|
|
26
28
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
27
29
|
import { Type } from "typebox";
|
|
30
|
+
import { grepFiles, findFiles, listDir, grepSchema, findSchema, lsSchema } from "./search.js";
|
|
28
31
|
|
|
29
32
|
const MARK = "<!-- pi-tool-discipline:v1 -->";
|
|
30
33
|
|
|
31
|
-
/**
|
|
34
|
+
/** Tool names that flip pi's hasGrep/hasFind/hasLs checks. */
|
|
32
35
|
const PLACEHOLDER_NAMES = ["grep", "find", "ls"] as const;
|
|
33
36
|
|
|
37
|
+
/** FFF search tools that indicate pi-fff (or equivalent) is installed. */
|
|
38
|
+
const FFF_TOOLS = ["ffgrep", "fffind"];
|
|
39
|
+
|
|
34
40
|
/**
|
|
35
41
|
* The exact guidelines pi's builder injects when no search tool is active.
|
|
36
42
|
* Stripped from the system prompt as a fallback (plan B).
|
|
@@ -47,12 +53,47 @@ const DISCIPLINE = `
|
|
|
47
53
|
Search tool priority:
|
|
48
54
|
|
|
49
55
|
1. ffgrep / fffind (from @ff-labs/pi-fff) — always preferred. They work with absolute paths outside the workspace and support regex / path / exclude filters.
|
|
50
|
-
2.
|
|
51
|
-
3. Never run bash \`grep\` or \`find\`. Never use bash \`ls\`/\`cat\`/\`head\`/\`tail\`/\`sed\`/\`which\` directly for searching or reading — use ffgrep / fffind / read instead.
|
|
56
|
+
2. Without pi-fff, use the grep / find / ls TOOLS (fallbacks provided by this extension). If a search seems to miss something, adjust its parameters (path, caseSensitive, maxResults) — never fall back to bash's grep or find.
|
|
57
|
+
3. Never run bash \`grep\` or \`find\`. Never use bash \`ls\`/\`cat\`/\`head\`/\`tail\`/\`sed\`/\`which\` directly for searching or reading — use ffgrep / fffind / read (or the grep/find/ls fallback tools) instead.
|
|
52
58
|
4. Read files with \`read\` (offset/limit for large files).
|
|
53
59
|
5. Bash stays allowed only when dedicated tools cannot do the job: pipelines, git, npm, running programs, network requests, file mutations.
|
|
54
60
|
6. If bash searching is truly unavoidable, use \`rg\` (never \`grep\`).`;
|
|
55
61
|
|
|
62
|
+
interface FallbackTool {
|
|
63
|
+
label: string;
|
|
64
|
+
description: string;
|
|
65
|
+
snippet: string;
|
|
66
|
+
parameters: ReturnType<typeof Type.Object>;
|
|
67
|
+
execute: (params: any, cwd: string) => string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const FALLBACK_TOOLS: Record<string, FallbackTool> = {
|
|
71
|
+
grep: {
|
|
72
|
+
label: "grep (fallback)",
|
|
73
|
+
description:
|
|
74
|
+
"Search file contents for a text pattern. Fallback for environments without ffgrep; prefer ffgrep when available.",
|
|
75
|
+
snippet: "Search file contents (fallback when ffgrep is unavailable)",
|
|
76
|
+
parameters: grepSchema,
|
|
77
|
+
execute: (params, cwd) => grepFiles({ ...params, cwd }),
|
|
78
|
+
},
|
|
79
|
+
find: {
|
|
80
|
+
label: "find (fallback)",
|
|
81
|
+
description:
|
|
82
|
+
"Find files by path/name substring. Fallback for environments without fffind; prefer fffind when available.",
|
|
83
|
+
snippet: "Find files by path/name (fallback when fffind is unavailable)",
|
|
84
|
+
parameters: findSchema,
|
|
85
|
+
execute: (params, cwd) => findFiles({ ...params, cwd }),
|
|
86
|
+
},
|
|
87
|
+
ls: {
|
|
88
|
+
label: "ls (fallback)",
|
|
89
|
+
description:
|
|
90
|
+
"List directory entries. Fallback for environments without fffind; prefer fffind when available.",
|
|
91
|
+
snippet: "List directory entries (fallback when fffind is unavailable)",
|
|
92
|
+
parameters: lsSchema,
|
|
93
|
+
execute: (params, cwd) => listDir({ ...params, cwd }),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
56
97
|
function stripBashGuidelines(prompt: string): string {
|
|
57
98
|
let out = prompt;
|
|
58
99
|
for (const guideline of BASH_GUIDELINES) {
|
|
@@ -62,35 +103,61 @@ function stripBashGuidelines(prompt: string): string {
|
|
|
62
103
|
}
|
|
63
104
|
|
|
64
105
|
export default function toolDiscipline(pi: ExtensionAPI) {
|
|
65
|
-
// A. Register
|
|
106
|
+
// A. Register search-tool names so pi stops generating the bash guideline.
|
|
66
107
|
// Done in session_start: action methods (getAllTools/registerTool) are not
|
|
67
108
|
// available during extension loading, and tools registered here are
|
|
68
109
|
// refreshed into the session (and system prompt) immediately.
|
|
69
110
|
pi.on("session_start", () => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
111
|
+
const all = new Set(pi.getAllTools().map((t) => t.name));
|
|
112
|
+
const active = new Set(pi.getActiveTools());
|
|
113
|
+
const hasFfgrep = active.has("ffgrep");
|
|
114
|
+
const hasFffind = active.has("fffind");
|
|
115
|
+
|
|
116
|
+
// 1) Activate built-in grep/find/ls when they exist (pi 0.84+ ships real
|
|
117
|
+
// definitions via createAllToolDefinitions but only activates
|
|
118
|
+
// read/bash/edit/write by default). Activating them flips
|
|
119
|
+
// hasGrep/hasFind/hasLs so the bash guideline is never generated, and
|
|
120
|
+
// gives the model real built-in search tools.
|
|
121
|
+
const toActivate = PLACEHOLDER_NAMES.filter((name) => all.has(name) && !active.has(name));
|
|
122
|
+
if (toActivate.length > 0) {
|
|
123
|
+
// setActiveTools rebuilds the system prompt immediately.
|
|
124
|
+
pi.setActiveTools([...active, ...toActivate]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 2) Older pi without built-in grep/find/ls: register working fs-based
|
|
128
|
+
// fallbacks (search.ts). Visibility is per-capability: hidden while
|
|
129
|
+
// the FFF counterpart is active, visible when it is not.
|
|
130
|
+
let registeredAny = false;
|
|
131
|
+
const registerSearchTool = (name: string, fffActive: boolean) => {
|
|
132
|
+
if (all.has(name)) return; // built-in exists — handled above
|
|
133
|
+
const fallback = FALLBACK_TOOLS[name];
|
|
73
134
|
pi.registerTool({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
135
|
+
name,
|
|
136
|
+
label: fallback.label,
|
|
137
|
+
description: fallback.description,
|
|
138
|
+
promptSnippet: fffActive ? undefined : fallback.snippet,
|
|
139
|
+
promptGuidelines: [
|
|
140
|
+
"Use ffgrep/fffind when they are available; grep/find/ls are fallbacks only for environments without pi-fff.",
|
|
141
|
+
],
|
|
142
|
+
parameters: fallback.parameters,
|
|
143
|
+
async execute(_toolCallId, params, _signal, _onUpdate, execCtx) {
|
|
144
|
+
const text = fallback.execute(params ?? {}, execCtx.cwd);
|
|
145
|
+
return {
|
|
146
|
+
content: [{ type: "text", text }],
|
|
147
|
+
details: { fallback: true },
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
registeredAny = true;
|
|
152
|
+
};
|
|
153
|
+
registerSearchTool("grep", hasFfgrep);
|
|
154
|
+
registerSearchTool("find", hasFffind);
|
|
155
|
+
registerSearchTool("ls", hasFffind);
|
|
156
|
+
// Tools registered in session_start do not enter selectedTools until the
|
|
157
|
+
// registry is refreshed. refreshTools exists at runtime (ExtensionActions)
|
|
158
|
+
// but is not declared on ExtensionAPI's type. Known limitation: with peer
|
|
159
|
+
// version "*", other pi versions may differ.
|
|
160
|
+
if (registeredAny) (pi as unknown as { refreshTools: () => void }).refreshTools();
|
|
94
161
|
});
|
|
95
162
|
|
|
96
163
|
// B. Inject the discipline into the system prompt (idempotent per turn).
|
|
@@ -101,16 +168,21 @@ export default function toolDiscipline(pi: ExtensionAPI) {
|
|
|
101
168
|
return { systemPrompt: `${prompt}\n${MARK}\n${DISCIPLINE}` };
|
|
102
169
|
});
|
|
103
170
|
|
|
104
|
-
// Status command: /tool-discipline — verify
|
|
171
|
+
// Status command: /tool-discipline — verify tool activation and injection.
|
|
105
172
|
pi.registerCommand("tool-discipline", {
|
|
106
|
-
description: "Show pi-tool-discipline status (
|
|
173
|
+
description: "Show pi-tool-discipline status (tools + injected guideline)",
|
|
107
174
|
handler: async (_args, ctx) => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
175
|
+
const all = new Set(pi.getAllTools().map((t) => t.name));
|
|
176
|
+
const active = new Set(pi.getActiveTools());
|
|
177
|
+
const fff = FFF_TOOLS.filter((name) => active.has(name));
|
|
178
|
+
const registered = PLACEHOLDER_NAMES.filter((n) => all.has(n));
|
|
179
|
+
const activeTools = PLACEHOLDER_NAMES.filter((n) => active.has(n));
|
|
110
180
|
const injected = ctx.getSystemPrompt().includes(MARK);
|
|
111
181
|
ctx.ui.notify(
|
|
112
182
|
`pi-tool-discipline\n` +
|
|
113
|
-
`
|
|
183
|
+
`fff active: ${fff.length > 0 ? fff.join(", ") : "(none)"}\n` +
|
|
184
|
+
`grep/find/ls defined: ${registered.length > 0 ? registered.join(", ") : "(none)"}\n` +
|
|
185
|
+
`grep/find/ls active: ${activeTools.length > 0 ? activeTools.join(", ") : "(none)"}\n` +
|
|
114
186
|
`discipline injected: ${injected ? "yes" : "no"}`,
|
|
115
187
|
"info",
|
|
116
188
|
);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fallback search implementations for environments without @ff-labs/pi-fff.
|
|
3
|
+
* Pure Node fs-based; no shell, no external deps.
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync, readFileSync, statSync, lstatSync } from "fs";
|
|
6
|
+
import { join, resolve, relative } from "path";
|
|
7
|
+
import { Type } from "typebox";
|
|
8
|
+
|
|
9
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", ".hg", ".svn"]);
|
|
10
|
+
const MAX_FILES = 2000;
|
|
11
|
+
const MAX_VISITED = 5000; // total entries touched, bounds slow/odd trees
|
|
12
|
+
const MAX_FILE_BYTES = 1024 * 1024; // content search skips files larger than 1 MiB
|
|
13
|
+
const MAX_OUTPUT_BYTES = 50 * 1024;
|
|
14
|
+
const TRUNCATE_MARKER = "\n[output truncated]";
|
|
15
|
+
|
|
16
|
+
interface FileMatch {
|
|
17
|
+
file: string;
|
|
18
|
+
line: number;
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Collect regular files under dir (depth-limited, symlink-safe, bounded).
|
|
24
|
+
* Only isFile() entries are collected — FIFOs, devices, sockets can block a
|
|
25
|
+
* synchronous read. Synchronous by design (fallback tools, low frequency);
|
|
26
|
+
* the walk is bounded by MAX_FILES/MAX_VISITED so it cannot hang forever.
|
|
27
|
+
* Does NOT filter by size — path search must find large files too.
|
|
28
|
+
*/
|
|
29
|
+
function walk(dir: string, out: string[], state: { visited: number }, depth = 0): void {
|
|
30
|
+
if (depth > 12 || state.visited >= MAX_VISITED) return;
|
|
31
|
+
let entries: string[];
|
|
32
|
+
try {
|
|
33
|
+
entries = readdirSync(dir);
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (out.length >= MAX_FILES || state.visited >= MAX_VISITED) return; // in-loop bound
|
|
39
|
+
if (entry.startsWith(".") || SKIP_DIRS.has(entry)) continue;
|
|
40
|
+
const p = join(dir, entry);
|
|
41
|
+
state.visited++;
|
|
42
|
+
try {
|
|
43
|
+
const lst = lstatSync(p);
|
|
44
|
+
if (lst.isSymbolicLink()) continue; // never follow symlinks
|
|
45
|
+
if (lst.isDirectory()) walk(p, out, state, depth + 1);
|
|
46
|
+
else if (lst.isFile()) out.push(p); // regular files only
|
|
47
|
+
} catch {
|
|
48
|
+
// unreadable entries are skipped
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Truncate by BYTE length, keep complete lines, reserve space for the marker. */
|
|
54
|
+
function truncate(text: string, maxBytes = MAX_OUTPUT_BYTES): string {
|
|
55
|
+
const buf = Buffer.from(text, "utf8");
|
|
56
|
+
const budget = maxBytes - Buffer.byteLength(TRUNCATE_MARKER);
|
|
57
|
+
if (buf.length <= budget) return text;
|
|
58
|
+
const cut = buf.subarray(0, budget).toString("utf8");
|
|
59
|
+
const lastNewline = cut.lastIndexOf("\n");
|
|
60
|
+
if (lastNewline <= 0) return TRUNCATE_MARKER.trim(); // nothing complete fits
|
|
61
|
+
return `${cut.slice(0, lastNewline)}\n${TRUNCATE_MARKER}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function grepFiles(opts: {
|
|
65
|
+
pattern: string;
|
|
66
|
+
path?: string;
|
|
67
|
+
caseSensitive?: boolean;
|
|
68
|
+
maxResults?: number;
|
|
69
|
+
cwd: string;
|
|
70
|
+
}): string {
|
|
71
|
+
const root = resolve(opts.cwd, opts.path || ".");
|
|
72
|
+
const pattern = opts.caseSensitive ? opts.pattern : opts.pattern.toLowerCase();
|
|
73
|
+
const limit = opts.maxResults ?? 100;
|
|
74
|
+
const files: string[] = [];
|
|
75
|
+
walk(root, files, { visited: 0 });
|
|
76
|
+
const matches: FileMatch[] = [];
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
if (matches.length >= limit) break;
|
|
79
|
+
try {
|
|
80
|
+
if (statSync(file).size > MAX_FILE_BYTES) continue; // cap only before reading content
|
|
81
|
+
} catch {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
let content: string;
|
|
85
|
+
try {
|
|
86
|
+
content = readFileSync(file, "utf8");
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const lines = content.split("\n");
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
const haystack = opts.caseSensitive ? lines[i] : lines[i].toLowerCase();
|
|
93
|
+
if (haystack.includes(pattern)) {
|
|
94
|
+
matches.push({ file: relative(root, file), line: i + 1, text: lines[i].trim().slice(0, 200) });
|
|
95
|
+
if (matches.length >= limit) break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (matches.length === 0) return "No matches found";
|
|
100
|
+
let out = "";
|
|
101
|
+
for (const m of matches) out += `${m.file}:${m.line}: ${m.text}\n`;
|
|
102
|
+
return truncate(out);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function findFiles(opts: { pattern?: string; path?: string; maxResults?: number; cwd: string }): string {
|
|
106
|
+
const root = resolve(opts.cwd, opts.path || ".");
|
|
107
|
+
const files: string[] = [];
|
|
108
|
+
walk(root, files, { visited: 0 });
|
|
109
|
+
const needle = opts.pattern?.toLowerCase();
|
|
110
|
+
// Match against the RELATIVE path so a pattern matching an ancestor
|
|
111
|
+
// directory does not hit every file, and rendered output stays relative.
|
|
112
|
+
const rel = files.map((f) => relative(root, f));
|
|
113
|
+
const hits = needle ? rel.filter((r) => r.toLowerCase().includes(needle)) : rel;
|
|
114
|
+
if (hits.length === 0) return "No matching files found";
|
|
115
|
+
return truncate(hits.slice(0, opts.maxResults ?? 100).join("\n"));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function listDir(opts: { path?: string; cwd: string }): string {
|
|
119
|
+
const dir = resolve(opts.cwd, opts.path || ".");
|
|
120
|
+
try {
|
|
121
|
+
return truncate(readdirSync(dir).join("\n"));
|
|
122
|
+
} catch (error: any) {
|
|
123
|
+
return `Error listing ${dir}: ${error.message}`;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const grepSchema = Type.Object({
|
|
128
|
+
pattern: Type.String({ description: "Text to search for in file contents" }),
|
|
129
|
+
path: Type.Optional(Type.String({ description: "Directory to search (defaults to cwd)" })),
|
|
130
|
+
caseSensitive: Type.Optional(Type.Boolean({ description: "Case-sensitive match (default false)" })),
|
|
131
|
+
maxResults: Type.Optional(Type.Integer({ minimum: 1, description: "Max matches (default 100)" })),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
export const findSchema = Type.Object({
|
|
135
|
+
pattern: Type.Optional(Type.String({ description: "Substring to match in file path or name (empty lists all)" })),
|
|
136
|
+
path: Type.Optional(Type.String({ description: "Directory to search (defaults to cwd)" })),
|
|
137
|
+
maxResults: Type.Optional(Type.Integer({ minimum: 1, description: "Max results (default 100)" })),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const lsSchema = Type.Object({
|
|
141
|
+
path: Type.Optional(Type.String({ description: "Directory to list (defaults to cwd)" })),
|
|
142
|
+
});
|
package/package.json
CHANGED