drafted 1.25.5 → 1.25.7
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 +67 -10
- package/mcp/flow-tools.mjs +88 -0
- package/mcp/server.mjs +1 -1
- package/package.json +3 -2
- package/src/shared/action-state.mjs +15 -0
- package/src/shared/agent-output.mjs +29 -0
package/cli/drafted.mjs
CHANGED
|
@@ -19,6 +19,7 @@ import { DESIGN_SYSTEM_PROMPT, buildDesignPrompt } from './prompts.mjs';
|
|
|
19
19
|
import { scanRepoAgents, detectDefaultBranch, detectRemoteUrl, isGitUrl, cloneToTemp, classifySeed } from './repo-scan.mjs';
|
|
20
20
|
import { setupTrusted } from './skill-setup-trust.mjs';
|
|
21
21
|
import { openCommandFor } from './open-url.mjs';
|
|
22
|
+
import { actionState } from '../src/shared/action-state.mjs';
|
|
22
23
|
|
|
23
24
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
25
|
const __dirname = dirname(__filename);
|
|
@@ -2250,16 +2251,6 @@ async function minionSetEnabled(id, enabled, format) {
|
|
|
2250
2251
|
// Approving stays a browser-only act in Drafted; nothing here can approve.
|
|
2251
2252
|
const actionCmd = program.command('action').description('Propose an outward effect for a person to approve, and read its decision');
|
|
2252
2253
|
|
|
2253
|
-
function actionState(actions) {
|
|
2254
|
-
const st = (actions || []).map((a) => a.status);
|
|
2255
|
-
if (!st.length) return 'unknown';
|
|
2256
|
-
if (st.every((s) => s === 'proposed')) return 'waiting';
|
|
2257
|
-
if (st.includes('rejected')) return 'rejected';
|
|
2258
|
-
if (st.includes('failed')) return 'failed';
|
|
2259
|
-
if (st.every((s) => s === 'executed')) return 'executed';
|
|
2260
|
-
return 'approved';
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
2254
|
async function actionApi(method, apiPath, org, body) {
|
|
2264
2255
|
requireLogin();
|
|
2265
2256
|
const server = getServerUrl().replace(/\/$/, '');
|
|
@@ -2317,6 +2308,72 @@ actionCmd
|
|
|
2317
2308
|
else console.log(`${out.state}\t${out.decidedBy || ''}\t${out.note || ''}`);
|
|
2318
2309
|
});
|
|
2319
2310
|
|
|
2311
|
+
const flowToolsCmd = program.command('flow-tools').description('Read-only tools for an agent@1 flow step run outside Drafted (served by the drafted-flow-tools MCP server)');
|
|
2312
|
+
|
|
2313
|
+
flowToolsCmd
|
|
2314
|
+
.command('token')
|
|
2315
|
+
.description('Mint a run-scoped, read-only token for drafted-flow-tools: one project, the listed tools, expiring (default 60, at most 240 minutes). Prints the token once.')
|
|
2316
|
+
.requiredOption('--project <id>', 'project (id) the step runs in')
|
|
2317
|
+
.requiredOption('--tools <names>', 'comma-separated tools from the read-only toolset, e.g. search_wiki,read_frame')
|
|
2318
|
+
.option('--ttl <minutes>', 'minutes until it expires')
|
|
2319
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
2320
|
+
.action(async (opts) => {
|
|
2321
|
+
const tools = String(opts.tools).split(',').map((t) => t.trim()).filter(Boolean);
|
|
2322
|
+
const r = await actionApi('POST', '/api/flow-tools/tokens', null, { projectId: opts.project, tools, ttlMinutes: opts.ttl ? Number(opts.ttl) : undefined });
|
|
2323
|
+
if (!r.ok) {
|
|
2324
|
+
const out = { status: 'error', error: r.data.error || `HTTP ${r.status}` };
|
|
2325
|
+
if (opts.format === 'json') console.log(JSON.stringify(out)); else console.error(`error\t${out.error}`);
|
|
2326
|
+
process.exit(1);
|
|
2327
|
+
}
|
|
2328
|
+
if (opts.format === 'json') console.log(JSON.stringify({ status: 'ok', token: r.data.token, expiresAt: r.data.expiresAt, tools: r.data.tools }));
|
|
2329
|
+
else console.log(r.data.token);
|
|
2330
|
+
});
|
|
2331
|
+
|
|
2332
|
+
// ── Automations: start a skill flow in Drafted ──────────────────────────
|
|
2333
|
+
// The "local agent" trigger (U2): the same run as the Flows tab's Run now, at the
|
|
2334
|
+
// published version, as you. The request returns once the run finishes or parks
|
|
2335
|
+
// on an approval, so the status printed is where it stands, not "started".
|
|
2336
|
+
const automationCmd = program.command('automation').description('Run a skill\'s flows in Drafted');
|
|
2337
|
+
|
|
2338
|
+
automationCmd
|
|
2339
|
+
.command('run <skill> [flow]')
|
|
2340
|
+
.description('Run a flow (default: "default") of an org skill at its published version. --input becomes the run\'s untrusted trigger.json input.')
|
|
2341
|
+
.option('--project <id>', 'project the run happens in (defaults to your active project)')
|
|
2342
|
+
.option('--input <text>', 'input text for the run')
|
|
2343
|
+
.option('--input-file <path>', 'read the input from a file ("-" for stdin)')
|
|
2344
|
+
.option('--org <org>', 'org (id or slug) the skill belongs to')
|
|
2345
|
+
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
2346
|
+
.action(async (skillSlug, flow, opts) => {
|
|
2347
|
+
const fail = (error) => {
|
|
2348
|
+
if (opts.format === 'json') console.log(JSON.stringify({ status: 'error', error }));
|
|
2349
|
+
else console.error(`error\t${error}`);
|
|
2350
|
+
process.exit(1);
|
|
2351
|
+
};
|
|
2352
|
+
const projectId = opts.project || getActiveProject()?.id;
|
|
2353
|
+
if (!projectId) fail('no project: pass --project or run `drafted use-project <id>` first');
|
|
2354
|
+
let input = opts.input;
|
|
2355
|
+
if (opts.inputFile) {
|
|
2356
|
+
try { input = readFileSync(opts.inputFile === '-' ? 0 : opts.inputFile, 'utf8'); } catch (err) { fail(`cannot read ${opts.inputFile}: ${err.message}`); }
|
|
2357
|
+
}
|
|
2358
|
+
const chain = flow || 'default';
|
|
2359
|
+
const found = await actionApi('GET', `/api/skills/slug/${encodeURIComponent(skillSlug)}?lookup=1`, opts.org);
|
|
2360
|
+
if (!found.ok) fail(found.data.error || `HTTP ${found.status}`);
|
|
2361
|
+
const skillId = found.data.id;
|
|
2362
|
+
const run = await actionApi('POST', `/api/skills/${encodeURIComponent(skillId)}/flows/${encodeURIComponent(chain)}/run`, opts.org, { projectId, input: input ?? '' });
|
|
2363
|
+
if (!run.ok) fail(run.data.error || `HTTP ${run.status}`);
|
|
2364
|
+
const list = await actionApi('GET', `/api/skills/${encodeURIComponent(skillId)}/automation-runs`, opts.org);
|
|
2365
|
+
const r = (list.ok && (list.data.runs || []).find((x) => x.id === run.data.runId)) || {};
|
|
2366
|
+
const out = {
|
|
2367
|
+
status: 'ok', runId: run.data.runId, version: run.data.version, flow: chain,
|
|
2368
|
+
state: r.status || 'unknown', error: r.error || null,
|
|
2369
|
+
steps: (r.steps || []).map((st) => ({ id: st.id, status: st.status, actionGroupId: st.actionGroupId || null })),
|
|
2370
|
+
};
|
|
2371
|
+
if (opts.format === 'json') { console.log(JSON.stringify(out)); return; }
|
|
2372
|
+
console.log(`${out.runId}\t${out.state}\tv${out.version}${out.error ? `\t${out.error}` : ''}`);
|
|
2373
|
+
for (const st of out.steps) console.log(` ${st.id}\t${st.status}${st.actionGroupId ? `\t${st.actionGroupId}` : ''}`);
|
|
2374
|
+
if (out.state === 'failed') process.exit(1);
|
|
2375
|
+
});
|
|
2376
|
+
|
|
2320
2377
|
const minionCmd = program.command('minion').description('Minion management (checklist-driven intake surfaces). Requires the agent allowlist.');
|
|
2321
2378
|
|
|
2322
2379
|
minionCmd
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* drafted-flow-tools: the stdio MCP server an agent@1 step runs against when it
|
|
4
|
+
* runs OUTSIDE Drafted (a Causeway backend). It serves exactly two kinds of tool:
|
|
5
|
+
*
|
|
6
|
+
* - the step's read-only tools, listed and executed by Drafted itself
|
|
7
|
+
* (GET/POST /api/flow-tools, server/lib/flow-tools-routes.mjs) — the same
|
|
8
|
+
* code Drafted's own runner calls, so "search_wiki" means one thing;
|
|
9
|
+
* - submit_output, whose parameters ARE the author's output schema. An answer
|
|
10
|
+
* is checked with src/shared/agent-output.mjs (the same check Drafted's
|
|
11
|
+
* runner uses) and, when it fits, written to the step's output file.
|
|
12
|
+
*
|
|
13
|
+
* Configuration (environment):
|
|
14
|
+
* DRAFTED_FLOW_TOKEN a run-scoped token from POST /api/flow-tools/tokens
|
|
15
|
+
* DRAFTED_URL Drafted's base URL (default https://drafted.live)
|
|
16
|
+
* FLOW_OUTPUT_SCHEMA path to the step's output schema (JSON)
|
|
17
|
+
* FLOW_OUTPUT_FILE where an accepted answer is written (JSON)
|
|
18
|
+
*
|
|
19
|
+
* It holds no other credential and offers no other tool: a backend pointed at
|
|
20
|
+
* this server and nothing else can read what the token allows, and answer.
|
|
21
|
+
*/
|
|
22
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
23
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
24
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
25
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
26
|
+
import { outputErrors } from '../src/shared/agent-output.mjs';
|
|
27
|
+
|
|
28
|
+
const SUBMIT = 'submit_output';
|
|
29
|
+
|
|
30
|
+
function config(env = process.env) {
|
|
31
|
+
const missing = ['DRAFTED_FLOW_TOKEN', 'FLOW_OUTPUT_SCHEMA', 'FLOW_OUTPUT_FILE'].filter((k) => !env[k]);
|
|
32
|
+
if (missing.length) throw new Error(`drafted-flow-tools: set ${missing.join(', ')}`);
|
|
33
|
+
return {
|
|
34
|
+
token: env.DRAFTED_FLOW_TOKEN,
|
|
35
|
+
base: (env.DRAFTED_URL || 'https://drafted.live').replace(/\/+$/, ''),
|
|
36
|
+
schema: JSON.parse(readFileSync(env.FLOW_OUTPUT_SCHEMA, 'utf8')),
|
|
37
|
+
outputFile: env.FLOW_OUTPUT_FILE,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function drafted(cfg, method, path, body) {
|
|
42
|
+
const res = await fetch(cfg.base + '/api/flow-tools' + path, {
|
|
43
|
+
method,
|
|
44
|
+
headers: { 'content-type': 'application/json', 'x-drafted-flow-token': cfg.token },
|
|
45
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
46
|
+
});
|
|
47
|
+
const json = await res.json().catch(() => ({}));
|
|
48
|
+
if (!res.ok) throw new Error(json.error || `Drafted answered ${res.status}`);
|
|
49
|
+
return json;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function main() {
|
|
53
|
+
const cfg = config();
|
|
54
|
+
const { tools } = await drafted(cfg, 'GET', '/tools');
|
|
55
|
+
const names = new Set(tools.map((t) => t.name));
|
|
56
|
+
let accepted = false;
|
|
57
|
+
const server = new Server({ name: 'drafted-flow-tools', version: '1' }, { capabilities: { tools: {} } });
|
|
58
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
59
|
+
tools: [
|
|
60
|
+
...tools.map((t) => ({ name: t.name, description: t.description, inputSchema: t.parameters })),
|
|
61
|
+
{ name: SUBMIT, description: 'Submit your final answer. Call it once, with arguments matching this schema exactly.', inputSchema: cfg.schema },
|
|
62
|
+
],
|
|
63
|
+
}));
|
|
64
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
65
|
+
const { name, arguments: args = {} } = req.params;
|
|
66
|
+
const text = (value) => ({ content: [{ type: 'text', text: typeof value === 'string' ? value : JSON.stringify(value) }] });
|
|
67
|
+
if (name === SUBMIT) {
|
|
68
|
+
if (accepted) return { ...text('An answer was already accepted for this step.'), isError: true };
|
|
69
|
+
const errors = outputErrors(cfg.schema, args);
|
|
70
|
+
if (errors.length) return { ...text(`Not accepted: ${errors.slice(0, 5).join('; ')}. Fix it and call submit_output again.`), isError: true };
|
|
71
|
+
writeFileSync(cfg.outputFile, JSON.stringify(args));
|
|
72
|
+
accepted = true;
|
|
73
|
+
return text('Accepted.');
|
|
74
|
+
}
|
|
75
|
+
if (!names.has(name)) return { ...text(`Tool not available: ${name}`), isError: true };
|
|
76
|
+
try {
|
|
77
|
+
return text(await drafted(cfg, 'POST', '/call', { name, args }));
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return { ...text(`Error: ${err.message}`), isError: true };
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
await server.connect(new StdioServerTransport());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
main().catch((err) => {
|
|
86
|
+
console.error(err.message || err);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
});
|
package/mcp/server.mjs
CHANGED
|
@@ -2830,7 +2830,7 @@ tool('trigger', {
|
|
|
2830
2830
|
executor: z.enum(['queue', 'automation']).optional().describe('[create|update] who runs deliveries: "queue" (default) parks deliveries for a LOCAL agent to poll via pending/claim/complete — this is the Minion hand-off case and what a schedule would use; "automation" runs a skill\'s chain (automationSkill) at a pinned version in Drafted for each delivery, payload as the untrusted input trigger.json — only an org OWNER can create one or move its pin, and the chain must use built-ins Drafted runs (template@1, approval@1, when:). There is no webhook executor: the public ingress was retired on 2026-10-25.'),
|
|
2831
2831
|
automationSkill: z.string().optional().describe('[create|update, executor=automation] the skill whose chain runs: its slug or id, in the project\'s org.'),
|
|
2832
2832
|
automationChain: z.string().optional().describe('[create|update, executor=automation] which named chain (default "default").'),
|
|
2833
|
-
automationSkillVersion: z.number().optional().describe('[create|update, executor=automation] the
|
|
2833
|
+
automationSkillVersion: z.number().optional().describe('[create|update, executor=automation] the skill version to run (default: the skill\'s published version, else its current one). One version runs for every automation of a skill, so pinning one publishes it and moves the skill\'s other automation triggers too: owner only.'),
|
|
2834
2834
|
deliveryId: z.string().optional().describe('[complete] delivery ID returned by claim'),
|
|
2835
2835
|
ok: z.boolean().optional().describe('[complete] whether the claimed work succeeded (default true)'),
|
|
2836
2836
|
error: z.string().optional().describe('[complete] error note when ok=false'),
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.7",
|
|
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
|
"bin": {
|
|
7
7
|
"drafted": "cli/drafted.mjs",
|
|
8
|
-
"drafted-mcp": "mcp/server.mjs"
|
|
8
|
+
"drafted-mcp": "mcp/server.mjs",
|
|
9
|
+
"drafted-flow-tools": "mcp/flow-tools.mjs"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
|
11
12
|
"cli/",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state of a proposed action group, from its actions' statuses: what an
|
|
3
|
+
* approval@1 step decides on. One implementation for every runner: Drafted's
|
|
4
|
+
* automation runner and the `drafted action status` command Causeway's
|
|
5
|
+
* approval@1 shim calls both import it (chain-spec/semantics.json holds the cases).
|
|
6
|
+
*/
|
|
7
|
+
export function actionState(actions) {
|
|
8
|
+
const st = (actions || []).map((a) => a.status);
|
|
9
|
+
if (!st.length) return 'unknown';
|
|
10
|
+
if (st.every((s) => s === 'proposed')) return 'waiting';
|
|
11
|
+
if (st.includes('rejected')) return 'rejected';
|
|
12
|
+
if (st.includes('failed')) return 'failed';
|
|
13
|
+
if (st.every((s) => s === 'executed')) return 'executed';
|
|
14
|
+
return 'approved';
|
|
15
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check an agent step's answer against the output schema its author wrote
|
|
3
|
+
* (the chain contract's subset: type, properties, required, items, enum,
|
|
4
|
+
* additionalProperties: false). One implementation for every runner: Drafted's
|
|
5
|
+
* in-process agent step and the drafted-flow-tools MCP server a Causeway
|
|
6
|
+
* backend calls both import it, so an answer accepted in one is accepted in the
|
|
7
|
+
* other. Returns a list of problems, empty when the answer fits.
|
|
8
|
+
*/
|
|
9
|
+
export function outputErrors(schema, value, at = 'output') {
|
|
10
|
+
const t = schema.type;
|
|
11
|
+
const ok = t === 'integer' ? Number.isInteger(value)
|
|
12
|
+
: t === 'number' ? typeof value === 'number' && Number.isFinite(value)
|
|
13
|
+
: t === 'array' ? Array.isArray(value)
|
|
14
|
+
: t === 'object' ? !!value && typeof value === 'object' && !Array.isArray(value)
|
|
15
|
+
: typeof value === t;
|
|
16
|
+
if (!ok) return [`${at} must be ${t}`];
|
|
17
|
+
if (schema.enum && !schema.enum.some((e) => e === value)) return [`${at} must be one of ${schema.enum.map((e) => JSON.stringify(e)).join(', ')}`];
|
|
18
|
+
const errs = [];
|
|
19
|
+
if (t === 'object') {
|
|
20
|
+
const props = schema.properties || {};
|
|
21
|
+
for (const r of schema.required || []) if (value[r] === undefined) errs.push(`${at}.${r} is required`);
|
|
22
|
+
for (const [k, v] of Object.entries(value)) {
|
|
23
|
+
if (props[k]) errs.push(...outputErrors(props[k], v, `${at}.${k}`));
|
|
24
|
+
else if (schema.additionalProperties === false) errs.push(`${at}.${k} is not allowed`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (t === 'array') value.forEach((v, i) => errs.push(...outputErrors(schema.items, v, `${at}[${i}]`)));
|
|
28
|
+
return errs;
|
|
29
|
+
}
|