@coffer-org/server 2.2.2 → 2.3.1

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.
@@ -161,6 +161,7 @@ export const ThreadMessageSchema = new EntitySchema({
161
161
  role: { type: 'text' },
162
162
  sender: { type: 'text', nullable: true },
163
163
  text: { type: 'text' },
164
+ attachments: { type: 'text', nullable: true },
164
165
  ts: { type: 'integer' },
165
166
  reply_to_id: { type: 'text', nullable: true },
166
167
  },
@@ -16,7 +16,6 @@ export interface McpToolDef {
16
16
  }
17
17
  export interface RagDeps {
18
18
  embeddingApiKey: string;
19
- topK: number;
20
19
  }
21
20
  export declare function formatHits(hits: EmbeddingHit[]): string;
22
21
  export declare function resolveRagDeps(): Promise<RagDeps | null>;
package/dist/mcp-tools.js CHANGED
@@ -16,6 +16,7 @@ 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, topK };
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, topK } = opts.rag;
130
+ const { embeddingApiKey } = opts.rag;
131
131
  out.push({
132
132
  server: 'rag',
133
133
  bareName: 'search_records',
@@ -139,7 +139,7 @@ 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 ?? topK);
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) {
@@ -1,5 +1,7 @@
1
1
  import type { PluginAssetRecord } from './plugin-discovery.ts';
2
- export declare function checkLatestVersion(pkgName: string): Promise<string | null>;
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;
@@ -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)
@@ -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>;
@@ -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 withUpdates = req.query.updates === '1';
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 latestVersion = runtime.local ? null : await checkLatestVersion(runtime.packageName);
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) => {
@@ -3,9 +3,16 @@ export interface StoredMsg {
3
3
  role: 'user' | 'assistant' | 'reasoning';
4
4
  sender: string | null;
5
5
  text: string;
6
+ attachments?: StoredAttachment[];
6
7
  ts: number;
7
8
  replyToId: string | null;
8
9
  }
10
+ export interface StoredAttachment {
11
+ name: string;
12
+ mime?: string;
13
+ size?: number;
14
+ label?: string;
15
+ }
9
16
  export interface ThreadChat {
10
17
  chatId: string;
11
18
  lastTs: number;
@@ -19,6 +26,7 @@ export declare function putThreadMessage(m: {
19
26
  msgId: string;
20
27
  role: 'user' | 'assistant' | 'reasoning';
21
28
  sender?: string | null;
29
+ attachments?: StoredAttachment[];
22
30
  text: string;
23
31
  ts: number;
24
32
  replyToId: string | null;
@@ -1,6 +1,27 @@
1
1
  import { getEm } from "./db.js";
2
2
  function toStored(r) {
3
- return { msgId: r.msg_id, role: r.role, sender: r.sender, text: r.text, ts: r.ts, replyToId: r.reply_to_id };
3
+ let attachments;
4
+ if (r.attachments) {
5
+ try {
6
+ const parsed = JSON.parse(r.attachments);
7
+ if (Array.isArray(parsed)) {
8
+ const valid = parsed.filter((v) => typeof v === 'object' && v !== null && typeof v.name === 'string');
9
+ if (valid.length)
10
+ attachments = valid;
11
+ }
12
+ }
13
+ catch {
14
+ }
15
+ }
16
+ return {
17
+ msgId: r.msg_id,
18
+ role: r.role,
19
+ sender: r.sender,
20
+ text: r.text,
21
+ ...(attachments ? { attachments } : {}),
22
+ ts: r.ts,
23
+ replyToId: r.reply_to_id,
24
+ };
4
25
  }
5
26
  export async function getThreadMessage(connector, chatId, msgId) {
6
27
  const em = getEm().fork();
@@ -15,6 +36,7 @@ export async function putThreadMessage(m) {
15
36
  msg_id: m.msgId,
16
37
  role: m.role,
17
38
  sender: m.sender ?? null,
39
+ attachments: m.attachments?.length ? JSON.stringify(m.attachments) : null,
18
40
  text: m.text,
19
41
  ts: m.ts,
20
42
  reply_to_id: m.replyToId,
package/dist/uploads.d.ts CHANGED
@@ -2,4 +2,14 @@ export declare function uploadsDir(): string;
2
2
  export declare function seedAsset(absPath: string): {
3
3
  name: string;
4
4
  };
5
+ export interface StoredUpload {
6
+ name: string;
7
+ mime?: string;
8
+ size: number;
9
+ }
10
+ export declare function saveUploadBytes(bytes: Uint8Array, opts?: {
11
+ originalName?: string;
12
+ mime?: string;
13
+ maxBytes?: number;
14
+ }): Promise<StoredUpload>;
5
15
  export declare function seedAssetPath(metaUrl: string): (rel: string) => string;
package/dist/uploads.js CHANGED
@@ -1,6 +1,18 @@
1
1
  import { copyFileSync, mkdirSync } from 'node:fs';
2
- import { basename, join } from 'node:path';
2
+ import { writeFile } from 'node:fs/promises';
3
+ import { basename, extname, join } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
3
5
  import { fileURLToPath } from 'node:url';
6
+ const MIME_EXT = {
7
+ 'image/jpeg': '.jpg',
8
+ 'image/png': '.png',
9
+ 'image/gif': '.gif',
10
+ 'image/webp': '.webp',
11
+ 'application/pdf': '.pdf',
12
+ 'text/plain': '.txt',
13
+ 'text/csv': '.csv',
14
+ 'application/json': '.json',
15
+ };
4
16
  export function uploadsDir() {
5
17
  const dir = process.env.UPLOADS ?? join(process.cwd(), 'data', 'uploads');
6
18
  mkdirSync(dir, { recursive: true });
@@ -11,6 +23,17 @@ export function seedAsset(absPath) {
11
23
  copyFileSync(absPath, join(uploadsDir(), name));
12
24
  return { name };
13
25
  }
26
+ export async function saveUploadBytes(bytes, opts = {}) {
27
+ const maxBytes = opts.maxBytes ?? 25 * 1024 * 1024;
28
+ if (bytes.byteLength > maxBytes)
29
+ throw new Error(`upload exceeds ${maxBytes} bytes`);
30
+ const originalExt = opts.originalName ? extname(basename(opts.originalName)).toLowerCase() : '';
31
+ const ext = originalExt && /^[.][a-z0-9]{1,10}$/.test(originalExt) ? originalExt : MIME_EXT[opts.mime ?? ''] ?? '';
32
+ const name = `${randomUUID()}${ext}`;
33
+ await writeFile(join(uploadsDir(), name), bytes);
34
+ const mime = opts.mime === 'application/octet-stream' ? undefined : opts.mime;
35
+ return { name, ...(mime ? { mime } : {}), size: bytes.byteLength };
36
+ }
14
37
  export function seedAssetPath(metaUrl) {
15
38
  return (rel) => fileURLToPath(new URL(`../../seed/assets/${rel}`, metaUrl));
16
39
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffer-org/server",
3
- "version": "2.2.2",
3
+ "version": "2.3.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=24"