drafted 1.11.12 → 1.11.14
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/agent-instructions/global.md +2 -0
- package/cli/drafted.mjs +149 -4
- package/install-mcp.sh +2 -0
- package/mcp/server.mjs +151 -5
- package/package.json +1 -1
|
@@ -29,6 +29,8 @@ Skill rules:
|
|
|
29
29
|
- Search skills before starting repeatable work: skill(action="search") or skill(action="list").
|
|
30
30
|
- Load and follow relevant skills with skill(action="load"). Read supporting skill files when needed.
|
|
31
31
|
- When the user asks to record, distill, create, save, install, or update a skill/procedure/SOP/checklist/method/protocol/playbook, create or update a Drafted skill for the org with skill tools.
|
|
32
|
+
- Every skill has a portable part — its method, SKILL.md, and any script source — and that part always belongs in Drafted, not only in a local repo. When you build a runnable skill, push the source to Drafted (skill action="push", or skill action="add" for prose) and declare how to rebuild it in the skill's `setup:` frontmatter (e.g. ["npm ci"]) so any machine or agent can regenerate it. Do not leave a reusable skill authored only as local files when Drafted is connected.
|
|
33
|
+
- Machine-specific build output (node_modules, downloaded browsers, compiled binaries) is never portable: build it into a `.skillinstall/` directory inside the skill. Drafted always strips `.skillinstall/` from a pushed bundle and skill push auto-gitignores it, so the rebuildable bundle stays local while the method and recipe stay in Drafted.
|
|
32
34
|
- Improve skills when you find a better checklist, standard, or operating method.
|
|
33
35
|
|
|
34
36
|
Project/producible rules:
|
package/cli/drafted.mjs
CHANGED
|
@@ -1828,7 +1828,7 @@ skillCmd
|
|
|
1828
1828
|
function collectSkillTree(dir) {
|
|
1829
1829
|
const root = resolve(dir);
|
|
1830
1830
|
if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
1831
|
-
const SKIP_DIRS = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next', 'target', 'coverage', '.cache', '.turbo', '.gradle', 'Pods', '.terraform']);
|
|
1831
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next', 'target', 'coverage', '.cache', '.turbo', '.gradle', 'Pods', '.terraform', '.skillinstall']);
|
|
1832
1832
|
let ignore = () => false;
|
|
1833
1833
|
const ignPath = join(root, '.skillignore');
|
|
1834
1834
|
if (existsSync(ignPath)) {
|
|
@@ -1854,9 +1854,25 @@ function collectSkillTree(dir) {
|
|
|
1854
1854
|
return out;
|
|
1855
1855
|
}
|
|
1856
1856
|
|
|
1857
|
+
// Ensure the pushed source tree's .gitignore excludes the rebuildable bundle dir,
|
|
1858
|
+
// so a skill's machine-specific build output (built into .skillinstall/ by its
|
|
1859
|
+
// `setup:` recipe) can never be committed. The server already strips .skillinstall/
|
|
1860
|
+
// from the bundle (skill-ingest DENY_DIRS) — this keeps the author's git clean too.
|
|
1861
|
+
// Idempotent; returns true only when it adds the entry.
|
|
1862
|
+
function ensureSkillInstallIgnored(dir) {
|
|
1863
|
+
try {
|
|
1864
|
+
const gi = join(dir, '.gitignore');
|
|
1865
|
+
const existing = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
|
|
1866
|
+
if (existing.split(/\r?\n/).some((l) => l.trim().replace(/\/$/, '') === '.skillinstall')) return false;
|
|
1867
|
+
const body = existing && !existing.endsWith('\n') ? existing + '\n' : existing;
|
|
1868
|
+
writeFileSync(gi, body + '.skillinstall/\n');
|
|
1869
|
+
return true;
|
|
1870
|
+
} catch { return false; }
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1857
1873
|
skillCmd
|
|
1858
1874
|
.command('push')
|
|
1859
|
-
.description('Push a local source tree into a skill (bulk ingest; server
|
|
1875
|
+
.description('Push a local source tree into a Drafted skill (bulk ingest). Stores the portable bits — SKILL.md, scripts, and the setup: recipe; build output is stripped server-side. Build machine-specific output into .skillinstall/ (auto-gitignored, always stripped) and declare its rebuild in setup:.')
|
|
1860
1876
|
.option('--id <id>', 'skill id')
|
|
1861
1877
|
.option('--slug <slug>', 'skill slug (resolved to id)')
|
|
1862
1878
|
.requiredOption('--dir <dir>', 'local source tree to push')
|
|
@@ -1878,8 +1894,137 @@ skillCmd
|
|
|
1878
1894
|
});
|
|
1879
1895
|
const data = await res.json().catch(() => ({}));
|
|
1880
1896
|
if (!res.ok) { emitSkillResult(opts.format, { status: 'error', id, error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
1881
|
-
|
|
1882
|
-
|
|
1897
|
+
const gitignored = ensureSkillInstallIgnored(opts.dir);
|
|
1898
|
+
if (opts.format === 'json') console.log(JSON.stringify({ status: 'ok', id, hash: data.hash, count: data.count, stripped: data.stripped, gitignored, files: data.files }));
|
|
1899
|
+
else console.log(`ok\t${id}\t${data.hash}\tpushed ${data.count}${data.stripped ? `, stripped ${data.stripped}` : ''}${gitignored ? ', gitignored .skillinstall/' : ''}`);
|
|
1900
|
+
});
|
|
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 });
|
|
1883
2028
|
});
|
|
1884
2029
|
|
|
1885
2030
|
program.parse();
|
package/install-mcp.sh
CHANGED
|
@@ -443,6 +443,8 @@ Skill rules:
|
|
|
443
443
|
- Search skills before starting repeatable work: skill(action="search") or skill(action="list").
|
|
444
444
|
- Load and follow relevant skills with skill(action="load"). Read supporting skill files when needed.
|
|
445
445
|
- When the user asks to record, distill, create, save, install, or update a skill/procedure/SOP/checklist/method/protocol/playbook, create or update a Drafted skill for the org with skill tools.
|
|
446
|
+
- Every skill has a portable part — its method, SKILL.md, and any script source — and that part always belongs in Drafted, not only in a local repo. When you build a runnable skill, push the source to Drafted (skill action="push", or skill action="add" for prose) and declare how to rebuild it in the skill's `setup:` frontmatter (e.g. ["npm ci"]) so any machine or agent can regenerate it. Do not leave a reusable skill authored only as local files when Drafted is connected.
|
|
447
|
+
- Machine-specific build output (node_modules, downloaded browsers, compiled binaries) is never portable: build it into a `.skillinstall/` directory inside the skill. Drafted always strips `.skillinstall/` from a pushed bundle and skill push auto-gitignores it, so the rebuildable bundle stays local while the method and recipe stay in Drafted.
|
|
446
448
|
- Improve skills when you find a better checklist, standard, or operating method.
|
|
447
449
|
|
|
448
450
|
Project/producible rules:
|
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 ${
|
|
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
|
}
|
|
@@ -2751,7 +2757,7 @@ tool('layout', 'Auto-arrange frames using graph layout algorithm. Positions conn
|
|
|
2751
2757
|
function collectSkillTreeForPush(dir) {
|
|
2752
2758
|
const root = resolve(dir);
|
|
2753
2759
|
if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
2754
|
-
const SKIP = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next', 'target', 'coverage', '.cache', '.turbo', '.gradle', 'Pods', '.terraform']);
|
|
2760
|
+
const SKIP = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next', 'target', 'coverage', '.cache', '.turbo', '.gradle', 'Pods', '.terraform', '.skillinstall']);
|
|
2755
2761
|
const NUL = String.fromCharCode(0);
|
|
2756
2762
|
let ignore = () => false;
|
|
2757
2763
|
const ign = join(root, '.skillignore');
|
|
@@ -2778,7 +2784,22 @@ function collectSkillTreeForPush(dir) {
|
|
|
2778
2784
|
return out;
|
|
2779
2785
|
}
|
|
2780
2786
|
|
|
2781
|
-
|
|
2787
|
+
// Ensure a pushed source tree's .gitignore excludes the rebuildable bundle dir, so
|
|
2788
|
+
// a skill's machine-specific build output (built into .skillinstall/ by its `setup:`
|
|
2789
|
+
// recipe) can never be committed. The server also strips .skillinstall/ from the
|
|
2790
|
+
// bundle (skill-ingest DENY_DIRS); this keeps the author's git clean. Idempotent.
|
|
2791
|
+
function ensureSkillInstallIgnored(dir) {
|
|
2792
|
+
try {
|
|
2793
|
+
const gi = join(dir, '.gitignore');
|
|
2794
|
+
const existing = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
|
|
2795
|
+
if (existing.split(/\r?\n/).some((l) => l.trim().replace(/\/$/, '') === '.skillinstall')) return false;
|
|
2796
|
+
const body = existing && !existing.endsWith('\n') ? existing + '\n' : existing;
|
|
2797
|
+
writeFileSync(gi, body + '.skillinstall/\n');
|
|
2798
|
+
return true;
|
|
2799
|
+
} catch { return false; }
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/guidelines agents can load and follow. When you build a reusable skill, author it HERE — add for prose, push to ingest a local source tree — so its method and setup: recipe live in Drafted and any machine or agent can reuse it; keep only machine-specific build output local in .skillinstall/ (always stripped on push). Dispatch by `action`: search/load/list for discovery; history for a skill\'s version git-log; add/update/remove for org skills; fork/push for source-only skills; attach/detach for project binding; favorite/unfavorite for personal pins; read_file/update_file for supporting files inside a skill directory.', {
|
|
2782
2803
|
action: z.enum([
|
|
2783
2804
|
'search', 'load', 'list', 'history',
|
|
2784
2805
|
'add', 'update', 'remove',
|
|
@@ -2805,7 +2826,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2805
2826
|
org: z.string().optional().describe('[fork|push|update] resolve/fork into this Drafted org (id or name); scopes the request without switching the session'),
|
|
2806
2827
|
setup: z.array(z.string()).optional().describe('[add|update] setup command(s) (in order) run on materialize to build a source-only skill, e.g. ["npm ci","npm run build"]'),
|
|
2807
2828
|
files: z.array(z.object({ path: z.string(), content: z.string() })).optional().describe('[push] source files to push (path + UTF-8 content); server strips artifacts + enforces caps'),
|
|
2808
|
-
dir: z.string().optional().describe('[push] local directory to push instead of files[]; walked locally (heavy dirs
|
|
2829
|
+
dir: z.string().optional().describe('[push] local directory to push instead of files[]; walked locally (heavy dirs, .skillinstall/, and .skillignore pre-filtered), server re-enforces. On push the dir\'s .gitignore is auto-updated to exclude .skillinstall/ (the rebuildable bundle).'),
|
|
2809
2830
|
deleteMissing: z.boolean().optional().describe('[push] remove stored files not present in the pushed set'),
|
|
2810
2831
|
}, async (args) => {
|
|
2811
2832
|
try {
|
|
@@ -2952,7 +2973,9 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2952
2973
|
let fileList = files;
|
|
2953
2974
|
if (!fileList && dir) fileList = collectSkillTreeForPush(dir);
|
|
2954
2975
|
if (!Array.isArray(fileList) || fileList.length === 0) throw new Error('files[] (non-empty) or dir required for action=push');
|
|
2955
|
-
|
|
2976
|
+
const pushed = await api('POST', `/api/skills/${id}/files/bulk`, { files: fileList, deleteMissing: !!deleteMissing }, extra);
|
|
2977
|
+
if (dir) { try { if (ensureSkillInstallIgnored(dir)) pushed.gitignored = '.skillinstall/'; } catch { /* best-effort */ } }
|
|
2978
|
+
return ok(pushed);
|
|
2956
2979
|
}
|
|
2957
2980
|
case 'attach': {
|
|
2958
2981
|
const { skillId } = args;
|
|
@@ -3406,6 +3429,129 @@ tool('wiki', 'Per-org wiki. Markdown pages with paths as hierarchy. You and othe
|
|
|
3406
3429
|
} catch (error) { return err(error); }
|
|
3407
3430
|
});
|
|
3408
3431
|
|
|
3432
|
+
// ── Collectors ────────────────────────────────────────────────────
|
|
3433
|
+
|
|
3434
|
+
function compactCollectorEntry(c) {
|
|
3435
|
+
if (!c || typeof c !== 'object') return c;
|
|
3436
|
+
return { id: c.id, slug: c.slug, name: c.name, enabled: c.enabled, projectId: c.projectId };
|
|
3437
|
+
}
|
|
3438
|
+
|
|
3439
|
+
// Shape a {collectors:[...]} list with limit/offset pagination + optional compact
|
|
3440
|
+
// mode, mirroring shapeSkillCatalog so large lists stay within token budget.
|
|
3441
|
+
function shapeCollectorList(result, { limit, offset = 0, compact = false } = {}) {
|
|
3442
|
+
if (!Array.isArray(result?.collectors)) return result;
|
|
3443
|
+
const total = result.collectors.length;
|
|
3444
|
+
const start = Math.max(0, Math.floor(Number(offset) || 0));
|
|
3445
|
+
const cap = Math.min(Math.max(1, Math.floor(Number(limit) || 25)), 100);
|
|
3446
|
+
const page = result.collectors.slice(start, start + cap);
|
|
3447
|
+
result.totalAvailable = total;
|
|
3448
|
+
result.offset = start;
|
|
3449
|
+
result.returned = page.length;
|
|
3450
|
+
result.truncated = start + page.length < total;
|
|
3451
|
+
result.collectors = compact ? page.map(compactCollectorEntry) : page;
|
|
3452
|
+
result.note = compact
|
|
3453
|
+
? 'Compact list: {id,slug,name,enabled,projectId} only. Use collector(action="get", id="<id>") for full config; limit/offset to page.'
|
|
3454
|
+
: '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.';
|
|
3455
|
+
return result;
|
|
3456
|
+
}
|
|
3457
|
+
|
|
3458
|
+
// Friendlier message when the agent allowlist gate (requireAgentAccess) rejects.
|
|
3459
|
+
function collectorGateError(e) {
|
|
3460
|
+
if (e?.status === 403 || e?.code === 'agent_disabled') {
|
|
3461
|
+
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.');
|
|
3462
|
+
}
|
|
3463
|
+
return e;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
tool('collector', {
|
|
3467
|
+
action: z.enum(['meta', 'list', 'get', 'create', 'update', 'enable', 'disable', 'delete']).describe('Operation to perform.'),
|
|
3468
|
+
id: z.string().optional().describe('[get|update|enable|disable|delete] collector ID (UUID)'),
|
|
3469
|
+
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.'),
|
|
3470
|
+
name: z.string().optional().describe('[create|update] collector name'),
|
|
3471
|
+
description: z.string().optional().describe('[create|update] one-line description shown to the consumer'),
|
|
3472
|
+
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.'),
|
|
3473
|
+
target: z.object({
|
|
3474
|
+
type: z.enum(['new-record', 'layer', 'frame']).describe('what the run writes against'),
|
|
3475
|
+
layer: z.string().optional(),
|
|
3476
|
+
frameId: z.string().optional(),
|
|
3477
|
+
}).optional().describe('[create|update] where the run writes: a new record, into a layer, or updating a specific frame'),
|
|
3478
|
+
checklist: z.array(z.object({
|
|
3479
|
+
id: z.string().describe('stable item id'),
|
|
3480
|
+
label: z.string().describe('what the consumer is asked for'),
|
|
3481
|
+
description: z.string().optional(),
|
|
3482
|
+
evidence: z.enum(['text', 'file', 'photo', 'none']).optional().describe('evidence required (default none)'),
|
|
3483
|
+
required: z.boolean().optional(),
|
|
3484
|
+
})).optional().describe('[create|update] ordered checklist the Minion guides the consumer through'),
|
|
3485
|
+
output: z.object({
|
|
3486
|
+
mode: z.enum(['generate', 'update', 'annotate']).describe('how the producible is written'),
|
|
3487
|
+
layer: z.string().optional(),
|
|
3488
|
+
lane: z.string().optional(),
|
|
3489
|
+
filenameTemplate: z.string().optional().describe('e.g. "<id>.md"'),
|
|
3490
|
+
skillSlug: z.string().optional().describe('skill that shapes the produced frame'),
|
|
3491
|
+
grouping: z.string().optional().describe('"lane" gives each submission its own lane'),
|
|
3492
|
+
}).optional().describe('[create|update] where/how the producible lands'),
|
|
3493
|
+
limit: z.number().optional().describe('[list] max results per page (default 25, max 100)'),
|
|
3494
|
+
offset: z.number().optional().describe('[list] skip N results for pagination (default 0)'),
|
|
3495
|
+
compact: z.boolean().optional().describe('[list] return only {id,slug,name,enabled,projectId} per collector'),
|
|
3496
|
+
}, async (args) => {
|
|
3497
|
+
try {
|
|
3498
|
+
const { action } = args;
|
|
3499
|
+
switch (action) {
|
|
3500
|
+
case 'meta': {
|
|
3501
|
+
const active = getState().projectId;
|
|
3502
|
+
const pid = active || args.projectId;
|
|
3503
|
+
if (!pid) throw new Error('No active project. Call project(action="open") first, or pass projectId.');
|
|
3504
|
+
// api() auto-appends the active project; add projectId explicitly only when none is active.
|
|
3505
|
+
const path = active ? '/api/collectors/meta' : `/api/collectors/meta?projectId=${encodeURIComponent(pid)}`;
|
|
3506
|
+
return ok(await api('GET', path));
|
|
3507
|
+
}
|
|
3508
|
+
case 'list': {
|
|
3509
|
+
// api() auto-appends the active project as ?projectId — so this lists the
|
|
3510
|
+
// active project's collectors, or all org collectors when none is open.
|
|
3511
|
+
const result = await api('GET', '/api/collectors');
|
|
3512
|
+
return ok(shapeCollectorList(result, { limit: args.limit, offset: args.offset, compact: args.compact }));
|
|
3513
|
+
}
|
|
3514
|
+
case 'get': {
|
|
3515
|
+
if (!args.id) throw new Error('id is required for action=get');
|
|
3516
|
+
return ok(await api('GET', `/api/collectors/${args.id}`));
|
|
3517
|
+
}
|
|
3518
|
+
case 'create': {
|
|
3519
|
+
// projectId lives in the BODY (the POST route reads body, ignores the query).
|
|
3520
|
+
const projectId = args.projectId || getState().projectId;
|
|
3521
|
+
if (!projectId) throw new Error('No active project. Call project(action="open") first, or pass projectId.');
|
|
3522
|
+
const { name, description, target, checklist, output, enabled } = args;
|
|
3523
|
+
if (!name || !target || !Array.isArray(checklist) || !output) {
|
|
3524
|
+
throw new Error('name, target, checklist[], and output are required for action=create');
|
|
3525
|
+
}
|
|
3526
|
+
const body = { projectId, name, target, checklist, output };
|
|
3527
|
+
if (description !== undefined) body.description = description;
|
|
3528
|
+
if (enabled !== undefined) body.enabled = enabled;
|
|
3529
|
+
return ok(await api('POST', '/api/collectors', body));
|
|
3530
|
+
}
|
|
3531
|
+
case 'update': {
|
|
3532
|
+
if (!args.id) throw new Error('id is required for action=update');
|
|
3533
|
+
const body = {};
|
|
3534
|
+
for (const k of ['name', 'description', 'target', 'checklist', 'output', 'enabled']) {
|
|
3535
|
+
if (args[k] !== undefined) body[k] = args[k];
|
|
3536
|
+
}
|
|
3537
|
+
if (Object.keys(body).length === 0) throw new Error('At least one field is required for action=update');
|
|
3538
|
+
return ok(await api('PATCH', `/api/collectors/${args.id}`, body));
|
|
3539
|
+
}
|
|
3540
|
+
case 'enable':
|
|
3541
|
+
case 'disable': {
|
|
3542
|
+
if (!args.id) throw new Error(`id is required for action=${action}`);
|
|
3543
|
+
return ok(await api('PATCH', `/api/collectors/${args.id}`, { enabled: action === 'enable' }));
|
|
3544
|
+
}
|
|
3545
|
+
case 'delete': {
|
|
3546
|
+
if (!args.id) throw new Error('id is required for action=delete');
|
|
3547
|
+
return ok(await api('DELETE', `/api/collectors/${args.id}`));
|
|
3548
|
+
}
|
|
3549
|
+
default:
|
|
3550
|
+
throw new Error(`Unknown collector action: ${action}`);
|
|
3551
|
+
}
|
|
3552
|
+
} catch (error) { return err(collectorGateError(error)); }
|
|
3553
|
+
});
|
|
3554
|
+
|
|
3409
3555
|
// ── Resource: canvas info ─────────────────────────────────────────
|
|
3410
3556
|
|
|
3411
3557
|
server.resource('info', 'drafted://info', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.14",
|
|
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": [
|