@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.
Files changed (123) hide show
  1. package/CHANGELOG.md +72 -126
  2. package/README.md +290 -102
  3. package/dist/cli.js +41 -1
  4. package/dist/cli.js.map +1 -1
  5. package/dist/edges.js +22 -11
  6. package/dist/edges.js.map +1 -1
  7. package/dist/indexer.js +3 -3
  8. package/dist/indexer.js.map +1 -1
  9. package/dist/init.d.ts +28 -0
  10. package/dist/init.js +112 -0
  11. package/dist/init.js.map +1 -0
  12. package/dist/interactions.d.ts +5 -4
  13. package/dist/interactions.js +0 -0
  14. package/dist/interactions.js.map +1 -1
  15. package/dist/mcp.js +119 -90
  16. package/dist/mcp.js.map +1 -1
  17. package/dist/repo-resolve.d.ts +47 -0
  18. package/dist/repo-resolve.js +195 -0
  19. package/dist/repo-resolve.js.map +1 -0
  20. package/dist/storage.js +10 -1
  21. package/dist/storage.js.map +1 -1
  22. package/dist/symbols.js +26 -2
  23. package/dist/symbols.js.map +1 -1
  24. package/dist/validation.js +30 -4
  25. package/dist/validation.js.map +1 -1
  26. package/dist/validators.d.ts +1 -5
  27. package/dist/validators.js +0 -11
  28. package/dist/validators.js.map +1 -1
  29. package/dist/watcher.d.ts +10 -0
  30. package/dist/watcher.js +70 -7
  31. package/dist/watcher.js.map +1 -1
  32. package/examples/README.md +105 -0
  33. package/examples/claude-code/README.md +125 -0
  34. package/examples/claude-code/claude-routing.md +102 -0
  35. package/examples/claude-code/claude_config.json +8 -0
  36. package/examples/cline/.clinerules +39 -0
  37. package/examples/cline/README.md +104 -0
  38. package/examples/cline/cline_mcp_settings.json +10 -0
  39. package/examples/continue/.continuerules +39 -0
  40. package/examples/continue/README.md +98 -0
  41. package/examples/continue/config.json +13 -0
  42. package/examples/cursor/.cursorrules +39 -0
  43. package/examples/cursor/README.md +98 -0
  44. package/examples/cursor/mcp.json +11 -0
  45. package/examples/windsurf/.windsurfrules +39 -0
  46. package/examples/windsurf/README.md +85 -0
  47. package/examples/windsurf/mcp_config.json +8 -0
  48. package/package.json +14 -4
  49. package/.editorconfig +0 -15
  50. package/.github/CODEOWNERS +0 -22
  51. package/.github/FUNDING.yml +0 -1
  52. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -33
  53. package/.github/ISSUE_TEMPLATE/config.yml +0 -5
  54. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -23
  55. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  56. package/.github/dependabot.yml +0 -15
  57. package/.github/workflows/ci.yml +0 -62
  58. package/.github/workflows/release.yml +0 -50
  59. package/.prettierignore +0 -19
  60. package/.prettierrc.json +0 -20
  61. package/CODE_OF_CONDUCT.md +0 -83
  62. package/CONTRIBUTING.md +0 -111
  63. package/bench/README.md +0 -544
  64. package/bench/results/agent-tier-2026-05-22.md +0 -28
  65. package/bench/results/agent-tier-summary.md +0 -44
  66. package/bench/results/baseline-tier-2026-05-22.md +0 -23
  67. package/bench/results/baseline.json +0 -810
  68. package/bench/results/baseline.md +0 -28
  69. package/bench/run-agent-tier-automated.ts +0 -234
  70. package/bench/run-agent-tier.md +0 -125
  71. package/bench/run-baseline-tier.ts +0 -200
  72. package/bench/run.ts +0 -210
  73. package/bench/runner-baseline.ts +0 -177
  74. package/bench/runner-graphpilot.ts +0 -131
  75. package/bench/score-agent-tier.ts +0 -191
  76. package/bench/score.ts +0 -59
  77. package/bench/tasks.ts +0 -236
  78. package/dist/provenance.d.ts +0 -74
  79. package/dist/provenance.js +0 -95
  80. package/dist/provenance.js.map +0 -1
  81. package/docs/architecture.md +0 -311
  82. package/docs/limitations.md +0 -156
  83. package/docs/mcp-setup.md +0 -231
  84. package/docs/quickstart.md +0 -202
  85. package/eslint.config.js +0 -148
  86. package/lefthook.yml +0 -81
  87. package/pnpm-workspace.yaml +0 -6
  88. package/scripts/smoke-stdio.mjs +0 -97
  89. package/src/cli.ts +0 -171
  90. package/src/edges.ts +0 -202
  91. package/src/git.ts +0 -255
  92. package/src/graph-schema.ts +0 -229
  93. package/src/impact.ts +0 -218
  94. package/src/indexer.ts +0 -152
  95. package/src/interactions.ts +0 -0
  96. package/src/mcp.ts +0 -652
  97. package/src/parser.ts +0 -138
  98. package/src/provenance.ts +0 -115
  99. package/src/query.ts +0 -148
  100. package/src/redact.ts +0 -122
  101. package/src/storage.ts +0 -115
  102. package/src/symbols.ts +0 -173
  103. package/src/validation.ts +0 -69
  104. package/src/validators.ts +0 -253
  105. package/src/watcher.ts +0 -383
  106. package/tests/edges.test.ts +0 -175
  107. package/tests/fixtures/sample.ts +0 -32
  108. package/tests/git.test.ts +0 -303
  109. package/tests/graph-schema.test.ts +0 -321
  110. package/tests/impact.test.ts +0 -454
  111. package/tests/interactions.test.ts +0 -180
  112. package/tests/lint-policy.test.ts +0 -106
  113. package/tests/mcp-stdio.test.ts +0 -171
  114. package/tests/mcp.test.ts +0 -335
  115. package/tests/parser.test.ts +0 -31
  116. package/tests/provenance.test.ts +0 -132
  117. package/tests/query.test.ts +0 -160
  118. package/tests/redact.test.ts +0 -167
  119. package/tests/security.test.ts +0 -144
  120. package/tests/symbols.test.ts +0 -78
  121. package/tests/validators.test.ts +0 -193
  122. package/tests/watcher.test.ts +0 -250
  123. 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
- }