Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

### Added

- Native Sheets structure commands create/rename tabs, insert bounded rows/columns and format
at most 10,000 cells. Formatting masks only supplied leaf properties and preserves cell values.
Equivalent raw/generated batch requests check the same profile permission keys.

- Live Sheets commands inspect tabs, read ranges or formulas, update finite ranges, append rows
and clear values. Writes require the explicit Drive write grant and reuse profile policy guards.
Values default to literals; formula parsing is opt-in. Enable Sheets API for these commands.
Expand All @@ -13,6 +17,9 @@

### Changed

- Uses exact `@wirecat/cli-base` 0.40.1 with the existing compatible Core pin. Six Sheets naming
exceptions are removed; the command lint baseline does not grow.

- Uses `@wirecat/cli-core` 0.307.0.

## 0.15.1 — 2026-10-17
Expand Down
69 changes: 69 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -2774,6 +2774,75 @@ drive <profile> api sheets-spreadsheets-values-update [options]

read spreadsheet tabs and ranges, or change cells live in Google Sheets

### `drive sheets create`

create an empty tab inside an existing spreadsheet

This command can change Google data or send data to the configured model.

```sh
drive <profile> sheets create <spreadsheet> <title>
```

| Argument | | What it is |
|---|---|---|
| `spreadsheet` | required | spreadsheet ID or Google Sheets URL. |
| `title` | required | name of the new tab. |

### `drive sheets rename`

rename one tab while preserving its ID and other properties

This command can change Google data or send data to the configured model.

```sh
drive <profile> sheets rename <spreadsheet> <sheet-id> <title>
```

| Argument | | What it is |
|---|---|---|
| `spreadsheet` | required | spreadsheet ID or Google Sheets URL. |
| `sheet-id` | required | numeric tab ID from sheets info. |
| `title` | required | new tab name. |

### `drive sheets insert`

insert bounded rows or columns before a zero-based index

This command can change Google data or send data to the configured model.

```sh
drive <profile> sheets insert <spreadsheet> <sheet-id> <axis> <index> <count>
```

| Argument | | What it is |
|---|---|---|
| `spreadsheet` | required | spreadsheet ID or Google Sheets URL. |
| `sheet-id` | required | numeric tab ID from sheets info. |
| `axis` | required | rows or columns. |
| `index` | required | zero-based index to insert before. |
| `count` | required | number to insert, from 1 to 1000. |

### `drive sheets format`

change only supplied formatting fields in at most 10000 cells

This command can change Google data or send data to the configured model.

```sh
drive <profile> sheets format <spreadsheet> <sheet-id> <range> [options]
```

| Argument | | What it is |
|---|---|---|
| `spreadsheet` | required | spreadsheet ID or Google Sheets URL. |
| `sheet-id` | required | numeric tab ID from sheets info. |
| `range` | required | finite cells within that tab, such as A1:C4; no tab prefix. |

| Option | What it does |
|---|---|
| `--body <json>` | supported CellFormat fields as JSON; values and formulas are refused. |

### `drive sheets info`

list spreadsheet properties and tabs without downloading grid data
Expand Down
20 changes: 13 additions & 7 deletions docs/dev/CLI_ALIGNMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,16 @@ work and does not certify complete command-standard conformance.

## Native Sheets vocabulary

Sheets uses the canonical `info`, `show`, `update`, `add` and `clear` verbs. `add` appends rows;
`info` returns spreadsheet properties and tab metadata. Generated operations retain Google's names.
The pinned cli-base vocabulary predates this domain. Six dated exceptions register `sheets` and
its `--values` (value), `--input` (mode) and `--render` (output) roles until the shared vocabulary
adopts them ([central vocabulary](https://github.com/WireCatLabs/cli-base/pull/178)). They do not
introduce query filters or baseline entries. Remove these exceptions at
the next compatible shared-vocabulary adoption; the existing baseline remains unchanged.
Sheets uses `info`, `show`, `update`, `add`, `clear`, `create` and `rename` from the shared
vocabulary. `add` appends rows; `create` adds a tab within an existing spreadsheet. Generated
operations retain Google's names. Base 0.40.1 registers `sheets`, `--values` (value), `--input`
(mode) and `--render` (output), so those six dated exceptions are removed. Two dated verb
exceptions remain for `insert` and `format` until a published shared version includes
[their vocabulary registration](https://github.com/WireCatLabs/cli-base/pull/182). The
existing baseline files are unchanged.

Friendly structure commands submit one `spreadsheets.batchUpdate` request. Rename masks only
`title`; format constructs finite GridRange bounds and leaf masks under `userEnteredFormat`.
Raw/generated batches check the corresponding friendly permission keys after body validation,
before preview/credentials/provider dispatch. Broad cell batches consult both format/update
keys when they can affect both. This retains the same guards across command paths.
3 changes: 2 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,8 @@ Replace `FOLDER_ID` and the profile name. Continue sync while pending work remai

Local indexing supports native Google Docs and Sheets, and uploaded Markdown/plain text, PDF with a
text layer, Word `.docx`, Excel `.xlsx`, CSV and TSV. Sheets are imported for reading and search; live [Sheets commands](recipes.md#read-and-change-spreadsheet-cells)
read ranges, update values/formulas, append rows and clear selected cells. Scanned PDFs, Slides, audio and shortcuts
read ranges, update values/formulas, append rows and clear selected cells.
They also [organize tabs and format bounded ranges](recipes.md#organize-tabs-and-format-selected-cells). Scanned PDFs, Slides, audio and shortcuts
have no indexed text. Images are searchable by name, or by what they show when you enable
[picture search](search.md#search-images-by-what-they-show). Native Docs indexing includes body text,
tables and footnotes across tabs; images, headers, footers, comments and suggested edits are not
Expand Down
53 changes: 53 additions & 0 deletions docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ file. [Import and storage](usage.md) explains what the saved copy contains.
| Inspect comments or history | Yes | No automatic import | No |
| Create or edit a Doc | Yes | Imported on the next sync | Yes |
| Read or edit spreadsheet cells | Yes | Imported on the next sync | Only when explicitly changing cells |
| Organize spreadsheet tabs and formatting | Yes | Imported on the next sync | Yes |

## First searchable folder, without extra exports

Expand Down Expand Up @@ -376,3 +377,55 @@ or 10,000-cell checks. Raw `api call` writes also require `--allow-writes`. Gene
comma-separated; use a JSON array in raw `api call --params` when a tab name itself contains a comma.
Some POST-based lookup methods require the broader Drive grant even though their effect is a read;
inspect the method contract and Google authorization requirements before reconnecting.

## Organize tabs and format selected cells

Use tab IDs from `sheets info` to change a specific tab inside an existing spreadsheet. These
commands use the same explicit write connection as cell updates. They change Google data;
refresh the imported copy with `sync` when local search needs the updated source.

```text prompt
Use drive CLI with profile personal. Inspect the tabs in my selected spreadsheet. Create a
Quarter 2 tab, rename the tab I identify, and make only its A1:C1 header bold. Show the returned
tab IDs and changed range. Preserve existing cell values and other formatting.
```

```sh
drive personal sheets info SHEET_ID --json
drive personal sheets create SHEET_ID 'Quarter 2' --json
drive personal sheets rename SHEET_ID 101 'Quarter 1' --json
drive personal sheets insert SHEET_ID 101 rows 1 3 --json
drive personal sheets insert SHEET_ID 101 columns 0 1 --json
drive personal sheets format SHEET_ID 101 A1:C1 --body '{"textFormat":{"bold":true}}' --json
drive personal sheets format SHEET_ID 101 B2:B10 --body '{"numberFormat":{"type":"NUMBER","pattern":"0.00"}}' --json
```

`create` adds an empty tab to the existing spreadsheet, rather than creating a new spreadsheet
file. Replace `101` with the numeric tab ID Google returned. The ID remains stable after rename;
rename changes only the title. Tab names must contain 1–100 characters; Google also checks name
validity and uniqueness.

Insert takes `rows` or `columns`, a **zero-based index to insert before**, and a count of 1–1,000.
Index `0` means the first row/column; row index `1` inserts before the second row. Later entries
shift. Google validates the current grid bounds and spreadsheet limits; use the generated API
for appending grid dimensions at the end. The command requests formatting inheritance from the
dimension after the insertion position.

Format uses an explicit tab ID and a finite range **without a tab prefix**, such as `A1:C4`.
At most 10,000 cells can be selected. Its JSON body supports text bold/italic/underline/
strikethrough, font family/size, foreground/background colors, horizontal/vertical alignment,
wrap strategy and number format. Field names follow Google's `CellFormat` JSON. Unknown fields,
empty objects, cell values and formulas are refused.

Only the formatting properties you provide are included in the request's field mask. Setting
`textFormat.bold` preserves italic text, number formatting and cell values; use `false` to remove
bold. Both color channels (`red`, `green`, `blue`, `alpha`) and theme-color styles are supported.
For other formatting fields or multiple operations in one request, use the generated
`api sheets-spreadsheets-batch-update` command. A generic batch has broader effects and none of
the friendly formatter's 10,000-cell/allowed-field limits.

All paths retain profile policy, OAuth write, offline/read-only, dry-run and uncertain-write
handling. A profile denial such as `sheets.rename` also applies to an equivalent raw/generated
batch request. Inspect current cells before repeating an uncertain structure change; otherwise
you could create duplicate tabs or insert more rows. Separate commands can observe collaborator
changes between requests.
40 changes: 6 additions & 34 deletions lint/overrides.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,16 @@
"review_after": "2026-11-16"
},
{
"rule": "unknown-resource",
"target": "sheets",
"reason": "Spreadsheet-specific resource; shared cli-base 0.40.0 predates the native Sheets commands. Register the noun centrally before removing this exception. Track in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"owner": "@WireCatLabs",
"review_after": "2026-11-17"
},
{
"rule": "option-role",
"target": "sheets show --render",
"reason": "Output role: select Google formatted values, unformatted values or formulas, not a list filter. Track in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"owner": "@WireCatLabs",
"review_after": "2026-11-17"
},
{
"rule": "option-role",
"target": "sheets update --values",
"reason": "Value role: bounded JSON cell rows for this explicit remote write. Track in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"owner": "@WireCatLabs",
"review_after": "2026-11-17"
},
{
"rule": "option-role",
"target": "sheets add --values",
"reason": "Value role: bounded JSON cell rows appended to the selected table. Track in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"owner": "@WireCatLabs",
"review_after": "2026-11-17"
},
{
"rule": "option-role",
"target": "sheets update --input",
"reason": "Mode role: literal RAW input by default; explicit USER_ENTERED formula/locale parsing. Track in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"rule": "unknown-verb",
"target": "sheets insert",
"reason": "Insert bounded source rows or columns before an explicit index. Shared vocabulary registration is tracked in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"owner": "@WireCatLabs",
"review_after": "2026-11-17"
},
{
"rule": "option-role",
"target": "sheets add --input",
"reason": "Mode role: literal RAW input by default; explicit USER_ENTERED formula/locale parsing. Track in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"rule": "unknown-verb",
"target": "sheets format",
"reason": "Change only selected cell-format fields, not source values/formulas. Shared vocabulary registration is tracked in docs/dev/CLI_ALIGNMENT.md; no baseline entries added.",
"owner": "@WireCatLabs",
"review_after": "2026-11-17"
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"@googleapis/driveactivity": "11.0.1",
"@googleapis/sheets": "18.1.1",
"@modelcontextprotocol/server": "2.1.0",
"@wirecat/cli-base": "0.40.0",
"@wirecat/cli-base": "0.40.1",
"@wirecat/cli-core": "0.307.0",
"commander": "15.0.0",
"google-auth-library": "11.2.0",
Expand Down
16 changes: 8 additions & 8 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ allowBuilds:
esbuild: true
lefthook: true
minimumReleaseAgeExclude:
- '@wirecat/cli-base@0.21.0 || 0.22.0 || 0.24.0 || 0.26.0 || 0.27.1 || 0.27.2 || 0.28.1 || 0.28.2 || 0.31.0 || 0.31.1 || 0.32.0 || 0.33.0 || 0.35.0 || 0.40.0'
- '@wirecat/cli-base@0.21.0 || 0.22.0 || 0.24.0 || 0.26.0 || 0.27.1 || 0.27.2 || 0.28.1 || 0.28.2 || 0.31.0 || 0.31.1 || 0.32.0 || 0.33.0 || 0.35.0 || 0.40.0 || 0.40.1'
- '@wirecat/cli-meetings@0.2.0 || 0.2.1 || 0.2.3 || 0.2.7 || 0.3.0'
- '@wirecat/cli-messaging@0.226.0 || 0.231.0 || 0.253.0 || 0.256.0 || 0.258.0'
- '@wirecat/cli-core@0.262.0 || 0.264.0 || 0.265.0 || 0.268.0 || 0.272.0 || 0.273.0 || 0.275.0 || 0.277.0 || 0.278.0 || 0.281.0 || 0.283.0 || 0.285.0 || 0.287.0 || 0.288.0 || 0.289.0 || 0.292.0 || 0.294.0 || 0.295.0 || 0.299.0 || 0.301.0 || 0.307.0'
Expand Down
16 changes: 16 additions & 0 deletions scripts/smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ try {
)
assert.equal(sheets.item.method, "sheets.spreadsheets.values.update")
assert.equal(sheets.item.mutates, true)
const format = JSON.parse(
run([
"synthetic",
"--dry-run",
"sheets",
"format",
"synthetic-sheet",
"0",
"A1",
"--body",
'{"textFormat":{"bold":true}}',
"--json",
]),
)
assert.equal(format.item.method, "sheets.spreadsheets.batchUpdate")
assert.equal(format.item.mutates, true)
console.log(
`Installed ${JSON.parse(readFileSync(join(root, "package.json"), "utf8")).name}: offline help/version/registry/error checks passed (${temp})`,
)
Expand Down
9 changes: 8 additions & 1 deletion src/api-contract.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CliError } from "@wirecat/cli-base"
import type { ManifestOperation } from "@wirecat/cli-base/codegen"
import { apiJson, checkApiBody, checkApiParameter } from "@wirecat/cli-core/cli"
import { apiJson, apiPlainJson, checkApiBody, checkApiParameter, parseApiJson } from "@wirecat/cli-core/cli"
import { operations } from "./generated/manifest.js"
import { schemas } from "./generated/schemas.js"

Expand Down Expand Up @@ -45,3 +45,10 @@ export const validateRequest = (name: string, params: Record<string, unknown>, b
if (params.alt === "media")
throw new CliError("validation_error", "this API surface supports JSON; use document export for content")
}

export const jsonObject = (value: string): Record<string, unknown> => {
const parsed = apiPlainJson(parseApiJson(value))
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
throw new CliError("validation_error", "use a JSON object")
return parsed as Record<string, unknown>
}
13 changes: 3 additions & 10 deletions src/commands/remote.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { CliError } from "@wirecat/cli-base"
import { annotate } from "@wirecat/cli-base/commands"
import type { QueryRegistry } from "@wirecat/cli-base/query"
import {
apiPlainJson,
type Body,
checkApiBody,
checkApiParameter,
Expand All @@ -12,14 +10,13 @@ import {
LOCAL_READ,
listOf,
NO_EFFECTS,
parseApiJson,
queryTerms,
readApiBody,
termField,
withQuery,
} from "@wirecat/cli-core/cli"
import type { Command } from "commander"
import { operationOf, operations, schemas } from "../api-contract.js"
import { jsonObject, operationOf, operations, schemas } from "../api-contract.js"
import { fileId } from "../config.js"
import { apiMethods, validateApiParams } from "../google-api.js"
import { allOf } from "../output.js"
Expand All @@ -33,12 +30,8 @@ const API_METHOD_QUERY = {
fields: [termField("service", "enum", "service:docs", { values: ["drive", "docs", "driveactivity", "sheets"] })],
} as const satisfies QueryRegistry

export const jsonObject = (value: string): Record<string, unknown> => {
const parsed = apiPlainJson(parseApiJson(value))
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
throw new CliError("validation_error", "use a JSON object")
return parsed as Record<string, unknown>
}
export { jsonObject } from "../api-contract.js"

/** One page of a Google list, which names its rows `key` and the next page `nextPageToken`. */
const pageOf = (
command: string,
Expand Down
Loading
Loading