@arbidocs/cli 0.3.65 → 0.3.67

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.3.67
4
+
5
+ [compare changes](https://github.com/arbicity/ARBI-frontend/compare/v0.3.66...HEAD)
6
+
7
+ ### 🚀 Enhancements
8
+
9
+ - **tui:** SOTA amendments — citations, events, tool blocks, view/sources/history ([#774](https://github.com/arbicity/ARBI-frontend/pull/774))
10
+ - **sdk,cli,tui:** Consume GET /v1/assistant/skills for slash autocomplete ([#776](https://github.com/arbicity/ARBI-frontend/pull/776))
11
+ - **tui,cli:** Unified slash dispatcher + autocomplete merges static commands and skills ([#777](https://github.com/arbicity/ARBI-frontend/pull/777))
12
+ - **sdk,tui,cli,react:** Skill iteration loop — edit/install/pre-flight + React slash menu ([#778](https://github.com/arbicity/ARBI-frontend/pull/778))
13
+
14
+ ## v0.3.66
15
+
16
+ [compare changes](https://github.com/arbicity/ARBI-frontend/compare/v0.3.65...HEAD)
17
+
18
+ ### 🚀 Enhancements
19
+
20
+ - **artifact:** Always-editable .md tab + mount-time normalisation absorption ([#754](https://github.com/arbicity/ARBI-frontend/pull/754))
21
+ - **dm:** WhatsApp-style chat bubbles, conversation header, date separators ([#762](https://github.com/arbicity/ARBI-frontend/pull/762))
22
+
23
+ ### 🩹 Fixes
24
+
25
+ - **pdf:** Polyfill the bleeding-edge TC39 APIs pdfjs 5.x relies on ([#755](https://github.com/arbicity/ARBI-frontend/pull/755))
26
+ - **grid:** Filter task_update transactions to rows the grid actually has ([#756](https://github.com/arbicity/ARBI-frontend/pull/756))
27
+ - **auth:** Gate protected queries on token+user, not token alone ([#757](https://github.com/arbicity/ARBI-frontend/pull/757))
28
+ - **mcp:** Read 'sk' JWT claim, not 'session_key', in openAllWorkspacesForSession ([#758](https://github.com/arbicity/ARBI-frontend/pull/758))
29
+ - **docviewer:** Treat *.md uploads as markdown docs; restore Edit/Cancel toggle ([#760](https://github.com/arbicity/ARBI-frontend/pull/760))
30
+ - **ui:** Restore logout modal with active sessions list ([#768](https://github.com/arbicity/ARBI-frontend/pull/768))
31
+ - **sdk,cli:** Keep PA-agent CLI on the same session across workspace touches ([#769](https://github.com/arbicity/ARBI-frontend/pull/769))
32
+ - **sdk:** Restore userExtId and always call /open in session fast-path ([#771](https://github.com/arbicity/ARBI-frontend/pull/771))
33
+
34
+ ### 💅 Refactors
35
+
36
+ - Remove obsolete isDelegatedSession state ([#765](https://github.com/arbicity/ARBI-frontend/pull/765), [#766](https://github.com/arbicity/ARBI-frontend/pull/766))
37
+
38
+ ### 🏡 Chore
39
+
40
+ - **lint:** Zero warnings — drop unused userSettings hook in Dashboard, extract UserSettings experimental section ([#753](https://github.com/arbicity/ARBI-frontend/pull/753))
41
+ - **ci:** Run SDK unit tests on every PR ([#770](https://github.com/arbicity/ARBI-frontend/pull/770))
42
+
3
43
  ## v0.3.65
4
44
 
5
45
  [compare changes](https://github.com/arbicity/ARBI-frontend/compare/v0.3.64...HEAD)
package/dist/index.js CHANGED
@@ -113,9 +113,9 @@ function getCachedWorkspaceName(id) {
113
113
  return null;
114
114
  }
115
115
  }
116
- function updateCompletionCache(workspaces3) {
116
+ function updateCompletionCache(workspaces4) {
117
117
  const cache = {
118
- workspaces: workspaces3.filter((w) => w.external_id).map((w) => ({ id: w.external_id, name: w.name ?? "" })),
118
+ workspaces: workspaces4.filter((w) => w.external_id).map((w) => ({ id: w.external_id, name: w.name ?? "" })),
119
119
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
120
120
  };
121
121
  const filePath = getCacheFile();
@@ -3694,7 +3694,7 @@ function getLatestVersion(skipCache = false) {
3694
3694
  }
3695
3695
  }
3696
3696
  function getCurrentVersion() {
3697
- return "0.3.65";
3697
+ return "0.3.67";
3698
3698
  }
3699
3699
  function readChangelog(fromVersion, toVersion) {
3700
3700
  try {
@@ -3747,17 +3747,17 @@ function showChangelog(fromVersion, toVersion) {
3747
3747
  async function checkForUpdates(autoUpdate) {
3748
3748
  try {
3749
3749
  const latest = getLatestVersion();
3750
- if (!latest || latest === "0.3.65") return;
3750
+ if (!latest || latest === "0.3.67") return;
3751
3751
  if (autoUpdate) {
3752
3752
  warn(`
3753
- Your arbi version is out of date (${"0.3.65"} \u2192 ${latest}). Updating...`);
3753
+ Your arbi version is out of date (${"0.3.67"} \u2192 ${latest}). Updating...`);
3754
3754
  child_process.execSync("npm install -g @arbidocs/cli@latest", { stdio: "inherit" });
3755
- showChangelog("0.3.65", latest);
3755
+ showChangelog("0.3.67", latest);
3756
3756
  console.log(`Updated to ${latest}.`);
3757
3757
  } else {
3758
3758
  warn(
3759
3759
  `
3760
- Your arbi version is out of date (${"0.3.65"} \u2192 ${latest}).
3760
+ Your arbi version is out of date (${"0.3.67"} \u2192 ${latest}).
3761
3761
  Run "arbi update" to upgrade, or "arbi update auto" to always stay up to date.`
3762
3762
  );
3763
3763
  }
@@ -3789,10 +3789,10 @@ function markNagShown(latest) {
3789
3789
  function hintUpdateOnError() {
3790
3790
  try {
3791
3791
  const cached = readCache();
3792
- if (!cached || cached.latest === "0.3.65") return;
3792
+ if (!cached || cached.latest === "0.3.67") return;
3793
3793
  if (!shouldShowNag(cached.latest)) return;
3794
3794
  warn(
3795
- `Your arbi version is out of date (${"0.3.65"} \u2192 ${cached.latest}). Run "arbi update".`
3795
+ `Your arbi version is out of date (${"0.3.67"} \u2192 ${cached.latest}). Run "arbi update".`
3796
3796
  );
3797
3797
  markNagShown(cached.latest);
3798
3798
  } catch {
@@ -4353,8 +4353,8 @@ Open this URL in your browser:
4353
4353
  })();
4354
4354
  if (!opts.json) success(`Logged in as ${email}`);
4355
4355
  clearChatSession();
4356
- const { data: workspaces3 } = await arbi.fetch.GET("/v1/user/workspaces");
4357
- const wsList = workspaces3 || [];
4356
+ const { data: workspaces4 } = await arbi.fetch.GET("/v1/user/workspaces");
4357
+ const wsList = workspaces4 || [];
4358
4358
  updateCompletionCache(wsList);
4359
4359
  const memberWorkspaces = wsList.filter(
4360
4360
  (ws) => ws.users?.some((u) => u.user.email === email)
@@ -4688,8 +4688,8 @@ async function loginAfterRegister(config, email, password2, { json = false } = {
4688
4688
  try {
4689
4689
  const { arbi, loginResult } = await sdk.performPasswordLogin(config, email, password2, store);
4690
4690
  if (!json) success(`Logged in as ${email}`);
4691
- const { data: workspaces3 } = await arbi.fetch.GET("/v1/user/workspaces");
4692
- const wsList = workspaces3 || [];
4691
+ const { data: workspaces4 } = await arbi.fetch.GET("/v1/user/workspaces");
4692
+ const wsList = workspaces4 || [];
4693
4693
  updateCompletionCache(wsList);
4694
4694
  const memberWorkspaces = wsList.filter((ws) => ws.users?.some((u) => u.user.email === email));
4695
4695
  let selectedWorkspace;
@@ -4842,8 +4842,12 @@ function registerWorkspacesCommand(program2) {
4842
4842
  }
4843
4843
  if (opts.json) {
4844
4844
  const selectedId2 = getConfig()?.selectedWorkspaceId ?? null;
4845
+ const myEmail = store.requireCredentials().email;
4845
4846
  const out = data.map((w) => {
4846
- const role = w.users?.[0]?.role ?? null;
4847
+ const myRow = w.users?.find(
4848
+ (u) => u?.user?.email === myEmail
4849
+ );
4850
+ const role = myRow?.role ?? null;
4847
4851
  return {
4848
4852
  id: w.external_id,
4849
4853
  name: w.name,
@@ -4884,10 +4888,16 @@ function registerWorkspacesCommand(program2) {
4884
4888
  {
4885
4889
  header: "ROLE",
4886
4890
  width: 12,
4887
- // `common` (italics dimmed) for memberless rows the caller can
4888
- // see only because it's a deployment-wide workspace. The blank
4889
- // ROLE that used to render here read like missing data.
4890
- value: (r) => r.users?.[0]?.role ?? "common"
4891
+ // `common` (italics dimmed) for memberless rows the caller
4892
+ // can see only because it's a deployment-wide workspace.
4893
+ // Match by the caller's email so we render *our* role, not
4894
+ // whichever membership happens to come first in the array.
4895
+ value: (r) => {
4896
+ const myRow = r.users?.find(
4897
+ (u) => u?.user?.email === store.requireCredentials().email
4898
+ );
4899
+ return myRow?.role ?? "common";
4900
+ }
4891
4901
  }
4892
4902
  ],
4893
4903
  data
@@ -5359,7 +5369,13 @@ function registerDocsCommand(program2) {
5359
5369
  "Write output to a file instead of stdout. Extension determines the format when combined with --ids/--json/--csv (default: csv with doc_id, file_name, folder, status)"
5360
5370
  ).action(
5361
5371
  (opts) => runAction(async () => {
5362
- const { arbi } = await resolveWorkspace(opts.workspace);
5372
+ const { arbi, workspaceId } = await resolveWorkspace(opts.workspace);
5373
+ let workspaceName = null;
5374
+ try {
5375
+ const list = await sdk.workspaces.listWorkspaces(arbi);
5376
+ workspaceName = list.find((w) => w.external_id === workspaceId)?.name ?? null;
5377
+ } catch {
5378
+ }
5363
5379
  const fields = opts.lite ? "lite" : void 0;
5364
5380
  const hardLimit = opts.limit ? parseInt(opts.limit, 10) : void 0;
5365
5381
  const needsSort = !!(opts.sort && opts.sort !== "status");
@@ -5416,15 +5432,27 @@ function registerDocsCommand(program2) {
5416
5432
  }
5417
5433
  if (totalFetched === 0 && data.length === 0) {
5418
5434
  if (opts.json) {
5419
- if (opts.output) fs5.writeFileSync(opts.output, "[]\n");
5420
- else console.log("[]");
5435
+ const emptyPayload = JSON.stringify(
5436
+ {
5437
+ workspace: { external_id: workspaceId, name: workspaceName },
5438
+ documents: []
5439
+ },
5440
+ null,
5441
+ opts.output ? 2 : 0
5442
+ );
5443
+ if (opts.output) fs5.writeFileSync(opts.output, emptyPayload + "\n");
5444
+ else console.log(emptyPayload);
5421
5445
  } else if (opts.ids) {
5422
5446
  if (opts.output) fs5.writeFileSync(opts.output, "");
5423
5447
  } else if (opts.csv) {
5424
5448
  if (opts.output) fs5.writeFileSync(opts.output, csvHeader + "\n");
5425
5449
  } else if (opts.count) {
5450
+ const wsLabel2 = workspaceName ? `${workspaceName} (${workspaceId})` : workspaceId;
5451
+ console.log(chalk2__default.default.dim(`Workspace: ${wsLabel2}`));
5426
5452
  console.log("0");
5427
5453
  } else {
5454
+ const wsLabel2 = workspaceName ? `${workspaceName} (${workspaceId})` : workspaceId;
5455
+ console.log(chalk2__default.default.dim(`Workspace: ${wsLabel2}`));
5428
5456
  console.log("No documents found.");
5429
5457
  }
5430
5458
  return;
@@ -5466,6 +5494,8 @@ function registerDocsCommand(program2) {
5466
5494
  }).length;
5467
5495
  const totalPages = data.reduce((sum, d) => sum + (d.n_pages ?? 0), 0);
5468
5496
  const totalTokens = data.reduce((sum, d) => sum + (d.tokens ?? 0), 0);
5497
+ const wsLabel2 = workspaceName ? `${workspaceName} (${workspaceId})` : workspaceId;
5498
+ console.log(chalk2__default.default.dim(`Workspace: ${wsLabel2}`));
5469
5499
  console.log(chalk2__default.default.bold(`Total: ${data.length} documents`));
5470
5500
  for (const [status2, count] of Object.entries(counts).sort()) {
5471
5501
  const colorFn = statusColor(status2);
@@ -5491,7 +5521,11 @@ function registerDocsCommand(program2) {
5491
5521
  return;
5492
5522
  }
5493
5523
  if (opts.json) {
5494
- writeOut(JSON.stringify(data, null, opts.output ? 2 : 0), "json");
5524
+ const payload = {
5525
+ workspace: { external_id: workspaceId, name: workspaceName },
5526
+ documents: data
5527
+ };
5528
+ writeOut(JSON.stringify(payload, null, opts.output ? 2 : 0), "json");
5495
5529
  return;
5496
5530
  }
5497
5531
  if (opts.csv || opts.output && !opts.ids && !opts.json) {
@@ -5502,6 +5536,8 @@ function registerDocsCommand(program2) {
5502
5536
  writeOut(lines.join("\n"), "csv");
5503
5537
  return;
5504
5538
  }
5539
+ const wsLabel = workspaceName ? `${workspaceName} (${workspaceId})` : workspaceId;
5540
+ console.log(chalk2__default.default.dim(`Workspace: ${wsLabel}`));
5505
5541
  console.log(chalk2__default.default.dim(`${data.length} documents
5506
5542
  `));
5507
5543
  printTable(
@@ -6335,7 +6371,10 @@ function registerUploadCommand(program2) {
6335
6371
  ).option(
6336
6372
  "-s, --s3",
6337
6373
  "Upload via the direct-to-MinIO flow (SecretBox on the client, presigned PUT)"
6338
- ).option("--folder <name>", "Backend folder for plain-file uploads (direct upload only)").action(
6374
+ ).option("--folder <name>", "Backend folder for plain-file uploads (direct upload only)").option(
6375
+ "--wp-type <type>",
6376
+ "Work product type: source (default), skill, memory, artifact, webpage. Use --wp-type skill with --folder skills/<slug> to upload a SKILL.md."
6377
+ ).action(
6339
6378
  (paths, opts) => runAction(async () => {
6340
6379
  const isManifestMode = Boolean(opts.manifest || opts.log || opts.resume);
6341
6380
  const inputPaths = paths ?? [];
@@ -6492,7 +6531,10 @@ function registerUploadCommand(program2) {
6492
6531
  summary.totalFiles += 1;
6493
6532
  continue;
6494
6533
  }
6495
- const result = await sdk.documentsNode.uploadLocalFile(auth, filePath);
6534
+ const result = await sdk.documentsNode.uploadLocalFile(auth, filePath, {
6535
+ folder: opts.folder,
6536
+ wpType: opts.wpType
6537
+ });
6496
6538
  if (!opts.json) {
6497
6539
  success(`Uploaded: ${result.fileName} (${(result.doc_ext_ids ?? []).join(", ")})`);
6498
6540
  if (result.skipped && result.skipped.length > 0) {
@@ -8947,8 +8989,8 @@ async function runQuickstart(url) {
8947
8989
  try {
8948
8990
  const { arbi, loginResult } = await sdk.performPasswordLogin(config, email, password2, store);
8949
8991
  success(`Logged in as ${email}`);
8950
- const { data: workspaces3 } = await arbi.fetch.GET("/v1/user/workspaces");
8951
- const wsList = workspaces3 || [];
8992
+ const { data: workspaces4 } = await arbi.fetch.GET("/v1/user/workspaces");
8993
+ const wsList = workspaces4 || [];
8952
8994
  const memberWorkspaces = wsList.filter((ws) => ws.users?.some((u) => u.user.email === email));
8953
8995
  const userProjects = await sdk.projects.listProjects(arbi);
8954
8996
  const defaultProjectExtId = userProjects[0]?.external_id;
@@ -10040,6 +10082,142 @@ function registerAuthCommand(program2) {
10040
10082
  await auth.commands.find((c) => c.name() === "profile").parseAsync(tail, { from: "user" });
10041
10083
  });
10042
10084
  }
10085
+ function extractSlugFromFrontmatter(body) {
10086
+ const normalized = body.replace(/\r\n/g, "\n");
10087
+ if (!normalized.startsWith("---\n")) return null;
10088
+ const end = normalized.indexOf("\n---", 4);
10089
+ if (end < 0) return null;
10090
+ const block = normalized.slice(4, end);
10091
+ for (const line of block.split("\n")) {
10092
+ const m = /^name:\s*(.+?)\s*$/.exec(line);
10093
+ if (m) {
10094
+ return m[1].toLowerCase().replace(/['"]/g, "").replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
10095
+ }
10096
+ }
10097
+ return null;
10098
+ }
10099
+ async function loadSkillSource(source) {
10100
+ if (source.startsWith("http://") || source.startsWith("https://")) {
10101
+ const res = await fetch(source);
10102
+ if (!res.ok) {
10103
+ throw new Error(`Failed to fetch ${source}: HTTP ${res.status} ${res.statusText}`);
10104
+ }
10105
+ return await res.text();
10106
+ }
10107
+ const stat = fs5.statSync(source);
10108
+ if (stat.isDirectory()) {
10109
+ throw new Error(
10110
+ `${source} is a directory. Pass a single SKILL.md file, or use \`\`arbi upload --folder skills/<slug> --wp-type skill\`\` for multi-file skills.`
10111
+ );
10112
+ }
10113
+ return fs5.readFileSync(source, "utf8");
10114
+ }
10115
+ function registerSkillsCommand(program2) {
10116
+ const skills = program2.command("skills").description("Skills: list, show");
10117
+ skills.command("list").description("List user-invocable skills in the active workspace").option("--json", "Output as JSON").option("--include-hidden", "Include skills with user-invocable: false").action(
10118
+ (opts) => runAction(async () => {
10119
+ const { arbi } = await resolveWorkspace();
10120
+ const list = await sdk.assistant.listSkills(arbi, {
10121
+ includeHidden: opts.includeHidden
10122
+ });
10123
+ if (opts.json) {
10124
+ console.log(JSON.stringify(list, null, 2));
10125
+ return;
10126
+ }
10127
+ if (list.length === 0) {
10128
+ process.stderr.write(
10129
+ "No skills in this workspace yet. Upload a SKILL.md with wp_type=skill to add one.\n"
10130
+ );
10131
+ return;
10132
+ }
10133
+ printTable(
10134
+ [
10135
+ { header: "SLUG", width: 20, value: (r) => r.slug ?? "" },
10136
+ { header: "NAME", width: 24, value: (r) => r.name ?? "" },
10137
+ {
10138
+ header: "TYPE",
10139
+ width: 10,
10140
+ value: (r) => r.skill_type ?? "markdown"
10141
+ },
10142
+ {
10143
+ header: "ARGS",
10144
+ width: 20,
10145
+ value: (r) => r.arg_hint ?? ""
10146
+ },
10147
+ {
10148
+ header: "DESCRIPTION",
10149
+ width: 40,
10150
+ value: (r) => r.description ?? ""
10151
+ }
10152
+ ],
10153
+ list
10154
+ );
10155
+ })()
10156
+ );
10157
+ skills.command("install <source>").description(
10158
+ "Install a skill from a URL or local path (SKILL.md). Slug derived from frontmatter unless --slug is given."
10159
+ ).option("--slug <slug>", "Override the slug (default: derived from frontmatter name)").option("--json", "Output the upload result as JSON").action(
10160
+ (source, opts) => runAction(async () => {
10161
+ const ctx = await resolveWorkspace();
10162
+ const authHeaders = {
10163
+ baseUrl: ctx.config.baseUrl,
10164
+ accessToken: ctx.accessToken
10165
+ };
10166
+ const body = await loadSkillSource(source);
10167
+ const slug = opts.slug ?? extractSlugFromFrontmatter(body) ?? (() => {
10168
+ process.stderr.write(
10169
+ "Could not derive slug: no --slug flag and no ``name:`` in frontmatter.\nAdd a frontmatter block like:\n\n ---\n name: my-skill\n description: \u2026\n ---\n\nor pass ``--slug my-skill`` explicitly.\n"
10170
+ );
10171
+ process.exit(1);
10172
+ })();
10173
+ const blob = new Blob([body], { type: "text/markdown" });
10174
+ const result = await sdk.documents.uploadFile(authHeaders, blob, "SKILL.md", {
10175
+ folder: `skills/${slug}`,
10176
+ wpType: "skill"
10177
+ });
10178
+ if (opts.json) {
10179
+ console.log(JSON.stringify({ slug, ...result }, null, 2));
10180
+ return;
10181
+ }
10182
+ console.log(bold(`Installed skill: ${slug}`));
10183
+ console.log(dim(`source: ${source}`));
10184
+ console.log(dim(`folder: skills/${slug}`));
10185
+ console.log(
10186
+ dim("Backend frontmatter loader picks it up on the next /v1/assistant/skills call.")
10187
+ );
10188
+ })()
10189
+ );
10190
+ skills.command("show <slug>").description("Show a skill's SKILL.md body (read-only)").option("--json", "Output as JSON (includes metadata)").action(
10191
+ (slug, opts) => runAction(async () => {
10192
+ const ctx = await resolveWorkspace();
10193
+ const list = await sdk.assistant.listSkills(ctx.arbi, { includeHidden: true });
10194
+ const skill = list.find((s) => s.slug === slug || s.name === slug);
10195
+ if (!skill) {
10196
+ process.stderr.write(
10197
+ `No skill matching "${slug}" in this workspace. Try \`arbi skills list\`.
10198
+ `
10199
+ );
10200
+ process.exit(1);
10201
+ }
10202
+ const authHeaders = {
10203
+ baseUrl: ctx.config.baseUrl,
10204
+ accessToken: ctx.accessToken
10205
+ };
10206
+ const dlRes = await sdk.documents.downloadDocument(authHeaders, skill.doc_ext_id);
10207
+ const body = dlRes.ok ? await dlRes.text() : "";
10208
+ if (opts.json) {
10209
+ console.log(JSON.stringify({ ...skill, body }, null, 2));
10210
+ return;
10211
+ }
10212
+ console.log(bold(`${skill.name} (${skill.slug})`));
10213
+ if (skill.description) console.log(dim(skill.description));
10214
+ if (skill.arg_hint) console.log(dim(`args: ${skill.arg_hint}`));
10215
+ console.log(dim(`type: ${skill.skill_type} doc: ${skill.doc_ext_id}`));
10216
+ console.log("");
10217
+ console.log(body);
10218
+ })()
10219
+ );
10220
+ }
10043
10221
 
10044
10222
  // src/index.ts
10045
10223
  console.debug = () => {
@@ -10050,7 +10228,7 @@ console.info = (...args) => {
10050
10228
  _origInfo(...args);
10051
10229
  };
10052
10230
  var program = new commander.Command();
10053
- program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.3.65").showHelpAfterError(true).showSuggestionAfterError(true);
10231
+ program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.3.67").showHelpAfterError(true).showSuggestionAfterError(true);
10054
10232
  registerConfigCommand(program);
10055
10233
  registerLoginCommand(program);
10056
10234
  registerRegisterCommand(program);
@@ -10087,6 +10265,7 @@ registerProjectCommand(program);
10087
10265
  registerFilesCommand(program);
10088
10266
  registerUsageCommand(program);
10089
10267
  registerAuthCommand(program);
10268
+ registerSkillsCommand(program);
10090
10269
  applyHelpGroups(program, {
10091
10270
  "Getting started:": [
10092
10271
  "quickstart",