@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chatpanel/gateway",
3
- "version": "0.6.37",
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": {
@@ -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. Use this to recall what was discussed or written when the current context does not already contain it.',
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 browse or page the corpus.',
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
- if (!rows.length) return `No matching history for: ${args.query}`;
146
- return [`${rows.length} result(s) for "${args.query}" (of ${data.size} indexed):`, ...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.';
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
- return [`${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');
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.37';
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));