mikoshi 0.1.2 → 0.1.4
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/LICENSE +21 -0
- package/PROVENANCE.md +14 -0
- package/README.md +22 -3
- package/package.json +13 -9
- package/src/mikoshi/agent_runtime.js +291 -0
- package/src/mikoshi/auth.js +53 -23
- package/src/mikoshi/chat.js +438 -0
- package/src/mikoshi/chat_models.js +62 -0
- package/src/mikoshi/cli.js +5010 -17
- package/src/mikoshi/indexing/file_scanner.js +8 -1
- package/src/mikoshi/indexing/indexer.js +25 -2
- package/src/mikoshi/mcp_server/server.js +96 -16
- package/src/mikoshi/retrieval/hybrid.js +22 -0
- package/src/mikoshi/retrieval/semantic.js +82 -18
- package/src/mikoshi/session_memory.js +140 -0
- package/src/mikoshi/settings.js +78 -0
- package/src/mikoshi/updates.js +118 -0
- package/src/mikoshi/user_config.js +144 -0
- package/src/mikoshi/utils/types.js +0 -44
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NEETlabs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PROVENANCE.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Provenance
|
|
2
|
+
|
|
3
|
+
Mikoshi's agent system is an independent implementation.
|
|
4
|
+
|
|
5
|
+
- It is informed by black-box behavior of agentic developer tools.
|
|
6
|
+
- It does not copy proprietary source code, prompts, event names, or protocol strings from other products.
|
|
7
|
+
- Agent/runtime contracts in Mikoshi use `mk_`-prefixed event names, lifecycle types, and state transitions implemented in this repository.
|
|
8
|
+
|
|
9
|
+
Design goals:
|
|
10
|
+
|
|
11
|
+
- Deterministic state transitions (`IDLE`, `PLANNING`, `AWAITING_PERMISSION`, `EXECUTING`, `FINALIZING`).
|
|
12
|
+
- Auditable capability grants (`filesystem.write`, `process.spawn`, `network.external`).
|
|
13
|
+
- Typed runtime events rendered by UI; raw tool payloads are not intended for direct UI display.
|
|
14
|
+
- Multi-tool execution with sequential default and isolated parallel option.
|
package/README.md
CHANGED
|
@@ -2,7 +2,13 @@
|
|
|
2
2
|
npm install -g mikoshi
|
|
3
3
|
```
|
|
4
4
|
|
|
5
|
-
✨ Mikoshi —
|
|
5
|
+
✨ Mikoshi — Context Engine for Private local code search + MCP
|
|
6
|
+
|
|
7
|
+
This system is an independent implementation inspired by publicly observable behavior of agentic developer tools.
|
|
8
|
+
The agent runtime, tool lifecycle, permission flow, and event protocol are clean-room implementations using `mk_`-prefixed types and events.
|
|
9
|
+
|
|
10
|
+
## Provenance
|
|
11
|
+
See `PROVENANCE.md` for implementation provenance and design constraints.
|
|
6
12
|
|
|
7
13
|
## Install
|
|
8
14
|
```bash
|
|
@@ -20,6 +26,21 @@ MCP
|
|
|
20
26
|
mikoshi-mcp
|
|
21
27
|
```
|
|
22
28
|
|
|
29
|
+
Interactive permissions
|
|
30
|
+
```text
|
|
31
|
+
/permissions
|
|
32
|
+
# configure: filesystem.write, process.spawn, network.external
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Models
|
|
36
|
+
```text
|
|
37
|
+
/models
|
|
38
|
+
# provider: openai or anthropic
|
|
39
|
+
# API keys are stored per provider+model
|
|
40
|
+
# openai models: gpt-5.2, gpt-5.2-codex, gpt-5.2-pro
|
|
41
|
+
# anthropic models: claude-sonnet-4-5, claude-haiku-4-5, claude-opus-4-6
|
|
42
|
+
```
|
|
43
|
+
|
|
23
44
|
Codex MCP config (config.toml)
|
|
24
45
|
```toml
|
|
25
46
|
[mcp_servers.mikoshi]
|
|
@@ -37,5 +58,3 @@ npm install
|
|
|
37
58
|
npm test
|
|
38
59
|
mikoshi doctor
|
|
39
60
|
```
|
|
40
|
-
|
|
41
|
-
Note: First run may download the local embedding model once.
|
package/package.json
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mikoshi",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Private local code search + MCP",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"description": "Context Engine for Private local code search + MCP",
|
|
5
|
+
"license": "MIT",
|
|
5
6
|
"type": "module",
|
|
7
|
+
"homepage": "https://NEET.gg",
|
|
6
8
|
"bin": {
|
|
7
9
|
"mikoshi": "src/mikoshi/cli.js",
|
|
8
10
|
"mikoshi-mcp": "src/mikoshi/mcp_server/server.js"
|
|
9
11
|
},
|
|
10
12
|
"files": [
|
|
11
13
|
"src/mikoshi/**",
|
|
12
|
-
"README.md"
|
|
14
|
+
"README.md",
|
|
15
|
+
"PROVENANCE.md"
|
|
13
16
|
],
|
|
14
17
|
"scripts": {
|
|
15
18
|
"test": "node --test",
|
|
16
|
-
"lint": "node --check ./src/mikoshi/cli.js"
|
|
19
|
+
"lint": "node --check ./src/mikoshi/cli.js",
|
|
20
|
+
"audit:unused": "node scripts/audit-unused.js",
|
|
21
|
+
"prepack": "node scripts/set-git-head.js"
|
|
17
22
|
},
|
|
18
|
-
"dependencies": {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@xenova/transformers": "^2.17.2"
|
|
25
|
+
},
|
|
26
|
+
"gitHead": "0c4ae59"
|
|
23
27
|
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
export const mk_AgentState = Object.freeze({
|
|
2
|
+
IDLE: "IDLE",
|
|
3
|
+
PLANNING: "PLANNING",
|
|
4
|
+
AWAITING_PERMISSION: "AWAITING_PERMISSION",
|
|
5
|
+
EXECUTING: "EXECUTING",
|
|
6
|
+
FINALIZING: "FINALIZING",
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function mk_nowIso() {
|
|
10
|
+
return new Date().toISOString();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function mk_safeText(value) {
|
|
14
|
+
return String(value || "").trim();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function mk_bool(value) {
|
|
18
|
+
return value === true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class mk_AgentRuntime {
|
|
22
|
+
constructor({
|
|
23
|
+
sessionId,
|
|
24
|
+
onEvent,
|
|
25
|
+
requestCapability,
|
|
26
|
+
executeTool,
|
|
27
|
+
deriveCapability,
|
|
28
|
+
resolveCapabilityMode,
|
|
29
|
+
} = {}) {
|
|
30
|
+
this.sessionId = mk_safeText(sessionId) || `mk_${Date.now().toString(36)}`;
|
|
31
|
+
this.state = mk_AgentState.IDLE;
|
|
32
|
+
this.pendingTools = [];
|
|
33
|
+
this.grantedCapabilities = new Set();
|
|
34
|
+
this.executionLog = [];
|
|
35
|
+
this._toolCounter = 0;
|
|
36
|
+
this._onEvent = typeof onEvent === "function" ? onEvent : () => {};
|
|
37
|
+
this._requestCapability =
|
|
38
|
+
typeof requestCapability === "function" ? requestCapability : async () => true;
|
|
39
|
+
this._executeTool =
|
|
40
|
+
typeof executeTool === "function" ? executeTool : async () => ({ output: {} });
|
|
41
|
+
this._deriveCapability = typeof deriveCapability === "function" ? deriveCapability : () => null;
|
|
42
|
+
this._resolveCapabilityMode =
|
|
43
|
+
typeof resolveCapabilityMode === "function" ? resolveCapabilityMode : () => "prompt";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
emit(event) {
|
|
47
|
+
if (!event || typeof event !== "object") return;
|
|
48
|
+
const payload = { ...event };
|
|
49
|
+
this._onEvent(payload);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setState(state, context = undefined) {
|
|
53
|
+
if (!state || this.state === state) return;
|
|
54
|
+
this.state = state;
|
|
55
|
+
this.emit({
|
|
56
|
+
type: "mk_agent.state",
|
|
57
|
+
state,
|
|
58
|
+
context: context && typeof context === "object" ? context : undefined,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setIntent(goal, meta = undefined) {
|
|
63
|
+
this.emit({
|
|
64
|
+
type: "mk_agent.intent",
|
|
65
|
+
goal: mk_safeText(goal),
|
|
66
|
+
meta: meta && typeof meta === "object" ? meta : undefined,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
queueTools(calls) {
|
|
71
|
+
const items = Array.isArray(calls) ? calls : [];
|
|
72
|
+
const queued = [];
|
|
73
|
+
for (const call of items) {
|
|
74
|
+
if (!call || !call.toolName) continue;
|
|
75
|
+
this._toolCounter += 1;
|
|
76
|
+
const execution = {
|
|
77
|
+
id: `mk_tool_${this._toolCounter}`,
|
|
78
|
+
toolName: mk_safeText(call.toolName),
|
|
79
|
+
inputs: call.inputs && typeof call.inputs === "object" ? call.inputs : {},
|
|
80
|
+
approved: mk_bool(call.approved),
|
|
81
|
+
executedAt: undefined,
|
|
82
|
+
output: undefined,
|
|
83
|
+
error: "",
|
|
84
|
+
isolated: mk_bool(call.isolated),
|
|
85
|
+
providerToolCallId: mk_safeText(call.providerToolCallId),
|
|
86
|
+
display: mk_safeText(call.display) || mk_safeText(call.toolName),
|
|
87
|
+
finalized: false,
|
|
88
|
+
};
|
|
89
|
+
this.pendingTools.push(execution);
|
|
90
|
+
queued.push(execution);
|
|
91
|
+
}
|
|
92
|
+
return queued;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async executePendingTools({ parallelIsolated = false } = {}) {
|
|
96
|
+
if (!this.pendingTools.length) {
|
|
97
|
+
return { toolResults: [], stop: false };
|
|
98
|
+
}
|
|
99
|
+
this.setState(mk_AgentState.EXECUTING, { pending: this.pendingTools.length });
|
|
100
|
+
|
|
101
|
+
const toolResults = [];
|
|
102
|
+
let stop = false;
|
|
103
|
+
const sequential = this.pendingTools.filter((tool) => !tool.isolated || !parallelIsolated);
|
|
104
|
+
const parallel = this.pendingTools.filter((tool) => tool.isolated && parallelIsolated);
|
|
105
|
+
const totalSteps = this.pendingTools.length;
|
|
106
|
+
let stepCursor = 0;
|
|
107
|
+
|
|
108
|
+
for (const tool of sequential) {
|
|
109
|
+
stepCursor += 1;
|
|
110
|
+
const outcome = await this._executeSingleTool(tool, stepCursor, totalSteps);
|
|
111
|
+
toolResults.push(outcome.toolResult);
|
|
112
|
+
if (outcome.stop) {
|
|
113
|
+
stop = true;
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!stop && parallel.length) {
|
|
119
|
+
const baseStep = stepCursor;
|
|
120
|
+
const results = await Promise.all(
|
|
121
|
+
parallel.map(async (tool, idx) => ({
|
|
122
|
+
idx,
|
|
123
|
+
outcome: await this._executeSingleTool(tool, baseStep + idx + 1, totalSteps),
|
|
124
|
+
}))
|
|
125
|
+
);
|
|
126
|
+
results
|
|
127
|
+
.sort((a, b) => a.idx - b.idx)
|
|
128
|
+
.forEach(({ outcome }) => {
|
|
129
|
+
toolResults.push(outcome.toolResult);
|
|
130
|
+
if (outcome.stop) stop = true;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.pendingTools = this.pendingTools.filter((tool) => !tool.finalized);
|
|
135
|
+
return { toolResults, stop };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
finalize() {
|
|
139
|
+
this.setState(mk_AgentState.FINALIZING, { pending: this.pendingTools.length });
|
|
140
|
+
this.pendingTools = [];
|
|
141
|
+
this.setState(mk_AgentState.IDLE, { pending: 0 });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async _executeSingleTool(tool, step, totalSteps) {
|
|
145
|
+
const capability = mk_safeText(this._deriveCapability(tool)) || "";
|
|
146
|
+
if (capability && !tool.approved) {
|
|
147
|
+
const mode = mk_safeText(
|
|
148
|
+
this._resolveCapabilityMode({
|
|
149
|
+
capability,
|
|
150
|
+
toolExecution: tool,
|
|
151
|
+
})
|
|
152
|
+
).toLowerCase();
|
|
153
|
+
const isPromptMode = !mode || mode === "prompt";
|
|
154
|
+
if (isPromptMode) {
|
|
155
|
+
this.setState(mk_AgentState.AWAITING_PERMISSION, {
|
|
156
|
+
capability,
|
|
157
|
+
tool_id: tool.id,
|
|
158
|
+
});
|
|
159
|
+
this.emit({
|
|
160
|
+
type: "mk_capability.request",
|
|
161
|
+
capability,
|
|
162
|
+
reason: tool.display || tool.toolName,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const granted = await this._requestCapability({
|
|
166
|
+
capability,
|
|
167
|
+
reason: tool.display || tool.toolName,
|
|
168
|
+
toolExecution: tool,
|
|
169
|
+
});
|
|
170
|
+
tool.approved = mk_bool(granted);
|
|
171
|
+
if (tool.approved) this.grantedCapabilities.add(capability);
|
|
172
|
+
if (isPromptMode || !tool.approved) {
|
|
173
|
+
this.emit({
|
|
174
|
+
type: "mk_capability.receipt",
|
|
175
|
+
capability,
|
|
176
|
+
granted: tool.approved,
|
|
177
|
+
details: {
|
|
178
|
+
tool_id: tool.id,
|
|
179
|
+
tool: tool.toolName,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (isPromptMode) {
|
|
184
|
+
this.setState(mk_AgentState.EXECUTING, { pending: this.pendingTools.length });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
this.emit({
|
|
189
|
+
type: "mk_tool.progress",
|
|
190
|
+
tool_id: tool.id,
|
|
191
|
+
step,
|
|
192
|
+
total_steps: totalSteps,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (!tool.approved && capability) {
|
|
196
|
+
const error = `Permission denied for ${capability}.`;
|
|
197
|
+
tool.executedAt = mk_nowIso();
|
|
198
|
+
tool.error = error;
|
|
199
|
+
tool.output = { error, capability };
|
|
200
|
+
tool.finalized = true;
|
|
201
|
+
const toolResult = {
|
|
202
|
+
id: tool.id,
|
|
203
|
+
tool_call_id: tool.providerToolCallId || "",
|
|
204
|
+
toolUseId: tool.providerToolCallId || "",
|
|
205
|
+
tool: tool.toolName,
|
|
206
|
+
input: tool.inputs || {},
|
|
207
|
+
ok: false,
|
|
208
|
+
output: tool.output,
|
|
209
|
+
summary: `denied ${capability}`,
|
|
210
|
+
};
|
|
211
|
+
this.executionLog.push({
|
|
212
|
+
...toolResult,
|
|
213
|
+
ts: tool.executedAt,
|
|
214
|
+
});
|
|
215
|
+
this.emit({
|
|
216
|
+
type: "mk_tool.result",
|
|
217
|
+
tool_id: tool.id,
|
|
218
|
+
success: false,
|
|
219
|
+
output: tool.output,
|
|
220
|
+
error,
|
|
221
|
+
summary: `denied ${capability}`,
|
|
222
|
+
});
|
|
223
|
+
return { toolResult, stop: false };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const result = await this._executeTool(tool);
|
|
228
|
+
tool.executedAt = mk_nowIso();
|
|
229
|
+
tool.output = result?.output || {};
|
|
230
|
+
const summary = mk_safeText(result?.summary);
|
|
231
|
+
const toolError = mk_safeText(tool.output?.error);
|
|
232
|
+
tool.error = toolError;
|
|
233
|
+
tool.finalized = true;
|
|
234
|
+
const toolResult = {
|
|
235
|
+
id: tool.id,
|
|
236
|
+
tool_call_id: tool.providerToolCallId || "",
|
|
237
|
+
toolUseId: tool.providerToolCallId || "",
|
|
238
|
+
tool: tool.toolName,
|
|
239
|
+
input: tool.inputs || {},
|
|
240
|
+
ok: !toolError,
|
|
241
|
+
output: tool.output,
|
|
242
|
+
summary,
|
|
243
|
+
};
|
|
244
|
+
if (mk_safeText(result?.preview)) {
|
|
245
|
+
toolResult.preview = mk_safeText(result.preview);
|
|
246
|
+
}
|
|
247
|
+
this.executionLog.push({
|
|
248
|
+
...toolResult,
|
|
249
|
+
ts: tool.executedAt,
|
|
250
|
+
});
|
|
251
|
+
this.emit({
|
|
252
|
+
type: "mk_tool.result",
|
|
253
|
+
tool_id: tool.id,
|
|
254
|
+
success: !toolError,
|
|
255
|
+
output: tool.output,
|
|
256
|
+
error: toolError || undefined,
|
|
257
|
+
summary: summary || (toolError ? toolError : "completed"),
|
|
258
|
+
});
|
|
259
|
+
return { toolResult, stop: mk_bool(result?.stop) };
|
|
260
|
+
} catch (err) {
|
|
261
|
+
const error = mk_safeText(err?.message || err) || "Tool execution failed.";
|
|
262
|
+
tool.executedAt = mk_nowIso();
|
|
263
|
+
tool.error = error;
|
|
264
|
+
tool.output = { error };
|
|
265
|
+
tool.finalized = true;
|
|
266
|
+
const toolResult = {
|
|
267
|
+
id: tool.id,
|
|
268
|
+
tool_call_id: tool.providerToolCallId || "",
|
|
269
|
+
toolUseId: tool.providerToolCallId || "",
|
|
270
|
+
tool: tool.toolName,
|
|
271
|
+
input: tool.inputs || {},
|
|
272
|
+
ok: false,
|
|
273
|
+
output: tool.output,
|
|
274
|
+
summary: error,
|
|
275
|
+
};
|
|
276
|
+
this.executionLog.push({
|
|
277
|
+
...toolResult,
|
|
278
|
+
ts: tool.executedAt,
|
|
279
|
+
});
|
|
280
|
+
this.emit({
|
|
281
|
+
type: "mk_tool.result",
|
|
282
|
+
tool_id: tool.id,
|
|
283
|
+
success: false,
|
|
284
|
+
output: tool.output,
|
|
285
|
+
error,
|
|
286
|
+
summary: error,
|
|
287
|
+
});
|
|
288
|
+
return { toolResult, stop: false };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
package/src/mikoshi/auth.js
CHANGED
|
@@ -8,7 +8,8 @@ import { DEFAULT_FEATURES, DEFAULT_PLAN } from "./entitlements.js";
|
|
|
8
8
|
export const DEFAULT_API_BASE_URL = "https://neet.gg";
|
|
9
9
|
|
|
10
10
|
const AUTH_FILENAME = "auth.json";
|
|
11
|
-
const CONFIG_FILENAME = "config.json";
|
|
11
|
+
const CONFIG_FILENAME = "auth-config.json";
|
|
12
|
+
const LEGACY_CONFIG_FILENAME = "config.json";
|
|
12
13
|
|
|
13
14
|
export class AuthError extends Error {}
|
|
14
15
|
|
|
@@ -25,18 +26,25 @@ export function configPath() {
|
|
|
25
26
|
return path.join(indexRoot(), CONFIG_FILENAME);
|
|
26
27
|
}
|
|
27
28
|
|
|
29
|
+
function legacyConfigPath() {
|
|
30
|
+
return path.join(indexRoot(), LEGACY_CONFIG_FILENAME);
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
export function loadBrokerConfig() {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
const candidates = [configPath(), legacyConfigPath()];
|
|
35
|
+
for (const cfgPath of candidates) {
|
|
36
|
+
if (!fs.existsSync(cfgPath)) continue;
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
39
|
+
const apiBase = String(data.api_base_url || "").trim().replace(/\/+$/, "");
|
|
40
|
+
if (apiBase) {
|
|
41
|
+
return { api_base_url: apiBase };
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore invalid config and continue
|
|
45
|
+
}
|
|
39
46
|
}
|
|
47
|
+
return { api_base_url: DEFAULT_API_BASE_URL };
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
export function saveBrokerConfig(apiBaseUrl) {
|
|
@@ -150,20 +158,41 @@ async function promptLine(prompt) {
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
function parsePastePayload(text) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
161
|
+
const input = String(text || "").trim();
|
|
162
|
+
if (!input) throw new AuthError("Missing response.");
|
|
163
|
+
if (input.startsWith("{")) {
|
|
164
|
+
let data;
|
|
165
|
+
try {
|
|
166
|
+
data = JSON.parse(input);
|
|
167
|
+
} catch {
|
|
168
|
+
throw new AuthError("Invalid response payload.");
|
|
169
|
+
}
|
|
170
|
+
if (!data || typeof data !== "object") throw new AuthError("Invalid response payload.");
|
|
171
|
+
const code = String(data.code || "");
|
|
172
|
+
const state = String(data.state || "");
|
|
173
|
+
if (!code || !state) throw new AuthError("Missing code or state.");
|
|
174
|
+
return { code, state };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const queryPart = input.includes("?") ? input.split("?").pop() : input;
|
|
178
|
+
const hashPart = queryPart.includes("#") ? queryPart.split("#").pop() : queryPart;
|
|
179
|
+
const params = new URLSearchParams(hashPart);
|
|
180
|
+
const codeFromParams = params.get("code");
|
|
181
|
+
const stateFromParams = params.get("state");
|
|
182
|
+
if (codeFromParams && stateFromParams) {
|
|
183
|
+
return { code: codeFromParams, state: stateFromParams };
|
|
158
184
|
}
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
const
|
|
162
|
-
if (
|
|
163
|
-
|
|
185
|
+
|
|
186
|
+
const codeMatch = input.match(/code\s*[:=]\s*([A-Za-z0-9._~-]+)/i);
|
|
187
|
+
const stateMatch = input.match(/state\s*[:=]\s*([A-Za-z0-9._~-]+)/i);
|
|
188
|
+
if (codeMatch && stateMatch) {
|
|
189
|
+
return { code: codeMatch[1], state: stateMatch[1] };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
throw new AuthError("Missing code or state.");
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
export async function login() {
|
|
195
|
+
export async function login({ promptLabel } = {}) {
|
|
167
196
|
const broker = loadBrokerConfig();
|
|
168
197
|
const state = generateState();
|
|
169
198
|
const verifier = generateCodeVerifier();
|
|
@@ -172,7 +201,8 @@ export async function login() {
|
|
|
172
201
|
|
|
173
202
|
await openUrl(url);
|
|
174
203
|
|
|
175
|
-
const
|
|
204
|
+
const promptText = String(promptLabel || "Paste the response here: ");
|
|
205
|
+
const pasted = await promptLine(promptText);
|
|
176
206
|
const { code, state: returnedState } = parsePastePayload(pasted);
|
|
177
207
|
if (returnedState !== state) {
|
|
178
208
|
throw new AuthError("Invalid state.");
|