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.
Files changed (2) hide show
  1. package/cli/drafted.mjs +186 -9
  2. 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
- // Commands removed: list, remove, clear
1058
- // These now live in the MCP server as fs tools (ls, rm, batch)
1059
- // Use the Drafted MCP tools or the /api/fs/* REST API instead.
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
- // Legacy stubs for backward compat (inform users)
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('(deprecated) Use MCP tools: ls')
1065
- .action(() => {
1066
- console.log('The `list` command has been removed.');
1067
- console.log('Use the Drafted MCP tools (ls) or the /api/fs/ REST API instead.');
1068
- process.exit(0);
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.9",
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": [