@colbymchenry/codegraph 1.0.0 → 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.
Files changed (34) hide show
  1. package/README.md +87 -51
  2. package/dist/bin/fatal-handler.d.ts +20 -0
  3. package/dist/db/index.d.ts +22 -1
  4. package/dist/db/queries.d.ts +14 -0
  5. package/dist/directory.d.ts +58 -0
  6. package/dist/extraction/grammars.d.ts +11 -3
  7. package/dist/extraction/tree-sitter-types.d.ts +13 -0
  8. package/dist/extraction/tree-sitter.d.ts +119 -0
  9. package/dist/index.d.ts +33 -0
  10. package/dist/installer/instructions-template.d.ts +3 -3
  11. package/dist/installer/targets/claude.d.ts +18 -12
  12. package/dist/installer/targets/shared.d.ts +12 -2
  13. package/dist/installer/targets/types.d.ts +7 -0
  14. package/dist/mcp/daemon-manager.d.ts +42 -0
  15. package/dist/mcp/daemon-registry.d.ts +47 -0
  16. package/dist/mcp/index.d.ts +1 -0
  17. package/dist/mcp/liveness-watchdog.d.ts +18 -0
  18. package/dist/mcp/server-instructions.d.ts +18 -14
  19. package/dist/mcp/tools.d.ts +51 -1
  20. package/dist/project-config.d.ts +19 -0
  21. package/dist/reasoning/config.d.ts +45 -0
  22. package/dist/reasoning/credentials.d.ts +5 -0
  23. package/dist/reasoning/login.d.ts +21 -0
  24. package/dist/reasoning/reasoner.d.ts +43 -0
  25. package/dist/resolution/c-fnptr-synthesizer.d.ts +33 -0
  26. package/dist/resolution/callback-synthesizer.d.ts +6 -1
  27. package/dist/resolution/frameworks/goframe.d.ts +41 -0
  28. package/dist/resolution/frameworks/index.d.ts +1 -0
  29. package/dist/resolution/goframe-synthesizer.d.ts +28 -0
  30. package/dist/resolution/strip-comments.d.ts +1 -1
  31. package/dist/sync/watcher.d.ts +68 -1
  32. package/dist/telemetry/index.d.ts +0 -3
  33. package/dist/utils.d.ts +14 -1
  34. package/package.json +7 -7
@@ -0,0 +1,45 @@
1
+ /** Managed tier ("CodeGraph AI") — the metered gateway used when logged in. */
2
+ export declare const MANAGED_DEFAULT_URL = "https://ai.getcodegraph.com/v1";
3
+ /** The gateway's public model id (it translates this to the upstream provider id). */
4
+ export declare const MANAGED_DEFAULT_MODEL = "openai/gpt-oss-120b";
5
+ export interface OffloadConfig {
6
+ /** Managed tier: route through CodeGraph AI (metered) with the logged-in org token. */
7
+ managed?: boolean;
8
+ /** OpenAI-compatible base URL ending in `/v1` (e.g. https://api.cerebras.ai/v1). */
9
+ url?: string;
10
+ /** Model id to request (default `gpt-oss-120b` BYO, `openai/gpt-oss-120b` managed). */
11
+ model?: string;
12
+ /** Name of the env var holding the provider API key (never persisted). BYO only. */
13
+ keyEnv?: string;
14
+ /** reasoning_effort: low | medium | high (default `low`). */
15
+ effort?: string;
16
+ /** Output style: plain | report (default `plain`). */
17
+ style?: string;
18
+ }
19
+ export interface ResolvedOffload {
20
+ /** True when the offload is usable (endpoint present; for managed, a token too). */
21
+ enabled: boolean;
22
+ /** Managed tier (CodeGraph AI, metered) vs BYO endpoint. */
23
+ managed: boolean;
24
+ url?: string;
25
+ model: string;
26
+ /** Resolved API key / org token (from env, the configured `keyEnv`, or login), if any. */
27
+ apiKey?: string;
28
+ /** Where the key/token came from (for `status` display) — never the secret itself. */
29
+ keySource?: string;
30
+ effort: string;
31
+ style: string;
32
+ timeoutMs: number;
33
+ maxTokens: number;
34
+ strip: boolean;
35
+ debug: boolean;
36
+ /** Where the endpoint came from — drives `codegraph offload status`. */
37
+ origin: 'env' | 'config' | 'none';
38
+ }
39
+ /** The persisted offload block (empty object if none). */
40
+ export declare function readOffloadConfig(): OffloadConfig;
41
+ /** Persist (or, with `null`, clear) the offload block, leaving other config keys intact. */
42
+ export declare function writeOffloadConfig(offload: OffloadConfig | null): void;
43
+ /** Merge the persisted config with `CODEGRAPH_OFFLOAD_*` env overrides (env wins). */
44
+ export declare function resolveOffload(env?: NodeJS.ProcessEnv): ResolvedOffload;
45
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,5 @@
1
+ /** The stored managed-offload org token, if the machine is logged in. */
2
+ export declare function readOffloadToken(): string | undefined;
3
+ /** Persist (or, with `null`, clear) the managed-offload org token at `0600`. */
4
+ export declare function writeOffloadToken(token: string | null): void;
5
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1,21 @@
1
+ /** Dashboard base for the device-login endpoints; override for testing via CODEGRAPH_LOGIN_URL. */
2
+ export declare function loginBaseUrl(): string;
3
+ /** The dashboard's response to a device-authorization start request. */
4
+ export interface DeviceStart {
5
+ device_code: string;
6
+ user_code: string;
7
+ verification_uri: string;
8
+ /** Same URL with the code prefilled, for one-click open. */
9
+ verification_uri_complete?: string;
10
+ /** Seconds the CLI should wait between polls. */
11
+ interval?: number;
12
+ /** Seconds until the request expires. */
13
+ expires_in?: number;
14
+ }
15
+ /** Begin a device-authorization request. */
16
+ export declare function startDeviceLogin(): Promise<DeviceStart>;
17
+ /** Poll until the user approves in the browser; resolves with the org token. */
18
+ export declare function pollForToken(deviceCode: string, intervalSec: number, expiresInSec: number): Promise<string>;
19
+ /** Best-effort: open a URL in the default browser. Never throws — the URL is also printed. */
20
+ export declare function openBrowser(url: string): Promise<void>;
21
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1,43 @@
1
+ interface SynthArgs {
2
+ query: string;
3
+ context: string;
4
+ }
5
+ /** True when a reasoning offload endpoint is configured (env or `~/.codegraph/config.json`). */
6
+ export declare function isOffloadEnabled(): boolean;
7
+ export interface OffloadUsage {
8
+ plan?: string;
9
+ allowance?: number;
10
+ used?: number;
11
+ overage?: number;
12
+ remaining?: number;
13
+ periodEnd?: number;
14
+ unlimited?: boolean;
15
+ banned?: boolean;
16
+ tokensLast30?: number;
17
+ callsLast30?: number;
18
+ creditsLast30?: number;
19
+ models?: string[];
20
+ }
21
+ /**
22
+ * GET `/v1/usage` from the configured (managed) endpoint → the org's credit
23
+ * balance/usage, or null on any failure. Drives `codegraph offload status`.
24
+ */
25
+ export declare function fetchUsage(): Promise<OffloadUsage | null>;
26
+ /**
27
+ * Strip sections of the explore output addressed to the AGENT (not useful to a
28
+ * reasoning model): the "Not shown above" pointer list, the completeness signal,
29
+ * the explore-budget note, the trimmed/truncation notices, and the redundant
30
+ * "## Exploration:/Found N symbols" header (the query is sent separately). Left
31
+ * in, some models regurgitate them ("We have 2 explore calls. Let's explore…")
32
+ * and they add noise. Source code, blast radius, relationships, and flow stay.
33
+ * Opt-in (`CODEGRAPH_OFFLOAD_STRIP=1`) — default off (it also removes the "Not
34
+ * shown above" pointers, which can be useful navigation).
35
+ */
36
+ export declare function stripAgentDirectives(context: string): string;
37
+ /**
38
+ * Offload reasoning over the retrieved `context` to the configured model and
39
+ * return its synthesized answer, or null to signal "fall back to local source".
40
+ */
41
+ export declare function synthesizeOffload({ query, context }: SynthArgs): Promise<string | null>;
42
+ export {};
43
+ //# sourceMappingURL=reasoner.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * C/C++ function-pointer dispatch synthesis (#932).
3
+ *
4
+ * C/C++ polymorphism is the function pointer: a struct carries a fn-pointer
5
+ * field (`int (*fn)(int)`, or a fn-pointer-typedef field `hook_func func`),
6
+ * concrete functions are *registered* into it through a table
7
+ * (`static struct cmd cmds[] = {{"add", cmd_add}, …}`, a designated
8
+ * `.fn = cmd_add`, or `x->fn = cmd_add`), and the dispatcher calls through it
9
+ * indirectly (`p->fn(argv)`). Static extraction captures neither the
10
+ * registration→field binding nor the indirect call, so the dispatcher→handler
11
+ * edge is missing and `git`'s `run_builtin` looks like it calls nothing, the
12
+ * hooks in `hook_demo.c` are unreachable, etc.
13
+ *
14
+ * This bridges it, keyed by **(struct type, fn-pointer field)**:
15
+ * • registrations — a function bound to `S.field` via a positional
16
+ * initializer (matched by field index), a designated `.field = fn`, or a
17
+ * direct `x.field = fn` / `x->field = fn` assignment;
18
+ * • dispatch — `recv->field(…)` / `recv.field(…)` where `recv` resolves to a
19
+ * value of struct type `S` (from the enclosing function's params / locals),
20
+ * falling back to the field name when it is unique to one struct;
21
+ * • field←field propagation — `a->f = b->g` merges `B.g`'s handlers into
22
+ * `A.f`, so a generic single-slot hook that is reassigned from a registry
23
+ * (the `hook_demo.c` shape: `h->func = found->fn`) still resolves.
24
+ *
25
+ * Whole-graph pass after base resolution; all edges are `provenance:'heuristic'`
26
+ * (`synthesizedBy:'fn-pointer-dispatch'`). High precision via the (type, field)
27
+ * key + a real-function gate; a project with no fn-pointer dispatch is a no-op.
28
+ */
29
+ import type { Edge } from '../types';
30
+ import type { QueryBuilder } from '../db/queries';
31
+ import type { ResolutionContext } from './types';
32
+ export declare function cFnPointerDispatchEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[];
33
+ //# sourceMappingURL=c-fnptr-synthesizer.d.ts.map
@@ -3,7 +3,12 @@ import type { ResolutionContext } from './types';
3
3
  /**
4
4
  * Synthesize dispatcher→callback edges (field observers + EventEmitters +
5
5
  * React re-render + JSX children + Vue templates + SvelteKit load + RN event
6
- * channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain).
6
+ * channel + Fabric native-impl + MyBatis Java↔XML + Gin middleware chain +
7
+ * Redux-thunk dispatch chain + object-literal registry dispatch + RTK Query
8
+ * generated-hook → endpoint + Pinia useStore().action() + Vuex string dispatch +
9
+ * Celery task .delay()/.apply_async() → task body + Spring publishEvent → @EventListener +
10
+ * MediatR Send/Publish → IRequestHandler/INotificationHandler +
11
+ * Sidekiq Worker.perform_async → #perform + Laravel event(new X) → listener handle).
7
12
  * Returns the count added. Never throws into indexing — callers wrap in try/catch.
8
13
  */
9
14
  export declare function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GoFrame Framework Resolver (route metadata) — issue #747.
3
+ *
4
+ * GoFrame's "standard router" binds routes reflectively, so there is no literal
5
+ * path string at a `.GET("/x", handler)` call site and no static edge from a
6
+ * route to the controller method that serves it. The structural facts live in
7
+ * two places, joined only at runtime by GoFrame:
8
+ *
9
+ * // api/user/v1/user_sign_in.go — the route lives in a struct tag on the request type
10
+ * type SignInReq struct {
11
+ * g.Meta `path:"/user/sign-in" method:"post" tags:"UserService" summary:"…"`
12
+ * …
13
+ * }
14
+ * // internal/controller/user/user_v1_sign_in.go — the handler takes *that* request type
15
+ * func (c *ControllerV1) SignIn(ctx context.Context, req *v1.SignInReq) (res *v1.SignInRes, err error)
16
+ * // internal/cmd/cmd.go — reflective binding (no path, no handler name)
17
+ * group.Bind(user.NewV1())
18
+ *
19
+ * This resolver handles the FIRST half: it reads the `g.Meta` struct tag on a
20
+ * request type into a `route` node (`POST /user/sign-in`). The route → handler
21
+ * EDGE is the genuinely reflective part — the method name is NOT derivable from
22
+ * the request type (`DeptSearchReq` is served by `List`, `DeptAddReq` by `Add`),
23
+ * so the only reliable join is the request type appearing in the method's
24
+ * parameter signature. That whole-graph join is done by the companion
25
+ * `goframeRouteEdges` synthesizer, which reads the request type back out of the
26
+ * route node's qualifiedName.
27
+ *
28
+ * Honesty note: the route node carries the `g.Meta` path verbatim. The group
29
+ * prefix from `s.Group("/api", …)` / nested `group.Group("/v1", …)` is applied
30
+ * by reflective `Bind` at runtime and is deliberately NOT reconstructed here —
31
+ * the discriminating, structural part is the per-route path + method.
32
+ */
33
+ import { FrameworkResolver } from '../types';
34
+ /** Marker embedded in a route node's qualifiedName so the synthesizer can read
35
+ * back the request type to join on. The value after it is the package-qualified
36
+ * request type (`cash.ListReq`) — the package disambiguates the many identical
37
+ * bare names (`ListReq`, `GetReq`) a large app defines, one per module. Falls
38
+ * back to the bare type when no `package` declaration is found. */
39
+ export declare const GOFRAME_ROUTE_MARKER = "::goframe-route:";
40
+ export declare const goframeResolver: FrameworkResolver;
41
+ //# sourceMappingURL=goframe.d.ts.map
@@ -39,6 +39,7 @@ export { railsResolver } from './ruby';
39
39
  export { springResolver } from './java';
40
40
  export { playResolver } from './play';
41
41
  export { goResolver } from './go';
42
+ export { goframeResolver } from './goframe';
42
43
  export { rustResolver } from './rust';
43
44
  export { aspnetResolver } from './csharp';
44
45
  export { swiftUIResolver, uikitResolver, vaporResolver } from './swift';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * GoFrame route → controller-method dispatch synthesis (#747).
3
+ *
4
+ * GoFrame binds routes reflectively (`group.Bind(user.NewV1())`), so the route
5
+ * declared in a request type's `g.Meta` tag has no static edge to the method
6
+ * that serves it. The `goframeResolver` extract pass turns each `g.Meta` into a
7
+ * `route` node carrying its request type in the qualifiedName; this whole-graph
8
+ * pass closes the loop by joining each route to its handler.
9
+ *
10
+ * The join key is the REQUEST TYPE, not the method name — GoFrame method names
11
+ * are free (`DeptSearchReq` is served by `List`, `DeptAddReq` by `Add`), so the
12
+ * only reliable link is the request type appearing in the handler's parameter
13
+ * signature:
14
+ *
15
+ * func (c *sysDeptController) Add(ctx context.Context, req *system.DeptAddReq) (…)
16
+ * ^^^^^^^^^^^^^^^^ the join
17
+ *
18
+ * Go method nodes already carry that signature, so no source re-read is needed.
19
+ * Each synthesized edge is `kind:'calls'`, `provenance:'heuristic'`,
20
+ * `metadata.synthesizedBy:'goframe-route'` — a reflective-dispatch bridge, so
21
+ * `codegraph_explore` surfaces it as a dynamic hop rather than a literal call,
22
+ * and the handler's callers list the route that reaches it. A project with no
23
+ * GoFrame routes is a no-op.
24
+ */
25
+ import type { Edge } from '../types';
26
+ import type { ResolutionContext } from './types';
27
+ export declare function goframeRouteEdges(ctx: ResolutionContext): Edge[];
28
+ //# sourceMappingURL=goframe-synthesizer.d.ts.map
@@ -22,6 +22,6 @@
22
22
  * `path(...)`, `Route::get(...)`, `app.get(...)` style patterns that
23
23
  * framework extractors scan for.
24
24
  */
25
- export type CommentLang = 'python' | 'javascript' | 'typescript' | 'php' | 'ruby' | 'java' | 'csharp' | 'swift' | 'go' | 'rust';
25
+ export type CommentLang = 'python' | 'javascript' | 'typescript' | 'php' | 'ruby' | 'java' | 'csharp' | 'swift' | 'go' | 'rust' | 'c' | 'cpp';
26
26
  export declare function stripCommentsForRegex(content: string, lang: CommentLang): string;
27
27
  //# sourceMappingURL=strip-comments.d.ts.map
@@ -30,6 +30,16 @@
30
30
  * events are dropped before any sync is scheduled. Either way the watcher's
31
31
  * scope matches the indexer's (#276 / #407).
32
32
  */
33
+ import * as fs from 'fs';
34
+ /**
35
+ * Indirection over `fs.watch` so tests can inject a fake that throws or emits
36
+ * `EMFILE`/`ENFILE` deterministically (real watch-resource exhaustion can't be
37
+ * provoked reliably, and `fs.watch` is a non-configurable property so it can't
38
+ * be spied). Production always uses the real `fs.watch`.
39
+ */
40
+ type WatchFn = typeof fs.watch;
41
+ /** @internal Test-only seam to inject a fake fs.watch implementation. */
42
+ export declare function __setFsWatchForTests(fn: WatchFn | null): void;
33
43
  /**
34
44
  * Options for the file watcher
35
45
  */
@@ -51,6 +61,14 @@ export interface WatchOptions {
51
61
  * Callback when a sync errors (for logging/diagnostics).
52
62
  */
53
63
  onSyncError?: (error: Error) => void;
64
+ /**
65
+ * Callback fired ONCE when live watching degrades permanently and auto-sync
66
+ * is disabled — OS watch-resource exhaustion (EMFILE/ENFILE), or a write lock
67
+ * held past the retry budget. The string is an actionable, human-readable
68
+ * reason. Lets a host (MCP server, daemon, CLI) tell the user that the index
69
+ * will no longer auto-update instead of silently serving stale results.
70
+ */
71
+ onDegraded?: (reason: string) => void;
54
72
  /**
55
73
  * Test-only. When true, `start()` installs NO OS-level fs.watch — the
56
74
  * watcher is "inert" and only the {@link __emitWatchEventForTests} /
@@ -111,6 +129,22 @@ export declare class FileWatcher {
111
129
  private dirWatchers;
112
130
  /** Set once the per-directory watch cap is hit, so we log only once. */
113
131
  private dirCapWarned;
132
+ /**
133
+ * Set once the Linux inotify watch limit (ENOSPC) is hit. Double duty: we
134
+ * warn only once, AND we stop attempting new directory watches for the rest
135
+ * of the session — once the kernel budget is exhausted every further
136
+ * `inotify_add_watch` fails too, so trying the rest of the tree is pure
137
+ * waste. NON-fatal (does not degrade): installed watches keep working.
138
+ */
139
+ private inotifyLimitWarned;
140
+ /**
141
+ * One-way latch: the reason live watching was permanently disabled at runtime
142
+ * (watch-resource exhaustion, or lock contention past the retry budget), or
143
+ * null while healthy. Set by {@link degrade}; cleared only by a fresh start().
144
+ */
145
+ private degradedReason;
146
+ /** Consecutive lock-contention retries for watcher-triggered syncs. */
147
+ private lockRetryCount;
114
148
  /** Test-only inert mode: started, but with no OS watcher installed. */
115
149
  private inert;
116
150
  private debounceTimer;
@@ -152,6 +186,7 @@ export declare class FileWatcher {
152
186
  private readonly syncFn;
153
187
  private readonly onSyncComplete?;
154
188
  private readonly onSyncError?;
189
+ private readonly onDegraded?;
155
190
  private readonly inertForTests;
156
191
  constructor(projectRoot: string, syncFn: () => Promise<{
157
192
  filesChanged: number;
@@ -211,6 +246,31 @@ export declare class FileWatcher {
211
246
  * dir-only ignore rule like `build/` matches.
212
247
  */
213
248
  private shouldIgnoreDir;
249
+ /**
250
+ * Permanently disable live watching after a terminal runtime failure
251
+ * (watch-resource exhaustion, or lock contention past the retry budget).
252
+ * Idempotent: logs one actionable warning, fires {@link WatchOptions.onDegraded}
253
+ * once, and stops the watcher. A subsequent start() clears the latch.
254
+ */
255
+ private degrade;
256
+ /**
257
+ * Warn ONCE that the Linux inotify watch budget is exhausted (ENOSPC), and
258
+ * stop adding new watches for the rest of this session — every further
259
+ * `inotify_add_watch` would fail too, so walking the rest of the tree is
260
+ * waste. Unlike {@link degrade} this is NON-fatal: the watches already
261
+ * installed keep firing, and `codegraph sync` covers the unwatched remainder.
262
+ * The message names the kernel knob to raise (`fs.inotify.max_user_watches`).
263
+ */
264
+ private warnInotifyLimit;
265
+ /**
266
+ * Whether live watching has degraded permanently (until the next start()).
267
+ * Distinct from {@link isActive}: a degraded watcher is inactive, but an
268
+ * inactive watcher is not necessarily degraded (it may simply be stopped or
269
+ * never started). Hosts use this to tell the user auto-sync is off.
270
+ */
271
+ isDegraded(): boolean;
272
+ /** The reason live watching degraded, or null if it is healthy. */
273
+ getDegradedReason(): string | null;
214
274
  /**
215
275
  * Stop watching for file changes.
216
276
  */
@@ -238,9 +298,15 @@ export declare class FileWatcher {
238
298
  */
239
299
  waitUntilReady(timeoutMs?: number): Promise<void>;
240
300
  /**
241
- * Schedule a debounced sync.
301
+ * Schedule a normal debounced sync after a source edit.
242
302
  */
243
303
  private scheduleSync;
304
+ /**
305
+ * Schedule a retry after a recoverable sync failure (lock contention). Kept
306
+ * separate from {@link scheduleSync} so prolonged contention backs off
307
+ * exponentially instead of hammering the lock every debounce cycle.
308
+ */
309
+ private scheduleRetrySync;
244
310
  /**
245
311
  * Flush pending changes by running sync.
246
312
  *
@@ -280,4 +346,5 @@ export declare class FileWatcher {
280
346
  * test runtime, where the registry is intentionally not populated).
281
347
  */
282
348
  export declare function __emitWatchEventForTests(projectRoot: string, relPath: string): boolean;
349
+ export {};
283
350
  //# sourceMappingURL=watcher.d.ts.map
@@ -28,8 +28,6 @@ export type LifecycleEvent = 'install' | 'index' | 'uninstall';
28
28
  /** Coarse buckets — exact counts are deliberately not collected. */
29
29
  export declare function bucketFileCount(n: number): '<100' | '100-1k' | '1k-10k' | '10k+';
30
30
  export declare function bucketDuration(ms: number): '<10s' | '10-60s' | '1-5m' | '5m+';
31
- /** Collapse a backend identifier (e.g. `node-sqlite`) to the schema's enum. */
32
- export declare function backendKind(backend: string): 'native' | 'wasm';
33
31
  /**
34
32
  * Shared "a full index completed" event (CLI init/index + installer local
35
33
  * init): language names and coarse buckets only — never paths, file names,
@@ -39,7 +37,6 @@ export declare function recordIndexEvent(cg: {
39
37
  getStats(): {
40
38
  filesByLanguage: Record<string, number>;
41
39
  };
42
- getBackend(): string;
43
40
  }, result: {
44
41
  filesIndexed: number;
45
42
  durationMs: number;
package/dist/utils.d.ts CHANGED
@@ -57,12 +57,25 @@ export declare function isConfigLeafNode(node: {
57
57
  * (codegraph_node `includeCode`, codegraph_explore source) go through here, so
58
58
  * this is the chokepoint that keeps out-of-root file contents from leaking.
59
59
  *
60
+ * `allowSymlinkEscape` waives **only** the realpath-escape rejection (the
61
+ * lexical `../` guard still applies) for the INDEXING read path. The directory
62
+ * walk deliberately descends into in-root symlinks whose targets live outside
63
+ * the root (e.g. a `game/` symlink in a Dota custom-game tree, #935); discovery
64
+ * and the reader must agree, or every file the walk enumerated fails to index.
65
+ * Indexing only reads paths it just discovered, into a local index — it never
66
+ * serves them to an agent, so this does not widen the #527 leak surface. The
67
+ * content-serving sinks must never pass this flag.
68
+ *
60
69
  * @param projectRoot - The project root directory
61
70
  * @param filePath - The (relative or absolute) file path to validate
71
+ * @param options.allowSymlinkEscape - Follow in-root symlinks out of the root
72
+ * (indexing read path only); defaults to the strict, leak-safe behavior.
62
73
  * @returns The resolved absolute path (realpath when it exists), or null if it
63
74
  * escapes the root
64
75
  */
65
- export declare function validatePathWithinRoot(projectRoot: string, filePath: string): string | null;
76
+ export declare function validatePathWithinRoot(projectRoot: string, filePath: string, options?: {
77
+ allowSymlinkEscape?: boolean;
78
+ }): string | null;
66
79
  /**
67
80
  * Validate that a path is a safe project root directory.
68
81
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colbymchenry/codegraph",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Local-first code intelligence for AI agents (MCP). Self-contained — bundles its own runtime.",
5
5
  "bin": {
6
6
  "codegraph": "npm-shim.js"
@@ -15,12 +15,12 @@
15
15
  "./package.json": "./package.json"
16
16
  },
17
17
  "optionalDependencies": {
18
- "@colbymchenry/codegraph-darwin-arm64": "1.0.0",
19
- "@colbymchenry/codegraph-darwin-x64": "1.0.0",
20
- "@colbymchenry/codegraph-linux-arm64": "1.0.0",
21
- "@colbymchenry/codegraph-linux-x64": "1.0.0",
22
- "@colbymchenry/codegraph-win32-arm64": "1.0.0",
23
- "@colbymchenry/codegraph-win32-x64": "1.0.0"
18
+ "@colbymchenry/codegraph-darwin-arm64": "1.1.0",
19
+ "@colbymchenry/codegraph-darwin-x64": "1.1.0",
20
+ "@colbymchenry/codegraph-linux-arm64": "1.1.0",
21
+ "@colbymchenry/codegraph-linux-x64": "1.1.0",
22
+ "@colbymchenry/codegraph-win32-arm64": "1.1.0",
23
+ "@colbymchenry/codegraph-win32-x64": "1.1.0"
24
24
  },
25
25
  "files": [
26
26
  "npm-shim.js",