drafted 1.19.17 → 1.19.19

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/cli/drafted.mjs CHANGED
@@ -300,10 +300,24 @@ async function readApiGet(command, apiPath, org) {
300
300
  // A valid session now targets the server it was minted for (see getServerUrl),
301
301
  // so a 401/403 here means the credential is genuinely missing/expired — surface
302
302
  // an actionable next step instead of the bare server "Unauthenticated" string.
303
- if (res.status === 401 || res.status === 403) {
303
+ // THE SERVER'S MESSAGE WINS. This used to replace every 401/403 with "your
304
+ // session is missing or expired — run `drafted login`", which is a guess. A
305
+ // valid, signed-in session that simply had not been NAMED yet returns 403
306
+ // {code:'session_unnamed'} with the exact remedy in the body — and that got
307
+ // discarded and reported as an auth failure, sending readers off to re-run a
308
+ // device-code login that could never fix it. Only invent a message when the
309
+ // server sent none.
310
+ // A bare `{"error":"Unauthenticated"}` explains nothing, so the actionable
311
+ // auth message is genuinely better. A body with a `code` is the server
312
+ // naming a SPECIFIC condition (session_unnamed, and anything added later)
313
+ // and usually carrying its own remedy — replacing that is what sent the last
314
+ // reader off to re-run a login that could not have helped.
315
+ const serverNamedTheReason = typeof data?.code === 'string' && data.code && data.code !== 'unauthenticated';
316
+ if ((res.status === 401 || res.status === 403) && !serverNamedTheReason) {
304
317
  msg = `Not authenticated for ${serverUrl} — your Drafted session is missing or expired. `
305
- + `Run \`drafted login\` to sign in. `
306
- + `The CLI and MCP share ${DEFAULT_AUTH_FILE}, so signing in once works for both.`;
318
+ + `If you are a person at a terminal: run \`drafted login\`. `
319
+ + `If you are an agent: call the Drafted MCP auth tool with action="get_link" and give the URL to your user — you cannot complete a sign-in yourself. `
320
+ + `Either way the approval lands in ${DEFAULT_AUTH_FILE}, which the CLI and MCP share.`;
307
321
  }
308
322
  jsonOut(false, command, msg);
309
323
  console.error(`❌ ${msg}`);
package/mcp/server.mjs CHANGED
@@ -2082,12 +2082,33 @@ async function waitForBootstrapAuth(deadline, staleSid) {
2082
2082
  return null;
2083
2083
  }
2084
2084
 
2085
- if (!isRemote) tool('auth', 'Sign in to Drafted. On a local install the DESKTOP APP is the sign-in surface: both actions open the Drafted app to its sign-in window no device link is shown. `action=login` opens the app and waits for the in-app sign-in to complete; `action=get_link` opens the app and returns immediately (the next Drafted tool call picks up the captured session). A device-code link is used ONLY as a fallback on platforms without the desktop app (the web MCP uses OAuth2, not this tool).', {
2085
+ if (!isRemote) tool('auth', 'Sign in to Drafted.\n\n`action=get_link` ALWAYS returns a sign-in URL. This is the action to use when you are an agent: you cannot complete a sign-in, so put the URL in your reply and stop the human opens it in their own browser, approves, and your next Drafted call picks up the session. Approval also writes ~/.drafted/auth.json, so the `drafted` CLI is signed in too.\n\n`action=login` signs in and WAITS for it to land, opening the desktop app when one is installed. Use it only when a human is watching this terminal.\n\n(The web MCP uses OAuth2 and does not have this tool.)', {
2086
2086
  action: z.enum(['get_link', 'login']).describe('Operation to perform.'),
2087
2087
  }, async ({ action }) => {
2088
2088
  try {
2089
- // Local install open the desktop app's sign-in window (no link). Falls through to the
2090
- // device-code flow below only when no desktop app is installed on this platform.
2089
+ // get_link's ONE job is to return a URL a human can click. It runs before the
2090
+ // already-authenticated check and before the desktop app on purpose:
2091
+ // - an agent cannot complete a sign-in, so handing back a link is the only
2092
+ // safe shape — the human clicks it out of band, in their own browser;
2093
+ // - "already authenticated" is about THIS MCP's session, which says nothing
2094
+ // about the CLI's on-disk credential, so short-circuiting on it used to
2095
+ // make the link unobtainable exactly when it was needed;
2096
+ // - opening the desktop app returns no link at all, which is useless to an
2097
+ // agent that must put something in its reply.
2098
+ // Approval writes ~/.drafted/auth.json, so the CLI picks it up too.
2099
+ if (action === 'get_link') {
2100
+ const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
2101
+ if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
2102
+ const data = await codeRes.json();
2103
+ persistPendingDeviceCode(data);
2104
+ return ok({
2105
+ signInUrl: data.verificationUrl,
2106
+ instruction: 'Give this URL to the user and stop. They open it in their own browser and approve; you cannot complete this for them. Retry your request afterwards — the next Drafted call picks up the approved session.',
2107
+ expiresInSeconds: data.expiresIn ?? null,
2108
+ });
2109
+ }
2110
+ // `login` waits for the sign-in to land, so the desktop app is the better
2111
+ // surface when one is installed: no code to type.
2091
2112
  {
2092
2113
  const activeSession = getState().sessionId;
2093
2114
  const bootstrapSession = getBootstrapSessionId();
@@ -2108,9 +2129,6 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. On a local install the DESKTOP
2108
2129
  }
2109
2130
  const staleSid = getBootstrapSessionId();
2110
2131
  if (await launchDesktopSignin()) {
2111
- if (action === 'get_link') {
2112
- return ok('Opening the Drafted app to sign in — approve in the app window, then retry your request.');
2113
- }
2114
2132
  const sid = await waitForBootstrapAuth(Date.now() + 180000, staleSid);
2115
2133
  if (!sid) throw new Error('Timed out waiting for sign-in. Complete sign-in in the Drafted app window, then retry.');
2116
2134
  getState().sessionId = null;
@@ -2129,13 +2147,6 @@ if (!isRemote) tool('auth', 'Sign in to Drafted. On a local install the DESKTOP
2129
2147
  return ok({ status: 'logged_in', via: 'desktop-app' });
2130
2148
  }
2131
2149
  }
2132
- if (action === 'get_link') {
2133
- const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
2134
- if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
2135
- const data = await codeRes.json();
2136
- persistPendingDeviceCode(data);
2137
- return ok(data.verificationUrl);
2138
- }
2139
2150
  if (action === 'login') {
2140
2151
  // Prefer the active request session (injected by runWithRequestState on
2141
2152
  // remote, or cloneSession on stdio) over the on-disk bootstrap session, so
@@ -3802,7 +3813,7 @@ tool('fs', 'Navigate Drafted like a local filesystem. A FOLDER is the single con
3802
3813
  // Agents reach the repos table through this tool. Content stays in git;
3803
3814
  // Drafted keeps a read-only index. `add`/`rescan` mutate; `list`/`entries`
3804
3815
  // are read-only and paginated with a compact mode (collection rule).
3805
- tool('repo', 'Registered git repos — the org index of .agents/ skills + identities. A connected repo is the source of truth for its skills (git owns content, Drafted renders + indexes). Dispatch by `action`:\n- `list` — the org\'s linked repos (paginated, compact mode).\n\nLINKING AND UNLINKING ARE NOT HERE. A person does them in the Drafted UI (Settings > Organization > GitHub to connect an account, then the folder menu), because linking moves a folder\'s wiki + skills into git and makes Drafted read-only for them. Agents read and rescan; they do not change who owns the content.\n- `rescan` — re-fetch the tracked branch and refresh the index.\n- `entries` — search the index across all the org\'s repos (skills + identities).\n\nAuthoring a Drafted skill whose slug collides with a repo-indexed skill returns 409 repo_owned pointing at the repo — commit to git instead.', {
3816
+ tool('repo', 'Registered git repos — the org index of .agents/ skills + identities. A connected repo is the source of truth for its skills (git owns content, Drafted renders + indexes). Dispatch by `action`:\n- `list` — the org\'s linked repos (paginated, compact mode).\n- `entries` — search the index across all the org\'s repos (skills + identities).\n- `rescan` — re-fetch the tracked branch and refresh the index.\n\nLINKING AND UNLINKING ARE NOT HERE. A person does them in the Drafted UI (Settings > Organization > GitHub to connect an account, then the folder menu), because linking moves a folder\'s wiki + skills into git and makes Drafted read-only for them. Agents read and rescan; they do not change who owns the content.\n\nAuthoring a Drafted skill whose slug collides with a repo-indexed skill returns 409 repo_owned pointing at the repo — commit to git instead.', {
3806
3817
  action: z.enum(['list', 'entries', 'rescan']).describe('list: the org\'s connected repos; entries: search the git index across them; rescan: re-fetch a tracked branch and refresh its index.'),
3807
3818
  slug: z.string().optional().describe('[rescan] the repo slug or UUID.'),
3808
3819
  repoId: z.string().optional().describe('[rescan] repo UUID (alternative to slug).'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.19.17",
3
+ "version": "1.19.19",
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": [