drafted 1.13.0 → 1.14.1
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 +56 -56
- package/install-mcp.sh +14 -4
- package/mcp/server.mjs +112 -37
- package/package.json +1 -1
package/cli/drafted.mjs
CHANGED
|
@@ -1911,89 +1911,89 @@ skillCmd
|
|
|
1911
1911
|
else console.log(`ok\t${id}\t${data.hash}\tpushed ${data.count}${data.stripped ? `, stripped ${data.stripped}` : ''}${gitignored ? ', gitignored .skillinstall/' : ''}`);
|
|
1912
1912
|
});
|
|
1913
1913
|
|
|
1914
|
-
// ──
|
|
1915
|
-
//
|
|
1916
|
-
// agent allowlist
|
|
1917
|
-
function
|
|
1914
|
+
// ── Minions: management seam (parity with the MCP `minion` tool) ──
|
|
1915
|
+
// Minions are checklist-driven intake surfaces. Management is behind the
|
|
1916
|
+
// agent allowlist and scoped to the session's active org.
|
|
1917
|
+
function emitMinionResult(format, obj) {
|
|
1918
1918
|
if (format === 'json') { console.log(JSON.stringify(obj)); return; }
|
|
1919
1919
|
console.log([obj.status, obj.id || '', obj.slug || '', obj.name || '', obj.error || ''].join('\t'));
|
|
1920
1920
|
}
|
|
1921
1921
|
|
|
1922
|
-
async function
|
|
1922
|
+
async function minionSetEnabled(id, enabled, format) {
|
|
1923
1923
|
requireLogin();
|
|
1924
1924
|
const server = getServerUrl().replace(/\/$/, '');
|
|
1925
|
-
const res = await authFetch(`${server}/api/
|
|
1925
|
+
const res = await authFetch(`${server}/api/minions/${encodeURIComponent(id)}`, {
|
|
1926
1926
|
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled }),
|
|
1927
1927
|
});
|
|
1928
1928
|
const data = await res.json().catch(() => ({}));
|
|
1929
|
-
if (!res.ok) {
|
|
1930
|
-
const c = data.
|
|
1931
|
-
|
|
1929
|
+
if (!res.ok) { emitMinionResult(format, { status: res.status === 404 ? 'not-found' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
1930
|
+
const c = data.minion || {};
|
|
1931
|
+
emitMinionResult(format, { status: 'ok', id: c.id, slug: c.slug, name: c.name });
|
|
1932
1932
|
}
|
|
1933
1933
|
|
|
1934
|
-
const
|
|
1934
|
+
const minionCmd = program.command('minion').description('Minion management (checklist-driven intake surfaces). Requires the agent allowlist.');
|
|
1935
1935
|
|
|
1936
|
-
|
|
1936
|
+
minionCmd
|
|
1937
1937
|
.command('list')
|
|
1938
|
-
.description('List
|
|
1938
|
+
.description('List Minions (scoped to --project or your active project; all org Minions if neither)')
|
|
1939
1939
|
.option('--project <id>', 'project to scope to (defaults to your active project)')
|
|
1940
1940
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1941
1941
|
.action(async (opts) => {
|
|
1942
1942
|
const pid = opts.project || getActiveProject()?.id;
|
|
1943
|
-
const data = await readApiGet('
|
|
1944
|
-
const rows = data.
|
|
1943
|
+
const data = await readApiGet('minion:list', withQuery('/api/minions', { projectId: pid }));
|
|
1944
|
+
const rows = data.minions || [];
|
|
1945
1945
|
if (opts.format === 'json') { console.log(JSON.stringify(rows)); return; }
|
|
1946
1946
|
for (const c of rows) console.log(`${c.id}\t${c.slug}\t${c.enabled ? 'enabled' : 'disabled'}\t${c.name}`);
|
|
1947
1947
|
});
|
|
1948
1948
|
|
|
1949
|
-
|
|
1949
|
+
minionCmd
|
|
1950
1950
|
.command('get <id>')
|
|
1951
|
-
.description('Get a
|
|
1951
|
+
.description('Get a Minion by id')
|
|
1952
1952
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1953
1953
|
.action(async (id, opts) => {
|
|
1954
|
-
const data = await readApiGet('
|
|
1955
|
-
if (opts.format === 'json') { console.log(JSON.stringify(data.
|
|
1956
|
-
const c = data.
|
|
1954
|
+
const data = await readApiGet('minion:get', `/api/minions/${encodeURIComponent(id)}`);
|
|
1955
|
+
if (opts.format === 'json') { console.log(JSON.stringify(data.minion || data)); return; }
|
|
1956
|
+
const c = data.minion || {};
|
|
1957
1957
|
console.log(`${c.id}\t${c.slug}\t${c.enabled ? 'enabled' : 'disabled'}\t${c.name}`);
|
|
1958
1958
|
});
|
|
1959
1959
|
|
|
1960
|
-
|
|
1960
|
+
minionCmd
|
|
1961
1961
|
.command('meta')
|
|
1962
|
-
.description('Project layers/lanes/frames for building a
|
|
1962
|
+
.description('Project layers/lanes/frames for building a Minion target/output')
|
|
1963
1963
|
.option('--project <id>', 'project (defaults to your active project)')
|
|
1964
1964
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1965
1965
|
.action(async (opts) => {
|
|
1966
1966
|
const pid = opts.project || getActiveProject()?.id;
|
|
1967
1967
|
if (!pid) { console.error('No active project. Pass --project <id> or run `drafted use <project>`.'); process.exit(1); }
|
|
1968
|
-
const data = await readApiGet('
|
|
1968
|
+
const data = await readApiGet('minion:meta', withQuery('/api/minions/meta', { projectId: pid }));
|
|
1969
1969
|
console.log(JSON.stringify(data));
|
|
1970
1970
|
});
|
|
1971
1971
|
|
|
1972
|
-
|
|
1972
|
+
minionCmd
|
|
1973
1973
|
.command('create')
|
|
1974
|
-
.description('Create a
|
|
1974
|
+
.description('Create a Minion from stdin JSON {name,description?,target,checklist,output,enabled?}')
|
|
1975
1975
|
.option('--project <id>', 'project to bind to (defaults to your active project)')
|
|
1976
1976
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1977
1977
|
.action(async (opts) => {
|
|
1978
1978
|
requireLogin();
|
|
1979
1979
|
const pid = opts.project || getActiveProject()?.id;
|
|
1980
|
-
if (!pid) {
|
|
1980
|
+
if (!pid) { emitMinionResult(opts.format, { status: 'error', error: 'no project — pass --project <id> or run `drafted use <project>`' }); process.exit(1); }
|
|
1981
1981
|
let p;
|
|
1982
1982
|
try { p = readStdinJSON(); } catch { console.error('invalid JSON on stdin'); process.exit(1); }
|
|
1983
1983
|
const server = getServerUrl().replace(/\/$/, '');
|
|
1984
|
-
const res = await authFetch(`${server}/api/
|
|
1984
|
+
const res = await authFetch(`${server}/api/minions`, {
|
|
1985
1985
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1986
1986
|
body: JSON.stringify({ projectId: pid, name: p.name, description: p.description, target: p.target, checklist: p.checklist, output: p.output, enabled: p.enabled }),
|
|
1987
1987
|
});
|
|
1988
1988
|
const data = await res.json().catch(() => ({}));
|
|
1989
|
-
if (!res.ok) {
|
|
1990
|
-
const c = data.
|
|
1991
|
-
|
|
1989
|
+
if (!res.ok) { emitMinionResult(opts.format, { status: res.status === 403 ? 'forbidden' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
1990
|
+
const c = data.minion || {};
|
|
1991
|
+
emitMinionResult(opts.format, { status: 'ok', id: c.id, slug: c.slug, name: c.name });
|
|
1992
1992
|
});
|
|
1993
1993
|
|
|
1994
|
-
|
|
1994
|
+
minionCmd
|
|
1995
1995
|
.command('update <id>')
|
|
1996
|
-
.description('Update a
|
|
1996
|
+
.description('Update a Minion from stdin JSON {name?,description?,target?,checklist?,output?,enabled?}')
|
|
1997
1997
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
1998
1998
|
.action(async (id, opts) => {
|
|
1999
1999
|
requireLogin();
|
|
@@ -2003,33 +2003,33 @@ collectorCmd
|
|
|
2003
2003
|
for (const k of ['name', 'description', 'target', 'checklist', 'output', 'enabled']) {
|
|
2004
2004
|
if (p[k] !== undefined) body[k] = p[k];
|
|
2005
2005
|
}
|
|
2006
|
-
if (Object.keys(body).length === 0) {
|
|
2006
|
+
if (Object.keys(body).length === 0) { emitMinionResult(opts.format, { status: 'error', error: 'no fields to update' }); process.exit(1); }
|
|
2007
2007
|
const server = getServerUrl().replace(/\/$/, '');
|
|
2008
|
-
const res = await authFetch(`${server}/api/
|
|
2008
|
+
const res = await authFetch(`${server}/api/minions/${encodeURIComponent(id)}`, {
|
|
2009
2009
|
method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body),
|
|
2010
2010
|
});
|
|
2011
2011
|
const data = await res.json().catch(() => ({}));
|
|
2012
|
-
if (!res.ok) {
|
|
2013
|
-
const c = data.
|
|
2014
|
-
|
|
2012
|
+
if (!res.ok) { emitMinionResult(opts.format, { status: res.status === 404 ? 'not-found' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
2013
|
+
const c = data.minion || {};
|
|
2014
|
+
emitMinionResult(opts.format, { status: 'ok', id: c.id, slug: c.slug, name: c.name });
|
|
2015
2015
|
});
|
|
2016
2016
|
|
|
2017
|
-
|
|
2017
|
+
minionCmd
|
|
2018
2018
|
.command('enable <id>')
|
|
2019
|
-
.description('Make a
|
|
2019
|
+
.description('Make a Minion live')
|
|
2020
2020
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
2021
|
-
.action((id, opts) =>
|
|
2021
|
+
.action((id, opts) => minionSetEnabled(id, true, opts.format));
|
|
2022
2022
|
|
|
2023
|
-
|
|
2023
|
+
minionCmd
|
|
2024
2024
|
.command('disable <id>')
|
|
2025
|
-
.description('Take a
|
|
2025
|
+
.description('Take a Minion offline')
|
|
2026
2026
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
2027
|
-
.action((id, opts) =>
|
|
2027
|
+
.action((id, opts) => minionSetEnabled(id, false, opts.format));
|
|
2028
2028
|
|
|
2029
|
-
async function
|
|
2029
|
+
async function minionTestPost(id, path, body, format) {
|
|
2030
2030
|
requireLogin();
|
|
2031
2031
|
const server = getServerUrl().replace(/\/$/, '');
|
|
2032
|
-
const res = await authFetch(`${server}/api/
|
|
2032
|
+
const res = await authFetch(`${server}/api/minions/${encodeURIComponent(id)}/${path}`, {
|
|
2033
2033
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}),
|
|
2034
2034
|
});
|
|
2035
2035
|
const data = await res.json().catch(() => ({}));
|
|
@@ -2041,39 +2041,39 @@ async function collectorTestPost(id, path, body, format) {
|
|
|
2041
2041
|
console.log(JSON.stringify(data));
|
|
2042
2042
|
}
|
|
2043
2043
|
|
|
2044
|
-
|
|
2044
|
+
minionCmd
|
|
2045
2045
|
.command('test-start <id>')
|
|
2046
|
-
.description('QA: start (or resume) a test run of a
|
|
2046
|
+
.description('QA: start (or resume) a test run of a Minion you own — works even when disabled. --fresh for a new run.')
|
|
2047
2047
|
.option('--fresh', 'start a brand-new run instead of resuming', false)
|
|
2048
2048
|
.option('--format <fmt>', 'output format: json (default)', 'json')
|
|
2049
|
-
.action((id, opts) =>
|
|
2049
|
+
.action((id, opts) => minionTestPost(id, 'test-start', { fresh: !!opts.fresh }, opts.format));
|
|
2050
2050
|
|
|
2051
|
-
|
|
2051
|
+
minionCmd
|
|
2052
2052
|
.command('test-say <id>')
|
|
2053
2053
|
.description('QA: send a text answer to your test run; returns the agent reply, checklist state, pending actions.')
|
|
2054
2054
|
.requiredOption('--text <text>', 'the consumer message to send')
|
|
2055
2055
|
.option('--format <fmt>', 'output format: json (default)', 'json')
|
|
2056
|
-
.action((id, opts) =>
|
|
2056
|
+
.action((id, opts) => minionTestPost(id, 'test-message', { content: opts.text }, opts.format));
|
|
2057
2057
|
|
|
2058
|
-
|
|
2058
|
+
minionCmd
|
|
2059
2059
|
.command('test-resolve <id>')
|
|
2060
2060
|
.description('QA: approve or reject a pending destructive action in your test run.')
|
|
2061
2061
|
.requiredOption('--action <actionId>', 'the pending action id')
|
|
2062
2062
|
.option('--reject', 'reject instead of approve', false)
|
|
2063
2063
|
.option('--format <fmt>', 'output format: json (default)', 'json')
|
|
2064
|
-
.action((id, opts) =>
|
|
2064
|
+
.action((id, opts) => minionTestPost(id, 'test-resolve', { actionId: opts.action, approve: !opts.reject }, opts.format));
|
|
2065
2065
|
|
|
2066
|
-
|
|
2066
|
+
minionCmd
|
|
2067
2067
|
.command('delete <id>')
|
|
2068
|
-
.description('Delete a
|
|
2068
|
+
.description('Delete a Minion (past submissions are kept as history)')
|
|
2069
2069
|
.option('--format <fmt>', 'output format: json or text', 'text')
|
|
2070
2070
|
.action(async (id, opts) => {
|
|
2071
2071
|
requireLogin();
|
|
2072
2072
|
const server = getServerUrl().replace(/\/$/, '');
|
|
2073
|
-
const res = await authFetch(`${server}/api/
|
|
2073
|
+
const res = await authFetch(`${server}/api/minions/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
2074
2074
|
const data = await res.json().catch(() => ({}));
|
|
2075
|
-
if (!res.ok) {
|
|
2076
|
-
|
|
2075
|
+
if (!res.ok) { emitMinionResult(opts.format, { status: res.status === 404 ? 'not-found' : 'error', error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
2076
|
+
emitMinionResult(opts.format, { status: 'ok', id: data.id || id });
|
|
2077
2077
|
});
|
|
2078
2078
|
|
|
2079
2079
|
program.parse();
|
package/install-mcp.sh
CHANGED
|
@@ -227,7 +227,17 @@ if [ -n "$STALE_DRAFTED_BIN" ]; then
|
|
|
227
227
|
fi
|
|
228
228
|
fi
|
|
229
229
|
|
|
230
|
-
npm config set prefix
|
|
230
|
+
# Do NOT `npm config set prefix` — that repoints the user's GLOBAL npm prefix, so every
|
|
231
|
+
# `npm install -g <pkg>` they ever run lands in our dir AND our uninstall (`rm -rf ~/.drafted`)
|
|
232
|
+
# would wipe all their other globals. Install drafted with an explicit per-command --prefix
|
|
233
|
+
# instead (below), and HEAL any global prefix pin an older version of this installer wrote so
|
|
234
|
+
# the user's default is restored. Preserve every other ~/.npmrc line (auth tokens, etc.).
|
|
235
|
+
NPMRC="${npm_config_userconfig:-$HOME/.npmrc}"
|
|
236
|
+
if [ -f "$NPMRC" ] && grep -Eq '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC"; then
|
|
237
|
+
tmp_npmrc="$(mktemp)"
|
|
238
|
+
grep -Ev '^[[:space:]]*prefix[[:space:]]*=.*/\.drafted/npm-global/?[[:space:]]*$' "$NPMRC" > "$tmp_npmrc" && cat "$tmp_npmrc" > "$NPMRC"
|
|
239
|
+
rm -f "$tmp_npmrc"
|
|
240
|
+
fi
|
|
231
241
|
export PATH="$NPM_GLOBAL_PREFIX/bin:$PATH"
|
|
232
242
|
|
|
233
243
|
# Persist the prefix's bin dir at the FRONT of PATH for future shells — without
|
|
@@ -262,7 +272,7 @@ step "Installing Drafted"
|
|
|
262
272
|
install_drafted_pkg() {
|
|
263
273
|
attempts=5; delay=4; n=1
|
|
264
274
|
while :; do
|
|
265
|
-
if npm install -g drafted@latest --force; then return 0; fi
|
|
275
|
+
if npm install -g drafted@latest --force --prefix "$NPM_GLOBAL_PREFIX"; then return 0; fi
|
|
266
276
|
if [ "$n" -ge "$attempts" ]; then return 1; fi
|
|
267
277
|
echo -e " ${YELLOW}npm install failed (attempt $n/$attempts) — retrying in ${delay}s (a new release may still be propagating to the npm CDN)...${RESET}"
|
|
268
278
|
sleep "$delay"
|
|
@@ -274,7 +284,7 @@ if ! install_drafted_pkg; then
|
|
|
274
284
|
exit 1
|
|
275
285
|
fi
|
|
276
286
|
hash -r 2>/dev/null || true
|
|
277
|
-
NPM_ROOT="$(npm root -g 2>/dev/null || true)"
|
|
287
|
+
NPM_ROOT="$(npm root -g --prefix "$NPM_GLOBAL_PREFIX" 2>/dev/null || true)"
|
|
278
288
|
MCP_SERVER_MODULE="$NPM_ROOT/drafted/mcp/server.mjs"
|
|
279
289
|
if [ -n "$NPM_ROOT" ] && [ -f "$MCP_SERVER_MODULE" ]; then
|
|
280
290
|
node -e "import('node:url').then(({ pathToFileURL }) => import(pathToFileURL(process.argv[1]).href)).then(() => process.exit(0), (err) => { console.error(err); process.exit(1); })" "$MCP_SERVER_MODULE"
|
|
@@ -1288,7 +1298,7 @@ echo ""
|
|
|
1288
1298
|
echo -e " ${DIM}MCP name:${RESET} ${BOLD}$INSTALL_NAME${RESET}"
|
|
1289
1299
|
echo -e " ${DIM}Server:${RESET} ${BOLD}$INSTALL_SERVER${RESET}"
|
|
1290
1300
|
echo -e " ${DIM}To update production:${RESET} rerun curl -fsSL https://drafted.live/install.sh | bash"
|
|
1291
|
-
echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted && rm -rf ~/.drafted"
|
|
1301
|
+
echo -e " ${DIM}To uninstall:${RESET} npm uninstall -g drafted --prefix ~/.drafted/npm-global && rm -rf ~/.drafted"
|
|
1292
1302
|
echo ""
|
|
1293
1303
|
echo -e "${YELLOW}${BOLD}"
|
|
1294
1304
|
echo " ┌─────────────────────────────────────────────────────────┐"
|
package/mcp/server.mjs
CHANGED
|
@@ -144,7 +144,7 @@ const REMOTE_JSON_STRING_PARAMS = {
|
|
|
144
144
|
wiki: ['frontmatter', 'pages'],
|
|
145
145
|
project: ['layers'],
|
|
146
146
|
template: ['layers'],
|
|
147
|
-
|
|
147
|
+
minion: ['target', 'checklist', 'output'],
|
|
148
148
|
};
|
|
149
149
|
|
|
150
150
|
// Remove sentences that reference local-file params from a tool description,
|
|
@@ -258,8 +258,8 @@ const TOOL_ANNOTATIONS = {
|
|
|
258
258
|
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.' },
|
|
259
259
|
wiki: { title: 'Wiki', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Per-org wiki. Markdown pages with paths as hierarchy. Dispatch by `action`.' },
|
|
260
260
|
|
|
261
|
-
//
|
|
262
|
-
|
|
261
|
+
// Minions — checklist-driven intake surfaces bound to a project
|
|
262
|
+
minion: { title: 'Minions', readOnlyHint: false, destructiveHint: true, openWorldHint: false, description: 'Manage Minions: 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. QA your own Minions with test_start/test_say/test_resolve — drive the checklist conversation yourself (works even when disabled) and verify it produces the right Doc/Sheet output. Requires the agent allowlist.' },
|
|
263
263
|
};
|
|
264
264
|
|
|
265
265
|
function isMutatingToolCall(name, args = {}) {
|
|
@@ -285,7 +285,7 @@ function isMutatingToolCall(name, args = {}) {
|
|
|
285
285
|
return ['add', 'update', 'remove', 'attach', 'detach', 'favorite', 'unfavorite', 'update_file'].includes(action);
|
|
286
286
|
case 'wiki':
|
|
287
287
|
return ['log', 'write', 'edit', 'mv', 'rm', 'source-register', 'bulk-write'].includes(action);
|
|
288
|
-
case '
|
|
288
|
+
case 'minion':
|
|
289
289
|
return ['create', 'update', 'enable', 'disable', 'delete', 'test_start', 'test_say', 'test_resolve'].includes(action);
|
|
290
290
|
case 'rm':
|
|
291
291
|
case 'shape':
|
|
@@ -1462,10 +1462,85 @@ async function consumePendingDeviceCode() {
|
|
|
1462
1462
|
// meaningless and disruptive — it returns spurious sign-in URLs and, on login,
|
|
1463
1463
|
// spawns a server-side browser-open and blocks polling until timeout. Register
|
|
1464
1464
|
// it only on stdio.
|
|
1465
|
-
|
|
1465
|
+
// --- Local-install auth surface: the desktop app, never a device link ---
|
|
1466
|
+
// The stdio installer ALWAYS installs the Drafted desktop app alongside the MCP, so on a
|
|
1467
|
+
// local install the app IS the sign-in surface. Spawning it hands off to the always-running
|
|
1468
|
+
// instance (single-instance plugin), which opens the sign-in window and starts the native
|
|
1469
|
+
// cookie->auth.json capture that this MCP reads via getBootstrapSessionId(). Only the web MCP
|
|
1470
|
+
// is app-less, and that path authenticates via OAuth2 in the browser — never this tool. The
|
|
1471
|
+
// device-code flow below is kept ONLY as a fallback for platforms without the desktop app.
|
|
1472
|
+
function desktopAppBinary() {
|
|
1473
|
+
try {
|
|
1474
|
+
if (process.platform === 'darwin') {
|
|
1475
|
+
const p = '/Applications/Drafted.app/Contents/MacOS/drafted-desktop';
|
|
1476
|
+
return existsSync(p) ? p : null;
|
|
1477
|
+
}
|
|
1478
|
+
if (process.platform === 'win32') {
|
|
1479
|
+
const p = join(process.env.LOCALAPPDATA || '', 'Programs', 'Drafted', 'Drafted.exe');
|
|
1480
|
+
return existsSync(p) ? p : null;
|
|
1481
|
+
}
|
|
1482
|
+
} catch { /* fall through to no-app */ }
|
|
1483
|
+
return null; // e.g. Linux — no desktop app → device-code fallback
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async function launchDesktopSignin() {
|
|
1487
|
+
const bin = desktopAppBinary();
|
|
1488
|
+
if (!bin) return false;
|
|
1489
|
+
try {
|
|
1490
|
+
const { spawn } = await import('child_process');
|
|
1491
|
+
// If the app is already running (macOS KeepAlive normally guarantees it), the single-instance
|
|
1492
|
+
// handler opens the sign-in window and DRAFTED_OPEN_LOGIN is ignored. If it isn't running,
|
|
1493
|
+
// the fresh primary instance honors DRAFTED_OPEN_LOGIN=1 and opens sign-in on boot.
|
|
1494
|
+
const child = spawn(bin, [], {
|
|
1495
|
+
detached: true,
|
|
1496
|
+
stdio: 'ignore',
|
|
1497
|
+
env: { ...process.env, DRAFTED_OPEN_LOGIN: '1' },
|
|
1498
|
+
});
|
|
1499
|
+
child.unref();
|
|
1500
|
+
return true;
|
|
1501
|
+
} catch { return false; }
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
// Poll auth.json (written by the desktop app's cookie->auth.json bridge on in-app sign-in)
|
|
1505
|
+
// until a valid session id lands or the deadline passes.
|
|
1506
|
+
async function waitForBootstrapAuth(deadline) {
|
|
1507
|
+
while (Date.now() < deadline) {
|
|
1508
|
+
const sid = getBootstrapSessionId();
|
|
1509
|
+
if (sid) return sid;
|
|
1510
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
1511
|
+
}
|
|
1512
|
+
return null;
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
if (!isRemote) tool('auth', 'Sign in to Drafted. On a local install the DESKTOP APP is the sign-in surface: both actions open the Drafted app to its sign-in window — no device link is shown. `action=login` opens the app and waits for the in-app sign-in to complete; `action=get_link` opens the app and returns immediately (the next Drafted tool call picks up the captured session). A device-code link is used ONLY as a fallback on platforms without the desktop app (the web MCP uses OAuth2, not this tool).', {
|
|
1466
1516
|
action: z.enum(['get_link', 'login']).describe('Operation to perform.'),
|
|
1467
1517
|
}, async ({ action }) => {
|
|
1468
1518
|
try {
|
|
1519
|
+
// Local install → open the desktop app's sign-in window (no link). Falls through to the
|
|
1520
|
+
// device-code flow below only when no desktop app is installed on this platform.
|
|
1521
|
+
{
|
|
1522
|
+
const existing = getState().sessionId || getBootstrapSessionId();
|
|
1523
|
+
if (existing) {
|
|
1524
|
+
try {
|
|
1525
|
+
const meRes = await serverFetch(`${getServerUrl()}/auth/me`, { headers: { Cookie: `gc_session=${existing}` } });
|
|
1526
|
+
if (meRes.ok) {
|
|
1527
|
+
const me = await meRes.json();
|
|
1528
|
+
return ok({ status: 'already_authenticated', userId: me.userId, email: me.userEmail, org: me.currentOrg?.name });
|
|
1529
|
+
}
|
|
1530
|
+
} catch { /* stale session — continue to sign-in */ }
|
|
1531
|
+
}
|
|
1532
|
+
if (await launchDesktopSignin()) {
|
|
1533
|
+
if (action === 'get_link') {
|
|
1534
|
+
return ok('Opening the Drafted app to sign in — approve in the app window, then retry your request.');
|
|
1535
|
+
}
|
|
1536
|
+
const sid = await waitForBootstrapAuth(Date.now() + 180000);
|
|
1537
|
+
if (!sid) throw new Error('Timed out waiting for sign-in. Complete sign-in in the Drafted app window, then retry.');
|
|
1538
|
+
getState().sessionId = null;
|
|
1539
|
+
await cloneSession();
|
|
1540
|
+
connectAgentWs();
|
|
1541
|
+
return ok({ status: 'logged_in', via: 'desktop-app' });
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1469
1544
|
if (action === 'get_link') {
|
|
1470
1545
|
const codeRes = await serverFetch(`${getServerUrl()}/auth/device/code`, { method: 'POST' });
|
|
1471
1546
|
if (!codeRes.ok) throw new Error(`Failed to start device authorization (HTTP ${codeRes.status})`);
|
|
@@ -4096,51 +4171,51 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
4096
4171
|
} catch (error) { return err(error); }
|
|
4097
4172
|
});
|
|
4098
4173
|
|
|
4099
|
-
// ──
|
|
4174
|
+
// ── Minions ───────────────────────────────────────────────────────
|
|
4100
4175
|
|
|
4101
|
-
function
|
|
4176
|
+
function compactMinionEntry(c) {
|
|
4102
4177
|
if (!c || typeof c !== 'object') return c;
|
|
4103
4178
|
return { id: c.id, slug: c.slug, name: c.name, enabled: c.enabled, projectId: c.projectId };
|
|
4104
4179
|
}
|
|
4105
4180
|
|
|
4106
|
-
// Shape a {
|
|
4181
|
+
// Shape a {minions:[...]} list with limit/offset pagination + optional compact
|
|
4107
4182
|
// mode, mirroring shapeSkillCatalog so large lists stay within token budget.
|
|
4108
|
-
function
|
|
4109
|
-
if (!Array.isArray(result?.
|
|
4110
|
-
const total = result.
|
|
4183
|
+
function shapeMinionList(result, { limit, offset = 0, compact = false } = {}) {
|
|
4184
|
+
if (!Array.isArray(result?.minions)) return result;
|
|
4185
|
+
const total = result.minions.length;
|
|
4111
4186
|
const start = Math.max(0, Math.floor(Number(offset) || 0));
|
|
4112
4187
|
const cap = Math.min(Math.max(1, Math.floor(Number(limit) || 25)), 100);
|
|
4113
|
-
const page = result.
|
|
4188
|
+
const page = result.minions.slice(start, start + cap);
|
|
4114
4189
|
result.totalAvailable = total;
|
|
4115
4190
|
result.offset = start;
|
|
4116
4191
|
result.returned = page.length;
|
|
4117
4192
|
result.truncated = start + page.length < total;
|
|
4118
|
-
result.
|
|
4193
|
+
result.minions = compact ? page.map(compactMinionEntry) : page;
|
|
4119
4194
|
result.note = compact
|
|
4120
|
-
? 'Compact list: {id,slug,name,enabled,projectId} only. Use
|
|
4121
|
-
: '
|
|
4195
|
+
? 'Compact list: {id,slug,name,enabled,projectId} only. Use minion(action="get", id="<id>") for full config; limit/offset to page.'
|
|
4196
|
+
: 'Minions are scoped to the active project (all org Minions when no project is open). Use limit/offset to page; compact=true for a leaner list.';
|
|
4122
4197
|
return result;
|
|
4123
4198
|
}
|
|
4124
4199
|
|
|
4125
4200
|
// Friendlier message when the agent allowlist gate (requireAgentAccess) rejects.
|
|
4126
|
-
function
|
|
4201
|
+
function minionGateError(e) {
|
|
4127
4202
|
if (e?.status === 403 || e?.code === 'agent_disabled') {
|
|
4128
|
-
return new Error('
|
|
4203
|
+
return new Error('Minion 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.');
|
|
4129
4204
|
}
|
|
4130
4205
|
return e;
|
|
4131
4206
|
}
|
|
4132
4207
|
|
|
4133
|
-
tool('
|
|
4134
|
-
action: z.enum(['meta', 'list', 'get', 'create', 'update', 'enable', 'disable', 'delete', 'test_start', 'test_say', 'test_resolve']).describe('Operation to perform. test_* drive a QA conversation against a
|
|
4135
|
-
id: z.string().optional().describe('[get|update|enable|disable|delete|test_*]
|
|
4208
|
+
tool('minion', {
|
|
4209
|
+
action: z.enum(['meta', 'list', 'get', 'create', 'update', 'enable', 'disable', 'delete', 'test_start', 'test_say', 'test_resolve']).describe('Operation to perform. test_* drive a QA conversation against a Minion you own (even disabled) to verify it end-to-end.'),
|
|
4210
|
+
id: z.string().optional().describe('[get|update|enable|disable|delete|test_*] Minion ID (UUID)'),
|
|
4136
4211
|
text: z.string().optional().describe('[test_say] the consumer message to send to your test run'),
|
|
4137
4212
|
fresh: z.boolean().optional().describe('[test_start] start a brand-new run instead of resuming your latest'),
|
|
4138
4213
|
actionId: z.string().optional().describe('[test_resolve] id of the pending action to resolve'),
|
|
4139
4214
|
approve: z.boolean().optional().describe('[test_resolve] approve (default true) or reject the pending action'),
|
|
4140
4215
|
projectId: z.string().optional().describe('[create|meta] project to bind/scope to (defaults to the active project). The org derives from this project — open the target project first via project(action="open") if none is active.'),
|
|
4141
|
-
name: z.string().optional().describe('[create|update]
|
|
4216
|
+
name: z.string().optional().describe('[create|update] Minion name'),
|
|
4142
4217
|
description: z.string().optional().describe('[create|update] one-line description shown to the consumer'),
|
|
4143
|
-
enabled: z.boolean().optional().describe('[create|update] whether the
|
|
4218
|
+
enabled: z.boolean().optional().describe('[create|update] whether the Minion is live; a disabled Minion 404s on its /c/<slug> link. enable/disable set this directly.'),
|
|
4144
4219
|
target: z.object({
|
|
4145
4220
|
type: z.enum(['new-record', 'layer', 'frame']).describe('what the run writes against'),
|
|
4146
4221
|
layer: z.string().optional(),
|
|
@@ -4169,7 +4244,7 @@ tool('collector', {
|
|
|
4169
4244
|
}).optional().describe('[create|update] where/how the producible lands'),
|
|
4170
4245
|
limit: z.number().optional().describe('[list] max results per page (default 25, max 100)'),
|
|
4171
4246
|
offset: z.number().optional().describe('[list] skip N results for pagination (default 0)'),
|
|
4172
|
-
compact: z.boolean().optional().describe('[list] return only {id,slug,name,enabled,projectId} per
|
|
4247
|
+
compact: z.boolean().optional().describe('[list] return only {id,slug,name,enabled,projectId} per Minion'),
|
|
4173
4248
|
}, async (args) => {
|
|
4174
4249
|
try {
|
|
4175
4250
|
const { action } = args;
|
|
@@ -4179,18 +4254,18 @@ tool('collector', {
|
|
|
4179
4254
|
const pid = active || args.projectId;
|
|
4180
4255
|
if (!pid) throw new Error('No active project. Call project(action="open") first, or pass projectId.');
|
|
4181
4256
|
// api() auto-appends the active project; add projectId explicitly only when none is active.
|
|
4182
|
-
const path = active ? '/api/
|
|
4257
|
+
const path = active ? '/api/minions/meta' : `/api/minions/meta?projectId=${encodeURIComponent(pid)}`;
|
|
4183
4258
|
return ok(await api('GET', path));
|
|
4184
4259
|
}
|
|
4185
4260
|
case 'list': {
|
|
4186
4261
|
// api() auto-appends the active project as ?projectId — so this lists the
|
|
4187
|
-
// active project's
|
|
4188
|
-
const result = await api('GET', '/api/
|
|
4189
|
-
return ok(
|
|
4262
|
+
// active project's Minions, or all org Minions when none is open.
|
|
4263
|
+
const result = await api('GET', '/api/minions');
|
|
4264
|
+
return ok(shapeMinionList(result, { limit: args.limit, offset: args.offset, compact: args.compact }));
|
|
4190
4265
|
}
|
|
4191
4266
|
case 'get': {
|
|
4192
4267
|
if (!args.id) throw new Error('id is required for action=get');
|
|
4193
|
-
return ok(await api('GET', `/api/
|
|
4268
|
+
return ok(await api('GET', `/api/minions/${args.id}`));
|
|
4194
4269
|
}
|
|
4195
4270
|
case 'create': {
|
|
4196
4271
|
// projectId lives in the BODY (the POST route reads body, ignores the query).
|
|
@@ -4203,7 +4278,7 @@ tool('collector', {
|
|
|
4203
4278
|
const body = { projectId, name, target, checklist, output };
|
|
4204
4279
|
if (description !== undefined) body.description = description;
|
|
4205
4280
|
if (enabled !== undefined) body.enabled = enabled;
|
|
4206
|
-
return ok(await api('POST', '/api/
|
|
4281
|
+
return ok(await api('POST', '/api/minions', body));
|
|
4207
4282
|
}
|
|
4208
4283
|
case 'update': {
|
|
4209
4284
|
if (!args.id) throw new Error('id is required for action=update');
|
|
@@ -4212,35 +4287,35 @@ tool('collector', {
|
|
|
4212
4287
|
if (args[k] !== undefined) body[k] = args[k];
|
|
4213
4288
|
}
|
|
4214
4289
|
if (Object.keys(body).length === 0) throw new Error('At least one field is required for action=update');
|
|
4215
|
-
return ok(await api('PATCH', `/api/
|
|
4290
|
+
return ok(await api('PATCH', `/api/minions/${args.id}`, body));
|
|
4216
4291
|
}
|
|
4217
4292
|
case 'enable':
|
|
4218
4293
|
case 'disable': {
|
|
4219
4294
|
if (!args.id) throw new Error(`id is required for action=${action}`);
|
|
4220
|
-
return ok(await api('PATCH', `/api/
|
|
4295
|
+
return ok(await api('PATCH', `/api/minions/${args.id}`, { enabled: action === 'enable' }));
|
|
4221
4296
|
}
|
|
4222
4297
|
case 'delete': {
|
|
4223
4298
|
if (!args.id) throw new Error('id is required for action=delete');
|
|
4224
|
-
return ok(await api('DELETE', `/api/
|
|
4299
|
+
return ok(await api('DELETE', `/api/minions/${args.id}`));
|
|
4225
4300
|
}
|
|
4226
4301
|
case 'test_start': {
|
|
4227
4302
|
if (!args.id) throw new Error('id is required for action=test_start');
|
|
4228
|
-
return ok(await api('POST', `/api/
|
|
4303
|
+
return ok(await api('POST', `/api/minions/${args.id}/test-start`, { fresh: !!args.fresh }));
|
|
4229
4304
|
}
|
|
4230
4305
|
case 'test_say': {
|
|
4231
4306
|
if (!args.id) throw new Error('id is required for action=test_say');
|
|
4232
4307
|
if (!args.text) throw new Error('text is required for action=test_say');
|
|
4233
|
-
return ok(await api('POST', `/api/
|
|
4308
|
+
return ok(await api('POST', `/api/minions/${args.id}/test-message`, { content: args.text }));
|
|
4234
4309
|
}
|
|
4235
4310
|
case 'test_resolve': {
|
|
4236
4311
|
if (!args.id) throw new Error('id is required for action=test_resolve');
|
|
4237
4312
|
if (!args.actionId) throw new Error('actionId is required for action=test_resolve');
|
|
4238
|
-
return ok(await api('POST', `/api/
|
|
4313
|
+
return ok(await api('POST', `/api/minions/${args.id}/test-resolve`, { actionId: args.actionId, approve: args.approve !== false }));
|
|
4239
4314
|
}
|
|
4240
4315
|
default:
|
|
4241
|
-
throw new Error(`Unknown
|
|
4316
|
+
throw new Error(`Unknown minion action: ${action}`);
|
|
4242
4317
|
}
|
|
4243
|
-
} catch (error) { return err(
|
|
4318
|
+
} catch (error) { return err(minionGateError(error)); }
|
|
4244
4319
|
});
|
|
4245
4320
|
|
|
4246
4321
|
// ── Resource: canvas info ─────────────────────────────────────────
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.1",
|
|
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": [
|