drafted 1.19.7 → 1.19.8

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 +99 -7
  2. package/package.json +1 -1
package/mcp/server.mjs CHANGED
@@ -384,11 +384,11 @@ const TOOL_ANNOTATIONS = {
384
384
  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.' },
385
385
 
386
386
  // Identity — read-only introspection of THIS agent's session
387
- whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
387
+ whoami: { title: 'Session identity', readOnlyHint: true, destructiveHint: false, openWorldHint: false, description: 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 (`authState` says whether this session is signed in, and `authNote` names the exact call that fixes it when it is not never diagnose a null userId yourself) — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only. Use this — not guesses from the host environment — to report which session you are.' },
388
388
 
389
389
  // Session naming — the name-before-work gate: every agent session must set a short
390
390
  // name describing the work before any other tool call succeeds.
391
- session: { title: 'Session', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Name THIS agent session (and rename it later). The name is what the user sees on your surface tab — pick a short 2-3 word description of the work (e.g. "beoflow backend", "drafted fs work"). The name persists across reconnects and restarts; you only set it once unless the work changes. Dispatch by `action`: `name` (set/rename with the `name` param).' },
391
+ session: { title: 'Session', readOnlyHint: false, destructiveHint: false, openWorldHint: false, description: 'Name THIS agent session (and rename it later). The name is what the user sees on your surface tab — pick a short 2-3 word description of the work (e.g. "beoflow backend", "drafted fs work"). The name persists across reconnects and restarts; you only set it once unless the work changes. Dispatch by `action`: `name` (set/rename with the `name` param) or `update` (update the npm-installed Drafted MCP on THIS machine when whoami reports mcpUpdate.stale — run it yourself rather than handing the user a shell command; `dryRun` reports without starting it).' },
392
392
 
393
393
  // Comments — the review loop agents could previously only reach over raw HTTP
394
394
  comment: { title: 'Comments', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Read and write review comments on frames. Comments are notes ABOUT THE WORK: they attach to a frame, optionally to one element inside it, and anyone who can see the frame sees them. Dispatch by `action`: list (paginated, compact mode), add (with an optional element anchor or a reply), resolve, reopen, delete. Use this to leave findings a human can action in place, and to read the feedback they left you.' },
@@ -2204,6 +2204,8 @@ async function sessionSurfaceBlock() {
2204
2204
  if (agentSurface) {
2205
2205
  return {
2206
2206
  sessionId,
2207
+ authenticated: !!agentSurface.userId,
2208
+ authState: agentSurface.userId ? 'authenticated' : 'signed_out',
2207
2209
  userId: agentSurface.userId ?? null,
2208
2210
  orgId: agentSurface.orgId ?? null,
2209
2211
  projectId: agentSurface.projectId ?? null,
@@ -2232,6 +2234,7 @@ async function sessionSurfaceBlock() {
2232
2234
  try {
2233
2235
  const res = await serverFetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${cookieSid}` } });
2234
2236
  if (res.ok) me = await res.json();
2237
+ // else: the server ANSWERED and rejected this session — a real sign-out, not a blip.
2235
2238
  } catch (e) {
2236
2239
  // Transport failure, NOT a sign-out. Conflating the two made agents tell
2237
2240
  // users "you're signed out" during a network blip and start needless
@@ -2242,8 +2245,26 @@ async function sessionSurfaceBlock() {
2242
2245
  }
2243
2246
  }
2244
2247
  const nameRequired = !!me?.agentClone && !me?.surfaceName;
2248
+ // A null userId used to come back BARE — no reason, no next step — and agents filled the
2249
+ // gap by inventing one: a real report has an agent telling the user to look for a pending
2250
+ // "device/session approval prompt" in the desktop app (no such prompt exists) and then
2251
+ // waiting on it. Name which of the four states this is and the exact call that ends it.
2252
+ const pending = (!me && !unreachable) ? getPendingDeviceCode() : null;
2253
+ const authState = me?.userId ? 'authenticated'
2254
+ : unreachable ? 'server_unreachable'
2255
+ : pending ? 'awaiting_approval'
2256
+ : cookieSid ? 'session_rejected'
2257
+ : 'signed_out';
2258
+ const AUTH_NOTES = {
2259
+ awaiting_approval: `Sign-in started but NOT yet approved. Give the user this link and ask them to open it: ${pending?.verificationUrl || '(run auth(action="get_link") for a fresh link)'} — this session authenticates itself once they approve; nothing to approve inside the desktop app.`,
2260
+ session_rejected: 'Not signed in: this machine has a stored Drafted session but the server rejected it (expired or revoked). Call auth(action="get_link") and give the user the returned URL. Do NOT ask them to look for an approval prompt in the desktop app — being signed in to the app does not sign THIS agent in.',
2261
+ signed_out: 'Not signed in: no Drafted session on this machine. Call auth(action="get_link") and give the user the returned URL. Do NOT ask them to look for an approval prompt in the desktop app — being signed in to the app does not sign THIS agent in.',
2262
+ };
2245
2263
  return {
2246
2264
  sessionId: cookieSid,
2265
+ authenticated: !!me?.userId,
2266
+ authState,
2267
+ ...(AUTH_NOTES[authState] ? { authNote: AUTH_NOTES[authState] } : {}),
2247
2268
  userId: me?.userId ?? null,
2248
2269
  orgId: me?.currentOrg?.id ?? null,
2249
2270
  projectId: getState().projectId ?? null,
@@ -2254,6 +2275,9 @@ async function sessionSurfaceBlock() {
2254
2275
  color: null,
2255
2276
  surfaced: false,
2256
2277
  alive: false,
2278
+ // `alive` is bare here where it isn't in the acked branch, and a bare false reads as
2279
+ // "something is broken" — it only means the surface WebSocket hasn't acked yet.
2280
+ aliveMeaning: 'The surface WebSocket has not acked this session yet, so no canvas is known to be open. Frame, wiki, and skill writes all persist normally — only focus/presence have nothing to draw on. Do not change what you write because of this, and do not report it as an error.',
2257
2281
  ...(unreachable ? {
2258
2282
  serverUnreachable: true,
2259
2283
  note: unreachableWhy || `Could not reach ${getServerUrl()} — identity UNKNOWN, not signed out. This is a network/transport failure: do not tell the user they are logged out and do not start a new login; retry when connectivity is back.`,
@@ -2285,7 +2309,7 @@ async function getGoogleDriveAvailability() {
2285
2309
  // reachability, and installed-MCP staleness in ONE bootstrap call. The update data is
2286
2310
  // cached (5min), so repeat `whoami` calls are free; the server-side update gate still
2287
2311
  // blocks mutating calls on its own, independent of this tool. Read-only — no state changed.
2288
- tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only.', {}, async () => {
2312
+ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human-readable name (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 (`authState` says whether this session is signed in, and `authNote` names the exact call that fixes it when it is not never diagnose a null userId yourself) — PLUS server health, the installed MCP version/update status (cached ~5min), and `googleDrive` — whether the working org has Google Drive connected: when `googleDrive.connected` is true, strongly prefer Google Workspace frames (.google-doc/.google-sheet/.google-slide) for docs, sheets, and decks; when false they cannot be created at all. Call once per session, right after starting, so a required update surfaces before you act on stale tool behavior. Read-only.', {}, async () => {
2289
2313
  try {
2290
2314
  // Ensure the child clone exists BEFORE reading identity — otherwise the /auth/me
2291
2315
  // fallback (pre-WS-ack) queries the ROOT session and reports the wrong naming state.
@@ -2295,7 +2319,19 @@ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human
2295
2319
  // Tell the agent to actually surface its name to the user — returning `name` in the JSON isn't
2296
2320
  // enough; without an explicit instruction agents rarely say which session they are, so users
2297
2321
  // can't match them to their tab on the Drafted surface.
2298
- const instruction = block.nameRequired
2322
+ // Auth outranks naming: an unauthenticated session has nothing to name, and leaving the
2323
+ // instruction empty is what let agents invent their own explanation for the null ids.
2324
+ // A stale install can now fix ITSELF — say so here, since this is where staleness is
2325
+ // reported and an agent that only reads `stale: true` goes back to the user with a shell
2326
+ // command they didn't ask for.
2327
+ const staleNote = mcpUpdate.stale && mcpUpdate.mode === 'stdio'
2328
+ ? ` This machine's installed Drafted MCP is out of date (${mcpUpdate.currentVersion} → ${mcpUpdate.latestVersion || 'latest'})${mcpUpdate.required ? ' and BELOW the minimum required version, so mutating tools are blocked' : ''}. Call session(action="update") to update it yourself — do not hand the user a shell command.`
2329
+ : '';
2330
+ const instruction = block.authNote
2331
+ ? block.authNote + staleNote
2332
+ : staleNote && !block.nameRequired
2333
+ ? (block.name ? `You are the session named "${block.name}". Tell the user you're "${block.name}" in your reply so they can match you to your tab on the Drafted surface.` : '') + staleNote
2334
+ : block.nameRequired
2299
2335
  ? `You have NO session name yet — the name-before-work gate blocks all other tools until you set one. Name this session by the work you're doing with the user: a short 2-3 word description (suggested: "${block.suggestedName || '...'}"). Set it now with session(action="name", name="<your name>"), then tell the user you're "<name>".`
2300
2336
  : block.name
2301
2337
  ? `You are the session named "${block.name}". Tell the user you're "${block.name}" in your reply so they can match you to your tab on the Drafted surface.`
@@ -2314,13 +2350,69 @@ tool('whoami', 'Return THIS agent session\'s identity: its server-assigned human
2314
2350
  } catch (error) { return err(error); }
2315
2351
  });
2316
2352
 
2353
+ // Self-update: run the installed CLI's own updater so THIS machine's stale stdio MCP can be
2354
+ // refreshed from inside a tool call. Before this, the only path was telling the user to run a
2355
+ // shell command — which a web/Cowork agent cannot do at all, and which a stale install needs
2356
+ // most (an out-of-date MCP is exactly the session least able to ask for help). The updater
2357
+ // mechanics live in ONE place, `drafted update --yes` (cli/drafted.mjs) — do not reimplement
2358
+ // the installer command here, that's the two-writers drift.
2359
+ //
2360
+ // The `drafted` bin is resolved at the FIXED prefix first: the stdio MCP resolves from
2361
+ // ~/.drafted/npm-global, which a non-login shell's PATH does not carry, so bare `drafted`
2362
+ // finds nothing (or worse, a different install) on many machines.
2363
+ function installedDraftedBin() {
2364
+ const prefixed = platform() === 'win32'
2365
+ ? join(homedir(), '.drafted', 'npm-global', 'drafted.cmd')
2366
+ : join(homedir(), '.drafted', 'npm-global', 'bin', 'drafted');
2367
+ return existsSync(prefixed) ? prefixed : 'drafted';
2368
+ }
2369
+
2370
+ async function updateInstalledMcp({ dryRun = false } = {}) {
2371
+ try {
2372
+ const metadata = await getMcpUpdateMetadata();
2373
+ const instructions = buildInstalledMcpUpdateInstructions(metadata);
2374
+ // Hosted HTTP MCP has no local daemon — instructions already say so.
2375
+ if (!instructions.updateSupported) return ok({ ...instructions, action: 'update' });
2376
+ if (dryRun) return ok({ ...instructions, action: 'update', started: false, dryRun: true });
2377
+
2378
+ const bin = installedDraftedBin();
2379
+ const result = await new Promise((resolve) => {
2380
+ execFile(bin, ['update', '--yes', '--json'], { timeout: 60_000 }, (error, stdout) => {
2381
+ if (error) return resolve({ ok: false, error: error.code === 'ENOENT' ? `Drafted CLI not found (looked for ${bin})` : (error.killed ? 'Updater did not start within 60s' : error.message) });
2382
+ try { resolve({ ok: true, data: JSON.parse(stdout) }); }
2383
+ catch { resolve({ ok: true, data: null }); }
2384
+ });
2385
+ });
2386
+
2387
+ if (!result.ok) {
2388
+ return ok({
2389
+ ...instructions,
2390
+ action: 'update',
2391
+ started: false,
2392
+ error: result.error,
2393
+ note: `Could not start the updater from this process (${result.error}). Give the user this command to run in a terminal instead: ${instructions.manualCommand}`,
2394
+ });
2395
+ }
2396
+ return ok({
2397
+ ...instructions,
2398
+ action: 'update',
2399
+ started: true,
2400
+ // Be exact about what did and did not happen: the installer runs DETACHED and does not
2401
+ // replace the already-loaded process, so "updated" would be a lie until a restart.
2402
+ note: 'The updater is running in the background (~30-60s). It does NOT replace this already-running MCP process: tell the user to restart their agent/editor once it finishes, then call whoami to confirm the new mcpVersion.',
2403
+ });
2404
+ } catch (error) { return err(error); }
2405
+ }
2406
+
2317
2407
  // Session naming: set/rename THIS agent session's name — the name-before-work gate stays
2318
2408
  // closed until this succeeds. The name persists server-side (sessions.surface_name), so
2319
2409
  // reconnects and server restarts keep it and the gate never re-fires for a named session.
2320
- tool('session', 'Name THIS agent session (and rename it later). The name is what the user sees on your surface tab pick a short 2-3 word description of the work (e.g. "beoflow backend", "drafted fs work"). The name persists across reconnects and restarts; you only set it once unless the work changes.', {
2321
- action: z.enum(['name']).describe('Operation — currently only `name` (set/rename this session).'),
2410
+ tool('session', 'Manage THIS agent session. `action="name"` sets/renames it the name is what the user sees on your surface tab, so pick a short 2-3 word description of the work (e.g. "beoflow backend"); it persists across reconnects and restarts. `action="update"` updates the npm-installed Drafted MCP on THIS machine when whoami reports mcpUpdate.stale — run it yourself instead of asking the user to run a shell command.', {
2411
+ action: z.enum(['name', 'update']).describe('Operation: `name` (set/rename this session) or `update` (update the installed Drafted MCP on this machine).'),
2322
2412
  name: z.string().optional().describe('[name] the session name — a short 2-3 word description of the work (e.g. "beoflow backend"). Max 5 words / 50 chars.'),
2323
- }, async ({ action, name }) => {
2413
+ dryRun: z.boolean().optional().describe('[update] report what the update would do without starting it.'),
2414
+ }, async ({ action, name, dryRun }) => {
2415
+ if (action === 'update') return updateInstalledMcp({ dryRun });
2324
2416
  if (action !== 'name') return err(new Error(`unknown session action: ${action}`));
2325
2417
  if (!name || !String(name).trim()) return err(new Error('name required — a short 2-3 word description of the work, e.g. session(action="name", name="beoflow backend")'));
2326
2418
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.7",
3
+ "version": "1.19.8",
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": [