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.
@@ -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
- const DEFAULT_AUTH_FILE = join(DEFAULT_STATE_DIR, 'auth.json');
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: Get server URL (supports DRAFTED_SERVER env var or --server flag)
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
- const msg = data.error || `HTTP ${res.status}`;
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 strips artifacts + enforces caps)')
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
- if (opts.format === 'json') console.log(JSON.stringify({ status: 'ok', id, hash: data.hash, count: data.count, stripped: data.stripped, files: data.files }));
1843
- else console.log(`ok\t${id}\t${data.hash}\tpushed ${data.count}${data.stripped ? `, stripped ${data.stripped}` : ''}`);
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
- tool('skill', 'Manage the Drafted skill library. Skills are reusable prompts/guidelines agents can load and follow. 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.', {
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 + .skillignore pre-filtered), server re-enforces'),
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
- return ok(await api('POST', `/api/skills/${id}/files/bulk`, { files: fileList, deleteMissing: !!deleteMissing }, extra));
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.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": [