drafted 1.11.9 → 1.11.11
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 +186 -9
- package/package.json +1 -1
package/cli/drafted.mjs
CHANGED
|
@@ -221,6 +221,92 @@ async function authFetch(url, options = {}) {
|
|
|
221
221
|
return fetch(url, { ...options, headers });
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// --- Read helpers (remote, read-only) ---
|
|
225
|
+
|
|
226
|
+
// Append non-empty query params to a path/URL.
|
|
227
|
+
function withQuery(base, params) {
|
|
228
|
+
const usp = new URLSearchParams();
|
|
229
|
+
for (const [k, v] of Object.entries(params)) {
|
|
230
|
+
if (v !== undefined && v !== null && v !== '') usp.set(k, String(v));
|
|
231
|
+
}
|
|
232
|
+
const qs = usp.toString();
|
|
233
|
+
return qs ? `${base}?${qs}` : base;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// 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. An
|
|
238
|
+
// optional --org override is forwarded as the X-Drafted-Org header (the server
|
|
239
|
+
// validates it against the caller's memberships). On any failure it emits a
|
|
240
|
+
// structured error (for --json) and exits non-zero.
|
|
241
|
+
async function readApiGet(command, apiPath, org) {
|
|
242
|
+
requireLogin();
|
|
243
|
+
const serverUrl = getServerUrl();
|
|
244
|
+
const headers = org ? { 'X-Drafted-Org': org } : {};
|
|
245
|
+
let res;
|
|
246
|
+
try {
|
|
247
|
+
res = await authFetch(`${serverUrl}${apiPath}`, { headers });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
jsonOut(false, command, `Could not reach server at ${serverUrl}: ${err.message}`);
|
|
250
|
+
console.error(`❌ Could not reach server at ${serverUrl}: ${err.message}`);
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const data = await res.json().catch(() => ({}));
|
|
254
|
+
if (!res.ok) {
|
|
255
|
+
const msg = data.error || `HTTP ${res.status}`;
|
|
256
|
+
jsonOut(false, command, msg);
|
|
257
|
+
console.error(`❌ ${msg}`);
|
|
258
|
+
process.exit(1);
|
|
259
|
+
}
|
|
260
|
+
return data;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Resolve a frame path / frame URL (/f/<uuid>) / bare UUID to its /api/fs read
|
|
264
|
+
// endpoint. Mirrors the MCP `frame(action="read")` resolution so the CLI and MCP
|
|
265
|
+
// accept the same identifiers.
|
|
266
|
+
function frameReadPath(pathOrId, projectId, lines) {
|
|
267
|
+
const frameUrlMatch = pathOrId.match(/\/f\/([a-f0-9-]{36})/);
|
|
268
|
+
const uuidMatch = /^[a-f0-9-]{36}$/.test(pathOrId);
|
|
269
|
+
const frameId = frameUrlMatch?.[1] || (uuidMatch ? pathOrId : null);
|
|
270
|
+
if (frameId) return withQuery(`/api/fs/by-id/${frameId}`, { projectId, lines });
|
|
271
|
+
const parts = pathOrId.replace(/^\/+/, '').split('/');
|
|
272
|
+
if (parts.length !== 3) {
|
|
273
|
+
throw new Error('Path must be /{layer}/{lane}/{filename}, a frame URL, or a frame ID');
|
|
274
|
+
}
|
|
275
|
+
return withQuery(`/api/fs/${parts[0]}/${parts[1]}/${parts[2]}`, { projectId, lines });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Human-readable printer for `ls` output. Handles both the rich root listing
|
|
279
|
+
// ({ layers, connectors }) and a path listing ({ path, entries }).
|
|
280
|
+
function printLs(data) {
|
|
281
|
+
const proj = data.project;
|
|
282
|
+
if (proj) {
|
|
283
|
+
const name = typeof proj === 'string' ? proj : (proj.name || proj.slug || proj.id);
|
|
284
|
+
console.log(`Project: ${name}${proj.workflow ? ` (${proj.workflow})` : ''}`);
|
|
285
|
+
console.log('');
|
|
286
|
+
}
|
|
287
|
+
if (Array.isArray(data.layers)) {
|
|
288
|
+
for (const l of data.layers) {
|
|
289
|
+
console.log(` ${l.path} — ${l.label || ''}`);
|
|
290
|
+
for (const lane of l.lanes || []) console.log(` ${lane}`);
|
|
291
|
+
for (const a of l.anchored || []) console.log(` * ${a.path} (anchored)`);
|
|
292
|
+
}
|
|
293
|
+
if (Array.isArray(data.connectors) && data.connectors.length) {
|
|
294
|
+
console.log('');
|
|
295
|
+
console.log(` Connectors (${data.connectors.length}):`);
|
|
296
|
+
for (const c of data.connectors) console.log(` ${c.source} → ${c.target}${c.label ? ` "${c.label}"` : ''}`);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const entries = data.entries || [];
|
|
301
|
+
console.log(`${data.path || '/'} (${entries.length}):`);
|
|
302
|
+
for (const e of entries) {
|
|
303
|
+
const meta = e.type === 'frame'
|
|
304
|
+
? ` [${e.contentType || e.title || 'frame'}${e.size != null ? `, ${e.size}b` : ''}]`
|
|
305
|
+
: '';
|
|
306
|
+
console.log(` ${e.path}${e.type && e.type !== 'frame' ? '/' : ''}${meta}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
224
310
|
// Global options
|
|
225
311
|
program
|
|
226
312
|
.option('--json', 'Output as JSON');
|
|
@@ -1054,18 +1140,109 @@ program
|
|
|
1054
1140
|
}
|
|
1055
1141
|
});
|
|
1056
1142
|
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1143
|
+
// ── Read commands (read-only) ─────────────────────────────────────
|
|
1144
|
+
// Scriptable, non-interactive reads for external automation / CI. These mirror
|
|
1145
|
+
// the rich read ops the MCP exposes but over the same authenticated /api/fs/
|
|
1146
|
+
// surface the `add` command already uses — strictly GET, scoped to the logged-in
|
|
1147
|
+
// user's own orgs/projects. No new data exposure beyond what the MCP grants.
|
|
1148
|
+
//
|
|
1149
|
+
// Project scoping: --project <id> selects the project (passed through as a query
|
|
1150
|
+
// param). When omitted, the server resolves the caller's active project. Org is
|
|
1151
|
+
// the logged-in session's active org unless --org <id|name> is given, which the
|
|
1152
|
+
// server validates against the caller's memberships and scopes per-request via
|
|
1153
|
+
// the X-Drafted-Org header — without mutating the shared session's active org.
|
|
1154
|
+
|
|
1155
|
+
// Command: ls (alias: list) — list layers / lanes / frames
|
|
1156
|
+
program
|
|
1157
|
+
.command('ls [path]')
|
|
1158
|
+
.alias('list')
|
|
1159
|
+
.description('List layers, lanes, or frames in a project (read-only)')
|
|
1160
|
+
.option('--project <id>', 'Target project ID (defaults to your active project)')
|
|
1161
|
+
.option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
|
|
1162
|
+
.option('--recursive', 'Recurse into subdirectories (forces summary mode)')
|
|
1163
|
+
.option('--summary', 'Include size, updatedAt, and title for frames')
|
|
1164
|
+
.option('--pattern <glob>', 'Filter frame filenames (e.g. "*.html")')
|
|
1165
|
+
.action(async (path, options) => {
|
|
1166
|
+
const params = { path: path || '/', projectId: options.project, pattern: options.pattern };
|
|
1167
|
+
if (options.recursive) { params.recursive = 'true'; params.summary = 'true'; }
|
|
1168
|
+
else if (options.summary) params.summary = 'true';
|
|
1169
|
+
const data = await readApiGet('ls', withQuery('/api/fs/', params), options.org);
|
|
1170
|
+
jsonOut(true, 'ls', data);
|
|
1171
|
+
printLs(data);
|
|
1172
|
+
});
|
|
1060
1173
|
|
|
1061
|
-
//
|
|
1174
|
+
// Command: read (aliases: get, cat) — read a frame's content
|
|
1062
1175
|
program
|
|
1176
|
+
.command('read <pathOrId>')
|
|
1177
|
+
.aliases(['get', 'cat'])
|
|
1178
|
+
.description("Read a frame's content (HTML/text/markdown), or binary metadata, by path / frame URL / ID (read-only)")
|
|
1179
|
+
.option('--project <id>', 'Target project ID (defaults to your active project)')
|
|
1180
|
+
.option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
|
|
1181
|
+
.option('--lines <range>', 'Line range to read, e.g. "1-40" or "20"')
|
|
1182
|
+
.action(async (pathOrId, options) => {
|
|
1183
|
+
let apiPath;
|
|
1184
|
+
try {
|
|
1185
|
+
apiPath = frameReadPath(pathOrId, options.project, options.lines);
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
jsonOut(false, 'read', err.message);
|
|
1188
|
+
console.error(`❌ ${err.message}`);
|
|
1189
|
+
process.exit(1);
|
|
1190
|
+
}
|
|
1191
|
+
const data = await readApiGet('read', apiPath, options.org);
|
|
1192
|
+
jsonOut(true, 'read', data);
|
|
1193
|
+
if (data.type === 'binary') {
|
|
1194
|
+
console.log(`# ${data.path} (binary)`);
|
|
1195
|
+
if (data.contentType) console.log(`Type: ${data.contentType}`);
|
|
1196
|
+
if (data.sourceUrl) console.log(`Storage key: ${data.sourceUrl}`);
|
|
1197
|
+
} else {
|
|
1198
|
+
// Emit raw content to stdout so callers can pipe it into extractors
|
|
1199
|
+
// (e.g. pull <script type="application/json" id="..."> token blocks).
|
|
1200
|
+
const content = data.content || '';
|
|
1201
|
+
process.stdout.write(content);
|
|
1202
|
+
if (content && !content.endsWith('\n')) process.stdout.write('\n');
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// Command: asset — read-only project asset operations
|
|
1207
|
+
const assetCmd = program.command('asset').description('Project asset operations (read-only)');
|
|
1208
|
+
|
|
1209
|
+
assetCmd
|
|
1063
1210
|
.command('list')
|
|
1064
|
-
.description('(
|
|
1065
|
-
.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1211
|
+
.description('List supporting assets (CSS/JS/images/fonts) in a project')
|
|
1212
|
+
.requiredOption('--project <id>', 'Target project ID')
|
|
1213
|
+
.option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
|
|
1214
|
+
.option('--frame <id>', 'Only assets associated with this frame ID')
|
|
1215
|
+
.action(async (options) => {
|
|
1216
|
+
const apiPath = withQuery(`/api/projects/${encodeURIComponent(options.project)}/assets`, { frameId: options.frame });
|
|
1217
|
+
const data = await readApiGet('asset-list', apiPath, options.org);
|
|
1218
|
+
jsonOut(true, 'asset-list', data);
|
|
1219
|
+
const assets = data.assets || [];
|
|
1220
|
+
if (assets.length === 0) { console.log('No assets'); return; }
|
|
1221
|
+
console.log(`Assets (${assets.length}):\n`);
|
|
1222
|
+
for (const a of assets) {
|
|
1223
|
+
console.log(` ${a.path}\t${a.contentType || ''}${a.size != null ? `\t${a.size} bytes` : ''}`);
|
|
1224
|
+
}
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
assetCmd
|
|
1228
|
+
.command('get <asset_path>')
|
|
1229
|
+
.description("Show a single asset's metadata (path, type, size, frame). Metadata only — asset bytes are not served over the API.")
|
|
1230
|
+
.requiredOption('--project <id>', 'Target project ID')
|
|
1231
|
+
.option('--org <org>', 'Resolve against this org (id or name); scopes per-request without switching the session')
|
|
1232
|
+
.action(async (assetPath, options) => {
|
|
1233
|
+
const data = await readApiGet('asset-get', `/api/projects/${encodeURIComponent(options.project)}/assets`, options.org);
|
|
1234
|
+
const wanted = assetPath.replace(/^\/+/, '');
|
|
1235
|
+
const asset = (data.assets || []).find(a => a.path === assetPath || a.path === wanted);
|
|
1236
|
+
if (!asset) {
|
|
1237
|
+
jsonOut(false, 'asset-get', `Asset not found: ${assetPath}`);
|
|
1238
|
+
console.error(`❌ Asset not found: ${assetPath}`);
|
|
1239
|
+
process.exit(1);
|
|
1240
|
+
}
|
|
1241
|
+
jsonOut(true, 'asset-get', { asset });
|
|
1242
|
+
console.log(asset.path);
|
|
1243
|
+
console.log(` Type: ${asset.contentType || 'unknown'}`);
|
|
1244
|
+
console.log(` Size: ${asset.size != null ? `${asset.size} bytes` : 'unknown'}`);
|
|
1245
|
+
if (asset.frameId) console.log(` Frame: ${asset.frameId}`);
|
|
1069
1246
|
});
|
|
1070
1247
|
|
|
1071
1248
|
program
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.11",
|
|
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": [
|