pi-neutrasearch 0.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/LICENSE +21 -0
- package/README.md +91 -0
- package/index.js +152 -0
- package/lib.js +181 -0
- package/package.json +47 -0
- package/skills/neutrasearch/SKILL.md +22 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NetroAki
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# pi-neutrasearch
|
|
2
|
+
|
|
3
|
+
Token-efficient native indexed filename and path search for [Pi](https://pi.dev).
|
|
4
|
+
|
|
5
|
+
`pi-neutrasearch` registers one read-only `neutrasearch` tool. It queries an existing Neutrasearch compact index without walking directories, opening file contents, writing files, elevating privileges, or using the network.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install Neutrasearch and build its index first, then install the Pi package:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
pi install npm:pi-neutrasearch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Use `/neutrasearch` inside Pi to confirm executable discovery.
|
|
16
|
+
|
|
17
|
+
The package resolves, in order:
|
|
18
|
+
|
|
19
|
+
1. `NEUTRASEARCH_QUERY`
|
|
20
|
+
2. `neutrasearch-query` on `PATH`
|
|
21
|
+
3. `NEUTRASEARCH_BIN`
|
|
22
|
+
4. `neutrasearch` on `PATH`
|
|
23
|
+
|
|
24
|
+
Set `NEUTRASEARCH_INDEX` to override Neutrasearch's platform-default index path.
|
|
25
|
+
|
|
26
|
+
## Agent tool
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"action": "search",
|
|
31
|
+
"query": "invoice ext:pdf",
|
|
32
|
+
"scope": "/workspace/project",
|
|
33
|
+
"limit": 20,
|
|
34
|
+
"relative_paths": true,
|
|
35
|
+
"metadata": false,
|
|
36
|
+
"max_chars": 6000
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The output is deliberately compact:
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
scope=/workspace/project
|
|
44
|
+
matched=2 returned=2 search_us=184
|
|
45
|
+
records/2025/invoice-1042.pdf
|
|
46
|
+
records/2024/invoice-991.pdf
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Defaults are optimized for agent context:
|
|
50
|
+
|
|
51
|
+
- 20 results
|
|
52
|
+
- relative paths
|
|
53
|
+
- no metadata columns
|
|
54
|
+
- 6,000-character hard output budget
|
|
55
|
+
- two-line numeric header
|
|
56
|
+
|
|
57
|
+
Use `metadata: true` only when kind, size, or modification time is needed.
|
|
58
|
+
|
|
59
|
+
## Search language
|
|
60
|
+
|
|
61
|
+
- words and `"quoted phrases"`
|
|
62
|
+
- `ext:rs,toml`
|
|
63
|
+
- `kind:file`, `kind:dir`, or `kind:link`
|
|
64
|
+
- `fs:btrfs,ext4,ntfs`
|
|
65
|
+
- `size:>10M`, `size:<4K`, or `size:1M..20M`
|
|
66
|
+
|
|
67
|
+
Neutrasearch searches indexed names and paths, not file contents. Locate candidates with this tool, then use targeted `read` or `grep` calls.
|
|
68
|
+
|
|
69
|
+
## Scope and privacy
|
|
70
|
+
|
|
71
|
+
Every search is scoped to the current Pi workspace by default. A requested scope must resolve inside that workspace.
|
|
72
|
+
|
|
73
|
+
To authorize additional roots explicitly, set an OS-path-delimited list:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
export NEUTRASEARCH_PI_ALLOWED_ROOTS="$HOME/Documents:$HOME/Projects"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The extension verifies returned paths again and drops any path outside the approved scope. It never starts the scanner or helper daemon.
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
cd packages/pi-neutrasearch
|
|
85
|
+
npm test
|
|
86
|
+
pi -e .
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { compactSearchResult, limits, queryArguments, resolveNeutrasearch, resolveScope } from "./lib.js";
|
|
2
|
+
|
|
3
|
+
const TOOL_NAME = "neutrasearch";
|
|
4
|
+
|
|
5
|
+
function shortError(value, max = 1600) {
|
|
6
|
+
const text = String(value || "unknown error").replace(/\s+/g, " ").trim();
|
|
7
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function result(text, details = {}) {
|
|
11
|
+
return { content: [{ type: "text", text }], details };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function status(binary) {
|
|
15
|
+
if (!binary) {
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
tool: TOOL_NAME,
|
|
19
|
+
error: "Neutrasearch executable not found",
|
|
20
|
+
setup: "Install Neutrasearch, or set NEUTRASEARCH_QUERY / NEUTRASEARCH_BIN.",
|
|
21
|
+
token_defaults: { limit: limits.defaultLimit, max_chars: limits.defaultMaxChars },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
ok: true,
|
|
26
|
+
tool: TOOL_NAME,
|
|
27
|
+
executable: binary.command,
|
|
28
|
+
executable_kind: binary.kind,
|
|
29
|
+
index: process.env.NEUTRASEARCH_INDEX || "platform default",
|
|
30
|
+
default_scope: "current Pi workspace",
|
|
31
|
+
extra_scope_env: "NEUTRASEARCH_PI_ALLOWED_ROOTS",
|
|
32
|
+
token_defaults: {
|
|
33
|
+
limit: limits.defaultLimit,
|
|
34
|
+
max_chars: limits.defaultMaxChars,
|
|
35
|
+
relative_paths: true,
|
|
36
|
+
metadata: false,
|
|
37
|
+
},
|
|
38
|
+
safety: "read-only index query; never scans, indexes, elevates, writes, or uses the network",
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function search(pi, params, signal, ctx) {
|
|
43
|
+
if (!String(params.query || "").trim()) throw new Error("query is required for action=search");
|
|
44
|
+
const binary = resolveNeutrasearch();
|
|
45
|
+
if (!binary) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"Neutrasearch executable not found. Install Neutrasearch or set NEUTRASEARCH_QUERY / NEUTRASEARCH_BIN.",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const { scope } = resolveScope(params.scope, ctx.cwd);
|
|
51
|
+
const { args, limit } = queryArguments(params, scope);
|
|
52
|
+
const timeout = Math.max(1000, Math.min(30000, Math.trunc(Number(params.timeout_ms) || 10000)));
|
|
53
|
+
const execution = await pi.exec(binary.command, [...binary.prefix, ...args], {
|
|
54
|
+
cwd: ctx.cwd,
|
|
55
|
+
signal,
|
|
56
|
+
timeout,
|
|
57
|
+
});
|
|
58
|
+
if (execution.code !== 0) {
|
|
59
|
+
throw new Error(`Neutrasearch exited ${execution.code}: ${shortError(execution.stderr || execution.stdout)}`);
|
|
60
|
+
}
|
|
61
|
+
let payload;
|
|
62
|
+
try {
|
|
63
|
+
payload = JSON.parse(execution.stdout);
|
|
64
|
+
} catch {
|
|
65
|
+
throw new Error(`Neutrasearch returned non-JSON output: ${shortError(execution.stdout)}`);
|
|
66
|
+
}
|
|
67
|
+
const compact = compactSearchResult(payload, {
|
|
68
|
+
scope,
|
|
69
|
+
relative: params.relative_paths !== false,
|
|
70
|
+
metadata: Boolean(params.metadata),
|
|
71
|
+
maxChars: params.max_chars,
|
|
72
|
+
});
|
|
73
|
+
return result(compact.text, {
|
|
74
|
+
...compact.details,
|
|
75
|
+
query: params.query,
|
|
76
|
+
limit,
|
|
77
|
+
executable_kind: binary.kind,
|
|
78
|
+
token_efficient: true,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const PARAMETERS = {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
action: {
|
|
86
|
+
enum: ["search", "status"],
|
|
87
|
+
description: "search (default) or status.",
|
|
88
|
+
},
|
|
89
|
+
query: {
|
|
90
|
+
type: "string",
|
|
91
|
+
description: "Filename/path query. Supports ext:, kind:, fs:, size:, and quoted terms.",
|
|
92
|
+
},
|
|
93
|
+
scope: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Existing directory inside the current workspace. Defaults to the workspace root.",
|
|
96
|
+
},
|
|
97
|
+
limit: {
|
|
98
|
+
type: "integer",
|
|
99
|
+
minimum: 1,
|
|
100
|
+
maximum: limits.maxLimit,
|
|
101
|
+
description: `Maximum matches. Default ${limits.defaultLimit}; keep small for token efficiency.`,
|
|
102
|
+
},
|
|
103
|
+
relative_paths: {
|
|
104
|
+
type: "boolean",
|
|
105
|
+
description: "Return paths relative to scope. Default true.",
|
|
106
|
+
},
|
|
107
|
+
metadata: {
|
|
108
|
+
type: "boolean",
|
|
109
|
+
description: "Include kind, size, and mtime columns. Default false.",
|
|
110
|
+
},
|
|
111
|
+
max_chars: {
|
|
112
|
+
type: "integer",
|
|
113
|
+
minimum: 500,
|
|
114
|
+
maximum: limits.maxOutputChars,
|
|
115
|
+
description: `Hard output budget. Default ${limits.defaultMaxChars}.`,
|
|
116
|
+
},
|
|
117
|
+
timeout_ms: {
|
|
118
|
+
type: "integer",
|
|
119
|
+
minimum: 1000,
|
|
120
|
+
maximum: 30000,
|
|
121
|
+
description: "Query timeout. Default 10000.",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
additionalProperties: false,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default function register(pi) {
|
|
128
|
+
pi.registerTool({
|
|
129
|
+
name: TOOL_NAME,
|
|
130
|
+
label: "Neutrasearch",
|
|
131
|
+
description:
|
|
132
|
+
"Token-efficient, read-only indexed filename/path search. Prefer this over find or broad filesystem scans when locating files. It does not search file contents; use grep only after locating candidate files.",
|
|
133
|
+
parameters: PARAMETERS,
|
|
134
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
135
|
+
const action = String(params?.action || "search");
|
|
136
|
+
if (action === "status") {
|
|
137
|
+
const current = status(resolveNeutrasearch());
|
|
138
|
+
return result(JSON.stringify(current), current);
|
|
139
|
+
}
|
|
140
|
+
if (action !== "search") throw new Error(`unknown action: ${action}`);
|
|
141
|
+
return search(pi, params || {}, signal, ctx);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
pi.registerCommand("neutrasearch", {
|
|
146
|
+
description: "Show Neutrasearch Pi integration status",
|
|
147
|
+
handler: async (_args, ctx) => {
|
|
148
|
+
const current = status(resolveNeutrasearch());
|
|
149
|
+
ctx.ui.notify(current.ok ? `Neutrasearch ready: ${current.executable}` : current.error, current.ok ? "info" : "warning");
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
package/lib.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_LIMIT = 20;
|
|
6
|
+
const MAX_LIMIT = 200;
|
|
7
|
+
const DEFAULT_MAX_CHARS = 6000;
|
|
8
|
+
const MAX_OUTPUT_CHARS = 30000;
|
|
9
|
+
|
|
10
|
+
function clampInteger(value, fallback, minimum, maximum) {
|
|
11
|
+
const parsed = Number(value);
|
|
12
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
13
|
+
return Math.max(minimum, Math.min(maximum, Math.trunc(parsed)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function executableNames(name, platform = process.platform, pathExt = process.env.PATHEXT) {
|
|
17
|
+
if (platform !== "win32" || path.extname(name)) return [name];
|
|
18
|
+
const extensions = (pathExt || ".EXE;.CMD;.BAT;.COM")
|
|
19
|
+
.split(";")
|
|
20
|
+
.filter(Boolean)
|
|
21
|
+
.map((extension) => extension.toLowerCase());
|
|
22
|
+
return [name, ...extensions.map((extension) => `${name}${extension}`)];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function canExecute(candidate, platform = process.platform) {
|
|
26
|
+
try {
|
|
27
|
+
fs.accessSync(candidate, platform === "win32" ? fs.constants.F_OK : fs.constants.X_OK);
|
|
28
|
+
return fs.statSync(candidate).isFile();
|
|
29
|
+
} catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findOnPath(command, env = process.env, platform = process.platform) {
|
|
35
|
+
if (path.isAbsolute(command) || command.includes("/") || command.includes("\\")) {
|
|
36
|
+
const expanded = command.startsWith("~/") ? path.join(os.homedir(), command.slice(2)) : command;
|
|
37
|
+
return canExecute(expanded, platform) ? path.resolve(expanded) : null;
|
|
38
|
+
}
|
|
39
|
+
const pathValue = env.PATH || env.Path || env.path || "";
|
|
40
|
+
for (const directory of pathValue.split(path.delimiter).filter(Boolean)) {
|
|
41
|
+
for (const name of executableNames(command, platform, env.PATHEXT)) {
|
|
42
|
+
const candidate = path.join(directory.replace(/^~(?=$|[\\/])/, os.homedir()), name);
|
|
43
|
+
if (canExecute(candidate, platform)) return candidate;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resolveNeutrasearch(env = process.env, platform = process.platform) {
|
|
50
|
+
const candidates = [
|
|
51
|
+
env.NEUTRASEARCH_QUERY
|
|
52
|
+
? { command: env.NEUTRASEARCH_QUERY, prefix: [], kind: "query" }
|
|
53
|
+
: null,
|
|
54
|
+
{ command: "neutrasearch-query", prefix: [], kind: "query" },
|
|
55
|
+
env.NEUTRASEARCH_BIN
|
|
56
|
+
? { command: env.NEUTRASEARCH_BIN, prefix: ["search"], kind: "launcher" }
|
|
57
|
+
: null,
|
|
58
|
+
{ command: "neutrasearch", prefix: ["search"], kind: "launcher" },
|
|
59
|
+
].filter(Boolean);
|
|
60
|
+
|
|
61
|
+
for (const candidate of candidates) {
|
|
62
|
+
const resolved = findOnPath(candidate.command, env, platform);
|
|
63
|
+
if (resolved) return { ...candidate, command: resolved };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function canonicalDirectory(input) {
|
|
69
|
+
const resolved = fs.realpathSync(path.resolve(input));
|
|
70
|
+
if (!fs.statSync(resolved).isDirectory()) throw new Error(`scope is not a directory: ${input}`);
|
|
71
|
+
return resolved;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function comparable(value, platform = process.platform) {
|
|
75
|
+
const normalized = path.resolve(value).replace(/[\\/]+$/, "");
|
|
76
|
+
return platform === "win32" || platform === "darwin" ? normalized.toLowerCase() : normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function pathIsInside(candidate, root, platform = process.platform) {
|
|
80
|
+
const normalizedCandidate = comparable(candidate, platform);
|
|
81
|
+
const normalizedRoot = comparable(root, platform);
|
|
82
|
+
if (normalizedCandidate === normalizedRoot) return true;
|
|
83
|
+
return normalizedCandidate.startsWith(`${normalizedRoot}${path.sep}`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function resolveScope(requested, cwd, env = process.env, platform = process.platform) {
|
|
87
|
+
const workspace = canonicalDirectory(cwd);
|
|
88
|
+
const configured = String(env.NEUTRASEARCH_PI_ALLOWED_ROOTS || "")
|
|
89
|
+
.split(path.delimiter)
|
|
90
|
+
.map((entry) => entry.trim())
|
|
91
|
+
.filter(Boolean)
|
|
92
|
+
.map(canonicalDirectory);
|
|
93
|
+
const allowed = [workspace, ...configured];
|
|
94
|
+
const scope = canonicalDirectory(requested || workspace);
|
|
95
|
+
if (!allowed.some((root) => pathIsInside(scope, root, platform))) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
"scope is outside this Pi workspace; add its canonical root to NEUTRASEARCH_PI_ALLOWED_ROOTS to opt in",
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return { scope, workspace, allowed };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pathForOutput(candidate, scope, relative) {
|
|
104
|
+
if (!relative) return candidate;
|
|
105
|
+
const rel = path.relative(scope, candidate);
|
|
106
|
+
return rel || ".";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function compactSearchResult(payload, options) {
|
|
110
|
+
if (!payload || !Array.isArray(payload.paths)) throw new Error("Neutrasearch returned invalid JSON");
|
|
111
|
+
const scope = options.scope;
|
|
112
|
+
const relative = options.relative !== false;
|
|
113
|
+
const maxChars = clampInteger(options.maxChars, DEFAULT_MAX_CHARS, 500, MAX_OUTPUT_CHARS);
|
|
114
|
+
const recordsByPath = new Map(
|
|
115
|
+
Array.isArray(payload.records) ? payload.records.map((record) => [record.path, record]) : [],
|
|
116
|
+
);
|
|
117
|
+
const safe = [];
|
|
118
|
+
let rejected = 0;
|
|
119
|
+
for (const candidate of payload.paths) {
|
|
120
|
+
if (typeof candidate !== "string" || !path.isAbsolute(candidate) || !pathIsInside(candidate, scope)) {
|
|
121
|
+
rejected += 1;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
safe.push(candidate);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = [
|
|
128
|
+
`scope=${scope}`,
|
|
129
|
+
`matched=${Number(payload.matched || 0)} returned=${safe.length} search_us=${Number(payload.search_us || 0)}`,
|
|
130
|
+
];
|
|
131
|
+
let omittedByBudget = 0;
|
|
132
|
+
for (let index = 0; index < safe.length; index += 1) {
|
|
133
|
+
const candidate = safe[index];
|
|
134
|
+
const record = recordsByPath.get(candidate);
|
|
135
|
+
const displayPath = pathForOutput(candidate, scope, relative);
|
|
136
|
+
const line = options.metadata && record
|
|
137
|
+
? `${displayPath}\t${record.kind}\t${record.size}\t${record.mtime}`
|
|
138
|
+
: displayPath;
|
|
139
|
+
if (lines.join("\n").length + line.length + 1 > maxChars) {
|
|
140
|
+
omittedByBudget = safe.length - index;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
lines.push(line);
|
|
144
|
+
}
|
|
145
|
+
if (omittedByBudget || rejected) {
|
|
146
|
+
lines.push(`omitted_budget=${omittedByBudget} rejected_outside_scope=${rejected}`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
text: lines.join("\n"),
|
|
150
|
+
details: {
|
|
151
|
+
scope,
|
|
152
|
+
matched: Number(payload.matched || 0),
|
|
153
|
+
returned: safe.length,
|
|
154
|
+
emitted: safe.length - omittedByBudget,
|
|
155
|
+
omitted_by_budget: omittedByBudget,
|
|
156
|
+
rejected_outside_scope: rejected,
|
|
157
|
+
search_us: Number(payload.search_us || 0),
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function queryArguments(params, scope, env = process.env) {
|
|
163
|
+
const limit = clampInteger(params.limit, DEFAULT_LIMIT, 1, MAX_LIMIT);
|
|
164
|
+
const args = [
|
|
165
|
+
String(params.query || ""),
|
|
166
|
+
"--scope",
|
|
167
|
+
scope,
|
|
168
|
+
"--limit",
|
|
169
|
+
String(limit),
|
|
170
|
+
params.metadata ? "--json" : "--json-paths",
|
|
171
|
+
];
|
|
172
|
+
if (env.NEUTRASEARCH_INDEX) args.push("--index", env.NEUTRASEARCH_INDEX);
|
|
173
|
+
return { args, limit };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const limits = {
|
|
177
|
+
defaultLimit: DEFAULT_LIMIT,
|
|
178
|
+
maxLimit: MAX_LIMIT,
|
|
179
|
+
defaultMaxChars: DEFAULT_MAX_CHARS,
|
|
180
|
+
maxOutputChars: MAX_OUTPUT_CHARS,
|
|
181
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-neutrasearch",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Token-efficient native indexed filename and path search for Pi agents",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "NetroAki",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"neutrasearch",
|
|
12
|
+
"file-search",
|
|
13
|
+
"indexed-search",
|
|
14
|
+
"agent-tools"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/NetroAki/neutrasearch.git",
|
|
19
|
+
"directory": "packages/pi-neutrasearch"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/NetroAki/neutrasearch#pi-package",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/NetroAki/neutrasearch/issues"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"index.js",
|
|
27
|
+
"lib.js",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"skills"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "node --test",
|
|
37
|
+
"prepublishOnly": "npm test"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"extensions": [
|
|
41
|
+
"./index.js"
|
|
42
|
+
],
|
|
43
|
+
"skills": [
|
|
44
|
+
"./skills"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: use-neutrasearch
|
|
3
|
+
description: Locate filenames and paths through the local Neutrasearch index instead of walking the filesystem
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Use Neutrasearch
|
|
7
|
+
|
|
8
|
+
Use the `neutrasearch` tool before `find`, directory walking, or broad globbing when the task is to locate files or folders by name, extension, type, size, or indexed path.
|
|
9
|
+
|
|
10
|
+
## Token-efficient defaults
|
|
11
|
+
|
|
12
|
+
1. Start with `limit: 20`, `relative_paths: true`, and `metadata: false`.
|
|
13
|
+
2. Scope defaults to the current Pi workspace. Narrow `scope` further when possible.
|
|
14
|
+
3. Refine with query filters such as `ext:rs,toml`, `kind:file`, `kind:dir`, `size:>10M`, or quoted terms before increasing the limit.
|
|
15
|
+
4. Request metadata only when size, kind, filesystem, or modification time is needed.
|
|
16
|
+
5. Increase `max_chars` or `limit` only after a narrow query still omits needed candidates.
|
|
17
|
+
|
|
18
|
+
## Boundary
|
|
19
|
+
|
|
20
|
+
Neutrasearch searches indexed metadata—filenames and paths—not file contents. After locating a small candidate set, use `read` or targeted `grep` for content inspection.
|
|
21
|
+
|
|
22
|
+
The tool is read-only. It must not initiate indexing, request privileges, write files, or contact the network. Outside-workspace searches require the user to pre-authorize canonical roots through `NEUTRASEARCH_PI_ALLOWED_ROOTS`.
|