drafted 1.11.37 → 1.12.0

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.
Files changed (2) hide show
  1. package/mcp/server.mjs +149 -17
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -215,6 +215,9 @@ const TOOL_ANNOTATIONS = {
215
215
  // Auth — initiates external browser / email flows
216
216
  auth: { title: 'Sign in', readOnlyHint: false, destructiveHint: false, openWorldHint: true, description: 'Sign in to Drafted. `action=get_link` returns a URL immediately and starts background approval polling; after the user opens the link, later Drafted tool calls also auto-consume the approved login. `action=login` opens a browser when needed and explicitly waits/polls for approval.' },
217
217
 
218
+ // Identity — read-only introspection of THIS agent's session
219
+ whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name + emoji (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
220
+
218
221
  // Projects
219
222
  project: { title: 'Projects', readOnlyHint: false, destructiveHint: false, openWorldHint: false, widgetUri: 'ui://widget/drafted-canvas-overview.html', description: 'Manage projects: list (start here), open (switch active project), create, update, move to another org.' },
220
223
  get_org: { title: 'Organization', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Get the active organization (action="get", default), Google Drive availability, or switch to a different org (action="switch", orgId=...). Use switch when you need wiki/skill work in an org that has no projects — opening a project also switches, but is unavailable in empty orgs. When googleDrive.connected is true, strongly prefer Google Workspace frames for documents, sheets, and slides.' },
@@ -515,26 +518,89 @@ function mcpMode() {
515
518
  return process.argv.includes('--http') ? 'http' : 'stdio';
516
519
  }
517
520
 
521
+ // ponytail: reports that fail to send (e.g. server unreachable) queue here on disk
522
+ // instead of dropping silently, and get retried on the next tool call.
523
+ const PENDING_REPORTS_PATH = () => join(homedir(), '.drafted', 'pending-reports.jsonl');
524
+ const MAX_PENDING_REPORTS = 50;
525
+
526
+ function readPendingReports() {
527
+ try {
528
+ return readFileSync(PENDING_REPORTS_PATH(), 'utf8')
529
+ .split('\n')
530
+ .filter(Boolean)
531
+ .map((line) => { try { return JSON.parse(line); } catch { return null; } })
532
+ .filter(Boolean);
533
+ } catch {
534
+ return [];
535
+ }
536
+ }
537
+
538
+ function writePendingReports(reports) {
539
+ const reportsPath = PENDING_REPORTS_PATH();
540
+ if (!reports.length) {
541
+ try { unlinkSync(reportsPath); } catch { /* nothing to remove */ }
542
+ return;
543
+ }
544
+ try {
545
+ mkdirSync(dirname(reportsPath), { recursive: true });
546
+ writeFileSync(reportsPath, reports.slice(-MAX_PENDING_REPORTS).map((r) => JSON.stringify(r)).join('\n') + '\n', { mode: 0o600 });
547
+ } catch { /* best effort */ }
548
+ }
549
+
550
+ async function flushPendingReports(serverUrl) {
551
+ const pending = readPendingReports();
552
+ if (!pending.length) return;
553
+ const stillPending = [];
554
+ for (const body of pending) {
555
+ try {
556
+ await fetch(`${serverUrl}/api/installations/report`, {
557
+ method: 'POST',
558
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
559
+ body: JSON.stringify(body),
560
+ });
561
+ } catch {
562
+ stillPending.push(body);
563
+ }
564
+ }
565
+ writePendingReports(stillPending);
566
+ }
567
+
568
+ // Serializes all report sends/flushes so rapid back-to-back tool calls (the common
569
+ // case during an outage, when a client retries repeatedly) don't race on a
570
+ // read-modify-write of the same pending-reports file and drop queued events.
571
+ let installReportChain = Promise.resolve();
572
+
573
+ async function sendInstallationReport(serverUrl, body) {
574
+ await flushPendingReports(serverUrl);
575
+ try {
576
+ await fetch(`${serverUrl}/api/installations/report`, {
577
+ method: 'POST',
578
+ headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
579
+ body: JSON.stringify(body),
580
+ });
581
+ } catch {
582
+ writePendingReports([...readPendingReports(), body]);
583
+ }
584
+ }
585
+
518
586
  function reportInstallationEvent(event, extra = {}) {
519
587
  const info = getInstallInfo();
520
588
  if (!info) return;
521
- fetch(`${getServerUrl()}/api/installations/report`, {
522
- method: 'POST',
523
- headers: { 'Content-Type': 'application/json', 'User-Agent': `Drafted MCP/${PACKAGE_VERSION}` },
524
- body: JSON.stringify({
525
- installId: info.installId,
526
- event,
527
- schemaVersion: 1,
528
- cliVersion: PACKAGE_VERSION,
529
- osFamily: osFamily(),
530
- osVersion: osRelease().slice(0, 60),
531
- arch: normalizedArch(),
532
- nodeVersion: process.version,
533
- mcpMode: mcpMode(),
534
- source: 'mcp',
535
- ...extra,
536
- }),
537
- }).catch(() => {});
589
+ const serverUrl = getServerUrl();
590
+ const body = {
591
+ installId: info.installId,
592
+ event,
593
+ schemaVersion: 1,
594
+ cliVersion: PACKAGE_VERSION,
595
+ osFamily: osFamily(),
596
+ osVersion: osRelease().slice(0, 60),
597
+ arch: normalizedArch(),
598
+ nodeVersion: process.version,
599
+ mcpMode: mcpMode(),
600
+ source: 'mcp',
601
+ ...extra,
602
+ };
603
+ installReportChain = installReportChain.then(() => sendInstallationReport(serverUrl, body)).catch(() => {});
538
604
  }
539
605
 
540
606
  function classifyMcpError(error) {
@@ -1001,6 +1067,10 @@ function withProjectBreadcrumb(result) {
1001
1067
 
1002
1068
  let agentWs = null;
1003
1069
  let agentWsReconnectTimer = null;
1070
+ // Cached agent-hello-ack: this agent's own surface identity (name/emoji/alive) as
1071
+ // assigned by the server. The playful name is the correlation key between an agent
1072
+ // window and its web-app session tab; whoami reads it from here.
1073
+ let agentSurface = null;
1004
1074
 
1005
1075
  function setMcpActiveProject(projectId, meta = null) {
1006
1076
  const s = getState();
@@ -1052,6 +1122,21 @@ async function connectAgentWs() {
1052
1122
  }
1053
1123
  });
1054
1124
 
1125
+ agentWs.on('message', (raw) => {
1126
+ // agent-hello-ack carries this agent's server-assigned surface identity back on the
1127
+ // owning connection. Cache it so whoami can report the name without a prod WS probe.
1128
+ try {
1129
+ const m = JSON.parse(raw.toString());
1130
+ if (m.type === 'agent-hello-ack') {
1131
+ agentSurface = {
1132
+ sessionId: m.sessionId, userId: m.userId, orgId: m.orgId, projectId: m.projectId,
1133
+ name: m.name, emoji: m.emoji, alive: m.alive, surfaced: true,
1134
+ capturedAt: Date.now(),
1135
+ };
1136
+ }
1137
+ } catch { /* ignore non-JSON / unexpected */ }
1138
+ });
1139
+
1055
1140
  agentWs.on('close', () => {
1056
1141
  console.error('[MCP-WS] Disconnected, reconnecting in 5s...');
1057
1142
  agentWs = null;
@@ -1426,6 +1511,53 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a ver
1426
1511
  } catch (error) { return err(error); }
1427
1512
  });
1428
1513
 
1514
+ // Identity: report THIS agent session's own surface identity. The name/emoji come from
1515
+ // the agent-hello-ack cached on the WS; falls back to /auth/me for userId/org if the WS
1516
+ // ack hasn't landed yet. Read-only — no state changed.
1517
+ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name + emoji (the correlation key between an agent window and its web-app session tab), sessionId, userId, orgId, active projectId, editor label, server URL, and surfaced/alive state. Read-only.', {}, async () => {
1518
+ try {
1519
+ const server = getServerUrl();
1520
+ const editor = (process.env.DRAFTED_AGENT_NAME || '').trim() || null;
1521
+ const base = {
1522
+ server,
1523
+ editor,
1524
+ agentLabel: getAgentLabel(),
1525
+ sessionId: agentSurface?.sessionId || getState().sessionId || null,
1526
+ };
1527
+ if (agentSurface) {
1528
+ return ok({
1529
+ ...base,
1530
+ userId: agentSurface.userId ?? null,
1531
+ orgId: agentSurface.orgId ?? null,
1532
+ projectId: agentSurface.projectId ?? null,
1533
+ name: agentSurface.name,
1534
+ emoji: agentSurface.emoji,
1535
+ surfaced: true,
1536
+ alive: !!agentSurface.alive,
1537
+ });
1538
+ }
1539
+ // No WS ack yet — best-effort identity from /auth/me so callers still get a userId/org.
1540
+ const sid = base.sessionId || getBootstrapSessionId();
1541
+ let me = null;
1542
+ if (sid) {
1543
+ try {
1544
+ const res = await fetch(`${server}/auth/me`, { headers: { Cookie: `gc_session=${sid}` } });
1545
+ if (res.ok) me = await res.json();
1546
+ } catch { /* not yet authenticated */ }
1547
+ }
1548
+ return ok({
1549
+ ...base,
1550
+ userId: me?.userId ?? null,
1551
+ orgId: me?.currentOrg?.id ?? null,
1552
+ projectId: getState().projectId ?? null,
1553
+ name: null,
1554
+ emoji: null,
1555
+ surfaced: false,
1556
+ alive: false,
1557
+ });
1558
+ } catch (error) { return err(error); }
1559
+ });
1560
+
1429
1561
  // ── Project management tools (direct HTTP) ────────────────────────
1430
1562
 
1431
1563
  tool('project', 'START HERE for project management. Dispatch by `action`: list (lists all projects across all orgs — always call first), open (switch the active project; required before reading/writing frames), create (new project, optionally from a template), update (change name/folder/description/layers), move (transfer to another org). Opening a project auto-switches the org. To change orgs WITHOUT a project (for wiki/skill work in an empty org), use get_org(action="switch", orgId=...). **Skill gate:** projects with attached skills will REJECT all mutations (write, edit, mv, rm, shape, group, connector, layout, layer, asset upload) until you have loaded each attached skill via skill(action="load"). Skills tell you HOW to do the work — they\'re not optional. Open returns the attached skill list and auto-inlines content for projects with ≤3 skills.', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.37",
3
+ "version": "1.12.0",
4
4
  "description": "Drafted — visual thinking surface for humans and AI agents. Renders HTML, markdown, images, and code as frames on a zoomable canvas, with MCP tools for AI agents and real-time sync for humans.",
5
5
  "type": "module",
6
6
  "files": [