@coffer-org/server 2.3.0 → 2.3.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/dist/embed-openai.d.ts +14 -0
- package/dist/embed-openai.js +65 -6
- package/dist/mcp-tools.d.ts +0 -1
- package/dist/mcp-tools.js +6 -6
- package/dist/plugin-updates.d.ts +3 -1
- package/dist/plugin-updates.js +2 -2
- package/dist/plugins-api.d.ts +1 -1
- package/dist/plugins-api.js +9 -6
- package/package.json +1 -1
package/dist/embed-openai.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
export declare const EMBED_MODEL = "text-embedding-3-small";
|
|
2
2
|
export declare const EMBED_DIM = 1024;
|
|
3
|
+
export type EmbeddingFailureReason = 'billing' | 'auth' | 'rate_limit' | 'server' | 'network' | 'unknown';
|
|
4
|
+
export declare class OpenAIEmbeddingError extends Error {
|
|
5
|
+
readonly status: number | null;
|
|
6
|
+
readonly code: string | null;
|
|
7
|
+
readonly reason: EmbeddingFailureReason;
|
|
8
|
+
constructor(args: {
|
|
9
|
+
status?: number | null;
|
|
10
|
+
code?: string | null;
|
|
11
|
+
detail?: string;
|
|
12
|
+
reason?: EmbeddingFailureReason;
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function embeddingFailureMessage(error: unknown): string;
|
|
3
17
|
export declare function embedBatch(texts: string[], apiKey: string, fetchImpl?: typeof fetch): Promise<{
|
|
4
18
|
vectors: number[][];
|
|
5
19
|
tokens: number;
|
package/dist/embed-openai.js
CHANGED
|
@@ -1,19 +1,78 @@
|
|
|
1
1
|
const OPENAI_URL = 'https://api.openai.com/v1/embeddings';
|
|
2
2
|
export const EMBED_MODEL = 'text-embedding-3-small';
|
|
3
3
|
export const EMBED_DIM = 1024;
|
|
4
|
+
export class OpenAIEmbeddingError extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
reason;
|
|
8
|
+
constructor(args) {
|
|
9
|
+
const status = args.status ?? null;
|
|
10
|
+
const suffix = args.detail ? `: ${args.detail}` : '';
|
|
11
|
+
super(`openai embeddings${status == null ? '' : ` ${status}`}${suffix}`, { cause: args.cause });
|
|
12
|
+
this.name = 'OpenAIEmbeddingError';
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = args.code ?? null;
|
|
15
|
+
this.reason = args.reason ?? classifyEmbeddingFailure(status, args.code, args.detail);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function parseErrorBody(body) {
|
|
19
|
+
try {
|
|
20
|
+
const value = JSON.parse(body);
|
|
21
|
+
const code = typeof value.error?.code === 'string' ? value.error.code : null;
|
|
22
|
+
const message = typeof value.error?.message === 'string' ? value.error.message : '';
|
|
23
|
+
if (code || message)
|
|
24
|
+
return { code, detail: message || code || 'request failed' };
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
}
|
|
28
|
+
return { code: null, detail: body.slice(0, 200) || 'request failed' };
|
|
29
|
+
}
|
|
30
|
+
function classifyEmbeddingFailure(status, code, detail) {
|
|
31
|
+
const marker = `${code ?? ''} ${detail ?? ''}`.toLowerCase();
|
|
32
|
+
if (status === 402 || /insufficient_quota|billing|payment_required|payment required|hard.?limit|quota exceeded/.test(marker))
|
|
33
|
+
return 'billing';
|
|
34
|
+
if (status === 401 || status === 403)
|
|
35
|
+
return 'auth';
|
|
36
|
+
if (status === 429 || /rate.?limit/.test(marker))
|
|
37
|
+
return 'rate_limit';
|
|
38
|
+
if (status != null && status >= 500)
|
|
39
|
+
return 'server';
|
|
40
|
+
return 'unknown';
|
|
41
|
+
}
|
|
42
|
+
export function embeddingFailureMessage(error) {
|
|
43
|
+
if (!(error instanceof OpenAIEmbeddingError))
|
|
44
|
+
return error instanceof Error ? error.message : String(error);
|
|
45
|
+
switch (error.reason) {
|
|
46
|
+
case 'billing':
|
|
47
|
+
return 'OpenAI billing or quota is unavailable. RAG indexing/search is paused; records are still saved and indexing will retry after billing is restored.';
|
|
48
|
+
case 'auth':
|
|
49
|
+
return 'The OpenAI API key was rejected. RAG indexing/search is unavailable; records are still saved.';
|
|
50
|
+
case 'rate_limit':
|
|
51
|
+
return 'OpenAI rate limit reached. RAG indexing/search will retry; records are still saved.';
|
|
52
|
+
default:
|
|
53
|
+
return `OpenAI embeddings are unavailable${error.status == null ? '' : ` (HTTP ${error.status})`}. RAG will retry; records are still saved.`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
4
56
|
export async function embedBatch(texts, apiKey, fetchImpl = fetch) {
|
|
5
57
|
if (texts.length === 0)
|
|
6
58
|
return { vectors: [], tokens: 0 };
|
|
7
59
|
if (!apiKey)
|
|
8
60
|
throw new Error('embeddings: missing API key (OPENAI_API_KEY)');
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
61
|
+
let res;
|
|
62
|
+
try {
|
|
63
|
+
res = await fetchImpl(OPENAI_URL, {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
66
|
+
body: JSON.stringify({ model: EMBED_MODEL, input: texts, dimensions: EMBED_DIM }),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (cause) {
|
|
70
|
+
throw new OpenAIEmbeddingError({ reason: 'network', detail: 'network request failed', cause });
|
|
71
|
+
}
|
|
14
72
|
if (!res.ok) {
|
|
15
73
|
const body = await res.text().catch(() => '');
|
|
16
|
-
|
|
74
|
+
const parsed = parseErrorBody(body);
|
|
75
|
+
throw new OpenAIEmbeddingError({ status: res.status, code: parsed.code, detail: parsed.detail });
|
|
17
76
|
}
|
|
18
77
|
const json = (await res.json());
|
|
19
78
|
const vectors = [...json.data].sort((a, b) => a.index - b.index).map((d) => d.embedding);
|
package/dist/mcp-tools.d.ts
CHANGED
package/dist/mcp-tools.js
CHANGED
|
@@ -11,11 +11,12 @@ import { describeCondition } from '@coffer-org/sdk/condition';
|
|
|
11
11
|
import { getEm } from "./db.js";
|
|
12
12
|
import { getPluginSettings } from "./plugin-runtime.js";
|
|
13
13
|
import { searchEmbeddings } from "./embeddings.js";
|
|
14
|
-
import { embedOne } from "./embed-openai.js";
|
|
14
|
+
import { embedOne, embeddingFailureMessage } from "./embed-openai.js";
|
|
15
15
|
import { getLogger } from "./log.js";
|
|
16
16
|
import { writePluginSettings, listSettings } from "./settings-write.js";
|
|
17
17
|
import { ValidationError, NotFoundError } from "./mutate.js";
|
|
18
18
|
const log = getLogger('mcp-tools');
|
|
19
|
+
const DEFAULT_RAG_TOP_K = 5;
|
|
19
20
|
export function formatHits(hits) {
|
|
20
21
|
if (hits.length === 0)
|
|
21
22
|
return 'No matching records.';
|
|
@@ -31,10 +32,9 @@ export async function resolveRagDeps() {
|
|
|
31
32
|
const db = (await getPluginSettings('claude-agent'));
|
|
32
33
|
const enabled = db['rag_enabled'] !== false;
|
|
33
34
|
const embeddingApiKey = (process.env.OPENAI_API_KEY ?? db['openai_api_key'] ?? '');
|
|
34
|
-
const topK = 5;
|
|
35
35
|
if (!enabled || !embeddingApiKey)
|
|
36
36
|
return null;
|
|
37
|
-
return { embeddingApiKey
|
|
37
|
+
return { embeddingApiKey };
|
|
38
38
|
}
|
|
39
39
|
export async function collectMcpTools(opts = {}) {
|
|
40
40
|
const out = [];
|
|
@@ -127,7 +127,7 @@ export async function collectMcpTools(opts = {}) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
if (opts.rag) {
|
|
130
|
-
const { embeddingApiKey
|
|
130
|
+
const { embeddingApiKey } = opts.rag;
|
|
131
131
|
out.push({
|
|
132
132
|
server: 'rag',
|
|
133
133
|
bareName: 'search_records',
|
|
@@ -139,11 +139,11 @@ export async function collectMcpTools(opts = {}) {
|
|
|
139
139
|
handler: async (args) => {
|
|
140
140
|
try {
|
|
141
141
|
const { vector } = await embedOne(args.query, embeddingApiKey);
|
|
142
|
-
const hits = await searchEmbeddings(vector, args.k ??
|
|
142
|
+
const hits = await searchEmbeddings(vector, args.k ?? DEFAULT_RAG_TOP_K);
|
|
143
143
|
return { content: [{ type: 'text', text: formatHits(hits) }] };
|
|
144
144
|
}
|
|
145
145
|
catch (e) {
|
|
146
|
-
return fail(`
|
|
146
|
+
return fail(`RAG unavailable: ${embeddingFailureMessage(e)} Use the regular coffer tools (list_records/get_record) instead.`);
|
|
147
147
|
}
|
|
148
148
|
},
|
|
149
149
|
});
|
package/dist/plugin-updates.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { PluginAssetRecord } from './plugin-discovery.ts';
|
|
2
|
-
export declare function checkLatestVersion(pkgName: string
|
|
2
|
+
export declare function checkLatestVersion(pkgName: string, opts?: {
|
|
3
|
+
force?: boolean;
|
|
4
|
+
}): Promise<string | null>;
|
|
3
5
|
export type UpdateTarget = {
|
|
4
6
|
ok: true;
|
|
5
7
|
packageName: string;
|
package/dist/plugin-updates.js
CHANGED
|
@@ -14,12 +14,12 @@ function readCache(raw) {
|
|
|
14
14
|
return null;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
-
export async function checkLatestVersion(pkgName) {
|
|
17
|
+
export async function checkLatestVersion(pkgName, opts = {}) {
|
|
18
18
|
const key = stateKey(pkgName);
|
|
19
19
|
let cached = null;
|
|
20
20
|
try {
|
|
21
21
|
cached = readCache(await getPluginState(STATE_PLUGIN, key));
|
|
22
|
-
if (cached && Date.now() - cached.checkedAt < TTL_MS)
|
|
22
|
+
if (!opts.force && cached && Date.now() - cached.checkedAt < TTL_MS)
|
|
23
23
|
return cached.latestVersion;
|
|
24
24
|
const res = await fetch(`https://registry.npmjs.org/${pkgName}/latest`, { signal: AbortSignal.timeout(5000) });
|
|
25
25
|
if (!res.ok)
|
package/dist/plugins-api.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export interface PluginListEntry {
|
|
|
15
15
|
web?: string;
|
|
16
16
|
css?: string;
|
|
17
17
|
}
|
|
18
|
-
export declare function buildPluginListResponse(plugins: PluginManifest[], assets: PluginAssetRecord[], disabled: Set<string>, withUpdates?: boolean): Promise<PluginListEntry[]>;
|
|
18
|
+
export declare function buildPluginListResponse(plugins: PluginManifest[], assets: PluginAssetRecord[], disabled: Set<string>, withUpdates?: boolean, forceUpdates?: boolean): Promise<PluginListEntry[]>;
|
|
19
19
|
export declare function registerPluginsApi(app: FastifyInstance): Promise<void>;
|
package/dist/plugins-api.js
CHANGED
|
@@ -5,14 +5,14 @@ import { getPlugins, readDisabled } from "./plugin-runtime.js";
|
|
|
5
5
|
import { checkLatestVersion } from "./plugin-updates.js";
|
|
6
6
|
const nmRoot = () => join(process.cwd(), 'node_modules');
|
|
7
7
|
const ASSET_KEY = { 'schema.js': 'schema', 'web.js': 'web', 'web.css': 'css' };
|
|
8
|
-
export async function buildPluginListResponse(plugins, assets, disabled, withUpdates = true) {
|
|
8
|
+
export async function buildPluginListResponse(plugins, assets, disabled, withUpdates = true, forceUpdates = false) {
|
|
9
9
|
const assetById = new Map(assets.map((a) => [a.id, a]));
|
|
10
10
|
return Promise.all(plugins.map(async (p) => {
|
|
11
11
|
const a = assetById.get(p.id);
|
|
12
12
|
return {
|
|
13
13
|
id: p.id,
|
|
14
14
|
installedVersion: a?.version ?? p.version,
|
|
15
|
-
latestVersion: withUpdates && a && !a.local ? await checkLatestVersion(a.packageName) : null,
|
|
15
|
+
latestVersion: withUpdates && a && !a.local ? await checkLatestVersion(a.packageName, { force: forceUpdates }) : null,
|
|
16
16
|
packageName: a?.packageName ?? null,
|
|
17
17
|
dependsOn: p.dependsOn,
|
|
18
18
|
enabled: !disabled.has(p.id),
|
|
@@ -27,15 +27,18 @@ export async function buildPluginListResponse(plugins, assets, disabled, withUpd
|
|
|
27
27
|
}
|
|
28
28
|
export async function registerPluginsApi(app) {
|
|
29
29
|
app.get('/api/plugins', async (req) => {
|
|
30
|
-
const
|
|
30
|
+
const query = req.query;
|
|
31
|
+
const withUpdates = query.updates === '1';
|
|
32
|
+
const forceUpdates = query.refresh === '1';
|
|
31
33
|
const [plugins, assets, disabled] = await Promise.all([getPlugins(), discoverPluginAssets(), readDisabled()]);
|
|
32
|
-
return buildPluginListResponse(plugins, assets, disabled, withUpdates);
|
|
34
|
+
return buildPluginListResponse(plugins, assets, disabled, withUpdates, forceUpdates);
|
|
33
35
|
});
|
|
34
|
-
app.get('/api/runtime', async () => {
|
|
36
|
+
app.get('/api/runtime', async (req) => {
|
|
35
37
|
const runtime = await discoverRuntime();
|
|
36
38
|
if (!runtime)
|
|
37
39
|
return { installedVersion: null, latestVersion: null };
|
|
38
|
-
const
|
|
40
|
+
const forceUpdates = req.query.refresh === '1';
|
|
41
|
+
const latestVersion = runtime.local ? null : await checkLatestVersion(runtime.packageName, { force: forceUpdates });
|
|
39
42
|
return { installedVersion: runtime.installedVersion, latestVersion };
|
|
40
43
|
});
|
|
41
44
|
app.get('/plugins/:id/:file', async (req, reply) => {
|