@chatpanel/gateway 0.6.37 → 0.6.38
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/package.json +1 -1
- package/src/history-store.js +8 -0
- package/src/mcp.js +17 -6
- package/src/server.js +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/gateway",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.38",
|
|
4
4
|
"description": "Local privacy gateway \u2014 redacts PII out of OpenAI/Anthropic API traffic before it reaches a model, then restores it in the reply. Point opencode, codex, aider, Claude Code, etc. at it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/history-store.js
CHANGED
|
@@ -96,6 +96,14 @@ export class HistoryStore {
|
|
|
96
96
|
return this.records.size;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// The timestamp of the freshest record, so a client can tell how current this warm copy
|
|
100
|
+
// is — the difference between "no such meeting" and "not synced yet".
|
|
101
|
+
get newest() {
|
|
102
|
+
let max = 0;
|
|
103
|
+
for (const r of this.records.values()) if ((r.date || 0) > max) max = r.date || 0;
|
|
104
|
+
return max || null;
|
|
105
|
+
}
|
|
106
|
+
|
|
99
107
|
key() {
|
|
100
108
|
if (!this._key) this._key = loadOrCreateKey();
|
|
101
109
|
return this._key;
|
package/src/mcp.js
CHANGED
|
@@ -61,7 +61,7 @@ async function bridgeJson(path) {
|
|
|
61
61
|
const TOOLS = [
|
|
62
62
|
{
|
|
63
63
|
name: 'search_history',
|
|
64
|
-
description: 'Full-text search the user\'s ChatPanel history — past chats, meeting transcripts, and notes — by keyword relevance.
|
|
64
|
+
description: 'Full-text search the user\'s ChatPanel history — past chats, meeting transcripts, and notes — by keyword relevance. This is a LOCAL WARM COPY that syncs from ChatPanel; very recent items (a meeting from the last few hours) may not be here yet — every result reports how current the index is. If the user is sure something exists and it is not found, it likely has not synced; say so rather than concluding it does not exist. Meeting titles are often generic ("Zoom Meeting"), so search by CONTENT (topics, names, decisions), not the meeting title.',
|
|
65
65
|
inputSchema: {
|
|
66
66
|
type: 'object',
|
|
67
67
|
properties: {
|
|
@@ -82,7 +82,7 @@ const TOOLS = [
|
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
name: 'list_history',
|
|
85
|
-
description: 'List history records (newest first) with their id, title, type and date — no bodies. Use to
|
|
85
|
+
description: 'List history records (newest first) with their id, title, type and date — no bodies. Reports how current this warm copy is (its newest record). Use to see the index horizon and browse/page the corpus.',
|
|
86
86
|
inputSchema: {
|
|
87
87
|
type: 'object',
|
|
88
88
|
properties: {
|
|
@@ -119,6 +119,15 @@ const TOOLS = [
|
|
|
119
119
|
},
|
|
120
120
|
];
|
|
121
121
|
|
|
122
|
+
// A one-line freshness banner from the store's newest record, so every answer states the
|
|
123
|
+
// index horizon — the model can then say "not synced yet" instead of "does not exist".
|
|
124
|
+
function horizonLine(newest, size) {
|
|
125
|
+
if (!newest) return `Index: ${size} records (warm copy synced from ChatPanel).`;
|
|
126
|
+
const d = new Date(newest);
|
|
127
|
+
const iso = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
|
|
128
|
+
return `Index: ${size} records, current through ${iso} (local warm copy — items newer than this may not have synced from ChatPanel yet).`;
|
|
129
|
+
}
|
|
130
|
+
|
|
122
131
|
async function gatewayJson(path, init) {
|
|
123
132
|
let res;
|
|
124
133
|
try {
|
|
@@ -142,8 +151,9 @@ async function callTool(name, args = {}) {
|
|
|
142
151
|
body: JSON.stringify({ query: String(args.query || ''), limit: Number(args.limit) || 10 }),
|
|
143
152
|
});
|
|
144
153
|
const rows = data.results || [];
|
|
145
|
-
|
|
146
|
-
|
|
154
|
+
const horizon = horizonLine(data.newest, data.size);
|
|
155
|
+
if (!rows.length) return `No match for "${args.query}".\n${horizon}\nIf you expected a recent item, it may not have synced yet — check ChatPanel directly, or try broader content keywords (titles are often generic).`;
|
|
156
|
+
return [horizon, '', `${rows.length} result(s) for "${args.query}":`, ...rows.map((r, i) => `${i + 1}. [${r.id}] ${r.title || '(untitled)'} · ${r.type}${r.date ? ' · ' + new Date(r.date).toISOString().slice(0, 10) : ''} · score ${r.score?.toFixed?.(3) ?? r.score}`)].join('\n') + '\n\nUse get_record with an id for the full text.';
|
|
147
157
|
}
|
|
148
158
|
if (name === 'get_record') {
|
|
149
159
|
const data = await gatewayJson(`/v1/history/get?id=${encodeURIComponent(String(args.id || ''))}`);
|
|
@@ -154,8 +164,9 @@ async function callTool(name, args = {}) {
|
|
|
154
164
|
const q = new URLSearchParams({ limit: String(Number(args.limit) || 50), offset: String(Number(args.offset) || 0) });
|
|
155
165
|
const data = await gatewayJson(`/v1/history/list?${q}`);
|
|
156
166
|
const items = data.items || [];
|
|
157
|
-
if (!items.length) return 'History is empty (or the gateway has not been seeded yet).';
|
|
158
|
-
|
|
167
|
+
if (!items.length) return 'History is empty (or the gateway has not been seeded yet — open ChatPanel with warm sync enabled).';
|
|
168
|
+
const newest = items[0]?.date || null;
|
|
169
|
+
return [horizonLine(newest, data.total), '', `${items.length} of ${data.total} records:`, ...items.map((it) => `[${it.id}] ${it.title || '(untitled)'} · ${it.type}${it.date ? ' · ' + new Date(it.date).toISOString().slice(0, 10) : ''} · ${it.chars} chars`)].join('\n');
|
|
159
170
|
}
|
|
160
171
|
if (name === 'list_skills') {
|
|
161
172
|
let data;
|
package/src/server.js
CHANGED
|
@@ -45,7 +45,7 @@ import * as openai from './openai.js';
|
|
|
45
45
|
import * as responses from './responses.js';
|
|
46
46
|
import * as anthropic from './anthropic.js';
|
|
47
47
|
|
|
48
|
-
export const VERSION = '0.6.
|
|
48
|
+
export const VERSION = '0.6.38';
|
|
49
49
|
|
|
50
50
|
// WARM search tier — SQLite + FTS5 record store (falls back to an encrypted-JSON
|
|
51
51
|
// store if SQLite can't load), fed by the extension's ingest sync + backup-ingest.
|
|
@@ -579,13 +579,13 @@ export function createGateway(cfg = loadConfig()) {
|
|
|
579
579
|
try {
|
|
580
580
|
const body = JSON.parse((await readBody(req, cfg.maxBodyBytes)).toString('utf8')) || {};
|
|
581
581
|
const results = historyStore.search(String(body.query || ''), { limit: Number(body.limit) || 10 });
|
|
582
|
-
return sendJson(res, 200, { ok: true, size: historyStore.size, results });
|
|
582
|
+
return sendJson(res, 200, { ok: true, size: historyStore.size, newest: historyStore.newest, results });
|
|
583
583
|
} catch (e) {
|
|
584
584
|
return sendJson(res, 400, { error: { message: `search failed: ${e.message}`, type: 'search_error' } });
|
|
585
585
|
}
|
|
586
586
|
}
|
|
587
587
|
if (pathname === '/v1/history/status' && req.method === 'GET') {
|
|
588
|
-
return sendJson(res, 200, { ok: true, size: historyStore.size });
|
|
588
|
+
return sendJson(res, 200, { ok: true, size: historyStore.size, newest: historyStore.newest });
|
|
589
589
|
}
|
|
590
590
|
if (pathname === '/v1/history/list' && req.method === 'GET') {
|
|
591
591
|
const limit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit')) || 50));
|