drafted 1.11.8 → 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.
- package/cli/drafted.mjs +180 -9
- package/install-mcp.sh +28 -12
- 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
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
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
|
-
//
|
|
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('(
|
|
1065
|
-
.
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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/install-mcp.sh
CHANGED
|
@@ -827,19 +827,35 @@ struct Actions {
|
|
|
827
827
|
alert.runModal()
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
-
// Installed version
|
|
831
|
-
//
|
|
830
|
+
// Installed version, resolved WITHOUT depending on the login-shell PATH —
|
|
831
|
+
// the installer's npm prefix (~/.drafted/npm-global) is never written to any
|
|
832
|
+
// login profile, so `bash -lc "drafted"` finds nothing. Read the installed
|
|
833
|
+
// package.json at the fixed prefix first (the source npm itself writes), then
|
|
834
|
+
// fall back to the absolute binary, then a login shell (custom prefixes on PATH).
|
|
832
835
|
static func installedVersion() -> String? {
|
|
833
|
-
let
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
836
|
+
let pkg = home.appendingPathComponent(".drafted/npm-global/lib/node_modules/drafted/package.json")
|
|
837
|
+
if
|
|
838
|
+
let data = try? Data(contentsOf: pkg),
|
|
839
|
+
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
840
|
+
let version = json["version"] as? String,
|
|
841
|
+
isSemver(version)
|
|
842
|
+
{ return version }
|
|
843
|
+
|
|
844
|
+
let bin = home.appendingPathComponent(".drafted/npm-global/bin/drafted").path
|
|
845
|
+
for command in ["\"\(bin)\" --version 2>/dev/null", "drafted --version 2>/dev/null"] {
|
|
846
|
+
let process = Process()
|
|
847
|
+
process.executableURL = URL(fileURLWithPath: "/bin/bash")
|
|
848
|
+
process.arguments = ["-lc", command]
|
|
849
|
+
let pipe = Pipe()
|
|
850
|
+
process.standardOutput = pipe
|
|
851
|
+
process.standardError = FileHandle.nullDevice
|
|
852
|
+
do { try process.run(); process.waitUntilExit() } catch { continue }
|
|
853
|
+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
854
|
+
if let out = String(data: data, encoding: .utf8), let version = firstSemver(in: out) {
|
|
855
|
+
return version
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return nil
|
|
843
859
|
}
|
|
844
860
|
|
|
845
861
|
static func latestVersion(_ completion: @escaping (String?) -> Void) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
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": [
|