@bli-cockpit/cli 0.2.112 → 0.2.114

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.
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
15
15
  }
16
16
 
17
17
  if (command === "--version" || command === "-V" || command === "version") {
18
- writeLine(io?.stdout ?? process.stdout, "0.2.112");
18
+ writeLine(io?.stdout ?? process.stdout, "0.2.114");
19
19
  return 0;
20
20
  }
21
21
 
@@ -108,6 +108,16 @@ export async function runAttributedWorktreeSync(options) {
108
108
  now,
109
109
  claudePriorDurablePointers: worktreePass.claudePriorDurablePointers,
110
110
  });
111
+ // BLI-4341: every session should arrive counted. Say how many did, and say
112
+ // it on the success path too: a silent drop from "all" to "some" is exactly
113
+ // the kind of decay nobody notices.
114
+ const sessionsCounted = sessions.filter((session) => session.session_facts).length;
115
+ console.error("[session-sync] session facts counted at upload", JSON.stringify({
116
+ reason: sessionsCounted === sessions.length ? "counted_at_upload" : "some_sessions_unreadable",
117
+ sessions: sessions.length,
118
+ counted: sessionsCounted,
119
+ tokens_total: sessions.reduce((total, session) => total + (session.session_facts?.total_tokens ?? 0), 0),
120
+ }));
111
121
  const delivery = await reportSessionsAndAdvanceCursors({
112
122
  run: options,
113
123
  paths,
@@ -0,0 +1,94 @@
1
+ /**
2
+ * What bare `cockpit settings` shows: every section at once (BLI-3461).
3
+ *
4
+ * It lives beside `settings.ts` because it is the only caller that has to hold
5
+ * six independent reads in its head at the same time and decide, per section,
6
+ * between a body, "admin only" and a named failure. The per-section verbs next
7
+ * door each answer for exactly one route.
8
+ *
9
+ * "Admin only" is not an error here. A read that 403s is the system working,
10
+ * so a section this person cannot see renders as one line saying so.
11
+ */
12
+ import { writeLine } from "./cli-io.js";
13
+ import { renderCliFloor, renderEnvBlobs, renderModelRouting, renderPersonal, renderSwitches, renderTeamSummary, } from "./settings-render.js";
14
+ import { asRecord, callTower, isForbidden, writeCommandFailure, } from "./tower-command.js";
15
+ /**
16
+ * Everything at once, with the sections this person may not see named as such.
17
+ *
18
+ * The six reads go out together: they are independent, and a person waiting on
19
+ * six sequential round trips would reasonably conclude the command had hung.
20
+ */
21
+ export async function showOverview(command, tower, io) {
22
+ const [personal, switches, models, env, team, cliFloor] = await Promise.all([
23
+ callTower(tower, { path: "/api/settings/personal", label: "settings personal" }),
24
+ callTower(tower, { path: "/api/settings/switches", label: "settings switches" }),
25
+ callTower(tower, { path: "/api/settings/model-routing", label: "settings models" }),
26
+ callTower(tower, { path: "/api/settings/env-blobs", label: "settings env" }),
27
+ callTower(tower, { path: "/api/team/members", label: "settings team" }),
28
+ // BLI-3557: the floor rides the overview because "which version is the
29
+ // fleet being pulled to?" is a question people ask about their own laptop,
30
+ // and it went unanswered for weeks precisely because nothing showed it.
31
+ callTower(tower, { path: "/api/settings/cli-floor", label: "settings cli-floor" }),
32
+ ]);
33
+ // The one read every signed-in person is entitled to. If THAT is refused,
34
+ // nothing else is going to work either, so say so once and stop.
35
+ if (!personal.ok && !isForbidden(personal)) {
36
+ return writeCommandFailure(io, command.json, personal);
37
+ }
38
+ if (command.json) {
39
+ writeLine(io.stdout, JSON.stringify({
40
+ ok: true,
41
+ personal: sectionPayload(personal),
42
+ switches: sectionPayload(switches),
43
+ models: sectionPayload(models),
44
+ env: sectionPayload(env),
45
+ team: sectionPayload(team),
46
+ cliFloor: sectionPayload(cliFloor),
47
+ }));
48
+ return 0;
49
+ }
50
+ writeLine(io.stdout, "TOWER SETTINGS");
51
+ writeSection(io, "PERSONAL", personal, (body) => renderPersonal(body));
52
+ writeSection(io, "TEAM", team, (body) => renderTeamSummary(body));
53
+ writeSection(io, "SWITCHES", switches, (body) => renderSwitches(body));
54
+ writeSection(io, "MODELS", models, (body) => renderModelRouting(body));
55
+ writeSection(io, "ENV FILES", env, (body) => renderEnvBlobs(body));
56
+ writeSection(io, "FLEET CLI FLOOR", cliFloor, (body) => renderCliFloor(body));
57
+ writeLine(io.stderr, `[settings cli] overview ${JSON.stringify({
58
+ personal: outcome(personal),
59
+ team: outcome(team),
60
+ switches: outcome(switches),
61
+ models: outcome(models),
62
+ env: outcome(env),
63
+ cli_floor: outcome(cliFloor),
64
+ })}`);
65
+ return 0;
66
+ }
67
+ /** One section's heading and body — or the one line that says it is not yours. */
68
+ function writeSection(io, heading, result, render) {
69
+ writeLine(io.stdout, "");
70
+ writeLine(io.stdout, heading);
71
+ if (isForbidden(result)) {
72
+ writeLine(io.stdout, " admin only");
73
+ return;
74
+ }
75
+ if (!result.ok) {
76
+ // Not "admin only" and not shown: a real failure, named where it happened.
77
+ writeLine(io.stdout, ` unavailable — ${result.detail}`);
78
+ return;
79
+ }
80
+ for (const line of render(asRecord(result.body)))
81
+ writeLine(io.stdout, line);
82
+ }
83
+ function sectionPayload(result) {
84
+ if (result.ok)
85
+ return result.body;
86
+ if (isForbidden(result))
87
+ return { visible: false, reason: "admin_only" };
88
+ return { visible: false, reason: result.reason, detail: result.detail };
89
+ }
90
+ function outcome(result) {
91
+ if (result.ok)
92
+ return "ok";
93
+ return isForbidden(result) ? "admin_only" : result.reason;
94
+ }
@@ -21,7 +21,8 @@
21
21
  */
22
22
  import { isInteractiveStdin, readLine, writeLine, yesByDefault } from "./cli-io.js";
23
23
  import { asList, asRecord, callTower, isForbidden, openTower, parseModelKey, readAllStdin, writeCommandFailure, } from "./tower-command.js";
24
- import { renderCliFloor, renderEnvBlobs, renderModelRouting, renderPersonal, renderSwitches, renderTeamSummary, } from "./settings-render.js";
24
+ import { showOverview } from "./settings-overview.js";
25
+ import { renderCliFloor, renderEnvBlobs, renderModelRouting, renderPersonal, renderSwitches, } from "./settings-render.js";
25
26
  export async function runSettings(command, io) {
26
27
  const tower = await openTower("settings", command, io);
27
28
  switch (command.section) {
@@ -51,87 +52,6 @@ export async function runSettings(command, io) {
51
52
  return listEnvBlobs(command, tower, io);
52
53
  }
53
54
  }
54
- // ── overview ──────────────────────────────────────────────────────────
55
- /**
56
- * Everything at once, with the sections this person may not see named as such.
57
- *
58
- * The five reads go out together: they are independent, and a person waiting on
59
- * five sequential round trips would reasonably conclude the command had hung.
60
- */
61
- async function showOverview(command, tower, io) {
62
- const [personal, switches, models, env, team, cliFloor] = await Promise.all([
63
- callTower(tower, { path: "/api/settings/personal", label: "settings personal" }),
64
- callTower(tower, { path: "/api/settings/switches", label: "settings switches" }),
65
- callTower(tower, { path: "/api/settings/model-routing", label: "settings models" }),
66
- callTower(tower, { path: "/api/settings/env-blobs", label: "settings env" }),
67
- callTower(tower, { path: "/api/team/members", label: "settings team" }),
68
- // BLI-3557: the floor rides the overview because "which version is the
69
- // fleet being pulled to?" is a question people ask about their own laptop,
70
- // and it went unanswered for weeks precisely because nothing showed it.
71
- callTower(tower, { path: "/api/settings/cli-floor", label: "settings cli-floor" }),
72
- ]);
73
- // The one read every signed-in person is entitled to. If THAT is refused,
74
- // nothing else is going to work either, so say so once and stop.
75
- if (!personal.ok && !isForbidden(personal)) {
76
- return writeCommandFailure(io, command.json, personal);
77
- }
78
- if (command.json) {
79
- writeLine(io.stdout, JSON.stringify({
80
- ok: true,
81
- personal: sectionPayload(personal),
82
- switches: sectionPayload(switches),
83
- models: sectionPayload(models),
84
- env: sectionPayload(env),
85
- team: sectionPayload(team),
86
- cliFloor: sectionPayload(cliFloor),
87
- }));
88
- return 0;
89
- }
90
- writeLine(io.stdout, "TOWER SETTINGS");
91
- writeSection(io, "PERSONAL", personal, (body) => renderPersonal(body));
92
- writeSection(io, "TEAM", team, (body) => renderTeamSummary(body));
93
- writeSection(io, "SWITCHES", switches, (body) => renderSwitches(body));
94
- writeSection(io, "MODELS", models, (body) => renderModelRouting(body));
95
- writeSection(io, "ENV FILES", env, (body) => renderEnvBlobs(body));
96
- writeSection(io, "FLEET CLI FLOOR", cliFloor, (body) => renderCliFloor(body));
97
- writeLine(io.stderr, `[settings cli] overview ${JSON.stringify({
98
- personal: outcome(personal),
99
- team: outcome(team),
100
- switches: outcome(switches),
101
- models: outcome(models),
102
- env: outcome(env),
103
- cli_floor: outcome(cliFloor),
104
- })}`);
105
- return 0;
106
- }
107
- /** One section's heading and body — or the one line that says it is not yours. */
108
- function writeSection(io, heading, result, render) {
109
- writeLine(io.stdout, "");
110
- writeLine(io.stdout, heading);
111
- if (isForbidden(result)) {
112
- writeLine(io.stdout, " admin only");
113
- return;
114
- }
115
- if (!result.ok) {
116
- // Not "admin only" and not shown: a real failure, named where it happened.
117
- writeLine(io.stdout, ` unavailable — ${result.detail}`);
118
- return;
119
- }
120
- for (const line of render(asRecord(result.body)))
121
- writeLine(io.stdout, line);
122
- }
123
- function sectionPayload(result) {
124
- if (result.ok)
125
- return result.body;
126
- if (isForbidden(result))
127
- return { visible: false, reason: "admin_only" };
128
- return { visible: false, reason: result.reason, detail: result.detail };
129
- }
130
- function outcome(result) {
131
- if (result.ok)
132
- return "ok";
133
- return isForbidden(result) ? "admin_only" : result.reason;
134
- }
135
55
  // ── personal ──────────────────────────────────────────────────────────
136
56
  async function showPersonal(command, tower, io) {
137
57
  const result = await callTower(tower, {
@@ -43,7 +43,7 @@ export async function runUsage(command, io) {
43
43
  if (!email)
44
44
  return failAgentDoor(door, "[usage]", "caller_email_unavailable", "The paired session has no email. Sign in again or pass --person <email>.");
45
45
  body.people = (body.people ?? []).filter((person) => person.email?.toLowerCase() === email.toLowerCase());
46
- body.coverage = { sessions_labelled: body.people.reduce((total, person) => total + (person.sessions_labelled ?? 0), 0), sessions_extracted: body.people.reduce((total, person) => total + person.sessions_extracted, 0), sessions_observed: body.people.reduce((total, person) => total + person.sessions_observed, 0) };
46
+ body.coverage = { sessions_labelled: body.people.reduce((total, person) => total + (person.sessions_labelled ?? 0), 0), sessions_extracted: body.people.reduce((total, person) => total + person.sessions_extracted, 0), sessions_observed: body.people.reduce((total, person) => total + person.sessions_observed, 0), sessions_counted_at_upload: body.people.reduce((total, person) => total + (person.sessions_counted_at_upload ?? 0), 0), sessions_counted_by_server: body.people.reduce((total, person) => total + (person.sessions_counted_by_server ?? 0), 0) };
47
47
  }
48
48
  if (command.byRepo && body.people?.some((person) => !Array.isArray(person.repos))) {
49
49
  return failAgentDoor(door, "[usage]", "repo_grouping_unavailable", "The dashboard returned person totals without project rows. Deploy the dashboard project split before using --by-repo.");
@@ -104,6 +104,9 @@ export async function runUsage(command, io) {
104
104
  writeLine(io.stdout, "");
105
105
  writeLine(io.stdout, body.api_list_price_equivalent_label ?? "API list-price equivalent (not actual spend)");
106
106
  writeLine(io.stdout, `${body.coverage?.sessions_extracted ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions extracted`);
107
+ // BLI-4341: a session should arrive counted. This line is how a person sees
108
+ // whether that is happening, or whether the server is still doing the work.
109
+ writeLine(io.stdout, `${body.coverage?.sessions_counted_at_upload ?? 0} of ${body.coverage?.sessions_observed ?? 0} sessions counted at upload, ${body.coverage?.sessions_counted_by_server ?? 0} by server backfill`);
107
110
  if (command.bySubject)
108
111
  writeLine(io.stdout, `${(body.people ?? []).reduce((n, person) => n + (person.sessions_summarized ?? 0), 0)} of ${body.coverage?.sessions_observed ?? 0} sessions summarized`);
109
112
  if (command.byTopic)
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Why a folder cannot be a collection root, and exactly what we say about it.
3
+ *
4
+ * Split out of onboarding-roots.ts so the file that RESOLVES a root reads as
5
+ * the conversation, and this one holds the sentences. Every string here is
6
+ * read by a person mid-install and matched verbatim by tests: change the
7
+ * wording only on purpose.
8
+ */
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+ export function homeRootConsentPrompt(homeDirInput) {
12
+ const homeDir = path.resolve(homeDirInput ?? os.homedir());
13
+ return `You're in your home folder (${homeDir}).\n Sync ALL projects on this machine? Every git repo under here gets captured now and in the future.\n This is a work machine — exclude personal projects yourself if needed.\n Press Enter to sync everything, or answer n to name one folder instead. [Y/n]: `;
14
+ }
15
+ export const HOME_ROOT_DECLINE_PATH_PROMPT = "Okay — which folder should I sync? Enter the full path to your work directory: ";
16
+ export const HOME_ROOT_NO_FOLDER_CHOSEN_MESSAGE = "No folder chosen. Re-run: cockpit onboard --workspace <path-to-your-work-folder>";
17
+ export function rootRejectionExplanation(rejection, options = {}) {
18
+ switch (rejection.reason) {
19
+ case "home_dir":
20
+ return homeRootTutorial(options.homeDir);
21
+ case "fs_root":
22
+ return filesystemRootTutorial(options.homeDir);
23
+ }
24
+ }
25
+ export function homeRootTutorial(homeDirInput) {
26
+ const homeDir = path.resolve(homeDirInput ?? os.homedir());
27
+ const bliRoot = path.join(homeDir, "BLI");
28
+ const otherRoot = path.join(homeDir, "other");
29
+ return [
30
+ "Your home folder can't be a collection root by default:",
31
+ ` scanning all of ${homeDir} would include personal folders and every repo you ever create.`,
32
+ "What you can do:",
33
+ ` 1) Specific folders: cockpit onboard --workspace "${bliRoot},${otherRoot}"`,
34
+ " 2) One work parent: mkdir ~/BLI && move your repos in, then: cockpit onboard --workspace ~/BLI",
35
+ " 3) Collect everything anyway (company machine): rerun with --allow-home-root",
36
+ ].join("\n");
37
+ }
38
+ function filesystemRootTutorial(homeDirInput) {
39
+ const homeDir = path.resolve(homeDirInput ?? os.homedir());
40
+ const bliRoot = path.join(homeDir, "BLI");
41
+ const otherRoot = path.join(homeDir, "other");
42
+ return [
43
+ "Your filesystem root can't be a collection root:",
44
+ ` scanning all of ${path.parse(homeDir).root} would include system folders, private data, and every mounted repo.`,
45
+ "What you can do:",
46
+ ` 1) Specific folders: cockpit onboard --workspace "${bliRoot},${otherRoot}"`,
47
+ " 2) One work parent: mkdir ~/BLI && move your repos in, then: cockpit onboard --workspace ~/BLI",
48
+ ].join("\n");
49
+ }
50
+ export function missingCollectionRootMessage(options = {}) {
51
+ const homeDir = path.resolve(options.homeDir ?? os.homedir());
52
+ return [
53
+ "No collection root was confirmed:",
54
+ " Tower only scans folders you name or confirm.",
55
+ "What you can do:",
56
+ ` 1) Paste one work parent: ${path.join(homeDir, "BLI")}`,
57
+ ` 2) Paste specific folders: ${path.join(homeDir, "repo-a")},${path.join(homeDir, "repo-b")}`,
58
+ " 3) Stop and rerun with a flag: cockpit onboard --workspace ~/BLI",
59
+ ].join("\n");
60
+ }
@@ -2,7 +2,12 @@ import fs from "node:fs/promises";
2
2
  import { realpathSync } from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { HOME_ROOT_DECLINE_PATH_PROMPT, HOME_ROOT_NO_FOLDER_CHOSEN_MESSAGE, homeRootConsentPrompt, homeRootTutorial, missingCollectionRootMessage, rootRejectionExplanation, } from "./onboarding-root-guidance.js";
5
6
  import { isSamePath, normalizeCollectionRoots, } from "./root-normalization.js";
7
+ // Every sentence this file says to a person lives next door, with the reasons
8
+ // it says them. Re-exported here because this is the address callers and tests
9
+ // have always imported them from.
10
+ export { HOME_ROOT_DECLINE_PATH_PROMPT, HOME_ROOT_NO_FOLDER_CHOSEN_MESSAGE, homeRootConsentPrompt, missingCollectionRootMessage, rootRejectionExplanation, } from "./onboarding-root-guidance.js";
6
11
  export const COLLECTION_ROOT_REQUIRED = "collection_root_required";
7
12
  /**
8
13
  * No approved collection root, so there is nothing this machine may look at.
@@ -21,12 +26,6 @@ export class CollectionRootRequiredError extends Error {
21
26
  this.name = "CollectionRootRequiredError";
22
27
  }
23
28
  }
24
- export function homeRootConsentPrompt(homeDirInput) {
25
- const homeDir = path.resolve(homeDirInput ?? os.homedir());
26
- return `You're in your home folder (${homeDir}).\n Sync ALL projects on this machine? Every git repo under here gets captured now and in the future.\n This is a work machine — exclude personal projects yourself if needed.\n Press Enter to sync everything, or answer n to name one folder instead. [Y/n]: `;
27
- }
28
- export const HOME_ROOT_DECLINE_PATH_PROMPT = "Okay — which folder should I sync? Enter the full path to your work directory: ";
29
- export const HOME_ROOT_NO_FOLDER_CHOSEN_MESSAGE = "No folder chosen. Re-run: cockpit onboard --workspace <path-to-your-work-folder>";
30
29
  export async function resolveOnboardingRoots(options) {
31
30
  const explicitInput = options.explicitRoots ?? [];
32
31
  const explicit = normalizeRootsDetailed(explicitInput, {
@@ -207,28 +206,12 @@ function explainRejectedRoots(options, rejections) {
207
206
  return;
208
207
  const prompt = requirePrompt(options);
209
208
  for (const rejection of rejections) {
210
- prompt.message?.(rootRejectionPromptHint(rejection, options));
211
- }
212
- }
213
- export function rootRejectionExplanation(rejection, options = {}) {
214
- switch (rejection.reason) {
215
- case "home_dir":
216
- return homeRootTutorial(options.homeDir);
217
- case "fs_root":
218
- return filesystemRootTutorial(options.homeDir);
209
+ prompt.message?.(rootRejectionExplanation(rejection, options));
219
210
  }
220
211
  }
221
212
  function withoutHomeRejections(rejections) {
222
213
  return rejections.filter((rejection) => rejection.reason !== "home_dir");
223
214
  }
224
- function rootRejectionPromptHint(rejection, options = {}) {
225
- switch (rejection.reason) {
226
- case "home_dir":
227
- return homeRootTutorial(options.homeDir);
228
- case "fs_root":
229
- return filesystemRootTutorial(options.homeDir);
230
- }
231
- }
232
215
  async function promptForHomeRootOptIn(options, rejections) {
233
216
  if (!rejections.some((rejection) => rejection.reason === "home_dir"))
234
217
  return null;
@@ -324,42 +307,6 @@ function canonicalPath(input, pathApi = path, realpath = realpathSync) {
324
307
  function isHomeRoot(root, homeDir) {
325
308
  return isSamePath(canonicalPath(root), canonicalPath(homeDir ?? os.homedir()));
326
309
  }
327
- function homeRootTutorial(homeDirInput) {
328
- const homeDir = path.resolve(homeDirInput ?? os.homedir());
329
- const bliRoot = path.join(homeDir, "BLI");
330
- const otherRoot = path.join(homeDir, "other");
331
- return [
332
- "Your home folder can't be a collection root by default:",
333
- ` scanning all of ${homeDir} would include personal folders and every repo you ever create.`,
334
- "What you can do:",
335
- ` 1) Specific folders: cockpit onboard --workspace "${bliRoot},${otherRoot}"`,
336
- " 2) One work parent: mkdir ~/BLI && move your repos in, then: cockpit onboard --workspace ~/BLI",
337
- " 3) Collect everything anyway (company machine): rerun with --allow-home-root",
338
- ].join("\n");
339
- }
340
- function filesystemRootTutorial(homeDirInput) {
341
- const homeDir = path.resolve(homeDirInput ?? os.homedir());
342
- const bliRoot = path.join(homeDir, "BLI");
343
- const otherRoot = path.join(homeDir, "other");
344
- return [
345
- "Your filesystem root can't be a collection root:",
346
- ` scanning all of ${path.parse(homeDir).root} would include system folders, private data, and every mounted repo.`,
347
- "What you can do:",
348
- ` 1) Specific folders: cockpit onboard --workspace "${bliRoot},${otherRoot}"`,
349
- " 2) One work parent: mkdir ~/BLI && move your repos in, then: cockpit onboard --workspace ~/BLI",
350
- ].join("\n");
351
- }
352
- export function missingCollectionRootMessage(options = {}) {
353
- const homeDir = path.resolve(options.homeDir ?? os.homedir());
354
- return [
355
- "No collection root was confirmed:",
356
- " Tower only scans folders you name or confirm.",
357
- "What you can do:",
358
- ` 1) Paste one work parent: ${path.join(homeDir, "BLI")}`,
359
- ` 2) Paste specific folders: ${path.join(homeDir, "repo-a")},${path.join(homeDir, "repo-b")}`,
360
- " 3) Stop and rerun with a flag: cockpit onboard --workspace ~/BLI",
361
- ].join("\n");
362
- }
363
310
  export function likelyBliRootFromCwd(cwd, pathApi = path) {
364
311
  const root = pathApi.parse(cwd).root;
365
312
  const parts = pathApi
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/cli",
3
- "version": "0.2.112",
3
+ "version": "0.2.114",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,8 +27,8 @@
27
27
  "test": "node dist/cli.js --help && node ../../scripts/assert-public-cli-routing.mjs && node ../../scripts/assert-public-cli-verb-help.mjs && node ../../scripts/assert-public-cli-runtime-files.mjs && node ../../scripts/assert-public-cli-exit-contract.mjs && node ../../scripts/assert-public-cli-no-fleet-posts.mjs && node ../../scripts/assert-public-package-pack.mjs --workspace=@bli-cockpit/cli"
28
28
  },
29
29
  "dependencies": {
30
- "@bli-cockpit/memory-mcp": "0.1.30",
31
- "@bli-cockpit/mcp": "0.1.40",
32
- "@bli-cockpit/telemetry-core": "0.1.46"
30
+ "@bli-cockpit/memory-mcp": "0.1.32",
31
+ "@bli-cockpit/mcp": "0.1.42",
32
+ "@bli-cockpit/telemetry-core": "0.1.48"
33
33
  }
34
34
  }