drafted 1.11.10 → 1.11.12

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/cli/drafted.mjs +60 -15
  2. package/package.json +1 -1
package/cli/drafted.mjs CHANGED
@@ -23,7 +23,12 @@ const __dirname = dirname(__filename);
23
23
  const DEFAULT_STATE_DIR = join(homedir(), '.drafted');
24
24
  const DEFAULT_PROJECTS_FILE = join(DEFAULT_STATE_DIR, 'projects.json');
25
25
  const DEFAULT_PID_FILE = join(DEFAULT_STATE_DIR, 'server.pid');
26
- const DEFAULT_AUTH_FILE = join(DEFAULT_STATE_DIR, 'auth.json');
26
+ // Honor DRAFTED_AUTH_FILE so the CLI reads the SAME credential file the MCP does.
27
+ // install-mcp.sh's --local mode points the MCP at auth.local.json via this env var;
28
+ // without honoring it here the CLI would read auth.json while the MCP used a
29
+ // different file, and the two would disagree about who is signed in (DRAFT-32).
30
+ const DEFAULT_AUTH_FILE = process.env.DRAFTED_AUTH_FILE || join(DEFAULT_STATE_DIR, 'auth.json');
31
+ const DEFAULT_CONFIG_FILE = join(DEFAULT_STATE_DIR, 'config.json');
27
32
  const DEFAULT_PORT = 3477;
28
33
  const PACKAGE_VERSION = (() => {
29
34
  try {
@@ -133,11 +138,37 @@ function isServerRunning() {
133
138
  }
134
139
  }
135
140
 
136
- // Helper: Get server URL (supports DRAFTED_SERVER env var or --server flag)
141
+ // Helper: Read the install's server URL from ~/.drafted/config.json. This is the
142
+ // SAME file the MCP server reads (written by install-mcp.sh), so the CLI and MCP
143
+ // resolve to the same deployment.
144
+ function readConfigServer() {
145
+ try {
146
+ if (existsSync(DEFAULT_CONFIG_FILE)) {
147
+ const cfg = JSON.parse(readFileSync(DEFAULT_CONFIG_FILE, 'utf8'));
148
+ return cfg.server || cfg.publicUrl || null;
149
+ }
150
+ } catch { /* ignore unreadable/corrupt config */ }
151
+ return null;
152
+ }
153
+
154
+ // Helper: Get server URL.
155
+ // Precedence mirrors the MCP so a CLI invocation transparently targets the SAME
156
+ // server the MCP (and the stored session) use — fixing DRAFT-32, where a CLI
157
+ // holding a valid cloud session defaulted to localhost and got "Unauthenticated":
158
+ // 1. DRAFTED_SERVER env / --server flag (explicit override; --server sets the env var)
159
+ // 2. the auth file's `server` field — where the stored session is actually valid
160
+ // 3. ~/.drafted/config.json (written by install-mcp.sh; the file the MCP reads)
161
+ // 4. localhost (no cloud install configured)
162
+ // (2) is preferred over (3) because the session cookie is only valid on the
163
+ // server it was minted for; sending it anywhere else just yields a 401.
137
164
  function getServerUrl() {
138
165
  if (process.env.DRAFTED_SERVER) {
139
166
  return process.env.DRAFTED_SERVER.replace(/\/$/, '');
140
167
  }
168
+ const fromAuth = readAuth()?.server;
169
+ if (fromAuth) return String(fromAuth).replace(/\/$/, '');
170
+ const fromConfig = readConfigServer();
171
+ if (fromConfig) return String(fromConfig).replace(/\/$/, '');
141
172
  return `http://localhost:${process.env.DRAFTED_PORT || DEFAULT_PORT}`;
142
173
  }
143
174
 
@@ -234,14 +265,17 @@ function withQuery(base, params) {
234
265
  }
235
266
 
236
267
  // GET a Drafted API path with the stored session and return parsed JSON.
237
- // Strictly read-only: callers only ever issue GETs through this seam. On any
238
- // failure it emits a structured error (for --json) and exits non-zero.
239
- async function readApiGet(command, apiPath) {
268
+ // Strictly read-only: callers only ever issue GETs through this seam. An
269
+ // optional --org override is forwarded as the X-Drafted-Org header (the server
270
+ // validates it against the caller's memberships). On any failure it emits a
271
+ // structured error (for --json) and exits non-zero.
272
+ async function readApiGet(command, apiPath, org) {
240
273
  requireLogin();
241
274
  const serverUrl = getServerUrl();
275
+ const headers = org ? { 'X-Drafted-Org': org } : {};
242
276
  let res;
243
277
  try {
244
- res = await authFetch(`${serverUrl}${apiPath}`);
278
+ res = await authFetch(`${serverUrl}${apiPath}`, { headers });
245
279
  } catch (err) {
246
280
  jsonOut(false, command, `Could not reach server at ${serverUrl}: ${err.message}`);
247
281
  console.error(`❌ Could not reach server at ${serverUrl}: ${err.message}`);
@@ -249,7 +283,15 @@ async function readApiGet(command, apiPath) {
249
283
  }
250
284
  const data = await res.json().catch(() => ({}));
251
285
  if (!res.ok) {
252
- const msg = data.error || `HTTP ${res.status}`;
286
+ let msg = data.error || `HTTP ${res.status}`;
287
+ // A valid session now targets the server it was minted for (see getServerUrl),
288
+ // so a 401/403 here means the credential is genuinely missing/expired — surface
289
+ // an actionable next step instead of the bare server "Unauthenticated" string.
290
+ if (res.status === 401 || res.status === 403) {
291
+ msg = `Not authenticated for ${serverUrl} — your Drafted session is missing or expired. `
292
+ + `Run \`drafted login\` to sign in. `
293
+ + `The CLI and MCP share ${DEFAULT_AUTH_FILE}, so signing in once works for both.`;
294
+ }
253
295
  jsonOut(false, command, msg);
254
296
  console.error(`❌ ${msg}`);
255
297
  process.exit(1);
@@ -1145,10 +1187,9 @@ program
1145
1187
  //
1146
1188
  // Project scoping: --project <id> selects the project (passed through as a query
1147
1189
  // param). When omitted, the server resolves the caller's active project. Org is
1148
- // the logged-in session's active org identical to `add`. (Cross-org reads
1149
- // require switching org first; a per-request --org override is a deliberate
1150
- // follow-up, not added here to avoid implying access the /api/fs/ routes don't
1151
- // yet scope.)
1190
+ // the logged-in session's active org unless --org <id|name> is given, which the
1191
+ // server validates against the caller's memberships and scopes per-request via
1192
+ // the X-Drafted-Org header without mutating the shared session's active org.
1152
1193
 
1153
1194
  // Command: ls (alias: list) — list layers / lanes / frames
1154
1195
  program
@@ -1156,6 +1197,7 @@ program
1156
1197
  .alias('list')
1157
1198
  .description('List layers, lanes, or frames in a project (read-only)')
1158
1199
  .option('--project <id>', 'Target project ID (defaults to your active project)')
1200
+ .option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
1159
1201
  .option('--recursive', 'Recurse into subdirectories (forces summary mode)')
1160
1202
  .option('--summary', 'Include size, updatedAt, and title for frames')
1161
1203
  .option('--pattern <glob>', 'Filter frame filenames (e.g. "*.html")')
@@ -1163,7 +1205,7 @@ program
1163
1205
  const params = { path: path || '/', projectId: options.project, pattern: options.pattern };
1164
1206
  if (options.recursive) { params.recursive = 'true'; params.summary = 'true'; }
1165
1207
  else if (options.summary) params.summary = 'true';
1166
- const data = await readApiGet('ls', withQuery('/api/fs/', params));
1208
+ const data = await readApiGet('ls', withQuery('/api/fs/', params), options.org);
1167
1209
  jsonOut(true, 'ls', data);
1168
1210
  printLs(data);
1169
1211
  });
@@ -1174,6 +1216,7 @@ program
1174
1216
  .aliases(['get', 'cat'])
1175
1217
  .description("Read a frame's content (HTML/text/markdown), or binary metadata, by path / frame URL / ID (read-only)")
1176
1218
  .option('--project <id>', 'Target project ID (defaults to your active project)')
1219
+ .option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
1177
1220
  .option('--lines <range>', 'Line range to read, e.g. "1-40" or "20"')
1178
1221
  .action(async (pathOrId, options) => {
1179
1222
  let apiPath;
@@ -1184,7 +1227,7 @@ program
1184
1227
  console.error(`❌ ${err.message}`);
1185
1228
  process.exit(1);
1186
1229
  }
1187
- const data = await readApiGet('read', apiPath);
1230
+ const data = await readApiGet('read', apiPath, options.org);
1188
1231
  jsonOut(true, 'read', data);
1189
1232
  if (data.type === 'binary') {
1190
1233
  console.log(`# ${data.path} (binary)`);
@@ -1206,10 +1249,11 @@ assetCmd
1206
1249
  .command('list')
1207
1250
  .description('List supporting assets (CSS/JS/images/fonts) in a project')
1208
1251
  .requiredOption('--project <id>', 'Target project ID')
1252
+ .option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
1209
1253
  .option('--frame <id>', 'Only assets associated with this frame ID')
1210
1254
  .action(async (options) => {
1211
1255
  const apiPath = withQuery(`/api/projects/${encodeURIComponent(options.project)}/assets`, { frameId: options.frame });
1212
- const data = await readApiGet('asset-list', apiPath);
1256
+ const data = await readApiGet('asset-list', apiPath, options.org);
1213
1257
  jsonOut(true, 'asset-list', data);
1214
1258
  const assets = data.assets || [];
1215
1259
  if (assets.length === 0) { console.log('No assets'); return; }
@@ -1223,8 +1267,9 @@ assetCmd
1223
1267
  .command('get <asset_path>')
1224
1268
  .description("Show a single asset's metadata (path, type, size, frame). Metadata only — asset bytes are not served over the API.")
1225
1269
  .requiredOption('--project <id>', 'Target project ID')
1270
+ .option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
1226
1271
  .action(async (assetPath, options) => {
1227
- const data = await readApiGet('asset-get', `/api/projects/${encodeURIComponent(options.project)}/assets`);
1272
+ const data = await readApiGet('asset-get', `/api/projects/${encodeURIComponent(options.project)}/assets`, options.org);
1228
1273
  const wanted = assetPath.replace(/^\/+/, '');
1229
1274
  const asset = (data.assets || []).find(a => a.path === assetPath || a.path === wanted);
1230
1275
  if (!asset) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.10",
3
+ "version": "1.11.12",
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": [