sdocs-dev 1.14.1 → 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 +35 -11
- package/lib/agent-block.js +254 -51
- package/lib/agent-files.js +308 -74
- package/lib/cells-verify.js +1 -0
- package/lib/cloud-bindings.js +85 -0
- package/lib/cloud-commands.js +741 -0
- package/lib/cloud-credentials.js +380 -0
- package/lib/code-langs.js +8 -2
- package/lib/commands.js +121 -40
- package/lib/help-text.js +342 -52
- package/lib/io.js +100 -5
- package/lib/library-commands.js +6 -15
- package/lib/library-scan.js +16 -6
- package/lib/library-server.js +4 -12
- package/lib/setup.js +145 -189
- package/lib/slides-verify.js +109 -0
- package/package.json +1 -1
- package/shared/sdocs-cells-formula.js +547 -73
- package/shared/sdocs-cells.js +164 -18
- package/shared/sdocs-shapes.js +1044 -0
- package/shared/sdocs-slide-resolve.js +258 -0
- package/shared/sdocs-slide-stdlib.js +180 -0
- package/shared/sdocs-styles.js +1 -1
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) => {
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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,
|
package/lib/agent-block.js
CHANGED
|
@@ -1,27 +1,53 @@
|
|
|
1
|
-
// Pure data model for the SmallDocs agent
|
|
1
|
+
// Pure data model for the SmallDocs agent skill + legacy-block migration.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
-
|
|
22
|
-
const
|
|
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
|
-
|
|
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
|
|
37
|
-
- \`sdoc
|
|
38
|
-
- \`sdoc
|
|
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
|
|
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
|
-
- \`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:"
|
|
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
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
//
|
|
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 ===
|
|
295
|
+
if (bookended.version === SKILL_VERSION) {
|
|
126
296
|
return { changed: false, reason: 'current' };
|
|
127
297
|
}
|
|
128
|
-
if (bookended.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(
|
|
304
|
+
+ formatAgentBlock(SKILL_VERSION, SKILL_BODY)
|
|
135
305
|
+ content.slice(bookended.end),
|
|
136
306
|
fromVersion: bookended.version,
|
|
137
|
-
toVersion:
|
|
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(
|
|
317
|
+
+ formatAgentBlock(SKILL_VERSION, SKILL_BODY)
|
|
148
318
|
+ content.slice(legacy.end),
|
|
149
319
|
fromVersion: legacy.version,
|
|
150
|
-
toVersion:
|
|
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.
|
|
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,
|