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.
- package/cli/drafted.mjs +60 -15
- 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
|
-
|
|
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:
|
|
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.
|
|
238
|
-
//
|
|
239
|
-
|
|
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
|
-
|
|
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
|
|
1149
|
-
//
|
|
1150
|
-
//
|
|
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.
|
|
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": [
|