costlayers 0.8.10 → 0.8.13
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/README.md +33 -9
- package/bin/agentspend.js +99 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# CostLayers CLI
|
|
2
2
|
|
|
3
|
-
CostLayers helps
|
|
3
|
+
CostLayers helps coding-agent users stop paying for repeated repo context. API users can route model calls through the gateway for invoice savings. ChatGPT-login Codex users get a usage-stretch meter that shows how much repeated context was avoided.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ One-command setup:
|
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
10
|
cd your-repo
|
|
11
|
-
npx -y costlayers
|
|
11
|
+
npx -y https://costlayers.com/costlayers-0.8.13.tgz start --email you@example.com
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
@@ -24,19 +24,42 @@ Launch Codex with CostLayers context:
|
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
26
|
cd your-repo
|
|
27
|
-
npx -y costlayers
|
|
27
|
+
npx -y https://costlayers.com/costlayers-0.8.13.tgz start --email you@example.com -- codex
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
This gives Codex `.agentspend/repo-pack.md` and `.agentspend/runtime-plan.md`
|
|
31
31
|
so it can avoid repeated broad repo exploration. It also writes
|
|
32
32
|
`~/.codex/costlayers.config.toml` and runs Codex as
|
|
33
|
-
`codex --profile costlayers`, enabling
|
|
34
|
-
|
|
33
|
+
`codex --profile costlayers`, enabling redacted Codex telemetry for
|
|
34
|
+
ChatGPT-login sessions while preserving Codex's native model provider.
|
|
35
|
+
|
|
36
|
+
This mode is for getting more useful work out of the same ChatGPT/Codex plan.
|
|
37
|
+
It does not reduce a flat ChatGPT subscription invoice.
|
|
38
|
+
|
|
39
|
+
## Invoice Savings Mode
|
|
40
|
+
|
|
41
|
+
To reduce an OpenAI Platform invoice, route API-key billed model calls through
|
|
42
|
+
the CostLayers gateway. This requires an OpenAI Platform API key with Responses
|
|
43
|
+
API write permission:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
export OPENAI_API_KEY=sk-proj-...
|
|
47
|
+
npx -y https://costlayers.com/costlayers-0.8.13.tgz start --email you@example.com --codex-proxy -- codex
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
ChatGPT-login Codex can be metered, but it does not create per-request OpenAI
|
|
51
|
+
Platform invoice savings because it is not billed through your Platform API key.
|
|
52
|
+
|
|
53
|
+
## Which Mode Should I Use?
|
|
54
|
+
|
|
55
|
+
- ChatGPT-login Codex: use `start -- codex` to reduce repeated repo context and stretch usage limits.
|
|
56
|
+
- OpenAI Platform API billing: use `--codex-proxy` with `OPENAI_API_KEY` for invoice-backed savings.
|
|
57
|
+
- Other OpenAI-compatible clients: point the client at the CostLayers gateway URL and check `costlayers gateway report`.
|
|
35
58
|
|
|
36
59
|
To install only the Codex profile after signup:
|
|
37
60
|
|
|
38
61
|
```bash
|
|
39
|
-
npx -y costlayers
|
|
62
|
+
npx -y https://costlayers.com/costlayers-0.8.13.tgz codex-profile
|
|
40
63
|
codex --profile costlayers
|
|
41
64
|
```
|
|
42
65
|
|
|
@@ -50,9 +73,10 @@ costlayers gateway report
|
|
|
50
73
|
costlayers dashboard
|
|
51
74
|
```
|
|
52
75
|
|
|
53
|
-
Metered
|
|
54
|
-
|
|
55
|
-
uses the CostLayers profile or emits token telemetry to the
|
|
76
|
+
Metered gateway savings increase when model API traffic is routed through the
|
|
77
|
+
CostLayers gateway URL with a provider API key. Codex-metered dashboard savings
|
|
78
|
+
increase when Codex uses the CostLayers profile or emits token telemetry to the
|
|
79
|
+
CostLayers meter.
|
|
56
80
|
For ChatGPT-login Codex, dollar savings are modeled from observed Codex token
|
|
57
81
|
events because the ChatGPT plan invoice is not an OpenAI Platform API invoice.
|
|
58
82
|
|
package/bin/agentspend.js
CHANGED
|
@@ -9,7 +9,8 @@ const https = require("https");
|
|
|
9
9
|
const os = require("os");
|
|
10
10
|
const { spawnSync } = require("child_process");
|
|
11
11
|
|
|
12
|
-
const VERSION = "0.8.
|
|
12
|
+
const VERSION = "0.8.13";
|
|
13
|
+
const INSTALL_SPEC = "https://costlayers.com/costlayers-0.8.13.tgz";
|
|
13
14
|
const DEFAULT_RUNS_PER_WEEK = 20;
|
|
14
15
|
const WEEKS_PER_MONTH = 4.33;
|
|
15
16
|
const DEFAULT_EXCLUDES = new Set([
|
|
@@ -51,9 +52,9 @@ CostLayers ${VERSION}
|
|
|
51
52
|
Usage:
|
|
52
53
|
costlayers init [--repo <path>]
|
|
53
54
|
costlayers scan [--repo <path>] [--price-per-1m <usd>] [--tasks <n>] [--runs-per-week <n>]
|
|
54
|
-
costlayers start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [--runs-per-week <n>] [-- <agent command>]
|
|
55
|
+
costlayers start [--email <email>] [--provider-url <url>] [--mode measure|reduce] [--runs-per-week <n>] [--codex-proxy] [-- <agent command>]
|
|
55
56
|
costlayers signup [--email <email>] [--engine-url <url>]
|
|
56
|
-
costlayers codex-profile [--repo <path>]
|
|
57
|
+
costlayers codex-profile [--repo <path>] [--codex-proxy]
|
|
57
58
|
costlayers connect --engine-url <url> [--api-key <key>]
|
|
58
59
|
costlayers gateway start [--provider-url <url>] [--api-key-env <name>] [--mode measure|reduce]
|
|
59
60
|
costlayers gateway report
|
|
@@ -122,7 +123,7 @@ function guardRepoRoot(repo, args) {
|
|
|
122
123
|
"",
|
|
123
124
|
"Run it inside a project folder instead:",
|
|
124
125
|
" cd path/to/your-repo",
|
|
125
|
-
|
|
126
|
+
` npx -y ${INSTALL_SPEC} start --email you@example.com`,
|
|
126
127
|
"",
|
|
127
128
|
"Or pass --repo path/to/your-repo from anywhere.",
|
|
128
129
|
"If you really intend to scan your whole home directory, add --allow-home.",
|
|
@@ -506,6 +507,7 @@ function printSavingsSummary(report) {
|
|
|
506
507
|
const projection = savingsProjection(report);
|
|
507
508
|
const avoided = Number(report.tokens_avoided_per_repeated_task || 0);
|
|
508
509
|
process.stdout.write(`\n${avoided > 0 ? "CostLayers found repeated context waste" : "CostLayers built your repo context pack"}\n`);
|
|
510
|
+
process.stdout.write(` Evidence: context estimate from local repo scan\n`);
|
|
509
511
|
process.stdout.write(` Tokens avoided per repeated task: ${formatInt(report.tokens_avoided_per_repeated_task)}\n`);
|
|
510
512
|
if (avoided > 0) {
|
|
511
513
|
process.stdout.write(` Before vs after context: ${formatInt(report.baseline_broad_read_tokens)} -> ${formatInt(report.context_pack_tokens)} tokens (${report.estimated_reduction_percent}% less)\n`);
|
|
@@ -513,8 +515,9 @@ function printSavingsSummary(report) {
|
|
|
513
515
|
process.stdout.write(` No repeated-context reduction found yet for this small/simple repo.\n`);
|
|
514
516
|
process.stdout.write(` Context pack size: ${formatInt(report.context_pack_tokens)} tokens from ${formatInt(report.source_tokens_indexed)} indexed source tokens\n`);
|
|
515
517
|
}
|
|
516
|
-
process.stdout.write(`
|
|
517
|
-
process.stdout.write(`
|
|
518
|
+
process.stdout.write(` Estimated waste value per ${formatInt(report.repeated_tasks_modeled)} repeated tasks: ${formatUsd(report.estimated_usd_saved)}\n`);
|
|
519
|
+
process.stdout.write(` Usage-stretch estimate at ${formatInt(projection.runsPerWeek)} agent runs/week: ${formatUsd(projection.weeklyUsd)}/week, ${formatUsd(projection.monthlyUsd)}/month\n`);
|
|
520
|
+
process.stdout.write(` Invoice savings require API traffic through CostLayers invoice mode.\n`);
|
|
518
521
|
process.stdout.write(` Source tokens indexed: ${formatInt(report.source_tokens_indexed)}\n`);
|
|
519
522
|
process.stdout.write(` Compact repo pack: ${formatInt(report.context_pack_tokens)} tokens\n`);
|
|
520
523
|
}
|
|
@@ -523,25 +526,60 @@ function codexHomeDir() {
|
|
|
523
526
|
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
524
527
|
}
|
|
525
528
|
|
|
526
|
-
function
|
|
529
|
+
function codexProxyEnabled(args = {}) {
|
|
530
|
+
return Boolean(args["codex-proxy"] || args["proxy-codex"]);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function codexProxyApiKeyEnv(args = {}) {
|
|
534
|
+
return String(args["api-key-env"] || "OPENAI_API_KEY");
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function assertCodexProxyApiKey(args = {}) {
|
|
538
|
+
if (!codexProxyEnabled(args)) return;
|
|
539
|
+
const keyEnv = codexProxyApiKeyEnv(args);
|
|
540
|
+
if (process.env[keyEnv]) return;
|
|
541
|
+
process.stderr.write([
|
|
542
|
+
`CostLayers invoice mode needs ${keyEnv} in this shell.`,
|
|
543
|
+
"",
|
|
544
|
+
"Set an OpenAI Platform API key with Responses API write permission, then rerun:",
|
|
545
|
+
` export ${keyEnv}=sk-proj-...`,
|
|
546
|
+
` npx -y ${INSTALL_SPEC} start --email you@example.com --codex-proxy -- codex`,
|
|
547
|
+
"",
|
|
548
|
+
"ChatGPT-login Codex can be metered, but it cannot produce provider invoice savings because there is no per-request Platform invoice to reduce.",
|
|
549
|
+
""
|
|
550
|
+
].join("\n"));
|
|
551
|
+
process.exit(2);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function profileTomlString(connection, args = {}) {
|
|
527
555
|
const gateway = String(connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key)).replace(/\/+$/, "");
|
|
528
556
|
const baseUrl = `${gateway}/v1`;
|
|
529
557
|
const engineUrl = String(connection.engine_url || "https://costlayers.com/engine").replace(/\/+$/, "");
|
|
530
|
-
|
|
531
|
-
|
|
558
|
+
const apiKeyEnv = codexProxyApiKeyEnv(args);
|
|
559
|
+
const lines = [
|
|
560
|
+
"# Generated by CostLayers. This profile sends Codex telemetry to the CostLayers meter.",
|
|
532
561
|
"# Keep this file private because it contains your keyed CostLayers endpoint.",
|
|
533
|
-
"",
|
|
562
|
+
"# Default mode preserves Codex's native model provider/auth, which works with ChatGPT-login Codex.",
|
|
563
|
+
""
|
|
564
|
+
];
|
|
565
|
+
if (codexProxyEnabled(args)) {
|
|
566
|
+
lines.push(
|
|
567
|
+
"# Proxy mode routes Codex model calls through the CostLayers gateway.",
|
|
568
|
+
`# It uses ${apiKeyEnv} from your shell and requires OpenAI Platform Responses API write scope.`,
|
|
569
|
+
"",
|
|
534
570
|
'model_provider = "costlayers"',
|
|
535
571
|
"",
|
|
536
572
|
"[model_providers.costlayers]",
|
|
537
573
|
'name = "CostLayers"',
|
|
538
574
|
`base_url = ${JSON.stringify(baseUrl)}`,
|
|
539
575
|
'wire_api = "responses"',
|
|
540
|
-
|
|
541
|
-
|
|
576
|
+
`env_key = ${JSON.stringify(apiKeyEnv)}`,
|
|
577
|
+
""
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
lines.push(
|
|
542
581
|
"[otel]",
|
|
543
582
|
'environment = "costlayers"',
|
|
544
|
-
'exporter = "otlp-http"',
|
|
545
583
|
"log_user_prompt = false",
|
|
546
584
|
"",
|
|
547
585
|
'[otel.exporter."otlp-http"]',
|
|
@@ -551,14 +589,15 @@ function profileTomlString(connection) {
|
|
|
551
589
|
'[otel.exporter."otlp-http".headers]',
|
|
552
590
|
`"x-costlayers-key" = ${JSON.stringify(connection.api_key || "")}`,
|
|
553
591
|
""
|
|
554
|
-
|
|
592
|
+
);
|
|
593
|
+
return lines.join("\n");
|
|
555
594
|
}
|
|
556
595
|
|
|
557
|
-
function writeCodexProfile(connection) {
|
|
596
|
+
function writeCodexProfile(connection, args = {}) {
|
|
558
597
|
const dir = codexHomeDir();
|
|
559
598
|
ensureDir(dir);
|
|
560
599
|
const file = path.join(dir, "costlayers.config.toml");
|
|
561
|
-
fs.writeFileSync(file, profileTomlString(connection), "utf8");
|
|
600
|
+
fs.writeFileSync(file, profileTomlString(connection, args), "utf8");
|
|
562
601
|
return file;
|
|
563
602
|
}
|
|
564
603
|
|
|
@@ -626,9 +665,10 @@ async function signup(repo, args) {
|
|
|
626
665
|
|
|
627
666
|
async function codexProfile(repo, args) {
|
|
628
667
|
const connection = await ensureConnection(repo, args);
|
|
629
|
-
const profilePath = writeCodexProfile(connection);
|
|
668
|
+
const profilePath = writeCodexProfile(connection, args);
|
|
630
669
|
process.stdout.write(`CostLayers Codex profile installed\n`);
|
|
631
670
|
process.stdout.write(`Profile: ${profilePath}\n`);
|
|
671
|
+
process.stdout.write(`Mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode, native Codex provider preserved"}\n`);
|
|
632
672
|
process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
633
673
|
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
634
674
|
process.stdout.write(`Run: codex --profile costlayers\n`);
|
|
@@ -748,10 +788,12 @@ async function runAgent(repo, args, argv, options = {}) {
|
|
|
748
788
|
process.stdout.write(`Runtime plan: ${path.join(outDir, "runtime-plan.md")}\n`);
|
|
749
789
|
let commandToRun = command;
|
|
750
790
|
if (connection && connection.engine_url && isCodexCommand(command)) {
|
|
751
|
-
|
|
791
|
+
assertCodexProxyApiKey(args);
|
|
792
|
+
const profilePath = writeCodexProfile(connection, args);
|
|
752
793
|
commandToRun = withCostLayersCodexProfile(command);
|
|
753
794
|
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
754
795
|
process.stdout.write(`Codex metering enabled: ${commandToRun.join(" ")}\n`);
|
|
796
|
+
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
755
797
|
process.stdout.write(`Savings dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
756
798
|
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
757
799
|
}
|
|
@@ -828,19 +870,22 @@ async function dashboard(repo, args) {
|
|
|
828
870
|
process.stdout.write(`URL: ${dashboardUrl}\n`);
|
|
829
871
|
process.stdout.write(`status: ${status.status || "unknown"}\n`);
|
|
830
872
|
process.stdout.write(`plan: ${status.free_beta ? "free beta" : "metered"}\n`);
|
|
831
|
-
process.stdout.write(`
|
|
832
|
-
process.stdout.write(`
|
|
873
|
+
process.stdout.write(`api_invoice_savings_usd: ${status.metered_savings_usd ?? ""}\n`);
|
|
874
|
+
process.stdout.write(`chatgpt_codex_usage_stretch_usd: ${status.codex_metered_savings_usd ?? ""}\n`);
|
|
833
875
|
process.stdout.write(`codex_meter_total_tokens: ${status.codex_meter_total_tokens ?? 0}\n`);
|
|
834
|
-
process.stdout.write(`
|
|
876
|
+
process.stdout.write(`context_waste_estimate_usd: ${status.context_savings_usd ?? ""}\n`);
|
|
835
877
|
process.stdout.write(`context_tokens_avoided_per_task: ${status.context_tokens_avoided_per_task ?? 0}\n`);
|
|
836
878
|
process.stdout.write(`gateway_requests: ${status.gateway_request_count ?? 0}\n`);
|
|
837
879
|
process.stdout.write(`blocked_requests: ${status.blocked_request_count ?? 0}\n`);
|
|
838
880
|
process.stdout.write(`rate_limit_per_minute: ${status.rate_limit_per_minute ?? ""}\n`);
|
|
881
|
+
process.stdout.write(`claim: API invoice savings only count when provider API traffic is routed through CostLayers. ChatGPT-login Codex shows usage stretch, not invoice reduction.\n`);
|
|
839
882
|
}
|
|
840
883
|
|
|
841
884
|
async function start(repo, args, argv) {
|
|
842
885
|
const dash = argv.indexOf("--");
|
|
843
886
|
const command = dash >= 0 ? argv.slice(dash + 1) : [];
|
|
887
|
+
const codexTelemetryRun = command.length > 0 && isCodexCommand(command) && !codexProxyEnabled(args);
|
|
888
|
+
if (command.length > 0 && isCodexCommand(command)) assertCodexProxyApiKey(args);
|
|
844
889
|
init(repo);
|
|
845
890
|
process.stdout.write(`Scanning repo: ${repo}\n`);
|
|
846
891
|
const precomputed = scanToFiles(repo, args);
|
|
@@ -856,37 +901,49 @@ async function start(repo, args, argv) {
|
|
|
856
901
|
process.stderr.write(`Dashboard sync delayed; local report is still available: ${err.message}\n`);
|
|
857
902
|
}
|
|
858
903
|
process.stdout.write(`CostLayers connection ready\n`);
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
904
|
+
let gatewayBaseUrl = connection.gateway_url || defaultPublicGatewayUrl(connection.engine_url, connection.api_key);
|
|
905
|
+
if (codexTelemetryRun) {
|
|
906
|
+
process.stdout.write(`ChatGPT-login Codex mode: native Codex provider preserved; model calls are not routed through CostLayers.\n`);
|
|
907
|
+
process.stdout.write(`What users get: less repeated repo context and a usage-stretch meter. This does not reduce a flat ChatGPT subscription invoice.\n`);
|
|
908
|
+
} else {
|
|
909
|
+
const providerUrl = typeof args["provider-url"] === "string" ? args["provider-url"] : "https://api.openai.com";
|
|
910
|
+
const payload = {
|
|
911
|
+
host: args.host || "127.0.0.1",
|
|
912
|
+
port: Number(args.port || 8788),
|
|
913
|
+
provider_url: providerUrl,
|
|
914
|
+
api_key_env: args["api-key-env"] || "OPENAI_API_KEY",
|
|
915
|
+
mode: args.mode || "reduce",
|
|
916
|
+
dry_run: Boolean(args["dry-run"]),
|
|
917
|
+
public_gateway_url: args["public-gateway-url"] || gatewayBaseUrl
|
|
918
|
+
};
|
|
919
|
+
const result = await postJson(`${connection.engine_url}/v1/gateway/start`, payload, connection.api_key);
|
|
920
|
+
if (!result.ok) {
|
|
921
|
+
process.stderr.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
gatewayBaseUrl = result.base_url || gatewayBaseUrl;
|
|
925
|
+
process.stdout.write(`CostLayers gateway ready: ${gatewayBaseUrl}\n`);
|
|
926
|
+
process.stdout.write(`OpenAI-compatible base URL: ${gatewayBaseUrl}\n`);
|
|
927
|
+
if (codexProxyEnabled(args)) {
|
|
928
|
+
process.stdout.write(`API invoice mode: Codex will use ${codexProxyApiKeyEnv(args)} through the CostLayers gateway.\n`);
|
|
929
|
+
}
|
|
873
930
|
}
|
|
874
|
-
process.stdout.write(`CostLayers gateway ready: ${result.base_url}\n`);
|
|
875
|
-
process.stdout.write(`OpenAI-compatible base URL: ${result.base_url}\n`);
|
|
876
931
|
process.stdout.write(`Dashboard: ${dashboardUrlFromConnection(connection)}\n`);
|
|
877
932
|
process.stdout.write(`Keep this dashboard URL private; it contains your keyed CostLayers path.\n`);
|
|
878
933
|
process.stdout.write(`Plan: free beta\n`);
|
|
879
|
-
const profilePath = writeCodexProfile(connection);
|
|
934
|
+
const profilePath = writeCodexProfile(connection, args);
|
|
880
935
|
process.stdout.write(`CostLayers Codex profile: ${profilePath}\n`);
|
|
936
|
+
process.stdout.write(`Codex profile mode: ${codexProxyEnabled(args) ? "API invoice mode" : "ChatGPT usage-stretch mode; native Codex model path preserved"}\n`);
|
|
881
937
|
if (command.length > 0) {
|
|
882
938
|
return runAgent(repo, args, argv, { skipSetup: true, precomputed });
|
|
883
939
|
}
|
|
884
940
|
process.stdout.write(`\nNext options:\n`);
|
|
885
|
-
process.stdout.write(` Use gateway URL in your model client: ${
|
|
886
|
-
process.stdout.write(` Run Codex metered: npx -y
|
|
941
|
+
process.stdout.write(` Use gateway URL in your model client: ${gatewayBaseUrl}\n`);
|
|
942
|
+
process.stdout.write(` Run Codex metered: npx -y ${INSTALL_SPEC} start --email you@example.com -- codex\n`);
|
|
943
|
+
process.stdout.write(` Reduce an OpenAI API invoice: export OPENAI_API_KEY=sk-proj-... && npx -y ${INSTALL_SPEC} start --email you@example.com --codex-proxy -- codex\n`);
|
|
887
944
|
process.stdout.write(` Or run Codex directly: codex --profile costlayers\n`);
|
|
888
|
-
process.stdout.write(` View
|
|
889
|
-
process.stdout.write(` Dashboard: npx -y
|
|
945
|
+
process.stdout.write(` View report: npx -y ${INSTALL_SPEC} gateway report\n`);
|
|
946
|
+
process.stdout.write(` Dashboard: npx -y ${INSTALL_SPEC} dashboard\n`);
|
|
890
947
|
}
|
|
891
948
|
|
|
892
949
|
function doctor() {
|
package/package.json
CHANGED