drafted 1.18.0 → 1.18.2
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 +28 -16
- package/agent-instructions/global.md +3 -0
- package/cli/repo-scan.mjs +156 -0
- package/install-mcp.sh +1269 -0
- package/mcp/active-project-store.mjs +81 -0
- package/mcp/gates.mjs +187 -0
- package/mcp/server.mjs +125 -28
- package/mcp/test-file-path-text.mjs +58 -0
- package/mcp/test-org-guards.mjs +172 -0
- package/mcp/test-project-index.mjs +88 -0
- package/mcp/widgets/canvas-overview.html +229 -0
- package/mcp/widgets/frame-preview.html +162 -0
- package/package.json +80 -16
- package/plugin/commands/create-project.md +20 -0
- package/plugin/commands/create-skill.md +18 -0
- package/plugin/commands/extract.md +16 -0
- package/plugin/commands/improve-project-harness.md +16 -0
- package/plugin/commands/improve-skill.md +14 -0
- package/plugin/commands/improve-wiki.md +14 -0
- package/plugin/commands/ingest.md +20 -0
- package/plugin/commands/onboard-drafted.md +17 -0
- package/plugin/skills/drafted/SKILL.md +90 -0
- package/server/lib/umami.mjs +162 -0
- package/src/shared/excalidraw.mjs +84 -0
- package/src/shared/gate-budget.mjs +30 -0
- package/src/shared/minion-presets.mjs +67 -0
- package/src/shared/okf-log.mjs +62 -0
- package/src/shared/record-conformance.mjs +167 -0
- package/src/shared/test-excalidraw-merge.mjs +53 -0
- package/src/shared/wiki-excalidraw.mjs +90 -0
- package/skills/import-website-to-drafted.md +0 -317
- /package/{shared → src/shared}/constants.mjs +0 -0
package/README.md
CHANGED
|
@@ -1,32 +1,44 @@
|
|
|
1
1
|
# Drafted
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
+
## Install
|
|
14
17
|
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
- App: http://localhost:3477
|
|
41
|
+
- Email inbox (magic links): http://localhost:8025
|
|
42
|
+
- MinIO console: http://localhost:9001
|
|
30
43
|
|
|
31
|
-
|
|
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>
|
|
@@ -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
|
+
}
|