@meyverick/omnicode 0.1.2 → 0.2.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/README.md CHANGED
@@ -1,49 +1,31 @@
1
1
  # omnicode
2
2
 
3
- The cross-platform command-line entrypoint for running OpenCode through OmniRoute.
3
+ The cross-platform command-line entrypoint for running OpenCode through OmniRoute, backed by AI-powered reference indexing with Qdrant.
4
4
 
5
5
  ## What is omnicode?
6
6
 
7
- `omnicode` is a thin wrapper that launches OpenCode through OmniRoute. It expects you to install the underlying tools yourself, then handles optional GrayMatter and OpenSpec initialization, background OmniRoute lifecycle, and project-local OpenCode sessions. Developed and tested on Ubuntu Linux; cross-platform by design but untested on Windows, macOS, and other Linux distributions.
7
+ `omnicode` is a wrapper that launches OpenCode through OmniRoute with zero friction. It handles GrayMatter memory, OpenSpec change tracking, and OmniRoute lifecycle automatically. On top of that, it indexes your `./references/` directory into a Qdrant vector database unlocking semantic search for AI agents working in your codebase.
8
8
 
9
- ## Why
10
-
11
- Because I'm lazy. Not "I'll automate this repetitive task" lazy, but "I keep opening a second terminal just to run `omniroute`, then manually init GrayMatter, then manually init OpenSpec, then forget which session ID I was using" lazy.
9
+ Developed and tested on Ubuntu Linux; cross-platform by design (Node.js, no bash).
12
10
 
13
- Yes, I know OmniRoute is designed to run on a server. But I don't have a server at home, and I still want to save tokens by routing requests through it locally. Bonus: I get to use a bunch of free tiers from different providers without having to remember which API key goes where.
11
+ ## Why
14
12
 
15
- So I wrote this wrapper. It starts OmniRoute, inits GrayMatter and OpenSpec quietly in the background, remembers my session per project, and shuts everything down when I'm done. I run one command and pretend I'm a responsible developer with proper infrastructure.
13
+ Because I kept opening a second terminal to run `omniroute`, manually init GrayMatter, manually init OpenSpec, and forget which session ID I was using. I wrote a wrapper so I can run one command and pretend I'm a responsible developer with proper infrastructure.
16
14
 
17
15
  ## Features
18
16
 
19
- - Thin npm global command written in Node.js (no bash needed).
20
- - Cross-platform by design: developed and tested on Ubuntu Linux; untested on Windows, macOS, and other Linux distributions.
21
- - Automatic session resume per project using the OpenCode database.
22
- - Background OmniRoute lifecycle with cleanup when OpenCode exits.
23
- - Quiet GrayMatter and OpenSpec initialization with captured logs.
24
- - Check runtime status with `omnicode --status`.
25
- - Print version with `omnicode --version`.
26
-
27
- ## Requirements
28
-
29
- - Node.js 22 or later (developed and tested on Node 26; OmniRoute requires Node >=22 <=24).
30
-
31
- ## Explore
32
-
33
- - [Getting Started](https://github.com/meyverick/omnicode/wiki/Getting-Started) - install, run, and uninstall.
34
- - [How it works](https://github.com/meyverick/omnicode/wiki/How-it-works) - runtime flow, session handling, and lifecycle.
35
- - [Configuration](https://github.com/meyverick/omnicode/wiki/Configuration) - paths and environment overrides.
36
- - [Troubleshooting](https://github.com/meyverick/omnicode/wiki/Troubleshooting) - common issues and rollback.
37
-
38
- ## Acknowledgments
39
-
40
- Built on top of:
17
+ - **Single command** launches OmniRoute + OpenCode + optional tools in one shot.
18
+ - **Automatic session resume** remembers your session per project via the OpenCode database.
19
+ - **Background OmniRoute lifecycle** starts with OpenCode, stops when no sessions remain.
20
+ - **Quiet initialization** GrayMatter and OpenSpec init run in the background with captured logs.
21
+ - **Semantic reference indexing** indexes `./references/` into Qdrant with BGE embeddings for AI-agent search.
22
+ - **Cross-platform runtime** pure Node.js, no bash dependency.
41
23
 
42
- - [OpenCode](https://github.com/opencode-ai/opencode) - AI-powered coding assistant
43
- - [OmniRoute](https://github.com/diegosouzapw/OmniRoute) - model routing layer
44
- - [OpenSpec](https://github.com/Fission-AI/OpenSpec) - AI agent workflow framework
45
- - [GrayMatter](https://github.com/angelnicolasc/graymatter) - persistent memory for AI agents
24
+ ## Quick start
46
25
 
47
- ## License
26
+ ```bash
27
+ npm install -g @meyverick/omnicode
28
+ omnicode
29
+ ```
48
30
 
49
- [Apache 2.0](LICENSE)
31
+ See the [wiki](https://github.com/meyverick/omnicode/wiki) for detailed setup, configuration, and troubleshooting.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meyverick/omnicode",
3
- "version": "0.1.2",
4
- "description": "Cross-platform command-line entrypoint for OpenCode through OmniRoute",
3
+ "version": "0.2.0",
4
+ "description": "Cross-platform CLI for OpenCode through OmniRoute with Qdrant semantic reference indexing",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "omnicode": "src/bin/omnicode.js"
@@ -22,5 +22,8 @@
22
22
  "cli"
23
23
  ],
24
24
  "author": "meyverick",
25
- "license": "Apache-2.0"
25
+ "license": "Apache-2.0",
26
+ "dependencies": {
27
+ "web-tree-sitter": "0.20.8"
28
+ }
26
29
  }
@@ -1,8 +1,12 @@
1
1
  import { spawn } from "node:child_process";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, writeFileSync, writeSync, unlinkSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
 
5
- import { commandExists, getDataDir, isPidAlive, isProcessRunningAsync } from "../installer/lib.js";
5
+ import { commandExists, getDataDir, isPidAlive, isProcessRunningAsync, detectQdrantMcp, generateQdrantConfig, ensureOpencodeConfig, ensureQdrantAgentInstructions, indexReferences, startMcpServer, stopMcpServer, getQdrantStoreEnv, getQdrantPidFile, isQdrantRunning, startQdrantContainer, stopQdrantContainer } from "../installer/lib.js";
6
+
7
+ process.on("unhandledRejection", (err) => {
8
+ console.error("[omnicode] runtime: UNHANDLED REJECTION:", err?.message || err);
9
+ });
6
10
 
7
11
  const MAX_OMNI_WAIT = 30;
8
12
  const OMNI_CHECK_DELAY = 1000;
@@ -21,15 +25,17 @@ async function initTool(name, args, logPath) {
21
25
  try {
22
26
  log = openSync(logPath, "w");
23
27
  const child = spawn(name, args, { stdio: ["ignore", log, log] });
28
+ let timer;
24
29
  const result = await Promise.race([
25
30
  new Promise((resolve) => {
26
31
  child.on("close", resolve);
27
32
  child.on("error", () => resolve(null));
28
33
  }),
29
34
  new Promise((resolve) => {
30
- setTimeout(() => { child.kill(); resolve(null); }, 30000);
35
+ timer = setTimeout(() => { child.kill(); resolve(null); }, 30000);
31
36
  }),
32
37
  ]);
38
+ clearTimeout(timer);
33
39
  if (result === null) {
34
40
  console.log(`[omnicode] WARNING: ${name} init timed out; continuing. Log: ${logPath}`);
35
41
  } else if (result === 0) {
@@ -109,6 +115,8 @@ async function stopOmnirouteIfIdle(pidFile) {
109
115
  return;
110
116
  }
111
117
 
118
+ stopQdrantContainer();
119
+
112
120
  if (existsSync(pidFile)) {
113
121
  let pid = null;
114
122
  try {
@@ -141,8 +149,13 @@ export async function runRuntime(mode) {
141
149
  process.umask(0o077);
142
150
  }
143
151
 
144
- const cleanup = async () => { await stopOmnirouteIfIdle(pidFile); };
152
+ let startedIndexing = false;
153
+ const cleanup = async () => {
154
+ if (startedIndexing) try { unlinkSync(join(process.cwd(), ".qdrant", ".indexing")); } catch {}
155
+ await stopOmnirouteIfIdle(pidFile);
156
+ };
145
157
  process.on("exit", () => {
158
+ if (startedIndexing) try { unlinkSync(join(process.cwd(), ".qdrant", ".indexing")); } catch {}
146
159
  try {
147
160
  if (!existsSync(pidFile)) return;
148
161
  const pid = parseInt(readFileSync(pidFile, "utf8").trim(), 10);
@@ -159,26 +172,61 @@ export async function runRuntime(mode) {
159
172
 
160
173
  await Promise.all([
161
174
  pid !== null ? waitForOmniroute(pid, logFile) : Promise.resolve(),
175
+ startQdrantContainer(),
162
176
  initTools(dataDir),
163
177
  ]);
164
178
 
165
- if (mode.flag === "-s" && mode.id) {
166
- console.log(`[omnicode] launching opencode (session: ${mode.id})`);
167
- await new Promise((resolve) => {
168
- const child = spawn("opencode", ["-s", mode.id], {
169
- stdio: "inherit",
170
- cwd: process.cwd(),
171
- });
172
- child.on("close", () => resolve());
173
- });
174
- } else {
175
- console.log("[omnicode] launching opencode (new session)");
176
- await new Promise((resolve) => {
177
- const child = spawn("opencode", [], {
178
- stdio: "inherit",
179
- cwd: process.cwd(),
179
+ const hasQdrant = detectQdrantMcp();
180
+ const refsDir = join(process.cwd(), "references");
181
+ let qdrantConfig = null;
182
+
183
+ if (hasQdrant) {
184
+ qdrantConfig = generateQdrantConfig();
185
+ ensureOpencodeConfig(qdrantConfig);
186
+ ensureQdrantAgentInstructions();
187
+ console.log("[omnicode] qdrant MCP configured");
188
+ }
189
+
190
+ const ac = new AbortController();
191
+ if (hasQdrant && existsSync(refsDir) && qdrantConfig) {
192
+ if (existsSync(join(process.cwd(), ".qdrant", ".indexing"))) {
193
+ console.log("[omnicode] background indexing is already running, skipping");
194
+ } else {
195
+ startedIndexing = true;
196
+ console.log("[omnicode] background indexing started");
197
+ indexReferences(refsDir, qdrantConfig, null, false, ac.signal).catch((err) => {
198
+ console.error(`[omnicode] background index failed: ${err.message}`);
180
199
  });
181
- child.on("close", () => resolve());
182
- });
200
+ }
183
201
  }
202
+
203
+ const launchOpencode = () => new Promise((resolve) => {
204
+ const args = mode.flag === "-s" && mode.id ? ["-s", mode.id] : [];
205
+ if (mode.flag === "-s" && mode.id) {
206
+ console.log(`[omnicode] launching opencode (session: ${mode.id})`);
207
+ } else {
208
+ console.log("[omnicode] launching opencode (new session)");
209
+ }
210
+ const child = spawn("opencode", args, { stdio: "inherit", cwd: process.cwd() });
211
+ const logFd = openSync(join(dataDir, "background.log"), "w");
212
+ const origLog = console.log;
213
+ const origWarn = console.warn;
214
+ const origError = console.error;
215
+ console.log = (...args) => writeSync(logFd, args.join(" ") + "\n");
216
+ console.warn = (...args) => writeSync(logFd, "WARN: " + args.join(" ") + "\n");
217
+ console.error = (...args) => writeSync(logFd, "ERROR: " + args.join(" ") + "\n");
218
+ child.on("close", () => {
219
+ setTimeout(() => {
220
+ console.log = origLog;
221
+ console.warn = origWarn;
222
+ console.error = origError;
223
+ try { closeSync(logFd); } catch {}
224
+ resolve();
225
+ }, 100);
226
+ });
227
+ });
228
+
229
+ await launchOpencode();
230
+ ac.abort();
231
+ await cleanup();
184
232
  }
@@ -1,9 +1,15 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, realpathSync } from "node:fs";
2
+ import { readFileSync, realpathSync, existsSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
- import { commandExists, getOpencodeDbPath, isProcessRunning } from "../installer/lib.js";
6
+ process.on("unhandledRejection", (err) => {
7
+ console.error("[omnicode] UNHANDLED REJECTION:", err?.message || err);
8
+ process.exit(1);
9
+ });
10
+
11
+ import { commandExists, getOpencodeDbPath, isProcessRunning, detectQdrantMcp, generateQdrantConfig, ensureOpencodeConfig, indexReferences, isQdrantRunning, countProcesses, getQdrantRunningCount, countActiveIndexers } from "../installer/lib.js";
12
+ import { downloadLanguageCmd } from "../installer/tree-sitter.js";
7
13
  import { runRuntime } from "./omnicode-runtime.js";
8
14
 
9
15
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
@@ -19,7 +25,7 @@ try {
19
25
  }
20
26
 
21
27
  export function printUsage() {
22
- console.log(`Usage: omnicode [-s <session_id>] [-c] [--status] [--version]`);
28
+ console.log(`Usage: omnicode [-s <session_id>] [-c] [--status] [--version] [index]`);
23
29
  }
24
30
 
25
31
  let _cachedVersion = null;
@@ -32,20 +38,28 @@ export function getVersion() {
32
38
  }
33
39
 
34
40
  export function getProcessStatus() {
41
+ const indexingLockPath = join(process.cwd(), ".qdrant", ".indexing");
35
42
  return {
36
- opencode: isProcessRunning("opencode"),
37
- omniroute: isProcessRunning("omniroute"),
43
+ opencode: countProcesses("opencode"),
44
+ omniroute: countProcesses("omniroute"),
45
+ qdrant: getQdrantRunningCount(),
46
+ indexing: existsSync(indexingLockPath),
47
+ indexingCount: countActiveIndexers(),
38
48
  };
39
49
  }
40
50
 
41
51
  export function printStatus(status = getProcessStatus()) {
42
- console.log(`[omnicode] opencode: ${status.opencode ? "running" : "stopped"}`);
43
- console.log(`[omnicode] omniroute: ${status.omniroute ? "running" : "stopped"}`);
52
+ console.log(`[omnicode] opencode: ${status.opencode > 0 ? `running (${status.opencode})` : "stopped"}`);
53
+ console.log(`[omnicode] omniroute: ${status.omniroute > 0 ? `running (${status.omniroute})` : "stopped"}`);
54
+ console.log(`[omnicode] qdrant: ${status.qdrant > 0 ? `running (${status.qdrant})` : "stopped"}`);
55
+ console.log(`[omnicode] indexing: ${status.indexing ? `true (${status.indexingCount})` : "false"}`);
44
56
  }
45
57
 
46
58
  export function parseArgs(argv) {
47
59
  let sessionId = null;
48
60
  let continueSession = false;
61
+ let index = false;
62
+ let forceReindex = false;
49
63
  for (let i = 2; i < argv.length; i++) {
50
64
  const arg = argv[i];
51
65
  if (arg === "-h" || arg === "--help") {
@@ -64,6 +78,22 @@ export function parseArgs(argv) {
64
78
  continueSession = true;
65
79
  continue;
66
80
  }
81
+ if (arg === "--force-reindex") {
82
+ forceReindex = true;
83
+ continue;
84
+ }
85
+ if (arg === "--index" || arg === "index") {
86
+ index = true;
87
+ continue;
88
+ }
89
+ if (arg === "download-language") {
90
+ const language = argv[++i];
91
+ if (!language) {
92
+ console.error("[omnicode] ERROR: download-language requires a language argument");
93
+ process.exit(2);
94
+ }
95
+ return { downloadLanguage: language };
96
+ }
67
97
  if (arg === "-s") {
68
98
  sessionId = argv[++i];
69
99
  if (!sessionId) {
@@ -83,7 +113,7 @@ export function parseArgs(argv) {
83
113
  process.exit(2);
84
114
  }
85
115
  }
86
- return { sessionId, continueSession };
116
+ return { sessionId, continueSession, index, forceReindex };
87
117
  }
88
118
 
89
119
  export async function getLatestSessionId(directory = realpathSync(process.cwd())) {
@@ -113,6 +143,27 @@ export async function resolveSessionMode(sessionId, latestSessionId = null) {
113
143
  async function main() {
114
144
  const args = parseArgs(process.argv);
115
145
 
146
+ if (args.downloadLanguage) {
147
+ const success = await downloadLanguageCmd(args.downloadLanguage);
148
+ process.exit(success ? 0 : 1);
149
+ }
150
+
151
+ if (args.index) {
152
+ if (!detectQdrantMcp()) {
153
+ console.error("[omnicode] ERROR: uvx mcp-server-qdrant not found. Install uvx first.");
154
+ process.exit(1);
155
+ }
156
+ if (isQdrantRunning()) {
157
+ console.error("[omnicode] ERROR: a qdrant MCP server is already running for this project.");
158
+ console.error("[omnicode] Close the opencode session first, or wait for indexing to finish.");
159
+ process.exit(1);
160
+ }
161
+ const qdrantConfig = generateQdrantConfig();
162
+ ensureOpencodeConfig(qdrantConfig);
163
+ await indexReferences(join(process.cwd(), "references"), qdrantConfig, null, args.forceReindex);
164
+ process.exit(0);
165
+ }
166
+
116
167
  const missing = ["opencode", "omniroute"].filter((cmd) => !commandExists(cmd));
117
168
  if (missing.length > 0) {
118
169
  console.error(`[omnicode] ERROR: missing required tool(s): ${missing.join(", ")}`);
@@ -0,0 +1,13 @@
1
+ <!-- qdrant:instructions:begin — managed by `omnicode`; edits inside this block are overwritten -->
2
+ ## Qdrant MCP (Semantic Search)
3
+
4
+ Qdrant MCP [configured] -> execute semantic search over `./references/`.
5
+ Index [maintained] via omnicode. Storage [isolated] -> `./.qdrant/` (local, zero external servers).
6
+ Model -> `BAAI/bge-small-en-v1.5` (384 dimensions).
7
+
8
+ - `qdrant-store(information, metadata?, collection_name?)` -> embed && store text chunk. `information` -> target text. Default collection -> `references`.
9
+ - `qdrant-find(query, collection_name?)` -> execute semantic search via natural-language `query` -> retrieve optimal matches.
10
+
11
+ **Execution Trigger [CRITICAL]:**
12
+ Execute `qdrant-find` prior to codebase mutations -> retrieve reference context, architectural patterns, && documentation -> prevent hallucination.
13
+ <!-- qdrant:instructions:end -->