sdocs-dev 1.15.0 → 1.18.0

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/bin/sdocs-dev.js CHANGED
@@ -32,8 +32,10 @@ const io = require('../lib/io');
32
32
  const helpText = require('../lib/help-text');
33
33
  const commands = require('../lib/commands');
34
34
  const cellsVerify = require('../lib/cells-verify');
35
+ const slidesVerify = require('../lib/slides-verify');
35
36
  const bridgeCommands = require('../lib/bridge-commands');
36
37
  const libraryCommands = require('../lib/library-commands');
38
+ const cloudCommands = require('../lib/cloud-commands');
37
39
 
38
40
  // ── Router ────────────────────────────────────────────────
39
41
  // One place that knows the full set of verbs. New chunks register here.
@@ -49,6 +51,8 @@ function buildRouter() {
49
51
  r.register('diagrams', { handler: () => { console.log(helpText.DIAGRAMS_HELP); process.exit(0); } });
50
52
  r.register('videos', { handler: () => { console.log(helpText.VIDEOS_HELP); process.exit(0); } });
51
53
  r.register('video', { handler: () => { console.log(helpText.VIDEOS_HELP); process.exit(0); } });
54
+ r.register('apps', { handler: () => { console.log(helpText.APPS_HELP); process.exit(0); } });
55
+ r.register('app', { handler: () => { console.log(helpText.APPS_HELP); process.exit(0); } });
52
56
  // `sdoc cells` prints the reference; `sdoc cells verify <file>` evaluates a
53
57
  // document's tabs headlessly and prints the computed values (the handler
54
58
  // calls process.exit with the 0/1/2 result code).
@@ -94,7 +98,10 @@ function buildRouter() {
94
98
  r.register('feedback', { handler: (opts) => bridgeCommands.feedbackCommand(opts) });
95
99
 
96
100
  // `sdoc slides` family — reference text + helper subcommands.
97
- r.register('slides', { handler: (opts) => { commands.slidesCommand(opts); process.exit(0); } });
101
+ r.register('slides', { handler: (opts) => {
102
+ if ((opts.file || '').toLowerCase() === 'verify') return slidesVerify.slidesVerifyCommand(opts);
103
+ commands.slidesCommand(opts); process.exit(0);
104
+ } });
98
105
  // `sdoc present <file>` — same as the default open flow but enters
99
106
  // fullscreen slide view on load.
100
107
  r.register('present', { handler: (opts) => commands.presentCommand(opts) });
@@ -107,6 +114,7 @@ function buildRouter() {
107
114
  // `sdoc library [enable|disable|status|rebuild]`. No sub-arg opens
108
115
  // the library UI.
109
116
  r.register('library', { handler: async (opts) => { await libraryCommands.libraryCommand(opts); /* libraryOpen blocks */ } });
117
+ r.register('cloud', { handler: async (opts) => { await cloudCommands.runCloudCommand(opts); } });
110
118
 
111
119
  r.register(null, { handler: (opts) => {
112
120
  // Index-on-open tap: fires before the open so any +tag CLI args land
@@ -163,16 +171,32 @@ module.exports = {
163
171
  // CLI parsing
164
172
  parseArgs: io.parseArgs,
165
173
 
166
- // Agent block (pure functions and constants)
167
- AGENT_BLOCK_VERSION: agentBlock.AGENT_BLOCK_VERSION,
168
- AGENT_BLOCK_BODY: agentBlock.AGENT_BLOCK_BODY,
169
- formatAgentBlock: agentBlock.formatAgentBlock,
170
- findBookendedBlock: agentBlock.findBookendedBlock,
171
- findLegacyBlock: agentBlock.findLegacyBlock,
172
- refreshContent: agentBlock.refreshContent,
173
- compareVersions: agentBlock.compareVersions,
174
- migrateSetupState: agentBlock.migrateSetupState,
175
- implicitConsentState: agentBlock.implicitConsentState,
174
+ // Agent skill (pure functions and constants)
175
+ SKILL_VERSION: agentBlock.SKILL_VERSION,
176
+ SKILL_NAME: agentBlock.SKILL_NAME,
177
+ SKILL_DESCRIPTION: agentBlock.SKILL_DESCRIPTION,
178
+ CLOUD_SKILL_DESCRIPTION: agentBlock.CLOUD_SKILL_DESCRIPTION,
179
+ SKILL_BODY: agentBlock.SKILL_BODY,
180
+ CLOUD_SKILL_BODY: agentBlock.CLOUD_SKILL_BODY,
181
+ formatSkill: agentBlock.formatSkill,
182
+ readSkillVersion: agentBlock.readSkillVersion,
183
+ readSkillEdition: agentBlock.readSkillEdition,
184
+ canonicalSkillFile: agentBlock.canonicalSkillFile,
185
+ canonicalSkillDir: agentBlock.canonicalSkillDir,
186
+ resolveSkillAgents: agentBlock.resolveSkillAgents,
187
+ legacyBlockTargets: agentBlock.legacyBlockTargets,
188
+ removeBlockContent: agentBlock.removeBlockContent,
189
+ // Legacy block detection (migration + tests). AGENT_BLOCK_VERSION/BODY are
190
+ // back-compat aliases that now point at the skill version/body.
191
+ AGENT_BLOCK_VERSION: agentBlock.AGENT_BLOCK_VERSION,
192
+ AGENT_BLOCK_BODY: agentBlock.AGENT_BLOCK_BODY,
193
+ formatAgentBlock: agentBlock.formatAgentBlock,
194
+ findBookendedBlock: agentBlock.findBookendedBlock,
195
+ findLegacyBlock: agentBlock.findLegacyBlock,
196
+ refreshContent: agentBlock.refreshContent,
197
+ compareVersions: agentBlock.compareVersions,
198
+ migrateSetupState: agentBlock.migrateSetupState,
199
+ implicitConsentState: agentBlock.implicitConsentState,
176
200
 
177
201
  // Router (so contract tests can exercise it directly)
178
202
  buildRouter,
@@ -1,27 +1,53 @@
1
- // Pure data model for the SmallDocs agent integration block.
1
+ // Pure data model for the SmallDocs agent skill + legacy-block migration.
2
2
  //
3
- // IMPORTANT: keep AGENT_BLOCK_BODY in sync with the per-agent setup
4
- // snippets in public/sdoc.md (the "Set up your agent" section). If you
5
- // reword one, reword the other.
3
+ // `sdoc setup` installs a discoverable SKILL.md (YAML frontmatter preamble
4
+ // that is always in agent context + a body loaded on demand via the `skill`
5
+ // tool) instead of an always-on block pasted into AGENTS.md. One canonical
6
+ // copy lives at ~/.agents/skills/smalldocs/SKILL.md; every other supported
7
+ // agent gets a relative symlink into its own skills directory. The agent
8
+ // table is derived from vercel-labs/skills/src/agents.ts.
6
9
  //
7
- // Release checklist when AGENT_BLOCK_BODY changes:
8
- // 1. Bump AGENT_BLOCK_VERSION below.
9
- // 2. Set AGENT_BLOCK_REASON to a one-line summary of what changed.
10
- // 3. Prepend a new section to public/agent-changes.md.
11
- // 4. Reword public/sdoc.md per-agent snippets to match.
10
+ // MIGRATION: the previous scheme wrote a `## SmallDocs` block (wrapped in
11
+ // <!-- sdocs-agent-block:start v=N --> bookends) into a handful of agent
12
+ // config files. On setup/refresh we strip any recognised block from those
13
+ // files so the content is not loaded twice. The detection functions below
14
+ // (findBookendedBlock / findLegacyBlock / removeBlockContent) drive that.
15
+ //
16
+ // This module also owns the on-disk schema for ~/.sdocs/setup.json.
12
17
  //
13
- // This module also owns the on-disk schema for ~/.sdocs/setup.json
14
- // (read/write/migrate). The tests cover both the block format and the
15
- // state migration, so they live together as one cohesive module.
18
+ // Release checklist when the skill body changes:
19
+ // 1. Bump SKILL_VERSION below.
20
+ // 2. Set SKILL_REASON to a one-line summary of what changed.
21
+ // 3. Prepend a new section to public/agent-changes.md.
22
+ // 4. Reword public/sdoc.md setup copy to match.
23
+ // 5. Refresh the agent table from vercel-labs/skills/src/agents.ts if new
24
+ // agents landed upstream.
16
25
 
17
26
  const fs = require('fs');
18
27
  const path = require('path');
19
28
  const { SETUP_CACHE } = require('./constants');
20
29
 
21
- const AGENT_BLOCK_VERSION = 13;
22
- const AGENT_BLOCK_REASON = 'Agent annotations now render as a guided code walkthrough: each note is a callout below its line with a Prev/Next stepper, walked in the order the notes are passed (not line order), and naming several files (sdoc app.py 5:"..." util.py 12:"..." app.py 9:"...") narrates across them as tabs the walk hops between. The `sdoc code` bullet gains this multi-file walkthrough description plus a trigger to build one when the user asks to be walked through code, an MR, a diff, or the current changes. Only the `sdoc code` bullet changed from v12.';
30
+ // ── Skill model ────────────────────────────────────────────
31
+ const SKILL_VERSION = 27;
32
+ const SKILL_REASON = 'Agents can now guide readers through rendered prose, rich blocks, and inline code lines.';
33
+ const SKILL_NAME = 'smalldocs';
34
+
35
+ // Always-in-context preamble. Concise trigger text; the full reference lives
36
+ // in SKILL_BODY and loads on demand. Plain text: no backticks, no em/en dashes,
37
+ // no double quotes (it is emitted as a double-quoted YAML scalar).
38
+ const SKILL_DESCRIPTION = "Use SmallDocs when the user says sdoc, S-doc, smalldoc, sdoc this, or asks to open, present, share, style, save, or walk through a Markdown document with SmallDocs. Create or locate the Markdown file and use sdoc FILE.md for normal viewing. For a document walkthrough add source-line annotations to the sdoc command; the source file stays unchanged. For a presentation run sdoc slides, create a Markdown deck from the source material, run sdoc slides verify FILE.md --json and fix every error, then run sdoc present FILE.md. For a runnable browser component run sdoc apps, use a sdoc-app block, and check inline, fullscreen, narrow, and wide layouts. Run the matching bare reference command before other specialised syntax. Local files stay local unless the user explicitly requests sharing or Cloud storage.";
39
+
40
+ const CLOUD_SKILL_DESCRIPTION = "Use SmallDocs when the user says sdoc, S-doc, smalldoc, sdoc this, or asks to open, present, share, style, search, save, or walk through a Markdown document with SmallDocs. For a document walkthrough add source-line annotations to the sdoc command; the source file stays unchanged. For a presentation run sdoc slides, create a Markdown deck, run sdoc slides verify FILE.md --json and fix every error, then run sdoc present FILE.md. For a runnable browser component run sdoc apps, use a sdoc-app block, and check inline, fullscreen, narrow, and wide layouts. This user has enabled SmallDocs Cloud. Use sdoc FILE.md for ordinary local viewing and do not search Cloud when a named local source is sufficient. For relevant prior Cloud material run sdoc cloud status --json, search, then sdoc cloud pull DOCUMENT_UUID --output PATH --no-bind --json.";
41
+
42
+ const STANDARD_CLOUD_SKILL_SECTION = `### SmallDocs Cloud is available
23
43
 
24
- const AGENT_BLOCK_BODY = `## SmallDocs
44
+ SmallDocs Cloud is an optional paid feature for documents the user chooses to add. It provides browser, mobile, and CLI access across devices, text and tag search, revision history, account member permissions, and notifications. This standard skill does not indicate that Cloud has been set up for the user, so local viewing remains the default.
45
+
46
+ When those capabilities fit the task, mention Cloud as an option. Run \`sdoc cloud\` for a local overview and setup command only when the user wants to explore or use it. Do not upload a document merely because Cloud is available.
47
+ `;
48
+
49
+ // The on-demand reference body. Loaded only when the agent invokes the skill.
50
+ const SKILL_BODY = `## SmallDocs
25
51
 
26
52
  The \`sdoc\` CLI (\`sdoc path/to/file.md\`) is installed globally and renders local Markdown files securely in the browser (at https://smalldocs.org) in a way that's comfortable for your user to read and share. Nothing hits a server unless the user explicitly saves the file to the SmallDocs cloud or runs \`sdoc share\`.
27
53
 
@@ -33,44 +59,192 @@ Use it (or offer it) when the user wants to read, share, or export a \`.md\` fil
33
59
 
34
60
  - \`sdoc file.md\` - the default way to open a file, for comfortable reading or quick sharing.
35
61
  - \`sdoc bridge file.md\` - open a live editing session while you iterate on a file with the user: edits in the browser autosave to the file on disk, and your edits to the file push to the open page. It parks the terminal until the tab closes, so run it in the background when you want to keep working. The first time the page connects, the browser asks to reach a local process (Chrome calls this "Apps on device" / Local Network Access) - the user has to accept, or the page stays read-only. Reach for this when you and the user are working a file back and forth, not for a one-off open.
36
- - \`sdoc library\` - opens a library view in the browser. SmallDocs automatically indexes every \`.md\` under the user's home directory; filter by directory, date, or tags (the index doesn't search file content - fall back to \`grep\` for that). Opt out per-directory with \`.sdocsignore\` or per-file with \`sdocs-library: false\` in front matter. (\`sdoc library --help\` for the full reference.)
37
- - \`sdoc file.md +tag1 +tag2\` - open the file and inject tags into its YAML front matter which persist. The \`+\` prefix is shell-safe. Tag files when they're worth rediscovering - the library filters by tag, not by content.
38
- - \`sdoc library ls --tags\` - print the tags (tag - count) for the current project directory. If you think you might tag the file, run this first so you reuse the project's existing tag vocabulary instead of inventing parallel ones.
62
+ - \`sdoc library\` - opens a library view containing files previously opened with \`sdoc path/to/file.md\`; filter by directory, date, or tags (the index doesn't search file content - fall back to \`grep\` for that). Opt out per-file with \`sdocs-library: false\` in front matter. (\`sdoc library --help\` for the full reference.)
63
+ - \`sdoc library ls --tags\` - list the current project's tags by frequency. When tags would make a document worth rediscovering, run this before choosing them. Prefer an existing tag that fits; introduce a new one when none does.
64
+ - \`sdoc file.md +tag1 +tag2\` - open the file and add the selected tags to its YAML front matter. The \`+\` prefix is shell-safe and the tags persist.
39
65
  - \`sdoc share file.md\` - copy an encrypted short URL to the clipboard for sending to someone else. The link decrypts in the recipient's browser; the server only sees ciphertext. The agent can't actually deliver - paste the link into wherever the user talks to that person.
66
+ - \`sdoc report.md 12:"start here" 24-28:"compare these results"\` - open a guided walkthrough of a regular Markdown document. Source lines in prose highlight the matching rendered text. A source line inside an ordinary code fence highlights that code line and places the note beneath it without leaving the reading surface. Charts, diagrams, sheets, slides, forms, math, videos, and runnable components are highlighted as complete rendered elements. Each note is a markdown callout with Prev / Next controls, walked in the order you pass the notes. Use this when the user asks for a walkthrough of a report, plan, design document, or other prose document. The annotations ride in the URL and through \`sdoc share\`; the source file is unchanged.
40
67
  - \`sdoc --help\` - full reference.
41
68
 
69
+ ${STANDARD_CLOUD_SKILL_SECTION}
70
+
42
71
  ### SmallDocs expands what you can create with Markdown
43
72
 
44
73
  SmallDocs uses the browser to extend what Markdown can be: a styled doc, a chart, a diagram, a slide deck, or an interactive form whose answers come back to you. Reach for one of these when a visual or interactive artifact will land harder than prose - not as a default for every reply. To create something new, write the \`.md\` file first, then \`sdoc path/to/file.md\`.
45
74
 
46
75
  Each command below prints its reference when run with no arguments - run it before writing the matching fenced block. The JSON / DSL shapes are specific and easy to get wrong from memory.
47
76
 
77
+ For a presentation request, follow this sequence rather than treating it as an ordinary document:
78
+
79
+ 1. Run \`sdoc slides\` and use that reference while writing the slide blocks.
80
+ 2. Save the Markdown source.
81
+ 3. Run \`sdoc slides verify FILE.md --json\`, fix every diagnostic, and rerun until it exits 0.
82
+ 4. Run \`sdoc present FILE.md\` so the user sees the deck in presentation mode.
83
+
48
84
  - \`sdoc charts\` - rendering inline charts (\`\`\`chart blocks)
49
85
  - \`sdoc diagrams\` - rendering inline Mermaid diagrams (\`\`\`mermaid blocks; has full-screen mode for zoom). Reach for this when drawing system or architectural diagrams (sequence, flow, component layout) - a diagram often communicates the shape of something faster than the equivalent prose.
50
- - \`sdoc slides\` - inline slide decks (\`\`\`slide / ~~~slide blocks; has full-screen presentation mode). Slides can be standalone exported as \`.pdf\` or \`.pptx\`. \`sdoc present file.md\` - open file directly in fullscreen presentation mode.
86
+ - \`sdoc apps\` - runnable HTML components (\`\`\`sdoc-app blocks): one complete HTML document with its own CSS, JavaScript, and data. Start with semantic HTML: SmallDocs supplies the document's current typography, colours, spacing, background, and control treatment in a low-priority CSS layer. Ordinary component CSS wins, and the \`--sdoc-app-*\` custom properties support targeted overrides. Let the tool's purpose determine its layout: prefer a clear page, list, table, or form before a dashboard of cards, use a canvas or stage for spatial interaction, and add surfaces, colour, and distinctive shapes when they encode structure or state. The component's document layout owns its inline height, while its width follows the reading column. Write responsive CSS for both the column and fullscreen viewport. It expands without losing state and joins Previous / Next navigation when the document contains several components. Use \`<title>\` to name it. Ordinary \`\`\`html remains source. Run \`sdoc apps\` before authoring and test every control inline, fullscreen, narrow, and wide.
87
+ - \`sdoc slides\` - inline slide decks (\`\`\`slide / ~~~slide blocks; has full-screen presentation mode). Slides can be standalone exported as \`.pdf\` or \`.pptx\`. Run \`sdoc slides verify file.md --json\` after authoring; fix every diagnostic, or add \`bleed=allow\` only to an individual shape whose off-canvas placement is intentional, then rerun until it exits 0. Use \`sdoc present file.md\` for the visual check that headless validation cannot perform.
51
88
  - \`sdoc cells\` - rendering spreadsheets (\`\`\`cells blocks): CSV rows where plain values and =formulas (SUM, AVERAGE, IF, ROUND...) sit in the same grid and compute live. The reader can sort, select ranges for quick stats, edit a scratch copy fullscreen, and download the sheet as Excel (.xlsx) with the formulas still working. Name a block (\`\`\`cells Expenses) to build a workbook of several tabs whose formulas reference each other across sheets (\`=Expenses!B4\`); run \`sdoc cells verify file.md\` to compute the whole workbook headlessly and read the values back. Reach for this when handing the user numbers they will want to check or play with - totals, budgets, projections. \`sdoc report.csv\` opens a CSV file directly as a sheet.
52
89
  - \`sdoc code\` - opening a source file or a fenced code block as a syntax-highlighted listing: a light code viewer for reading code with the user away from the IDE. \`sdoc app.rb\` (or \`.js\`, \`.py\`, \`.go\`, \`.rs\`, \`.ts\`...) opens a file as a highlighted listing; a \`\`\`lang fenced block is highlighted inline. Comments in the source get a prominent lane so the code reads clearly top to bottom. The fullscreen view adds a line-number gutter and language-aware folding (collapse a whole method or class); a comment mode lets the user annotate a line or method with review notes, kept in the browser rather than the file. You can also pin your own explanations to lines as you open a file - \`sdoc app.py 22:"the bug is here" 25-28:"wrong comparison"\` - and the file opens as a guided walkthrough: each note is a markdown callout below its line with a Prev / Next stepper, walked in the order you pass the notes (not line order). Name several files to narrate across them - \`sdoc app.py 5:"entry point" util.py 12:"it calls into here" app.py 9:"back here"\` - and each becomes a tab the walkthrough hops between. When the user asks you to walk them through code, an MR, a diff, or the current changes, build one of these. The file rides in the URL like any document; nothing is uploaded. Reach for it when reading or reviewing code with the user, not for prose.
53
90
  - \`sdoc schema\` - styling Markdown (fonts, colors, spacing). The default styles are already comfortable to read; reach for this only when they aren't enough - client-facing polish or a bit of fun.
54
91
  - \`sdoc feedback\` - rendering interactive elements (\`\`\`form blocks) to receive structured input from the user. Run \`sdoc feedback file.md\` and the user's submission lands as a JSON line on stdout. Good for eliciting complex/subtle feedback. All standard interactive HTML elements with prefilled (but editable) content of your choosing.
55
92
  `;
56
93
 
94
+ const CLOUD_SKILL_SECTION = `### SmallDocs Cloud for agents
95
+
96
+ This user has enabled SmallDocs Cloud. Local viewing remains the default when the request only asks to create or open a document. Consider Cloud without waiting for the user to say the word "Cloud" when the existing conversation or task calls for persistent storage, cross-device access, search, revisions, permissions, or notifications. If the intended destination is unclear and it changes who can access the document, discuss it with the user.
97
+
98
+ Treat Cloud as a source of context, not only a place to save new work. When earlier decisions, research, plans, or documentation could materially inform the task, search Cloud before recreating that context. Use specific project terms first and try shorter terms or existing tags when a search returns nothing. Do not search unrelated Cloud documents merely because Cloud is enabled.
99
+
100
+ Before reading or changing Cloud data, run \`sdoc cloud status --json\` for live authentication and account state. Run \`sdoc cloud --help\` for the search, read, and update workflow, exact result fields, and examples. Add \`--json\` for one stable machine-readable object on stdout.
101
+
102
+ When earlier Cloud material should inform new work, use this sequence:
103
+
104
+ 1. Run \`sdoc cloud status --json\`.
105
+ 2. Run \`sdoc cloud --help\` if the exact search or result fields are not already known.
106
+ 3. Run \`sdoc cloud search "SPECIFIC TERMS" --json\`, then shorten the query or inspect \`sdoc cloud tags --json\` only when needed.
107
+ 4. Pull a promising result with \`sdoc cloud pull DOCUMENT_UUID --output PATH --no-bind --json\` so reading it does not bind the file for a later update.
108
+
109
+ - Discover account access, people, tags, and document permission sets with \`sdoc cloud status --json\`, \`sdoc cloud members\`, \`sdoc cloud tags\`, and \`sdoc cloud permission-groups\`. When status reports more than one account, pass \`--account ACCOUNT_UUID\` to account-scoped commands.
110
+ - Find documents with \`sdoc cloud search "QUERY" --json\`. Search matches a case-insensitive phrase across titles, filenames, tags, and Markdown, returning document IDs and snippets rather than full content. Use \`sdoc cloud tags --json\` to discover existing vocabulary, \`--tag TAG\` to narrow results, \`sdoc cloud ls --shared-with-me --json\` for documents shared with the signed-in user, and \`--account ACCOUNT_UUID\` when the relevant account is known.
111
+ - Read a promising result without binding it for future updates with \`sdoc cloud pull DOCUMENT_UUID --output PATH --no-bind --json\`. To update it, pull without \`--no-bind\`, edit the local Markdown, then run \`sdoc cloud push PATH --json\`.
112
+ - Upload a new local file without opening a browser with \`sdoc cloud create FILE.md --account ACCOUNT_UUID --json\`. Omit \`--account\` when status reports one account.
113
+ - Set document access with \`sdoc cloud access DOCUMENT_UUID --only-you\`, \`--everyone\`, or one or more \`--member USER_UUID\` values. List members first. Notify existing members with \`sdoc cloud notify ...\`; notification does not grant access or create users.
114
+ - When updating a bound document, the local binding supplies the revision the agent edited. Cloud keeps separate changes from other writers; overlapping replacements may both remain. If the server combines content and the file did not change during upload, push writes the combined Markdown back to the local file. Inspect \`merge_classification\`, \`combined\`, and \`local_updated_from_cloud\` in the JSON result.
115
+ - Inspect or recover history with \`sdoc cloud history DOCUMENT_UUID\` and \`sdoc cloud restore DOCUMENT_UUID --revision REVISION_UUID\`.
116
+
117
+ Cloud documents are identified by UUID, not filename. An account is the billing and access boundary; tags organize documents inside it. Do not use \`sdoc share\` as a substitute for Cloud: share creates an encrypted snapshot link, while Cloud provides revisions, search, membership, and persistent agent access.
118
+
119
+ `;
120
+
121
+ const CLOUD_SKILL_BODY = SKILL_BODY.replace(STANDARD_CLOUD_SKILL_SECTION, CLOUD_SKILL_SECTION);
122
+
123
+ function formatSkill(version, options) {
124
+ const cloud = Boolean(options && options.cloud);
125
+ const description = cloud ? CLOUD_SKILL_DESCRIPTION : SKILL_DESCRIPTION;
126
+ const body = cloud ? CLOUD_SKILL_BODY : SKILL_BODY;
127
+ const edition = cloud ? 'cloud' : 'standard';
128
+ return `---\nname: ${SKILL_NAME}\ndescription: "${description}"\n---\n\n<!-- sdocs-skill: v=${version} -->\n<!-- sdocs-skill-edition: ${edition} -->\n${body}`;
129
+ }
130
+
131
+ const SKILL_VERSION_RE = /<!-- sdocs-skill: v=(\d+) -->/;
132
+ const SKILL_EDITION_RE = /<!-- sdocs-skill-edition: (standard|cloud) -->/;
133
+
134
+ // Returns the embedded skill version, or null if the content is not our skill.
135
+ function readSkillVersion(content) {
136
+ const m = SKILL_VERSION_RE.exec(content || '');
137
+ return m ? parseInt(m[1], 10) : null;
138
+ }
139
+
140
+ function readSkillEdition(content) {
141
+ const match = SKILL_EDITION_RE.exec(content || '');
142
+ return match ? match[1] : 'standard';
143
+ }
144
+
145
+ function canonicalSkillDir(home) {
146
+ return path.join(home, '.agents', 'skills', SKILL_NAME);
147
+ }
148
+ function canonicalSkillFile(home) {
149
+ return path.join(canonicalSkillDir(home), 'SKILL.md');
150
+ }
151
+
152
+ // ── Agent table (derived from vercel-labs/skills/src/agents.ts) ─
153
+ // Each entry: { name, displayName, dir (global skills dir), universal, detect[] }.
154
+ // `universal` agents discover skills via ~/.agents/skills directly, so the
155
+ // canonical copy already covers them and we skip their symlink (avoids the
156
+ // skill listing twice). Non-universal agents get a relative symlink from
157
+ // <dir>/<skill-name> to the canonical dir.
158
+ function resolveSkillAgents(home, env) {
159
+ env = env || {};
160
+ const configHome = (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.trim()) || path.join(home, '.config');
161
+ const claudeHome = (env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.trim()) || path.join(home, '.claude');
162
+ const codexHome = (env.CODEX_HOME && env.CODEX_HOME.trim()) || path.join(home, '.codex');
163
+ const vibeHome = (env.VIBE_HOME && env.VIBE_HOME.trim()) || path.join(home, '.vibe');
164
+ const cwd = env.PWD || process.cwd();
165
+ const h = (...p) => path.join(home, ...p);
166
+ const c = (...p) => path.join(configHome, ...p);
167
+ const e = (name, displayName, dir, universal, detect) => ({ name, displayName, dir, universal, detect });
168
+ const openClawHome = [h('.openclaw'), h('.clawdbot'), h('.moltbot')]
169
+ .find(p => { try { return fs.existsSync(p); } catch (_) { return false; } }) || h('.openclaw');
170
+ return [
171
+ // ── universal: discovered via ~/.agents/skills (canonical copy). No symlink. ──
172
+ e('opencode', 'opencode', c('opencode', 'skills'), true, [c('opencode')]),
173
+ e('codex', 'Codex', path.join(codexHome, 'skills'), true, [codexHome, '/etc/codex']),
174
+ e('gemini-cli', 'Gemini CLI', h('.gemini', 'skills'), true, [h('.gemini')]),
175
+ e('cursor', 'Cursor', h('.cursor', 'skills'), true, [h('.cursor')]),
176
+ e('cline', 'Cline', h('.agents', 'skills'), true, [h('.cline')]),
177
+ e('warp', 'Warp', h('.agents', 'skills'), true, [h('.warp')]),
178
+ e('amp', 'Amp', c('agents', 'skills'), true, [c('amp')]),
179
+ e('kimi-code-cli', 'Kimi Code CLI', c('agents', 'skills'), true, [h('.kimi-code'), h('.kimi')]),
180
+ e('replit', 'Replit', c('agents', 'skills'), true, [path.join(cwd, '.replit'), h('.replit')]),
181
+ e('antigravity', 'Antigravity', h('.gemini', 'antigravity', 'skills'), true, [h('.gemini', 'antigravity')]),
182
+ e('deepagents', 'Deep Agents', h('.deepagents', 'agent', 'skills'), true, [h('.deepagents')]),
183
+ e('firebender', 'Firebender', h('.firebender', 'skills'), true, [h('.firebender')]),
184
+ e('github-copilot', 'GitHub Copilot', h('.copilot', 'skills'), true, [h('.copilot')]),
185
+ // ── non-universal: symlink <dir>/smalldocs -> canonical ──
186
+ e('claude-code', 'Claude Code', path.join(claudeHome, 'skills'), false, [claudeHome]),
187
+ e('pi', 'Pi', h('.pi', 'agent', 'skills'), false, [h('.pi', 'agent')]),
188
+ e('codewhale', 'CodeWhale', h('.codewhale', 'skills'), false, [h('.codewhale')]),
189
+ e('augment', 'Augment', h('.augment', 'skills'), false, [h('.augment')]),
190
+ e('openhands', 'OpenHands', h('.openhands', 'skills'), false, [h('.openhands')]),
191
+ e('windsurf', 'Windsurf', h('.codeium', 'windsurf', 'skills'), false, [h('.codeium', 'windsurf')]),
192
+ e('goose', 'Goose', c('goose', 'skills'), false, [c('goose')]),
193
+ e('crush', 'Crush', c('crush', 'skills'), false, [c('crush')]),
194
+ e('cortex', 'Cortex Code', h('.snowflake', 'cortex', 'skills'), false, [h('.snowflake', 'cortex')]),
195
+ e('roo', 'Roo Code', h('.roo', 'skills'), false, [h('.roo')]),
196
+ e('kilo', 'Kilo Code', h('.kilocode', 'skills'), false, [h('.kilocode')]),
197
+ e('qwen-code', 'Qwen Code', h('.qwen', 'skills'), false, [h('.qwen')]),
198
+ e('qoder', 'Qoder', h('.qoder', 'skills'), false, [h('.qoder')]),
199
+ e('trae', 'Trae', h('.trae', 'skills'), false, [h('.trae')]),
200
+ e('trae-cn', 'Trae CN', h('.trae-cn', 'skills'), false, [h('.trae-cn')]),
201
+ e('droid', 'Droid', h('.factory', 'skills'), false, [h('.factory')]),
202
+ e('kode', 'Kode', h('.kode', 'skills'), false, [h('.kode')]),
203
+ e('kiro-cli', 'Kiro CLI', h('.kiro', 'skills'), false, [h('.kiro')]),
204
+ e('junie', 'Junie', h('.junie', 'skills'), false, [h('.junie')]),
205
+ e('iflow-cli', 'iFlow CLI', h('.iflow', 'skills'), false, [h('.iflow')]),
206
+ e('codebuddy', 'CodeBuddy', h('.codebuddy', 'skills'), false, [path.join(cwd, '.codebuddy'), h('.codebuddy')]),
207
+ e('continue', 'Continue', h('.continue', 'skills'), false, [path.join(cwd, '.continue'), h('.continue')]),
208
+ e('command-code', 'Command Code', h('.commandcode', 'skills'), false, [h('.commandcode')]),
209
+ e('mcpjam', 'MCPJam', h('.mcpjam', 'skills'), false, [h('.mcpjam')]),
210
+ e('mistral-vibe', 'Mistral Vibe', path.join(vibeHome, 'skills'), false, [vibeHome]),
211
+ e('mux', 'Mux', h('.mux', 'skills'), false, [h('.mux')]),
212
+ e('zencoder', 'Zencoder', h('.zencoder', 'skills'), false, [h('.zencoder')]),
213
+ e('neovate', 'Neovate', h('.neovate', 'skills'), false, [h('.neovate')]),
214
+ e('pochi', 'Pochi', h('.pochi', 'skills'), false, [h('.pochi')]),
215
+ e('adal', 'AdaL', h('.adal', 'skills'), false, [h('.adal')]),
216
+ e('bob', 'IBM Bob', h('.bob', 'skills'), false, [h('.bob')]),
217
+ e('openclaw', 'OpenClaw', path.join(openClawHome, 'skills'), false, [h('.openclaw'), h('.clawdbot'), h('.moltbot')]),
218
+ ];
219
+ }
220
+
221
+ // The agent config files that historically received an always-on SmallDocs
222
+ // block. Independent of the skill table: e.g. Codex/Gemini are universal for
223
+ // skills but still carry an old AGENTS.md/GEMINI.md block to strip.
224
+ function legacyBlockTargets(home, env) {
225
+ env = env || {};
226
+ const configHome = (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.trim()) || path.join(home, '.config');
227
+ const claudeHome = (env.CLAUDE_CONFIG_DIR && env.CLAUDE_CONFIG_DIR.trim()) || path.join(home, '.claude');
228
+ const codexHome = (env.CODEX_HOME && env.CODEX_HOME.trim()) || path.join(home, '.codex');
229
+ return [
230
+ { name: 'Claude Code', file: path.join(claudeHome, 'CLAUDE.md') },
231
+ { name: 'Codex', file: path.join(codexHome, 'AGENTS.md') },
232
+ { name: 'Gemini CLI', file: path.join(home, '.gemini', 'GEMINI.md') },
233
+ { name: 'opencode', file: path.join(configHome, 'opencode', 'AGENTS.md') },
234
+ { name: 'pi', file: path.join(home, '.pi', 'agent', 'AGENTS.md') },
235
+ { name: 'CodeWhale', file: path.join(home, '.codewhale', 'AGENTS.md') },
236
+ ];
237
+ }
238
+
239
+ // ── Legacy block detection (for migration stripping) ───────
57
240
  const AGENT_BLOCK_START_PREFIX = '<!-- sdocs-agent-block:start v=';
58
241
  const AGENT_BLOCK_START_RE = /<!-- sdocs-agent-block:start v=(\d+) -->/;
59
242
  const AGENT_BLOCK_END_MARKER = '<!-- sdocs-agent-block:end -->';
60
243
  const AGENT_BLOCK_LEGACY_OPEN = '<!-- sdocs-agent-block -->';
61
244
 
62
- // `detectDir` (optional) is the directory whose existence signals "this agent
63
- // is installed". It defaults to `dir`. pi keeps its global instructions one
64
- // level down (`~/.pi/agent/AGENTS.md`), so we detect on the parent `~/.pi`
65
- // the installer creates and let writeBookendedBlock mkdir the `agent` subdir.
66
- const AGENT_TARGETS = [
67
- { name: 'Claude Code', dir: '.claude', file: 'CLAUDE.md' },
68
- { name: 'Codex', dir: '.codex', file: 'AGENTS.md' },
69
- { name: 'Gemini CLI', dir: '.gemini', file: 'GEMINI.md' },
70
- { name: 'opencode', dir: path.join('.config', 'opencode'), file: 'AGENTS.md' },
71
- { name: 'pi', dir: path.join('.pi', 'agent'), file: 'AGENTS.md', detectDir: '.pi' },
72
- { name: 'CodeWhale', dir: '.codewhale', file: 'AGENTS.md' },
73
- ];
245
+ // Back-compat aliases (older code/tests reference these names).
246
+ const AGENT_BLOCK_VERSION = SKILL_VERSION;
247
+ const AGENT_BLOCK_BODY = SKILL_BODY;
74
248
 
75
249
  function formatAgentBlock(version, body) {
76
250
  return `${AGENT_BLOCK_START_PREFIX}${version} -->\n${body}${AGENT_BLOCK_END_MARKER}\n`;
@@ -99,8 +273,6 @@ function findBookendedBlock(content) {
99
273
  }
100
274
 
101
275
  // Find a legacy open-only block (1.4.x format). Returns { start, end, version } | null.
102
- // Only matches bodies whose terminator is the JoshInLisbon URL line, which is the
103
- // known shape of v1 (1.4.0/1.4.1) and v2 (1.4.2). Hand-edited bodies return null.
104
276
  function findLegacyBlock(content) {
105
277
  const idx = content.indexOf(AGENT_BLOCK_LEGACY_OPEN);
106
278
  if (idx < 0) return null;
@@ -111,30 +283,28 @@ function findLegacyBlock(content) {
111
283
  if (termIdx < 0) return null;
112
284
  const blockEnd = termIdx + terminator.length;
113
285
  const region = content.slice(idx, blockEnd);
114
- // Heuristic to recover from-version: v2 added the copy-code line, v1 didn't.
115
286
  const version = region.includes('Also handy for copying specific code') ? 2 : 1;
116
287
  return { start: idx, end: blockEnd, version };
117
288
  }
118
289
 
119
- // Pure: takes content, returns refresh result.
120
- // { changed: false, reason: 'absent'|'current'|'newer'|'hand_edited' }
121
- // { changed: true, content, fromVersion, toVersion }
290
+ // Pure: takes content, returns refresh result (rewrites block in place).
291
+ // Kept for tests / reference; production migration uses removeBlockContent.
122
292
  function refreshContent(content) {
123
293
  const bookended = findBookendedBlock(content);
124
294
  if (bookended) {
125
- if (bookended.version === AGENT_BLOCK_VERSION) {
295
+ if (bookended.version === SKILL_VERSION) {
126
296
  return { changed: false, reason: 'current' };
127
297
  }
128
- if (bookended.version > AGENT_BLOCK_VERSION) {
298
+ if (bookended.version > SKILL_VERSION) {
129
299
  return { changed: false, reason: 'newer' };
130
300
  }
131
301
  return {
132
302
  changed: true,
133
303
  content: content.slice(0, bookended.start)
134
- + formatAgentBlock(AGENT_BLOCK_VERSION, AGENT_BLOCK_BODY)
304
+ + formatAgentBlock(SKILL_VERSION, SKILL_BODY)
135
305
  + content.slice(bookended.end),
136
306
  fromVersion: bookended.version,
137
- toVersion: AGENT_BLOCK_VERSION,
307
+ toVersion: SKILL_VERSION,
138
308
  };
139
309
  }
140
310
  const legacy = findLegacyBlock(content);
@@ -144,13 +314,33 @@ function refreshContent(content) {
144
314
  return {
145
315
  changed: true,
146
316
  content: content.slice(0, legacy.start)
147
- + formatAgentBlock(AGENT_BLOCK_VERSION, AGENT_BLOCK_BODY)
317
+ + formatAgentBlock(SKILL_VERSION, SKILL_BODY)
148
318
  + content.slice(legacy.end),
149
319
  fromVersion: legacy.version,
150
- toVersion: AGENT_BLOCK_VERSION,
320
+ toVersion: SKILL_VERSION,
151
321
  };
152
322
  }
153
323
 
324
+ // Pure: remove any recognised SmallDocs block from content. Used by the
325
+ // skill migration so the reference is not loaded twice (always-on block +
326
+ // on-demand skill). Surrounding user text is preserved; only the blank-line
327
+ // seam the installer originally added is normalised.
328
+ function removeBlockContent(content) {
329
+ const region = findBookendedBlock(content) || findLegacyBlock(content);
330
+ if (!region) {
331
+ return { changed: false, reason: content.includes(AGENT_BLOCK_LEGACY_OPEN) ? 'hand_edited' : 'absent' };
332
+ }
333
+ const before = content.slice(0, region.start).replace(/\n+$/, '');
334
+ const after = content.slice(region.end).replace(/^\n+/, '');
335
+ let out;
336
+ if (before && after) out = before + '\n\n' + after;
337
+ else if (before) out = before;
338
+ else if (after) out = after;
339
+ else out = '';
340
+ if (out && !out.endsWith('\n')) out += '\n';
341
+ return { changed: true, content: out, version: region.version };
342
+ }
343
+
154
344
  function compareVersions(a, b) {
155
345
  const A = String(a || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
156
346
  const B = String(b || '0.0.0').split('.').map(n => parseInt(n, 10) || 0);
@@ -165,8 +355,6 @@ function compareVersions(a, b) {
165
355
 
166
356
  const SETUP_SCHEMA_VERSION = 1;
167
357
 
168
- // Pre-1.5.0 setup.json had no `schemaVersion`. Existing users wrote the block
169
- // (so they want it kept current) but were never asked about auto-install.
170
358
  function migrateSetupState(raw) {
171
359
  if (!raw || typeof raw !== 'object') return null;
172
360
  if (raw.schemaVersion === SETUP_SCHEMA_VERSION) return raw;
@@ -208,8 +396,7 @@ function writeSetupState(state) {
208
396
  }
209
397
 
210
398
  // Pure: given a batch of refresh results plus the current binary version,
211
- // decide whether a missing setup.json should be lazily populated. Returns the
212
- // state object to write, or null to leave state untouched.
399
+ // decide whether a missing setup.json should be lazily populated.
213
400
  function implicitConsentState(results, version, now = new Date()) {
214
401
  const changed = results.filter(r => r.changed);
215
402
  if (changed.length === 0) return null;
@@ -225,19 +412,35 @@ function implicitConsentState(results, version, now = new Date()) {
225
412
  }
226
413
 
227
414
  module.exports = {
415
+ SKILL_VERSION,
416
+ SKILL_REASON,
417
+ SKILL_NAME,
418
+ SKILL_DESCRIPTION,
419
+ CLOUD_SKILL_DESCRIPTION,
420
+ SKILL_BODY,
421
+ CLOUD_SKILL_BODY,
422
+ CLOUD_SKILL_SECTION,
423
+ formatSkill,
424
+ readSkillVersion,
425
+ readSkillEdition,
426
+ canonicalSkillDir,
427
+ canonicalSkillFile,
428
+ resolveSkillAgents,
429
+ legacyBlockTargets,
430
+ // legacy-block detection / migration
228
431
  AGENT_BLOCK_VERSION,
229
- AGENT_BLOCK_REASON,
230
432
  AGENT_BLOCK_BODY,
231
433
  AGENT_BLOCK_START_PREFIX,
232
434
  AGENT_BLOCK_START_RE,
233
435
  AGENT_BLOCK_END_MARKER,
234
436
  AGENT_BLOCK_LEGACY_OPEN,
235
- AGENT_TARGETS,
236
- SETUP_SCHEMA_VERSION,
237
437
  formatAgentBlock,
238
438
  findBookendedBlock,
239
439
  findLegacyBlock,
240
440
  refreshContent,
441
+ removeBlockContent,
442
+ // setup state
443
+ SETUP_SCHEMA_VERSION,
241
444
  compareVersions,
242
445
  migrateSetupState,
243
446
  readSetupState,