mikoshi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/package.json +23 -0
- package/src/mikoshi/auth.js +199 -0
- package/src/mikoshi/chunking.js +31 -0
- package/src/mikoshi/cli.js +274 -0
- package/src/mikoshi/config.js +58 -0
- package/src/mikoshi/entitlements.js +6 -0
- package/src/mikoshi/hashing.js +9 -0
- package/src/mikoshi/ignore.js +90 -0
- package/src/mikoshi/indexing/file_scanner.js +39 -0
- package/src/mikoshi/indexing/index_store.js +82 -0
- package/src/mikoshi/indexing/indexer.js +198 -0
- package/src/mikoshi/mcp_server/server.js +121 -0
- package/src/mikoshi/retrieval/hybrid.js +85 -0
- package/src/mikoshi/retrieval/lexical.js +53 -0
- package/src/mikoshi/retrieval/rerank.js +3 -0
- package/src/mikoshi/retrieval/semantic.js +210 -0
- package/src/mikoshi/utils/timer.js +9 -0
- package/src/mikoshi/utils/types.js +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
```bash
|
|
2
|
+
npm install -g mikoshi
|
|
3
|
+
```
|
|
4
|
+
|
|
5
|
+
✨ Mikoshi — private local code search + MCP
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
```bash
|
|
9
|
+
npm install -g mikoshi
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Use
|
|
13
|
+
```bash
|
|
14
|
+
mikoshi index ~/project
|
|
15
|
+
mikoshi search ~/project "query"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
MCP
|
|
19
|
+
```bash
|
|
20
|
+
mikoshi-mcp
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Codex MCP config (config.toml)
|
|
24
|
+
```toml
|
|
25
|
+
[mcp_servers.mikoshi]
|
|
26
|
+
command = "mikoshi-mcp"
|
|
27
|
+
args = []
|
|
28
|
+
enabled = true
|
|
29
|
+
|
|
30
|
+
[projects."~/project"]
|
|
31
|
+
trust_level = "trusted"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Developer
|
|
35
|
+
```bash
|
|
36
|
+
npm install
|
|
37
|
+
npm test
|
|
38
|
+
mikoshi doctor
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Note: First run may download the local embedding model once.
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mikoshi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Private local code search + MCP",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mikoshi": "src/mikoshi/cli.js",
|
|
8
|
+
"mikoshi-mcp": "src/mikoshi/mcp_server/server.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/mikoshi/**",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test",
|
|
16
|
+
"lint": "node --check ./src/mikoshi/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {},
|
|
19
|
+
"optionalDependencies": {
|
|
20
|
+
"@xenova/transformers": "^2.17.2",
|
|
21
|
+
"onnxruntime-node": "^1.20.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
import readline from "node:readline";
|
|
6
|
+
import { DEFAULT_FEATURES, DEFAULT_PLAN } from "./entitlements.js";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_API_BASE_URL = "https://neet.gg";
|
|
9
|
+
|
|
10
|
+
const AUTH_FILENAME = "auth.json";
|
|
11
|
+
const CONFIG_FILENAME = "config.json";
|
|
12
|
+
|
|
13
|
+
export class AuthError extends Error {}
|
|
14
|
+
|
|
15
|
+
function indexRoot() {
|
|
16
|
+
const root = process.env.MIKOSHI_INDEX_ROOT || "~/.mikoshi";
|
|
17
|
+
return path.resolve(root.replace(/^~\//, `${os.homedir()}/`));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function authPath() {
|
|
21
|
+
return path.join(indexRoot(), AUTH_FILENAME);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function configPath() {
|
|
25
|
+
return path.join(indexRoot(), CONFIG_FILENAME);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loadBrokerConfig() {
|
|
29
|
+
const cfgPath = configPath();
|
|
30
|
+
if (!fs.existsSync(cfgPath)) {
|
|
31
|
+
return { api_base_url: DEFAULT_API_BASE_URL };
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
35
|
+
const apiBase = String(data.api_base_url || "").trim().replace(/\/+$/, "");
|
|
36
|
+
return { api_base_url: apiBase || DEFAULT_API_BASE_URL };
|
|
37
|
+
} catch {
|
|
38
|
+
return { api_base_url: DEFAULT_API_BASE_URL };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function saveBrokerConfig(apiBaseUrl) {
|
|
43
|
+
const cleanUrl = String(apiBaseUrl || "").trim().replace(/\/+$/, "") || DEFAULT_API_BASE_URL;
|
|
44
|
+
const cfgPath = configPath();
|
|
45
|
+
fs.mkdirSync(path.dirname(cfgPath), { recursive: true });
|
|
46
|
+
const payload = JSON.stringify({ api_base_url: cleanUrl }, null, 2);
|
|
47
|
+
const tmp = cfgPath + ".tmp";
|
|
48
|
+
fs.writeFileSync(tmp, payload, { mode: 0o600 });
|
|
49
|
+
fs.renameSync(tmp, cfgPath);
|
|
50
|
+
fs.chmodSync(cfgPath, 0o600);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function loadAuthState() {
|
|
54
|
+
const p = authPath();
|
|
55
|
+
if (!fs.existsSync(p)) return null;
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
58
|
+
return {
|
|
59
|
+
access_token: String(data.access_token || ""),
|
|
60
|
+
expires_at: String(data.expires_at || ""),
|
|
61
|
+
plan: String(data.plan || DEFAULT_PLAN),
|
|
62
|
+
features: Array.isArray(data.features) ? data.features.map(String) : DEFAULT_FEATURES,
|
|
63
|
+
};
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function saveAuthState(state) {
|
|
70
|
+
const p = authPath();
|
|
71
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
72
|
+
const payload = JSON.stringify(
|
|
73
|
+
{
|
|
74
|
+
access_token: state.access_token,
|
|
75
|
+
expires_at: state.expires_at,
|
|
76
|
+
plan: state.plan || DEFAULT_PLAN,
|
|
77
|
+
features: state.features || DEFAULT_FEATURES,
|
|
78
|
+
},
|
|
79
|
+
null,
|
|
80
|
+
2
|
|
81
|
+
);
|
|
82
|
+
const tmp = p + ".tmp";
|
|
83
|
+
fs.writeFileSync(tmp, payload, { mode: 0o600 });
|
|
84
|
+
fs.renameSync(tmp, p);
|
|
85
|
+
fs.chmodSync(p, 0o600);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function clearAuthState() {
|
|
89
|
+
const p = authPath();
|
|
90
|
+
if (fs.existsSync(p)) fs.unlinkSync(p);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isExpired(state) {
|
|
94
|
+
if (!state?.expires_at) return true;
|
|
95
|
+
const value = state.expires_at.endsWith("Z")
|
|
96
|
+
? state.expires_at.slice(0, -1) + "+00:00"
|
|
97
|
+
: state.expires_at;
|
|
98
|
+
const parsed = Date.parse(value);
|
|
99
|
+
if (Number.isNaN(parsed)) return true;
|
|
100
|
+
return Date.now() >= parsed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function emailFromToken(token) {
|
|
104
|
+
try {
|
|
105
|
+
const parts = token.split(".");
|
|
106
|
+
if (parts.length < 2) return null;
|
|
107
|
+
const payload = parts[1] + "=".repeat((4 - (parts[1].length % 4)) % 4);
|
|
108
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
109
|
+
return decoded.email || decoded.user_email || decoded.preferred_username || null;
|
|
110
|
+
} catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function generateState() {
|
|
116
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function generateCodeVerifier() {
|
|
120
|
+
return crypto.randomBytes(64).toString("base64url");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function codeChallenge(verifier) {
|
|
124
|
+
return crypto.createHash("sha256").update(verifier).digest("base64url");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function loginUrl(apiBaseUrl, state, challenge) {
|
|
128
|
+
const params = new URLSearchParams({
|
|
129
|
+
response_type: "code",
|
|
130
|
+
client_id: "mikoshi",
|
|
131
|
+
code_challenge: challenge,
|
|
132
|
+
code_challenge_method: "S256",
|
|
133
|
+
state,
|
|
134
|
+
});
|
|
135
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}/login?${params.toString()}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function openUrl(url) {
|
|
139
|
+
const platform = process.platform;
|
|
140
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
|
|
141
|
+
const { spawn } = await import("node:child_process");
|
|
142
|
+
spawn(command, [url], { stdio: "ignore", detached: true }).unref();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function promptLine(prompt) {
|
|
146
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
147
|
+
const answer = await new Promise((resolve) => rl.question(prompt, resolve));
|
|
148
|
+
rl.close();
|
|
149
|
+
return String(answer || "");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parsePastePayload(text) {
|
|
153
|
+
let data;
|
|
154
|
+
try {
|
|
155
|
+
data = JSON.parse(text);
|
|
156
|
+
} catch (err) {
|
|
157
|
+
throw new AuthError("Invalid JSON response.");
|
|
158
|
+
}
|
|
159
|
+
if (!data || typeof data !== "object") throw new AuthError("Invalid JSON response.");
|
|
160
|
+
const code = String(data.code || "");
|
|
161
|
+
const state = String(data.state || "");
|
|
162
|
+
if (!code || !state) throw new AuthError("Missing code or state.");
|
|
163
|
+
return { code, state };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function login() {
|
|
167
|
+
const broker = loadBrokerConfig();
|
|
168
|
+
const state = generateState();
|
|
169
|
+
const verifier = generateCodeVerifier();
|
|
170
|
+
const challenge = codeChallenge(verifier);
|
|
171
|
+
const url = loginUrl(broker.api_base_url, state, challenge);
|
|
172
|
+
|
|
173
|
+
await openUrl(url);
|
|
174
|
+
|
|
175
|
+
const pasted = await promptLine("Paste the JSON response here: ");
|
|
176
|
+
const { code, state: returnedState } = parsePastePayload(pasted);
|
|
177
|
+
if (returnedState !== state) {
|
|
178
|
+
throw new AuthError("Invalid state.");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const response = await fetch(`${broker.api_base_url.replace(/\/+$/, "")}/cli/exchange`, {
|
|
182
|
+
method: "POST",
|
|
183
|
+
headers: { "Content-Type": "application/json" },
|
|
184
|
+
body: JSON.stringify({ code, code_verifier: verifier, state }),
|
|
185
|
+
});
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new AuthError("Login failed. Please try again.");
|
|
188
|
+
}
|
|
189
|
+
const payload = await response.json();
|
|
190
|
+
const accessToken = String(payload.access_token || "");
|
|
191
|
+
const expiresAt = String(payload.expires_at || "");
|
|
192
|
+
if (!accessToken || !expiresAt) throw new AuthError("Login failed. Please try again.");
|
|
193
|
+
|
|
194
|
+
const plan = String(payload.plan || DEFAULT_PLAN);
|
|
195
|
+
const features = Array.isArray(payload.features) ? payload.features.map(String) : DEFAULT_FEATURES;
|
|
196
|
+
saveAuthState({ access_token: accessToken, expires_at: expiresAt, plan, features });
|
|
197
|
+
|
|
198
|
+
return String(payload.email || emailFromToken(accessToken) || "unknown");
|
|
199
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {{start_line:number,end_line:number,text:string}} ChunkSpan
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Deterministic line-based chunking with overlap.
|
|
7
|
+
* @param {string} text
|
|
8
|
+
* @param {number} maxLines
|
|
9
|
+
* @param {number} overlap
|
|
10
|
+
* @returns {ChunkSpan[]}
|
|
11
|
+
*/
|
|
12
|
+
export function chunkText(text, maxLines, overlap) {
|
|
13
|
+
const lines = text.split(/\r?\n/);
|
|
14
|
+
if (maxLines <= 0) return [];
|
|
15
|
+
|
|
16
|
+
const spans = [];
|
|
17
|
+
let start = 0;
|
|
18
|
+
while (start < lines.length) {
|
|
19
|
+
const end = Math.min(start + maxLines, lines.length);
|
|
20
|
+
const slice = lines.slice(start, end);
|
|
21
|
+
const chunkText = slice.join("\n");
|
|
22
|
+
spans.push({
|
|
23
|
+
start_line: start + 1,
|
|
24
|
+
end_line: end,
|
|
25
|
+
text: chunkText,
|
|
26
|
+
});
|
|
27
|
+
if (end === lines.length) break;
|
|
28
|
+
start = Math.max(0, end - overlap);
|
|
29
|
+
}
|
|
30
|
+
return spans;
|
|
31
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { configureExternalLibs, ConfigError, loadConfig } from "./config.js";
|
|
5
|
+
import { indexRepo } from "./indexing/indexer.js";
|
|
6
|
+
import { IndexStore } from "./indexing/index_store.js";
|
|
7
|
+
import { searchRepo } from "./retrieval/hybrid.js";
|
|
8
|
+
import {
|
|
9
|
+
AuthError,
|
|
10
|
+
DEFAULT_API_BASE_URL,
|
|
11
|
+
clearAuthState,
|
|
12
|
+
emailFromToken,
|
|
13
|
+
isExpired,
|
|
14
|
+
loadAuthState,
|
|
15
|
+
login,
|
|
16
|
+
saveBrokerConfig,
|
|
17
|
+
} from "./auth.js";
|
|
18
|
+
|
|
19
|
+
function formatIndexRoot(rootPath) {
|
|
20
|
+
try {
|
|
21
|
+
const home = path.resolve(process.env.HOME || "");
|
|
22
|
+
const resolved = path.resolve(rootPath);
|
|
23
|
+
if (resolved === home) return "~";
|
|
24
|
+
if (resolved.startsWith(home + path.sep)) {
|
|
25
|
+
return resolved.replace(home, "~");
|
|
26
|
+
}
|
|
27
|
+
return resolved;
|
|
28
|
+
} catch {
|
|
29
|
+
return rootPath;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function cmdIndex(args) {
|
|
34
|
+
try {
|
|
35
|
+
const result = await indexRepo(args.path);
|
|
36
|
+
console.log(JSON.stringify({
|
|
37
|
+
repo_id: result.repo_id,
|
|
38
|
+
chunks_indexed: result.chunks_indexed,
|
|
39
|
+
took_ms: result.took_ms,
|
|
40
|
+
}, null, 2));
|
|
41
|
+
return 0;
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof ConfigError) {
|
|
44
|
+
console.error(`Config error: ${err.message}`);
|
|
45
|
+
return 2;
|
|
46
|
+
}
|
|
47
|
+
console.error(String(err.message || err));
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function cmdSearch(args) {
|
|
53
|
+
try {
|
|
54
|
+
const results = await searchRepo(args.path, args.query, args.k);
|
|
55
|
+
for (const result of results) {
|
|
56
|
+
console.log(`${result.relpath}:${result.start_line}-${result.end_line} (${result.score.toFixed(3)})`);
|
|
57
|
+
console.log(result.snippet);
|
|
58
|
+
console.log();
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
if (err instanceof ConfigError) {
|
|
63
|
+
console.error(`Config error: ${err.message}`);
|
|
64
|
+
return 2;
|
|
65
|
+
}
|
|
66
|
+
console.error(String(err.message || err));
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function cmdStatus(args) {
|
|
72
|
+
const config = loadConfig();
|
|
73
|
+
const repoRoot = path.resolve(args.path);
|
|
74
|
+
const store = new IndexStore(repoRoot, config.index_root);
|
|
75
|
+
const meta = store.loadMeta();
|
|
76
|
+
if (!meta) {
|
|
77
|
+
console.log(JSON.stringify({ indexed: false, chunks: 0, last_index_time: null, model: null }, null, 2));
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
console.log(JSON.stringify({
|
|
81
|
+
indexed: true,
|
|
82
|
+
chunks: meta.chunks,
|
|
83
|
+
last_index_time: meta.updated_at,
|
|
84
|
+
model: meta.model,
|
|
85
|
+
}, null, 2));
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cmdClear(args) {
|
|
90
|
+
const config = loadConfig();
|
|
91
|
+
const repoRoot = path.resolve(args.path);
|
|
92
|
+
const store = new IndexStore(repoRoot, config.index_root);
|
|
93
|
+
store.clear();
|
|
94
|
+
console.log(JSON.stringify({ ok: true }, null, 2));
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cmdDoctor(args) {
|
|
99
|
+
let exitCode = 0;
|
|
100
|
+
const version = process.versions.node.split(".").slice(0, 2).join(".");
|
|
101
|
+
const major = Number.parseInt(process.versions.node.split(".")[0], 10);
|
|
102
|
+
if (major >= 20) {
|
|
103
|
+
console.log(`✅ Node: ${version}.x`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`❌ Node: ${version}.x (requires 20+)`);
|
|
106
|
+
exitCode = 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let config;
|
|
110
|
+
try {
|
|
111
|
+
config = loadConfig();
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.log(`❌ Config: ${err.message}`);
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
console.log(`✅ Mikoshi index root: ${formatIndexRoot(config.index_root)}`);
|
|
118
|
+
console.log(`✅ Embeddings: provider=${config.embeddings.provider} model=${config.embeddings.model}`);
|
|
119
|
+
|
|
120
|
+
if (config.embeddings.provider === "local") {
|
|
121
|
+
const cacheDir = path.join(config.index_root, "models");
|
|
122
|
+
const cached = fs.existsSync(cacheDir) && fs.readdirSync(cacheDir).length > 0;
|
|
123
|
+
const offline = ["1", "true", "yes", "on"].includes((process.env.MIKOSHI_OFFLINE || "").toLowerCase());
|
|
124
|
+
if (!cached && offline) {
|
|
125
|
+
console.log("❌ Model cached: no (offline)");
|
|
126
|
+
exitCode = 1;
|
|
127
|
+
} else {
|
|
128
|
+
console.log(`✅ Model cached: ${cached ? "yes" : "no"}`);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
console.log("✅ Model cached: yes");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (args.path) {
|
|
135
|
+
const repoRoot = path.resolve(args.path);
|
|
136
|
+
const store = new IndexStore(repoRoot, config.index_root);
|
|
137
|
+
const meta = store.loadMeta();
|
|
138
|
+
if (meta) {
|
|
139
|
+
console.log(`✅ Repo indexed: yes (chunks=${meta.chunks}, last_index_time=${meta.updated_at})`);
|
|
140
|
+
} else {
|
|
141
|
+
console.log("✅ Repo indexed: no");
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return exitCode;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function cmdLogin() {
|
|
149
|
+
const state = loadAuthState();
|
|
150
|
+
if (state && !isExpired(state)) {
|
|
151
|
+
console.log("⚠️ You are already logged in to Mikoshi.");
|
|
152
|
+
console.log("Re-authenticating will replace your current session.");
|
|
153
|
+
const answer = await promptLine(
|
|
154
|
+
"Do you want to continue with re-authentication? This will invalidate your existing session if successful. (y/N): "
|
|
155
|
+
);
|
|
156
|
+
if (!new Set(["y", "yes"]).has(answer.trim().toLowerCase())) return 0;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
console.log("🔐 Starting NEET authentication...");
|
|
160
|
+
console.log("🌐 Opening authentication page in your browser...");
|
|
161
|
+
const email = await login();
|
|
162
|
+
console.log(`✅ Logged in as ${email}`);
|
|
163
|
+
return 0;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err instanceof AuthError) {
|
|
166
|
+
console.error(err.message);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
console.error(String(err.message || err));
|
|
170
|
+
return 1;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function cmdLogout() {
|
|
175
|
+
clearAuthState();
|
|
176
|
+
console.log("✅ Logged out");
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function cmdWhoami() {
|
|
181
|
+
const state = loadAuthState();
|
|
182
|
+
if (!state || isExpired(state)) {
|
|
183
|
+
console.log("🔒 Not signed in");
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
const email = emailFromToken(state.access_token) || "unknown";
|
|
187
|
+
const plan = state.plan ? state.plan[0].toUpperCase() + state.plan.slice(1) : "Free";
|
|
188
|
+
console.log(`✅ ${email} (${plan})`);
|
|
189
|
+
return 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function cmdAuthConfigure() {
|
|
193
|
+
const answer = await promptLine(`API base URL [${DEFAULT_API_BASE_URL}]: `);
|
|
194
|
+
const apiBase = answer.trim() || DEFAULT_API_BASE_URL;
|
|
195
|
+
try {
|
|
196
|
+
saveBrokerConfig(apiBase);
|
|
197
|
+
} catch (err) {
|
|
198
|
+
console.error(err.message || err);
|
|
199
|
+
return 2;
|
|
200
|
+
}
|
|
201
|
+
console.log("✅ Auth configured");
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseArgs(argv) {
|
|
206
|
+
const args = { _: [] };
|
|
207
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
208
|
+
const arg = argv[i];
|
|
209
|
+
if (arg === "--verbose") {
|
|
210
|
+
args.verbose = true;
|
|
211
|
+
} else {
|
|
212
|
+
args._.push(arg);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return args;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function promptLine(prompt) {
|
|
219
|
+
const rl = (await import("node:readline")).createInterface({
|
|
220
|
+
input: process.stdin,
|
|
221
|
+
output: process.stdout,
|
|
222
|
+
});
|
|
223
|
+
const answer = await new Promise((resolve) => rl.question(prompt, resolve));
|
|
224
|
+
rl.close();
|
|
225
|
+
return String(answer || "");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function main() {
|
|
229
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
230
|
+
if (parsed.verbose) {
|
|
231
|
+
process.env.MIKOSHI_QUIET_LIBS = "0";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const config = loadConfig();
|
|
235
|
+
configureExternalLibs(config.quiet_external_libs);
|
|
236
|
+
|
|
237
|
+
const [command, ...rest] = parsed._;
|
|
238
|
+
switch (command) {
|
|
239
|
+
case "index":
|
|
240
|
+
return process.exit(await cmdIndex({ path: rest[0] }));
|
|
241
|
+
case "search":
|
|
242
|
+
return process.exit(await cmdSearch({ path: rest[0], query: rest[1], k: Number(rest[2] || 8) }));
|
|
243
|
+
case "status":
|
|
244
|
+
return process.exit(cmdStatus({ path: rest[0] }));
|
|
245
|
+
case "clear":
|
|
246
|
+
return process.exit(cmdClear({ path: rest[0] }));
|
|
247
|
+
case "doctor":
|
|
248
|
+
return process.exit(cmdDoctor({ path: rest[0] }));
|
|
249
|
+
case "login":
|
|
250
|
+
return process.exit(await cmdLogin());
|
|
251
|
+
case "logout":
|
|
252
|
+
return process.exit(cmdLogout());
|
|
253
|
+
case "whoami":
|
|
254
|
+
return process.exit(cmdWhoami());
|
|
255
|
+
case "auth":
|
|
256
|
+
if (rest[0] === "configure") return process.exit(await cmdAuthConfigure());
|
|
257
|
+
break;
|
|
258
|
+
default:
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log("Mikoshi CLI");
|
|
263
|
+
console.log("Usage:");
|
|
264
|
+
console.log(" mikoshi index <path>");
|
|
265
|
+
console.log(" mikoshi search <path> <query> [k]");
|
|
266
|
+
console.log(" mikoshi status <path>");
|
|
267
|
+
console.log(" mikoshi clear <path>");
|
|
268
|
+
console.log(" mikoshi doctor [path]");
|
|
269
|
+
console.log(" mikoshi login | logout | whoami");
|
|
270
|
+
console.log(" mikoshi auth configure");
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
main();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export class ConfigError extends Error {}
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MODEL = "sentence-transformers/all-MiniLM-L6-v2";
|
|
7
|
+
|
|
8
|
+
export function configureExternalLibs(quiet) {
|
|
9
|
+
if (!quiet) return;
|
|
10
|
+
process.env.HF_HUB_DISABLE_PROGRESS_BARS = "1";
|
|
11
|
+
process.env.TRANSFORMERS_VERBOSITY = "error";
|
|
12
|
+
process.env.TOKENIZERS_PARALLELISM = "false";
|
|
13
|
+
process.env.PYTHONWARNINGS = "ignore";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseIntEnv(name, fallback) {
|
|
17
|
+
const raw = process.env[name];
|
|
18
|
+
if (!raw) return fallback;
|
|
19
|
+
const value = Number.parseInt(raw, 10);
|
|
20
|
+
return Number.isFinite(value) ? value : fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function loadConfig() {
|
|
24
|
+
const indexRoot = process.env.MIKOSHI_INDEX_ROOT
|
|
25
|
+
? path.resolve(process.env.MIKOSHI_INDEX_ROOT.replace(/^~\//, `${os.homedir()}/`))
|
|
26
|
+
: path.join(os.homedir(), ".mikoshi");
|
|
27
|
+
|
|
28
|
+
const provider = (process.env.MIKOSHI_EMBEDDINGS_PROVIDER || "local").toLowerCase();
|
|
29
|
+
const model = process.env.MIKOSHI_EMBED_MODEL || DEFAULT_MODEL;
|
|
30
|
+
const openaiKey = process.env.MIKOSHI_OPENAI_API_KEY || process.env.OPENAI_API_KEY || "";
|
|
31
|
+
const openaiBaseUrl =
|
|
32
|
+
process.env.MIKOSHI_OPENAI_BASE_URL || process.env.OPENAI_BASE_URL || "https://api.openai.com";
|
|
33
|
+
const openaiModel = process.env.MIKOSHI_OPENAI_EMBED_MODEL || "text-embedding-3-small";
|
|
34
|
+
|
|
35
|
+
if (provider !== "local" && provider !== "openai") {
|
|
36
|
+
throw new ConfigError(`Unknown embeddings provider: ${provider}`);
|
|
37
|
+
}
|
|
38
|
+
if (provider === "openai" && !openaiKey) {
|
|
39
|
+
throw new ConfigError(
|
|
40
|
+
"OpenAI embeddings provider selected but API key missing. Set MIKOSHI_OPENAI_API_KEY or OPENAI_API_KEY."
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
index_root: indexRoot,
|
|
46
|
+
quiet_external_libs: process.env.MIKOSHI_QUIET_LIBS !== "0",
|
|
47
|
+
max_bytes: parseIntEnv("MIKOSHI_MAX_BYTES", 2_000_000),
|
|
48
|
+
chunk_lines: parseIntEnv("MIKOSHI_CHUNK_LINES", 200),
|
|
49
|
+
chunk_overlap: parseIntEnv("MIKOSHI_CHUNK_OVERLAP", 40),
|
|
50
|
+
embeddings: {
|
|
51
|
+
provider,
|
|
52
|
+
model,
|
|
53
|
+
openai_key: openaiKey,
|
|
54
|
+
openai_base_url: openaiBaseUrl,
|
|
55
|
+
openai_model: openaiModel,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|