drafted 1.18.1 → 1.18.3

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/README.md CHANGED
@@ -1,32 +1,44 @@
1
1
  # Drafted
2
2
 
3
- Shared surface for AI-human collaboration. MCP server and CLI for organizing agent work into projects, skills, and durable knowledge.
3
+ Multi-tenant shared surface for AI-human collaboration. Drafted organizes agent-produced work into **projects** (frames on a zoomable real-time surface), **skills** (reusable operating procedures agents load), and a **wiki** (durable org knowledge) — with MCP tools for agents and a browser UI for humans.
4
4
 
5
- ## Install
5
+ - Product vision and positioning: [PRODUCT.md](PRODUCT.md)
6
+ - Architecture, development, and operations: [AGENTS.md](AGENTS.md)
7
+ - Live: [drafted.live](https://drafted.live)
6
8
 
7
- ```bash
8
- npm install -g drafted
9
- ```
9
+ ## OKF-native
10
+
11
+ Drafted is a native producer and consumer of the **Open Knowledge Format (OKF) v0.1** ([GoogleCloudPlatform/knowledge-catalog](https://github.com/GoogleCloudPlatform/knowledge-catalog)):
10
12
 
11
- ## Usage
13
+ - **The org wiki is an OKF v0.1 bundle.** Every page carries conformant YAML frontmatter (required `type`, one-line `description`, `tags`, synthesized `timestamp`; unknown keys preserved). `index.md` at every level is synthesized, `log.md` keeps the date-grouped change history, links resolve with or without `.md`, and broken links are legal. The wiki UI shows a live OKF conformance badge.
14
+ - **Bundle exchange everywhere.** Export/import the wiki (`GET /api/wiki/export.tar.gz`, `POST /api/wiki/import`), the skill library (`skills/<slug>/SKILL.md` layout), and whole projects (`<layer>/<lane>/<file>.md` concepts; markdown links round-trip as connectors) — via HTTP or the MCP `wiki`, `skill`, and `project` tools, with dry-run reports.
12
15
 
13
- ### As an MCP server (for Claude Desktop, Claude Code, Codex, Cursor)
16
+ ## Install
14
17
 
15
- The MCP server is automatically available after install:
18
+ **Claude Code, Codex, or any MCP client** — installs the MCP server, the Drafted skill, *and* the eight slash commands (into `~/.claude/commands/drafted/` and `~/.codex/prompts/drafted/`):
16
19
 
17
20
  ```bash
18
- drafted-mcp
21
+ curl -fsSL https://drafted.live/install.sh | bash # macOS / Linux
22
+ irm https://drafted.live/install.ps1 | iex # Windows
19
23
  ```
20
24
 
21
- ### As a CLI
25
+ > Adding the MCP server on its own (e.g. `claude mcp add`) gives you the tools without the skill or the commands.
26
+
27
+ **Claude on the web (claude.ai) and Cowork** — install the plugin, not the bare connector. The plugin bundles the Drafted connector *plus* the Drafted skill and the eight slash commands (`/drafted:onboard-drafted`, `/drafted:create-project`, `/drafted:create-skill`, `/drafted:ingest`, `/drafted:extract`, `/drafted:improve-wiki`, `/drafted:improve-skill`, `/drafted:improve-project-harness`); adding the connector URL on its own gives you the tools with none of the guidance.
28
+
29
+ > 1. Settings → Customize plugins → Add → Add marketplace → `https://github.com/ddfourtwo/drafted-web` → install **Drafted**.
30
+ > 2. Settings → Connectors → find **Drafted** → **Connect**, then approve the sign-in.
31
+ >
32
+ > Both halves matter: the plugin carries the skill and commands, the connector carries the tools. Installing the plugin does not sign you in to the connector.
33
+
34
+ ## Quick start (developing Drafted itself)
22
35
 
23
36
  ```bash
24
- drafted login
25
- drafted ls
26
- drafted write designs/default/hero.html < design.html
37
+ npm install && npm run dev # Docker services + schema + server with hot reload
27
38
  ```
28
39
 
29
- ## Links
40
+ - App: http://localhost:3477
41
+ - Email inbox (magic links): http://localhost:8025
42
+ - MinIO console: http://localhost:9001
30
43
 
31
- - **App**: https://drafted.live
32
- - **Docs**: https://drafted.live
44
+ Sign in with any email and grab the magic link from Mailpit.
@@ -0,0 +1,3 @@
1
+ <drafted>
2
+ You have Drafted MCP tools — a shared surface for durable, reviewable work: produce substantive output as frames on the surface (not only in chat), put knowledge in the org wiki, and encode repeatable methods as skills. The full operating manual is the `drafted` skill installed with the plugin — follow it when working with Drafted. Address orgs by path: `fs(ls, path="/")` lists them, and `/o/<org>/<root>/...` (root ∈ wiki, skills, projects) is the canonical path form — the org is part of the path, never a separate switch. Before writing, verify the org/project echoed in the response is the one you intend.
3
+ </drafted>
package/cli/drafted.mjs CHANGED
@@ -1679,7 +1679,7 @@ function emitSkillResult(format, obj) {
1679
1679
 
1680
1680
  skillCmd
1681
1681
  .command('add')
1682
- .description('Create a skill in Drafted from stdin JSON {name,description,content,readme,tags?,triggerPatterns?} — a README.md (readme field) is required (Phase 1 gate)')
1682
+ .description('Create a skill in Drafted from stdin JSON {name,description,content,readme?,tags?,triggerPatterns?} — readme is optional; omitting it returns the skill with a warning, not an error')
1683
1683
  .option('--format <fmt>', 'output format: json or text', 'text')
1684
1684
  .action(async (opts) => {
1685
1685
  requireLogin();
@@ -0,0 +1,156 @@
1
+ // Repo `.agents/` scanner (org substrate normalization, Move C).
2
+ //
3
+ // Pure filesystem scan — no network, no DB. The CLI runs this against a repo
4
+ // checkout (cloned to a temp dir for a URL, or a local path directly) and POSTs
5
+ // the catalog to /api/repos. Content stays in git; only metadata + pointers are
6
+ // sent. Mirrors the Agent Skills open format: `.agents/skills/<name>/SKILL.md`
7
+ // (name + description frontmatter) and `.agents/identities/<slug>/identity.yaml`
8
+ // (slug + name). See docs/plans/org-substrate-normalization.md.
9
+
10
+ import { readFileSync, readdirSync, existsSync, mkdtempSync } from 'fs';
11
+ import { join } from 'path';
12
+ import { tmpdir } from 'os';
13
+ import { execSync } from 'child_process';
14
+ import { parse as parseYaml } from 'yaml';
15
+
16
+ // ── Frontmatter ────────────────────────────────────────────────────
17
+
18
+ /** Parse a YAML frontmatter block (`---\n...\n---`) from text. null if absent/invalid. */
19
+ export function parseFrontmatter(text) {
20
+ const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---/);
21
+ if (!m) return null;
22
+ try {
23
+ const parsed = parseYaml(m[1]);
24
+ return parsed && typeof parsed === 'object' ? parsed : null;
25
+ } catch {
26
+ return null;
27
+ }
28
+ }
29
+
30
+ // ── Scan ────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Scan <dir>/.agents/skills/<name>/SKILL.md and <dir>/.agents/identities/<slug>/
34
+ * identity.yaml. Returns { skills, identities } catalog entries (metadata +
35
+ * pointers only — no file content). A skill with no frontmatter still indexes
36
+ * (name falls back to the dir). Identities index {slug, name} from identity.yaml.
37
+ */
38
+ export function scanRepoAgents(dir) {
39
+ const skills = [];
40
+ const identities = [];
41
+ const agentsDir = join(dir, '.agents');
42
+
43
+ const skillsDir = join(agentsDir, 'skills');
44
+ if (existsSync(skillsDir)) {
45
+ for (const name of readdirSafe(skillsDir)) {
46
+ const skillMd = join(skillsDir, name, 'SKILL.md');
47
+ if (!existsSync(skillMd)) continue;
48
+ const fm = parseFrontmatter(readFileSync(skillMd, 'utf8')) || {};
49
+ // README gate (Phase 1): a skill with no README.md still imports (foreign
50
+ // skills in the open Agent Skills format may legitimately omit one), but
51
+ // is flagged warn so the gap is visible. Never dropped.
52
+ const hasReadme = existsSync(join(skillsDir, name, 'README.md'));
53
+ skills.push({
54
+ kind: 'skill',
55
+ name: String(fm.name || name),
56
+ slug: name,
57
+ description: fm.description != null ? String(fm.description) : '',
58
+ sourcePath: `.agents/skills/${name}`,
59
+ readmePath: hasReadme ? `.agents/skills/${name}/README.md` : null,
60
+ status: hasReadme ? 'ok' : 'warn',
61
+ statusReason: hasReadme ? null : 'no README',
62
+ });
63
+ }
64
+ }
65
+
66
+ const identDir = join(agentsDir, 'identities');
67
+ if (existsSync(identDir)) {
68
+ for (const name of readdirSafe(identDir)) {
69
+ const idYaml = join(identDir, name, 'identity.yaml');
70
+ if (!existsSync(idYaml)) continue;
71
+ let fm = null;
72
+ try {
73
+ fm = parseYaml(readFileSync(idYaml, 'utf8')) || {};
74
+ } catch {
75
+ fm = {};
76
+ }
77
+ identities.push({
78
+ kind: 'identity',
79
+ name: String(fm.name || fm.slug || name),
80
+ slug: String(fm.slug || name),
81
+ description: null,
82
+ sourcePath: `.agents/identities/${name}`,
83
+ });
84
+ }
85
+ }
86
+
87
+ return { skills, identities };
88
+ }
89
+
90
+ function readdirSafe(dir) {
91
+ try {
92
+ return readdirSync(dir).filter((n) => !n.startsWith('.'));
93
+ } catch {
94
+ return [];
95
+ }
96
+ }
97
+
98
+ /** Best-effort default branch of a git checkout. 'main' if git is unavailable. */
99
+ export function detectDefaultBranch(dir) {
100
+ try {
101
+ const out = execSync('git rev-parse --abbrev-ref HEAD', {
102
+ cwd: dir,
103
+ stdio: ['ignore', 'pipe', 'ignore'],
104
+ encoding: 'utf8',
105
+ });
106
+ const branch = out.trim();
107
+ return branch || 'main';
108
+ } catch {
109
+ return 'main';
110
+ }
111
+ }
112
+
113
+ /** Remote origin URL of a git checkout, or null. */
114
+ export function detectRemoteUrl(dir) {
115
+ try {
116
+ const out = execSync('git config --get remote.origin.url', {
117
+ cwd: dir,
118
+ stdio: ['ignore', 'pipe', 'ignore'],
119
+ encoding: 'utf8',
120
+ });
121
+ const url = out.trim();
122
+ return url || null;
123
+ } catch {
124
+ return null;
125
+ }
126
+ }
127
+
128
+ /** True if the argument looks like a git URL (not a local path). */
129
+ export function isGitUrl(s) {
130
+ return /^(https?:|git@|ssh:\/\/|git:\/\/)/.test(s);
131
+ }
132
+
133
+ /** Clone a git URL into a fresh temp dir, returning { dir, cleanup }.
134
+ * When `branch` is given, clone that branch only (--branch --single-branch) so a
135
+ * missing branch fails loudly instead of silently landing on the default. */
136
+ export function cloneToTemp(url, branch) {
137
+ const dir = mkdtempSync(join(tmpdir(), 'drafted-repo-'));
138
+ const branchArgs = branch ? `--branch ${shquote(branch)} --single-branch` : '';
139
+ execSync(`git clone --depth 1 ${branchArgs} ${shquote(url)} ${shquote(dir)}`, {
140
+ stdio: ['ignore', 'pipe', 'pipe'],
141
+ encoding: 'utf8',
142
+ });
143
+ return {
144
+ dir,
145
+ cleanup: () => {
146
+ try { execSync(`rm -rf ${shquote(dir)}`, { stdio: 'ignore' }); } catch { /* best effort */ }
147
+ },
148
+ };
149
+ }
150
+
151
+ // Minimal shell quoting — clone targets are user-supplied URLs. We only pass
152
+ // them to git as a single argv via execSync through a shell string, so quote to
153
+ // avoid metacharacter injection. (The URL is also validated by git itself.)
154
+ function shquote(s) {
155
+ return `'${String(s).replace(/'/g, "'\\''")}'`;
156
+ }