drafted 1.11.13 → 1.11.15

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 CHANGED
@@ -1899,4 +1899,132 @@ skillCmd
1899
1899
  else console.log(`ok\t${id}\t${data.hash}\tpushed ${data.count}${data.stripped ? `, stripped ${data.stripped}` : ''}${gitignored ? ', gitignored .skillinstall/' : ''}`);
1900
1900
  });
1901
1901
 
1902
+ // ── Collectors: management seam (parity with the MCP `collector` tool) ──
1903
+ // Collectors are checklist-driven intake surfaces. Management is behind the
1904
+ // agent allowlist (same gate as Minion) and scoped to the session's active org.
1905
+ function emitCollectorResult(format, obj) {
1906
+ if (format === 'json') { console.log(JSON.stringify(obj)); return; }
1907
+ console.log([obj.status, obj.id || '', obj.slug || '', obj.name || '', obj.error || ''].join('\t'));
1908
+ }
1909
+
1910
+ async function collectorSetEnabled(id, enabled, format) {
1911
+ requireLogin();
1912
+ const server = getServerUrl().replace(/\/$/, '');
1913
+ const res = await authFetch(`${server}/api/collectors/${encodeURIComponent(id)}`, {
1914
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }),
1915
+ });
1916
+ const data = await res.json().catch(() => ({}));
1917
+ if (!res.ok) { emitCollectorResult(format, { status: res.status === 404 ? 'not-found' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
1918
+ const c = data.collector || {};
1919
+ emitCollectorResult(format, { status: 'ok', id: c.id, slug: c.slug, name: c.name });
1920
+ }
1921
+
1922
+ const collectorCmd = program.command('collector').description('Collector management (checklist-driven intake surfaces). Requires the agent allowlist.');
1923
+
1924
+ collectorCmd
1925
+ .command('list')
1926
+ .description('List collectors (scoped to --project or your active project; all org collectors if neither)')
1927
+ .option('--project <id>', 'project to scope to (defaults to your active project)')
1928
+ .option('--format <fmt>', 'output format: json or text', 'text')
1929
+ .action(async (opts) => {
1930
+ const pid = opts.project || getActiveProject()?.id;
1931
+ const data = await readApiGet('collector:list', withQuery('/api/collectors', { projectId: pid }));
1932
+ const rows = data.collectors || [];
1933
+ if (opts.format === 'json') { console.log(JSON.stringify(rows)); return; }
1934
+ for (const c of rows) console.log(`${c.id}\t${c.slug}\t${c.enabled ? 'enabled' : 'disabled'}\t${c.name}`);
1935
+ });
1936
+
1937
+ collectorCmd
1938
+ .command('get <id>')
1939
+ .description('Get a collector by id')
1940
+ .option('--format <fmt>', 'output format: json or text', 'text')
1941
+ .action(async (id, opts) => {
1942
+ const data = await readApiGet('collector:get', `/api/collectors/${encodeURIComponent(id)}`);
1943
+ if (opts.format === 'json') { console.log(JSON.stringify(data.collector || data)); return; }
1944
+ const c = data.collector || {};
1945
+ console.log(`${c.id}\t${c.slug}\t${c.enabled ? 'enabled' : 'disabled'}\t${c.name}`);
1946
+ });
1947
+
1948
+ collectorCmd
1949
+ .command('meta')
1950
+ .description('Project layers/lanes/frames for building a collector target/output')
1951
+ .option('--project <id>', 'project (defaults to your active project)')
1952
+ .option('--format <fmt>', 'output format: json or text', 'text')
1953
+ .action(async (opts) => {
1954
+ const pid = opts.project || getActiveProject()?.id;
1955
+ if (!pid) { console.error('No active project. Pass --project <id> or run `drafted use <project>`.'); process.exit(1); }
1956
+ const data = await readApiGet('collector:meta', withQuery('/api/collectors/meta', { projectId: pid }));
1957
+ console.log(JSON.stringify(data));
1958
+ });
1959
+
1960
+ collectorCmd
1961
+ .command('create')
1962
+ .description('Create a collector from stdin JSON {name,description?,target,checklist,output,enabled?}')
1963
+ .option('--project <id>', 'project to bind to (defaults to your active project)')
1964
+ .option('--format <fmt>', 'output format: json or text', 'text')
1965
+ .action(async (opts) => {
1966
+ requireLogin();
1967
+ const pid = opts.project || getActiveProject()?.id;
1968
+ if (!pid) { emitCollectorResult(opts.format, { status: 'error', error: 'no project — pass --project <id> or run `drafted use <project>`' }); process.exit(1); }
1969
+ let p;
1970
+ try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
1971
+ const server = getServerUrl().replace(/\/$/, '');
1972
+ const res = await authFetch(`${server}/api/collectors`, {
1973
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1974
+ body: JSON.stringify({ projectId: pid, name: p.name, description: p.description, target: p.target, checklist: p.checklist, output: p.output, enabled: p.enabled }),
1975
+ });
1976
+ const data = await res.json().catch(() => ({}));
1977
+ if (!res.ok) { emitCollectorResult(opts.format, { status: res.status === 403 ? 'forbidden' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
1978
+ const c = data.collector || {};
1979
+ emitCollectorResult(opts.format, { status: 'ok', id: c.id, slug: c.slug, name: c.name });
1980
+ });
1981
+
1982
+ collectorCmd
1983
+ .command('update <id>')
1984
+ .description('Update a collector from stdin JSON {name?,description?,target?,checklist?,output?,enabled?}')
1985
+ .option('--format <fmt>', 'output format: json or text', 'text')
1986
+ .action(async (id, opts) => {
1987
+ requireLogin();
1988
+ let p;
1989
+ try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
1990
+ const body = {};
1991
+ for (const k of ['name', 'description', 'target', 'checklist', 'output', 'enabled']) {
1992
+ if (p[k] !== undefined) body[k] = p[k];
1993
+ }
1994
+ if (Object.keys(body).length === 0) { emitCollectorResult(opts.format, { status: 'error', error: 'no fields to update' }); process.exit(1); }
1995
+ const server = getServerUrl().replace(/\/$/, '');
1996
+ const res = await authFetch(`${server}/api/collectors/${encodeURIComponent(id)}`, {
1997
+ method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
1998
+ });
1999
+ const data = await res.json().catch(() => ({}));
2000
+ if (!res.ok) { emitCollectorResult(opts.format, { status: res.status === 404 ? 'not-found' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
2001
+ const c = data.collector || {};
2002
+ emitCollectorResult(opts.format, { status: 'ok', id: c.id, slug: c.slug, name: c.name });
2003
+ });
2004
+
2005
+ collectorCmd
2006
+ .command('enable <id>')
2007
+ .description('Make a collector live')
2008
+ .option('--format <fmt>', 'output format: json or text', 'text')
2009
+ .action((id, opts) => collectorSetEnabled(id, true, opts.format));
2010
+
2011
+ collectorCmd
2012
+ .command('disable <id>')
2013
+ .description('Take a collector offline')
2014
+ .option('--format <fmt>', 'output format: json or text', 'text')
2015
+ .action((id, opts) => collectorSetEnabled(id, false, opts.format));
2016
+
2017
+ collectorCmd
2018
+ .command('delete <id>')
2019
+ .description('Delete a collector (past submissions are kept as history)')
2020
+ .option('--format <fmt>', 'output format: json or text', 'text')
2021
+ .action(async (id, opts) => {
2022
+ requireLogin();
2023
+ const server = getServerUrl().replace(/\/$/, '');
2024
+ const res = await authFetch(`${server}/api/collectors/${encodeURIComponent(id)}`, { method: 'DELETE' });
2025
+ const data = await res.json().catch(() => ({}));
2026
+ if (!res.ok) { emitCollectorResult(opts.format, { status: res.status === 404 ? 'not-found' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
2027
+ emitCollectorResult(opts.format, { status: 'ok', id: data.id || id });
2028
+ });
2029
+
1902
2030
  program.parse();
package/mcp/server.mjs CHANGED
@@ -139,6 +139,7 @@ const REMOTE_JSON_STRING_PARAMS = {
139
139
  wiki: ['frontmatter', 'pages'],
140
140
  project: ['layers'],
141
141
  template: ['layers'],
142
+ collector: ['target', 'checklist', 'output'],
142
143
  };
143
144
 
144
145
  // Remove sentences that reference local-file params from a tool description,
@@ -242,6 +243,9 @@ const TOOL_ANNOTATIONS = {
242
243
  // Skills
243
244
  skill: { title: 'Skills', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Manage the Drafted skill library: search, load, add, update, remove, attach/detach from projects, favorite, and edit skill files.' },
244
245
  wiki: { title: 'Wiki', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Per-org wiki. Markdown pages with paths as hierarchy. Dispatch by `action`.' },
246
+
247
+ // Collectors — checklist-driven, Minion-run intake surfaces bound to a project
248
+ collector: { title: 'Collectors', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Manage Collectors: checklist-driven intake surfaces that guide a consumer through a checklist (via a shareable /c/<slug> link) and then write a producible into the project. Dispatch by `action`: meta (discover layers/lanes/frames), list, get, create, update, enable, disable, delete. Requires the agent allowlist (same gate as Minion).' },
245
249
  };
246
250
 
247
251
  function isMutatingToolCall(name, args = {}) {
@@ -267,6 +271,8 @@ function isMutatingToolCall(name, args = {}) {
267
271
  return ['add', 'update', 'remove', 'attach', 'detach', 'favorite', 'unfavorite', 'update_file'].includes(action);
268
272
  case 'wiki':
269
273
  return ['log', 'write', 'edit', 'mv', 'rm', 'source-register', 'bulk-write'].includes(action);
274
+ case 'collector':
275
+ return ['create', 'update', 'enable', 'disable', 'delete'].includes(action);
270
276
  case 'rm':
271
277
  case 'shape':
272
278
  case 'group':
@@ -361,7 +367,7 @@ function tool(name, descOrSchema, schemaOrHandler, handler) {
361
367
  const v = args[0][k];
362
368
  if (typeof v === 'string' && v.trim()) {
363
369
  try { args[0][k] = JSON.parse(v); }
364
- catch { return err(new Error(`Param "${k}" must be a JSON-encoded ${k === 'excalidraw_data' || k === 'frontmatter' ? 'object' : 'array'} string; JSON.parse failed.`)); }
370
+ catch { return err(new Error(`Param "${k}" must be a JSON-encoded ${['excalidraw_data', 'frontmatter', 'target', 'output'].includes(k) ? 'object' : 'array'} string; JSON.parse failed.`)); }
365
371
  }
366
372
  }
367
373
  }
@@ -1224,7 +1230,13 @@ async function consumePendingDeviceCode() {
1224
1230
  return false;
1225
1231
  }
1226
1232
 
1227
- tool('auth', 'Sign in to Drafted. `action=get_link` returns a verification URL immediately (use for SSH/headless/tmux where a browser may not open) and starts background polling; after the user opens the link, later Drafted tool calls also auto-consume the approved login. `action=login` opens a browser when needed and explicitly waits/polls for approval. If get_link was called first, login reuses that pending code instead of opening a new browser.', {
1233
+ // Device-flow sign-in is stdio-only. Web/Cowork connectors authenticate via
1234
+ // OAuth at the transport layer (the hosted /mcp handler injects a Drafted
1235
+ // session per request), so advertising a device-flow auth tool there is
1236
+ // meaningless and disruptive — it returns spurious sign-in URLs and, on login,
1237
+ // spawns a server-side browser-open and blocks polling until timeout. Register
1238
+ // it only on stdio.
1239
+ if (!isRemote) tool('auth', 'Sign in to Drafted. `action=get_link` returns a verification URL immediately (use for SSH/headless/tmux where a browser may not open) and starts background polling; after the user opens the link, later Drafted tool calls also auto-consume the approved login. `action=login` opens a browser when needed and explicitly waits/polls for approval. If get_link was called first, login reuses that pending code instead of opening a new browser.', {
1228
1240
  action: z.enum(['get_link', 'login']).describe('Operation to perform.'),
1229
1241
  }, async ({ action }) => {
1230
1242
  try {
@@ -1236,7 +1248,11 @@ tool('auth', 'Sign in to Drafted. `action=get_link` returns a verification URL i
1236
1248
  return ok(data.verificationUrl);
1237
1249
  }
1238
1250
  if (action === 'login') {
1239
- const existing = getBootstrapSessionId();
1251
+ // Prefer the active request session (injected by runWithRequestState on
1252
+ // remote, or cloneSession on stdio) over the on-disk bootstrap session, so
1253
+ // an already-authenticated caller short-circuits to already_authenticated
1254
+ // instead of starting a needless device flow.
1255
+ const existing = getState().sessionId || getBootstrapSessionId();
1240
1256
  if (existing) {
1241
1257
  try {
1242
1258
  const res = await fetch(`${getServerUrl()}/auth/me`, {
@@ -3423,6 +3439,129 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
3423
3439
  } catch (error) { return err(error); }
3424
3440
  });
3425
3441
 
3442
+ // ── Collectors ────────────────────────────────────────────────────
3443
+
3444
+ function compactCollectorEntry(c) {
3445
+ if (!c || typeof c !== 'object') return c;
3446
+ return { id: c.id, slug: c.slug, name: c.name, enabled: c.enabled, projectId: c.projectId };
3447
+ }
3448
+
3449
+ // Shape a {collectors:[...]} list with limit/offset pagination + optional compact
3450
+ // mode, mirroring shapeSkillCatalog so large lists stay within token budget.
3451
+ function shapeCollectorList(result, { limit, offset = 0, compact = false } = {}) {
3452
+ if (!Array.isArray(result?.collectors)) return result;
3453
+ const total = result.collectors.length;
3454
+ const start = Math.max(0, Math.floor(Number(offset) || 0));
3455
+ const cap = Math.min(Math.max(1, Math.floor(Number(limit) || 25)), 100);
3456
+ const page = result.collectors.slice(start, start + cap);
3457
+ result.totalAvailable = total;
3458
+ result.offset = start;
3459
+ result.returned = page.length;
3460
+ result.truncated = start + page.length < total;
3461
+ result.collectors = compact ? page.map(compactCollectorEntry) : page;
3462
+ result.note = compact
3463
+ ? 'Compact list: {id,slug,name,enabled,projectId} only. Use collector(action="get", id="<id>") for full config; limit/offset to page.'
3464
+ : 'Collectors are scoped to the active project (all org collectors when no project is open). Use limit/offset to page; compact=true for a leaner list.';
3465
+ return result;
3466
+ }
3467
+
3468
+ // Friendlier message when the agent allowlist gate (requireAgentAccess) rejects.
3469
+ function collectorGateError(e) {
3470
+ if (e?.status === 403 || e?.code === 'agent_disabled') {
3471
+ return new Error('Collector management is not enabled for this org/account (agent allowlist). Ask an admin to add your org or email to DRAFTED_AGENT_ALLOWED_ORGS / DRAFTED_AGENT_ALLOWED_EMAILS.');
3472
+ }
3473
+ return e;
3474
+ }
3475
+
3476
+ tool('collector', {
3477
+ action: z.enum(['meta', 'list', 'get', 'create', 'update', 'enable', 'disable', 'delete']).describe('Operation to perform.'),
3478
+ id: z.string().optional().describe('[get|update|enable|disable|delete] collector ID (UUID)'),
3479
+ projectId: z.string().optional().describe('[create|meta] project to bind/scope to (defaults to the active project). Operates on the session’s active org — open the target project first via project(action="open"), or get_org(action="switch") to change org.'),
3480
+ name: z.string().optional().describe('[create|update] collector name'),
3481
+ description: z.string().optional().describe('[create|update] one-line description shown to the consumer'),
3482
+ enabled: z.boolean().optional().describe('[create|update] whether the collector is live; a disabled collector 404s on its /c/<slug> link. enable/disable set this directly.'),
3483
+ target: z.object({
3484
+ type: z.enum(['new-record', 'layer', 'frame']).describe('what the run writes against'),
3485
+ layer: z.string().optional(),
3486
+ frameId: z.string().optional(),
3487
+ }).optional().describe('[create|update] where the run writes: a new record, into a layer, or updating a specific frame'),
3488
+ checklist: z.array(z.object({
3489
+ id: z.string().describe('stable item id'),
3490
+ label: z.string().describe('what the consumer is asked for'),
3491
+ description: z.string().optional(),
3492
+ evidence: z.enum(['text', 'file', 'photo', 'none']).optional().describe('evidence required (default none)'),
3493
+ required: z.boolean().optional(),
3494
+ })).optional().describe('[create|update] ordered checklist the Minion guides the consumer through'),
3495
+ output: z.object({
3496
+ mode: z.enum(['generate', 'update', 'annotate']).describe('how the producible is written'),
3497
+ layer: z.string().optional(),
3498
+ lane: z.string().optional(),
3499
+ filenameTemplate: z.string().optional().describe('e.g. "<id>.md"'),
3500
+ skillSlug: z.string().optional().describe('skill that shapes the produced frame'),
3501
+ grouping: z.string().optional().describe('"lane" gives each submission its own lane'),
3502
+ }).optional().describe('[create|update] where/how the producible lands'),
3503
+ limit: z.number().optional().describe('[list] max results per page (default 25, max 100)'),
3504
+ offset: z.number().optional().describe('[list] skip N results for pagination (default 0)'),
3505
+ compact: z.boolean().optional().describe('[list] return only {id,slug,name,enabled,projectId} per collector'),
3506
+ }, async (args) => {
3507
+ try {
3508
+ const { action } = args;
3509
+ switch (action) {
3510
+ case 'meta': {
3511
+ const active = getState().projectId;
3512
+ const pid = active || args.projectId;
3513
+ if (!pid) throw new Error('No active project. Call project(action="open") first, or pass projectId.');
3514
+ // api() auto-appends the active project; add projectId explicitly only when none is active.
3515
+ const path = active ? '/api/collectors/meta' : `/api/collectors/meta?projectId=${encodeURIComponent(pid)}`;
3516
+ return ok(await api('GET', path));
3517
+ }
3518
+ case 'list': {
3519
+ // api() auto-appends the active project as ?projectId — so this lists the
3520
+ // active project's collectors, or all org collectors when none is open.
3521
+ const result = await api('GET', '/api/collectors');
3522
+ return ok(shapeCollectorList(result, { limit: args.limit, offset: args.offset, compact: args.compact }));
3523
+ }
3524
+ case 'get': {
3525
+ if (!args.id) throw new Error('id is required for action=get');
3526
+ return ok(await api('GET', `/api/collectors/${args.id}`));
3527
+ }
3528
+ case 'create': {
3529
+ // projectId lives in the BODY (the POST route reads body, ignores the query).
3530
+ const projectId = args.projectId || getState().projectId;
3531
+ if (!projectId) throw new Error('No active project. Call project(action="open") first, or pass projectId.');
3532
+ const { name, description, target, checklist, output, enabled } = args;
3533
+ if (!name || !target || !Array.isArray(checklist) || !output) {
3534
+ throw new Error('name, target, checklist[], and output are required for action=create');
3535
+ }
3536
+ const body = { projectId, name, target, checklist, output };
3537
+ if (description !== undefined) body.description = description;
3538
+ if (enabled !== undefined) body.enabled = enabled;
3539
+ return ok(await api('POST', '/api/collectors', body));
3540
+ }
3541
+ case 'update': {
3542
+ if (!args.id) throw new Error('id is required for action=update');
3543
+ const body = {};
3544
+ for (const k of ['name', 'description', 'target', 'checklist', 'output', 'enabled']) {
3545
+ if (args[k] !== undefined) body[k] = args[k];
3546
+ }
3547
+ if (Object.keys(body).length === 0) throw new Error('At least one field is required for action=update');
3548
+ return ok(await api('PATCH', `/api/collectors/${args.id}`, body));
3549
+ }
3550
+ case 'enable':
3551
+ case 'disable': {
3552
+ if (!args.id) throw new Error(`id is required for action=${action}`);
3553
+ return ok(await api('PATCH', `/api/collectors/${args.id}`, { enabled: action === 'enable' }));
3554
+ }
3555
+ case 'delete': {
3556
+ if (!args.id) throw new Error('id is required for action=delete');
3557
+ return ok(await api('DELETE', `/api/collectors/${args.id}`));
3558
+ }
3559
+ default:
3560
+ throw new Error(`Unknown collector action: ${action}`);
3561
+ }
3562
+ } catch (error) { return err(collectorGateError(error)); }
3563
+ });
3564
+
3426
3565
  // ── Resource: canvas info ─────────────────────────────────────────
3427
3566
 
3428
3567
  server.resource('info', 'drafted://info', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drafted",
3
- "version": "1.11.13",
3
+ "version": "1.11.15",
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": [