pi-provider-qoder-cn 0.2.1 → 0.2.3

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 (3) hide show
  1. package/README.md +54 -11
  2. package/dist/index.js +201 -37
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -64,14 +64,18 @@ export QODERCN_PERSONAL_ACCESS_TOKEN="<your PAT>"
64
64
 
65
65
  ### `/qoder-cn.usage`
66
66
 
67
- Shows the plan allowance and any purchased add-on credits.
67
+ Shows the plan allowance and any purchased add-on credits as one aligned grid.
68
+ In a terminal the gauge is coloured by how much of the allowance is gone —
69
+ green under 60%, yellow to 85%, red above that, and bold red once the quota is
70
+ exceeded.
68
71
 
69
72
  ```text
70
73
  Qoder CN Plan (personal_standard)
71
- [--------------------] 0% used
72
- [--------------------] Add-on quota: 0 credits / 700 credits used (0%) · 700 credits left
73
- Resets: never
74
- Manage: https://qoder.com.cn/account/usage
74
+ Overall [███░░░░░░░░░░░░░░░░░░] 12.5% used
75
+ Plan quota [███░░░░░░░░░░░░░░░░░░] 250 / 2000 credits 12.5% 1750 left
76
+ Add-on [░░░░░░░░░░░░░░░░░░░░░] 0 / 700 credits 0% 700 left
77
+ Resets never
78
+ Manage https://qoder.com.cn/account/usage
75
79
  ```
76
80
 
77
81
  Append `json` to print the untouched API payload — useful when a field you need
@@ -81,13 +85,24 @@ is not in the formatted view:
81
85
  /qoder-cn.usage json
82
86
  ```
83
87
 
88
+ Append `plain` for the same grid without the colour codes, e.g. when pasting it
89
+ into an issue:
90
+
91
+ ```text
92
+ /qoder-cn.usage plain
93
+ ```
94
+
84
95
  Notes:
85
96
 
86
- - The plan allowance (`Plan quota`) and top-up credits (`Add-on quota`) are
97
+ - The plan allowance (`Plan quota`) and top-up credits (`Add-on`) are
87
98
  independent buckets with independent expiry, so both are listed when present.
88
- - `Resets: never` means the API reported the year-9999 sentinel, i.e. the
99
+ Columns are sized to the rows that came back, so a single-bucket plan does
100
+ not reserve space for one it does not have.
101
+ - Colour is suppressed automatically for a `NO_COLOR` environment, a dumb
102
+ terminal, and any non-TTY output that is not the interactive UI.
103
+ - `Resets never` means the API reported the year-9999 sentinel, i.e. the
89
104
  allowance does not reset on a schedule.
90
- - `(plan quota is prorated)` appears after a mid-cycle plan change.
105
+ - `Note plan quota is prorated` appears after a mid-cycle plan change.
91
106
  - The command uses the same stored token as chat requests, refreshing it first,
92
107
  so it reports the same quota the model is actually billed against.
93
108
 
@@ -123,11 +138,39 @@ at startup is:
123
138
 
124
139
  1. `QODER_VPC_ENDPOINT` / `QODERCN_VPC_ENDPOINT`
125
140
  2. `~/.pi/agent/qoder-cn-settings.json`
126
- 3. `~/.pi/agent/auth.json` (the `qoder-cn` entry)
127
- 4. `~/.qoder-cn/settings.json` (the Qoder IDE's own file)
141
+
142
+ Only these two count as chosen by you. This extension deliberately ignores
143
+ endpoint values written by other Qoder clients — pi is one client among several,
144
+ and another client's endpoint must not silently redirect pi's traffic.
128
145
 
129
146
  > The token and the endpoint must belong to the same instance. Pointing an
130
- > enterprise endpoint at a public-gateway token fails authentication.
147
+ > enterprise endpoint at a public-gateway token fails authentication, which is
148
+ > why `/login` settles the endpoint before it asks for the PAT.
149
+
150
+ ### Endpoint choice during `/login`
151
+
152
+ `/login qoder-cn` asks which endpoint to use **before** prompting for the PAT,
153
+ because the enterprise gateway also serves the token exchange. The list shows
154
+ the currently saved endpoint and defaults to the personal account:
155
+
156
+ ```text
157
+ Choose the Qoder CN endpoint (default: personal account)
158
+ → Personal account — public gateway (qoder.com.cn)
159
+ Enterprise account — use a VPC endpoint
160
+ Enterprise account — <detected value> (detected: stored credential)
161
+ ```
162
+
163
+ - **Personal account** uses the public gateway and pins that choice, so a value
164
+ left behind by another client cannot resurface. If `QODER_VPC_ENDPOINT` /
165
+ `QODERCN_VPC_ENDPOINT` is set, it still wins — `/login` says so and warns
166
+ that a personal pick reverts on restart until the variable is removed.
167
+ - **Enterprise account** then prompts for the instance name or domain.
168
+ - Switching back and forth is expected; re-running `/login` presents the saved
169
+ endpoint so you can change it.
170
+
171
+ A `vpc_endpoint` found in `~/.pi/agent/auth.json` is offered as the third option
172
+ but never applied on its own. `/qoder-endpoint` with no argument also reports it
173
+ under “Detected (not applied)”.
131
174
 
132
175
  ## Models
133
176
 
package/dist/index.js CHANGED
@@ -268,21 +268,14 @@ function readVpcEndpointFromAuth() {
268
268
  }
269
269
  return void 0;
270
270
  }
271
- function readVpcEndpointFromQoderIde() {
272
- try {
273
- const path = join2(getHomeDir2(), ".qoder-cn", "settings.json");
274
- if (!existsSync2(path)) return void 0;
275
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
276
- const value = parsed?.vpc_endpoint ?? parsed?.vpcEndpoint ?? parsed?.vpcInstanceName;
277
- if (value !== void 0 && value !== "") return String(value);
278
- } catch {
279
- }
280
- return void 0;
271
+ function readInheritedVpcEndpoint() {
272
+ const value = readVpcEndpointFromAuth();
273
+ return value ? { value, source: "stored credential" } : void 0;
281
274
  }
282
275
  function readQoderVpcEndpoint() {
283
276
  const fromEnv = process.env.QODER_VPC_ENDPOINT || process.env.QODERCN_VPC_ENDPOINT;
284
277
  if (fromEnv) return fromEnv;
285
- return readVpcEndpointFromSettings() ?? readVpcEndpointFromAuth() ?? readVpcEndpointFromQoderIde() ?? "";
278
+ return readVpcEndpointFromSettings() ?? "";
286
279
  }
287
280
  function isResetValue(value) {
288
281
  const lowered = value.toLowerCase();
@@ -1217,6 +1210,15 @@ import crypto2 from "node:crypto";
1217
1210
  function getPrompt(callbacks) {
1218
1211
  return callbacks.onPrompt;
1219
1212
  }
1213
+ function getSelect(callbacks) {
1214
+ const fn = callbacks.onSelect;
1215
+ return typeof fn === "function" ? fn : void 0;
1216
+ }
1217
+ function isLoginCancellation(error, callbacks) {
1218
+ if (getSignal(callbacks)?.aborted) return true;
1219
+ const message = error instanceof Error ? error.message : String(error);
1220
+ return message.toLowerCase().includes("cancel");
1221
+ }
1220
1222
  function getProgress(callbacks) {
1221
1223
  return callbacks.onProgress;
1222
1224
  }
@@ -1240,8 +1242,90 @@ function parseExpiresAt(s, expiresInSeconds) {
1240
1242
  }
1241
1243
  return Date.now() + 30 * 24 * 60 * 60 * 1e3;
1242
1244
  }
1245
+ async function confirmCnEndpoint(callbacks, mode) {
1246
+ if (mode !== "cn") return;
1247
+ const current = getQoderCNEndpoints();
1248
+ const inherited = readInheritedVpcEndpoint();
1249
+ const envNames = ["QODER_VPC_ENDPOINT", "QODERCN_VPC_ENDPOINT"].filter((name) => process.env[name]);
1250
+ const personalId = "personal";
1251
+ const enterpriseId = "enterprise";
1252
+ const inheritedId = "enterprise-inherited";
1253
+ const options = [
1254
+ { id: personalId, label: "Personal account \u2014 public gateway (qoder.com.cn)" },
1255
+ { id: enterpriseId, label: "Enterprise account \u2014 use a VPC endpoint" }
1256
+ ];
1257
+ if (inherited) {
1258
+ options.push({
1259
+ id: inheritedId,
1260
+ label: `Enterprise account \u2014 ${inherited.value} (detected: ${inherited.source})`
1261
+ });
1262
+ }
1263
+ let message = current.isDefault ? "Choose the Qoder CN endpoint to log in against (default: personal account)" : `Choose the Qoder CN endpoint to log in against (current: ${current.raw})`;
1264
+ if (envNames.length > 0) {
1265
+ message += `
1266
+ (current value comes from ${envNames.join(" / ")}; it still wins after a restart)`;
1267
+ }
1268
+ const choice = await askEndpointChoice(callbacks, message, options);
1269
+ if (choice === personalId) {
1270
+ setQoderCNEndpoint("");
1271
+ if (envNames.length > 0) {
1272
+ getProgress(callbacks)?.(
1273
+ `Personal gateway selected. However, ${envNames.join(" / ")} takes precedence and restores the enterprise endpoint after a restart; remove the variable, then log in again.`
1274
+ );
1275
+ }
1276
+ return;
1277
+ }
1278
+ if (choice === inheritedId && inherited) {
1279
+ setQoderCNEndpoint(inherited.value);
1280
+ return;
1281
+ }
1282
+ if (choice === enterpriseId) {
1283
+ const prompt = getPrompt(callbacks);
1284
+ const raw = await prompt({
1285
+ message: "Enterprise VPC endpoint (instance name or domain)",
1286
+ placeholder: "acme",
1287
+ allowEmpty: true
1288
+ });
1289
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
1290
+ const value = raw?.trim();
1291
+ if (!value) return;
1292
+ setQoderCNEndpoint(value);
1293
+ return;
1294
+ }
1295
+ }
1296
+ async function askEndpointChoice(callbacks, message, options) {
1297
+ const select = getSelect(callbacks);
1298
+ if (select) {
1299
+ try {
1300
+ const picked = await select({ message, options });
1301
+ if (picked) return picked;
1302
+ return void 0;
1303
+ } catch (error) {
1304
+ if (isLoginCancellation(error, callbacks)) throw error;
1305
+ }
1306
+ }
1307
+ const prompt = getPrompt(callbacks);
1308
+ const lines = options.map((option, index) => `${index + 1}. ${option.label}`);
1309
+ const raw = await prompt({
1310
+ message: `${message}
1311
+ ${lines.join("\n")}
1312
+ Enter a number (Enter = 1, personal account)`,
1313
+ placeholder: "1",
1314
+ allowEmpty: true
1315
+ });
1316
+ if (getSignal(callbacks)?.aborted) throw new Error("Login cancelled");
1317
+ const trimmed = raw?.trim();
1318
+ if (!trimmed) return options[0]?.id;
1319
+ const byIndex = Number.parseInt(trimmed, 10);
1320
+ if (!Number.isNaN(byIndex) && byIndex >= 1 && byIndex <= options.length) {
1321
+ return options[byIndex - 1]?.id;
1322
+ }
1323
+ const byId = options.find((option) => option.id === trimmed || option.label === trimmed);
1324
+ return byId?.id ?? options[0]?.id;
1325
+ }
1243
1326
  async function interactiveLogin(callbacks, mode) {
1244
1327
  const region = getQoderRegionConfig(mode);
1328
+ await confirmCnEndpoint(callbacks, mode);
1245
1329
  const prompt = getPrompt(callbacks);
1246
1330
  const pat = await prompt({
1247
1331
  message: !region.supportsBrowserLogin ? "Paste a Qoder CN Personal Access Token, or leave empty to cancel" : "Paste a Qoder Personal Access Token (pt-...), or leave empty for browser login",
@@ -1377,6 +1461,7 @@ var init_login = __esm({
1377
1461
  "use strict";
1378
1462
  init_cosy();
1379
1463
  init_region();
1464
+ init_vpc();
1380
1465
  init_pat();
1381
1466
  }
1382
1467
  });
@@ -1685,10 +1770,13 @@ function describeEndpoint() {
1685
1770
  async function runEndpointCommand(args, ctx) {
1686
1771
  const input = (args || "").trim();
1687
1772
  if (!input) {
1773
+ const inherited = readInheritedVpcEndpoint();
1774
+ const inheritedHint = inherited ? `
1775
+ Detected (not applied): ${inherited.value} from ${inherited.source} \u2014 run /qoder-endpoint ${inherited.value} to use it.` : "";
1688
1776
  ctx?.ui?.notify(
1689
1777
  `Current Qoder CN endpoint: ${describeEndpoint()}
1690
1778
  To set: /qoder-endpoint <domain>
1691
- To reset: /qoder-endpoint default`,
1779
+ To reset: /qoder-endpoint default${inheritedHint}`,
1692
1780
  "info"
1693
1781
  );
1694
1782
  return;
@@ -1719,11 +1807,37 @@ To reset: /qoder-endpoint default`,
1719
1807
 
1720
1808
  // src/commands/usage.ts
1721
1809
  init_region();
1722
- function usageBar(percentage, width = 20) {
1723
- if (typeof percentage !== "number" || !Number.isFinite(percentage)) return `[${"?".repeat(width)}]`;
1724
- const ratio = Math.max(0, Math.min(1, percentage / 100));
1725
- const filled = Math.max(0, Math.min(width, Math.round(ratio * width)));
1726
- return `[${"#".repeat(filled)}${"-".repeat(width - filled)}]`;
1810
+ var ANSI = {
1811
+ reset: "\x1B[0m",
1812
+ bold: "\x1B[1m",
1813
+ dim: "\x1B[2m",
1814
+ red: "\x1B[31m",
1815
+ green: "\x1B[32m",
1816
+ yellow: "\x1B[33m"
1817
+ };
1818
+ var FILLED_CHAR = "\u2588";
1819
+ var EMPTY_CHAR = "\u2591";
1820
+ var UNKNOWN_CHAR = "?";
1821
+ var BAR_WIDTH = 20;
1822
+ var COLUMN_GAP = " ";
1823
+ function paint(text, code) {
1824
+ return code && text ? `${code}${text}${ANSI.reset}` : text;
1825
+ }
1826
+ function usageColor(percentage, danger = false) {
1827
+ if (danger) return `${ANSI.red}${ANSI.bold}`;
1828
+ if (typeof percentage !== "number" || !Number.isFinite(percentage)) return void 0;
1829
+ if (percentage >= 85) return `${ANSI.red}${ANSI.bold}`;
1830
+ if (percentage >= 60) return ANSI.yellow;
1831
+ return ANSI.green;
1832
+ }
1833
+ function usageBar(percentage, options = {}) {
1834
+ const width = options.width ?? BAR_WIDTH;
1835
+ const known = typeof percentage === "number" && Number.isFinite(percentage);
1836
+ const filled = known ? Math.max(0, Math.min(width, Math.round(percentage / 100 * width))) : 0;
1837
+ const fill = known ? FILLED_CHAR.repeat(filled) : "";
1838
+ const rest = known ? EMPTY_CHAR.repeat(width - filled) : UNKNOWN_CHAR.repeat(width);
1839
+ if (!options.color) return `[${fill}${rest}]`;
1840
+ return paint("[", ANSI.dim) + paint(fill, usageColor(percentage, options.danger)) + paint(rest, ANSI.dim) + paint("]", ANSI.dim);
1727
1841
  }
1728
1842
  function formatAmount(value, unit) {
1729
1843
  if (typeof value !== "number" || !Number.isFinite(value)) return "?";
@@ -1748,41 +1862,82 @@ function formatResetTime(expiresAt, now = Date.now()) {
1748
1862
  const relative = days > 0 ? `${days}d ${hours}h` : hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
1749
1863
  return `${new Date(timestamp).toISOString().slice(0, 10)} (in ${relative})`;
1750
1864
  }
1751
- function formatBucketLine(label, bucket) {
1752
- const unit = bucket?.unit || "credits";
1865
+ function isBucketRow(row) {
1866
+ return typeof row.used === "number" || typeof row.total === "number";
1867
+ }
1868
+ function rightCell(text, width, code) {
1869
+ return " ".repeat(Math.max(0, width - text.length)) + paint(text, code);
1870
+ }
1871
+ function renderUsageRows(rows, notes, color) {
1872
+ const buckets = rows.filter(isBucketRow);
1873
+ const usedWidth = Math.max(0, ...buckets.map((row) => formatAmount(row.used).length));
1874
+ const totalWidth = Math.max(0, ...buckets.map((row) => formatAmount(row.total).length));
1875
+ const unitWidth = Math.max(0, ...buckets.map((row) => (row.unit ?? "").length));
1876
+ const remainWidth = Math.max(0, ...buckets.map((row) => formatAmount(row.remaining).length));
1877
+ const labelWidth = Math.max(0, ...rows.map((row) => row.label.length), ...notes.map((note) => note.label.length));
1878
+ const percentWidth = Math.max(0, ...rows.map((row) => row.percent === void 0 ? 1 : `${row.percent}%`.length));
1879
+ const amountWidth = buckets.length > 0 ? usedWidth + 3 + totalWidth + 1 + unitWidth : 0;
1880
+ const remainder = (row) => isBucketRow(row) ? `${formatAmount(row.remaining).padStart(remainWidth)} left` : row.tail ?? "";
1881
+ const remainderWidth = Math.max(0, ...rows.map((row) => remainder(row).length));
1882
+ const lines = rows.map((row) => {
1883
+ const amount = isBucketRow(row) ? `${formatAmount(row.used).padStart(usedWidth)} / ${formatAmount(row.total).padStart(totalWidth)} ${(row.unit ?? "").padEnd(unitWidth)}` : " ".repeat(amountWidth);
1884
+ const percent = row.percent === void 0 ? UNKNOWN_CHAR : `${row.percent}%`;
1885
+ return [
1886
+ row.label.padEnd(labelWidth),
1887
+ usageBar(row.percent, { color, danger: row.danger }),
1888
+ amount,
1889
+ rightCell(percent, percentWidth, color ? usageColor(row.percent, row.danger) : void 0),
1890
+ remainder(row).padEnd(remainderWidth)
1891
+ ].join(COLUMN_GAP);
1892
+ });
1893
+ for (const note of notes) {
1894
+ lines.push(`${note.label.padEnd(labelWidth)}${COLUMN_GAP}${color ? paint(note.value, note.color) : note.value}`);
1895
+ }
1896
+ return lines.map((line) => line.replace(/ +$/, ""));
1897
+ }
1898
+ function bucketRow(label, bucket, danger) {
1753
1899
  const rawPercent = bucket?.percentage;
1754
1900
  const derivedPercent = typeof bucket?.used === "number" && typeof bucket?.total === "number" && bucket.total > 0 ? bucket.used / bucket.total * 100 : void 0;
1755
1901
  const percent = formatPercent(
1756
1902
  typeof rawPercent === "number" && Number.isFinite(rawPercent) ? rawPercent : derivedPercent
1757
1903
  );
1758
- const percentText = percent === void 0 ? "?" : `${percent}%`;
1759
- return ` ${usageBar(percent)} ${label}: ${formatAmount(bucket?.used, unit)} / ${formatAmount(
1760
- bucket?.total,
1761
- unit
1762
- )} used (${percentText}) \xB7 ${formatAmount(bucket?.remaining, unit)} left`;
1904
+ return {
1905
+ label,
1906
+ percent,
1907
+ used: bucket?.used,
1908
+ total: bucket?.total,
1909
+ remaining: bucket?.remaining,
1910
+ unit: bucket?.unit || "credits",
1911
+ danger
1912
+ };
1763
1913
  }
1764
1914
  function hasBucket(bucket) {
1765
1915
  if (!bucket) return false;
1766
1916
  return (bucket.total ?? 0) > 0 || (bucket.used ?? 0) > 0;
1767
1917
  }
1768
- function formatQoderUsage(raw, mode, now = Date.now()) {
1918
+ function formatQoderUsage(raw, mode, now = Date.now(), options = {}) {
1919
+ const color = options.color ?? false;
1769
1920
  const region = getQoderRegionConfig(mode);
1770
- const lines = [];
1921
+ const danger = Boolean(raw?.isQuotaExceeded);
1771
1922
  const userType = raw?.userType ? ` (${raw.userType})` : "";
1772
1923
  const overallPercent = formatPercent(raw?.totalUsagePercentage);
1773
- lines.push(`${region.usageTitle}${userType}`);
1774
- lines.push(`${usageBar(overallPercent)} ${overallPercent === void 0 ? "?" : `${overallPercent}%`} used`);
1775
- if (raw?.isQuotaExceeded) lines.push(" ! quota exceeded");
1924
+ const rows = [{ label: "Overall", percent: overallPercent, tail: "used", danger }];
1776
1925
  const showUserQuota = hasBucket(raw?.userQuota);
1777
1926
  const showAddOnQuota = hasBucket(raw?.addOnQuota);
1778
- if (showUserQuota) lines.push(formatBucketLine("Plan quota", raw.userQuota));
1779
- if (showAddOnQuota) lines.push(formatBucketLine("Add-on quota", raw.addOnQuota));
1780
- if (!showUserQuota && !showAddOnQuota) lines.push(" No quota buckets returned.");
1781
- if (raw?.isPlanQuotaProrated) lines.push(" (plan quota is prorated)");
1782
- lines.push(`Resets: ${formatResetTime(raw?.expiresAt, now)}`);
1927
+ if (showUserQuota) rows.push(bucketRow("Plan quota", raw.userQuota, danger));
1928
+ if (showAddOnQuota) rows.push(bucketRow("Add-on", raw.addOnQuota, danger));
1929
+ const notes = [];
1930
+ if (danger) notes.push({ label: "Status", value: "quota exceeded", color: ANSI.red });
1931
+ if (!showUserQuota && !showAddOnQuota) notes.push({ label: "Buckets", value: "none returned" });
1932
+ if (raw?.isPlanQuotaProrated) notes.push({ label: "Note", value: "plan quota is prorated" });
1933
+ notes.push({ label: "Resets", value: formatResetTime(raw?.expiresAt, now) });
1783
1934
  const manageUrl = raw?.addOnQuota?.detailUrl || raw?.upgradeUrl || region.manageUrl;
1784
- if (manageUrl) lines.push(`Manage: ${manageUrl}`);
1785
- return { title: region.usageTitle, lines };
1935
+ if (manageUrl) notes.push({ label: "Manage", value: manageUrl });
1936
+ const title = `${region.usageTitle}${userType}`;
1937
+ return {
1938
+ title: region.usageTitle,
1939
+ lines: [paint(title, color ? ANSI.bold : void 0), ...renderUsageRows(rows, notes, color)]
1940
+ };
1786
1941
  }
1787
1942
  async function fetchQoderQuota(accessToken, mode) {
1788
1943
  const response = await fetch(getQoderUsageURL(mode), {
@@ -1819,6 +1974,14 @@ async function resolveAccessToken(providerID, ctx) {
1819
1974
  return getCachedCredentials2("", providerID)?.access || void 0;
1820
1975
  }
1821
1976
  var FORMAT_ARGS = /* @__PURE__ */ new Set(["json", "raw", "debug"]);
1977
+ var PLAIN_ARGS = /* @__PURE__ */ new Set(["plain", "no-color", "nocolor"]);
1978
+ function shouldColorize(args, options = {}) {
1979
+ if (PLAIN_ARGS.has((args || "").trim().toLowerCase())) return false;
1980
+ const env = options.env ?? process.env;
1981
+ if (env.NO_COLOR || env.TERM === "dumb" || env.FORCE_COLOR === "0") return false;
1982
+ if (options.hasUI) return true;
1983
+ return options.isTTY ?? Boolean(process.stdout?.isTTY);
1984
+ }
1822
1985
  async function runUsageCommand(mode, args, ctx) {
1823
1986
  const region = getQoderRegionConfig(mode);
1824
1987
  const providerID = region.providerID;
@@ -1830,7 +1993,8 @@ async function runUsageCommand(mode, args, ctx) {
1830
1993
  return;
1831
1994
  }
1832
1995
  const raw = await fetchQoderQuota(accessToken, mode);
1833
- const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode).lines.join("\n");
1996
+ const color = !wantsRaw && shouldColorize(args, { hasUI: Boolean(ctx?.ui) });
1997
+ const output = wantsRaw ? JSON.stringify(raw, null, 2) : formatQoderUsage(raw, mode, Date.now(), { color }).lines.join("\n");
1834
1998
  ctx?.ui?.notify(output, "info");
1835
1999
  if (!ctx?.ui) console.log(output);
1836
2000
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-provider-qoder-cn",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Pi extension for Qoder CN — PAT/OAuth auth, /qoder-cn.usage quota command, and enterprise VPC endpoint support",
5
5
  "type": "module",
6
6
  "license": "MIT",