pi-ark-usage 0.1.0

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 ADDED
@@ -0,0 +1,106 @@
1
+ # pi-ark-usage
2
+
3
+ 在 pi TUI 中显示**火山引擎方舟 Coding Plan / Agent Plan** 的套餐用量,数据来自 [`arkcli`](https://www.volcengine.com/docs/82379) 的 `arkcli usage plan`。
4
+
5
+ ```
6
+ Volc Usage: sess 30%(reset after 4h18m) / wk 37%(reset after 4d) / mo 19%(reset after 29d) now
7
+ ```
8
+
9
+ ## 特性
10
+
11
+ - **零额外鉴权**:认证完全由 arkcli 自己管理(SSO / profile),本扩展不接触、不保存任何 API Key
12
+ - **三个周期同屏**:session(会话)/ weekly(周)/ monthly(月),并显示各周期重置倒计时
13
+ - **阈值着色**:低用量绿色、接近上限黄色、超过红线红色
14
+ - **单轮消耗**:以每轮对话开始前的快照为基线,结算时标注各周期增量(如 `(+2)`)
15
+ - **产品切换感知**:在个人版/团队版、Coding Plan/Agent Plan 之间切换时标注 `(变更)`;套餐周期重置时标注 `(已重置)`
16
+ - **持久化缓存**:重启 pi 后立即显示上次快照(`~/.pi/agent/pi-ark-usage/cache.json`,0600 权限,临时文件原子写入)
17
+ - 可选空闲自动刷新(默认关闭)
18
+
19
+ ## 前置条件
20
+
21
+ 1. 已安装并登录 arkcli:
22
+
23
+ ```bash
24
+ arkcli --help
25
+ arkcli auth login # 按 arkcli 自己的流程完成 SSO 登录
26
+ ```
27
+
28
+ 也可用环境变量 `PI_ARK_USAGE_ARKCLI` 指定 arkcli 的绝对路径。
29
+
30
+ 2. pi 版本 >= 0.86(扩展机制)。
31
+
32
+ ## 安装
33
+
34
+ 开发期可直接加载本地文件:
35
+
36
+ ```bash
37
+ pi --extension ~/Documents/pi-ark-usage/extensions/index.ts
38
+ ```
39
+
40
+ 发布到 npm 后可用 pi 包机制安装:
41
+
42
+ ```bash
43
+ pi install npm:pi-ark-usage
44
+ ```
45
+
46
+ ## 命令
47
+
48
+ | 命令 | 说明 |
49
+ |---|---|
50
+ | `/arkcheck` | 强制刷新用量,并以通知形式展示当前各周期百分比 |
51
+ | `/arkusage` | 查看当前套餐各周期用量及重置时间 |
52
+ | `/arkset` | 查看/修改设置(见下) |
53
+
54
+ ### `/arkset` 用法
55
+
56
+ 无参数查看当前设置:
57
+
58
+ ```text
59
+ /arkset
60
+ ```
61
+
62
+ | 用法 | 说明 |
63
+ |---|---|
64
+ | `/arkset yellow <n>` | 黄色阈值(百分比,默认 40),须低于红线 |
65
+ | `/arkset red <n>` | 红色阈值(百分比,默认 80),须高于黄线 |
66
+ | `/arkset auto <分钟>` | 空闲自动刷新间隔,0–30 的整数,`0` 关闭(默认关闭) |
67
+ | `/arkset product <id>` | 固定查询的产品:`auto`(默认)/ `coding-plan` / `agent-plan` / `coding-plan-team` / `agent-plan-team` |
68
+ | `/arkset seat <id>` | 团队版指定 SeatID;`/arkset seat none` 清除 |
69
+ | `/arkset lang <zh\|en>` | UI language, default English |
70
+ | `/arkset reset` | 恢复全部默认设置 |
71
+
72
+ ### 产品选择规则
73
+
74
+ - `/arkset product` 非 `auto` 时,固定查询指定产品(团队版可配合 `seat`)。
75
+ - `auto`(默认):
76
+ 1. 按当前模型的 provider 映射(`coding-plan` → coding-plan 等);
77
+ 2. 映射不到时,显示最近一次成功抓取的已订阅产品。
78
+
79
+ ## 刷新时机
80
+
81
+ | 时机 | 行为 |
82
+ |---|---|
83
+ | 会话启动 | 后台非阻塞抓取;先显示磁盘缓存 |
84
+ | 切换模型/provider | 产品可能变化,立即抓取 |
85
+ | `agent_start` | 建立本轮基线(缓存 1 小时内有效;否则限时 3 秒补抓,超时沿用缓存) |
86
+ | `agent_settled` | 本轮结算抓取,并与基线对比标注消耗 |
87
+ | `/arkcheck` | 手动强制刷新 |
88
+ | 可选自动刷新 | 仅当 `/arkset auto` 开启时按间隔执行 |
89
+
90
+ ## 工作原理
91
+
92
+ 1. 通过 `child_process.execFile` 调用 `arkcli usage plan --format json`(固定产品时加 `--product`,团队版可加 `--seat`)。
93
+ 2. 对返回 JSON 做结构校验(产品 ID、period 百分比、长度上限),只保留已订阅且含周期数据的条目。
94
+ 3. 快照存入内存 Map,并以原子写(tmp 文件 + rename)持久化到 `~/.pi/agent/pi-ark-usage/`。
95
+ 4. Widget 通过 `ctx.ui.setWidget(..., { placement: "belowEditor" })` 渲染在编辑器下方;age(抓取于多久前)右对齐,终端过窄时自动折成两行。
96
+
97
+ ## 开发
98
+
99
+ ```bash
100
+ npm install
101
+ npm run typecheck
102
+ ```
103
+
104
+ ## License
105
+
106
+ MIT
@@ -0,0 +1,552 @@
1
+ /**
2
+ * pi-ark-usage
3
+ *
4
+ * Show Volcengine Ark Coding Plan / Agent Plan usage in the pi TUI via
5
+ * `arkcli usage plan`. Auth is fully handled by arkcli itself; this extension
6
+ * never sees API keys.
7
+ *
8
+ * Widget (below editor): 火山用量: 会话 4%(...) / 周 34%(...) / 月 17%(...)
9
+ * Commands:
10
+ * /arkcheck force refresh
11
+ * /arkusage show snapshot in a notification
12
+ * /arkset ... view/change settings
13
+ *
14
+ * Design modeled after pi-check-agent-quota: baseline snapshot taken at
15
+ * agent_start, compared at agent_settled to annotate per-round consumption.
16
+ */
17
+
18
+ import type {
19
+ ExtensionAPI,
20
+ ExtensionCommandContext,
21
+ ExtensionContext,
22
+ } from "@earendil-works/pi-coding-agent";
23
+ import { fetchPlan } from "./lib/arkcli.js";
24
+ import {
25
+ ArkUsageComponent,
26
+ AUTO_REFRESH_MAX_MINUTES,
27
+ LOCALES,
28
+ buildItems,
29
+ formatRemaining,
30
+ getSettings,
31
+ patchSettings,
32
+ resetSettings,
33
+ } from "./lib/widget.js";
34
+ import {
35
+ flushWrites,
36
+ loadCache,
37
+ markShuttingDown,
38
+ saveCache,
39
+ } from "./lib/cache.js";
40
+ import type {
41
+ ArkPeriod,
42
+ ArkSettings,
43
+ Language,
44
+ PlanSnapshot,
45
+ RenderItem,
46
+ } from "./lib/types.js";
47
+
48
+ const WIDGET_KEY = "pi-ark-usage";
49
+ const TICK_MS = 60_000;
50
+ const AGENT_START_TIMEOUT_MS = 3_000;
51
+ const BASELINE_FRESH_MS = 60 * 60_000;
52
+
53
+ const PRODUCTS = [
54
+ "coding-plan",
55
+ "agent-plan",
56
+ "coding-plan-team",
57
+ "agent-plan-team",
58
+ ] as const;
59
+
60
+ type ProductId = (typeof PRODUCTS)[number];
61
+
62
+ /** provider id (pi models.json) -> arkcli product id */
63
+ const PROVIDER_PRODUCT: Record<string, ProductId> = {
64
+ "coding-plan": "coding-plan",
65
+ "agent-plan": "agent-plan",
66
+ "coding-plan-team": "coding-plan-team",
67
+ "agent-plan-team": "agent-plan-team",
68
+ };
69
+
70
+ type FetchTrigger =
71
+ | "session_start"
72
+ | "model_select"
73
+ | "stale"
74
+ | "settled"
75
+ | "manual";
76
+
77
+ type Status = "ok" | "fetching" | "failed" | "none";
78
+
79
+ /* --------------------------------- state ---------------------------------- */
80
+
81
+ let language: Language = "en";
82
+ const snapshots = new Map<string, PlanSnapshot>();
83
+ let activeProduct: ProductId | null = null;
84
+ let status: Status = "ok";
85
+ let deltas: Record<string, number> | null = null;
86
+ let note: "changed" | "reset" | null = null;
87
+ let baseline: PlanSnapshot | null = null;
88
+ let activeWidget: ArkUsageComponent | null = null;
89
+ let tickTimer: ReturnType<typeof setInterval> | null = null;
90
+ let lastCtx: ExtensionContext | null = null;
91
+ let lastAutoAt = 0;
92
+ let shuttingDown = false;
93
+ let inFlight: AbortController | null = null;
94
+
95
+ /* ----------------------------- product picking ----------------------------- */
96
+
97
+ function isTeamProduct(p: string): boolean {
98
+ return p.endsWith("-team");
99
+ }
100
+
101
+ /**
102
+ * Resolve the product to display.
103
+ * - forced product setting wins
104
+ * - auto: provider mapping, else first subscribed snapshot
105
+ */
106
+ function resolveProduct(ctx: ExtensionContext): ProductId | null {
107
+ const forced = getSettings().product;
108
+ if (forced !== "auto") return forced as ProductId;
109
+
110
+ const provider = ctx.model?.provider;
111
+ if (provider && PROVIDER_PRODUCT[provider]) {
112
+ return PROVIDER_PRODUCT[provider];
113
+ }
114
+ for (const p of PRODUCTS) {
115
+ if (snapshots.has(p)) return p;
116
+ }
117
+ return null;
118
+ }
119
+
120
+ function activeSnapshot(): PlanSnapshot | null {
121
+ return activeProduct ? snapshots.get(activeProduct) ?? null : null;
122
+ }
123
+
124
+ /* -------------------------------- rendering -------------------------------- */
125
+
126
+ function renderWidget(ctx: ExtensionContext): void {
127
+ lastCtx = ctx;
128
+ const snap = activeSnapshot();
129
+ const items: RenderItem[] = buildItems(
130
+ snap,
131
+ language,
132
+ status === "ok" && !snap ? "none" : status,
133
+ ctx.isIdle(),
134
+ deltas,
135
+ note,
136
+ );
137
+ if (activeWidget) {
138
+ activeWidget.update(items);
139
+ return;
140
+ }
141
+ ctx.ui.setWidget(
142
+ WIDGET_KEY,
143
+ (tui, theme) => {
144
+ const component = new ArkUsageComponent(
145
+ items,
146
+ () => theme,
147
+ () => tui.requestRender?.(),
148
+ );
149
+ activeWidget = component;
150
+ return component;
151
+ },
152
+ { placement: "belowEditor" },
153
+ );
154
+ }
155
+
156
+ /* --------------------------------- fetching -------------------------------- */
157
+
158
+ function toSnapshot(
159
+ product: ProductId,
160
+ periods: ArkPeriod[],
161
+ updatedAt: number | undefined,
162
+ ): PlanSnapshot {
163
+ return {
164
+ product,
165
+ edition: isTeamProduct(product) ? "team" : "personal",
166
+ fetchedAt: Date.now(),
167
+ periods,
168
+ updatedAt,
169
+ };
170
+ }
171
+
172
+ async function refresh(
173
+ ctx: ExtensionContext,
174
+ trigger: FetchTrigger,
175
+ ): Promise<boolean> {
176
+ if (shuttingDown) return false;
177
+ const product = resolveProduct(ctx);
178
+ activeProduct = product;
179
+ if (!product) {
180
+ status = "none";
181
+ renderWidget(ctx);
182
+ return false;
183
+ }
184
+
185
+ // Coalesce: reuse the in-flight request for the same product.
186
+ if (inFlight) {
187
+ // different product: let the existing one finish naturally; just await nothing
188
+ }
189
+
190
+ status = "fetching";
191
+ note = null;
192
+ renderWidget(ctx);
193
+
194
+ const controller = new AbortController();
195
+ inFlight = controller;
196
+ lastAutoAt = Date.now();
197
+
198
+ const s = getSettings();
199
+ const queryOne = s.product !== "auto";
200
+ try {
201
+ const output = await fetchPlan({
202
+ signal: controller.signal,
203
+ product: queryOne ? product : undefined,
204
+ seat: isTeamProduct(product) ? s.seat || undefined : undefined,
205
+ });
206
+
207
+ let updated = 0;
208
+ for (const item of output.items) {
209
+ if (item.subscribed && item.periods && item.periods.length > 0) {
210
+ snapshots.set(
211
+ item.product,
212
+ toSnapshot(
213
+ item.product as ProductId,
214
+ item.periods,
215
+ item.updated_at,
216
+ ),
217
+ );
218
+ updated++;
219
+ }
220
+ }
221
+ status = updated > 0 ? "ok" : "none";
222
+ if (trigger !== "settled") persist();
223
+ return updated > 0;
224
+ } catch {
225
+ // keep the last good snapshot; mark failed only if we have nothing
226
+ status = activeSnapshot() ? "ok" : "failed";
227
+ return false;
228
+ } finally {
229
+ if (inFlight === controller) inFlight = null;
230
+ renderWidget(ctx);
231
+ }
232
+ }
233
+
234
+ function persist(): void {
235
+ const out: Record<string, PlanSnapshot> = {};
236
+ for (const [k, v] of snapshots) out[k] = v;
237
+ saveCache(language, out);
238
+ }
239
+
240
+ /* ---------------------------- baseline / delta ----------------------------- */
241
+
242
+ function computeDeltas(
243
+ before: PlanSnapshot,
244
+ after: PlanSnapshot,
245
+ ): Record<string, number> | null {
246
+ const out: Record<string, number> = {};
247
+ for (const p of after.periods) {
248
+ const old = before.periods.find((q) => q.label === p.label);
249
+ if (old) out[p.label] = Math.round(old.percent - p.percent);
250
+ // note: stored as consumed-positive; renderer negates for display
251
+ }
252
+ // flip sign convention for renderer: it wants negative=consumed
253
+ const render: Record<string, number> = {};
254
+ for (const [k, v] of Object.entries(out)) render[k] = -v;
255
+ return render;
256
+ }
257
+
258
+ function looksReset(a: PlanSnapshot, b: PlanSnapshot): boolean {
259
+ // all periods dropped sharply at once => plan period reset
260
+ return b.periods.every((p) => {
261
+ const old = a.periods.find((q) => q.label === p.label);
262
+ return old !== undefined && p.percent - old.percent < -30;
263
+ });
264
+ }
265
+
266
+ async function handleStart(ctx: ExtensionContext): Promise<void> {
267
+ const product = resolveProduct(ctx);
268
+ if (!product) return;
269
+ const cached = snapshots.get(product) ?? null;
270
+ if (cached && Date.now() - cached.fetchedAt <= BASELINE_FRESH_MS) {
271
+ baseline = cached;
272
+ } else {
273
+ const win = await timeoutResult(refresh(ctx, "stale"), AGENT_START_TIMEOUT_MS);
274
+ baseline = win ? snapshots.get(product) ?? null : cached;
275
+ }
276
+ deltas = null;
277
+ renderWidget(ctx);
278
+ }
279
+
280
+ async function handleSettled(ctx: ExtensionContext): Promise<void> {
281
+ const product = resolveProduct(ctx);
282
+ if (!product) return;
283
+ const ok = await refresh(ctx, "settled");
284
+ if (!ok) return;
285
+ const snap = snapshots.get(product);
286
+ if (!snap) return;
287
+
288
+ if (baseline && baseline.product !== product) {
289
+ note = "changed";
290
+ deltas = null;
291
+ } else if (baseline && looksReset(baseline, snap)) {
292
+ note = "reset";
293
+ deltas = null;
294
+ } else if (baseline) {
295
+ deltas = computeDeltas(baseline, snap);
296
+ note = null;
297
+ }
298
+ baseline = null;
299
+ renderWidget(ctx);
300
+ }
301
+
302
+ async function timeoutResult<T>(p: Promise<T>, ms: number): Promise<T | null> {
303
+ let timer: ReturnType<typeof setTimeout> | undefined;
304
+ const timeout = new Promise<null>((resolve) => {
305
+ timer = setTimeout(() => resolve(null), ms);
306
+ });
307
+ try {
308
+ return await Promise.race<T | null>([p, timeout]);
309
+ } finally {
310
+ if (timer) clearTimeout(timer);
311
+ }
312
+ }
313
+
314
+ /* --------------------------------- ticker ---------------------------------- */
315
+
316
+ function ensureTicker(): void {
317
+ if (tickTimer) return;
318
+ tickTimer = setInterval(() => {
319
+ if (!lastCtx || shuttingDown) return;
320
+ const interval = getSettings().autoRefreshMinutes;
321
+ if (
322
+ interval > 0 &&
323
+ Date.now() - lastAutoAt >= interval * 60_000
324
+ ) {
325
+ lastAutoAt = Date.now();
326
+ void refresh(lastCtx, "manual");
327
+ return;
328
+ }
329
+ // age text ticks every minute even without refetch
330
+ if (activeWidget && lastCtx.isIdle()) renderWidget(lastCtx);
331
+ }, TICK_MS);
332
+ tickTimer.unref?.();
333
+ }
334
+
335
+ function stopTicker(): void {
336
+ if (tickTimer) {
337
+ clearInterval(tickTimer);
338
+ tickTimer = null;
339
+ }
340
+ }
341
+
342
+ /* -------------------------------- commands --------------------------------- */
343
+
344
+ async function cmdCheck(
345
+ _args: string,
346
+ ctx: ExtensionCommandContext,
347
+ ): Promise<void> {
348
+ const ok = await refresh(ctx, "manual");
349
+ const snap = activeSnapshot();
350
+ const locale = LOCALES[language];
351
+ if (!ok || !snap) {
352
+ ctx.ui.notify(locale.refreshFailed, "warning");
353
+ return;
354
+ }
355
+ const summary = snap.periods
356
+ .map((p) => {
357
+ const label = locale.labels[p.label] ?? p.label;
358
+ return `${label} ${Math.round(p.percent)}%`;
359
+ })
360
+ .join(" / ");
361
+ ctx.ui.notify(locale.refreshOk(summary), "info");
362
+ }
363
+
364
+ async function cmdUsage(
365
+ _args: string,
366
+ ctx: ExtensionCommandContext,
367
+ ): Promise<void> {
368
+ const snap = activeSnapshot();
369
+ const locale = LOCALES[language];
370
+ if (!snap) {
371
+ ctx.ui.notify(locale.noActiveProduct, "warning");
372
+ return;
373
+ }
374
+ const parts = snap.periods.map((p) => {
375
+ const label = locale.labels[p.label] ?? p.label;
376
+ let line = `${label} ${Math.round(p.percent)}%`;
377
+ if (p.reset_at) {
378
+ const remainMs = new Date(p.reset_at).getTime() - Date.now();
379
+ const t = formatRemaining(remainMs, language);
380
+ if (t) line += `(${locale.resetsIn(t)})`;
381
+ }
382
+ return line;
383
+ });
384
+ ctx.ui.notify(`${locale.title}: ${parts.join(" / ")}`, "info");
385
+ }
386
+
387
+ async function cmdSet(
388
+ args: string,
389
+ ctx: ExtensionCommandContext,
390
+ ): Promise<void> {
391
+ const locale = LOCALES[language];
392
+ const parts = args.trim().split(/\s+/).filter(Boolean);
393
+ const notifySaved = (): void => {
394
+ persist();
395
+ ctx.ui.notify(locale.settingsApplied, "info");
396
+ if (lastCtx) renderWidget(lastCtx);
397
+ };
398
+
399
+ if (parts.length === 0) {
400
+ ctx.ui.notify(locale.settingsShow(getSettings()), "info");
401
+ return;
402
+ }
403
+
404
+ const [key, raw] = parts;
405
+ switch (key) {
406
+ case "reset": {
407
+ resetSettings();
408
+ persist();
409
+ ctx.ui.notify(locale.settingsReset, "info");
410
+ if (lastCtx) renderWidget(lastCtx);
411
+ return;
412
+ }
413
+ case "yellow":
414
+ case "red": {
415
+ const n = Number(raw);
416
+ if (!Number.isFinite(n) || n <= 0 || n >= 100 || parts.length !== 2) {
417
+ ctx.ui.notify(locale.settingsUsage, "warning");
418
+ return;
419
+ }
420
+ const s = getSettings();
421
+ if (key === "yellow" && n >= s.pctRed) {
422
+ ctx.ui.notify(locale.settingsUsage, "warning");
423
+ return;
424
+ }
425
+ if (key === "red" && n <= s.pctYellow) {
426
+ ctx.ui.notify(locale.settingsUsage, "warning");
427
+ return;
428
+ }
429
+ patchSettings(key === "yellow" ? { pctYellow: n } : { pctRed: n });
430
+ notifySaved();
431
+ return;
432
+ }
433
+ case "auto": {
434
+ const n = Number(raw);
435
+ if (
436
+ parts.length !== 2 ||
437
+ !Number.isInteger(n) ||
438
+ n < 0 ||
439
+ n > AUTO_REFRESH_MAX_MINUTES
440
+ ) {
441
+ ctx.ui.notify(locale.settingsUsage, "warning");
442
+ return;
443
+ }
444
+ patchSettings({ autoRefreshMinutes: n });
445
+ notifySaved();
446
+ ctx.ui.notify(n > 0 ? locale.autoOn(n) : locale.autoOff, "info");
447
+ return;
448
+ }
449
+ case "product": {
450
+ if (
451
+ parts.length !== 2 ||
452
+ (raw !== "auto" && !PRODUCTS.includes(raw as ProductId))
453
+ ) {
454
+ ctx.ui.notify(locale.settingsUsage, "warning");
455
+ return;
456
+ }
457
+ patchSettings({ product: raw as ArkSettings["product"] });
458
+ notifySaved();
459
+ ctx.ui.notify(locale.productSet(raw), "info");
460
+ void refresh(ctx, "manual");
461
+ return;
462
+ }
463
+ case "seat": {
464
+ if (parts.length === 1 || raw === "none") {
465
+ patchSettings({ seat: "" });
466
+ notifySaved();
467
+ ctx.ui.notify(locale.seatCleared, "info");
468
+ return;
469
+ }
470
+ if (parts.length !== 2 || raw.length > 128) {
471
+ ctx.ui.notify(locale.settingsUsage, "warning");
472
+ return;
473
+ }
474
+ patchSettings({ seat: raw });
475
+ notifySaved();
476
+ ctx.ui.notify(locale.seatSet(raw), "info");
477
+ return;
478
+ }
479
+ case "lang": {
480
+ if (raw !== "zh" && raw !== "en") {
481
+ ctx.ui.notify(locale.settingsUsage, "warning");
482
+ return;
483
+ }
484
+ language = raw;
485
+ persist();
486
+ ctx.ui.notify(locale.languageChanged(language), "info");
487
+ if (lastCtx) renderWidget(lastCtx);
488
+ return;
489
+ }
490
+ default:
491
+ ctx.ui.notify(locale.settingsUsage, "warning");
492
+ }
493
+ }
494
+
495
+ /* --------------------------------- wiring ---------------------------------- */
496
+
497
+ export default function (pi: ExtensionAPI): void {
498
+ const loaded = loadCache();
499
+ if (loaded) {
500
+ language = loaded.language;
501
+ for (const [k, v] of Object.entries(loaded.snapshots)) {
502
+ snapshots.set(k, v);
503
+ }
504
+ }
505
+
506
+ pi.on("session_shutdown", async () => {
507
+ shuttingDown = true;
508
+ markShuttingDown();
509
+ stopTicker();
510
+ inFlight?.abort();
511
+ await flushWrites();
512
+ });
513
+
514
+ pi.on("session_start", (_event, ctx) => {
515
+ activeProduct = resolveProduct(ctx);
516
+ deltas = null;
517
+ note = null;
518
+ renderWidget(ctx);
519
+ ensureTicker();
520
+ void refresh(ctx, "session_start");
521
+ });
522
+
523
+ pi.on("model_select", (_event, ctx) => {
524
+ const next = resolveProduct(ctx);
525
+ if (baseline && next && baseline.product !== next) note = "changed";
526
+ activeProduct = next;
527
+ renderWidget(ctx);
528
+ void refresh(ctx, "model_select");
529
+ });
530
+
531
+ pi.on("agent_start", async (_event, ctx) => {
532
+ await handleStart(ctx);
533
+ });
534
+
535
+ pi.on("agent_settled", async (_event, ctx) => {
536
+ await handleSettled(ctx);
537
+ });
538
+
539
+ pi.registerCommand("arkcheck", {
540
+ description: "Force refresh Volcengine Ark plan usage",
541
+ handler: cmdCheck,
542
+ });
543
+ pi.registerCommand("arkusage", {
544
+ description: "Show Volcengine Ark plan usage details",
545
+ handler: cmdUsage,
546
+ });
547
+ pi.registerCommand("arkset", {
548
+ description:
549
+ "View/change ark usage settings: yellow|red|auto|product|seat|lang|reset",
550
+ handler: cmdSet,
551
+ });
552
+ }