@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 +78 -0
- package/README.md +21 -0
- package/package.json +7 -18
- package/src/codegraph.ts +255 -0
- package/src/config.ts +8 -0
- package/src/scanner.ts +8 -2
- package/src/settings-commands.ts +8 -0
- package/src/settings.ts +2 -0
- package/src/tools.ts +43 -4
- package/src/types.ts +10 -0
- package/tsconfig.json +22 -0
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.
|
|
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
|
}
|
package/src/codegraph.ts
ADDED
|
@@ -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
|
-
/**
|
|
63
|
-
|
|
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",
|
package/src/settings-commands.ts
CHANGED
|
@@ -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
|
|
316
|
+
const lines = [scopeWarning(root), summary, await codegraphLine(root, settings)].filter(Boolean)
|
|
302
317
|
return {
|
|
303
|
-
output:
|
|
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
|
+
}
|