@colbymchenry/codegraph 0.9.9 → 1.0.1

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 (52) hide show
  1. package/README.md +120 -24
  2. package/dist/bin/codegraph.d.ts +1 -0
  3. package/dist/bin/fatal-handler.d.ts +20 -0
  4. package/dist/db/migrations.d.ts +1 -1
  5. package/dist/db/queries.d.ts +43 -0
  6. package/dist/db/sqlite-adapter.d.ts +7 -0
  7. package/dist/directory.d.ts +49 -2
  8. package/dist/extraction/astro-extractor.d.ts +79 -0
  9. package/dist/extraction/extraction-version.d.ts +25 -0
  10. package/dist/extraction/function-ref.d.ts +118 -0
  11. package/dist/extraction/grammars.d.ts +7 -1
  12. package/dist/extraction/index.d.ts +34 -0
  13. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  14. package/dist/extraction/languages/csharp.d.ts +22 -0
  15. package/dist/extraction/languages/r.d.ts +3 -0
  16. package/dist/extraction/languages/typescript.d.ts +13 -0
  17. package/dist/extraction/liquid-extractor.d.ts +7 -0
  18. package/dist/extraction/razor-extractor.d.ts +42 -0
  19. package/dist/extraction/tree-sitter-types.d.ts +33 -0
  20. package/dist/extraction/tree-sitter.d.ts +211 -0
  21. package/dist/extraction/vue-extractor.d.ts +15 -0
  22. package/dist/index.d.ts +34 -2
  23. package/dist/installer/instructions-template.d.ts +34 -11
  24. package/dist/installer/targets/opencode.d.ts +9 -1
  25. package/dist/installer/targets/shared.d.ts +14 -0
  26. package/dist/mcp/daemon-manager.d.ts +42 -0
  27. package/dist/mcp/daemon-registry.d.ts +47 -0
  28. package/dist/mcp/daemon.d.ts +60 -1
  29. package/dist/mcp/dynamic-boundaries.d.ts +41 -0
  30. package/dist/mcp/index.d.ts +1 -0
  31. package/dist/mcp/liveness-watchdog.d.ts +18 -0
  32. package/dist/mcp/ppid-watchdog.d.ts +44 -0
  33. package/dist/mcp/proxy.d.ts +6 -0
  34. package/dist/mcp/server-instructions.d.ts +12 -1
  35. package/dist/mcp/session.d.ts +2 -0
  36. package/dist/mcp/stdin-teardown.d.ts +27 -0
  37. package/dist/mcp/tools.d.ts +71 -0
  38. package/dist/resolution/callback-synthesizer.d.ts +3 -3
  39. package/dist/resolution/frameworks/astro.d.ts +9 -0
  40. package/dist/resolution/frameworks/index.d.ts +1 -0
  41. package/dist/resolution/import-resolver.d.ts +10 -0
  42. package/dist/resolution/index.d.ts +80 -0
  43. package/dist/resolution/name-matcher.d.ts +61 -0
  44. package/dist/resolution/types.d.ts +27 -3
  45. package/dist/resolution/workspace-packages.d.ts +48 -0
  46. package/dist/search/query-utils.d.ts +17 -1
  47. package/dist/sync/watcher.d.ts +124 -32
  48. package/dist/telemetry/index.d.ts +146 -0
  49. package/dist/types.d.ts +17 -2
  50. package/dist/upgrade/index.d.ts +132 -0
  51. package/dist/utils.d.ts +30 -24
  52. package/package.json +7 -7
@@ -4,14 +4,31 @@
4
4
  * Watches the project directory for file changes and triggers debounced sync
5
5
  * operations to keep the code graph up-to-date.
6
6
  *
7
- * Uses chokidar, whose `ignored` callback filters directories BEFORE they are
8
- * watched so we never register inotify watches on excluded trees like
9
- * node_modules/, dist/, .git/ (fixes #276: recursive fs.watch exhausted the
10
- * kernel watch budget on large repos). The ignore decision reuses the indexer's
11
- * `buildDefaultIgnore` (built-in default-ignore dirs + the project's .gitignore)
12
- * so the watcher watches exactly the set the indexer indexes — in particular,
13
- * node_modules/build/cache dirs are excluded even when the repo has no
14
- * .gitignore (#407), which a .gitignore-only filter would miss.
7
+ * Uses Node's built-in `fs.watch` directly (no third-party watcher, no native
8
+ * addon) with a per-platform strategy chosen to keep the open-descriptor /
9
+ * kernel-watch cost BOUNDED rather than growing with the number of files:
10
+ *
11
+ * - macOS / Windows: a SINGLE recursive `fs.watch(root, {recursive:true})`.
12
+ * libuv maps this to one FSEvents stream (macOS) / one
13
+ * ReadDirectoryChangesW handle (Windows), so it costs O(1) descriptors no
14
+ * matter how large the tree. This is the fix for the macOS file-table
15
+ * exhaustion (#644 / #496 / #555 / #628): the previous watcher held one
16
+ * open fd PER WATCHED FILE on macOS (tens of thousands of REG fds), which
17
+ * exhausted `kern.maxfiles` and crashed unrelated processes system-wide.
18
+ *
19
+ * - Linux: recursive `fs.watch` is unsupported, so we watch each (non-ignored)
20
+ * DIRECTORY with one inotify watch — O(directories), NOT O(files). New
21
+ * directories are picked up dynamically and an overall watch cap bounds
22
+ * inotify usage on pathological monorepos (#579). A single inotify watch on
23
+ * a directory already reports create/modify/delete for its children, so
24
+ * per-file watches are never needed.
25
+ *
26
+ * Excluded trees (node_modules/, dist/, .git/, …) are filtered via the
27
+ * indexer's `buildScopeIgnore` (built-in default-ignore dirs + the project's
28
+ * .gitignore) — on Linux they're never descended into (so they cost no watch),
29
+ * and on macOS/Windows the single recursive stream still covers them but their
30
+ * events are dropped before any sync is scheduled. Either way the watcher's
31
+ * scope matches the indexer's (#276 / #407).
15
32
  */
16
33
  /**
17
34
  * Options for the file watcher
@@ -34,6 +51,15 @@ export interface WatchOptions {
34
51
  * Callback when a sync errors (for logging/diagnostics).
35
52
  */
36
53
  onSyncError?: (error: Error) => void;
54
+ /**
55
+ * Test-only. When true, `start()` installs NO OS-level fs.watch — the
56
+ * watcher is "inert" and only the {@link __emitWatchEventForTests} /
57
+ * {@link FileWatcher.ingestEventForTests} seam drives its pipeline. This
58
+ * restores the deterministic, OS-free behavior the unit tests need (real
59
+ * FSEvents/inotify delivery races under parallel vitest). Production never
60
+ * sets it.
61
+ */
62
+ inertForTests?: boolean;
37
63
  }
38
64
  /**
39
65
  * Thrown by a `syncFn` to signal that the underlying sync couldn't acquire
@@ -69,8 +95,9 @@ export interface PendingFile {
69
95
  * debounced sync operations via a provided callback.
70
96
  *
71
97
  * Design goals:
72
- * - Minimal resource usage (chokidar filters excluded directories before
73
- * registering an inotify watch see module docs / #276)
98
+ * - Bounded resource usage: O(1) descriptors on macOS/Windows (one recursive
99
+ * watch), O(directories) inotify watches on Linux never O(files), which
100
+ * was the system-crashing fd leak on macOS (#644/#496/#555/#628).
74
101
  * - Debounced to avoid thrashing on rapid saves
75
102
  * - Filters to supported source files by extension
76
103
  * - Ignores .codegraph/ and .git/ regardless of .gitignore
@@ -78,11 +105,18 @@ export interface PendingFile {
78
105
  * without blocking on a sync (issue #403)
79
106
  */
80
107
  export declare class FileWatcher {
81
- private watcher;
108
+ /** macOS/Windows: the single recursive watcher. Null on Linux. */
109
+ private recursiveWatcher;
110
+ /** Linux: one watcher per watched directory (keyed by absolute path). */
111
+ private dirWatchers;
112
+ /** Set once the per-directory watch cap is hit, so we log only once. */
113
+ private dirCapWarned;
114
+ /** Test-only inert mode: started, but with no OS watcher installed. */
115
+ private inert;
82
116
  private debounceTimer;
83
117
  /**
84
118
  * Files seen by the watcher since the last successful sync — populated on
85
- * every chokidar event, cleared at the start of a sync, and re-populated by
119
+ * every change event, cleared at the start of a sync, and re-populated by
86
120
  * events that arrive mid-sync (or restored on sync failure). Keyed by the
87
121
  * same project-relative POSIX path the rest of the codebase uses, so a
88
122
  * caller can intersect tool-response file paths against this map cheaply.
@@ -98,17 +132,18 @@ export declare class FileWatcher {
98
132
  private syncing;
99
133
  private stopped;
100
134
  /**
101
- * False until chokidar fires its `ready` event. Gates `pendingFiles`
102
- * insertion so the initial crawl's `add` events (one per pre-existing
103
- * source file) don't pollute the per-file staleness signal. The events
104
- * still flow into `scheduleSync()` to preserve the previous "initial
105
- * scan triggers a reconciling sync" behavior.
135
+ * True once the initial watch set is established. Unlike the previous
136
+ * chokidar implementation there is no asynchronous initial "crawl" emitting
137
+ * an `add` per existing file `fs.watch` only reports changes from the
138
+ * moment it's installed so this flips to true synchronously at the end of
139
+ * `start()`. The startup reconcile against on-disk state is handled
140
+ * separately by the engine's catch-up sync, not by the watcher.
106
141
  */
107
- private chokidarReady;
142
+ private ready;
108
143
  /**
109
- * Callbacks that resolve when chokidar fires `ready`. Used by tests (and
110
- * any production caller that cares about a clean baseline) to deterministically
111
- * gate on the end of the initial scan instead of guessing at a sleep duration.
144
+ * Callbacks that resolve when the watch set is established. Used by tests
145
+ * (and any production caller that cares about a clean baseline) to
146
+ * deterministically gate on watcher readiness.
112
147
  */
113
148
  private readyWaiters;
114
149
  private ignoreMatcher;
@@ -117,6 +152,7 @@ export declare class FileWatcher {
117
152
  private readonly syncFn;
118
153
  private readonly onSyncComplete?;
119
154
  private readonly onSyncError?;
155
+ private readonly inertForTests;
120
156
  constructor(projectRoot: string, syncFn: () => Promise<{
121
157
  filesChanged: number;
122
158
  durationMs: number;
@@ -126,32 +162,79 @@ export declare class FileWatcher {
126
162
  * Returns true if watching started successfully, false otherwise.
127
163
  */
128
164
  start(): boolean;
165
+ /**
166
+ * macOS/Windows: one recursive watcher for the whole tree. O(1) descriptors.
167
+ * `filename` arrives relative to the project root (with subdirectories), so
168
+ * it maps straight to a project-relative path.
169
+ */
170
+ private startRecursive;
171
+ /**
172
+ * Linux: walk the (non-ignored) tree and watch each directory. One inotify
173
+ * watch per directory reports create/modify/delete for that directory's
174
+ * direct children, so we never watch individual files.
175
+ */
176
+ private startPerDirectory;
177
+ /**
178
+ * Add an inotify watch for `dir` and recurse into its non-ignored
179
+ * subdirectories. When `markExisting` is true (a directory that appeared
180
+ * AFTER startup), the source files already inside it are recorded as pending
181
+ * — this closes the `mkdir + write` race where files created before the new
182
+ * directory's watch is installed would otherwise be missed until the next
183
+ * full sync. The initial startup walk passes false (the engine's catch-up
184
+ * sync owns the baseline).
185
+ */
186
+ private watchTree;
187
+ /**
188
+ * Linux per-directory event handler. `filename` is relative to `dir`. A new
189
+ * sub-directory is picked up by extending the watch tree; everything else is
190
+ * routed through the shared change handler.
191
+ */
192
+ private handleDirEvent;
193
+ /**
194
+ * Shared change handler for both watch strategies. `rel` is a
195
+ * project-relative POSIX path. Applies the ignore + source-file filters and,
196
+ * for a real source change, records it as pending (#403) and schedules a
197
+ * debounced sync.
198
+ *
199
+ * The recursive (macOS/Windows) watcher reports events for ignored trees too
200
+ * (one stream covers the whole repo), so the ignore check here is load-bearing
201
+ * — it drops node_modules/dist/.git churn before any sync is scheduled.
202
+ */
203
+ private handleChange;
204
+ /** Close and forget the watch for a directory that errored/was removed. */
205
+ private unwatchDir;
129
206
  /** Our own dirs are always ignored, regardless of .gitignore. */
130
207
  private isAlwaysIgnored;
131
208
  /**
132
- * chokidar `ignored` predicate — true for any path that should NOT be watched.
133
- * Uses chokidar's provided `stats` to decide directory-vs-file so a dir-only
134
- * rule like `build/` matches, without an extra `statSync` per path.
209
+ * True for any directory that should NOT be watched (used while building the
210
+ * Linux per-directory watch tree). Tests the directory form of the path so a
211
+ * dir-only ignore rule like `build/` matches.
135
212
  */
136
- private shouldIgnore;
213
+ private shouldIgnoreDir;
137
214
  /**
138
215
  * Stop watching for file changes.
139
216
  */
140
217
  stop(): void;
218
+ /**
219
+ * @internal Test-only: feed a synthetic project-relative change through the
220
+ * same filter → pendingFiles → debounced-sync path a real fs.watch event
221
+ * takes. Lets the watcher / staleness-banner suites stay deterministic
222
+ * instead of racing on OS watch-delivery latency. See
223
+ * {@link __emitWatchEventForTests}.
224
+ */
225
+ ingestEventForTests(relPath: string): void;
141
226
  /**
142
227
  * Whether the watcher is currently active.
143
228
  */
144
229
  isActive(): boolean;
145
230
  /**
146
- * Resolves once chokidar has fired its `ready` event (or immediately if
147
- * it has already done so). Useful for tests that need a deterministic
148
- * boundary before asserting on `pendingFiles` — guessing a sleep duration
149
- * is flaky under load because chokidar can take longer than expected to
150
- * finish its initial crawl on slow filesystems / parallel test runs.
231
+ * Resolves once the watch set has been installed (or immediately if it
232
+ * already has). Useful for tests that need a deterministic boundary before
233
+ * asserting on `pendingFiles`.
151
234
  *
152
235
  * Production callers don't need this: `pendingFiles` is read continuously,
153
- * the staleness banner is always correct (empty or populated), and the
154
- * initial-scan window is a small one-time startup cost.
236
+ * the staleness banner is always correct (empty or populated), and there is
237
+ * no asynchronous initial-scan window with `fs.watch`.
155
238
  */
156
239
  waitUntilReady(timeoutMs?: number): Promise<void>;
157
240
  /**
@@ -188,4 +271,13 @@ export declare class FileWatcher {
188
271
  */
189
272
  getPendingFiles(): PendingFile[];
190
273
  }
274
+ /**
275
+ * Test-only: synthesize a source-file change for the live watcher running at
276
+ * `projectRoot`, exercising the real filter → pendingFiles → debounced-sync
277
+ * logic without depending on fs.watch delivery timing (which races under
278
+ * parallel vitest). `relPath` is project-relative POSIX (e.g. "src/foo.ts").
279
+ * Returns false if no live watcher is registered for that root (e.g. outside a
280
+ * test runtime, where the registry is intentionally not populated).
281
+ */
282
+ export declare function __emitWatchEventForTests(projectRoot: string, relPath: string): boolean;
191
283
  //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Anonymous usage telemetry — client side.
3
+ *
4
+ * The contract for what may be collected lives in docs/design/telemetry.md
5
+ * (and user-facing TELEMETRY.md); the ingest endpoint that enforces it is
6
+ * public at telemetry-worker/. This module honors four invariants:
7
+ *
8
+ * 1. Zero hot-path cost: recording is an in-memory increment. Disk writes are
9
+ * a tiny synchronous append at process exit (works under `process.exit()`,
10
+ * where `beforeExit` never fires); network sends happen opportunistically
11
+ * (startup of long-running commands, daemon interval, bounded await at the
12
+ * end of install/init) and are fire-and-forget everywhere else.
13
+ * 2. Zero stdout: stdio is the MCP protocol channel. Notices and debug output
14
+ * go to stderr only.
15
+ * 3. Off is off: when disabled, nothing is recorded, nothing is sent, and no
16
+ * socket is opened — there is no "opted out" ping. Turning telemetry off
17
+ * also deletes any buffered, unsent data.
18
+ * 4. Fail silent: offline, endpoint down, disk full — every failure mode is
19
+ * silence, never a retry loop, never an error surfaced to the user/agent.
20
+ *
21
+ * Usage counts aggregate locally into per-day rollups; only *completed* (UTC)
22
+ * days are sent, so volume scales with active machines, not with tool calls.
23
+ */
24
+ export declare const TELEMETRY_ENDPOINT = "https://telemetry.getcodegraph.com/v1/events";
25
+ export declare const TELEMETRY_DOCS = "https://github.com/colbymchenry/codegraph/blob/main/TELEMETRY.md";
26
+ export type UsageKind = 'mcp_tool' | 'cli_command';
27
+ export type LifecycleEvent = 'install' | 'index' | 'uninstall';
28
+ /** Coarse buckets — exact counts are deliberately not collected. */
29
+ export declare function bucketFileCount(n: number): '<100' | '100-1k' | '1k-10k' | '10k+';
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
+ /**
34
+ * Shared "a full index completed" event (CLI init/index + installer local
35
+ * init): language names and coarse buckets only — never paths, file names,
36
+ * or exact counts. Structurally typed so callers don't need engine imports.
37
+ */
38
+ export declare function recordIndexEvent(cg: {
39
+ getStats(): {
40
+ filesByLanguage: Record<string, number>;
41
+ };
42
+ getBackend(): string;
43
+ }, result: {
44
+ filesIndexed: number;
45
+ durationMs: number;
46
+ }): void;
47
+ export interface ClientInfo {
48
+ name?: string;
49
+ version?: string;
50
+ }
51
+ export interface TelemetryStatus {
52
+ enabled: boolean;
53
+ /** What decided the current state — mirrors the precedence order. */
54
+ decidedBy: 'DO_NOT_TRACK' | 'CODEGRAPH_TELEMETRY' | 'config' | 'default';
55
+ machineId: string | null;
56
+ configPath: string;
57
+ }
58
+ export interface TelemetryOptions {
59
+ /** Global state dir; defaults to ~/.codegraph. Tests inject a temp dir. */
60
+ dir?: string;
61
+ fetchImpl?: typeof globalThis.fetch;
62
+ now?: () => Date;
63
+ env?: NodeJS.ProcessEnv;
64
+ stderr?: (line: string) => void;
65
+ /** Tests opt out so short-lived instances don't pile onto process 'exit'. */
66
+ installExitHook?: boolean;
67
+ }
68
+ export declare class Telemetry {
69
+ private readonly dir;
70
+ private readonly fetchImpl;
71
+ private readonly now;
72
+ private readonly env;
73
+ private readonly writeStderr;
74
+ private counts;
75
+ private events;
76
+ private readonly installExitHook;
77
+ private exitHookInstalled;
78
+ private configCache;
79
+ private intervalHandle;
80
+ constructor(opts?: TelemetryOptions);
81
+ get configPath(): string;
82
+ get queuePath(): string;
83
+ /**
84
+ * Resolution order (first match wins) — keep in sync with TELEMETRY.md:
85
+ * DO_NOT_TRACK=1 > CODEGRAPH_TELEMETRY=0|1 > stored config > default on.
86
+ */
87
+ getStatus(): TelemetryStatus;
88
+ isEnabled(): boolean;
89
+ /**
90
+ * Persist an explicit user choice (installer toggle or `codegraph
91
+ * telemetry on|off`). Turning telemetry off also deletes any buffered,
92
+ * unsent data — off means off.
93
+ */
94
+ setEnabled(enabled: boolean, source: 'installer' | 'cli'): void;
95
+ /** True once any consent decision (or the first-run notice) is on disk. */
96
+ hasStoredChoice(): boolean;
97
+ /** In-memory increment — safe on the MCP tool-call hot path. */
98
+ recordUsage(kind: UsageKind, name: string, ok: boolean, client?: ClientInfo): void;
99
+ /** install / index / uninstall — buffered like everything else. */
100
+ recordLifecycle(event: LifecycleEvent, props: Record<string, unknown>): void;
101
+ /**
102
+ * Fire-and-forget send of everything sendable. Never throws, never logs
103
+ * above debug. Safe to call at startup of long-running commands.
104
+ */
105
+ maybeFlush(): void;
106
+ /**
107
+ * Drain in-memory state to the buffer, then send completed-day rollups and
108
+ * lifecycle events. Bounded by `timeoutMs`; leftovers stay buffered for the
109
+ * next process. Awaited only where latency is invisible (install/init).
110
+ */
111
+ flushNow(timeoutMs?: number): Promise<void>;
112
+ /**
113
+ * Periodic flush for long-lived processes (MCP daemon / serve). Unref'd so
114
+ * it never keeps the process alive.
115
+ */
116
+ startInterval(everyMs?: number): void;
117
+ stopInterval(): void;
118
+ private utcDay;
119
+ private readConfig;
120
+ private writeConfig;
121
+ /**
122
+ * Default-on consent is gated by a one-time stderr notice (interactive
123
+ * installs record their choice explicitly and never reach this).
124
+ */
125
+ private firstRunNotice;
126
+ /**
127
+ * Synchronous, tiny, exit-safe: drain in-memory deltas to the JSONL queue.
128
+ * Runs on `process.on('exit')`, so it must never be async or slow.
129
+ */
130
+ persistSync(): void;
131
+ private appendLines;
132
+ /**
133
+ * Atomically claim the queue for sending (rename). Concurrent processes
134
+ * can't double-send; a crash mid-send leaves a claim file that
135
+ * `recoverStaleClaims` merges back after an hour.
136
+ */
137
+ private claimQueue;
138
+ private recoverStaleClaims;
139
+ /** Returns the lines that did NOT make it out (to be re-queued). */
140
+ private send;
141
+ private packageVersion;
142
+ private ensureExitHook;
143
+ private debug;
144
+ }
145
+ export declare function getTelemetry(): Telemetry;
146
+ //# sourceMappingURL=index.d.ts.map
package/dist/types.d.ts CHANGED
@@ -20,7 +20,7 @@ export type EdgeKind = 'contains' | 'calls' | 'imports' | 'exports' | 'extends'
20
20
  * Supported programming languages. See NODE_KINDS for why this is a
21
21
  * runtime-iterable const array.
22
22
  */
23
- export declare const LANGUAGES: readonly ["typescript", "javascript", "tsx", "jsx", "python", "go", "rust", "java", "c", "cpp", "csharp", "php", "ruby", "swift", "kotlin", "dart", "svelte", "vue", "liquid", "pascal", "scala", "lua", "luau", "objc", "yaml", "twig", "xml", "properties", "unknown"];
23
+ export declare const LANGUAGES: readonly ["typescript", "javascript", "tsx", "jsx", "python", "go", "rust", "java", "c", "cpp", "csharp", "razor", "php", "ruby", "swift", "kotlin", "dart", "svelte", "vue", "astro", "liquid", "pascal", "scala", "lua", "luau", "objc", "r", "yaml", "twig", "xml", "properties", "unknown"];
24
24
  export type Language = (typeof LANGUAGES)[number];
25
25
  /**
26
26
  * A node in the knowledge graph representing a code symbol
@@ -64,6 +64,14 @@ export interface Node {
64
64
  decorators?: string[];
65
65
  /** Generic type parameters */
66
66
  typeParameters?: string[];
67
+ /**
68
+ * Normalized return/result type name for a function/method (the bare class
69
+ * name, smart-pointer pointee unwrapped). Captured for C/C++ so resolution
70
+ * can infer a chained receiver's type from what the inner call returns —
71
+ * `Foo::instance().bar()` resolves `bar` on `Foo` (issue #645). Undefined for
72
+ * languages/symbols where it isn't captured.
73
+ */
74
+ returnType?: string;
67
75
  /** When the node was last updated */
68
76
  updatedAt: number;
69
77
  }
@@ -139,6 +147,13 @@ export interface ExtractionError {
139
147
  /** Error code for categorization */
140
148
  code?: string;
141
149
  }
150
+ /**
151
+ * Kinds an unresolved reference can carry. `function_ref` is internal-only —
152
+ * a function name used as a VALUE (callback registration, #756). It never
153
+ * becomes an edge kind: resolution maps it to a `references` edge targeting
154
+ * function/method nodes only (see `matchFunctionRef`).
155
+ */
156
+ export type ReferenceKind = EdgeKind | 'function_ref';
142
157
  /**
143
158
  * A reference that couldn't be resolved during extraction
144
159
  */
@@ -148,7 +163,7 @@ export interface UnresolvedReference {
148
163
  /** Name being referenced */
149
164
  referenceName: string;
150
165
  /** Type of reference (call, type, import, etc.) */
151
- referenceKind: EdgeKind;
166
+ referenceKind: ReferenceKind;
152
167
  /** Location of the reference */
153
168
  line: number;
154
169
  column: number;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * `codegraph upgrade`
3
+ *
4
+ * Self-update for the CLI, whatever way it was installed:
5
+ *
6
+ * - **bundle** — the self-contained runtime+app installed by `install.sh`
7
+ * (Linux/macOS) or `install.ps1` (Windows). Upgrading re-runs the SAME
8
+ * canonical installer script (single source of truth) so the download /
9
+ * version-resolution / PATH logic never drifts between first-install and
10
+ * upgrade.
11
+ * - **npm** — installed via `npm i -g @colbymchenry/codegraph`. Upgrading
12
+ * shells out to npm.
13
+ * - **npx** — ephemeral; nothing to upgrade (next `npx` fetches latest).
14
+ * - **source** — a git checkout running its own `dist/`; `git pull` + rebuild.
15
+ *
16
+ * Detection is structural (see `detectInstallMethod`): a bundle carries a
17
+ * vendored `node` binary and a `bin/codegraph` launcher next to its `lib/`, so
18
+ * we can recognize it from the running file's path without a marker file.
19
+ *
20
+ * Windows wrinkle: a running `node.exe` is locked and can't be deleted, so the
21
+ * bundle's `current\` dir can't be overwritten in place by the process doing
22
+ * the upgrade. We therefore spawn a DETACHED helper that waits for this
23
+ * process to exit (releasing the lock), then runs `install.ps1`. This is the
24
+ * conventional Windows self-update dance (rustup/nvm-windows do the same).
25
+ */
26
+ export declare const REPO = "colbymchenry/codegraph";
27
+ export declare const NPM_PACKAGE = "@colbymchenry/codegraph";
28
+ export declare const INSTALL_SH_URL = "https://raw.githubusercontent.com/colbymchenry/codegraph/main/install.sh";
29
+ export type InstallMethod = {
30
+ kind: 'bundle';
31
+ os: 'unix' | 'windows';
32
+ bundleRoot: string;
33
+ installDir: string | null;
34
+ } | {
35
+ kind: 'npm';
36
+ scope: 'global' | 'local';
37
+ } | {
38
+ kind: 'npx';
39
+ } | {
40
+ kind: 'source';
41
+ root: string;
42
+ } | {
43
+ kind: 'unknown';
44
+ reason: string;
45
+ };
46
+ export interface DetectInput {
47
+ /** `__filename` of the running CLI module — `<…>/dist/bin/codegraph.js`. */
48
+ filename: string;
49
+ platform: NodeJS.Platform;
50
+ cwd: string;
51
+ /** Injectable existence probe (defaults to fs.existsSync) — for tests. */
52
+ exists?: (p: string) => boolean;
53
+ }
54
+ /**
55
+ * Where the bundle installer keeps its install root, derived from the bundle
56
+ * dir so an upgrade reuses a custom `CODEGRAPH_INSTALL_DIR`. Returns null when
57
+ * the layout isn't the one the installer creates (then the installer falls
58
+ * back to its own default).
59
+ *
60
+ * unix: <installDir>/versions/<vX.Y.Z> (bundleRoot) → <installDir>
61
+ * windows: <installDir>\current (bundleRoot) → <installDir>
62
+ */
63
+ export declare function deriveInstallDir(bundleRoot: string, os: 'unix' | 'windows', exists: (p: string) => boolean): string | null;
64
+ export declare function detectInstallMethod(input: DetectInput): InstallMethod;
65
+ export interface Semver {
66
+ major: number;
67
+ minor: number;
68
+ patch: number;
69
+ pre: string | null;
70
+ }
71
+ export declare function parseSemver(version: string): Semver | null;
72
+ /** Returns >0 if a>b, <0 if a<b, 0 if equal. Throws on unparseable input. */
73
+ export declare function compareVersions(a: string, b: string): number;
74
+ export declare function isUpdateAvailable(current: string, latest: string): boolean;
75
+ /** `0.9.9` / `v0.9.9` → `v0.9.9` (release tags are v-prefixed). */
76
+ export declare function normalizeVersion(v: string): string;
77
+ /** Strip a leading `v`: `v0.9.9` → `0.9.9`. */
78
+ export declare function stripV(v: string): string;
79
+ /**
80
+ * Parse the release tag out of the `Location` header GitHub returns for
81
+ * `/releases/latest` → `…/releases/tag/v0.9.9`. Pure so it's unit-tested.
82
+ */
83
+ export declare function parseLatestTagFromLocation(location: string | undefined): string | null;
84
+ /**
85
+ * Resolve the latest release tag (e.g. `v0.9.9`).
86
+ *
87
+ * Primary: read the redirect `Location` from `github.com/<repo>/releases/latest`
88
+ * — same trick install.sh uses, because the unauthenticated GitHub API is
89
+ * rate-limited to 60 req/h/IP and 403s on shared/cloud hosts (issue #325). The
90
+ * redirect has no such limit. Fall back to the API only if the redirect can't
91
+ * be read.
92
+ */
93
+ export declare function resolveLatestVersion(repo?: string, timeoutMs?: number): Promise<string>;
94
+ export interface UpgradeOptions {
95
+ /** Pin a specific version (positional arg or CODEGRAPH_VERSION). */
96
+ version?: string;
97
+ /** Report current vs latest, don't change anything. */
98
+ check?: boolean;
99
+ /** Reinstall even if already on the resolved version. */
100
+ force?: boolean;
101
+ }
102
+ /** Injectable side-effects so the orchestrator stays unit-testable. */
103
+ export interface UpgradeDeps {
104
+ currentVersion: string;
105
+ method: InstallMethod;
106
+ resolveLatest: (pin?: string) => Promise<string>;
107
+ /** Run a command inheriting stdio; returns its exit code (-1 = spawn failed). */
108
+ run: (cmd: string, args: string[], env?: NodeJS.ProcessEnv) => number;
109
+ hasCommand: (cmd: string) => boolean;
110
+ log: (msg: string) => void;
111
+ warn: (msg: string) => void;
112
+ error: (msg: string) => void;
113
+ platform: NodeJS.Platform;
114
+ }
115
+ /** The honest, additive re-index reminder shown after a successful upgrade. */
116
+ export declare function reindexAdvisory(): string;
117
+ /**
118
+ * Returns the process exit code (0 = success / nothing to do, 1 = failure).
119
+ */
120
+ export declare function runUpgrade(opts: UpgradeOptions, deps: UpgradeDeps): Promise<number>;
121
+ /** Build the in-place Windows upgrade script (exported for unit-testing). */
122
+ export declare function buildWindowsUpgradeScript(bundleRoot: string, version: string, arch: string): string;
123
+ /**
124
+ * True if `cmd` resolves to an executable on PATH. A pure-Node PATH scan — NOT
125
+ * a spawned `command -v`/`which`: `command` is a shell builtin (no standalone
126
+ * binary on Debian, though macOS ships one), and `which` isn't guaranteed
127
+ * present on minimal images, so spawning either is unreliable. Scanning PATH
128
+ * ourselves behaves identically on every platform.
129
+ */
130
+ export declare function hasCommand(cmd: string): boolean;
131
+ export declare function defaultRun(cmd: string, args: string[], env?: NodeJS.ProcessEnv): number;
132
+ //# sourceMappingURL=index.d.ts.map
package/dist/utils.d.ts CHANGED
@@ -29,12 +29,38 @@
29
29
  * ```
30
30
  */
31
31
  /**
32
- * Validate that a resolved file path stays within the project root.
33
- * Prevents path traversal attacks (e.g. node.filePath = "../../etc/passwd").
32
+ * Config "languages" whose nodes are pure key/value DATA lifted from a config
33
+ * file (e.g. Spring `application.{yml,properties}`), not source code.
34
+ */
35
+ export declare const CONFIG_LEAF_LANGUAGES: ReadonlySet<string>;
36
+ /**
37
+ * A config-leaf node is a single key lifted out of a pure config/data file —
38
+ * `kind: 'constant'` in a {@link CONFIG_LEAF_LANGUAGES} language. Its on-disk
39
+ * line is `key = <value>`, and that value is routinely a secret (DB password,
40
+ * API key, JDBC URL with embedded creds). CodeGraph must surface the KEY only
41
+ * and never read/return the value, or it pushes secrets into agent context
42
+ * unbidden — the value isn't needed for resolution, and an agent that genuinely
43
+ * needs it can read the file directly. (#383)
44
+ */
45
+ export declare function isConfigLeafNode(node: {
46
+ kind: string;
47
+ language?: string;
48
+ }): boolean;
49
+ /**
50
+ * Validate that a file path stays within the project root, resolving symlinks.
51
+ *
52
+ * Two layers: a cheap lexical check that catches `../` traversal, then a
53
+ * realpath check that catches symlink escapes — an in-repo symlink whose
54
+ * logical path is inside the root but whose real target points outside it
55
+ * (issue #527). A symlink that stays within the root is still allowed, so
56
+ * legitimate in-tree symlinks keep working. Both content-serving read sinks
57
+ * (codegraph_node `includeCode`, codegraph_explore source) go through here, so
58
+ * this is the chokepoint that keeps out-of-root file contents from leaking.
34
59
  *
35
60
  * @param projectRoot - The project root directory
36
- * @param filePath - The relative file path to validate
37
- * @returns The resolved absolute path, or null if it escapes the root
61
+ * @param filePath - The (relative or absolute) file path to validate
62
+ * @returns The resolved absolute path (realpath when it exists), or null if it
63
+ * escapes the root
38
64
  */
39
65
  export declare function validatePathWithinRoot(projectRoot: string, filePath: string): string | null;
40
66
  /**
@@ -48,26 +74,6 @@ export declare function validatePathWithinRoot(projectRoot: string, filePath: st
48
74
  * @returns An error message if invalid, or null if valid
49
75
  */
50
76
  export declare function validateProjectPath(dirPath: string): string | null;
51
- /**
52
- * Check if a file path resolves to a location within the given root directory.
53
- *
54
- * Prevents path traversal attacks by ensuring the resolved absolute path
55
- * starts with the resolved root path. Handles '..' sequences, symlink-like
56
- * relative paths, and platform-specific separators.
57
- *
58
- * @param filePath - The path to check (can be relative or absolute)
59
- * @param rootDir - The root directory that filePath must stay within
60
- * @returns true if filePath resolves to a location within rootDir
61
- */
62
- export declare function isPathWithinRoot(filePath: string, rootDir: string): boolean;
63
- /**
64
- * Like isPathWithinRoot but also resolves symlinks via fs.realpathSync.
65
- *
66
- * This catches symlink escapes where the logical path appears to be within
67
- * root but the real path on disk points elsewhere. Falls back to logical
68
- * path checking if realpath resolution fails (e.g. broken symlink).
69
- */
70
- export declare function isPathWithinRootReal(filePath: string, rootDir: string): boolean;
71
77
  /**
72
78
  * Safely parse JSON with a fallback value.
73
79
  * Prevents crashes from corrupted database metadata.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colbymchenry/codegraph",
3
- "version": "0.9.9",
3
+ "version": "1.0.1",
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": "0.9.9",
19
- "@colbymchenry/codegraph-darwin-x64": "0.9.9",
20
- "@colbymchenry/codegraph-linux-arm64": "0.9.9",
21
- "@colbymchenry/codegraph-linux-x64": "0.9.9",
22
- "@colbymchenry/codegraph-win32-arm64": "0.9.9",
23
- "@colbymchenry/codegraph-win32-x64": "0.9.9"
18
+ "@colbymchenry/codegraph-darwin-arm64": "1.0.1",
19
+ "@colbymchenry/codegraph-darwin-x64": "1.0.1",
20
+ "@colbymchenry/codegraph-linux-arm64": "1.0.1",
21
+ "@colbymchenry/codegraph-linux-x64": "1.0.1",
22
+ "@colbymchenry/codegraph-win32-arm64": "1.0.1",
23
+ "@colbymchenry/codegraph-win32-x64": "1.0.1"
24
24
  },
25
25
  "files": [
26
26
  "npm-shim.js",