Skip to main content

onenote_cli/
schema.rs

1use crate::{error::CONTRACTS, model::FIELDS};
2use serde_json::{Value, json};
3
4fn arg(name: &str, kind: &str, required: bool) -> Value {
5    json!({"name":name,"type":kind,"required":required})
6}
7pub fn capabilities() -> Value {
8    json!({"backend":"desktop", "authentication":"existing Windows OneNote desktop session", "backends":["desktop","ssh"], "platforms":["windows","wsl","macos","linux"],
9        "requires":"Windows OneNote desktop and Windows PowerShell 5.1 on the target. Local WSL requires Windows interop; remote access requires OpenSSH and permission to register an interactive task as the signed-in Windows user", "writes":true,
10        "supported":["notebooks list","sections list","pages list","pages read","search","pages create","pages append","tui"],
11        "limitations":["SSH uses key/agent authentication and existing trusted host keys", "Read-only profiles refuse writes; dry-run remains available", "Only notebooks open in the desktop application", "Reads desktop state; does not guarantee fresh cloud sync", "Page Markdown is a text extraction; layout, ink, and binary content are not exported", "Pagination is positional; edits between requests can shift results", "COM retrieves a hierarchy before local pagination; bridge output is capped at 32 MiB", "Indexed search requires Windows Search; unindexed pages are omitted and reported via indexing_pending and unindexed_count", "search --scan reads titles and text directly with case-insensitive literal, whitespace-normalized matching; no ink, OCR, or attachment search", "Scans default to 100 pages, maximum 500, with a 20-second soft budget and 45-second bridge timeout; use next_scan_offset separately from result pagination; incomplete reports unread pages or locked sections", "Writes accept plain text up to 1 MiB; no Markdown or HTML interpretation", "Create and append are not idempotent; inspect OneNote before retrying any uncertain result", "Write verification checks desktop state, not cloud synchronization"]})
12}
13pub fn generate(filter: Option<&str>) -> Value {
14    let mut commands = Vec::new();
15    for (name, description, extra) in [
16        ("notebooks list", "List open notebooks", vec![]),
17        (
18            "sections list",
19            "List sections recursively within a notebook",
20            vec![arg("--notebook", "string", true)],
21        ),
22        (
23            "pages list",
24            "List pages within a section",
25            vec![arg("--section", "string", true)],
26        ),
27        (
28            "search",
29            "Search using OneNote syntax, or literal text with --scan",
30            vec![
31                arg("query", "string", true),
32                arg("--scope", "string", false),
33                arg("--scan", "boolean", false),
34                arg("--scan-limit", "integer", false),
35                arg("--scan-offset", "integer", false),
36            ],
37        ),
38    ] {
39        let mut args = vec![
40            arg("--limit", "integer", false),
41            arg("--offset", "integer", false),
42            arg("--fields", "string", false),
43        ];
44        args.extend(extra);
45        commands.push(json!({"name":name,"description":description,"effects":"read_only","cardinality":"unbounded",
46            "pagination":{"style":"offset","offset_arg":"--offset","limit_arg":"--limit"},"fields_arg":"--fields","args":args,
47            "output_fields":FIELDS.iter().map(|f| json!({"name":f,"type":if *f == "locked" {"boolean"} else {"string"},"nullable":matches!(*f,"parent_id"|"last_modified")})).collect::<Vec<_>>()}));
48    }
49    for (name, description, mut args) in [
50        (
51            "pages create",
52            "Create a page with verified plain-text content",
53            vec![
54                arg("--section", "string", true),
55                arg("--title", "string", true),
56            ],
57        ),
58        (
59            "pages append",
60            "Append a new text block with a modification-time check and read-back verification",
61            vec![arg("id", "string", true)],
62        ),
63    ] {
64        args.extend([
65            arg("--text", "string", false),
66            arg("--file", "string", false),
67            arg("--dry-run", "boolean", false),
68        ]);
69        commands.push(json!({"name":name,"description":description,"effects":"non_idempotent","cardinality":"single","args":args,"stdout_schema":{"type":"object"}}));
70    }
71    for (name, description, args) in [
72        (
73            "pages read",
74            "Read page text as Markdown; optionally include XML",
75            vec![arg("id", "string", true), arg("--xml", "boolean", false)],
76        ),
77        (
78            "doctor",
79            "Diagnose the local platform and desktop bridge",
80            vec![arg("--offline", "boolean", false)],
81        ),
82        (
83            "capabilities",
84            "Describe desktop capabilities and limitations",
85            vec![],
86        ),
87        (
88            "schema",
89            "Emit the offline CLI Spec v0.3 contract",
90            vec![arg("--command", "string", false)],
91        ),
92    ] {
93        commands.push(json!({"name":name,"description":description,"effects":"read_only","cardinality":"single","stdout_schema":{"type":"object"},"args":args}));
94    }
95    commands.push(json!({"name":"completions","description":"Generate shell completions","effects":"read_only","output_kind":"opaque","media_type":"text/plain","args":[arg("shell","string",true)]}));
96    commands.push(json!({"name":"tui","description":"Explore OneNote interactively; create and append actions are guarded by profile permissions and review screens","effects":"non_idempotent","output_kind":"opaque","media_type":"text/x-terminal","args":[]}));
97    for (name, description, effects, args) in [
98        (
99            "init",
100            "Check and save a desktop or SSH profile",
101            "idempotent",
102            vec![
103                arg("--backend", "string", false),
104                arg("--host", "string", false),
105                arg("--identity-file", "string", false),
106                arg("--port", "integer", false),
107                arg("--read-only", "boolean", false),
108                arg("--no-check", "boolean", false),
109                arg("--force", "boolean", false),
110            ],
111        ),
112        (
113            "profile list",
114            "List saved profiles and the active selection",
115            "read_only",
116            vec![],
117        ),
118        (
119            "profile use",
120            "Select the default profile",
121            "idempotent",
122            vec![arg("name", "string", true)],
123        ),
124        (
125            "profile remove",
126            "Remove connection settings",
127            "idempotent",
128            vec![arg("name", "string", true)],
129        ),
130        (
131            "config show",
132            "Show resolved settings without connecting",
133            "read_only",
134            vec![],
135        ),
136        (
137            "config path",
138            "Show the configuration file location",
139            "read_only",
140            vec![],
141        ),
142    ] {
143        commands.push(json!({"name":name,"description":description,"effects":effects,"cardinality":"single","stdout_schema":{"type":"object"},"args":args}));
144    }
145    if let Some(filter) = filter {
146        commands.retain(|c| c["name"] == filter);
147    }
148    json!({"$schema":"https://clispec.dev/schema/v0.3.json","clispec":"0.3","name":"onenote","version":env!("CARGO_PKG_VERSION"),
149        "description":"Read and capture OneNote notes through the Windows desktop application","output":{"tty":"text","piped":"json"},
150        "global_args":[{"name":"--profile","type":"string","description":"Profile override; also ONENOTE_PROFILE"},{"name":"--output","short":"-o","type":"string","enum":["auto","text","json"],"default":"auto"},{"name":"--no-color","type":"boolean"}],
151        "commands":commands,"errors":CONTRACTS.iter().map(|(kind,code,description)|json!({"kind":kind,"exit_code":code,"retryable":false,"description":description})).collect::<Vec<_>>(),
152        "extensions":capabilities()})
153}