drafted 1.11.9 → 1.11.10

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