@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.
@@ -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.3",
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.3",
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",
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)",