@beomsukoh/zotero-cli 0.2.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.
@@ -0,0 +1,52 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
3
+ import { parseIntOption, renderJson, resolveLibrary } from "./helpers.js";
4
+ export class CollectionsCommand {
5
+ name = "collections";
6
+ category = "Core";
7
+ description = "List collections.";
8
+ help() {
9
+ return [
10
+ "Usage: zt collections [<parentKey>] [options]",
11
+ "",
12
+ "List collections, optionally filtered by parent collection.",
13
+ "",
14
+ "Options:",
15
+ " --group <id> Library group ID",
16
+ " --parent <key> Parent collection key",
17
+ " --limit <n> Maximum number of results",
18
+ " --sort <field> Sort field (e.g. title, dateModified)",
19
+ " --direction <asc|desc> Sort direction",
20
+ "",
21
+ "Examples:",
22
+ " zt collections",
23
+ " zt collections --parent ABC12345",
24
+ " zt collections --group 123456 --limit 50"
25
+ ].join("\n");
26
+ }
27
+ parse(argv, context) {
28
+ const parsed = parseArgs(argv);
29
+ assertNoUnknownOptions(parsed, ["group", "parent", "limit", "sort", "direction"]);
30
+ assertNoMissingOptionValues(parsed, ["group", "parent", "limit", "sort", "direction"]);
31
+ if (parsed.positionals.length > 1) {
32
+ throw new ValidationError("collections accepts at most one positional argument (parent key).");
33
+ }
34
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
35
+ const parentKey = parsed.positionals[0] ?? getOption(parsed, "parent");
36
+ const limit = parseIntOption(parsed, "limit");
37
+ const sort = getOption(parsed, "sort");
38
+ const direction = getOption(parsed, "direction");
39
+ return { library, parentKey, limit, sort, direction };
40
+ }
41
+ async execute(input, context) {
42
+ const options = {};
43
+ if (input.limit !== undefined)
44
+ options.limit = input.limit;
45
+ if (input.sort)
46
+ options.sort = input.sort;
47
+ if (input.direction)
48
+ options.direction = input.direction;
49
+ const result = await context.zotero.listCollections(input.library, input.parentKey, Object.keys(options).length > 0 ? options : undefined);
50
+ context.output.write(renderJson(result));
51
+ }
52
+ }
@@ -0,0 +1,47 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
3
+ import { renderJson, resolveLibrary } from "./helpers.js";
4
+ export class CreateCollectionCommand {
5
+ name = "create:collection";
6
+ category = "Create";
7
+ description = "Create a collection.";
8
+ help() {
9
+ return [
10
+ "Usage: zt create:collection <name> [options]",
11
+ "",
12
+ "Create a new collection in the library.",
13
+ "",
14
+ "Options:",
15
+ " --group <id> Library group ID",
16
+ " --parent <key> Parent collection key (for subcollections)",
17
+ "",
18
+ "Examples:",
19
+ ' zt create:collection "My Papers"',
20
+ ' zt create:collection "Sub Collection" --parent ABC12345',
21
+ ' zt create:collection "Shared" --group 123456'
22
+ ].join("\n");
23
+ }
24
+ parse(argv, context) {
25
+ const parsed = parseArgs(argv);
26
+ assertNoUnknownOptions(parsed, ["group", "parent"]);
27
+ assertNoMissingOptionValues(parsed, ["group", "parent"]);
28
+ const name = parsed.positionals[0];
29
+ if (!name) {
30
+ throw new ValidationError("create:collection requires a name. Usage: zt create:collection <name>");
31
+ }
32
+ if (parsed.positionals.length > 1) {
33
+ throw new ValidationError("create:collection accepts at most one positional argument (name). Wrap multi-word names in quotes.");
34
+ }
35
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
36
+ const parentKey = getOption(parsed, "parent");
37
+ return { library, name, parentKey };
38
+ }
39
+ async execute(input, context) {
40
+ const data = { name: input.name };
41
+ if (input.parentKey) {
42
+ data.parentCollection = input.parentKey;
43
+ }
44
+ const result = await context.zotero.createCollection(input.library, data);
45
+ context.output.write(renderJson(result));
46
+ }
47
+ }
@@ -0,0 +1,91 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
3
+ import { renderJson, resolveLibrary } from "./helpers.js";
4
+ export class CreateItemCommand {
5
+ name = "create:item";
6
+ category = "Create";
7
+ description = "Create an item from a JSON string or key=value pairs.";
8
+ help() {
9
+ return [
10
+ "Usage:",
11
+ " zt create:item '<json>'",
12
+ ' zt create:item --type <itemType> --title "Title" [options]',
13
+ "",
14
+ "Create a new Zotero item from JSON or from individual options.",
15
+ "",
16
+ "Options:",
17
+ " --group <id> Library group ID",
18
+ " --type <itemType> Item type (e.g. journalArticle, book)",
19
+ ' --title <title> Item title',
20
+ ' --date <date> Date (e.g. "2026")',
21
+ " --url <url> URL",
22
+ " --doi <doi> DOI",
23
+ " --collection <key> Collection key to add the item to",
24
+ "",
25
+ "Examples:",
26
+ ` zt create:item '{"itemType":"journalArticle","title":"My Paper"}'`,
27
+ ' zt create:item --type journalArticle --title "My Paper" --date "2026"',
28
+ ' zt create:item --type book --title "My Book" --collection ABC12345'
29
+ ].join("\n");
30
+ }
31
+ parse(argv, context) {
32
+ const parsed = parseArgs(argv);
33
+ assertNoUnknownOptions(parsed, ["group", "type", "title", "date", "url", "doi", "collection"]);
34
+ assertNoMissingOptionValues(parsed, ["group", "type", "title", "date", "url", "doi", "collection"]);
35
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
36
+ const collection = getOption(parsed, "collection");
37
+ // If positional[0] looks like JSON, parse it directly
38
+ const positional = parsed.positionals[0];
39
+ if (positional && positional.trimStart().startsWith("{")) {
40
+ try {
41
+ const jsonData = JSON.parse(positional);
42
+ return { library, jsonData, collection };
43
+ }
44
+ catch {
45
+ throw new ValidationError("Invalid JSON provided. Ensure the JSON string is valid.");
46
+ }
47
+ }
48
+ if (parsed.positionals.length > 0 && !positional?.trimStart().startsWith("{")) {
49
+ throw new ValidationError("Unexpected positional argument. Provide JSON or use --type/--title options.");
50
+ }
51
+ // Build from options
52
+ const type = getOption(parsed, "type");
53
+ const title = getOption(parsed, "title");
54
+ const date = getOption(parsed, "date");
55
+ const url = getOption(parsed, "url");
56
+ const doi = getOption(parsed, "doi");
57
+ if (!type) {
58
+ throw new ValidationError("create:item requires --type or a JSON string. Usage: zt create:item --type <itemType> --title \"Title\"");
59
+ }
60
+ return { library, type, title, date, url, doi, collection };
61
+ }
62
+ async execute(input, context) {
63
+ let itemData;
64
+ if (input.jsonData) {
65
+ itemData = input.jsonData;
66
+ }
67
+ else {
68
+ // Get the template and overlay provided fields
69
+ const template = await context.zotero.getItemTemplate(input.type);
70
+ if (input.title !== undefined)
71
+ template["title"] = input.title;
72
+ if (input.date !== undefined)
73
+ template["date"] = input.date;
74
+ if (input.url !== undefined)
75
+ template["url"] = input.url;
76
+ if (input.doi !== undefined)
77
+ template["DOI"] = input.doi;
78
+ itemData = template;
79
+ }
80
+ // Add to collection if specified
81
+ if (input.collection) {
82
+ const data = itemData;
83
+ const existing = Array.isArray(data["collections"]) ? data["collections"] : [];
84
+ data["collections"] = [...existing, input.collection];
85
+ itemData = data;
86
+ }
87
+ // Zotero API expects an array for item creation
88
+ const result = await context.zotero.createItem(input.library, [itemData]);
89
+ context.output.write(renderJson(result));
90
+ }
91
+ }
@@ -0,0 +1,53 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, hasBoolean, parseArgs } from "../utils/args.js";
3
+ import { ensureNoPositionals, renderJson, resolveLibrary } from "./helpers.js";
4
+ export class DeleteCommand {
5
+ name = "delete";
6
+ category = "Core";
7
+ description = "Delete an item or collection.";
8
+ help() {
9
+ return [
10
+ "Usage: zt delete --key <key> [options]",
11
+ "",
12
+ "Delete an item or collection. The current version is fetched",
13
+ "automatically before deletion.",
14
+ "",
15
+ "Options:",
16
+ " --key <key> Item or collection key (required)",
17
+ " --group <id> Library group ID",
18
+ " --collection Delete a collection instead of an item",
19
+ "",
20
+ "Examples:",
21
+ ' zt delete --key ABC12345',
22
+ ' zt delete --key ABC12345 --collection',
23
+ ' zt delete --key ABC12345 --group 123456'
24
+ ].join("\n");
25
+ }
26
+ parse(argv, context) {
27
+ const parsed = parseArgs(argv);
28
+ assertNoUnknownOptions(parsed, ["key", "group", "collection"]);
29
+ assertNoMissingOptionValues(parsed, ["key", "group"]);
30
+ ensureNoPositionals(parsed, "delete");
31
+ const key = getOption(parsed, "key");
32
+ if (!key) {
33
+ throw new ValidationError("delete requires --key.");
34
+ }
35
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
36
+ const isCollection = hasBoolean(parsed, "collection");
37
+ return { library, key, isCollection };
38
+ }
39
+ async execute(input, context) {
40
+ if (input.isCollection) {
41
+ const collection = await context.zotero.getCollection(input.library, input.key);
42
+ const version = collection.version ?? 0;
43
+ const result = await context.zotero.deleteCollection(input.library, input.key, version);
44
+ context.output.write(renderJson(result));
45
+ }
46
+ else {
47
+ const item = await context.zotero.getItem(input.library, input.key);
48
+ const version = item.version ?? 0;
49
+ const result = await context.zotero.deleteItem(input.library, input.key, version);
50
+ context.output.write(renderJson(result));
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,56 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
3
+ import { ensureNoPositionals, parseIntOption, resolveLibrary } from "./helpers.js";
4
+ export class ExportCommand {
5
+ name = "export";
6
+ category = "Core";
7
+ description = "Export items in various formats.";
8
+ help() {
9
+ return [
10
+ "Usage:",
11
+ " zt export --format <format>",
12
+ "",
13
+ "Export items in a given citation/data format.",
14
+ "",
15
+ "Options:",
16
+ " --format <format> Export format (required): bibtex, biblatex, ris, csljson, csv, tei, wikipedia, mods, refer",
17
+ " --group <id> Library group ID",
18
+ " --collection <key> Limit to a specific collection",
19
+ " --limit <n> Maximum number of items",
20
+ " --tag <tag> Filter by tag",
21
+ " --type <itemType> Filter by item type",
22
+ "",
23
+ "Examples:",
24
+ " zt export --format bibtex",
25
+ " zt export --format ris --collection ABC12345",
26
+ " zt export --format csljson --limit 50 --tag physics"
27
+ ].join("\n");
28
+ }
29
+ parse(argv, context) {
30
+ const parsed = parseArgs(argv);
31
+ assertNoUnknownOptions(parsed, ["format", "group", "collection", "limit", "tag", "type"]);
32
+ assertNoMissingOptionValues(parsed, ["format", "group", "collection", "limit", "tag", "type"]);
33
+ ensureNoPositionals(parsed, "export");
34
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
35
+ const format = getOption(parsed, "format");
36
+ if (!format) {
37
+ throw new ValidationError("export requires --format. Usage: zt export --format <format>");
38
+ }
39
+ const limit = parseIntOption(parsed, "limit");
40
+ const tag = getOption(parsed, "tag");
41
+ const itemType = getOption(parsed, "type");
42
+ const collection = getOption(parsed, "collection");
43
+ const searchOptions = {};
44
+ if (limit !== undefined)
45
+ searchOptions.limit = limit;
46
+ if (tag)
47
+ searchOptions.tag = [tag];
48
+ if (itemType)
49
+ searchOptions.itemType = itemType;
50
+ return { library, format, searchOptions: { ...searchOptions, ...(collection ? { query: collection } : {}) } };
51
+ }
52
+ async execute(input, context) {
53
+ const result = await context.zotero.exportItems(input.library, input.format, input.searchOptions);
54
+ context.output.write(result);
55
+ }
56
+ }
@@ -0,0 +1,44 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, parseArgs } from "../utils/args.js";
3
+ import { renderJson, resolveLibrary } from "./helpers.js";
4
+ export class FulltextCommand {
5
+ name = "fulltext";
6
+ category = "Core";
7
+ description = "Get full-text content of an item.";
8
+ help() {
9
+ return [
10
+ "Usage:",
11
+ " zt fulltext <itemKey>",
12
+ " zt fulltext --key <itemKey>",
13
+ "",
14
+ "Retrieve the full-text content for an item.",
15
+ "",
16
+ "Options:",
17
+ " --key <key> Item key",
18
+ " --group <id> Library group ID",
19
+ "",
20
+ "Examples:",
21
+ " zt fulltext ABC12345",
22
+ " zt fulltext --key ABC12345",
23
+ " zt fulltext ABC12345 --group 123456"
24
+ ].join("\n");
25
+ }
26
+ parse(argv, context) {
27
+ const parsed = parseArgs(argv);
28
+ assertNoUnknownOptions(parsed, ["key", "group"]);
29
+ assertNoMissingOptionValues(parsed, ["key", "group"]);
30
+ if (parsed.positionals.length > 1) {
31
+ throw new ValidationError("fulltext accepts at most one positional argument (item key).");
32
+ }
33
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
34
+ const key = parsed.positionals[0] ?? getOption(parsed, "key");
35
+ if (!key) {
36
+ throw new ValidationError("fulltext requires an item key. Usage: zt fulltext <itemKey> or zt fulltext --key <itemKey>");
37
+ }
38
+ return { library, key };
39
+ }
40
+ async execute(input, context) {
41
+ const result = await context.zotero.getFullText(input.library, input.key);
42
+ context.output.write(renderJson(result));
43
+ }
44
+ }
@@ -0,0 +1,27 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { getOption } from "../utils/args.js";
3
+ export function renderJson(value) {
4
+ return JSON.stringify(value, null, 2);
5
+ }
6
+ export function ensureNoPositionals(parsed, label) {
7
+ if (parsed.positionals.length > 0) {
8
+ throw new ValidationError(`${label} does not accept positional arguments.`);
9
+ }
10
+ }
11
+ export function resolveLibrary(parsed, config) {
12
+ const group = getOption(parsed, "group");
13
+ if (group) {
14
+ return { type: "group", id: group };
15
+ }
16
+ const userId = config.userId ?? "0";
17
+ return { type: "user", id: userId };
18
+ }
19
+ export function parseIntOption(parsed, name) {
20
+ const value = getOption(parsed, name);
21
+ if (value === undefined)
22
+ return undefined;
23
+ const num = parseInt(value, 10);
24
+ if (isNaN(num))
25
+ throw new ValidationError(`Option --${name} must be a number.`);
26
+ return num;
27
+ }
@@ -0,0 +1,52 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, hasBoolean, parseArgs } from "../utils/args.js";
3
+ import { renderJson, resolveLibrary } from "./helpers.js";
4
+ export class ItemGetCommand {
5
+ name = "get";
6
+ category = "Core";
7
+ description = "Get an item by key.";
8
+ help() {
9
+ return [
10
+ "Usage:",
11
+ " zt get <key>",
12
+ " zt get --key <key>",
13
+ "",
14
+ "Retrieve a single item by its key.",
15
+ "",
16
+ "Options:",
17
+ " --key <key> Item key",
18
+ " --group <id> Library group ID",
19
+ " --children Show child items (attachments, notes)",
20
+ "",
21
+ "Examples:",
22
+ ' zt get ABC12345',
23
+ ' zt get --key ABC12345 --children',
24
+ ' zt get --key ABC12345 --group 123456'
25
+ ].join("\n");
26
+ }
27
+ parse(argv, context) {
28
+ const parsed = parseArgs(argv);
29
+ assertNoUnknownOptions(parsed, ["key", "group", "children"]);
30
+ assertNoMissingOptionValues(parsed, ["key", "group"]);
31
+ if (parsed.positionals.length > 1) {
32
+ throw new ValidationError("get accepts at most one positional argument (item key).");
33
+ }
34
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
35
+ const key = parsed.positionals[0] ?? getOption(parsed, "key");
36
+ const children = hasBoolean(parsed, "children");
37
+ if (!key) {
38
+ throw new ValidationError("get requires an item key. Usage: zt get <key> or zt get --key <key>");
39
+ }
40
+ return { library, key, children };
41
+ }
42
+ async execute(input, context) {
43
+ if (input.children) {
44
+ const result = await context.zotero.getItemChildren(input.library, input.key);
45
+ context.output.write(renderJson(result));
46
+ }
47
+ else {
48
+ const result = await context.zotero.getItem(input.library, input.key);
49
+ context.output.write(renderJson(result));
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,62 @@
1
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, getOptions, hasBoolean, parseArgs } from "../utils/args.js";
2
+ import { ensureNoPositionals, parseIntOption, renderJson, resolveLibrary } from "./helpers.js";
3
+ export class ItemsCommand {
4
+ name = "items";
5
+ category = "Core";
6
+ description = "List items.";
7
+ help() {
8
+ return [
9
+ "Usage: zt items [options]",
10
+ "",
11
+ "List items, optionally within a collection.",
12
+ "",
13
+ "Options:",
14
+ " --group <id> Library group ID",
15
+ " --collection <key> Collection key to list items from",
16
+ " --limit <n> Maximum number of results",
17
+ " --sort <field> Sort field (e.g. title, dateModified, dateAdded)",
18
+ " --direction <asc|desc> Sort direction",
19
+ " --top Only show top-level items (no attachments/notes)",
20
+ " --tag <tag> Filter by tag (repeatable)",
21
+ " --type <itemType> Filter by item type (e.g. book, journalArticle)",
22
+ "",
23
+ "Examples:",
24
+ " zt items --top --limit 25",
25
+ " zt items --collection ABC12345 --sort dateAdded --direction desc",
26
+ " zt items --tag \"machine learning\" --type journalArticle"
27
+ ].join("\n");
28
+ }
29
+ parse(argv, context) {
30
+ const parsed = parseArgs(argv);
31
+ assertNoUnknownOptions(parsed, ["group", "collection", "limit", "sort", "direction", "top", "tag", "type"]);
32
+ assertNoMissingOptionValues(parsed, ["group", "collection", "limit", "sort", "direction", "tag", "type"]);
33
+ ensureNoPositionals(parsed, "items");
34
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
35
+ const collection = getOption(parsed, "collection");
36
+ const limit = parseIntOption(parsed, "limit");
37
+ const sort = getOption(parsed, "sort");
38
+ const direction = getOption(parsed, "direction");
39
+ const top = hasBoolean(parsed, "top");
40
+ const tags = getOptions(parsed, "tag");
41
+ const itemType = getOption(parsed, "type");
42
+ return { library, collection, limit, sort, direction, top, tags, itemType };
43
+ }
44
+ async execute(input, context) {
45
+ const options = {};
46
+ if (input.limit !== undefined)
47
+ options.limit = input.limit;
48
+ if (input.sort)
49
+ options.sort = input.sort;
50
+ if (input.direction)
51
+ options.direction = input.direction;
52
+ if (input.tags.length > 0)
53
+ options.tag = input.tags;
54
+ if (input.itemType)
55
+ options.itemType = input.itemType;
56
+ const fetch = input.top
57
+ ? context.zotero.listTopItems
58
+ : context.zotero.listItems;
59
+ const result = await fetch.call(context.zotero, input.library, input.collection, Object.keys(options).length > 0 ? options : undefined);
60
+ context.output.write(renderJson(result));
61
+ }
62
+ }
@@ -0,0 +1,27 @@
1
+ import { assertNoUnknownOptions, parseArgs } from "../utils/args.js";
2
+ import { ensureNoPositionals, renderJson } from "./helpers.js";
3
+ export class LibrariesCommand {
4
+ name = "libraries";
5
+ category = "Core";
6
+ description = "List group libraries.";
7
+ help() {
8
+ return [
9
+ "Usage: zt libraries",
10
+ "",
11
+ "List the user's group libraries.",
12
+ "",
13
+ "Examples:",
14
+ " zt libraries"
15
+ ].join("\n");
16
+ }
17
+ parse(argv) {
18
+ const parsed = parseArgs(argv);
19
+ assertNoUnknownOptions(parsed, []);
20
+ ensureNoPositionals(parsed, "libraries");
21
+ return {};
22
+ }
23
+ async execute(_input, context) {
24
+ const result = await context.zotero.listLibraries();
25
+ context.output.write(renderJson(result));
26
+ }
27
+ }
@@ -0,0 +1,67 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, getOption, getOptions, parseArgs } from "../utils/args.js";
3
+ import { parseIntOption, renderJson, resolveLibrary } from "./helpers.js";
4
+ export class SearchCommand {
5
+ name = "search";
6
+ category = "Core";
7
+ description = "Search for items.";
8
+ help() {
9
+ return [
10
+ "Usage: zt search <query> [options]",
11
+ "",
12
+ "Search for items in the library.",
13
+ "",
14
+ "Options:",
15
+ " --group <id> Library group ID",
16
+ " --limit <n> Maximum number of results",
17
+ " --sort <field> Sort field (e.g. title, dateModified)",
18
+ " --direction <asc|desc> Sort direction",
19
+ " --tag <tag> Filter by tag (repeatable)",
20
+ " --type <itemType> Filter by item type (e.g. book, journalArticle)",
21
+ " --qmode <mode> Search mode: titleCreatorYear or everything",
22
+ "",
23
+ "Examples:",
24
+ ' zt search "machine learning"',
25
+ ' zt search "neural networks" --limit 10 --sort dateAdded',
26
+ ' zt search "deep learning" --tag AI --type journalArticle',
27
+ ' zt search "attention" --qmode everything'
28
+ ].join("\n");
29
+ }
30
+ parse(argv, context) {
31
+ const parsed = parseArgs(argv);
32
+ assertNoUnknownOptions(parsed, ["group", "limit", "sort", "direction", "tag", "type", "qmode"]);
33
+ assertNoMissingOptionValues(parsed, ["group", "limit", "sort", "direction", "tag", "type", "qmode"]);
34
+ const query = parsed.positionals[0];
35
+ if (!query) {
36
+ throw new ValidationError("search requires a query. Usage: zt search <query>");
37
+ }
38
+ if (parsed.positionals.length > 1) {
39
+ throw new ValidationError("search accepts at most one positional argument (query). Wrap multi-word queries in quotes.");
40
+ }
41
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
42
+ const limit = parseIntOption(parsed, "limit");
43
+ const sort = getOption(parsed, "sort");
44
+ const direction = getOption(parsed, "direction");
45
+ const tags = getOptions(parsed, "tag");
46
+ const itemType = getOption(parsed, "type");
47
+ const qmode = getOption(parsed, "qmode");
48
+ return { library, query, limit, sort, direction, tags, itemType, qmode };
49
+ }
50
+ async execute(input, context) {
51
+ const options = {};
52
+ if (input.limit !== undefined)
53
+ options.limit = input.limit;
54
+ if (input.sort)
55
+ options.sort = input.sort;
56
+ if (input.direction)
57
+ options.direction = input.direction;
58
+ if (input.tags.length > 0)
59
+ options.tag = input.tags;
60
+ if (input.itemType)
61
+ options.itemType = input.itemType;
62
+ if (input.qmode)
63
+ options.qmode = input.qmode;
64
+ const result = await context.zotero.searchItems(input.library, input.query, Object.keys(options).length > 0 ? options : undefined);
65
+ context.output.write(renderJson(result));
66
+ }
67
+ }
@@ -0,0 +1,39 @@
1
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, parseArgs } from "../utils/args.js";
2
+ import { ensureNoPositionals, parseIntOption, renderJson, resolveLibrary } from "./helpers.js";
3
+ export class TagsCommand {
4
+ name = "tags";
5
+ category = "Core";
6
+ description = "List tags.";
7
+ help() {
8
+ return [
9
+ "Usage: zt tags [options]",
10
+ "",
11
+ "List tags in the library.",
12
+ "",
13
+ "Options:",
14
+ " --group <id> Library group ID",
15
+ " --limit <n> Maximum number of results",
16
+ "",
17
+ "Examples:",
18
+ " zt tags",
19
+ " zt tags --limit 100",
20
+ " zt tags --group 123456"
21
+ ].join("\n");
22
+ }
23
+ parse(argv, context) {
24
+ const parsed = parseArgs(argv);
25
+ assertNoUnknownOptions(parsed, ["group", "limit"]);
26
+ assertNoMissingOptionValues(parsed, ["group", "limit"]);
27
+ ensureNoPositionals(parsed, "tags");
28
+ const library = resolveLibrary(parsed, context?.config ?? { baseUrl: "" });
29
+ const limit = parseIntOption(parsed, "limit");
30
+ return { library, limit };
31
+ }
32
+ async execute(input, context) {
33
+ const options = {};
34
+ if (input.limit !== undefined)
35
+ options.limit = input.limit;
36
+ const result = await context.zotero.listTags(input.library, Object.keys(options).length > 0 ? options : undefined);
37
+ context.output.write(renderJson(result));
38
+ }
39
+ }
@@ -0,0 +1,38 @@
1
+ import { ValidationError } from "../application/errors.js";
2
+ import { assertNoMissingOptionValues, assertNoUnknownOptions, parseArgs } from "../utils/args.js";
3
+ import { renderJson } from "./helpers.js";
4
+ export class TemplateCommand {
5
+ name = "template";
6
+ category = "Lookup";
7
+ description = "Get the JSON template for creating an item of a specific type.";
8
+ help() {
9
+ return [
10
+ "Usage:",
11
+ " zt template <itemType>",
12
+ "",
13
+ "Get the JSON template for creating a new item of the given type.",
14
+ "",
15
+ "Examples:",
16
+ " zt template journalArticle",
17
+ " zt template book",
18
+ " zt template conferencePaper"
19
+ ].join("\n");
20
+ }
21
+ parse(argv) {
22
+ const parsed = parseArgs(argv);
23
+ assertNoUnknownOptions(parsed, []);
24
+ assertNoMissingOptionValues(parsed, []);
25
+ const itemType = parsed.positionals[0];
26
+ if (!itemType) {
27
+ throw new ValidationError("template requires an item type. Usage: zt template <itemType>");
28
+ }
29
+ if (parsed.positionals.length > 1) {
30
+ throw new ValidationError("template accepts at most one positional argument (item type).");
31
+ }
32
+ return { itemType };
33
+ }
34
+ async execute(input, context) {
35
+ const result = await context.zotero.getItemTemplate(input.itemType);
36
+ context.output.write(renderJson(result));
37
+ }
38
+ }