drafted 1.11.11 → 1.11.13
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 +63 -7
- package/install-mcp.sh +2 -0
- package/mcp/server.mjs +21 -4
- 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
|
@@ -23,7 +23,12 @@ const __dirname = dirname(__filename);
|
|
|
23
23
|
const DEFAULT_STATE_DIR = join(homedir(), '.drafted');
|
|
24
24
|
const DEFAULT_PROJECTS_FILE = join(DEFAULT_STATE_DIR, 'projects.json');
|
|
25
25
|
const DEFAULT_PID_FILE = join(DEFAULT_STATE_DIR, 'server.pid');
|
|
26
|
-
|
|
26
|
+
// Honor DRAFTED_AUTH_FILE so the CLI reads the SAME credential file the MCP does.
|
|
27
|
+
// install-mcp.sh's --local mode points the MCP at auth.local.json via this env var;
|
|
28
|
+
// without honoring it here the CLI would read auth.json while the MCP used a
|
|
29
|
+
// different file, and the two would disagree about who is signed in (DRAFT-32).
|
|
30
|
+
const DEFAULT_AUTH_FILE = process.env.DRAFTED_AUTH_FILE || join(DEFAULT_STATE_DIR, 'auth.json');
|
|
31
|
+
const DEFAULT_CONFIG_FILE = join(DEFAULT_STATE_DIR, 'config.json');
|
|
27
32
|
const DEFAULT_PORT = 3477;
|
|
28
33
|
const PACKAGE_VERSION = (() => {
|
|
29
34
|
try {
|
|
@@ -133,11 +138,37 @@ function isServerRunning() {
|
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
140
|
|
|
136
|
-
// Helper:
|
|
141
|
+
// Helper: Read the install's server URL from ~/.drafted/config.json. This is the
|
|
142
|
+
// SAME file the MCP server reads (written by install-mcp.sh), so the CLI and MCP
|
|
143
|
+
// resolve to the same deployment.
|
|
144
|
+
function readConfigServer() {
|
|
145
|
+
try {
|
|
146
|
+
if (existsSync(DEFAULT_CONFIG_FILE)) {
|
|
147
|
+
const cfg = JSON.parse(readFileSync(DEFAULT_CONFIG_FILE, 'utf8'));
|
|
148
|
+
return cfg.server || cfg.publicUrl || null;
|
|
149
|
+
}
|
|
150
|
+
} catch { /* ignore unreadable/corrupt config */ }
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Helper: Get server URL.
|
|
155
|
+
// Precedence mirrors the MCP so a CLI invocation transparently targets the SAME
|
|
156
|
+
// server the MCP (and the stored session) use — fixing DRAFT-32, where a CLI
|
|
157
|
+
// holding a valid cloud session defaulted to localhost and got "Unauthenticated":
|
|
158
|
+
// 1. DRAFTED_SERVER env / --server flag (explicit override; --server sets the env var)
|
|
159
|
+
// 2. the auth file's `server` field — where the stored session is actually valid
|
|
160
|
+
// 3. ~/.drafted/config.json (written by install-mcp.sh; the file the MCP reads)
|
|
161
|
+
// 4. localhost (no cloud install configured)
|
|
162
|
+
// (2) is preferred over (3) because the session cookie is only valid on the
|
|
163
|
+
// server it was minted for; sending it anywhere else just yields a 401.
|
|
137
164
|
function getServerUrl() {
|
|
138
165
|
if (process.env.DRAFTED_SERVER) {
|
|
139
166
|
return process.env.DRAFTED_SERVER.replace(/\/$/, '');
|
|
140
167
|
}
|
|
168
|
+
const fromAuth = readAuth()?.server;
|
|
169
|
+
if (fromAuth) return String(fromAuth).replace(/\/$/, '');
|
|
170
|
+
const fromConfig = readConfigServer();
|
|
171
|
+
if (fromConfig) return String(fromConfig).replace(/\/$/, '');
|
|
141
172
|
return `http://localhost:${process.env.DRAFTED_PORT || DEFAULT_PORT}`;
|
|
142
173
|
}
|
|
143
174
|
|
|
@@ -252,7 +283,15 @@ async function readApiGet(command, apiPath, org) {
|
|
|
252
283
|
}
|
|
253
284
|
const data = await res.json().catch(() => ({}));
|
|
254
285
|
if (!res.ok) {
|
|
255
|
-
|
|
286
|
+
let msg = data.error || `HTTP ${res.status}`;
|
|
287
|
+
// A valid session now targets the server it was minted for (see getServerUrl),
|
|
288
|
+
// so a 401/403 here means the credential is genuinely missing/expired — surface
|
|
289
|
+
// an actionable next step instead of the bare server "Unauthenticated" string.
|
|
290
|
+
if (res.status === 401 || res.status === 403) {
|
|
291
|
+
msg = `Not authenticated for ${serverUrl} — your Drafted session is missing or expired. `
|
|
292
|
+
+ `Run \`drafted login\` to sign in. `
|
|
293
|
+
+ `The CLI and MCP share ${DEFAULT_AUTH_FILE}, so signing in once works for both.`;
|
|
294
|
+
}
|
|
256
295
|
jsonOut(false, command, msg);
|
|
257
296
|
console.error(`❌ ${msg}`);
|
|
258
297
|
process.exit(1);
|
|
@@ -1789,7 +1828,7 @@ skillCmd
|
|
|
1789
1828
|
function collectSkillTree(dir) {
|
|
1790
1829
|
const root = resolve(dir);
|
|
1791
1830
|
if (!existsSync(root) || !statSync(root).isDirectory()) throw new Error(`not a directory: ${dir}`);
|
|
1792
|
-
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']);
|
|
1793
1832
|
let ignore = () => false;
|
|
1794
1833
|
const ignPath = join(root, '.skillignore');
|
|
1795
1834
|
if (existsSync(ignPath)) {
|
|
@@ -1815,9 +1854,25 @@ function collectSkillTree(dir) {
|
|
|
1815
1854
|
return out;
|
|
1816
1855
|
}
|
|
1817
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
|
+
|
|
1818
1873
|
skillCmd
|
|
1819
1874
|
.command('push')
|
|
1820
|
-
.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:.')
|
|
1821
1876
|
.option('--id <id>', 'skill id')
|
|
1822
1877
|
.option('--slug <slug>', 'skill slug (resolved to id)')
|
|
1823
1878
|
.requiredOption('--dir <dir>', 'local source tree to push')
|
|
@@ -1839,8 +1894,9 @@ skillCmd
|
|
|
1839
1894
|
});
|
|
1840
1895
|
const data = await res.json().catch(() => ({}));
|
|
1841
1896
|
if (!res.ok) { emitSkillResult(opts.format, { status: 'error', id, error: data.error || `HTTP ${res.status}` }); process.exit(1); }
|
|
1842
|
-
|
|
1843
|
-
|
|
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/' : ''}`);
|
|
1844
1900
|
});
|
|
1845
1901
|
|
|
1846
1902
|
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
|
@@ -2751,7 +2751,7 @@ tool('layout', 'Auto-arrange frames using graph layout algorithm. Positions conn
|
|
|
2751
2751
|
function collectSkillTreeForPush(dir) {
|
|
2752
2752
|
const root = resolve(dir);
|
|
2753
2753
|
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']);
|
|
2754
|
+
const SKIP = new Set(['node_modules', '.git', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next', 'target', 'coverage', '.cache', '.turbo', '.gradle', 'Pods', '.terraform', '.skillinstall']);
|
|
2755
2755
|
const NUL = String.fromCharCode(0);
|
|
2756
2756
|
let ignore = () => false;
|
|
2757
2757
|
const ign = join(root, '.skillignore');
|
|
@@ -2778,7 +2778,22 @@ function collectSkillTreeForPush(dir) {
|
|
|
2778
2778
|
return out;
|
|
2779
2779
|
}
|
|
2780
2780
|
|
|
2781
|
-
|
|
2781
|
+
// Ensure a pushed source tree's .gitignore excludes the rebuildable bundle dir, so
|
|
2782
|
+
// a skill's machine-specific build output (built into .skillinstall/ by its `setup:`
|
|
2783
|
+
// recipe) can never be committed. The server also strips .skillinstall/ from the
|
|
2784
|
+
// bundle (skill-ingest DENY_DIRS); this keeps the author's git clean. Idempotent.
|
|
2785
|
+
function ensureSkillInstallIgnored(dir) {
|
|
2786
|
+
try {
|
|
2787
|
+
const gi = join(dir, '.gitignore');
|
|
2788
|
+
const existing = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
|
|
2789
|
+
if (existing.split(/\r?\n/).some((l) => l.trim().replace(/\/$/, '') === '.skillinstall')) return false;
|
|
2790
|
+
const body = existing && !existing.endsWith('\n') ? existing + '\n' : existing;
|
|
2791
|
+
writeFileSync(gi, body + '.skillinstall/\n');
|
|
2792
|
+
return true;
|
|
2793
|
+
} catch { return false; }
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
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
2797
|
action: z.enum([
|
|
2783
2798
|
'search', 'load', 'list', 'history',
|
|
2784
2799
|
'add', 'update', 'remove',
|
|
@@ -2805,7 +2820,7 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2805
2820
|
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
2821
|
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
2822
|
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
|
|
2823
|
+
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
2824
|
deleteMissing: z.boolean().optional().describe('[push] remove stored files not present in the pushed set'),
|
|
2810
2825
|
}, async (args) => {
|
|
2811
2826
|
try {
|
|
@@ -2952,7 +2967,9 @@ tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/gui
|
|
|
2952
2967
|
let fileList = files;
|
|
2953
2968
|
if (!fileList && dir) fileList = collectSkillTreeForPush(dir);
|
|
2954
2969
|
if (!Array.isArray(fileList) || fileList.length === 0) throw new Error('files[] (non-empty) or dir required for action=push');
|
|
2955
|
-
|
|
2970
|
+
const pushed = await api('POST', `/api/skills/${id}/files/bulk`, { files: fileList, deleteMissing: !!deleteMissing }, extra);
|
|
2971
|
+
if (dir) { try { if (ensureSkillInstallIgnored(dir)) pushed.gitignored = '.skillinstall/'; } catch { /* best-effort */ } }
|
|
2972
|
+
return ok(pushed);
|
|
2956
2973
|
}
|
|
2957
2974
|
case 'attach': {
|
|
2958
2975
|
const { skillId } = args;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drafted",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.13",
|
|
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": [
|