@javierkaiser/opencode-indexing 0.2.3 → 0.3.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 CHANGED
@@ -5,6 +5,84 @@ All notable changes to this project are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.3.0] - 2026-10-16
9
+
10
+ ### Added
11
+ - **`indexing_build` and `indexing_import` now also start codegraph** for the
12
+ workspace, so the semantic index and the code graph are built by the same
13
+ action instead of one silently going stale. `init` the first time a folder is
14
+ seen, `sync` afterwards — the second is what covers the case where codegraph's
15
+ daemon has already shut down, which it does after five minutes without
16
+ clients.
17
+
18
+ **codegraph is a companion, not a dependency.** Nothing about this plugin
19
+ requires it: with the CLI absent, `available()` returns false, nothing is
20
+ spawned, no build fails, and no line is added to the output. That path is
21
+ covered by a test that hides codegraph from `PATH` and runs the real spawner,
22
+ not a fake — an injected runner would have proved the logic while leaving the
23
+ wiring unchecked.
24
+
25
+ - `codegraph` setting, on by default (`/indexing-config set codegraph=false` to
26
+ turn it off). Read as `!== false`, because the key is in no config file anyone
27
+ has and a default that required it to be present would never apply.
28
+
29
+ - One line in `indexing_status` saying whether codegraph is indexed for the
30
+ folder. Read from the filesystem, so it costs no process — status runs on
31
+ every search through the footer.
32
+
33
+ ### Changed
34
+ - The build summary reports the codegraph command it started, and always in the
35
+ past tense of a *launch*, never of a success: the process is detached, so at
36
+ the moment the tool answers, codegraph has only begun. Anything that goes
37
+ wrong afterwards lands in `.codegraph/daemon.log`, which is where the message
38
+ points.
39
+
40
+ ### Notes for review
41
+ - A dangerous workspace root — the home directory or a drive root — is refused
42
+ rather than indexed. codegraph has `-f` for exactly that case, and passing it
43
+ because a tool call said so would index the whole machine.
44
+ - Launches are rate-limited per workspace to one per 15 seconds, because
45
+ codegraph runs its own daemon with a file watcher and two `sync` invocations
46
+ seconds apart contend for the same SQLite database.
47
+ - `.codegraph/` was already excluded from the scanner (every dot-directory that
48
+ is not explicitly allowed is skipped), and there is now a test pinning it:
49
+ codegraph writes a multi-megabyte SQLite database plus a WAL there, and
50
+ letting the scanner walk it would embed another indexer's storage into this
51
+ one, for money, forever.
52
+
53
+ ## [0.2.4] - 2026-10-16
54
+
55
+ ### Fixed
56
+ - **The prompt-footer indicator stopped rendering after OpenCode 2.0.30.** The
57
+ footer view imports `createSignal` from `solid-js` as a value, and `solid-js`,
58
+ `@opentui/core` and `@opentui/solid` were declared as *optional
59
+ peerDependencies* — which means npm never installs them, on the assumption that
60
+ the host already provides them. 2.0.30 stopped providing them, the plugin's own
61
+ `import()` of the footer view failed, the `catch` swallowed it, and the slot
62
+ rendered nothing. Everything else kept working, because nothing else needs
63
+ JSX: the command palette comes from `tui.ts` itself, which is what made it look
64
+ like a cosmetic regression rather than a missing dependency.
65
+
66
+ The three packages are now `optionalDependencies`. That still tolerates a
67
+ failed install, but unlike an optional peer it actually puts them on disk, so
68
+ the indicator no longer depends on what the host happens to supply.
69
+
70
+ - **A package listed in both `optionalDependencies` and `devDependencies` is not
71
+ installed at all.** npm 12 lets the dev list win, so `--omit=dev` skips the
72
+ package and the optionalDependency never reaches disk — and `package.json`
73
+ still reads as though it is declared. This is why moving the three packages
74
+ was not by itself enough; they had to leave `devDependencies` as well.
75
+
76
+ - `tsconfig.json` is published. `jsx: react-jsx` with
77
+ `jsxImportSource: "@opentui/solid"` is what makes a `.tsx` file load at all, and
78
+ it was not in `files`.
79
+
80
+ ### Added
81
+ - `test/package-deps.test.ts` guards the manifest against all three of the
82
+ above, and asserts that every value import in the published sources resolves
83
+ to something the manifest installs — so the next third-party import fails a
84
+ test instead of becoming another silent missing module at load time.
85
+
8
86
  ## [0.2.3] - 2026-10-16
9
87
 
10
88
  ### Added
package/README.md CHANGED
@@ -155,6 +155,7 @@ Settable keys (case-insensitive):
155
155
  | `ollamaurl` | Ollama base URL |
156
156
  | `openaicompatibleurl` / `openaicompatiblekey` | OpenAI-compatible endpoint |
157
157
  | `importfromkilo` | `true` to import from Kilo when building, `false` to start from scratch |
158
+ | `codegraph` | `false` to stop also running the `codegraph` CLI on build. On by default; a no-op when codegraph is not installed |
158
159
  | `autorefresh` | Refresh the own index automatically on search |
159
160
  | `enabled` | Master switch. `false` pauses writing and refreshing; search keeps reading Kilo's index |
160
161
  | `searchmaxresults` | Result cap per search |
@@ -383,6 +384,12 @@ Word about data safety: Kilo's indexes (`ws-*` collections and its LanceDB direc
383
384
  ## Requirements
384
385
 
385
386
  - OpenCode v2 (plugin API `@opencode/plugin@2.x`).
387
+ - **For the prompt-footer indicator:** `solid-js`, `@opentui/core` and
388
+ `@opentui/solid`, which the plugin installs itself (~14 MB). They used to be
389
+ declared as optional *peer* dependencies, on the assumption that the host
390
+ already had them; OpenCode 2.0.30 stopped providing them and the indicator
391
+ silently stopped rendering. Everything else works without them — the settings
392
+ page degrades to dialogs and the footer is simply absent.
386
393
  - **For the own index: nothing to run.** The default store is embedded LanceDB
387
394
  (`@lancedb/lancedb@0.26.2`), so there is no server to start, no port to free and
388
395
  no container to keep alive.
@@ -483,6 +490,20 @@ symbol/edge extraction into SQLite, exposed as `indexing_graph_*` tools that
483
490
  chain after `indexing_search`) is a candidate for a future major version; the
484
491
  tree-sitter infrastructure is already in place. Independently, the installer can itself install and register codegraph as a companion MCP (`--with-codegraph`, opt-in, published-package mode only).
485
492
 
493
+ **Indexing both at once.** `indexing_build` and `indexing_import` also start
494
+ codegraph for the workspace, so the two indexes are built by the same action:
495
+ `codegraph init --yes` the first time a folder is seen, `codegraph sync`
496
+ afterwards. codegraph is a **companion, not a dependency** — with the CLI absent
497
+ nothing is spawned, no build fails and no line is added to the output. Turn it
498
+ off with `/indexing-config set codegraph=false`.
499
+
500
+ The launch is detached and reported as started, never as finished: on a large
501
+ monorepo `init` runs for minutes, and a tool call that blocked that long would
502
+ be worse than one that returns. Anything that fails afterwards lands in
503
+ `.codegraph/daemon.log`. A home directory or a drive root is refused rather than
504
+ indexed, and launches are limited to one per 15 seconds per workspace because
505
+ codegraph's own daemon already watches files.
506
+
486
507
  ## Development
487
508
 
488
509
  ```powershell
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@javierkaiser/opencode-indexing",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "OpenCode plugin: semantic code search that reuses Kilo Code indexes (Qdrant/LanceDB) and maintains its own independent index",
5
5
  "license": "MIT",
6
6
  "author": "Carlos Javier Lust <javierkaiser@gmail.com>",
@@ -37,6 +37,7 @@
37
37
  "tui.ts",
38
38
  "tui",
39
39
  "src",
40
+ "tsconfig.json",
40
41
  "install.mjs",
41
42
  "install.sh",
42
43
  "install.ps1",
@@ -65,23 +66,20 @@
65
66
  "@lancedb/lancedb": "0.26.2",
66
67
  "tree-sitter-wasms": "0.1.13",
67
68
  "web-tree-sitter": "0.25.10",
68
- "apache-arrow": "18.1.0"
69
+ "apache-arrow": "18.1.0",
70
+ "solid-js": "^1.9.12",
71
+ "@opentui/core": "0.5.14",
72
+ "@opentui/solid": "0.5.14"
69
73
  },
70
74
  "devDependencies": {
71
75
  "@opencode/plugin": "2.0.23",
72
76
  "@opencode/theme": "^2.0.23",
73
- "@opentui/core": "^0.5.14",
74
- "@opentui/solid": "^0.5.14",
75
77
  "@types/node": "^24.0.0",
76
- "solid-js": "^1.9.12",
77
78
  "typescript": "^5.8.2"
78
79
  },
79
80
  "peerDependencies": {
80
81
  "@opencode/plugin": ">=2.0.0",
81
- "@opencode/theme": ">=2.0.23",
82
- "@opentui/core": ">=0.5.14",
83
- "@opentui/solid": ">=0.5.14",
84
- "solid-js": ">=1.9.12"
82
+ "@opencode/theme": ">=2.0.23"
85
83
  },
86
84
  "peerDependenciesMeta": {
87
85
  "@opencode/plugin": {
@@ -89,15 +87,6 @@
89
87
  },
90
88
  "@opencode/theme": {
91
89
  "optional": true
92
- },
93
- "@opentui/core": {
94
- "optional": true
95
- },
96
- "@opentui/solid": {
97
- "optional": true
98
- },
99
- "solid-js": {
100
- "optional": true
101
90
  }
102
91
  }
103
92
  }
@@ -0,0 +1,255 @@
1
+ import * as fs from "node:fs"
2
+ import * as path from "node:path"
3
+
4
+ import { assessScope } from "./scope.ts"
5
+
6
+ /**
7
+ * Companion integration with CodeGraph, the code-graph indexer.
8
+ *
9
+ * Two indexes, two signals: this plugin answers "where is the code that does
10
+ * something like X", codegraph answers "who calls X, what breaks if I change
11
+ * it". They are complementary, and indexing only one leaves the other stale.
12
+ *
13
+ * The decision this module makes is narrow on purpose. It never searches, never
14
+ * parses, never reads codegraph's database: it either launches `codegraph init`
15
+ * or `codegraph sync` and reports what it launched. Everything it knows about
16
+ * codegraph comes from running its CLI.
17
+ */
18
+
19
+ /** Directory codegraph writes its index into. Its presence is the "is it indexed" answer. */
20
+ const CODEGRAPH_DIR = ".codegraph"
21
+
22
+ /**
23
+ * Minimum seconds between launches for one workspace.
24
+ *
25
+ * codegraph is not the only writer of `.codegraph/`: it runs a daemon with a
26
+ * file watcher that auto-syncs on change. Two `sync` invocations seconds apart
27
+ * contend for the same database, so the second one is skipped rather than
28
+ * allowed to collide. Also the cheap defence against a user hammering build.
29
+ */
30
+ const MIN_LAUNCH_INTERVAL_MS = 15_000
31
+
32
+ export interface CodegraphLaunch {
33
+ /** `init` on a folder that was never indexed, `sync` otherwise. */
34
+ action: "init" | "sync"
35
+ /** Command as run, for the message the user sees. */
36
+ command: string
37
+ /** Why it ran, or why it did not. Always present. */
38
+ detail: string
39
+ }
40
+
41
+ export type CodegraphOutcome =
42
+ | { kind: "launched"; launch: CodegraphLaunch }
43
+ /** Nothing was started; `reason` is user-facing and complete on its own. */
44
+ | { kind: "skipped"; reason: string }
45
+ /** Could not start at all. Never fatal: the semantic index does not depend on this. */
46
+ | { kind: "failed"; reason: string }
47
+
48
+ /**
49
+ * The seam that keeps the test suite away from real processes.
50
+ *
51
+ * `src/` had no `child_process` use before this, so there is no precedent to
52
+ * follow. Defaulting to the real spawner and injecting in tests keeps the module
53
+ * honest without making every test shell out.
54
+ */
55
+ export interface CodegraphRunner {
56
+ /** Resolves true when the `codegraph` CLI answers. Never throws. */
57
+ available(): Promise<boolean>
58
+ /**
59
+ * Starts the command and returns immediately.
60
+ *
61
+ * Deliberately not awaited to completion: `init` on a large monorepo can run
62
+ * for minutes, and a tool call that blocks that long is worse than one that
63
+ * finishes. The caller therefore reports "started", never "succeeded".
64
+ */
65
+ launch(args: string[], cwd: string): Promise<void>
66
+ }
67
+
68
+ /** Monotonic-enough clock, injectable so tests do not have to wait 15 seconds. */
69
+ export interface CodegraphClock {
70
+ now(): number
71
+ }
72
+
73
+ const realClock: CodegraphClock = { now: () => Date.now() }
74
+
75
+ /** Last launch per workspace, so the interval gate survives across calls. */
76
+ const lastLaunch = new Map<string, number>()
77
+
78
+ /** Whether `.codegraph/` exists. Sync rather than async: it runs on every build. */
79
+ export function isCodegraphInitialized(root: string): boolean {
80
+ try {
81
+ return fs.existsSync(path.join(root, CODEGRAPH_DIR))
82
+ } catch {
83
+ return false
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Spawn options shared by both commands.
89
+ *
90
+ * `shell` is true on Windows and false elsewhere, and it is not a stylistic
91
+ * choice: npm installs its executables as `codegraph.cmd` / `codegraph.ps1`
92
+ * shims, which `spawn` cannot resolve on its own — it reports ENOENT for a
93
+ * binary that is installed and working. Verified by running both shapes.
94
+ *
95
+ * The arguments are fixed literals chosen here, never user input, so there is
96
+ * no path for quoting to matter; `shell` is what buys Windows compatibility.
97
+ */
98
+ function spawnOptions(cwd?: string): Record<string, unknown> {
99
+ return {
100
+ cwd,
101
+ stdio: "ignore",
102
+ windowsHide: true,
103
+ shell: process.platform === "win32",
104
+ }
105
+ }
106
+
107
+ /**
108
+ * The CLI runner.
109
+ *
110
+ * `version` is the cheapest command that proves both existence and runnability;
111
+ * `init --help` would do too but prints help. stdio is ignored rather than
112
+ * inherited so codegraph's output cannot corrupt the TUI, and `--yes` on init is
113
+ * what keeps it from blocking on a prompt nobody can answer.
114
+ */
115
+ export const realRunner: CodegraphRunner = {
116
+ async available() {
117
+ const { spawn } = await import("node:child_process")
118
+ return new Promise<boolean>((resolve) => {
119
+ try {
120
+ const child = spawn("codegraph", ["version"], spawnOptions())
121
+ child.once("error", () => resolve(false))
122
+ child.once("close", (code) => resolve(code === 0))
123
+ } catch {
124
+ resolve(false)
125
+ }
126
+ })
127
+ },
128
+ async launch(args, cwd) {
129
+ const { spawn } = await import("node:child_process")
130
+ await new Promise<void>((resolve) => {
131
+ let child: ReturnType<typeof spawn>
132
+ try {
133
+ child = spawn("codegraph", args, {
134
+ ...spawnOptions(cwd),
135
+ // Detached so the index keeps building after the service restarts.
136
+ // Without this a restart mid-init orphans a half-written database.
137
+ // Not on Windows: a detached shell process is not unref-able the same
138
+ // way, and the host holds the handle anyway.
139
+ detached: process.platform !== "win32",
140
+ })
141
+ // Nothing is awaited on purpose: see the interface note. `unref` only
142
+ // exists on non-Windows, where detached was requested at all.
143
+ child.unref()
144
+ } catch {
145
+ resolve()
146
+ return
147
+ }
148
+ child.once("error", () => resolve())
149
+ child.once("spawn", () => resolve())
150
+ })
151
+ },
152
+ }
153
+
154
+ export interface CodegraphOptions {
155
+ root: string
156
+ enabled: boolean
157
+ runner?: CodegraphRunner
158
+ clock?: CodegraphClock
159
+ }
160
+
161
+ /**
162
+ * Launch codegraph for a workspace, if it makes sense to.
163
+ *
164
+ * Returns what happened; the caller decides how to say it. Never throws: a
165
+ * companion indexer must not be able to fail the semantic one.
166
+ */
167
+ export async function indexWithCodegraph(options: CodegraphOptions): Promise<CodegraphOutcome> {
168
+ const { root, enabled } = options
169
+ if (!enabled) return { kind: "skipped", reason: "codegraph indexing is turned off (set codegraph=true)" }
170
+
171
+ const runner = options.runner ?? realRunner
172
+ const clock = options.clock ?? realClock
173
+
174
+ // A home directory or a drive root is reported rather than indexed. codegraph
175
+ // has `-f` for exactly this case; passing it here would index the user's whole
176
+ // machine because a tool call said so.
177
+ const scope = assessScope(root)
178
+ if (scope.level === "warn") {
179
+ return { kind: "skipped", reason: `${root} is ${scope.reason}; codegraph indexing was not started` }
180
+ }
181
+
182
+ const last = lastLaunch.get(root)
183
+ const now = clock.now()
184
+ if (last !== undefined && now - last < MIN_LAUNCH_INTERVAL_MS) {
185
+ return { kind: "skipped", reason: "codegraph was launched for this workspace moments ago" }
186
+ }
187
+
188
+ // "Not installed" arrives as either a false or a throw: spawn() reports a
189
+ // missing binary as ENOENT on the error event. Both mean the same ordinary
190
+ // thing, so both are a skip. A throw here is not a failure of this plugin —
191
+ // it is the absence of an optional companion, which is the case the whole
192
+ // design has to get right.
193
+ let available: boolean
194
+ try {
195
+ available = await runner.available()
196
+ } catch {
197
+ available = false
198
+ }
199
+ if (!available) {
200
+ return { kind: "skipped", reason: "the codegraph CLI is not installed (npm i -g codegraph)" }
201
+ }
202
+
203
+ const initialized = isCodegraphInitialized(root)
204
+ const args = initialized ? ["sync"] : ["init", "--yes"]
205
+ const action = initialized ? "sync" : "init"
206
+
207
+ // Recorded before launching, so a failed launch still rate-limits: otherwise
208
+ // a broken CLI would be spawned again on every single build.
209
+ lastLaunch.set(root, now)
210
+
211
+ try {
212
+ await runner.launch(args, root)
213
+ } catch (error) {
214
+ return { kind: "failed", reason: `could not start \`codegraph ${args.join(" ")}\`: ${message(error)}` }
215
+ }
216
+
217
+ return {
218
+ kind: "launched",
219
+ launch: {
220
+ action,
221
+ command: `codegraph ${args.join(" ")}`,
222
+ detail: initialized
223
+ ? "syncing changes since the last index"
224
+ : "building the first index for this folder",
225
+ },
226
+ }
227
+ }
228
+
229
+ /** Test seam: forget the rate-limit state. */
230
+ export function resetCodegraphLaunchState(): void {
231
+ lastLaunch.clear()
232
+ }
233
+
234
+ /**
235
+ * One line for `indexing_status`, or "" when there is nothing worth saying.
236
+ *
237
+ * Deliberately does not run the CLI: status is called on every search via the
238
+ * footer, and spawning a process to decorate a status line is not a trade worth
239
+ * making. The directory's existence is the whole answer.
240
+ */
241
+ export function codegraphStatusLine(root: string): string {
242
+ try {
243
+ if (!fs.existsSync(root)) return ""
244
+ const initialized = isCodegraphInitialized(root)
245
+ return initialized
246
+ ? "codegraph: indexed (synced on the next indexing_build)"
247
+ : "codegraph: not indexed (initialised on the next indexing_build)"
248
+ } catch {
249
+ return ""
250
+ }
251
+ }
252
+
253
+ function message(error: unknown): string {
254
+ return error instanceof Error ? error.message : String(error)
255
+ }
package/src/config.ts CHANGED
@@ -38,6 +38,8 @@ export interface PluginOptionsInput {
38
38
  /** Master switch: when false the own index is neither written nor refreshed. */
39
39
  enabled?: boolean
40
40
  importFromKilo?: boolean
41
+ /** Also launch the codegraph CLI on build. Absent means on; see IndexingSettings. */
42
+ codegraph?: boolean
41
43
  /** Disable reading Kilo's config file entirely. */
42
44
  ignoreKiloConfig?: boolean
43
45
  homeDir?: string
@@ -283,6 +285,11 @@ export function resolveSettings(options: PluginOptionsInput): IndexingSettings {
283
285
  // but nothing is written and nothing is auto-refreshed. Toggled from the TUI
284
286
  // footer indicator or `/indexing-config set enabled false`.
285
287
  const enabled = options.enabled ?? file?.enabled !== false
288
+ // Companion indexer. On by default, and a no-op when codegraph is not
289
+ // installed, so enabling it costs nothing for someone who does not use it.
290
+ // `!== false` rather than `=== true`: the key is absent from most configs and
291
+ // absent has to mean "on" or the default would never apply.
292
+ const codegraph = options.codegraph ?? file?.codegraph !== false
286
293
 
287
294
  return {
288
295
  provider,
@@ -306,6 +313,7 @@ export function resolveSettings(options: PluginOptionsInput): IndexingSettings {
306
313
  autoRefresh,
307
314
  importFromKilo,
308
315
  enabled,
316
+ codegraph,
309
317
  credentials,
310
318
  warnings,
311
319
  }
package/src/scanner.ts CHANGED
@@ -59,8 +59,14 @@ export const DEFAULT_IGNORE_GLOBS: readonly string[] = [
59
59
 
60
60
  const DEFAULT_MAX_FILE_SIZE_BYTES = 1024 * 1024
61
61
 
62
- /** Dot-directories that are still walked because they hold indexable config. */
63
- const ALLOWED_DOT_DIRS: ReadonlySet<string> = new Set([
62
+ /**
63
+ * Dot-directories that are still walked because they hold indexable config.
64
+ *
65
+ * Exported for the test that pins `.codegraph` out of it: codegraph's SQLite
66
+ * database sits in that directory, and letting the scanner walk it would embed
67
+ * another indexer's storage into this one.
68
+ */
69
+ export const ALLOWED_DOT_DIRS: ReadonlySet<string> = new Set([
64
70
  ".github",
65
71
  ".vscode",
66
72
  ".gitlab",
@@ -265,6 +265,14 @@ const KEY_SPECS: Record<string, KeySpec> = {
265
265
  toPatch: (value) => ({ enabled: Boolean(value) }),
266
266
  display: (settings) => (settings.enabled === false ? "false" : "true"),
267
267
  },
268
+ codegraph: {
269
+ group: "behavior",
270
+ kind: "boolean",
271
+ description: "Also run the codegraph CLI on build (companion code graph; no-op when codegraph is not installed)",
272
+ toPatch: (value) => ({ codegraph: Boolean(value) }),
273
+ // `!== false`, matching how the value resolves: absent means on.
274
+ display: (settings) => (settings.codegraph === false ? "false" : "true"),
275
+ },
268
276
  searchmaxresults: {
269
277
  group: "search",
270
278
  kind: "integer",
package/src/settings.ts CHANGED
@@ -39,6 +39,8 @@ export interface SettingsFile {
39
39
  autoRefresh?: boolean
40
40
  /** Master switch: when false the own index is neither written nor refreshed. */
41
41
  enabled?: boolean
42
+ /** Also launch the codegraph CLI on build. Absent means on; a no-op if not installed. */
43
+ codegraph?: boolean
42
44
  searchMinScore?: number | null
43
45
  searchMaxResults?: number
44
46
  embeddingBatchSize?: number
package/src/tools.ts CHANGED
@@ -5,6 +5,7 @@ import { indexStatusLine } from "./index-state.ts"
5
5
  import { discoverKiloSources, importFromKilo, type KiloSourceInfo } from "./import.ts"
6
6
  import { createOwnStore, createSettingsQdrant } from "./store-factory.ts"
7
7
  import { assessScope } from "./scope.ts"
8
+ import { codegraphStatusLine, indexWithCodegraph, type CodegraphOutcome } from "./codegraph.ts"
8
9
  import type { IndexingSettings, KVStore } from "./types.ts"
9
10
  import type { Qdrant } from "./qdrant.ts"
10
11
 
@@ -95,6 +96,11 @@ Behavior:
95
96
  - \`rebuild: true\` drops the own index first and re-imports/re-indexes everything.
96
97
  - \`skipImport: true\` forces indexing from scratch even when a Kilo index exists.
97
98
 
99
+ If the codegraph CLI is installed, it is also started for this folder in the
100
+ background (\`init\` the first time, \`sync\` afterwards) so the companion code
101
+ graph stays in step. It never blocks this tool and never fails it: without
102
+ codegraph this build is identical.
103
+
98
104
  This can take a while for large workspaces and may consume embedding API credits (except when importing).`
99
105
 
100
106
  const IMPORT_DESCRIPTION = `Import an existing Kilo Code index into the plugin's own index WITHOUT re-embedding.
@@ -103,7 +109,9 @@ Vectors and chunks are copied as-is (zero embedding API cost), which requires a
103
109
  - Kilo's Qdrant collection (\`ws-*\`).
104
110
  - Kilo's local LanceDB database for this workspace, when present.
105
111
 
106
- Use indexing_status to list detected sources. If profiles do not match, rebuild with the matching model instead.`
112
+ Use indexing_status to list detected sources. If profiles do not match, rebuild with the matching model instead.
113
+
114
+ If the codegraph CLI is installed, it is also started for this folder in the background, so the companion code graph covers the files this import brought in. Without codegraph this import is identical.`
107
115
 
108
116
  const jsonSchema = (properties: Record<string, unknown>, required: string[] = []): Record<string, unknown> => ({
109
117
  type: "object",
@@ -242,6 +250,13 @@ export function createTools(deps: ToolDeps): ToolDefinition[] {
242
250
  `Freshness: ${status.freshness.total} files scanned, ${status.freshness.added} new, ${status.freshness.maybeStale} possibly changed, ${status.freshness.deleted} deleted.`,
243
251
  )
244
252
  }
253
+ // Only when the companion indexer is actually in play: saying
254
+ // "codegraph: not indexed" to someone who never asked for codegraph is
255
+ // noise. Read from the filesystem, so this costs no process.
256
+ if (settings.codegraph !== false) {
257
+ const codegraph = codegraphStatusLine(root)
258
+ if (codegraph) lines.push(codegraph)
259
+ }
245
260
  lines.push(`Recommendation: ${status.recommendation}`)
246
261
  if (status.warnings.length > 0) lines.push(`Warnings: ${status.warnings.join("; ")}`)
247
262
  return { output: lines.join("\n"), metadata: status as unknown as Record<string, unknown> }
@@ -298,9 +313,9 @@ export function createTools(deps: ToolDeps): ToolDefinition[] {
298
313
  { mode: "build", rebuild: input.rebuild === true, skipImport: input.skipImport === true },
299
314
  )
300
315
  const summary = formatReport("build", report)
301
- const warning = scopeWarning(root)
316
+ const lines = [scopeWarning(root), summary, await codegraphLine(root, settings)].filter(Boolean)
302
317
  return {
303
- output: warning ? `${warning}\n${summary}` : summary,
318
+ output: lines.join("\n"),
304
319
  metadata: report as unknown as Record<string, unknown>,
305
320
  }
306
321
  },
@@ -368,14 +383,38 @@ export function createTools(deps: ToolDeps): ToolDefinition[] {
368
383
  const lines = [
369
384
  `Imported ${report.imported} chunks from ${report.source.kind} ${report.source.name} in ${(report.durationMs / 1000).toFixed(1)}s.`,
370
385
  `Batches: ${report.batches}, skipped: ${report.skipped}. Embedding API calls: 0.`,
386
+ await codegraphLine(root, settings),
371
387
  ]
372
388
  if (report.errors.length > 0) lines.push(`Errors: ${report.errors.join("; ")}`)
373
- return { output: lines.join("\n"), metadata: report as unknown as Record<string, unknown> }
389
+ return { output: lines.filter(Boolean).join("\n"), metadata: report as unknown as Record<string, unknown> }
374
390
  },
375
391
  },
376
392
  ]
377
393
  }
378
394
 
395
+ /**
396
+ * One line about the companion indexer, for the write tools.
397
+ *
398
+ * Never throws and never reports success: the launch is detached, so by the time
399
+ * this returns the CLI has only started. Anything that went wrong afterwards
400
+ * lands in `.codegraph/daemon.log`, which is where the message points.
401
+ */
402
+ async function codegraphLine(root: string, settings: IndexingSettings): Promise<string> {
403
+ let outcome: CodegraphOutcome
404
+ try {
405
+ outcome = await indexWithCodegraph({ root, enabled: settings.codegraph !== false })
406
+ } catch (error) {
407
+ return `Codegraph: not indexed (${error instanceof Error ? error.message : String(error)})`
408
+ }
409
+ if (outcome.kind === "launched") {
410
+ return `Codegraph: \`${outcome.launch.command}\` started in the background — ${outcome.launch.detail}.`
411
+ }
412
+ // A skip while the CLI is absent is the common case and not worth a line on
413
+ // every build; the other two say something the user may want to act on.
414
+ if (outcome.reason.startsWith("the codegraph CLI is not installed")) return ""
415
+ return `Codegraph: not indexed (${outcome.reason})`
416
+ }
417
+
379
418
  function makeProgress(context: { progress?: (m: unknown) => Promise<void> }): (update: {
380
419
  phase: string
381
420
  processed: number
package/src/types.ts CHANGED
@@ -96,6 +96,16 @@ export interface IndexingSettings extends EmbeddingProfile {
96
96
  importFromKilo: boolean
97
97
  /** Master switch: when false nothing is written to nor refreshed in the own index. */
98
98
  enabled: boolean
99
+ /**
100
+ * Also launch the codegraph CLI when the workspace is indexed.
101
+ *
102
+ * Optional so that every fixture, every caller and every consumer that builds
103
+ * settings by hand keeps compiling: a companion that must be threaded through
104
+ * eight test doubles to exist is a dependency, not a companion. Callers read
105
+ * it as `settings.codegraph !== false`, which treats absent as "try it" —
106
+ * and "try it" is a no-op when the CLI is not installed.
107
+ */
108
+ codegraph?: boolean
99
109
  credentials: ProviderCredentials
100
110
  /** Warnings produced while resolving configuration. */
101
111
  warnings: string[]
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
8
+ "types": ["node"],
9
+ "jsx": "react-jsx",
10
+ "jsxImportSource": "@opentui/solid",
11
+ "strict": true,
12
+ "noEmit": true,
13
+ "skipLibCheck": true,
14
+ "esModuleInterop": true,
15
+ "allowSyntheticDefaultImports": true,
16
+ "forceConsistentCasingInFileNames": true,
17
+ "resolveJsonModule": true,
18
+ "isolatedModules": true,
19
+ "verbatimModuleSyntax": true
20
+ },
21
+ "include": ["index.ts", "tui.ts", "tui/**/*.ts", "tui/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
22
+ }