@aliyunrds/ctxdb 0.0.10 → 1.0.0-beta.2
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 +4 -4
- package/dist/{chunk-XWPTIFUK.js → chunk-PLWEIEFH.js} +1 -1
- package/dist/{chunk-F25Q3WM4.js → chunk-QHYJ7OXC.js} +1 -0
- package/dist/{chunk-I5HRGFZO.js → chunk-QPFFMW52.js} +2 -2
- package/dist/cli/main.js +46 -7
- package/dist/hooks/session-start.js +3 -3
- package/dist/hooks/stop.js +2 -2
- package/dist/hooks/user-prompt-submit.js +3 -3
- package/dist/opencode/index.js +1008 -0
- package/package.json +2 -2
- package/dist/opencode/src/capture.ts +0 -187
- package/dist/opencode/src/config.ts +0 -160
- package/dist/opencode/src/hooks.ts +0 -252
- package/dist/opencode/src/http-client.ts +0 -147
- package/dist/opencode/src/index.ts +0 -9
- package/dist/opencode/src/kb-catalog.ts +0 -64
- package/dist/opencode/src/recall.ts +0 -87
- package/dist/opencode/src/warmup.ts +0 -51
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
// Minimal HTTP client for the ctxdb server (in-process plugin variant).
|
|
2
|
-
//
|
|
3
|
-
// Stripped down from `@aliyunrds/ctxdb`'s HttpClient: no multipart, no
|
|
4
|
-
// PUT/DELETE, no debug logging — the plugin only ever does:
|
|
5
|
-
//
|
|
6
|
-
// GET /v1/knowledge/knowledge_bases
|
|
7
|
-
// POST /v3/memories/search/
|
|
8
|
-
// POST /v3/memories/add/
|
|
9
|
-
//
|
|
10
|
-
// Per-call timeouts are mandatory (callers pass 5s for recall/KB,
|
|
11
|
-
// 8s for capture). Errors are caught at the hook layer; this client
|
|
12
|
-
// throws on non-2xx but never logs to stderr itself.
|
|
13
|
-
|
|
14
|
-
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
15
|
-
|
|
16
|
-
export class CtxdbHttpError extends Error {
|
|
17
|
-
readonly status: number | null;
|
|
18
|
-
readonly path: string;
|
|
19
|
-
constructor(path: string, status: number | null, message: string) {
|
|
20
|
-
super(message);
|
|
21
|
-
this.name = "CtxdbHttpError";
|
|
22
|
-
this.path = path;
|
|
23
|
-
this.status = status;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface HttpClientOptions {
|
|
28
|
-
baseUrl: string;
|
|
29
|
-
apiKey: string | null;
|
|
30
|
-
userAgent?: string;
|
|
31
|
-
fetchImpl?: typeof fetch;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface RequestOptions {
|
|
35
|
-
timeoutMs?: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export class HttpClient {
|
|
39
|
-
readonly baseUrl: string;
|
|
40
|
-
readonly apiKey: string | null;
|
|
41
|
-
readonly userAgent: string;
|
|
42
|
-
private readonly fetchImpl: typeof fetch;
|
|
43
|
-
|
|
44
|
-
constructor(opts: HttpClientOptions) {
|
|
45
|
-
this.baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
46
|
-
this.apiKey = opts.apiKey;
|
|
47
|
-
this.userAgent = opts.userAgent ?? "ctxdb-opencode-plugin/0.0.0";
|
|
48
|
-
this.fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private headers(contentType?: string): Record<string, string> {
|
|
52
|
-
const h: Record<string, string> = {
|
|
53
|
-
"User-Agent": this.userAgent,
|
|
54
|
-
Connection: "close",
|
|
55
|
-
};
|
|
56
|
-
if (this.apiKey) h.Authorization = `Token ${this.apiKey}`;
|
|
57
|
-
if (contentType) h["Content-Type"] = contentType;
|
|
58
|
-
return h;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
async get(path: string, params?: Record<string, unknown>, options: RequestOptions = {}): Promise<unknown> {
|
|
62
|
-
let url = `${this.baseUrl}${path}`;
|
|
63
|
-
if (params) {
|
|
64
|
-
const qs = new URLSearchParams();
|
|
65
|
-
for (const [k, v] of Object.entries(params)) {
|
|
66
|
-
if (v !== undefined && v !== null) qs.append(k, String(v));
|
|
67
|
-
}
|
|
68
|
-
const s = qs.toString();
|
|
69
|
-
if (s) url = `${url}?${s}`;
|
|
70
|
-
}
|
|
71
|
-
return this.request("GET", url, path, undefined, undefined, options.timeoutMs);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async postJson(
|
|
75
|
-
path: string,
|
|
76
|
-
body: Record<string, unknown> | unknown[],
|
|
77
|
-
options: RequestOptions = {},
|
|
78
|
-
): Promise<unknown> {
|
|
79
|
-
const url = `${this.baseUrl}${path}`;
|
|
80
|
-
return this.request("POST", url, path, JSON.stringify(body), "application/json", options.timeoutMs);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private async request(
|
|
84
|
-
method: string,
|
|
85
|
-
url: string,
|
|
86
|
-
path: string,
|
|
87
|
-
body: string | undefined,
|
|
88
|
-
contentType: string | undefined,
|
|
89
|
-
timeoutMs: number | undefined,
|
|
90
|
-
): Promise<unknown> {
|
|
91
|
-
const effectiveTimeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
92
|
-
const controller = new AbortController();
|
|
93
|
-
const timer = setTimeout(() => controller.abort(), effectiveTimeout);
|
|
94
|
-
try {
|
|
95
|
-
let resp: Response;
|
|
96
|
-
try {
|
|
97
|
-
resp = await this.fetchImpl(url, {
|
|
98
|
-
method,
|
|
99
|
-
headers: this.headers(contentType),
|
|
100
|
-
body,
|
|
101
|
-
signal: controller.signal,
|
|
102
|
-
});
|
|
103
|
-
} catch (err: any) {
|
|
104
|
-
if (err?.name === "AbortError") {
|
|
105
|
-
throw new CtxdbHttpError(path, null, `timeout after ${effectiveTimeout}ms`);
|
|
106
|
-
}
|
|
107
|
-
throw new CtxdbHttpError(path, null, `network error: ${err?.message ?? err}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (resp.status === 204) return {};
|
|
111
|
-
let text: string;
|
|
112
|
-
try {
|
|
113
|
-
text = await resp.text();
|
|
114
|
-
} catch (err: any) {
|
|
115
|
-
throw new CtxdbHttpError(path, resp.status, `body read failed: ${err?.message ?? err}`);
|
|
116
|
-
}
|
|
117
|
-
if (!resp.ok) {
|
|
118
|
-
throw new CtxdbHttpError(path, resp.status, extractDetail(text) || `HTTP ${resp.status}`);
|
|
119
|
-
}
|
|
120
|
-
if (!text) return {};
|
|
121
|
-
try {
|
|
122
|
-
return JSON.parse(text);
|
|
123
|
-
} catch {
|
|
124
|
-
return text;
|
|
125
|
-
}
|
|
126
|
-
} finally {
|
|
127
|
-
clearTimeout(timer);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function extractDetail(text: string): string {
|
|
133
|
-
if (!text) return "";
|
|
134
|
-
try {
|
|
135
|
-
const parsed = JSON.parse(text);
|
|
136
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
137
|
-
for (const k of ["detail", "message", "error"]) {
|
|
138
|
-
const v = (parsed as Record<string, unknown>)[k];
|
|
139
|
-
if (typeof v === "string" && v) return v;
|
|
140
|
-
}
|
|
141
|
-
return JSON.stringify(parsed);
|
|
142
|
-
}
|
|
143
|
-
return String(parsed);
|
|
144
|
-
} catch {
|
|
145
|
-
return text;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// Fetch /v1/knowledge/knowledge_bases (the existing endpoint used by
|
|
2
|
-
// `@aliyunrds/ctxdb`) and render the `<available-knowledge-bases>` block.
|
|
3
|
-
//
|
|
4
|
-
// The agent slug in the embedded `--agent=<X>` hint is hard-coded to
|
|
5
|
-
// "opencode" so the SKILL.md template substitution is consistent with
|
|
6
|
-
// what `ctxdb setup --agent opencode` writes to ~/.config/opencode/skills/.
|
|
7
|
-
|
|
8
|
-
import type { HttpClient } from "./http-client.ts";
|
|
9
|
-
|
|
10
|
-
export const KB_LIST_PATH = "/v1/knowledge/knowledge_bases";
|
|
11
|
-
|
|
12
|
-
interface KnowledgeBase {
|
|
13
|
-
name?: unknown;
|
|
14
|
-
status?: unknown;
|
|
15
|
-
key_entities?: unknown;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function sanitizeKeyEntities(raw: unknown): string[] {
|
|
19
|
-
if (!Array.isArray(raw)) return [];
|
|
20
|
-
const out: string[] = [];
|
|
21
|
-
for (const e of raw) {
|
|
22
|
-
if (typeof e !== "string") continue;
|
|
23
|
-
const cleaned = e.replace(/\s+/g, " ").trim();
|
|
24
|
-
if (cleaned) out.push(cleaned);
|
|
25
|
-
}
|
|
26
|
-
return out;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export async function fetchKbCatalogBlock(
|
|
30
|
-
client: HttpClient,
|
|
31
|
-
timeoutMs: number,
|
|
32
|
-
agent = "opencode",
|
|
33
|
-
): Promise<string> {
|
|
34
|
-
let resp: unknown;
|
|
35
|
-
try {
|
|
36
|
-
resp = await client.get(KB_LIST_PATH, undefined, { timeoutMs });
|
|
37
|
-
} catch {
|
|
38
|
-
return "";
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let kbs: KnowledgeBase[] = [];
|
|
42
|
-
if (Array.isArray(resp)) {
|
|
43
|
-
kbs = resp as KnowledgeBase[];
|
|
44
|
-
} else if (resp && typeof resp === "object") {
|
|
45
|
-
const o = resp as Record<string, unknown>;
|
|
46
|
-
const list = (o.knowledge_bases ?? o.results) as unknown;
|
|
47
|
-
if (Array.isArray(list)) kbs = list as KnowledgeBase[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const active = kbs.filter((kb) => kb.status === "active" && typeof kb.name === "string" && kb.name);
|
|
51
|
-
if (active.length === 0) return "";
|
|
52
|
-
|
|
53
|
-
const lines = active.map((kb) => {
|
|
54
|
-
const ents = sanitizeKeyEntities(kb.key_entities);
|
|
55
|
-
return ents.length > 0 ? `· ${kb.name}: ${ents.join(", ")}` : `· ${kb.name}`;
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
return [
|
|
59
|
-
"<available-knowledge-bases>",
|
|
60
|
-
`When you identify that relevant information may exist in the knowledge bases below, you MUST run \`ctxdb kb search "<query>" --kb=<name> --agent=${agent}\` with targeted keywords after initial analysis to supplement and correct your approach. For keyword-based KB search, keep each single query focused: use at most 5 keywords or short phrases; run multiple targeted searches if more are needed. Knowledge bases:`,
|
|
61
|
-
...lines,
|
|
62
|
-
"</available-knowledge-bases>",
|
|
63
|
-
].join("\n");
|
|
64
|
-
}
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
// Recall path: search /v3/memories/search/ → format via shared
|
|
2
|
-
// formatRecalledMemoriesBlock. Mirrors recall-orchestrator.ts in
|
|
3
|
-
// `@aliyunrds/ctxdb` but stripped to the bare minimum the plugin needs.
|
|
4
|
-
//
|
|
5
|
-
// All callers wrap this in their own try/catch + timeout (5s recall).
|
|
6
|
-
// This module never throws — all failures resolve to an empty result.
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
formatRecalledMemoriesBlock,
|
|
10
|
-
buildExternalKnowledgeBlock,
|
|
11
|
-
type MemoryItem,
|
|
12
|
-
type ExternalContextChunk,
|
|
13
|
-
} from "@aliyunrds/ctxdb-shared";
|
|
14
|
-
|
|
15
|
-
import type { OpencodeConfig } from "./config.ts";
|
|
16
|
-
import type { HttpClient } from "./http-client.ts";
|
|
17
|
-
|
|
18
|
-
export interface RecallResult {
|
|
19
|
-
/** Combined `<recalled-memories>` + `<external-knowledge>` block, or
|
|
20
|
-
* empty string when nothing to inject. */
|
|
21
|
-
block: string;
|
|
22
|
-
memoryCount: number;
|
|
23
|
-
knowledgeChunkCount: number;
|
|
24
|
-
reason: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const EMPTY: RecallResult = { block: "", memoryCount: 0, knowledgeChunkCount: 0, reason: "" };
|
|
28
|
-
|
|
29
|
-
function stripSystemReminders(raw: string): string {
|
|
30
|
-
const cleaned = raw.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
|
|
31
|
-
return cleaned || raw;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function searchAndFormatRecall(
|
|
35
|
-
prompt: string,
|
|
36
|
-
cfg: OpencodeConfig,
|
|
37
|
-
client: HttpClient,
|
|
38
|
-
timeoutMs: number,
|
|
39
|
-
): Promise<RecallResult> {
|
|
40
|
-
if (!prompt.trim()) return { ...EMPTY, reason: "empty_prompt" };
|
|
41
|
-
if (!cfg.baseUrl) return { ...EMPTY, reason: "config_incomplete" };
|
|
42
|
-
|
|
43
|
-
const body: Record<string, unknown> = {
|
|
44
|
-
query: stripSystemReminders(prompt),
|
|
45
|
-
user_id: cfg.userId,
|
|
46
|
-
top_k: cfg.topK,
|
|
47
|
-
threshold: cfg.threshold,
|
|
48
|
-
};
|
|
49
|
-
if (cfg.recallKnowledge) {
|
|
50
|
-
body.knowledge = { enable: true, top_k: cfg.knowledgeTopK };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
let resp: unknown;
|
|
54
|
-
try {
|
|
55
|
-
resp = await client.postJson("/v3/memories/search/", body, { timeoutMs });
|
|
56
|
-
} catch (err) {
|
|
57
|
-
return { ...EMPTY, reason: `http_error: ${(err as Error).message}` };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (!resp || typeof resp !== "object") {
|
|
61
|
-
return { ...EMPTY, reason: "bad_response_shape" };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const r = resp as Record<string, unknown>;
|
|
65
|
-
const memories: MemoryItem[] = Array.isArray(r.results)
|
|
66
|
-
? (r.results as MemoryItem[])
|
|
67
|
-
: Array.isArray(r.memories)
|
|
68
|
-
? (r.memories as MemoryItem[])
|
|
69
|
-
: [];
|
|
70
|
-
const knowledge =
|
|
71
|
-
r.knowledge && typeof r.knowledge === "object"
|
|
72
|
-
? (r.knowledge as { chunks?: ExternalContextChunk[] })
|
|
73
|
-
: null;
|
|
74
|
-
const chunks: ExternalContextChunk[] = Array.isArray(knowledge?.chunks) ? knowledge!.chunks! : [];
|
|
75
|
-
|
|
76
|
-
const blocks: string[] = [];
|
|
77
|
-
if (memories.length > 0) blocks.push(formatRecalledMemoriesBlock(memories, cfg.userId));
|
|
78
|
-
if (chunks.length > 0) blocks.push(buildExternalKnowledgeBlock(chunks, cfg.userId));
|
|
79
|
-
|
|
80
|
-
if (blocks.length === 0) return { ...EMPTY, reason: "nothing_to_inject" };
|
|
81
|
-
return {
|
|
82
|
-
block: blocks.join("\n\n"),
|
|
83
|
-
memoryCount: memories.length,
|
|
84
|
-
knowledgeChunkCount: chunks.length,
|
|
85
|
-
reason: "ok",
|
|
86
|
-
};
|
|
87
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
// Warmup query builder — collects cwd basename + git branch + last 3
|
|
2
|
-
// commit subjects, joins into a single query string. Search is run via
|
|
3
|
-
// the regular recall path (searchAndFormatRecall); only the query
|
|
4
|
-
// shape differs.
|
|
5
|
-
|
|
6
|
-
import { execSync } from "node:child_process";
|
|
7
|
-
import { basename } from "node:path";
|
|
8
|
-
|
|
9
|
-
export interface GitSignals {
|
|
10
|
-
branch: string;
|
|
11
|
-
recentCommits: string[];
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function collectGitSignals(cwd: string): GitSignals {
|
|
15
|
-
const result: GitSignals = { branch: "", recentCommits: [] };
|
|
16
|
-
try {
|
|
17
|
-
result.branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
18
|
-
cwd,
|
|
19
|
-
timeout: 500,
|
|
20
|
-
encoding: "utf-8",
|
|
21
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
22
|
-
}).trim();
|
|
23
|
-
} catch {
|
|
24
|
-
// not a git repo / no git
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
const log = execSync("git log --oneline -3 --no-decorate", {
|
|
28
|
-
cwd,
|
|
29
|
-
timeout: 500,
|
|
30
|
-
encoding: "utf-8",
|
|
31
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
32
|
-
}).trim();
|
|
33
|
-
if (log) {
|
|
34
|
-
result.recentCommits = log.split("\n").map((l) => {
|
|
35
|
-
const idx = l.indexOf(" ");
|
|
36
|
-
return idx > 0 ? l.slice(idx + 1) : l;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
} catch {
|
|
40
|
-
// same
|
|
41
|
-
}
|
|
42
|
-
return result;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function buildWarmupQuery(cwd: string, git: GitSignals): string {
|
|
46
|
-
const project = basename(cwd) || "unknown";
|
|
47
|
-
const parts: string[] = [`project: ${project}`];
|
|
48
|
-
if (git.branch) parts.push(`branch: ${git.branch}`);
|
|
49
|
-
if (git.recentCommits.length > 0) parts.push(`recent work: ${git.recentCommits.join("; ")}`);
|
|
50
|
-
return parts.join(", ");
|
|
51
|
-
}
|