@scales-baby/nest-bridge 1.0.3 → 1.0.4
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/crypto/contentSchema.js +8 -0
- package/dist/data.js +22 -0
- package/dist/mcp.js +140 -1
- package/manifest.json +15 -1
- package/package.json +1 -1
|
@@ -113,6 +113,14 @@ const route = {
|
|
|
113
113
|
clear: ["name", "city", "status", "date", "tags", "tagHashes"],
|
|
114
114
|
blankWith: {},
|
|
115
115
|
};
|
|
116
|
+
// NOMAD RESPONSE / FOUNDER LEAD ----------------------------------------------
|
|
117
|
+
// NOTE: these public research surveys are deliberately NOT in CONTENT_SCHEMA.
|
|
118
|
+
// Their Mongoose models carry no `enc` scaffold; the server stores them
|
|
119
|
+
// CLEARTEXT and serves them operator-only (GET is gated, POST is public). The
|
|
120
|
+
// bridge therefore exposes them READ-ONLY as a straight pass-through (see
|
|
121
|
+
// DataLayer.listReadonly / getReadonly) and never routes them through the
|
|
122
|
+
// encrypt/decrypt path. Keeping them out of CONTENT_SCHEMA is what makes this
|
|
123
|
+
// schema byte-identical with the Nest web app's contentSchema.ts.
|
|
116
124
|
exports.CONTENT_SCHEMA = {
|
|
117
125
|
person,
|
|
118
126
|
task,
|
package/dist/data.js
CHANGED
|
@@ -22,6 +22,10 @@ const PATHS = {
|
|
|
22
22
|
merchant: "/api/merchants",
|
|
23
23
|
route: "/api/routes",
|
|
24
24
|
};
|
|
25
|
+
const READONLY_PATHS = {
|
|
26
|
+
nomad: "/api/nomad",
|
|
27
|
+
founder: "/api/founder",
|
|
28
|
+
};
|
|
25
29
|
class DataLayer {
|
|
26
30
|
client;
|
|
27
31
|
vault;
|
|
@@ -117,5 +121,23 @@ class DataLayer {
|
|
|
117
121
|
await this.ensureUnlocked();
|
|
118
122
|
return this.client.get("/api/digest");
|
|
119
123
|
}
|
|
124
|
+
// --- READ-ONLY SURVEYS (cleartext pass-through) --------------------------
|
|
125
|
+
// List a survey collection. No decrypt: these docs are always cleartext.
|
|
126
|
+
async listReadonly(coll, query = {}) {
|
|
127
|
+
await this.ensureUnlocked();
|
|
128
|
+
const qs = Object.entries(query)
|
|
129
|
+
.filter(([, v]) => v !== undefined && v !== "")
|
|
130
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
|
131
|
+
.join("&");
|
|
132
|
+
const base = READONLY_PATHS[coll];
|
|
133
|
+
const path = qs ? `${base}?${qs}` : base;
|
|
134
|
+
return (await this.client.get(path)) ?? [];
|
|
135
|
+
}
|
|
136
|
+
// Get one survey row by _id. There is no per-id REST route for surveys, so we
|
|
137
|
+
// fetch the (operator-scoped) list and find the row locally.
|
|
138
|
+
async getReadonly(coll, id) {
|
|
139
|
+
const rows = await this.listReadonly(coll);
|
|
140
|
+
return rows.find((r) => String(r._id) === id) ?? null;
|
|
141
|
+
}
|
|
120
142
|
}
|
|
121
143
|
exports.DataLayer = DataLayer;
|
package/dist/mcp.js
CHANGED
|
@@ -86,7 +86,7 @@ async function buildServer(ctx) {
|
|
|
86
86
|
const { McpServer } = await loadSdk();
|
|
87
87
|
const server = new McpServer({
|
|
88
88
|
name: "nest-bridge",
|
|
89
|
-
version: "1.0.
|
|
89
|
+
version: "1.0.4",
|
|
90
90
|
});
|
|
91
91
|
// Descriptions are registered ONCE at startup, before the background unlock
|
|
92
92
|
// finishes — so this note can't depend on the (not-yet-known) unlock state.
|
|
@@ -308,6 +308,145 @@ async function buildServer(ctx) {
|
|
|
308
308
|
researchNotes: zod_1.z.string().optional(),
|
|
309
309
|
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
310
310
|
}, "Update an event.");
|
|
311
|
+
// ---- MERCHANTS ----------------------------------------------------------
|
|
312
|
+
// Shared field shape (loose strings/enums, mirroring the person/company tools)
|
|
313
|
+
// matching lib/crmValidation MerchantCreate/PatchSchema. notes + the contact/
|
|
314
|
+
// owner free-text fields are encrypted; the rest is cleartext metadata.
|
|
315
|
+
const merchantBase = {
|
|
316
|
+
zone: zod_1.z.string().optional(),
|
|
317
|
+
address: zod_1.z.string().optional(),
|
|
318
|
+
type: zod_1.z
|
|
319
|
+
.string()
|
|
320
|
+
.optional()
|
|
321
|
+
.describe("fnb_cafe|fnb_restaurant|fnb_bar|retail|hotel|coworking|wellness|other"),
|
|
322
|
+
paymentRails: zod_1.z.array(zod_1.z.string()).optional(),
|
|
323
|
+
foreignCustomerMix: zod_1.z.string().optional().describe("low|medium|high|unknown"),
|
|
324
|
+
visitStatus: zod_1.z
|
|
325
|
+
.string()
|
|
326
|
+
.optional()
|
|
327
|
+
.describe("not_visited|visited_cold|visited_pitched|signage_placed|activated|declined"),
|
|
328
|
+
lastVisit: zod_1.z.string().optional().describe("ISO date"),
|
|
329
|
+
preloadedCardGiven: zod_1.z.boolean().optional(),
|
|
330
|
+
preloadedCardAmount: zod_1.z.number().optional(),
|
|
331
|
+
contactPerson: zod_1.z.string().optional(),
|
|
332
|
+
contactInfo: zod_1.z.string().optional(),
|
|
333
|
+
ambassador: zod_1.z.string().optional().describe("Person _id"),
|
|
334
|
+
notes: zod_1.z.string().optional(),
|
|
335
|
+
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
336
|
+
source: zod_1.z.string().optional(),
|
|
337
|
+
qrInterested: zod_1.z.string().optional().describe("not_offered|yes|maybe|no"),
|
|
338
|
+
qrDeliveryStatus: zod_1.z.string().optional().describe("not_offered|pending|shipped|delivered"),
|
|
339
|
+
qrShippingContact: zod_1.z.string().optional(),
|
|
340
|
+
voiceMemoUrl: zod_1.z.string().optional(),
|
|
341
|
+
voiceMemoPath: zod_1.z.string().optional(),
|
|
342
|
+
intakeSubmittedAt: zod_1.z.string().optional().describe("ISO date"),
|
|
343
|
+
foreignPaymentIssues: zod_1.z.string().optional().describe("unknown|yes|sometimes|no"),
|
|
344
|
+
ownerName: zod_1.z.string().optional(),
|
|
345
|
+
paymentRanking: zod_1.z
|
|
346
|
+
.array(zod_1.z.object({ rail: zod_1.z.string(), rank: zod_1.z.number().int() }))
|
|
347
|
+
.optional(),
|
|
348
|
+
promptpayReasons: zod_1.z.array(zod_1.z.string()).optional(),
|
|
349
|
+
promptpayReasonOther: zod_1.z.string().optional(),
|
|
350
|
+
usdBalanceInterest: zod_1.z.string().optional(),
|
|
351
|
+
dailyVolume: zod_1.z.string().optional(),
|
|
352
|
+
foreignCustomerRegions: zod_1.z.array(zod_1.z.string()).optional(),
|
|
353
|
+
cryptoFamiliarity: zod_1.z.string().optional(),
|
|
354
|
+
customerCryptoAskFrequency: zod_1.z.string().optional(),
|
|
355
|
+
bestContactTime: zod_1.z.array(zod_1.z.string()).optional(),
|
|
356
|
+
yearsInBusiness: zod_1.z.string().optional(),
|
|
357
|
+
topPaymentMethod: zod_1.z.string().optional(),
|
|
358
|
+
ownerOrigin: zod_1.z.string().optional(),
|
|
359
|
+
foreignPaymentIssueTypes: zod_1.z.array(zod_1.z.string()).optional(),
|
|
360
|
+
customerForeignPayAsk: zod_1.z.string().optional(),
|
|
361
|
+
crossBorderPayOut: zod_1.z.string().optional(),
|
|
362
|
+
};
|
|
363
|
+
registerList("list_merchants", "merchant", "List merchants (cafe/shop field visits). Filter by status.");
|
|
364
|
+
registerGet("get_merchant", "merchant", "Get one merchant by id, with notes.");
|
|
365
|
+
registerSearch("search_merchants", "merchant", ["name", "city", "notes"], "Search merchants by name/city/notes.");
|
|
366
|
+
registerCreate("create_merchant", "merchant", {
|
|
367
|
+
name: zod_1.z.string().describe("Shop name"),
|
|
368
|
+
city: zod_1.z.string().describe("City"),
|
|
369
|
+
...merchantBase,
|
|
370
|
+
}, "Create a merchant.");
|
|
371
|
+
registerUpdate("update_merchant", "merchant", {
|
|
372
|
+
name: zod_1.z.string().optional(),
|
|
373
|
+
city: zod_1.z.string().optional(),
|
|
374
|
+
...merchantBase,
|
|
375
|
+
}, "Update a merchant (only the fields you pass change).");
|
|
376
|
+
// ---- ROUTES -------------------------------------------------------------
|
|
377
|
+
// Ordered walking/meeting routes. No encrypted fields today (stop notes live
|
|
378
|
+
// per-stop in cleartext); mirror lib/crmValidation RouteCreate/PatchSchema.
|
|
379
|
+
const routeStop = zod_1.z.object({
|
|
380
|
+
merchantId: zod_1.z.string().optional().describe("Merchant _id"),
|
|
381
|
+
name: zod_1.z.string().optional(),
|
|
382
|
+
address: zod_1.z.string().optional(),
|
|
383
|
+
googleMapsUrl: zod_1.z.string().optional(),
|
|
384
|
+
order: zod_1.z.number().int(),
|
|
385
|
+
walkingMinFromPrev: zod_1.z.number().optional(),
|
|
386
|
+
notes: zod_1.z.string().optional(),
|
|
387
|
+
visited: zod_1.z.boolean().optional(),
|
|
388
|
+
visitedAt: zod_1.z.string().optional().describe("ISO date"),
|
|
389
|
+
});
|
|
390
|
+
const routeBase = {
|
|
391
|
+
zone: zod_1.z.string().optional(),
|
|
392
|
+
session: zod_1.z.string().optional(),
|
|
393
|
+
date: zod_1.z.string().optional().describe("ISO date"),
|
|
394
|
+
ownerPersonId: zod_1.z.string().optional().describe("Person _id"),
|
|
395
|
+
status: zod_1.z.string().optional().describe("planned|in_progress|complete"),
|
|
396
|
+
stops: zod_1.z.array(routeStop).optional(),
|
|
397
|
+
isPublic: zod_1.z.boolean().optional(),
|
|
398
|
+
tags: zod_1.z.array(zod_1.z.string()).optional(),
|
|
399
|
+
source: zod_1.z.string().optional(),
|
|
400
|
+
};
|
|
401
|
+
registerList("list_routes", "route", "List routes. Filter by status.");
|
|
402
|
+
registerGet("get_route", "route", "Get one route by id, with its stops.");
|
|
403
|
+
registerSearch("search_routes", "route", ["name", "city"], "Search routes by name/city.");
|
|
404
|
+
registerCreate("create_route", "route", {
|
|
405
|
+
name: zod_1.z.string().describe("Route name"),
|
|
406
|
+
city: zod_1.z.string().describe("City"),
|
|
407
|
+
...routeBase,
|
|
408
|
+
}, "Create a route.");
|
|
409
|
+
registerUpdate("update_route", "route", {
|
|
410
|
+
name: zod_1.z.string().optional(),
|
|
411
|
+
city: zod_1.z.string().optional(),
|
|
412
|
+
...routeBase,
|
|
413
|
+
}, "Update a route (only the fields you pass change).");
|
|
414
|
+
// ---- SURVEYS (read-only) ------------------------------------------------
|
|
415
|
+
// Public research surveys (nomad travelers + company founders). READ-ONLY:
|
|
416
|
+
// submissions arrive via the public web forms, not the AI. These collections
|
|
417
|
+
// are stored CLEARTEXT server-side (operator-only GET), so the bridge serves
|
|
418
|
+
// them as a straight pass-through — no decrypt, no encrypted-vs-clear split.
|
|
419
|
+
function registerSurveyList(name, coll, desc) {
|
|
420
|
+
server.registerTool(name, {
|
|
421
|
+
description: desc,
|
|
422
|
+
inputSchema: { limit: zod_1.z.number().int().min(1).max(1000).optional() },
|
|
423
|
+
}, async (args) => {
|
|
424
|
+
try {
|
|
425
|
+
const rows = await ctx.data.listReadonly(coll, args);
|
|
426
|
+
return jsonResult({ count: rows.length, items: rows });
|
|
427
|
+
}
|
|
428
|
+
catch (e) {
|
|
429
|
+
return errResult(e);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function registerSurveyGet(name, coll, desc) {
|
|
434
|
+
server.registerTool(name, { description: desc, inputSchema: { id: zod_1.z.string().describe("The record's _id") } }, async (args) => {
|
|
435
|
+
try {
|
|
436
|
+
const row = await ctx.data.getReadonly(coll, args.id);
|
|
437
|
+
if (!row)
|
|
438
|
+
return errResult(new Error("not_found"));
|
|
439
|
+
return jsonResult(row);
|
|
440
|
+
}
|
|
441
|
+
catch (e) {
|
|
442
|
+
return errResult(e);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
registerSurveyList("list_nomad_responses", "nomad", "List nomad/traveler payment-research survey responses (newest first), with contact + free-text answer.");
|
|
447
|
+
registerSurveyGet("get_nomad_response", "nomad", "Get one nomad survey response by id, with contact + free-text answer.");
|
|
448
|
+
registerSurveyList("list_founder_leads", "founder", "List founder/company payroll-research survey leads (newest first), with contact + free-text answer.");
|
|
449
|
+
registerSurveyGet("get_founder_lead", "founder", "Get one founder survey lead by id, with contact + free-text answer.");
|
|
311
450
|
// ---- DIGEST -------------------------------------------------------------
|
|
312
451
|
server.registerTool("get_digest", {
|
|
313
452
|
description: "Get the actionable digest (overdue/today/this-week follow-ups + open tasks)." +
|
package/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"manifest_version": "0.3",
|
|
3
3
|
"name": "scales-nest",
|
|
4
4
|
"display_name": "Nest by SCALES",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.4",
|
|
6
6
|
"description": "Read and write your end-to-end-encrypted Nest through your own AI. Your encryption key is derived locally and never leaves your machine; Nest's servers only ever see ciphertext.",
|
|
7
7
|
"long_description": "Nest is your encrypted second brain (people, companies, tasks, events). This connector runs a small MCP server on your own machine. It fetches your password-wrapped key from Nest, unwraps it locally, then decrypts on read and encrypts on write right here. The Nest server only ever stores ciphertext and never receives your password, your key, or your plaintext. Mint a scoped API key in Nest (Settings then Connect your AI), paste it plus your encryption password below, and your AI can read and (with a full-control key) write your Nest data.",
|
|
8
8
|
"author": {
|
|
@@ -47,6 +47,20 @@
|
|
|
47
47
|
{ "name": "search_events", "description": "Search events." },
|
|
48
48
|
{ "name": "create_event", "description": "Create an event." },
|
|
49
49
|
{ "name": "update_event", "description": "Update an event." },
|
|
50
|
+
{ "name": "list_merchants", "description": "List merchants (field visits), decrypted locally." },
|
|
51
|
+
{ "name": "get_merchant", "description": "Get one merchant by id, decrypted locally." },
|
|
52
|
+
{ "name": "search_merchants", "description": "Search merchants by name/city/notes." },
|
|
53
|
+
{ "name": "create_merchant", "description": "Create a merchant; encrypted locally before it is stored." },
|
|
54
|
+
{ "name": "update_merchant", "description": "Update a merchant; re-encrypted locally before it is stored." },
|
|
55
|
+
{ "name": "list_routes", "description": "List walking/meeting routes." },
|
|
56
|
+
{ "name": "get_route", "description": "Get one route by id, with its stops." },
|
|
57
|
+
{ "name": "search_routes", "description": "Search routes by name/city." },
|
|
58
|
+
{ "name": "create_route", "description": "Create a route." },
|
|
59
|
+
{ "name": "update_route", "description": "Update a route." },
|
|
60
|
+
{ "name": "list_nomad_responses", "description": "List nomad/traveler payment-research survey responses (read-only)." },
|
|
61
|
+
{ "name": "get_nomad_response", "description": "Get one nomad survey response by id (read-only)." },
|
|
62
|
+
{ "name": "list_founder_leads", "description": "List founder/company payroll-research survey leads (read-only)." },
|
|
63
|
+
{ "name": "get_founder_lead", "description": "Get one founder survey lead by id (read-only)." },
|
|
50
64
|
{ "name": "get_digest", "description": "Get your follow-up digest (overdue and upcoming)." }
|
|
51
65
|
],
|
|
52
66
|
"keywords": ["crm", "encrypted", "mcp", "nest", "scales", "second-brain"],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scales-baby/nest-bridge",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Local MCP bridge for Nest. Read and write your end-to-end-encrypted Nest data through your own AI; the encryption key is derived locally from your password and never leaves your machine.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "SCALES (https://nest.scales.baby)",
|