@formstr/mcp 0.3.0 → 0.3.2

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/index.js CHANGED
@@ -24436,8 +24436,21 @@ async function loginNostrConnect(deps, log) {
24436
24436
  perms: NIP46_PERMS
24437
24437
  });
24438
24438
  }
24439
- async function doLogout(signer, pubkey) {
24440
- await signer.logout(pubkey);
24439
+ async function doLogout(signer, target) {
24440
+ if (target) {
24441
+ const match = findAccount(listAccounts(signer), target);
24442
+ if (!match) {
24443
+ throw new Error(
24444
+ `No stored account matching "${target}". Run \`formstr-mcp accounts\` to list them.`
24445
+ );
24446
+ }
24447
+ await signer.logout(match.pubkey);
24448
+ return match;
24449
+ }
24450
+ const active = signer.getActiveAccount();
24451
+ if (!active) return null;
24452
+ await signer.logout(active.pubkey);
24453
+ return active;
24441
24454
  }
24442
24455
  function whoami(signer) {
24443
24456
  return signer.getActiveAccount();
@@ -27521,7 +27534,7 @@ function helpText() {
27521
27534
  "Commands:",
27522
27535
  " run Run the stdio MCP server (default when no command is given).",
27523
27536
  " login Sign in (create / import / bunker URI / QR) and store the key.",
27524
- " logout [npub] Remove a stored account (defaults to the active one).",
27537
+ " logout [npub|hex] Permanently remove a stored account from the keystore (defaults to active).",
27525
27538
  " whoami Print the active account.",
27526
27539
  " accounts List stored accounts ('*' marks the active one).",
27527
27540
  " switch <npub> Set the active account (accepts an npub or hex pubkey).",
@@ -32161,14 +32174,22 @@ function buildSpecRows(formId, name, fields, settings) {
32161
32174
  for (const field of fields) rows.push(buildFieldTag(field));
32162
32175
  return rows;
32163
32176
  }
32164
- function buildEncryptedOuterTags(formId, name, relays, settings) {
32165
- const tags = [["d", formId], ["name", name], ...relays.map((r) => ["relay", r])];
32177
+ function buildParticipantTags(settings) {
32178
+ const tags = [];
32166
32179
  const allowed = settings?.allowedResponders ?? [];
32167
32180
  const pTags = /* @__PURE__ */ new Set([...allowed, ...settings?.collaborators ?? []]);
32168
32181
  for (const pk of allowed) tags.push(["allowed", pk]);
32169
32182
  for (const pk of pTags) tags.push(["p", pk]);
32170
32183
  return tags;
32171
32184
  }
32185
+ function buildEncryptedOuterTags(formId, name, relays, settings) {
32186
+ return [
32187
+ ["d", formId],
32188
+ ["name", name],
32189
+ ...relays.map((r) => ["relay", r]),
32190
+ ...buildParticipantTags(settings)
32191
+ ];
32192
+ }
32172
32193
  async function createForm(params) {
32173
32194
  await signerManager.getSigner();
32174
32195
  const formId = crypto.randomUUID().slice(0, 8);
@@ -32194,7 +32215,12 @@ async function createForm(params) {
32194
32215
  await appendToMyFormsList(signingPubkey, formId, relays[0] ?? "", signingKeyHex, viewKeyHex);
32195
32216
  return { formId, pubkey: signingPubkey, signingKey: signingKeyHex, viewKey: viewKeyHex };
32196
32217
  }
32197
- const tags = [...specRows, ["t", "public"], ...relays.map((r) => ["relay", r])];
32218
+ const tags = [
32219
+ ...specRows,
32220
+ ["t", "public"],
32221
+ ...relays.map((r) => ["relay", r]),
32222
+ ...buildParticipantTags(params.settings)
32223
+ ];
32198
32224
  const event = {
32199
32225
  kind: FORM_KINDS.template,
32200
32226
  created_at: Math.floor(Date.now() / 1e3),
@@ -32514,7 +32540,12 @@ async function updateForm(params) {
32514
32540
  await nostrRuntime.publish(relays, finalizeEvent(event2, signingKeyBytes));
32515
32541
  return;
32516
32542
  }
32517
- const tags = [...specRows, ["t", "public"], ...relays.map((r) => ["relay", r])];
32543
+ const tags = [
32544
+ ...specRows,
32545
+ ["t", "public"],
32546
+ ...relays.map((r) => ["relay", r]),
32547
+ ...buildParticipantTags(settings)
32548
+ ];
32518
32549
  const event = {
32519
32550
  kind: FORM_KINDS.template,
32520
32551
  created_at: Math.floor(Date.now() / 1e3),
@@ -32646,6 +32677,7 @@ function parseResponseEvent(event) {
32646
32677
  var service_exports2 = {};
32647
32678
  __export(service_exports2, {
32648
32679
  addEventToCalendarList: () => addEventToCalendarList,
32680
+ createCalendarEvent: () => createCalendarEvent,
32649
32681
  createCalendarList: () => createCalendarList,
32650
32682
  deleteCalendarEvent: () => deleteCalendarEvent,
32651
32683
  deleteCalendarList: () => deleteCalendarList,
@@ -33076,6 +33108,33 @@ async function publishPrivateCalendarEvent(draft, calendarId) {
33076
33108
  event: signed
33077
33109
  };
33078
33110
  }
33111
+ async function createCalendarEvent(draft, opts = {}) {
33112
+ let calendars = opts.calendars ?? null;
33113
+ const loadCalendars = async () => calendars ?? (calendars = await fetchCalendarLists());
33114
+ let targetCalendarId = draft.calendarId;
33115
+ let targetCalendar;
33116
+ if (targetCalendarId) {
33117
+ targetCalendar = (await loadCalendars()).find((c) => c.id === targetCalendarId);
33118
+ } else if (draft.isPrivate) {
33119
+ targetCalendar = (await loadCalendars())[0];
33120
+ if (!targetCalendar) targetCalendar = await createCalendarList("My Calendar", "#334155");
33121
+ targetCalendarId = targetCalendar.id;
33122
+ }
33123
+ const event = draft.isPrivate ? await publishPrivateCalendarEvent(
33124
+ { ...draft, calendarId: targetCalendarId },
33125
+ targetCalendarId ?? "default"
33126
+ ) : await publishPublicCalendarEvent(draft);
33127
+ let calendar;
33128
+ if (targetCalendar) {
33129
+ const ref = [
33130
+ `${event.kind}:${event.user}:${event.id}`,
33131
+ event.relayHint ?? "",
33132
+ event.viewKey ?? ""
33133
+ ];
33134
+ calendar = await addEventToCalendarList(targetCalendar, ref);
33135
+ }
33136
+ return { event, calendar };
33137
+ }
33079
33138
  async function fetchRelayListsForPubkeys(pubkeys) {
33080
33139
  const result = /* @__PURE__ */ new Map();
33081
33140
  if (pubkeys.length === 0) return result;
@@ -34202,6 +34261,7 @@ async function fetchDocMetadata(address) {
34202
34261
  async function saveDocMetadata(address, patch) {
34203
34262
  const existing = await fetchDocMetadata(address) ?? {};
34204
34263
  const merged = { ...existing, ...patch };
34264
+ if (!Array.isArray(merged.tags)) merged.tags = [];
34205
34265
  const signer = await signerManager.getSigner();
34206
34266
  const content = await nip44SelfEncrypt(signer, JSON.stringify(merged));
34207
34267
  const signed = await signer.signEvent({
@@ -34386,7 +34446,9 @@ async function uploadFile(params) {
34386
34446
  name: params.file.name,
34387
34447
  hash: result.sha256,
34388
34448
  size: params.file.size,
34389
- type: params.file.type,
34449
+ // Browsers report "" for unknown MIME types; coalesce exactly as upstream
34450
+ // formstr-drive's uploadPreparedFile does so the on-wire metadata matches.
34451
+ type: params.file.type || "application/octet-stream",
34390
34452
  folder: params.folder ?? "/",
34391
34453
  uploadedAt: Date.now(),
34392
34454
  server,
@@ -34602,8 +34664,9 @@ async function createPoll(draft) {
34602
34664
  for (const tag of draft.hashtags ?? []) {
34603
34665
  tags.push(["t", tag]);
34604
34666
  }
34605
- if (draft.endsAt) {
34606
- tags.push(["endsAt", String(Math.floor(draft.endsAt.getTime() / 1e3))]);
34667
+ const endsAtSec = draft.endsAt && Number.isFinite(draft.endsAt.getTime()) ? Math.floor(draft.endsAt.getTime() / 1e3) : void 0;
34668
+ if (endsAtSec !== void 0) {
34669
+ tags.push(["endsAt", String(endsAtSec)]);
34607
34670
  }
34608
34671
  const event = {
34609
34672
  kind: POLLS_KINDS.poll,
@@ -34620,7 +34683,7 @@ async function createPoll(draft) {
34620
34683
  pollType: draft.pollType,
34621
34684
  pubkey,
34622
34685
  createdAt: signed.created_at,
34623
- endsAt: draft.endsAt ? Math.floor(draft.endsAt.getTime() / 1e3) : void 0,
34686
+ endsAt: endsAtSec,
34624
34687
  relays,
34625
34688
  hashtags: draft.hashtags ?? [],
34626
34689
  event: signed
@@ -34815,6 +34878,25 @@ function computeResults(responses) {
34815
34878
  return { results, totalVotes: totalVoters };
34816
34879
  }
34817
34880
 
34881
+ // ../agent/src/tools/pubkey.ts
34882
+ function normalizePubkey(value) {
34883
+ const trimmed = value.trim();
34884
+ if (!trimmed) return null;
34885
+ if (/^[0-9a-f]{64}$/i.test(trimmed)) return trimmed.toLowerCase();
34886
+ if (trimmed.startsWith("npub1")) {
34887
+ try {
34888
+ const decoded = nip19_exports.decode(trimmed);
34889
+ if (decoded.type === "npub") return decoded.data;
34890
+ } catch {
34891
+ }
34892
+ }
34893
+ return null;
34894
+ }
34895
+ function normalizePubkeyList(values) {
34896
+ if (!Array.isArray(values)) return [];
34897
+ return values.map((v) => typeof v === "string" ? normalizePubkey(v) : null).filter((p) => !!p);
34898
+ }
34899
+
34818
34900
  // ../agent/src/tools/calendar.ts
34819
34901
  var calendarTools = buildCalendarTools();
34820
34902
  function buildCalendarTools() {
@@ -34854,7 +34936,7 @@ function buildCalendarTools() {
34854
34936
  server.registerTool(
34855
34937
  "create_calendar_event",
34856
34938
  {
34857
- description: "Schedule a calendar event. start/end are ISO 8601. Set isPrivate:true for an encrypted event; participants receive NIP-59 invitations.",
34939
+ description: "Schedule a calendar event. start/end are ISO 8601. Events default to PRIVATE (encrypted) and are linked into a calendar list \u2014 that is the only way they show on calendar.formstr.app, which renders only events referenced in a calendar list. Pass calendarId to choose which calendar; if omitted and the user already has calendars, this tool returns the list so you can ASK the user which one (then re-run with calendarId). Set isPrivate:false for a public, unencrypted event \u2014 note public events do NOT sync to calendar.formstr.app. participants (npub or hex) receive NIP-59 invitations.",
34858
34940
  inputSchema: {
34859
34941
  title: external_exports.string(),
34860
34942
  description: external_exports.string().optional(),
@@ -34862,6 +34944,7 @@ function buildCalendarTools() {
34862
34944
  end: external_exports.string().optional(),
34863
34945
  location: external_exports.string().optional(),
34864
34946
  isPrivate: external_exports.boolean().optional(),
34947
+ calendarId: external_exports.string().optional(),
34865
34948
  participants: external_exports.array(external_exports.string()).optional(),
34866
34949
  rrule: external_exports.string().optional(),
34867
34950
  startTzid: external_exports.string().optional(),
@@ -34869,6 +34952,22 @@ function buildCalendarTools() {
34869
34952
  }
34870
34953
  },
34871
34954
  async (args) => {
34955
+ const isPrivate = args.isPrivate ?? true;
34956
+ const calendars = await service_exports2.fetchCalendarLists();
34957
+ if (isPrivate && !args.calendarId && calendars.length > 0) {
34958
+ const choices = calendars.map((c) => `${c.title} (${c.id})`).join("; ");
34959
+ return fail(
34960
+ `Which calendar should "${args.title}" go in? Re-run create_calendar_event with calendarId set to one of: ${choices}.`,
34961
+ "CALENDAR_REQUIRED"
34962
+ );
34963
+ }
34964
+ if (args.calendarId && !calendars.some((c) => c.id === args.calendarId)) {
34965
+ const available = calendars.map((c) => c.id).join(", ") || "(none)";
34966
+ return fail(
34967
+ `No calendar found for id ${args.calendarId}. Available: ${available}.`,
34968
+ "NOT_FOUND"
34969
+ );
34970
+ }
34872
34971
  const begin = new Date(args.start);
34873
34972
  const end = args.end ? new Date(args.end) : new Date(begin.getTime() + 36e5);
34874
34973
  const draft = {
@@ -34877,19 +34976,26 @@ function buildCalendarTools() {
34877
34976
  begin,
34878
34977
  end,
34879
34978
  location: args.location,
34880
- participants: args.participants,
34881
- isPrivate: Boolean(args.isPrivate),
34979
+ // Accept npub OR hex for each participant — the wire (["p"] tags, NIP-59
34980
+ // invitation wraps, relay-list query) needs hex, so convert here.
34981
+ participants: normalizePubkeyList(args.participants),
34982
+ isPrivate,
34983
+ calendarId: args.calendarId,
34882
34984
  rrule: args.rrule,
34883
34985
  startTzid: args.startTzid,
34884
34986
  registrationFormRef: args.registrationFormRef
34885
34987
  };
34886
- const event = args.isPrivate ? await service_exports2.publishPrivateCalendarEvent(draft, "default") : await service_exports2.publishPublicCalendarEvent(draft);
34988
+ const { event, calendar: list } = await service_exports2.createCalendarEvent(draft, { calendars });
34887
34989
  const coordinate = `${event.kind}:${event.user}:${event.id}`;
34888
- return ok(`Created ${args.isPrivate ? "private" : "public"} event "${args.title}".`, {
34889
- id: event.id,
34890
- eventId: event.eventId,
34891
- coordinate
34892
- });
34990
+ return ok(
34991
+ `Created ${isPrivate ? "private" : "public"} event "${args.title}"${list ? ` in calendar "${list.title}"` : ""}.`,
34992
+ {
34993
+ id: event.id,
34994
+ eventId: event.eventId,
34995
+ coordinate,
34996
+ calendarId: list?.id
34997
+ }
34998
+ );
34893
34999
  }
34894
35000
  );
34895
35001
  server.registerTool(
@@ -34899,7 +35005,8 @@ function buildCalendarTools() {
34899
35005
  inputSchema: { coordinate: external_exports.string() }
34900
35006
  },
34901
35007
  async ({ coordinate }) => {
34902
- const event = await service_exports2.fetchCalendarEventByCoordinate(coordinate);
35008
+ const viewKey = await service_exports2.lookupEventViewKey(coordinate);
35009
+ const event = await service_exports2.fetchCalendarEventByCoordinate(coordinate, viewKey);
34903
35010
  if (!event) return fail(`No event found for ${coordinate}.`, "NOT_FOUND");
34904
35011
  return ok(`Event "${event.title}".`, {
34905
35012
  event: {
@@ -35150,7 +35257,8 @@ function buildCalendarTools() {
35150
35257
  `updates event ${args.coordinate}`
35151
35258
  );
35152
35259
  if (blocked) return blocked;
35153
- const existing = await service_exports2.fetchCalendarEventByCoordinate(args.coordinate);
35260
+ const viewKey = await service_exports2.lookupEventViewKey(args.coordinate);
35261
+ const existing = await service_exports2.fetchCalendarEventByCoordinate(args.coordinate, viewKey);
35154
35262
  if (!existing) return fail(`No event found for ${args.coordinate}.`, "NOT_FOUND");
35155
35263
  const draft = {
35156
35264
  title: args.title ?? existing.title,
@@ -35192,7 +35300,8 @@ function buildCalendarTools() {
35192
35300
  `attaches a form to ${coordinate}`
35193
35301
  );
35194
35302
  if (blocked) return blocked;
35195
- const existing = await service_exports2.fetchCalendarEventByCoordinate(coordinate);
35303
+ const viewKey = await service_exports2.lookupEventViewKey(coordinate);
35304
+ const existing = await service_exports2.fetchCalendarEventByCoordinate(coordinate, viewKey);
35196
35305
  if (!existing) return fail(`No event found for ${coordinate}.`, "NOT_FOUND");
35197
35306
  const draft = {
35198
35307
  title: existing.title,
@@ -35471,23 +35580,6 @@ function buildDriveTools() {
35471
35580
  }
35472
35581
 
35473
35582
  // ../agent/src/tools/shared.ts
35474
- function normalizePubkey(value) {
35475
- const trimmed = value.trim();
35476
- if (!trimmed) return null;
35477
- if (/^[0-9a-f]{64}$/i.test(trimmed)) return trimmed.toLowerCase();
35478
- if (trimmed.startsWith("npub1")) {
35479
- try {
35480
- const decoded = nip19_exports.decode(trimmed);
35481
- if (decoded.type === "npub") return decoded.data;
35482
- } catch {
35483
- }
35484
- }
35485
- return null;
35486
- }
35487
- function normalizePubkeyList(values) {
35488
- if (!Array.isArray(values)) return [];
35489
- return values.map((v) => typeof v === "string" ? normalizePubkey(v) : null).filter((p) => !!p);
35490
- }
35491
35583
  var ANSWER_TYPES = new Set(Object.values(AnswerType));
35492
35584
  var LEGACY_TYPE_ALIASES = {
35493
35585
  multiChoiceGrid: "multipleChoiceGrid" /* multipleChoiceGrid */
@@ -46521,8 +46613,10 @@ async function main() {
46521
46613
  await runLogin(cli);
46522
46614
  return cli.command;
46523
46615
  case "logout": {
46524
- await doLogout(await buildMcpSigner(), cli.account);
46525
- console.error("formstr-mcp: signed out.");
46616
+ const removed = await doLogout(await buildMcpSigner(), cli.target ?? cli.account);
46617
+ console.error(
46618
+ removed ? `formstr-mcp: removed ${removed.npub} (${removed.method}).` : "formstr-mcp: no account to remove."
46619
+ );
46526
46620
  return cli.command;
46527
46621
  }
46528
46622
  case "whoami": {