@graphpilot-oss/graphpilot 0.0.1 → 1.0.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/CHANGELOG.md +72 -126
- package/README.md +290 -102
- package/dist/cli.js +41 -1
- package/dist/cli.js.map +1 -1
- package/dist/edges.js +22 -11
- package/dist/edges.js.map +1 -1
- package/dist/indexer.js +3 -3
- package/dist/indexer.js.map +1 -1
- package/dist/init.d.ts +28 -0
- package/dist/init.js +112 -0
- package/dist/init.js.map +1 -0
- package/dist/interactions.d.ts +5 -4
- package/dist/interactions.js +0 -0
- package/dist/interactions.js.map +1 -1
- package/dist/mcp.js +119 -90
- package/dist/mcp.js.map +1 -1
- package/dist/repo-resolve.d.ts +47 -0
- package/dist/repo-resolve.js +195 -0
- package/dist/repo-resolve.js.map +1 -0
- package/dist/storage.js +10 -1
- package/dist/storage.js.map +1 -1
- package/dist/symbols.js +26 -2
- package/dist/symbols.js.map +1 -1
- package/dist/validation.js +30 -4
- package/dist/validation.js.map +1 -1
- package/dist/validators.d.ts +1 -5
- package/dist/validators.js +0 -11
- package/dist/validators.js.map +1 -1
- package/dist/watcher.d.ts +10 -0
- package/dist/watcher.js +70 -7
- package/dist/watcher.js.map +1 -1
- package/examples/README.md +105 -0
- package/examples/claude-code/README.md +125 -0
- package/examples/claude-code/claude-routing.md +102 -0
- package/examples/claude-code/claude_config.json +8 -0
- package/examples/cline/.clinerules +39 -0
- package/examples/cline/README.md +104 -0
- package/examples/cline/cline_mcp_settings.json +10 -0
- package/examples/continue/.continuerules +39 -0
- package/examples/continue/README.md +98 -0
- package/examples/continue/config.json +13 -0
- package/examples/cursor/.cursorrules +39 -0
- package/examples/cursor/README.md +98 -0
- package/examples/cursor/mcp.json +11 -0
- package/examples/windsurf/.windsurfrules +39 -0
- package/examples/windsurf/README.md +85 -0
- package/examples/windsurf/mcp_config.json +8 -0
- package/package.json +14 -4
- package/.editorconfig +0 -15
- package/.github/CODEOWNERS +0 -22
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
- package/.github/ISSUE_TEMPLATE/config.yml +0 -5
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
- package/.github/dependabot.yml +0 -15
- package/.github/workflows/ci.yml +0 -62
- package/.github/workflows/release.yml +0 -50
- package/.prettierignore +0 -19
- package/.prettierrc.json +0 -20
- package/CODE_OF_CONDUCT.md +0 -83
- package/CONTRIBUTING.md +0 -111
- package/bench/README.md +0 -544
- package/bench/results/agent-tier-2026-05-22.md +0 -28
- package/bench/results/agent-tier-summary.md +0 -44
- package/bench/results/baseline-tier-2026-05-22.md +0 -23
- package/bench/results/baseline.json +0 -810
- package/bench/results/baseline.md +0 -28
- package/bench/run-agent-tier-automated.ts +0 -234
- package/bench/run-agent-tier.md +0 -125
- package/bench/run-baseline-tier.ts +0 -200
- package/bench/run.ts +0 -210
- package/bench/runner-baseline.ts +0 -177
- package/bench/runner-graphpilot.ts +0 -131
- package/bench/score-agent-tier.ts +0 -191
- package/bench/score.ts +0 -59
- package/bench/tasks.ts +0 -236
- package/dist/provenance.d.ts +0 -74
- package/dist/provenance.js +0 -95
- package/dist/provenance.js.map +0 -1
- package/docs/architecture.md +0 -311
- package/docs/limitations.md +0 -156
- package/docs/mcp-setup.md +0 -231
- package/docs/quickstart.md +0 -202
- package/eslint.config.js +0 -148
- package/lefthook.yml +0 -81
- package/pnpm-workspace.yaml +0 -6
- package/scripts/smoke-stdio.mjs +0 -97
- package/src/cli.ts +0 -171
- package/src/edges.ts +0 -202
- package/src/git.ts +0 -255
- package/src/graph-schema.ts +0 -229
- package/src/impact.ts +0 -218
- package/src/indexer.ts +0 -152
- package/src/interactions.ts +0 -0
- package/src/mcp.ts +0 -652
- package/src/parser.ts +0 -138
- package/src/provenance.ts +0 -115
- package/src/query.ts +0 -148
- package/src/redact.ts +0 -122
- package/src/storage.ts +0 -115
- package/src/symbols.ts +0 -173
- package/src/validation.ts +0 -69
- package/src/validators.ts +0 -253
- package/src/watcher.ts +0 -383
- package/tests/edges.test.ts +0 -175
- package/tests/fixtures/sample.ts +0 -32
- package/tests/git.test.ts +0 -303
- package/tests/graph-schema.test.ts +0 -321
- package/tests/impact.test.ts +0 -454
- package/tests/interactions.test.ts +0 -180
- package/tests/lint-policy.test.ts +0 -106
- package/tests/mcp-stdio.test.ts +0 -171
- package/tests/mcp.test.ts +0 -335
- package/tests/parser.test.ts +0 -31
- package/tests/provenance.test.ts +0 -132
- package/tests/query.test.ts +0 -160
- package/tests/redact.test.ts +0 -167
- package/tests/security.test.ts +0 -144
- package/tests/symbols.test.ts +0 -78
- package/tests/validators.test.ts +0 -193
- package/tests/watcher.test.ts +0 -250
- package/tsconfig.json +0 -18
package/src/symbols.ts
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import type Parser from 'tree-sitter';
|
|
2
|
-
import { walk, type ParsedFile } from './parser.js';
|
|
3
|
-
import { redactSecrets } from './redact.js';
|
|
4
|
-
|
|
5
|
-
export type SymbolKind =
|
|
6
|
-
| 'function'
|
|
7
|
-
| 'class'
|
|
8
|
-
| 'method'
|
|
9
|
-
| 'interface'
|
|
10
|
-
| 'type'
|
|
11
|
-
| 'variable'
|
|
12
|
-
| 'enum';
|
|
13
|
-
|
|
14
|
-
export interface SymbolRecord {
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
kind: SymbolKind;
|
|
18
|
-
file: string;
|
|
19
|
-
line: number;
|
|
20
|
-
column: number;
|
|
21
|
-
endLine: number;
|
|
22
|
-
signature: string;
|
|
23
|
-
exported: boolean;
|
|
24
|
-
parent?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Extract every symbol-defining node from a parsed file.
|
|
29
|
-
* v1 covers: function decls, arrow/function-expr consts, classes, methods,
|
|
30
|
-
* interfaces, type aliases, enums.
|
|
31
|
-
*/
|
|
32
|
-
export function extractSymbols(parsed: ParsedFile): SymbolRecord[] {
|
|
33
|
-
const out: SymbolRecord[] = [];
|
|
34
|
-
for (const node of walk(parsed.tree.rootNode)) {
|
|
35
|
-
extractFromNode(node, parsed, out);
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function extractFromNode(node: Parser.SyntaxNode, parsed: ParsedFile, out: SymbolRecord[]): void {
|
|
41
|
-
switch (node.type) {
|
|
42
|
-
case 'function_declaration':
|
|
43
|
-
case 'generator_function_declaration': {
|
|
44
|
-
const name = node.childForFieldName('name')?.text;
|
|
45
|
-
if (!name) return;
|
|
46
|
-
out.push(record(node, parsed, name, 'function'));
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
case 'class_declaration': {
|
|
50
|
-
const name = node.childForFieldName('name')?.text;
|
|
51
|
-
if (!name) return;
|
|
52
|
-
out.push(record(node, parsed, name, 'class'));
|
|
53
|
-
extractClassMembers(node, parsed, name, out);
|
|
54
|
-
return;
|
|
55
|
-
}
|
|
56
|
-
case 'interface_declaration': {
|
|
57
|
-
const name = node.childForFieldName('name')?.text;
|
|
58
|
-
if (!name) return;
|
|
59
|
-
out.push(record(node, parsed, name, 'interface'));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
case 'type_alias_declaration': {
|
|
63
|
-
const name = node.childForFieldName('name')?.text;
|
|
64
|
-
if (!name) return;
|
|
65
|
-
out.push(record(node, parsed, name, 'type'));
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
case 'enum_declaration': {
|
|
69
|
-
const name = node.childForFieldName('name')?.text;
|
|
70
|
-
if (!name) return;
|
|
71
|
-
out.push(record(node, parsed, name, 'enum'));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
case 'variable_declarator': {
|
|
75
|
-
// Only emit if value is a function-like (matches Day-2 listFunctions logic).
|
|
76
|
-
const valueNode = node.childForFieldName('value');
|
|
77
|
-
if (
|
|
78
|
-
!valueNode ||
|
|
79
|
-
!(
|
|
80
|
-
valueNode.type === 'arrow_function' ||
|
|
81
|
-
valueNode.type === 'function_expression' ||
|
|
82
|
-
valueNode.type === 'function'
|
|
83
|
-
)
|
|
84
|
-
) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
const name = node.childForFieldName('name')?.text;
|
|
88
|
-
if (!name) return;
|
|
89
|
-
out.push(record(node, parsed, name, 'variable'));
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
default:
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function extractClassMembers(
|
|
98
|
-
classNode: Parser.SyntaxNode,
|
|
99
|
-
parsed: ParsedFile,
|
|
100
|
-
className: string,
|
|
101
|
-
out: SymbolRecord[],
|
|
102
|
-
): void {
|
|
103
|
-
const body = classNode.childForFieldName('body');
|
|
104
|
-
if (!body) return;
|
|
105
|
-
for (let i = 0; i < body.childCount; i++) {
|
|
106
|
-
const member = body.child(i);
|
|
107
|
-
if (!member) continue;
|
|
108
|
-
if (member.type === 'method_definition' || member.type === 'method_signature') {
|
|
109
|
-
const name = member.childForFieldName('name')?.text;
|
|
110
|
-
if (!name) continue;
|
|
111
|
-
out.push(record(member, parsed, name, 'method', className));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function record(
|
|
117
|
-
node: Parser.SyntaxNode,
|
|
118
|
-
parsed: ParsedFile,
|
|
119
|
-
name: string,
|
|
120
|
-
kind: SymbolKind,
|
|
121
|
-
parent?: string,
|
|
122
|
-
): SymbolRecord {
|
|
123
|
-
const line = node.startPosition.row + 1;
|
|
124
|
-
const column = node.startPosition.column + 1;
|
|
125
|
-
const endLine = node.endPosition.row + 1;
|
|
126
|
-
const signature = oneLineSignature(node, parsed.source);
|
|
127
|
-
const exported = isExported(node);
|
|
128
|
-
const id = `${parsed.path}#${parent ? parent + '.' : ''}${name}@${line}`;
|
|
129
|
-
return {
|
|
130
|
-
id,
|
|
131
|
-
name,
|
|
132
|
-
kind,
|
|
133
|
-
file: parsed.path,
|
|
134
|
-
line,
|
|
135
|
-
column,
|
|
136
|
-
endLine,
|
|
137
|
-
signature,
|
|
138
|
-
exported,
|
|
139
|
-
parent,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Extract a single-line signature from the node. Takes the first line of
|
|
145
|
-
* text up to the body/value, capped at 200 chars. Secrets matching known
|
|
146
|
-
* patterns (T3 defence) are redacted before the line is returned.
|
|
147
|
-
*/
|
|
148
|
-
function oneLineSignature(node: Parser.SyntaxNode, source: string): string {
|
|
149
|
-
// For variable_declarator, climb to the parent lexical/var declaration so we
|
|
150
|
-
// capture "export const foo = ..." rather than just "foo = ...".
|
|
151
|
-
let target: Parser.SyntaxNode = node;
|
|
152
|
-
if (node.type === 'variable_declarator' && node.parent) {
|
|
153
|
-
target = node.parent;
|
|
154
|
-
}
|
|
155
|
-
const raw = source.slice(target.startIndex, target.endIndex);
|
|
156
|
-
const firstLine = raw.split('\n')[0].trim();
|
|
157
|
-
const capped = firstLine.length > 200 ? firstLine.slice(0, 197) + '...' : firstLine;
|
|
158
|
-
// T3: redact known-format secrets (API keys, tokens, JWTs, PEM headers).
|
|
159
|
-
return redactSecrets(capped);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* A node is exported if it (or an ancestor variable/lexical decl) is wrapped
|
|
164
|
-
* in an `export_statement`.
|
|
165
|
-
*/
|
|
166
|
-
function isExported(node: Parser.SyntaxNode): boolean {
|
|
167
|
-
let cur: Parser.SyntaxNode | null = node;
|
|
168
|
-
while (cur) {
|
|
169
|
-
if (cur.type === 'export_statement') return true;
|
|
170
|
-
cur = cur.parent;
|
|
171
|
-
}
|
|
172
|
-
return false;
|
|
173
|
-
}
|
package/src/validation.ts
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { realpathSync } from 'node:fs';
|
|
2
|
-
import { homedir } from 'node:os';
|
|
3
|
-
import { resolve } from 'node:path';
|
|
4
|
-
|
|
5
|
-
/** Max bytes per source file we will read. Anything larger is skipped. */
|
|
6
|
-
export const MAX_FILE_BYTES = 5 * 1024 * 1024; // 5 MB
|
|
7
|
-
|
|
8
|
-
/** Max number of files we will index in one run. Hard fail above this. */
|
|
9
|
-
export const MAX_FILES_PER_INDEX = 50_000;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* System paths we refuse to index. Indexing these by accident would walk the
|
|
13
|
-
* whole machine, fill disk, and leak system files into ~/.graphpilot/.
|
|
14
|
-
*
|
|
15
|
-
* Each entry must match the *realpath* form (after `fs.realpathSync`). macOS
|
|
16
|
-
* symlinks /etc, /var, /tmp to /private/*, so we include both forms here.
|
|
17
|
-
*/
|
|
18
|
-
const DANGEROUS_PATHS = new Set([
|
|
19
|
-
'/',
|
|
20
|
-
'/bin',
|
|
21
|
-
'/sbin',
|
|
22
|
-
'/usr',
|
|
23
|
-
'/usr/bin',
|
|
24
|
-
'/usr/local',
|
|
25
|
-
'/etc',
|
|
26
|
-
'/var',
|
|
27
|
-
'/tmp',
|
|
28
|
-
'/private',
|
|
29
|
-
'/private/etc',
|
|
30
|
-
'/private/var',
|
|
31
|
-
'/private/tmp',
|
|
32
|
-
'/Library',
|
|
33
|
-
'/System',
|
|
34
|
-
'/Applications',
|
|
35
|
-
'/Volumes',
|
|
36
|
-
'/Users',
|
|
37
|
-
'/home',
|
|
38
|
-
'C:\\Windows',
|
|
39
|
-
'C:\\Windows\\System32',
|
|
40
|
-
'C:\\Program Files',
|
|
41
|
-
'C:\\Program Files (x86)',
|
|
42
|
-
]);
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Returns null if the path is safe to index, or a human-readable reason string
|
|
46
|
-
* if it should be refused.
|
|
47
|
-
*/
|
|
48
|
-
export function validateRootPath(rawPath: string): string | null {
|
|
49
|
-
const abs = resolve(rawPath);
|
|
50
|
-
|
|
51
|
-
let real: string;
|
|
52
|
-
try {
|
|
53
|
-
real = realpathSync(abs);
|
|
54
|
-
} catch {
|
|
55
|
-
return `Path does not exist or is not accessible: ${abs}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Reject any bare Windows drive root (C:\, D:\, etc.) — not just C:\
|
|
59
|
-
if (process.platform === 'win32' && /^[A-Za-z]:\\?$/.test(real)) {
|
|
60
|
-
return `Refusing to index system path: ${real}`;
|
|
61
|
-
}
|
|
62
|
-
if (DANGEROUS_PATHS.has(real)) {
|
|
63
|
-
return `Refusing to index system path: ${real}`;
|
|
64
|
-
}
|
|
65
|
-
if (real === homedir()) {
|
|
66
|
-
return `Refusing to index your home directory directly (${real}). Pass a specific project subdirectory instead.`;
|
|
67
|
-
}
|
|
68
|
-
return null;
|
|
69
|
-
}
|
package/src/validators.ts
DELETED
|
@@ -1,253 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hand-rolled validation for MCP tool inputs. No external deps — three tools,
|
|
3
|
-
* each with a handful of fields, makes a library overkill.
|
|
4
|
-
*
|
|
5
|
-
* Every tool validator returns a tagged result:
|
|
6
|
-
* { ok: true, value: <typed args> }
|
|
7
|
-
* { ok: false, error: <human-readable> }
|
|
8
|
-
*
|
|
9
|
-
* Rules (defence-in-depth — JSON schema is declared in the tool catalog
|
|
10
|
-
* too; this is the second wall):
|
|
11
|
-
* - Reject extra unknown keys (defends against agent typos & tampering)
|
|
12
|
-
* - Type-check every field
|
|
13
|
-
* - Range-check numbers (limit is bounded, no NaN)
|
|
14
|
-
* - Length-cap strings (no 10MB symbol names)
|
|
15
|
-
* - Strict enums (no surprise direction values)
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
export type Result<T> = { ok: true; value: T } | { ok: false; error: string };
|
|
19
|
-
|
|
20
|
-
function ok<T>(value: T): Result<T> {
|
|
21
|
-
return { ok: true, value };
|
|
22
|
-
}
|
|
23
|
-
function fail<T>(error: string): Result<T> {
|
|
24
|
-
return { ok: false, error };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function isPlainObject(x: unknown): x is Record<string, unknown> {
|
|
28
|
-
return typeof x === 'object' && x !== null && !Array.isArray(x);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const MAX_STRING_LEN = 2_000;
|
|
32
|
-
|
|
33
|
-
function pickString(
|
|
34
|
-
obj: Record<string, unknown>,
|
|
35
|
-
key: string,
|
|
36
|
-
opts: { required?: boolean; max?: number } = {},
|
|
37
|
-
): Result<string | undefined> {
|
|
38
|
-
const raw = obj[key];
|
|
39
|
-
if (raw === undefined) {
|
|
40
|
-
if (opts.required) return fail(`Missing required field: ${key}`);
|
|
41
|
-
return ok(undefined);
|
|
42
|
-
}
|
|
43
|
-
if (typeof raw !== 'string') return fail(`${key} must be a string`);
|
|
44
|
-
const max = opts.max ?? MAX_STRING_LEN;
|
|
45
|
-
if (raw.length > max) return fail(`${key} exceeds max length of ${max}`);
|
|
46
|
-
return ok(raw);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function pickNumber(
|
|
50
|
-
obj: Record<string, unknown>,
|
|
51
|
-
key: string,
|
|
52
|
-
opts: { min?: number; max?: number; integer?: boolean } = {},
|
|
53
|
-
): Result<number | undefined> {
|
|
54
|
-
const raw = obj[key];
|
|
55
|
-
if (raw === undefined) return ok(undefined);
|
|
56
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
57
|
-
return fail(`${key} must be a finite number`);
|
|
58
|
-
}
|
|
59
|
-
if (opts.integer && !Number.isInteger(raw)) {
|
|
60
|
-
return fail(`${key} must be an integer`);
|
|
61
|
-
}
|
|
62
|
-
if (opts.min !== undefined && raw < opts.min) {
|
|
63
|
-
return fail(`${key} must be >= ${opts.min}`);
|
|
64
|
-
}
|
|
65
|
-
if (opts.max !== undefined && raw > opts.max) {
|
|
66
|
-
return fail(`${key} must be <= ${opts.max}`);
|
|
67
|
-
}
|
|
68
|
-
return ok(raw);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function pickBoolean(obj: Record<string, unknown>, key: string): Result<boolean | undefined> {
|
|
72
|
-
const raw = obj[key];
|
|
73
|
-
if (raw === undefined) return ok(undefined);
|
|
74
|
-
if (typeof raw !== 'boolean') return fail(`${key} must be a boolean`);
|
|
75
|
-
return ok(raw);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function pickEnum<T extends string>(
|
|
79
|
-
obj: Record<string, unknown>,
|
|
80
|
-
key: string,
|
|
81
|
-
allowed: readonly T[],
|
|
82
|
-
): Result<T | undefined> {
|
|
83
|
-
const raw = obj[key];
|
|
84
|
-
if (raw === undefined) return ok(undefined);
|
|
85
|
-
if (typeof raw !== 'string' || !(allowed as readonly string[]).includes(raw)) {
|
|
86
|
-
return fail(`${key} must be one of: ${allowed.join(', ')}`);
|
|
87
|
-
}
|
|
88
|
-
return ok(raw as T);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function rejectExtraKeys(obj: Record<string, unknown>, allowed: readonly string[]): Result<true> {
|
|
92
|
-
const extras = Object.keys(obj).filter((k) => !allowed.includes(k));
|
|
93
|
-
if (extras.length > 0) {
|
|
94
|
-
return fail(`Unknown field(s): ${extras.join(', ')}. Allowed: ${allowed.join(', ')}`);
|
|
95
|
-
}
|
|
96
|
-
return ok(true);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// ----------------------------------------------------------------------------
|
|
100
|
-
// Per-tool validators
|
|
101
|
-
// ----------------------------------------------------------------------------
|
|
102
|
-
|
|
103
|
-
export interface GpIndexArgs {
|
|
104
|
-
path?: string;
|
|
105
|
-
}
|
|
106
|
-
const GP_INDEX_KEYS = ['path'] as const;
|
|
107
|
-
|
|
108
|
-
export function validateGpIndex(input: unknown): Result<GpIndexArgs> {
|
|
109
|
-
if (!isPlainObject(input)) return fail('arguments must be an object');
|
|
110
|
-
const extras = rejectExtraKeys(input, GP_INDEX_KEYS);
|
|
111
|
-
if (!extras.ok) return fail(extras.error);
|
|
112
|
-
const path = pickString(input, 'path', { max: 1024 });
|
|
113
|
-
if (!path.ok) return fail(path.error);
|
|
114
|
-
return ok({ path: path.value });
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export interface GpRecallArgs {
|
|
118
|
-
query: string;
|
|
119
|
-
limit?: number;
|
|
120
|
-
substring?: boolean;
|
|
121
|
-
path?: string;
|
|
122
|
-
}
|
|
123
|
-
const GP_RECALL_KEYS = ['query', 'limit', 'substring', 'path'] as const;
|
|
124
|
-
|
|
125
|
-
export function validateGpRecall(input: unknown): Result<GpRecallArgs> {
|
|
126
|
-
if (!isPlainObject(input)) return fail('arguments must be an object');
|
|
127
|
-
const extras = rejectExtraKeys(input, GP_RECALL_KEYS);
|
|
128
|
-
if (!extras.ok) return fail(extras.error);
|
|
129
|
-
|
|
130
|
-
const query = pickString(input, 'query', { required: true, max: 200 });
|
|
131
|
-
if (!query.ok) return fail(query.error);
|
|
132
|
-
if (!query.value || query.value.trim() === '') {
|
|
133
|
-
return fail('query must be a non-empty string');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const limit = pickNumber(input, 'limit', { min: 1, max: 50, integer: true });
|
|
137
|
-
if (!limit.ok) return fail(limit.error);
|
|
138
|
-
|
|
139
|
-
const substring = pickBoolean(input, 'substring');
|
|
140
|
-
if (!substring.ok) return fail(substring.error);
|
|
141
|
-
|
|
142
|
-
const path = pickString(input, 'path', { max: 1024 });
|
|
143
|
-
if (!path.ok) return fail(path.error);
|
|
144
|
-
|
|
145
|
-
return ok({
|
|
146
|
-
query: query.value,
|
|
147
|
-
limit: limit.value,
|
|
148
|
-
substring: substring.value,
|
|
149
|
-
path: path.value,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export type CallersDirection = 'callers' | 'callees';
|
|
154
|
-
|
|
155
|
-
export interface GpCallersArgs {
|
|
156
|
-
symbol: string;
|
|
157
|
-
direction?: CallersDirection;
|
|
158
|
-
limit?: number;
|
|
159
|
-
includeUnresolved?: boolean;
|
|
160
|
-
path?: string;
|
|
161
|
-
}
|
|
162
|
-
const GP_CALLERS_KEYS = ['symbol', 'direction', 'limit', 'includeUnresolved', 'path'] as const;
|
|
163
|
-
|
|
164
|
-
export function validateGpCallers(input: unknown): Result<GpCallersArgs> {
|
|
165
|
-
if (!isPlainObject(input)) return fail('arguments must be an object');
|
|
166
|
-
const extras = rejectExtraKeys(input, GP_CALLERS_KEYS);
|
|
167
|
-
if (!extras.ok) return fail(extras.error);
|
|
168
|
-
|
|
169
|
-
const symbol = pickString(input, 'symbol', { required: true, max: 500 });
|
|
170
|
-
if (!symbol.ok) return fail(symbol.error);
|
|
171
|
-
if (!symbol.value || symbol.value.trim() === '') {
|
|
172
|
-
return fail('symbol must be a non-empty string');
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const direction = pickEnum(input, 'direction', ['callers', 'callees'] as const);
|
|
176
|
-
if (!direction.ok) return fail(direction.error);
|
|
177
|
-
|
|
178
|
-
const limit = pickNumber(input, 'limit', { min: 1, max: 100, integer: true });
|
|
179
|
-
if (!limit.ok) return fail(limit.error);
|
|
180
|
-
|
|
181
|
-
const includeUnresolved = pickBoolean(input, 'includeUnresolved');
|
|
182
|
-
if (!includeUnresolved.ok) return fail(includeUnresolved.error);
|
|
183
|
-
|
|
184
|
-
const path = pickString(input, 'path', { max: 1024 });
|
|
185
|
-
if (!path.ok) return fail(path.error);
|
|
186
|
-
|
|
187
|
-
return ok({
|
|
188
|
-
symbol: symbol.value,
|
|
189
|
-
direction: direction.value,
|
|
190
|
-
limit: limit.value,
|
|
191
|
-
includeUnresolved: includeUnresolved.value,
|
|
192
|
-
path: path.value,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Tool name -> validator dispatcher (for the MCP server)
|
|
197
|
-
export type ToolName = 'gp_index' | 'gp_recall' | 'gp_callers' | 'gp_impact' | 'gp_stats';
|
|
198
|
-
|
|
199
|
-
export interface GpStatsArgs {
|
|
200
|
-
path?: string;
|
|
201
|
-
}
|
|
202
|
-
export function validateGpStats(input: unknown): Result<GpStatsArgs> {
|
|
203
|
-
if (!isPlainObject(input)) return fail('arguments must be an object');
|
|
204
|
-
const extras = rejectExtraKeys(input, ['path']);
|
|
205
|
-
if (!extras.ok) return fail(extras.error);
|
|
206
|
-
const path = pickString(input, 'path', { max: 1024 });
|
|
207
|
-
if (!path.ok) return fail(path.error);
|
|
208
|
-
return ok({ path: path.value });
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export interface GpImpactArgs {
|
|
212
|
-
symbol: string;
|
|
213
|
-
depth?: number;
|
|
214
|
-
path?: string;
|
|
215
|
-
since?: string;
|
|
216
|
-
}
|
|
217
|
-
const GP_IMPACT_KEYS = ['symbol', 'depth', 'path', 'since'] as const;
|
|
218
|
-
|
|
219
|
-
export function validateGpImpact(input: unknown): Result<GpImpactArgs> {
|
|
220
|
-
if (!isPlainObject(input)) return fail('arguments must be an object');
|
|
221
|
-
const extras = rejectExtraKeys(input, GP_IMPACT_KEYS);
|
|
222
|
-
if (!extras.ok) return fail(extras.error);
|
|
223
|
-
|
|
224
|
-
const symbol = pickString(input, 'symbol', { required: true, max: 500 });
|
|
225
|
-
if (!symbol.ok) return fail(symbol.error);
|
|
226
|
-
if (!symbol.value || symbol.value.trim() === '') {
|
|
227
|
-
return fail('symbol must be a non-empty string');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// BFS depth: hard-cap at 5. Deeper traversals explode in big repos and
|
|
231
|
-
// rarely add value — depth-3 already covers ~99% of real refactors.
|
|
232
|
-
const depth = pickNumber(input, 'depth', { min: 1, max: 5, integer: true });
|
|
233
|
-
if (!depth.ok) return fail(depth.error);
|
|
234
|
-
|
|
235
|
-
const path = pickString(input, 'path', { max: 1024 });
|
|
236
|
-
if (!path.ok) return fail(path.error);
|
|
237
|
-
|
|
238
|
-
// `since` accepts a commit SHA (full or short), tag, or branch name.
|
|
239
|
-
// Capped at 200 chars — refs are normally under 100, this is a sanity
|
|
240
|
-
// bound, not a security one (isomorphic-git handles ref resolution).
|
|
241
|
-
const since = pickString(input, 'since', { max: 200 });
|
|
242
|
-
if (!since.ok) return fail(since.error);
|
|
243
|
-
if (since.value !== undefined && since.value.trim() === '') {
|
|
244
|
-
return fail('since must be a non-empty string when provided');
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return ok({
|
|
248
|
-
symbol: symbol.value,
|
|
249
|
-
depth: depth.value,
|
|
250
|
-
path: path.value,
|
|
251
|
-
since: since.value,
|
|
252
|
-
});
|
|
253
|
-
}
|