@wafflebase/cli 0.3.5 → 0.3.7

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.
Files changed (51) hide show
  1. package/README.md +129 -0
  2. package/dist/bin.js +6 -12
  3. package/dist/client/http-client.d.ts +4 -1
  4. package/dist/client/http-client.js +12 -2
  5. package/dist/commands/api-keys.d.ts +2 -0
  6. package/dist/commands/{api-key.js → api-keys.js} +5 -2
  7. package/dist/commands/cells.d.ts +2 -0
  8. package/dist/commands/{cell.js → cells.js} +5 -2
  9. package/dist/commands/docs.d.ts +3 -0
  10. package/dist/commands/docs.js +306 -0
  11. package/dist/commands/sheets-export.d.ts +2 -0
  12. package/dist/commands/{export.js → sheets-export.js} +2 -2
  13. package/dist/commands/sheets-import.d.ts +2 -0
  14. package/dist/commands/{import.js → sheets-import.js} +2 -2
  15. package/dist/commands/sheets.d.ts +2 -0
  16. package/dist/commands/sheets.js +17 -0
  17. package/dist/commands/tabs.d.ts +2 -0
  18. package/dist/commands/{tab.js → tabs.js} +2 -2
  19. package/dist/docs/content.d.ts +37 -0
  20. package/dist/docs/content.js +90 -0
  21. package/dist/docs/docx-export.d.ts +23 -0
  22. package/dist/docs/docx-export.js +14 -0
  23. package/dist/docs/docx-import.d.ts +36 -0
  24. package/dist/docs/docx-import.js +70 -0
  25. package/dist/docs/dom-polyfill.d.ts +1 -0
  26. package/dist/docs/dom-polyfill.js +29 -0
  27. package/dist/docs/fontkit-measurer.d.ts +41 -0
  28. package/dist/docs/fontkit-measurer.js +64 -0
  29. package/dist/docs/import.d.ts +72 -0
  30. package/dist/docs/import.js +146 -0
  31. package/dist/docs/page-range.d.ts +29 -0
  32. package/dist/docs/page-range.js +74 -0
  33. package/dist/docs/page-slice.d.ts +45 -0
  34. package/dist/docs/page-slice.js +61 -0
  35. package/dist/docs/paginate.d.ts +13 -0
  36. package/dist/docs/paginate.js +19 -0
  37. package/dist/docs/pdf-export.d.ts +38 -0
  38. package/dist/docs/pdf-export.js +91 -0
  39. package/dist/output/binary.d.ts +37 -0
  40. package/dist/output/binary.js +47 -0
  41. package/dist/output/formatter.js +15 -1
  42. package/dist/schema/registry.d.ts +27 -0
  43. package/dist/schema/registry.js +103 -23
  44. package/package.json +7 -2
  45. package/dist/commands/api-key.d.ts +0 -2
  46. package/dist/commands/cell.d.ts +0 -2
  47. package/dist/commands/document.d.ts +0 -2
  48. package/dist/commands/document.js +0 -93
  49. package/dist/commands/export.d.ts +0 -2
  50. package/dist/commands/import.d.ts +0 -2
  51. package/dist/commands/tab.d.ts +0 -2
@@ -0,0 +1,74 @@
1
+ const SINGLE = /^\d+$/;
2
+ const RANGE = /^(\d+)-(\d+)$/;
3
+ /**
4
+ * Parse a `--pages` flag value (e.g., `"1-3,5,7-9"`) against a known
5
+ * page count.
6
+ *
7
+ * Throws `Error` on malformed input — empty strings, non-numeric tokens,
8
+ * a `0` page (1-based), or a reversed range (`"3-1"`). Out-of-range
9
+ * upper bounds are *not* errors: they get clamped to `totalPages` with a
10
+ * stderr-bound warning string in the result. Tokens whose entire range
11
+ * sits past `totalPages` collapse to no pages with a warning, never an
12
+ * error — this lets `--pages 1-99` work on a 3-page doc.
13
+ *
14
+ * `totalPages` of 0 is a valid input (empty doc) and yields an empty set
15
+ * with a single "document has no pages" warning, regardless of input.
16
+ */
17
+ export function parsePageRange(input, totalPages) {
18
+ if (totalPages < 0) {
19
+ throw new Error(`parsePageRange: totalPages must be ≥ 0, got ${totalPages}`);
20
+ }
21
+ const trimmed = input.trim();
22
+ if (trimmed === '') {
23
+ throw new Error('Page range is empty.');
24
+ }
25
+ const tokens = trimmed.split(',').map((t) => t.trim());
26
+ for (const tok of tokens) {
27
+ if (tok === '') {
28
+ throw new Error(`Empty token in page range "${input}"`);
29
+ }
30
+ }
31
+ const pages = new Set();
32
+ const warnings = [];
33
+ if (totalPages === 0) {
34
+ warnings.push('Document has no pages.');
35
+ return { pages, warnings };
36
+ }
37
+ for (const tok of tokens) {
38
+ if (SINGLE.test(tok)) {
39
+ const n = Number(tok);
40
+ if (n < 1) {
41
+ throw new Error(`Page numbers are 1-based; got "${tok}"`);
42
+ }
43
+ if (n > totalPages) {
44
+ warnings.push(`Page ${n} is beyond document end (${totalPages}); ignored.`);
45
+ continue;
46
+ }
47
+ pages.add(n);
48
+ continue;
49
+ }
50
+ const m = RANGE.exec(tok);
51
+ if (!m) {
52
+ throw new Error(`Invalid page token "${tok}" in "${input}"`);
53
+ }
54
+ const lo = Number(m[1]);
55
+ const hi = Number(m[2]);
56
+ if (lo < 1 || hi < 1) {
57
+ throw new Error(`Page numbers are 1-based; got "${tok}"`);
58
+ }
59
+ if (lo > hi) {
60
+ throw new Error(`Reversed range "${tok}" — start must be ≤ end.`);
61
+ }
62
+ if (lo > totalPages) {
63
+ warnings.push(`Range ${tok} starts beyond document end (${totalPages}); ignored.`);
64
+ continue;
65
+ }
66
+ const clampedHi = Math.min(hi, totalPages);
67
+ if (clampedHi < hi) {
68
+ warnings.push(`Range ${tok} clamped to ${lo}-${clampedHi} (document has ${totalPages} pages).`);
69
+ }
70
+ for (let p = lo; p <= clampedHi; p++)
71
+ pages.add(p);
72
+ }
73
+ return { pages, warnings };
74
+ }
@@ -0,0 +1,45 @@
1
+ import type { Block, BlockPageMeta, Document, PaginatedLayout } from '@wafflebase/docs';
2
+ import type { PageRange } from './page-range.js';
3
+ /**
4
+ * Output formats supported by the CLI's `docs content --pages` flow.
5
+ *
6
+ * `'json'` keeps full structural fidelity and triggers `pageMeta`
7
+ * emission so consumers can recover page boundaries without re-running
8
+ * pagination. `'md'` and `'text'` drop the metadata since the
9
+ * serializers themselves are page-blind — block selection alone is
10
+ * enough.
11
+ */
12
+ export type SliceFormat = 'json' | 'md' | 'text';
13
+ export interface PageSliceResult {
14
+ /**
15
+ * Subset of `doc.blocks` whose layout lines intersect the requested
16
+ * pages, in original document order. A block that spans multiple
17
+ * requested pages appears exactly once.
18
+ */
19
+ blocks: Block[];
20
+ /**
21
+ * Per-block 1-based page indices for the included blocks. Present
22
+ * only when `format === 'json'` so the JSON serializer's `_pageMeta`
23
+ * field stays accurate after slicing. Always in the same order as
24
+ * `blocks`.
25
+ */
26
+ pageMeta?: BlockPageMeta[];
27
+ }
28
+ /**
29
+ * Select the blocks of `doc` that participate in any of the pages named
30
+ * by `range`, optionally producing per-block page metadata for the JSON
31
+ * format. Block ordering matches `doc.blocks` — the slicer never
32
+ * reorders content, only filters it.
33
+ *
34
+ * Selection rule (per `docs/design/docs-cli.md` §5.2): include any block
35
+ * whose layout has at least one line on a requested page. Blocks emit
36
+ * whole — there is no mid-block cut. A block that straddles the
37
+ * boundary between a requested page and a non-requested one still
38
+ * appears in the result, in full.
39
+ *
40
+ * Blocks with no layout lines (e.g., zero-height collapsed blocks the
41
+ * paginator omitted) drop out entirely; if a future block type needs
42
+ * "always include" semantics, the caller should pre-tag those blocks
43
+ * before calling this function.
44
+ */
45
+ export declare function sliceBlocksByPages(doc: Document, layout: PaginatedLayout, range: PageRange, format: SliceFormat): PageSliceResult;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Select the blocks of `doc` that participate in any of the pages named
3
+ * by `range`, optionally producing per-block page metadata for the JSON
4
+ * format. Block ordering matches `doc.blocks` — the slicer never
5
+ * reorders content, only filters it.
6
+ *
7
+ * Selection rule (per `docs/design/docs-cli.md` §5.2): include any block
8
+ * whose layout has at least one line on a requested page. Blocks emit
9
+ * whole — there is no mid-block cut. A block that straddles the
10
+ * boundary between a requested page and a non-requested one still
11
+ * appears in the result, in full.
12
+ *
13
+ * Blocks with no layout lines (e.g., zero-height collapsed blocks the
14
+ * paginator omitted) drop out entirely; if a future block type needs
15
+ * "always include" semantics, the caller should pre-tag those blocks
16
+ * before calling this function.
17
+ */
18
+ export function sliceBlocksByPages(doc, layout, range, format) {
19
+ const wantedPages = range.pages;
20
+ // Group all line pageIndices by blockIndex so we can both decide
21
+ // inclusion and reuse the lookup for `pageMeta`. Walking pages in
22
+ // order keeps the per-block `lines` list in document order.
23
+ //
24
+ // Duplicates are intentional: `BlockPageMeta.lines` is contractually
25
+ // "one entry per layout line, in order" — a block with three lines
26
+ // all on page 1 yields `[1, 1, 1]`. See `serialize/json.ts` for the
27
+ // canonical contract; the consumer is meant to count entries to
28
+ // recover the per-block line count, not treat the array as a set.
29
+ const linesByBlock = new Map();
30
+ for (const page of layout.pages) {
31
+ for (const pl of page.lines) {
32
+ const bucket = linesByBlock.get(pl.blockIndex);
33
+ if (bucket)
34
+ bucket.push(pl.pageIndex);
35
+ else
36
+ linesByBlock.set(pl.blockIndex, [pl.pageIndex]);
37
+ }
38
+ }
39
+ const blocks = [];
40
+ const pageMeta = [];
41
+ for (let i = 0; i < doc.blocks.length; i++) {
42
+ const lines = linesByBlock.get(i);
43
+ if (!lines || lines.length === 0)
44
+ continue;
45
+ let intersects = false;
46
+ for (const p of lines) {
47
+ if (wantedPages.has(p)) {
48
+ intersects = true;
49
+ break;
50
+ }
51
+ }
52
+ if (!intersects)
53
+ continue;
54
+ const block = doc.blocks[i];
55
+ blocks.push(block);
56
+ if (format === 'json') {
57
+ pageMeta.push({ blockId: block.id, lines });
58
+ }
59
+ }
60
+ return format === 'json' ? { blocks, pageMeta } : { blocks };
61
+ }
@@ -0,0 +1,13 @@
1
+ import { type Document, type PaginatedLayout, type TextMeasurer } from '@wafflebase/docs';
2
+ /**
3
+ * Run `computeLayout` + `paginateLayout` for a fetched `Document` using
4
+ * the supplied measurer. The CLI calls this only when `--pages` is set
5
+ * (or any other flag that needs page boundaries) — the bare `docs
6
+ * content` JSON path skips pagination entirely.
7
+ *
8
+ * Page setup falls back to `DEFAULT_PAGE_SETUP` (US Letter portrait, 1in
9
+ * margins) when the document doesn't carry one. Content width is the
10
+ * effective page width minus left/right margins, matching what the
11
+ * editor's `DocCanvas` computes.
12
+ */
13
+ export declare function paginateForCli(doc: Document, measurer: TextMeasurer): PaginatedLayout;
@@ -0,0 +1,19 @@
1
+ import { computeLayout, paginateLayout, resolvePageSetup, getEffectiveDimensions, } from '@wafflebase/docs';
2
+ /**
3
+ * Run `computeLayout` + `paginateLayout` for a fetched `Document` using
4
+ * the supplied measurer. The CLI calls this only when `--pages` is set
5
+ * (or any other flag that needs page boundaries) — the bare `docs
6
+ * content` JSON path skips pagination entirely.
7
+ *
8
+ * Page setup falls back to `DEFAULT_PAGE_SETUP` (US Letter portrait, 1in
9
+ * margins) when the document doesn't carry one. Content width is the
10
+ * effective page width minus left/right margins, matching what the
11
+ * editor's `DocCanvas` computes.
12
+ */
13
+ export function paginateForCli(doc, measurer) {
14
+ const pageSetup = resolvePageSetup(doc.pageSetup);
15
+ const { width: paperWidth } = getEffectiveDimensions(pageSetup);
16
+ const contentWidth = paperWidth - pageSetup.margins.left - pageSetup.margins.right;
17
+ const { layout } = computeLayout(doc.blocks, measurer, contentWidth);
18
+ return paginateLayout(layout, pageSetup);
19
+ }
@@ -0,0 +1,38 @@
1
+ import { type Document, type PdfFontsOptions } from '@wafflebase/docs';
2
+ import type { PageRange } from './page-range.js';
3
+ /**
4
+ * Options for the CLI PDF export pipeline.
5
+ *
6
+ * `pages` is the resolved selection from `parsePageRange`; when present
7
+ * we render the full PDF first and then strip non-selected pages via
8
+ * `pdf-lib` (cheaper than re-implementing a partial paint path inside
9
+ * `PdfExporter`). The future plan is to push a `pageIndices` option
10
+ * down into `PdfExporter` itself; for now the post-process is correct
11
+ * but pays for layout/paint work it then throws away.
12
+ *
13
+ * `fontSources` lets tests inject in-memory buffers via `PdfFonts` so
14
+ * `exportPdf` can run without hitting jsdelivr.
15
+ */
16
+ export interface CliPdfExportOptions {
17
+ pages?: PageRange;
18
+ /** Currently informational — `PdfExporter` always paints
19
+ * header/footer when `doc.header`/`doc.footer` are present. The CLI
20
+ * surfaces the flag for parity with `docs export --include-header-footer`
21
+ * and to leave room for a future "strip header/footer" mode. */
22
+ includeHeaderFooter?: boolean;
23
+ fontSources?: PdfFontsOptions['sources'];
24
+ }
25
+ /**
26
+ * Render `doc` to a PDF byte buffer using the same `PdfExporter` the
27
+ * editor uses, with a `fontkit`-backed measurer in place of the browser
28
+ * Canvas measurer. Korean fonts referenced by the document are
29
+ * pre-loaded through `PdfFonts` and registered with the measurer so
30
+ * pagination matches what the painter then draws.
31
+ *
32
+ * Latin text falls back to the measurer's coarse 0.5em estimate
33
+ * (Helvetica/Times metrics aren't bundled). Pure-ASCII docs may
34
+ * therefore line-break differently in the CLI than in the browser; the
35
+ * paint output stays correct because `PdfPainter` uses pdf-lib's
36
+ * StandardFonts at draw time.
37
+ */
38
+ export declare function exportPdf(doc: Document, opts?: CliPdfExportOptions): Promise<Uint8Array>;
@@ -0,0 +1,91 @@
1
+ import { PDFDocument } from 'pdf-lib';
2
+ import { PdfExporter, PdfFonts, scanFontsUsed, } from '@wafflebase/docs';
3
+ import { FontkitMeasurer } from './fontkit-measurer.js';
4
+ /**
5
+ * Render `doc` to a PDF byte buffer using the same `PdfExporter` the
6
+ * editor uses, with a `fontkit`-backed measurer in place of the browser
7
+ * Canvas measurer. Korean fonts referenced by the document are
8
+ * pre-loaded through `PdfFonts` and registered with the measurer so
9
+ * pagination matches what the painter then draws.
10
+ *
11
+ * Latin text falls back to the measurer's coarse 0.5em estimate
12
+ * (Helvetica/Times metrics aren't bundled). Pure-ASCII docs may
13
+ * therefore line-break differently in the CLI than in the browser; the
14
+ * paint output stays correct because `PdfPainter` uses pdf-lib's
15
+ * StandardFonts at draw time.
16
+ */
17
+ export async function exportPdf(doc, opts = {}) {
18
+ const usage = scanFontsUsed(doc);
19
+ const fonts = new PdfFonts({ sources: opts.fontSources });
20
+ const measurer = new FontkitMeasurer();
21
+ await preloadKoreanFonts(fonts, measurer, usage);
22
+ const blob = await PdfExporter.export(doc, { measurer, fonts });
23
+ const fullBytes = new Uint8Array(await blob.arrayBuffer());
24
+ if (!opts.pages || opts.pages.pages.size === 0) {
25
+ return fullBytes;
26
+ }
27
+ return extractPages(fullBytes, opts.pages);
28
+ }
29
+ /**
30
+ * Load every Korean variant `usage` reports as needed and register each
31
+ * loaded buffer with the measurer under both the canonical `'Noto …'`
32
+ * family name and the editor-side aliases (`바탕`, `Batang`) so inline
33
+ * styles that pick a non-canonical family still hit a real font instead
34
+ * of the fallback estimate.
35
+ */
36
+ async function preloadKoreanFonts(fonts, measurer, usage) {
37
+ const variants = [];
38
+ if (usage.needsKR) {
39
+ variants.push({
40
+ key: 'kr-sans-regular',
41
+ weight: 'normal',
42
+ families: ['Noto Sans KR'],
43
+ });
44
+ }
45
+ if (usage.needsKR && usage.needsBold) {
46
+ variants.push({
47
+ key: 'kr-sans-bold',
48
+ weight: 'bold',
49
+ families: ['Noto Sans KR'],
50
+ });
51
+ }
52
+ if (usage.needsKRSerif) {
53
+ variants.push({
54
+ key: 'kr-serif-regular',
55
+ weight: 'normal',
56
+ families: ['Noto Serif KR', '바탕', 'Batang'],
57
+ });
58
+ }
59
+ if (usage.needsKRSerif && usage.needsBold) {
60
+ variants.push({
61
+ key: 'kr-serif-bold',
62
+ weight: 'bold',
63
+ families: ['Noto Serif KR', '바탕', 'Batang'],
64
+ });
65
+ }
66
+ for (const v of variants) {
67
+ const buf = await fonts.load(v.key);
68
+ for (const family of v.families) {
69
+ measurer.register(family, v.weight, 'normal', buf);
70
+ }
71
+ }
72
+ }
73
+ /**
74
+ * Keep only the pages named by `range` and return the new PDF bytes.
75
+ *
76
+ * Iterates from the highest index down so each `removePage(idx)` call
77
+ * doesn't shift the indices of pages we haven't visited yet. Pages in
78
+ * `range.pages` are 1-based; pdf-lib indexes from 0.
79
+ */
80
+ async function extractPages(bytes, range) {
81
+ const pdf = await PDFDocument.load(bytes);
82
+ const total = pdf.getPageCount();
83
+ for (let i = total - 1; i >= 0; i--) {
84
+ const oneBased = i + 1;
85
+ if (!range.pages.has(oneBased)) {
86
+ pdf.removePage(i);
87
+ }
88
+ }
89
+ const out = await pdf.save();
90
+ return out;
91
+ }
@@ -0,0 +1,37 @@
1
+ export interface WriteBinaryOptions {
2
+ /** Allow overwriting an existing file. Without this flag, `writeBinary`
3
+ * refuses to clobber and throws. Mirrors `--force` on the CLI. */
4
+ force?: boolean;
5
+ /** Suppress informational stderr output. The bytes themselves still
6
+ * reach stdout/the file regardless of this flag. */
7
+ quiet?: boolean;
8
+ }
9
+ /**
10
+ * Write `bytes` to `target`, where `target` is either a filesystem path
11
+ * or `'-'` for stdout. The default behavior refuses to overwrite an
12
+ * existing file because exports are usually irreversible (the source
13
+ * doc is the canonical store but a stale local copy can still mislead).
14
+ *
15
+ * The function is split out from the command action so binary output
16
+ * paths can be unit-tested without spawning the CLI binary; the action
17
+ * supplies real `process.stdout.write` and the IO surface forwards it.
18
+ *
19
+ * The default `writeFile` honors `force` via the `'w'` (overwrite) /
20
+ * `'wx'` (exclusive create) `fs` flags, folding the existence check
21
+ * and the write into a single syscall — avoids a TOCTOU window where
22
+ * something else could create the file between the check and the
23
+ * write.
24
+ */
25
+ export declare function writeBinary(bytes: Uint8Array, target: string, opts?: WriteBinaryOptions, io?: BinaryIO): void;
26
+ export interface BinaryIO {
27
+ stdout: (bytes: Uint8Array) => void;
28
+ stderr: (line: string) => void;
29
+ /**
30
+ * Write `bytes` to `path`. When `force` is `false`, the
31
+ * implementation MUST refuse to overwrite an existing file and
32
+ * throw — the production `defaultBinaryIO` does this via the
33
+ * exclusive-create `'wx'` flag.
34
+ */
35
+ writeFile: (path: string, bytes: Uint8Array, force: boolean) => void;
36
+ }
37
+ export declare const defaultBinaryIO: BinaryIO;
@@ -0,0 +1,47 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ /**
3
+ * Write `bytes` to `target`, where `target` is either a filesystem path
4
+ * or `'-'` for stdout. The default behavior refuses to overwrite an
5
+ * existing file because exports are usually irreversible (the source
6
+ * doc is the canonical store but a stale local copy can still mislead).
7
+ *
8
+ * The function is split out from the command action so binary output
9
+ * paths can be unit-tested without spawning the CLI binary; the action
10
+ * supplies real `process.stdout.write` and the IO surface forwards it.
11
+ *
12
+ * The default `writeFile` honors `force` via the `'w'` (overwrite) /
13
+ * `'wx'` (exclusive create) `fs` flags, folding the existence check
14
+ * and the write into a single syscall — avoids a TOCTOU window where
15
+ * something else could create the file between the check and the
16
+ * write.
17
+ */
18
+ export function writeBinary(bytes, target, opts = {}, io = defaultBinaryIO) {
19
+ if (target === '-') {
20
+ io.stdout(bytes);
21
+ return;
22
+ }
23
+ io.writeFile(target, bytes, opts.force === true);
24
+ if (!opts.quiet)
25
+ io.stderr(`Wrote ${bytes.length} bytes to ${target}`);
26
+ }
27
+ export const defaultBinaryIO = {
28
+ stdout: (bytes) => {
29
+ process.stdout.write(bytes);
30
+ },
31
+ stderr: (line) => {
32
+ console.error(line);
33
+ },
34
+ writeFile: (path, bytes, force) => {
35
+ try {
36
+ writeFileSync(path, bytes, { flag: force ? 'w' : 'wx' });
37
+ }
38
+ catch (err) {
39
+ if (err instanceof Error &&
40
+ 'code' in err &&
41
+ err.code === 'EEXIST') {
42
+ throw new Error(`Refusing to overwrite "${path}". Pass --force to allow overwrite.`);
43
+ }
44
+ throw err;
45
+ }
46
+ },
47
+ };
@@ -16,12 +16,26 @@ export function output(data, fmt, quiet) {
16
16
  return;
17
17
  console.log(format(data, fmt));
18
18
  }
19
+ /**
20
+ * Preserve a structured `code` from any thrown `Error` subclass that
21
+ * carries one (e.g., `InvalidDocxError`'s `code = 'INVALID_DOCX'`).
22
+ * Skill files document those codes, so silently flattening every
23
+ * failure to `'ERROR'` made agents unable to branch on the cause.
24
+ */
25
+ function errorCode(error) {
26
+ if (error instanceof Error && 'code' in error) {
27
+ const code = error.code;
28
+ if (typeof code === 'string' && code.length > 0)
29
+ return code;
30
+ }
31
+ return 'ERROR';
32
+ }
19
33
  export function outputError(error, quiet) {
20
34
  if (quiet) {
21
35
  process.exitCode = 1;
22
36
  return;
23
37
  }
24
38
  const message = error instanceof Error ? error.message : String(error);
25
- console.error(JSON.stringify({ error: { code: 'ERROR', message } }, null, 2));
39
+ console.error(JSON.stringify({ error: { code: errorCode(error), message } }, null, 2));
26
40
  process.exitCode = 1;
27
41
  }
@@ -1,4 +1,17 @@
1
1
  export type SafetyLevel = 'read-only' | 'write' | 'destructive';
2
+ /**
3
+ * Per-command safety variant — used by `docs.import` to spell out that
4
+ * the same command is `write` by default but `destructive` with
5
+ * `--replace`. Skill agents read this to decide when extra confirmation
6
+ * is needed.
7
+ */
8
+ export interface SafetyVariant {
9
+ when: string;
10
+ safety: SafetyLevel;
11
+ creates?: string;
12
+ modifies?: string;
13
+ removes?: string;
14
+ }
2
15
  export interface CommandSchema {
3
16
  name: string;
4
17
  description: string;
@@ -10,6 +23,20 @@ export interface CommandSchema {
10
23
  default?: string;
11
24
  }>;
12
25
  response: Record<string, unknown>;
26
+ /**
27
+ * Singular / legacy / namespace-stripped names that should resolve to
28
+ * this canonical entry. `getCommandSchema('cell.get')` returns the
29
+ * `sheets.cells.get` schema because `'cell.get'` appears here.
30
+ */
31
+ aliases?: string[];
32
+ /** Optional per-flag safety overrides (currently only `docs.import`). */
33
+ variants?: SafetyVariant[];
13
34
  }
35
+ /**
36
+ * Look up a schema entry by canonical name or any registered alias.
37
+ * Aliases let scripts and skills written against the v0.3.x singular
38
+ * names (`cell.get`, `doc.list`, `import`) keep working after the
39
+ * v0.3.7 namespace shuffle.
40
+ */
14
41
  export declare function getCommandSchema(name: string): CommandSchema | undefined;
15
42
  export declare function getAllCommandSchemas(): CommandSchema[];