@mercury-fw/core 0.25.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 +19 -0
- package/README.md +38 -0
- package/dist/index.d.ts +23 -0
- package/dist/src/admin/cli-routes.d.ts +22 -0
- package/dist/src/admin/env-file.d.ts +1 -0
- package/dist/src/admin/model-routes.d.ts +26 -0
- package/dist/src/admin/qdrant-scroll.d.ts +34 -0
- package/dist/src/admin/server.d.ts +40 -0
- package/dist/src/admin/wiki-routes.d.ts +31 -0
- package/dist/src/compose.d.ts +42 -0
- package/dist/src/config/define-config.d.ts +31 -0
- package/dist/src/cron/idle-session-cron.d.ts +80 -0
- package/dist/src/cron/idle-session-scanner.d.ts +16 -0
- package/dist/src/cron/self-review-cron.d.ts +55 -0
- package/dist/src/cron/semantic-consolidation.d.ts +71 -0
- package/dist/src/memory/embedder.d.ts +9 -0
- package/dist/src/memory/episodic-store.d.ts +121 -0
- package/dist/src/memory/memory-provider.d.ts +51 -0
- package/dist/src/memory/semantic-facts-store.d.ts +37 -0
- package/dist/src/memory/tool-corrections-store.d.ts +26 -0
- package/dist/src/memory/verbatim-archive-store.d.ts +86 -0
- package/dist/src/model/client.d.ts +24 -0
- package/dist/src/model/context-size.d.ts +30 -0
- package/dist/src/plugins/manifest.d.ts +29 -0
- package/dist/src/plugins/plugin-loader.d.ts +85 -0
- package/dist/src/router/channel-loader.d.ts +30 -0
- package/dist/src/router/provider.d.ts +7 -0
- package/dist/src/router/terminal-provider.d.ts +37 -0
- package/dist/src/router/terminal.d.ts +41 -0
- package/dist/src/router/tool-log.d.ts +65 -0
- package/dist/src/router/turn-runner.d.ts +86 -0
- package/dist/src/session/agent-turn.d.ts +266 -0
- package/dist/src/session/context-primer.d.ts +16 -0
- package/dist/src/session/episodic-summarizer.d.ts +25 -0
- package/dist/src/session/history.d.ts +95 -0
- package/dist/src/session/pending-confirmation.d.ts +8 -0
- package/dist/src/session/read-skill-tool.d.ts +4 -0
- package/dist/src/session/semantic-fact-extractor.d.ts +45 -0
- package/dist/src/session/step-info.d.ts +24 -0
- package/dist/src/session/summarizer.d.ts +23 -0
- package/dist/src/session/system-prompt.d.ts +38 -0
- package/dist/src/session/tool-correction-extractor.d.ts +43 -0
- package/dist/src/session/tool-log-buffer.d.ts +24 -0
- package/dist/src/session/tool-log-recall-tool.d.ts +18 -0
- package/dist/src/session/tool-start-hook.d.ts +57 -0
- package/dist/src/tools/display-store.d.ts +36 -0
- package/dist/src/tools/present-tool.d.ts +23 -0
- package/dist/src/wiki/frontmatter-schema.d.ts +53 -0
- package/dist/src/wiki/index-entry.d.ts +15 -0
- package/dist/src/wiki/orphan-detector.d.ts +1 -0
- package/dist/src/wiki/self-review-runner.d.ts +48 -0
- package/dist/src/wiki/self-review-tools.d.ts +22 -0
- package/dist/src/wiki/vault-cli.d.ts +2 -0
- package/dist/src/wiki/vault-init.d.ts +7 -0
- package/dist/src/wiki/wiki-note.d.ts +62 -0
- package/dist/src/wiki/wiki-read.d.ts +27 -0
- package/dist/src/wiki/wiki-tools.d.ts +7 -0
- package/index.ts +23 -0
- package/package.json +49 -0
- package/src/admin/cli-routes.ts +48 -0
- package/src/admin/env-file.ts +29 -0
- package/src/admin/model-routes.ts +71 -0
- package/src/admin/public/index.html +416 -0
- package/src/admin/qdrant-scroll.ts +45 -0
- package/src/admin/server.ts +188 -0
- package/src/admin/wiki-routes.ts +93 -0
- package/src/compose.ts +599 -0
- package/src/config/define-config.ts +35 -0
- package/src/cron/.gitkeep +0 -0
- package/src/cron/idle-session-cron.ts +144 -0
- package/src/cron/idle-session-scanner.ts +37 -0
- package/src/cron/self-review-cron.ts +103 -0
- package/src/cron/semantic-consolidation.ts +228 -0
- package/src/memory/.gitkeep +0 -0
- package/src/memory/embedder.ts +15 -0
- package/src/memory/episodic-store.ts +183 -0
- package/src/memory/memory-provider.ts +98 -0
- package/src/memory/semantic-facts-store.ts +89 -0
- package/src/memory/tool-corrections-store.ts +72 -0
- package/src/memory/verbatim-archive-store.ts +202 -0
- package/src/model/client.ts +33 -0
- package/src/model/context-size.ts +42 -0
- package/src/plugins/manifest.ts +47 -0
- package/src/plugins/plugin-loader.ts +205 -0
- package/src/router/channel-loader.ts +56 -0
- package/src/router/provider.ts +7 -0
- package/src/router/terminal-provider.ts +155 -0
- package/src/router/terminal.ts +151 -0
- package/src/router/tool-log.ts +116 -0
- package/src/router/turn-runner.ts +205 -0
- package/src/session/agent-turn.ts +391 -0
- package/src/session/context-primer.ts +134 -0
- package/src/session/episodic-summarizer.ts +38 -0
- package/src/session/history.ts +168 -0
- package/src/session/pending-confirmation.ts +8 -0
- package/src/session/read-skill-tool.ts +38 -0
- package/src/session/semantic-fact-extractor.ts +69 -0
- package/src/session/step-info.ts +27 -0
- package/src/session/summarizer.ts +36 -0
- package/src/session/system-prompt.ts +142 -0
- package/src/session/tool-correction-extractor.ts +133 -0
- package/src/session/tool-log-buffer.ts +73 -0
- package/src/session/tool-log-recall-tool.ts +38 -0
- package/src/session/tool-start-hook.ts +164 -0
- package/src/tools/display-store.ts +89 -0
- package/src/tools/present-tool.ts +41 -0
- package/src/wiki/.gitkeep +0 -0
- package/src/wiki/frontmatter-schema.ts +49 -0
- package/src/wiki/index-entry.ts +59 -0
- package/src/wiki/orphan-detector.ts +61 -0
- package/src/wiki/self-review-runner.ts +133 -0
- package/src/wiki/self-review-tools.ts +162 -0
- package/src/wiki/vault-cli.ts +143 -0
- package/src/wiki/vault-init.ts +43 -0
- package/src/wiki/wiki-note.ts +326 -0
- package/src/wiki/wiki-read.ts +122 -0
- package/src/wiki/wiki-tools.ts +112 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin panel POC — a separate `Bun.serve()` instance running inside the
|
|
3
|
+
* same Mercury process (see plan: reuses the already-constructed `qdrant`
|
|
4
|
+
* client, `activeCliConfigs`, model, and vault path directly, no IPC).
|
|
5
|
+
* Dev-only, unauthenticated by design (see docs/DECISIONS.md-adjacent
|
|
6
|
+
* scoping conversation) — never started unless `ADMIN_PANEL_ENABLED` is
|
|
7
|
+
* set (see `src/index.ts`).
|
|
8
|
+
*/
|
|
9
|
+
import type { LanguageModel } from "ai";
|
|
10
|
+
import type { CliConfig } from "@mercury-fw/cli-engine";
|
|
11
|
+
import type { runCli } from "@mercury-fw/cli-engine";
|
|
12
|
+
import { getCliStatus } from "./cli-routes.ts";
|
|
13
|
+
import { getModelStatus, getSelfHealth } from "./model-routes.ts";
|
|
14
|
+
import { scrollCollection, type ScrollableQdrantClient } from "./qdrant-scroll.ts";
|
|
15
|
+
import { setEnvValue } from "./env-file.ts";
|
|
16
|
+
import { getToolLog } from "../session/tool-log-buffer.ts";
|
|
17
|
+
import {
|
|
18
|
+
listWikiVault,
|
|
19
|
+
readWikiVaultFile,
|
|
20
|
+
grepWikiVault,
|
|
21
|
+
writeRawWikiEntry,
|
|
22
|
+
deleteWikiEntry,
|
|
23
|
+
editWikiViaModel,
|
|
24
|
+
} from "./wiki-routes.ts";
|
|
25
|
+
|
|
26
|
+
type QdrantDeps = ScrollableQdrantClient & { getCollections(): Promise<{ collections: Array<{ name: string }> }> };
|
|
27
|
+
|
|
28
|
+
/** The only `.env` keys the panel ever reads or writes — see plan §7. Never a secret. */
|
|
29
|
+
const ENV_ALLOWLIST = [
|
|
30
|
+
"OLLAMA_MODEL",
|
|
31
|
+
"STALE_TICKET_CHECK_INTERVAL_MS",
|
|
32
|
+
"STALE_PR_CHECK_INTERVAL_MS",
|
|
33
|
+
"SESSION_IDLE_TIMEOUT_MS",
|
|
34
|
+
"SESSION_IDLE_CHECK_INTERVAL_MS",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
export type AdminServerDeps = {
|
|
38
|
+
port: number;
|
|
39
|
+
vaultPath: string;
|
|
40
|
+
model: LanguageModel;
|
|
41
|
+
qdrant: QdrantDeps;
|
|
42
|
+
qdrantCollections: { episodic: string; semanticFacts: string };
|
|
43
|
+
activeCliConfigs: Record<string, CliConfig>;
|
|
44
|
+
runCliFn: typeof runCli;
|
|
45
|
+
ollamaHost: string;
|
|
46
|
+
ollamaModel: string;
|
|
47
|
+
systemPrompts: { terminal: string; googleChat: string };
|
|
48
|
+
envFilePath: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function json(data: unknown, status = 200): Response {
|
|
52
|
+
return Response.json(data, { status });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function errorResponse(err: unknown, status = 400): Response {
|
|
56
|
+
return json({ ok: false, error: err instanceof Error ? err.message : String(err) }, status);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function startAdminServer(deps: AdminServerDeps): ReturnType<typeof Bun.serve> {
|
|
60
|
+
const indexHtml = Bun.file(new URL("./public/index.html", import.meta.url));
|
|
61
|
+
|
|
62
|
+
return Bun.serve({
|
|
63
|
+
port: deps.port,
|
|
64
|
+
routes: {
|
|
65
|
+
"/": async () => new Response(indexHtml, { headers: { "content-type": "text/html; charset=utf-8" } }),
|
|
66
|
+
|
|
67
|
+
"/api/wiki/list": {
|
|
68
|
+
GET: async () => json({ ok: true, files: await listWikiVault(deps.vaultPath) }),
|
|
69
|
+
},
|
|
70
|
+
"/api/wiki/read": {
|
|
71
|
+
GET: async (req) => {
|
|
72
|
+
const path = new URL(req.url).searchParams.get("path");
|
|
73
|
+
if (!path) return errorResponse("missing ?path");
|
|
74
|
+
try {
|
|
75
|
+
return json({ ok: true, content: await readWikiVaultFile(deps.vaultPath, path) });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return errorResponse(err);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
"/api/wiki/grep": {
|
|
82
|
+
GET: async (req) => {
|
|
83
|
+
const pattern = new URL(req.url).searchParams.get("pattern");
|
|
84
|
+
if (!pattern) return errorResponse("missing ?pattern");
|
|
85
|
+
try {
|
|
86
|
+
return json({ ok: true, matches: await grepWikiVault(deps.vaultPath, pattern) });
|
|
87
|
+
} catch (err) {
|
|
88
|
+
return errorResponse(err);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
"/api/wiki/edit": {
|
|
93
|
+
POST: async (req) => {
|
|
94
|
+
const body = (await req.json()) as { instruction?: string };
|
|
95
|
+
if (!body.instruction?.trim()) return errorResponse("missing instruction");
|
|
96
|
+
try {
|
|
97
|
+
const reply = await editWikiViaModel(deps.model, deps.vaultPath, body.instruction);
|
|
98
|
+
return json({ ok: true, reply });
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return errorResponse(err, 500);
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
"/api/wiki/raw": {
|
|
105
|
+
POST: async (req) => {
|
|
106
|
+
const body = (await req.json()) as { path?: string; content?: string };
|
|
107
|
+
if (!body.path || body.content === undefined) return errorResponse("missing path or content");
|
|
108
|
+
try {
|
|
109
|
+
await writeRawWikiEntry(deps.vaultPath, body.path, body.content);
|
|
110
|
+
return json({ ok: true });
|
|
111
|
+
} catch (err) {
|
|
112
|
+
return errorResponse(err);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
"/api/wiki/entry": {
|
|
117
|
+
DELETE: async (req) => {
|
|
118
|
+
const path = new URL(req.url).searchParams.get("path");
|
|
119
|
+
if (!path) return errorResponse("missing ?path");
|
|
120
|
+
try {
|
|
121
|
+
await deleteWikiEntry(deps.vaultPath, path);
|
|
122
|
+
return json({ ok: true });
|
|
123
|
+
} catch (err) {
|
|
124
|
+
return errorResponse(err);
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
"/api/qdrant/collections": {
|
|
130
|
+
GET: async () => json({ ok: true, ...(await deps.qdrant.getCollections()) }),
|
|
131
|
+
},
|
|
132
|
+
"/api/qdrant/scroll": {
|
|
133
|
+
GET: async (req) => {
|
|
134
|
+
const url = new URL(req.url);
|
|
135
|
+
const collection = url.searchParams.get("collection");
|
|
136
|
+
if (!collection) return errorResponse("missing ?collection");
|
|
137
|
+
const limit = Number(url.searchParams.get("limit") ?? "50");
|
|
138
|
+
const offset = url.searchParams.get("offset") ?? undefined;
|
|
139
|
+
const page = await scrollCollection(deps.qdrant, collection, { limit, offset });
|
|
140
|
+
return json({ ok: true, ...page });
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
"/api/cli/status": {
|
|
145
|
+
GET: async () => json({ ok: true, clis: await getCliStatus(deps.activeCliConfigs, deps.runCliFn) }),
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
"/api/model/status": {
|
|
149
|
+
GET: async () => json({ ok: true, ...(await getModelStatus(deps.ollamaHost, deps.ollamaModel)) }),
|
|
150
|
+
},
|
|
151
|
+
"/api/health": {
|
|
152
|
+
GET: async () =>
|
|
153
|
+
json({ ok: true, ...(await getSelfHealth({ qdrant: deps.qdrant, ollamaHost: deps.ollamaHost })) }),
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
"/api/system-prompt": {
|
|
157
|
+
GET: async () => json({ ok: true, ...deps.systemPrompts }),
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
"/api/tool-log": {
|
|
161
|
+
GET: async () => json({ ok: true, entries: getToolLog() }),
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
"/api/env": {
|
|
165
|
+
GET: async () => {
|
|
166
|
+
const content = await Bun.file(deps.envFilePath).text();
|
|
167
|
+
const values: Record<string, string | undefined> = {};
|
|
168
|
+
for (const key of ENV_ALLOWLIST) {
|
|
169
|
+
const match = content.match(new RegExp(`^${key}=(.*)$`, "m"));
|
|
170
|
+
values[key] = match?.[1];
|
|
171
|
+
}
|
|
172
|
+
return json({ ok: true, keys: ENV_ALLOWLIST, values });
|
|
173
|
+
},
|
|
174
|
+
PUT: async (req) => {
|
|
175
|
+
const body = (await req.json()) as { key?: string; value?: string };
|
|
176
|
+
if (!body.key || body.value === undefined) return errorResponse("missing key or value");
|
|
177
|
+
if (!(ENV_ALLOWLIST as readonly string[]).includes(body.key)) {
|
|
178
|
+
return errorResponse(`"${body.key}" is not an editable key from this panel`);
|
|
179
|
+
}
|
|
180
|
+
const content = await Bun.file(deps.envFilePath).text();
|
|
181
|
+
await Bun.write(deps.envFilePath, setEnvValue(content, body.key, body.value));
|
|
182
|
+
return json({ ok: true });
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
error: (err) => errorResponse(err, 500),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin panel's Wiki tab. `list`/`read`/`grep` walk the whole vault
|
|
3
|
+
* directly (same trusted-admin-context choice `vault-cli.ts` already
|
|
4
|
+
* makes — bypassing `wiki-read.ts`'s per-user `allowedRoots` scoping,
|
|
5
|
+
* which exists to isolate what the MODEL can see per caller, not what an
|
|
6
|
+
* operator running this panel can see).
|
|
7
|
+
*
|
|
8
|
+
* Curated edits go through a real, short-lived agent turn instead of
|
|
9
|
+
* calling `writeCuratedNote` directly — see `editWikiViaModel`. Raw
|
|
10
|
+
* writes and deletes have no model-facing tool today, so they stay
|
|
11
|
+
* direct calls into `wiki-note.ts`, same as `vault-cli.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import type { LanguageModel } from "ai";
|
|
14
|
+
import { createSessionHistory } from "../session/history.ts";
|
|
15
|
+
import { runTurn } from "../session/agent-turn.ts";
|
|
16
|
+
import { createWikiTools } from "../wiki/wiki-tools.ts";
|
|
17
|
+
import { writeRawEntry, deleteCuratedEntry, deleteRawEntry } from "../wiki/wiki-note.ts";
|
|
18
|
+
import type { WikiGrepMatch } from "../wiki/wiki-read.ts";
|
|
19
|
+
|
|
20
|
+
export async function listWikiVault(vaultPath: string): Promise<string[]> {
|
|
21
|
+
const glob = new Bun.Glob("**/*.md");
|
|
22
|
+
const results: string[] = [];
|
|
23
|
+
for await (const file of glob.scan({ cwd: vaultPath })) {
|
|
24
|
+
results.push(file);
|
|
25
|
+
}
|
|
26
|
+
return results.sort();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function readWikiVaultFile(vaultPath: string, relativePath: string): Promise<string> {
|
|
30
|
+
return Bun.file(`${vaultPath}/${relativePath}`).text();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function grepWikiVault(vaultPath: string, pattern: string): Promise<WikiGrepMatch[]> {
|
|
34
|
+
const regex = new RegExp(pattern);
|
|
35
|
+
const files = await listWikiVault(vaultPath);
|
|
36
|
+
const matches: WikiGrepMatch[] = [];
|
|
37
|
+
for (const file of files) {
|
|
38
|
+
const content = await readWikiVaultFile(vaultPath, file);
|
|
39
|
+
content.split("\n").forEach((text, i) => {
|
|
40
|
+
if (regex.test(text)) {
|
|
41
|
+
matches.push({ path: file, line: i + 1, text });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
return matches;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripPrefix(vaultRelativePath: string, prefix: string): string {
|
|
49
|
+
if (!vaultRelativePath.startsWith(prefix)) {
|
|
50
|
+
throw new Error(`path must start with "${prefix}" (got "${vaultRelativePath}")`);
|
|
51
|
+
}
|
|
52
|
+
return vaultRelativePath.slice(prefix.length);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `vaultRelativePath` must start with `raw/`, matching `vault-cli.ts`'s `write-raw`. */
|
|
56
|
+
export async function writeRawWikiEntry(vaultPath: string, vaultRelativePath: string, body: string): Promise<void> {
|
|
57
|
+
await writeRawEntry(vaultPath, stripPrefix(vaultRelativePath, "raw/"), body);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `vaultRelativePath` must start with `curated/` or `raw/` — nothing else is deletable from here. */
|
|
61
|
+
export async function deleteWikiEntry(vaultPath: string, vaultRelativePath: string): Promise<void> {
|
|
62
|
+
if (vaultRelativePath.startsWith("curated/")) {
|
|
63
|
+
await deleteCuratedEntry(vaultPath, stripPrefix(vaultRelativePath, "curated/"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (vaultRelativePath.startsWith("raw/")) {
|
|
67
|
+
await deleteRawEntry(vaultPath, stripPrefix(vaultRelativePath, "raw/"));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`can only delete curated/ or raw/ entries (got "${vaultRelativePath}")`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Sends `instruction` through a real, short-lived agent turn scoped
|
|
75
|
+
* ONLY to the four wiki tools (`createWikiTools`) — never the full
|
|
76
|
+
* toolset a normal channel gets, so this box can't touch Jira/Bitbucket.
|
|
77
|
+
* Reuses the exact commit path a real conversation would take
|
|
78
|
+
* (`write_file` -> `writeCuratedNote` -> git commit, D-16); this
|
|
79
|
+
* function has no write logic of its own. History is fresh per call,
|
|
80
|
+
* never persisted — this isn't a real session.
|
|
81
|
+
*/
|
|
82
|
+
export async function editWikiViaModel(model: LanguageModel, vaultPath: string, instruction: string): Promise<string> {
|
|
83
|
+
const history = createSessionHistory(async () => "");
|
|
84
|
+
const tools = createWikiTools({ vaultPath, userId: "admin" });
|
|
85
|
+
return runTurn(history, instruction, {
|
|
86
|
+
model,
|
|
87
|
+
tools,
|
|
88
|
+
system:
|
|
89
|
+
"You are Mercury's wiki maintenance assistant, invoked from the admin panel. You can only use the " +
|
|
90
|
+
"wiki tools available to you (list_files, read_file, write_file, grep) — you have no access to Jira, " +
|
|
91
|
+
"Bitbucket, or any other tool. Follow the operator's instruction precisely and report back what you did.",
|
|
92
|
+
});
|
|
93
|
+
}
|