@datarails-ai/dr-cli 0.0.1-locked → 1.6.70

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 (58) hide show
  1. package/README.md +608 -0
  2. package/dist/chunk-DWMCBGQH.js +242 -0
  3. package/dist/cli.js +64468 -0
  4. package/dist/kb-6UWI7Q32.js +21 -0
  5. package/dist/scopes-map.json +4412 -0
  6. package/dist/skills/connector-sandbox/SKILL.md +95 -0
  7. package/dist/skills/datarails-connect-workato/SKILL.md +140 -0
  8. package/dist/skills/datarails-connectors-onboard-connector/SKILL.md +813 -0
  9. package/dist/skills/dr-cli/SKILL.md +659 -0
  10. package/dist/skills/dr-cli/reference/admin.md +796 -0
  11. package/dist/skills/dr-cli/reference/budget-builder.md +151 -0
  12. package/dist/skills/dr-cli/reference/calc-vals.md +73 -0
  13. package/dist/skills/dr-cli/reference/calcmodels.md +76 -0
  14. package/dist/skills/dr-cli/reference/cash.md +61 -0
  15. package/dist/skills/dr-cli/reference/dashboards-widgets.md +85 -0
  16. package/dist/skills/dr-cli/reference/datamappers.md +150 -0
  17. package/dist/skills/dr-cli/reference/desk.md +472 -0
  18. package/dist/skills/dr-cli/reference/filebox-documents.md +373 -0
  19. package/dist/skills/dr-cli/reference/forecast.md +51 -0
  20. package/dist/skills/dr-cli/reference/integrations.md +57 -0
  21. package/dist/skills/dr-cli/reference/lookups.md +168 -0
  22. package/dist/skills/dr-cli/reference/metrics.md +512 -0
  23. package/dist/skills/dr-cli/reference/multi-product.md +180 -0
  24. package/dist/skills/dr-cli/reference/reconciliation-mec.md +76 -0
  25. package/dist/skills/dr-cli/reference/rh-config.md +84 -0
  26. package/dist/skills/dr-cli/reference/safety.md +143 -0
  27. package/dist/skills/dr-cli/reference/spend-control.md +206 -0
  28. package/dist/skills/dr-cli/reference/templates.md +163 -0
  29. package/dist/skills/dr-cli/reference/workflows.md +137 -0
  30. package/dist/skills/dr-error-enrichment/SKILL.md +80 -0
  31. package/dist/skills/dr-error-enrichment/reference/core.md +257 -0
  32. package/dist/skills/dr-error-enrichment/reference/groundcover.md +84 -0
  33. package/dist/skills/dr-error-enrichment/reference/jira-bugs.md +219 -0
  34. package/dist/skills/dr-error-enrichment/reference/safety-backup.md +122 -0
  35. package/dist/skills/dr-error-enrichment-author/SKILL.md +219 -0
  36. package/dist/skills/dr-error-enrichment-improve/SKILL.md +218 -0
  37. package/dist/skills/dr-error-enrichment-maintain/SKILL.md +237 -0
  38. package/dist/skills/dr-error-enrichment-reconcile/SKILL.md +167 -0
  39. package/dist/skills/dr-filebox-api-setup/SKILL.md +170 -0
  40. package/dist/skills/dr-switch-user/SKILL.md +395 -0
  41. package/dist/skills/dr-validate/SKILL.md +392 -0
  42. package/dist/skills/dr-validate/report-template.html +950 -0
  43. package/dist/skills/fx-conversion-matrix/SKILL.md +763 -0
  44. package/dist/skills/fx-conversion-matrix/scripts/_args.mjs +52 -0
  45. package/dist/skills/fx-conversion-matrix/scripts/_xlsx.mjs +241 -0
  46. package/dist/skills/fx-conversion-matrix/scripts/build-matrix.mjs +366 -0
  47. package/dist/skills/fx-conversion-matrix/scripts/gen-mappers.mjs +171 -0
  48. package/dist/skills/fx-conversion-matrix/scripts/rates-to-xlsx.mjs +172 -0
  49. package/dist/skills/fx-onboard/SKILL.md +325 -0
  50. package/dist/skills/mec-table-validation/SKILL.md +383 -0
  51. package/dist/skills/plugins.json +24 -0
  52. package/dist/skills/validate-workato-migration/SKILL.md +167 -0
  53. package/dist/skills/validate-workato-migration/reference/gotchas.md +16 -0
  54. package/dist/skills/validate-workato-migration/reference/staging-orgs.md +26 -0
  55. package/dist/skills/validate-workato-migration/scripts/compare_files.py +255 -0
  56. package/dist/zendeskKb-JPSHVOPH.js +243 -0
  57. package/package.json +56 -9
  58. package/scripts/postinstall.mjs +91 -0
@@ -0,0 +1,255 @@
1
+ """Byte-compare Workato filebox versions against DRIP (dr-connectors) staging versions.
2
+
3
+ Usage:
4
+ python compare_files.py <manifest.json> [--report report.md]
5
+ python compare_files.py --pair <workato.csv> <staging.csv>
6
+
7
+ manifest.json is a list of pairs. Relative paths resolve against the manifest's own folder:
8
+ [{"name": "Shifts / Jarana Aventura / 2026-09-16",
9
+ "workato": "wk/617001_39532879.csv", "workato_version": 39532879,
10
+ "staging": "st/700001_39800001.csv", "staging_version": 39800001}, ...]
11
+
12
+ The pass rule is BYTE-IDENTICAL (sha256 equal). On any mismatch the script explains
13
+ WHY the bytes differ (encoding/BOM, line endings, trailing newline, header, rows,
14
+ cells, quoting-only), so the operator can decide what to change in the connector.
15
+ It never modifies anything. Exit code 0 = every pair identical, 1 = any mismatch/missing or no pairs.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import csv
22
+ import hashlib
23
+ import io
24
+ import json
25
+ import sys
26
+ from collections import Counter
27
+ from pathlib import Path
28
+
29
+ MAX_EXAMPLES = 10
30
+ MAX_VALUE_CHARS = 80
31
+ # Workato/connector CSVs can carry large cells (embedded JSON); the default 128 KiB limit raises
32
+ # csv.Error. 2**27 stays inside a 32-bit C long, so it is safe on Windows too.
33
+ csv.field_size_limit(2**27)
34
+
35
+
36
+ def sha256(data: bytes) -> str:
37
+ return hashlib.sha256(data).hexdigest()
38
+
39
+
40
+ def line_ending(data: bytes) -> str:
41
+ crlf = data.count(b"\r\n")
42
+ lf = data.count(b"\n") - crlf
43
+ if crlf and lf:
44
+ return f"mixed (CRLF={crlf}, LF={lf})"
45
+ return "CRLF" if crlf else ("LF" if lf else "none")
46
+
47
+
48
+ def decode(data: bytes) -> tuple[str, str]:
49
+ if data.startswith(b"\xef\xbb\xbf"):
50
+ return data[3:].decode("utf-8", errors="replace"), "utf-8-sig (BOM)"
51
+ try:
52
+ return data.decode("utf-8"), "utf-8"
53
+ except UnicodeDecodeError:
54
+ return data.decode("latin-1"), "NOT utf-8 (decoded as latin-1)"
55
+
56
+
57
+ def parse(text: str) -> tuple[list[list[str]] | None, str | None]:
58
+ """Parsed rows, or (None, reason) when the text isn't parseable CSV."""
59
+ try:
60
+ return list(csv.reader(io.StringIO(text, newline=""), strict=True)), None
61
+ except csv.Error as exc:
62
+ return None, str(exc)
63
+
64
+
65
+ def short(value: object) -> str:
66
+ """repr() of an example value, truncated so one huge cell can't flood the report."""
67
+ text = repr(value)
68
+ if len(text) <= MAX_VALUE_CHARS:
69
+ return text
70
+ return f"{text[:MAX_VALUE_CHARS]}... ({len(str(value))} chars)"
71
+
72
+
73
+ def multiset_diff(a: list, b: list) -> tuple[list, list]:
74
+ """Items only in a / only in b, respecting multiplicity."""
75
+ ca, cb = Counter(a), Counter(b)
76
+ return list((ca - cb).elements()), list((cb - ca).elements())
77
+
78
+
79
+ def diagnose(wk: bytes, st: bytes) -> list[str]:
80
+ notes: list[str] = []
81
+ wk_text, wk_enc = decode(wk)
82
+ st_text, st_enc = decode(st)
83
+ if wk_enc != st_enc:
84
+ notes.append(f"encoding: workato={wk_enc}, staging={st_enc}")
85
+ if line_ending(wk) != line_ending(st):
86
+ notes.append(
87
+ f"line endings: workato={line_ending(wk)}, staging={line_ending(st)}"
88
+ )
89
+ wk_nl, st_nl = wk.endswith(b"\n"), st.endswith(b"\n")
90
+ if wk_nl != st_nl:
91
+ notes.append(f"trailing newline: workato={wk_nl}, staging={st_nl}")
92
+
93
+ wk_rows, wk_err = parse(wk_text)
94
+ st_rows, st_err = parse(st_text)
95
+ for side, err in (("workato", wk_err), ("staging", st_err)):
96
+ if err:
97
+ notes.append(
98
+ f"CSV unparseable ({side}): {err}; no header/row/cell diagnosis"
99
+ )
100
+ if wk_rows is None or st_rows is None:
101
+ return notes
102
+
103
+ wk_head, st_head = (wk_rows[0] if wk_rows else []), (st_rows[0] if st_rows else [])
104
+ if wk_head != st_head:
105
+ missing, extra = multiset_diff(wk_head, st_head)
106
+ if missing or extra:
107
+ notes.append(
108
+ f"header differs: missing in staging={missing}, extra in staging={extra}"
109
+ )
110
+ else:
111
+ notes.append(
112
+ f"header column ORDER differs: workato={wk_head} staging={st_head}"
113
+ )
114
+
115
+ wk_body, st_body = wk_rows[1:], st_rows[1:]
116
+ if len(wk_body) != len(st_body):
117
+ # A positional diff would cascade after the first inserted/deleted row, so report the
118
+ # multiset difference instead.
119
+ notes.append(f"row count: workato={len(wk_body)}, staging={len(st_body)}")
120
+ only_wk, only_st = multiset_diff(
121
+ list(map(tuple, wk_body)), list(map(tuple, st_body))
122
+ )
123
+ notes.append(
124
+ f"rows only in workato: {len(only_wk)}, rows only in staging: {len(only_st)}"
125
+ )
126
+ for label, rows in (("only in workato", only_wk), ("only in staging", only_st)):
127
+ for row in rows[:MAX_EXAMPLES]:
128
+ notes.append(f" {label}: {short(list(row))}")
129
+ elif wk_body != st_body and Counter(map(tuple, wk_body)) == Counter(
130
+ map(tuple, st_body)
131
+ ):
132
+ notes.append("same rows, different ORDER")
133
+ else:
134
+ cell_diffs = []
135
+ for i, (a, b) in enumerate(zip(wk_body, st_body), start=2):
136
+ for j in range(max(len(a), len(b))):
137
+ va = a[j] if j < len(a) else "<missing>"
138
+ vb = b[j] if j < len(b) else "<missing>"
139
+ if va != vb:
140
+ col = wk_head[j] if j < len(wk_head) else f"#{j}"
141
+ cell_diffs.append((i, col, va, vb))
142
+ if cell_diffs:
143
+ by_col = Counter(col for _, col, _, _ in cell_diffs)
144
+ notes.append(
145
+ f"cell diffs: {len(cell_diffs)} total, by column {dict(by_col)}"
146
+ )
147
+ for i, col, va, vb in cell_diffs[:MAX_EXAMPLES]:
148
+ notes.append(
149
+ f" row {i} [{col}]: workato={short(va)} staging={short(vb)}"
150
+ )
151
+
152
+ if wk_rows == st_rows and not notes:
153
+ notes.append(
154
+ "parsed CSV values are IDENTICAL - difference is quoting/escaping only "
155
+ '(e.g. Workato writes empty cells as ""). Match Workato\'s quoting in the connector.'
156
+ )
157
+ elif wk_rows == st_rows:
158
+ notes.append(
159
+ "parsed CSV values are identical apart from the byte-level notes above"
160
+ )
161
+ if not notes:
162
+ notes.append(
163
+ "bytes differ but no specific cause found - inspect with a hex diff"
164
+ )
165
+ return notes
166
+
167
+
168
+ def compare(entry: dict, wk_path: Path, st_path: Path) -> dict:
169
+ res = {
170
+ "name": entry.get("name", wk_path.name),
171
+ "workato": str(wk_path),
172
+ "staging": str(st_path),
173
+ "workato_version": entry.get("workato_version", "-"),
174
+ "staging_version": entry.get("staging_version", "-"),
175
+ }
176
+ for side, p in (("workato", wk_path), ("staging", st_path)):
177
+ if not p.is_file():
178
+ res.update(status="MISSING", notes=[f"{side} file not found: {p}"])
179
+ return res
180
+ wk, st = wk_path.read_bytes(), st_path.read_bytes()
181
+ res.update(
182
+ workato_sha256=sha256(wk),
183
+ staging_sha256=sha256(st),
184
+ workato_bytes=len(wk),
185
+ staging_bytes=len(st),
186
+ )
187
+ if wk == st:
188
+ res.update(status="IDENTICAL", notes=[])
189
+ else:
190
+ res.update(status="DIFFERENT", notes=diagnose(wk, st))
191
+ return res
192
+
193
+
194
+ def cell(value: object) -> str:
195
+ return str(value).replace("|", r"\|")
196
+
197
+
198
+ def render_report(results: list[dict]) -> str:
199
+ ok = sum(r["status"] == "IDENTICAL" for r in results)
200
+ out = [f"# Workato vs DRIP comparison: {ok}/{len(results)} identical", ""]
201
+ out.append(
202
+ "| Pair | Status | Workato version | Staging version | Workato bytes | Staging bytes "
203
+ "| sha256 (workato / staging) |"
204
+ )
205
+ out.append("|---|---|---|---|---|---|---|")
206
+ for r in results:
207
+ out.append(
208
+ f"| {cell(r['name'])} | {r['status']} | {r['workato_version']} | {r['staging_version']} "
209
+ f"| {r.get('workato_bytes', '-')} | {r.get('staging_bytes', '-')} "
210
+ f"| {r.get('workato_sha256', '-')[:12]} / {r.get('staging_sha256', '-')[:12]} |"
211
+ )
212
+ for r in results:
213
+ if r["notes"]:
214
+ out += ["", f"## {r['name']} - {r['status']}", ""]
215
+ out += [
216
+ f"- {n}" if not n.startswith(" ") else f" - {n.strip()}"
217
+ for n in r["notes"]
218
+ ]
219
+ return "\n".join(out) + "\n"
220
+
221
+
222
+ def main() -> int:
223
+ ap = argparse.ArgumentParser()
224
+ ap.add_argument("manifest", nargs="?")
225
+ ap.add_argument("--pair", nargs=2, metavar=("WORKATO", "STAGING"))
226
+ ap.add_argument("--report")
227
+ args = ap.parse_args()
228
+ if args.pair:
229
+ base = Path.cwd()
230
+ pairs = [
231
+ {
232
+ "name": Path(args.pair[0]).name,
233
+ "workato": args.pair[0],
234
+ "staging": args.pair[1],
235
+ }
236
+ ]
237
+ elif args.manifest:
238
+ base = Path(args.manifest).resolve().parent
239
+ pairs = json.loads(Path(args.manifest).read_text(encoding="utf-8"))
240
+ else:
241
+ ap.error("pass a manifest or --pair")
242
+
243
+ def resolve(p: str) -> Path:
244
+ return Path(p) if Path(p).is_absolute() else base / p
245
+
246
+ results = [compare(p, resolve(p["workato"]), resolve(p["staging"])) for p in pairs]
247
+ report = render_report(results)
248
+ if args.report:
249
+ Path(args.report).write_text(report, encoding="utf-8")
250
+ sys.stdout.write(report)
251
+ return 0 if results and all(r["status"] == "IDENTICAL" for r in results) else 1
252
+
253
+
254
+ if __name__ == "__main__":
255
+ sys.exit(main())
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ extractImageUrls,
4
+ getArticle,
5
+ getArticleAttachments,
6
+ listArticlesInSection,
7
+ listCategories,
8
+ listRecentArticles,
9
+ listSections,
10
+ search
11
+ } from "./chunk-DWMCBGQH.js";
12
+
13
+ // src/mcp/servers/zendeskKb.ts
14
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
16
+ import { z } from "zod";
17
+
18
+ // src/mcp/servers/zendeskKbDescriptions.ts
19
+ var SEARCH_KB = `Search the Datarails customer-facing Help Center. Call this BEFORE running \`dr\` commands when the user's request uses product vocabulary the dr-cli surface doesn't mirror directly \u2014 e.g. "why isn't reconciliation pairing", "how does FX translation work in MEC", "what does a 'calc model self-loop' mean". Use the returned article as ground truth, then map to dr verbs. Trigger phrases (non-exhaustive): "why isn't X working", "how does X work in Datarails", "what does X mean", "the customer is asking", "explain X to me". Don't call when: the request is already in dr-cli verbs ("list dashboards in org X"), or you've already searched this turn.`;
20
+ var GET_ARTICLE = 'Fetch one Help Center article by ID. Call when you already have an article ID from a URL the user pasted ("https://support.datarails.com/hc/en-us/articles/12345"), from a prior search_kb call, or from a different tool that referenced an article. Faster than re-searching when you know the ID. Returns the same shape as search_kb items.';
21
+ var FETCH_ARTICLE_MEDIA = "Fetch inline images from an article and return them as image content blocks so you can SEE the screenshots. Call after search_kb / get_article when the article's visual content matters \u2014 comparing what the user describes against what the product UI actually looks like, reading text inside screenshots, identifying which screen the customer is on. Don't call routinely \u2014 most KB questions are answerable from the markdown body alone.";
22
+ var LIST_CATEGORIES = "List the top-level Help Center categories. Useful when the user's question is broad and you want to identify which area of the product the answer lives in before drilling down via list_sections / list_articles_in_section.";
23
+ var LIST_SECTIONS = "List sections within a category (or all sections). Use after list_categories to drill down, or directly when you already know the category_id from a prior call.";
24
+ var LIST_ARTICLES_IN_SECTION = "Enumerate articles in a specific section. Returns metadata only by default \u2014 pass include_bodies=true if you need full markdown content (more tokens). Use when you want to skim everything in one functional area of the product.";
25
+ var LIST_RECENT_ARTICLES = 'Articles updated within the last N days. Call when troubleshooting a feature the customer says is "new" or "just changed" \u2014 the recent updates list often shows the article documenting the change. Metadata only; follow up with get_article for content.';
26
+ var GET_ARTICLE_ATTACHMENTS = 'List ALL attachments on an article (both inline images and non-inline files like PDFs and ZIPs). Use when the user mentions a downloadable artifact ("the template they reference", "the example file"), or when the article body mentions an attachment without inlining it. Returns metadata + URLs only; does not download non-image binaries.';
27
+
28
+ // src/mcp/servers/zendeskKb.ts
29
+ function jsonText(value) {
30
+ return {
31
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
32
+ };
33
+ }
34
+ function plainText(text) {
35
+ return { content: [{ type: "text", text }] };
36
+ }
37
+ async function searchKbTool(args) {
38
+ return jsonText(await search(args));
39
+ }
40
+ async function getArticleTool(args) {
41
+ const article = await getArticle(args);
42
+ if (article === null) {
43
+ return plainText(`Article ${args.article_id} not found in locale '${args.locale ?? "en-us"}'.`);
44
+ }
45
+ return jsonText(article);
46
+ }
47
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
48
+ var CONTENT_TYPE_TO_MIME = {
49
+ "image/png": "image/png",
50
+ "image/jpeg": "image/jpeg",
51
+ "image/jpg": "image/jpeg",
52
+ "image/gif": "image/gif",
53
+ "image/webp": "image/webp"
54
+ };
55
+ var SUBDOMAIN = process.env.ZENDESK_SUBDOMAIN ?? "datarails";
56
+ var KB_BASE_URL = `https://${SUBDOMAIN}.zendesk.com/api/v2/help_center`;
57
+ function clamp(n, lo, hi) {
58
+ return Math.max(lo, Math.min(n, hi));
59
+ }
60
+ async function fetchArticleMediaTool(args) {
61
+ const locale = args.locale ?? "en-us";
62
+ const limit = clamp(args.limit ?? 5, 1, 10);
63
+ const r = await fetch(`${KB_BASE_URL}/${locale}/articles/${args.article_id}.json`);
64
+ if (r.status === 404) {
65
+ return plainText(`Article ${args.article_id} not found in locale '${locale}'.`);
66
+ }
67
+ if (!r.ok) {
68
+ throw new Error(`HTTP ${r.status} fetching article ${args.article_id}`);
69
+ }
70
+ const data = await r.json();
71
+ const raw = data.article;
72
+ if (raw === void 0) {
73
+ return plainText(`Article ${args.article_id} not found.`);
74
+ }
75
+ const urls = extractImageUrls(raw.body).slice(0, limit);
76
+ if (urls.length === 0) {
77
+ return plainText(`Article ${args.article_id} ('${raw.title ?? ""}') has no inline images.`);
78
+ }
79
+ const content = [
80
+ {
81
+ type: "text",
82
+ text: `Article ${args.article_id}: '${raw.title ?? ""}'
83
+ Images (${urls.length}):`
84
+ }
85
+ ];
86
+ for (const url of urls) {
87
+ try {
88
+ const ir = await fetch(url, { redirect: "follow" });
89
+ if (!ir.ok) {
90
+ content.push({ type: "text", text: `- ${url} (skipped: HTTP ${ir.status})` });
91
+ continue;
92
+ }
93
+ const ct = (ir.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase() ?? "";
94
+ const mime = CONTENT_TYPE_TO_MIME[ct];
95
+ if (mime === void 0) {
96
+ content.push({
97
+ type: "text",
98
+ text: `- ${url} (skipped: unsupported type '${ct}')`
99
+ });
100
+ continue;
101
+ }
102
+ const buf = Buffer.from(await ir.arrayBuffer());
103
+ if (buf.length > MAX_IMAGE_BYTES) {
104
+ content.push({
105
+ type: "text",
106
+ text: `- ${url} (skipped: ${buf.length} bytes > 5 MB cap)`
107
+ });
108
+ continue;
109
+ }
110
+ content.push({ type: "text", text: `- ${url}` });
111
+ content.push({ type: "image", data: buf.toString("base64"), mimeType: mime });
112
+ } catch (err) {
113
+ const msg = err instanceof Error ? err.message : String(err);
114
+ content.push({ type: "text", text: `- ${url} (skipped: ${msg})` });
115
+ }
116
+ }
117
+ return { content };
118
+ }
119
+ async function listCategoriesTool(args) {
120
+ return jsonText(await listCategories(args));
121
+ }
122
+ async function listSectionsTool(args) {
123
+ return jsonText(await listSections(args));
124
+ }
125
+ async function listArticlesInSectionTool(args) {
126
+ return jsonText(await listArticlesInSection(args));
127
+ }
128
+ async function listRecentArticlesTool(args) {
129
+ return jsonText(await listRecentArticles(args));
130
+ }
131
+ async function getArticleAttachmentsTool(args) {
132
+ return jsonText(await getArticleAttachments(args));
133
+ }
134
+ function createServer() {
135
+ const server = new McpServer({ name: "dr-zendesk-kb", version: "0.1.0" });
136
+ server.registerTool(
137
+ "search_kb",
138
+ {
139
+ description: SEARCH_KB,
140
+ inputSchema: {
141
+ query: z.string(),
142
+ locale: z.string().default("en-us"),
143
+ limit: z.number().int().default(5)
144
+ }
145
+ },
146
+ async (args) => searchKbTool(args)
147
+ );
148
+ server.registerTool(
149
+ "get_article",
150
+ {
151
+ description: GET_ARTICLE,
152
+ inputSchema: {
153
+ article_id: z.number().int(),
154
+ locale: z.string().default("en-us")
155
+ }
156
+ },
157
+ async (args) => getArticleTool(args)
158
+ );
159
+ server.registerTool(
160
+ "fetch_article_media",
161
+ {
162
+ description: FETCH_ARTICLE_MEDIA,
163
+ inputSchema: {
164
+ article_id: z.number().int(),
165
+ locale: z.string().default("en-us"),
166
+ limit: z.number().int().default(5)
167
+ }
168
+ },
169
+ async (args) => fetchArticleMediaTool(args)
170
+ );
171
+ server.registerTool(
172
+ "list_categories",
173
+ {
174
+ description: LIST_CATEGORIES,
175
+ inputSchema: { locale: z.string().default("en-us") }
176
+ },
177
+ async (args) => listCategoriesTool(args)
178
+ );
179
+ server.registerTool(
180
+ "list_sections",
181
+ {
182
+ description: LIST_SECTIONS,
183
+ inputSchema: {
184
+ category_id: z.number().int().optional(),
185
+ locale: z.string().default("en-us"),
186
+ limit: z.number().int().default(50)
187
+ }
188
+ },
189
+ async (args) => listSectionsTool(args)
190
+ );
191
+ server.registerTool(
192
+ "list_articles_in_section",
193
+ {
194
+ description: LIST_ARTICLES_IN_SECTION,
195
+ inputSchema: {
196
+ section_id: z.number().int(),
197
+ locale: z.string().default("en-us"),
198
+ limit: z.number().int().default(25),
199
+ include_bodies: z.boolean().default(false)
200
+ }
201
+ },
202
+ async (args) => listArticlesInSectionTool(args)
203
+ );
204
+ server.registerTool(
205
+ "list_recent_articles",
206
+ {
207
+ description: LIST_RECENT_ARTICLES,
208
+ inputSchema: {
209
+ days: z.number().int().default(30),
210
+ locale: z.string().default("en-us"),
211
+ limit: z.number().int().default(25)
212
+ }
213
+ },
214
+ async (args) => listRecentArticlesTool(args)
215
+ );
216
+ server.registerTool(
217
+ "get_article_attachments",
218
+ {
219
+ description: GET_ARTICLE_ATTACHMENTS,
220
+ inputSchema: {
221
+ article_id: z.number().int(),
222
+ locale: z.string().default("en-us")
223
+ }
224
+ },
225
+ async (args) => getArticleAttachmentsTool(args)
226
+ );
227
+ return server;
228
+ }
229
+ async function run() {
230
+ await createServer().connect(new StdioServerTransport());
231
+ }
232
+ export {
233
+ createServer,
234
+ fetchArticleMediaTool,
235
+ getArticleAttachmentsTool,
236
+ getArticleTool,
237
+ listArticlesInSectionTool,
238
+ listCategoriesTool,
239
+ listRecentArticlesTool,
240
+ listSectionsTool,
241
+ run,
242
+ searchKbTool
243
+ };
package/package.json CHANGED
@@ -1,11 +1,58 @@
1
1
  {
2
2
  "name": "@datarails-ai/dr-cli",
3
- "version": "0.0.1-locked",
4
- "description": "Reserved namespace. Install from the Datarails JFrog registry, not from npmjs.org.",
5
- "private": false,
6
- "repository": {
7
- "type": "git",
8
- "url": "https://github.com/Datarails/dr-cli"
9
- },
10
- "license": "UNLICENSED"
11
- }
3
+ "version": "1.6.70",
4
+ "description": "Datarails CLI",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=18"
8
+ },
9
+ "bin": {
10
+ "dr": "./dist/cli.js"
11
+ },
12
+ "files": [
13
+ "dist/",
14
+ "scripts/postinstall.mjs"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "dependencies": {
20
+ "@datarails-ai/dr-appspace-cli": "0.9.2",
21
+ "@modelcontextprotocol/sdk": "^1.29.0",
22
+ "@napi-rs/keyring": "^1.3.0",
23
+ "commander": "^12.1.0",
24
+ "jszip": "^3.10.1",
25
+ "node-html-parser": "^6.1.13",
26
+ "open": "^10.1.0",
27
+ "turndown": "^7.2.4",
28
+ "zod": "^3.25.76"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.14.0",
32
+ "@types/turndown": "^5.0.6",
33
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
34
+ "@typescript-eslint/parser": "^8.0.0",
35
+ "@vitest/coverage-v8": "^2.1.9",
36
+ "eslint": "^9.7.0",
37
+ "jsdom": "^29.1.1",
38
+ "msw": "^2.3.0",
39
+ "prettier": "^3.3.0",
40
+ "tsup": "^8.1.0",
41
+ "tsx": "^4.16.0",
42
+ "typescript": "^5.5.0",
43
+ "vitest": "^2.0.0"
44
+ },
45
+ "scripts": {
46
+ "dev": "tsx src/cli.ts",
47
+ "build": "tsup",
48
+ "gen:scopes": "tsx scripts/gen-scopes-map.mts",
49
+ "check:scopes": "tsx scripts/gen-scopes-map.mts --check",
50
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "lint": "eslint",
54
+ "format": "prettier --write .",
55
+ "format:check": "prettier --check .",
56
+ "postinstall": "node scripts/postinstall.mjs"
57
+ }
58
+ }
@@ -0,0 +1,91 @@
1
+ // scripts/postinstall.mjs
2
+ //
3
+ // Runs automatically after `npm install -g @datarails-ai/dr-cli` (or any npm install of
4
+ // this package). Copies every embedded skill into:
5
+ //
6
+ // ~/.claude/skills/<name>/SKILL.md (Claude Code path — unless the dr-cli
7
+ // plugin manages this machine, below)
8
+ // ~/.gemini/skills/<name>/SKILL.md (only when ~/.gemini/ already
9
+ // exists — Gemini CLI path)
10
+ //
11
+ // plus an idempotent managed block in ~/.gemini/GEMINI.md when the Gemini
12
+ // side ran. So every AI-assistant session on this machine picks up the
13
+ // dr-cli skills without manual setup.
14
+ //
15
+ // On a machine where Claude Code or the Claude desktop app has the dr-cli plugin
16
+ // installed, the CLI skips the
17
+ // `~/.claude/skills/` write, prints a notice, and removes the copies an earlier install
18
+ // wrote for every skill an installed dr-cli plugin now serves — only the files each
19
+ // copy's `.dr-cli-manifest.json` lists (see `pluginManagedProbe` and
20
+ // `removeUserSpaceCopy` in src/commands/skill.ts). Two copies make skill dispatch
21
+ // ambiguous. A satellite's copy stays while that satellite is absent. The Gemini
22
+ // mirror is unaffected. `dr skill install ~ --force` writes them anyway.
23
+ //
24
+ // `dr skill install` always overwrites differing on-disk content silently
25
+ // — a refresh on every package upgrade is the contract. Bytes-equal writes
26
+ // short-circuit as a perf optimisation.
27
+ //
28
+ // Failure modes that are intentionally non-fatal:
29
+ // - dist/cli.js missing (dev install before `pnpm build`) — skip silently
30
+ // - skill copy returns non-zero — log a warning and let the install
31
+ // succeed. The user can rerun `dr skill install ~` later.
32
+ //
33
+ // We intentionally do NOT use `process.exit(non-zero)` from this hook,
34
+ // because npm aborts the entire install on a non-zero postinstall exit.
35
+ // Failing to copy a skill shouldn't break the operator's install.
36
+ //
37
+ // Cleaning up removed skills: when a future release drops a skill that
38
+ // previous releases shipped, add an explicit `rm` call here for the
39
+ // orphaned `<home>/.claude/skills/<name>/` directory, kept for ~1–2
40
+ // release cycles before being pruned. Example pattern:
41
+ //
42
+ // import { rm } from 'node:fs/promises';
43
+ // import { join } from 'node:path';
44
+ // await rm(join(target, '.claude', 'skills', '<old-skill-name>'),
45
+ // { recursive: true, force: true });
46
+ //
47
+ // Files dropped *within* a surviving skill need no such call: each install records
48
+ // a `.dr-cli-manifest.json` per skill and prunes what the previous one listed.
49
+
50
+ import { spawnSync } from 'node:child_process';
51
+ import { existsSync } from 'node:fs';
52
+ import { homedir } from 'node:os';
53
+ import { dirname, resolve } from 'node:path';
54
+ import { fileURLToPath } from 'node:url';
55
+
56
+ const here = dirname(fileURLToPath(import.meta.url));
57
+ const cli = resolve(here, '..', 'dist', 'cli.js');
58
+
59
+ if (!existsSync(cli)) {
60
+ // Dev install before `pnpm build` — nothing to copy from yet.
61
+ process.exit(0);
62
+ }
63
+
64
+ const target = homedir();
65
+ const result = spawnSync('node', [cli, 'skill', 'install', target], { stdio: 'inherit' });
66
+
67
+ if (result.status !== 0) {
68
+ process.stderr.write(
69
+ `(dr-cli postinstall: skill copy returned ${result.status} — run \`dr skill install ~\` to retry)\n`,
70
+ );
71
+ }
72
+
73
+ // Auto-register every bundled MCP server with Claude Code. Same
74
+ // non-fatal contract as the skill install above: a non-zero exit just
75
+ // prints a warning and lets npm install succeed. The contract is
76
+ // "always overwrite" — a user who manually unregistered an MCP will
77
+ // see it reappear on the next `npm install -g @datarails-ai/dr-cli`, matching the
78
+ // `dr skill install` policy. If a future release drops an MCP, add an
79
+ // explicit `dr mcp unregister <old-name>` line here, kept for ~1–2
80
+ // release cycles before pruning.
81
+ const mcps = ['dr-zendesk-kb'];
82
+ for (const name of mcps) {
83
+ const mcpResult = spawnSync('node', [cli, 'mcp', 'register', name], { stdio: 'inherit' });
84
+ if (mcpResult.status !== 0) {
85
+ process.stderr.write(
86
+ `(dr-cli postinstall: MCP register ${name} returned ${mcpResult.status} — run \`dr mcp register ${name}\` to retry)\n`,
87
+ );
88
+ }
89
+ }
90
+
91
+ process.exit(0);