@ycodium-ai/agent-kimi 0.2.2690 → 0.2.2696

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 CHANGED
@@ -12,6 +12,18 @@ customModels } }`; launch arguments (`acp`) and the account-directory variable (
12
12
  - Models come from the read-only `kimi provider list --json` (`src/kimiModels.ts`); the version
13
13
  from `kimi --version`. No probe session is created.
14
14
  - Sign-in is `kimi login` (device-code flow), upgrade is `kimi upgrade`.
15
+ - Usage (ADR-0034), per instance, read only when the user asks. The account directory is the
16
+ `homePath` setting, an explicit `KIMI_CODE_HOME` in the instance environment, then the process
17
+ environment, then `~/.kimi-code`:
18
+ - Quota (`src/kimiQuota.ts`): `GET <base_url>/usages` with the access token from
19
+ `credentials/kimi-code.json`, through `host.fetch`. `base_url` is `KIMI_CODE_BASE_URL`, else the
20
+ one in `config.toml`, else the CLI default `https://api.kimi.com/coding/v1`; a non-official host
21
+ reports `customEndpoint`. The token lives ~15 minutes and only the CLI renews it: an expired token
22
+ reports `missingCredentials`, the refresh token is never used. Windows are labelled from
23
+ `{duration, timeUnit}` (`5h`, `7d`).
24
+ - Tokens (`src/kimiTokens.ts`): `readTokenUsage` reads the `usage.record` lines of every
25
+ `sessions/*/*/agents/*/wire.jsonl` (the ACP reply carries no usage), deduplicating fork copies, with an
26
+ in-memory byte-offset resume.
15
27
  - Manifest permission is `{ kind: "executor" }`.
16
28
 
17
29
  Shipped pre-installed as `core.agent-kimi`.
package/dist/index.js CHANGED
@@ -89,8 +89,8 @@ var KIMI_CARD = {
89
89
  };
90
90
 
91
91
  // src/kimiInstance.ts
92
- import * as NodeOS2 from "node:os";
93
- import * as NodePath3 from "node:path";
92
+ import * as NodeOS4 from "node:os";
93
+ import * as NodePath5 from "node:path";
94
94
  import { mergeLaunchEnv, modelSelectionStringOption } from "@ycodium-ai/plugin-api/executor";
95
95
  import { nodeStreamsFromHandle } from "@ycodium-ai/plugin-api/process-stdin";
96
96
 
@@ -1363,14 +1363,14 @@ function codePointLength(str) {
1363
1363
  const units = str.length;
1364
1364
  if (!highSurrogate.test(str))
1365
1365
  return units;
1366
- let count = units;
1366
+ let count2 = units;
1367
1367
  for (let i = 0; i < units - 1; i++) {
1368
1368
  if ((str.charCodeAt(i) & 64512) === 55296 && (str.charCodeAt(i + 1) & 64512) === 56320) {
1369
- count--;
1369
+ count2--;
1370
1370
  i++;
1371
1371
  }
1372
1372
  }
1373
- return count;
1373
+ return count2;
1374
1374
  }
1375
1375
  function getLengthableOrigin(input2) {
1376
1376
  if (Array.isArray(input2))
@@ -5175,7 +5175,7 @@ var recursive = /* @__PURE__ */ new WeakMap();
5175
5175
  var NONE = 0;
5176
5176
  var ASSUMED = 1;
5177
5177
  var PROVEN = 2;
5178
- function isRecursive(inst, stack, resolve2) {
5178
+ function isRecursive(inst, stack, resolve3) {
5179
5179
  const cached2 = recursive.get(inst);
5180
5180
  if (cached2 !== void 0)
5181
5181
  return cached2 ? PROVEN : NONE;
@@ -5185,7 +5185,7 @@ function isRecursive(inst, stack, resolve2) {
5185
5185
  let result = NONE;
5186
5186
  const check2 = (child) => {
5187
5187
  if (result !== PROVEN && child?._zod) {
5188
- const answer = isRecursive(child, stack, resolve2);
5188
+ const answer = isRecursive(child, stack, resolve3);
5189
5189
  if (answer > result)
5190
5190
  result = answer;
5191
5191
  }
@@ -5196,7 +5196,7 @@ function isRecursive(inst, stack, resolve2) {
5196
5196
  const desc = Object.getOwnPropertyDescriptor(sh, key);
5197
5197
  if (spread && !desc.enumerable)
5198
5198
  continue;
5199
- const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve2) : NONE;
5199
+ const child = desc.get ? ASSUMED : desc.value?._zod ? isRecursive(desc.value, stack, resolve3) : NONE;
5200
5200
  if (child > answer)
5201
5201
  answer = child;
5202
5202
  }
@@ -5260,7 +5260,7 @@ function isRecursive(inst, stack, resolve2) {
5260
5260
  break;
5261
5261
  // `$ZodLazy` caches its inner on the def, so a resolved edge is followed exactly
5262
5262
  case "lazy": {
5263
- const inner = def._cachedInner ?? (resolve2 ? inst._zod.innerType : void 0);
5263
+ const inner = def._cachedInner ?? (resolve3 ? inst._zod.innerType : void 0);
5264
5264
  merge2(inner ? isRecursive(inner, stack, false) : ASSUMED);
5265
5265
  break;
5266
5266
  }
@@ -5725,8 +5725,8 @@ function az_default() {
5725
5725
  }
5726
5726
 
5727
5727
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/be.js
5728
- function getBelarusianPlural(count, one, few, many) {
5729
- const absCount = Math.abs(count);
5728
+ function getBelarusianPlural(count2, one, few, many) {
5729
+ const absCount = Math.abs(count2);
5730
5730
  const lastDigit = absCount % 10;
5731
5731
  const lastTwoDigits = absCount % 100;
5732
5732
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
@@ -8373,8 +8373,8 @@ function hu_default() {
8373
8373
  }
8374
8374
 
8375
8375
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/hy.js
8376
- function getArmenianPlural(count, one, many) {
8377
- return Math.abs(count) === 1 ? one : many;
8376
+ function getArmenianPlural(count2, one, many) {
8377
+ return Math.abs(count2) === 1 ? one : many;
8378
8378
  }
8379
8379
  function withDefiniteArticle(word) {
8380
8380
  if (!word)
@@ -11112,8 +11112,8 @@ function ro_default() {
11112
11112
  }
11113
11113
 
11114
11114
  // ../../node_modules/.pnpm/zod@4.6.5/node_modules/zod/v4/locales/ru.js
11115
- function getRussianPlural(count, one, few, many) {
11116
- const absCount = Math.abs(count);
11115
+ function getRussianPlural(count2, one, few, many) {
11116
+ const absCount = Math.abs(count2);
11117
11117
  const lastDigit = absCount % 10;
11118
11118
  const lastTwoDigits = absCount % 100;
11119
11119
  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
@@ -21924,11 +21924,11 @@ var Connection = class {
21924
21924
  const id = this.nextRequestId++;
21925
21925
  let cancel = () => {
21926
21926
  };
21927
- const response = new Promise((resolve2, reject) => {
21927
+ const response = new Promise((resolve3, reject) => {
21928
21928
  const pendingResponse = {
21929
21929
  resolve: (value) => {
21930
21930
  try {
21931
- resolve2(mapResponse ? mapResponse(value) : value);
21931
+ resolve3(mapResponse ? mapResponse(value) : value);
21932
21932
  } catch (error62) {
21933
21933
  reject(error62);
21934
21934
  }
@@ -21985,8 +21985,8 @@ var Connection = class {
21985
21985
  this.stream = stream;
21986
21986
  this.staticHandlers = handlers;
21987
21987
  this.allowBatches = options?.allowBatches ?? true;
21988
- this.closedPromise = new Promise((resolve2) => {
21989
- this.abortController.signal.addEventListener("abort", () => resolve2());
21988
+ this.closedPromise = new Promise((resolve3) => {
21989
+ this.abortController.signal.addEventListener("abort", () => resolve3());
21990
21990
  });
21991
21991
  void this.receive();
21992
21992
  }
@@ -22054,9 +22054,9 @@ var Connection = class {
22054
22054
  return;
22055
22055
  }
22056
22056
  const responseBatch = isResponseBatch(batch);
22057
- const responseCount = responseBatch ? 0 : batch.reduce((count, message) => count + (isNotificationMessage(message) ? 0 : 1), 0);
22057
+ const responseCount = responseBatch ? 0 : batch.reduce((count2, message) => count2 + (isNotificationMessage(message) ? 0 : 1), 0);
22058
22058
  let remaining = responseCount;
22059
- let remainingNotifications = batch.reduce((count, message) => count + (isNotificationMessage(message) ? 1 : 0), 0);
22059
+ let remainingNotifications = batch.reduce((count2, message) => count2 + (isNotificationMessage(message) ? 1 : 0), 0);
22060
22060
  let responseSent = false;
22061
22061
  const responses = [];
22062
22062
  const sendResponsesIfReady = async () => {
@@ -22880,8 +22880,8 @@ var AsyncQueue = class {
22880
22880
  if (this.failed) {
22881
22881
  return Promise.reject(this.failure);
22882
22882
  }
22883
- return new Promise((resolve2, reject) => {
22884
- this.waiters.push({ resolve: resolve2, reject });
22883
+ return new Promise((resolve3, reject) => {
22884
+ this.waiters.push({ resolve: resolve3, reject });
22885
22885
  });
22886
22886
  }
22887
22887
  };
@@ -23868,8 +23868,8 @@ function createAcpConnection(options) {
23868
23868
  }
23869
23869
  await dispatchMessage(value);
23870
23870
  };
23871
- const pullEnqueued = async (count) => {
23872
- for (let i = 0; i < count; i += 1) {
23871
+ const pullEnqueued = async (count2) => {
23872
+ for (let i = 0; i < count2; i += 1) {
23873
23873
  const { value, done } = await incoming.read();
23874
23874
  if (done || value === void 0) {
23875
23875
  return;
@@ -23970,8 +23970,8 @@ function createAcpConnection(options) {
23970
23970
  failIfClosed();
23971
23971
  const id = nextRequestId;
23972
23972
  nextRequestId += 1;
23973
- const { promise: promise2, resolve: resolve2, reject } = Promise.withResolvers();
23974
- pending.set(String(id), { method, resolve: resolve2, reject });
23973
+ const { promise: promise2, resolve: resolve3, reject } = Promise.withResolvers();
23974
+ pending.set(String(id), { method, resolve: resolve3, reject });
23975
23975
  const message = params === void 0 ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
23976
23976
  try {
23977
23977
  await Promise.race([writeMessage(message), promise2]);
@@ -23992,8 +23992,8 @@ function createAcpConnection(options) {
23992
23992
  if (inboundBatches === 0) {
23993
23993
  return;
23994
23994
  }
23995
- await new Promise((resolve2) => {
23996
- idleWaiters.push(resolve2);
23995
+ await new Promise((resolve3) => {
23996
+ idleWaiters.push(resolve3);
23997
23997
  if (inboundBatches === 0) {
23998
23998
  releaseIdleWaiters();
23999
23999
  }
@@ -24172,8 +24172,8 @@ var UnboundedEventStream = class {
24172
24172
  iterators.subscribers.delete(subscriber);
24173
24173
  return { value: void 0, done: true };
24174
24174
  }
24175
- await new Promise((resolve2) => {
24176
- subscriber.wake = resolve2;
24175
+ await new Promise((resolve3) => {
24176
+ subscriber.wake = resolve3;
24177
24177
  });
24178
24178
  }
24179
24179
  },
@@ -24563,8 +24563,8 @@ var Mutex = class {
24563
24563
  this.locked = true;
24564
24564
  return Promise.resolve();
24565
24565
  }
24566
- return new Promise((resolve2) => {
24567
- this.waiters.push(resolve2);
24566
+ return new Promise((resolve3) => {
24567
+ this.waiters.push(resolve3);
24568
24568
  });
24569
24569
  }
24570
24570
  tryAcquire() {
@@ -24889,6 +24889,13 @@ function parseSessionModeState(sessionResponse) {
24889
24889
  availableModes
24890
24890
  };
24891
24891
  }
24892
+ function acpCommandsToSlashCommands(commands) {
24893
+ return commands.map((command) => ({
24894
+ name: command.name,
24895
+ ...command.description === void 0 ? {} : { description: command.description },
24896
+ ...command.inputHint === void 0 ? {} : { input: { hint: command.inputHint } }
24897
+ }));
24898
+ }
24892
24899
  function parseAvailableCommandsUpdate(params) {
24893
24900
  const update = params.update;
24894
24901
  if (update["sessionUpdate"] !== "available_commands_update") {
@@ -25340,24 +25347,7 @@ function filterMcpServersByAgentCapabilities(mcpServers, agentCapabilities) {
25340
25347
  });
25341
25348
  }
25342
25349
 
25343
- // ../../packages/ycodium-acp-executor/src/vendor-hooks.ts
25344
- var defaultVendorHooks = {
25345
- completedStopReason: (response) => response.stopReason
25346
- };
25347
-
25348
- // ../../packages/ycodium-acp-executor/src/executor.ts
25349
- var DEFAULT_CANCEL_GRACE_MS = 5e3;
25350
- var userInputAnswerOptionId = (question, answers) => {
25351
- const answer = answers[question.id];
25352
- const selected = typeof answer === "string" ? answer : Array.isArray(answer) && answer.length === 1 && typeof answer[0] === "string" ? answer[0] : void 0;
25353
- return selected !== void 0 && question.options.some((option) => option.id === selected) ? selected : void 0;
25354
- };
25355
- function defaultNowIso() {
25356
- return (/* @__PURE__ */ new Date()).toISOString();
25357
- }
25358
- function defaultRandomId() {
25359
- return NodeCrypto.randomUUID();
25360
- }
25350
+ // ../../packages/ycodium-acp-executor/src/model-selection.ts
25361
25351
  function normalizeModelId(value) {
25362
25352
  return value?.trim() || void 0;
25363
25353
  }
@@ -25376,6 +25366,25 @@ function modelSelectionStateFromSetup(setup) {
25376
25366
  advertisedModelValues: strategy === "config-option" ? modelConfigOption === void 0 ? [] : collectSessionConfigOptionValues(modelConfigOption) : collectSessionModelIds(sessionModels)
25377
25367
  };
25378
25368
  }
25369
+
25370
+ // ../../packages/ycodium-acp-executor/src/vendor-hooks.ts
25371
+ var defaultVendorHooks = {
25372
+ completedStopReason: (response) => response.stopReason
25373
+ };
25374
+
25375
+ // ../../packages/ycodium-acp-executor/src/executor.ts
25376
+ var DEFAULT_CANCEL_GRACE_MS = 5e3;
25377
+ var userInputAnswerOptionId = (question, answers) => {
25378
+ const answer = answers[question.id];
25379
+ const selected = typeof answer === "string" ? answer : Array.isArray(answer) && answer.length === 1 && typeof answer[0] === "string" ? answer[0] : void 0;
25380
+ return selected !== void 0 && question.options.some((option) => option.id === selected) ? selected : void 0;
25381
+ };
25382
+ function defaultNowIso() {
25383
+ return (/* @__PURE__ */ new Date()).toISOString();
25384
+ }
25385
+ function defaultRandomId() {
25386
+ return NodeCrypto.randomUUID();
25387
+ }
25379
25388
  function createAcpExecutor(options) {
25380
25389
  const hooks = { ...defaultVendorHooks, ...options.hooks };
25381
25390
  const cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
@@ -25585,6 +25594,7 @@ function createAcpExecutor(options) {
25585
25594
  const commands = parseAvailableCommandsUpdate(params);
25586
25595
  if (commands !== void 0) {
25587
25596
  availableCommands = commands;
25597
+ options.onAvailableCommands?.(commands);
25588
25598
  }
25589
25599
  if (sessionLoadInFlight) {
25590
25600
  droppedReplayUpdates += parsed.events.filter((event) => event._tag !== "ModeChanged").length;
@@ -25863,14 +25873,14 @@ function createAcpExecutor(options) {
25863
25873
  }
25864
25874
  }
25865
25875
  const requestId = randomId();
25866
- const outcome = await new Promise((resolve2) => {
25876
+ const outcome = await new Promise((resolve3) => {
25867
25877
  const pending = {
25868
25878
  requestId,
25869
25879
  request: params,
25870
25880
  permissionRequest,
25871
25881
  turnId: activeTurnId,
25872
25882
  settled: false,
25873
- resolve: resolve2
25883
+ resolve: resolve3
25874
25884
  };
25875
25885
  approvals.add(pending);
25876
25886
  emit(
@@ -26178,14 +26188,14 @@ function createAcpExecutor(options) {
26178
26188
  if (!promptMutex.isLocked) {
26179
26189
  return true;
26180
26190
  }
26181
- return await new Promise((resolve2) => {
26191
+ return await new Promise((resolve3) => {
26182
26192
  const timer = setTimeout(() => {
26183
26193
  promptWaiters = promptWaiters.filter((waiter) => waiter !== onSettle);
26184
- resolve2(false);
26194
+ resolve3(false);
26185
26195
  }, timeoutMs);
26186
26196
  const onSettle = () => {
26187
26197
  clearTimeout(timer);
26188
- resolve2(true);
26198
+ resolve3(true);
26189
26199
  };
26190
26200
  promptWaiters.push(onSettle);
26191
26201
  });
@@ -26651,7 +26661,7 @@ function toAcpMcpServers(entries) {
26651
26661
 
26652
26662
  // ../../packages/ycodium-acp-executor/src/probe.ts
26653
26663
  function withProbeTimeout(promise2, timeoutMs, what) {
26654
- return new Promise((resolve2, reject) => {
26664
+ return new Promise((resolve3, reject) => {
26655
26665
  const timer = setTimeout(() => {
26656
26666
  reject(new Error(`${what} timed out after ${timeoutMs}ms`));
26657
26667
  }, timeoutMs);
@@ -26659,7 +26669,7 @@ function withProbeTimeout(promise2, timeoutMs, what) {
26659
26669
  promise2.then(
26660
26670
  (value) => {
26661
26671
  clearTimeout(timer);
26662
- resolve2(value);
26672
+ resolve3(value);
26663
26673
  },
26664
26674
  (error62) => {
26665
26675
  clearTimeout(timer);
@@ -26944,6 +26954,14 @@ function readKimiContextWindow(input2) {
26944
26954
  return contents === void 0 ? void 0 : parseKimiContextWindow(contents);
26945
26955
  }
26946
26956
 
26957
+ // src/kimiQuota.ts
26958
+ import * as NodeFS3 from "node:fs";
26959
+ import * as NodeOS2 from "node:os";
26960
+ import * as NodePath3 from "node:path";
26961
+ import {
26962
+ parseRetryAfterMs
26963
+ } from "@ycodium-ai/plugin-api/usage";
26964
+
26947
26965
  // src/kimiAccount.ts
26948
26966
  import * as NodeFS2 from "node:fs";
26949
26967
  import * as NodeOS from "node:os";
@@ -26971,6 +26989,389 @@ function readKimiAuthState(resolvedHomePath) {
26971
26989
  return nonEmptyString(record2["refresh_token"]) || nonEmptyString(record2["access_token"]) ? "signed-in" : "signed-out";
26972
26990
  }
26973
26991
 
26992
+ // src/kimiQuota.ts
26993
+ var KIMI_OFFICIAL_HOSTS = /* @__PURE__ */ new Set(["api.kimi.com", "api.kimi.ai"]);
26994
+ var KIMI_DEFAULT_BASE_URL = "https://api.kimi.com/coding/v1";
26995
+ var KIMI_USAGES_PATH = "/usages";
26996
+ var QUOTA_TIMEOUT_MS = 1e4;
26997
+ var TOKEN_EXPIRY_SKEW_MS = 3e4;
26998
+ var asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
26999
+ var asString3 = (value) => typeof value === "string" && value.length > 0 ? value : null;
27000
+ var asNumber = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
27001
+ var asCount = (value) => {
27002
+ const direct = asNumber(value);
27003
+ if (direct !== null) return direct;
27004
+ const text = asString3(value);
27005
+ if (text === null) return null;
27006
+ const parsed = Number(text);
27007
+ return Number.isFinite(parsed) ? parsed : null;
27008
+ };
27009
+ var parseJson = (raw) => {
27010
+ try {
27011
+ return JSON.parse(raw);
27012
+ } catch {
27013
+ return null;
27014
+ }
27015
+ };
27016
+ var clampPercent = (value) => Math.min(100, Math.max(0, value));
27017
+ var expiryMs = (value) => {
27018
+ const raw = asNumber(value);
27019
+ if (raw === null || raw <= 0) return null;
27020
+ return raw > 1e12 ? raw : raw * 1e3;
27021
+ };
27022
+ var extractKimiToken = (auth, nowMs) => {
27023
+ const record2 = asObject(auth);
27024
+ if (record2 === null) return null;
27025
+ const token = asString3(record2["access_token"]);
27026
+ if (token === null) return null;
27027
+ const expiresAtMs = expiryMs(record2["expires_at"]);
27028
+ if (expiresAtMs === null) return token;
27029
+ return expiresAtMs - nowMs < TOKEN_EXPIRY_SKEW_MS ? null : token;
27030
+ };
27031
+ var isOfficialUrl = (value) => {
27032
+ const parsed = URL.parse(value.trim());
27033
+ return parsed === null ? false : KIMI_OFFICIAL_HOSTS.has(parsed.hostname.toLowerCase());
27034
+ };
27035
+ var readTomlScalar = (raw) => {
27036
+ const quoted = /^(["'])(.*?)\1/.exec(raw.trim());
27037
+ if (quoted?.[2] !== void 0) return quoted[2];
27038
+ const [beforeComment = ""] = raw.split("#");
27039
+ return beforeComment.trim();
27040
+ };
27041
+ var TABLE_HEADER = /^\s*\[\s*([^\]]+?)\s*\]\s*(?:#.*)?$/;
27042
+ var BASE_URL_LINE = /^\s*base_url\s*=\s*(\S.*)$/;
27043
+ var kimiConfigBaseUrls = (contents) => {
27044
+ const found = [];
27045
+ let table = "";
27046
+ for (const line of contents.split("\n")) {
27047
+ const header = TABLE_HEADER.exec(line);
27048
+ if (header !== null) {
27049
+ table = header[1] ?? "";
27050
+ continue;
27051
+ }
27052
+ const match = BASE_URL_LINE.exec(line);
27053
+ if (match === null) continue;
27054
+ const url2 = readTomlScalar(match[1] ?? "");
27055
+ if (url2.length > 0) found.push({ url: url2, isTool: table.startsWith("tools.") });
27056
+ }
27057
+ return found;
27058
+ };
27059
+ var usagesUrlFrom = (baseUrl) => baseUrl.trim().replace(/\/+$/, "") + KIMI_USAGES_PATH;
27060
+ var kimiUsagesEndpoint = (input2) => {
27061
+ const fromEnv = input2.envBaseUrl?.trim() ?? "";
27062
+ if (fromEnv.length > 0) {
27063
+ return isOfficialUrl(fromEnv) ? { usagesUrl: usagesUrlFrom(fromEnv) } : { custom: true };
27064
+ }
27065
+ const configured = input2.configToml === null ? [] : kimiConfigBaseUrls(input2.configToml);
27066
+ if (configured.some((entry) => !isOfficialUrl(entry.url))) return { custom: true };
27067
+ const root = configured.find((entry) => !entry.isTool)?.url ?? KIMI_DEFAULT_BASE_URL;
27068
+ return { usagesUrl: usagesUrlFrom(root) };
27069
+ };
27070
+ var TIME_UNIT_MINUTES = {
27071
+ TIME_UNIT_MINUTE: 1,
27072
+ TIME_UNIT_HOUR: 60,
27073
+ TIME_UNIT_DAY: 1440,
27074
+ TIME_UNIT_WEEK: 10080
27075
+ };
27076
+ var kimiWindowSpan = (window) => {
27077
+ const record2 = asObject(window);
27078
+ if (record2 === null) return null;
27079
+ const duration3 = asCount(record2["duration"]);
27080
+ const unit = asString3(record2["timeUnit"]);
27081
+ const minutesPerUnit = unit === null ? void 0 : TIME_UNIT_MINUTES[unit];
27082
+ if (duration3 === null || duration3 <= 0 || minutesPerUnit === void 0) return null;
27083
+ const minutes = duration3 * minutesPerUnit;
27084
+ const label = minutes % 1440 === 0 ? `${minutes / 1440}d` : minutes % 60 === 0 ? `${minutes / 60}h` : `${minutes}m`;
27085
+ return { label, windowMs: minutes * 6e4 };
27086
+ };
27087
+ var windowFrom = (id, label, detail, windowMs) => {
27088
+ const record2 = asObject(detail);
27089
+ if (record2 === null) return null;
27090
+ const limit = asCount(record2["limit"]);
27091
+ if (limit === null || limit <= 0) return null;
27092
+ const remaining = asCount(record2["remaining"]);
27093
+ const used = asCount(record2["used"]) ?? (remaining === null ? null : limit - remaining);
27094
+ if (used === null) return null;
27095
+ return {
27096
+ id,
27097
+ label,
27098
+ usedPercent: clampPercent(used / limit * 100),
27099
+ resetsAt: asString3(record2["resetTime"]),
27100
+ ...windowMs === void 0 ? {} : { windowMs }
27101
+ };
27102
+ };
27103
+ var kimiPlanName = (body) => {
27104
+ const level = asString3(asObject(asObject(asObject(body)?.["user"])?.["membership"])?.["level"]);
27105
+ if (level === null) return void 0;
27106
+ const bare = level.replace(/^LEVEL_/, "").toLowerCase();
27107
+ return bare.length === 0 ? void 0 : bare.charAt(0).toUpperCase() + bare.slice(1);
27108
+ };
27109
+ var kimiQuotaWindows = (body) => {
27110
+ const root = asObject(body);
27111
+ const windows = [];
27112
+ const subscription = windowFrom(
27113
+ "subscription",
27114
+ { key: "usage.window.subscription" },
27115
+ root?.["usage"],
27116
+ void 0
27117
+ );
27118
+ if (subscription !== null) windows.push(subscription);
27119
+ const limits = root?.["limits"];
27120
+ if (Array.isArray(limits)) {
27121
+ for (const [index, entry] of limits.entries()) {
27122
+ const row = asObject(entry);
27123
+ if (row === null) continue;
27124
+ const span = kimiWindowSpan(row["window"]);
27125
+ const name = asString3(row["name"]);
27126
+ const label = span !== null ? { key: "usage.window.duration", values: { duration: span.label } } : name !== null ? { text: name } : null;
27127
+ if (label === null) continue;
27128
+ const window = windowFrom(`limit-${index}`, label, row["detail"], span?.windowMs);
27129
+ if (window !== null) windows.push(window);
27130
+ }
27131
+ }
27132
+ return windows;
27133
+ };
27134
+ var kimiEffectiveHome = (resolvedHomePath, processEnvHome) => {
27135
+ if (resolvedHomePath.trim().length > 0) return resolvedHomePath;
27136
+ const fromEnv = processEnvHome?.trim() ?? "";
27137
+ return fromEnv.length > 0 ? fromEnv : "";
27138
+ };
27139
+ var createKimiUsage = (input2) => {
27140
+ const now = input2.now ?? Date.now;
27141
+ const processEnv = input2.processEnv ?? process.env;
27142
+ const home = input2.homePath.trim().length > 0 ? input2.homePath : NodePath3.join(NodeOS2.homedir(), ".kimi-code");
27143
+ const credentialsPath = kimiCredentialsPath(input2.homePath);
27144
+ const readQuota = async (request) => {
27145
+ let configToml = null;
27146
+ try {
27147
+ configToml = NodeFS3.readFileSync(NodePath3.join(home, "config.toml"), "utf8");
27148
+ } catch (error62) {
27149
+ if (error62.code !== "ENOENT") {
27150
+ return { status: "unavailable", reason: "unreachable" };
27151
+ }
27152
+ }
27153
+ const endpoint = kimiUsagesEndpoint({
27154
+ envBaseUrl: input2.environment["KIMI_CODE_BASE_URL"] ?? processEnv["KIMI_CODE_BASE_URL"],
27155
+ configToml
27156
+ });
27157
+ if ("custom" in endpoint) return { status: "unavailable", reason: "customEndpoint" };
27158
+ let token = null;
27159
+ try {
27160
+ token = extractKimiToken(parseJson(NodeFS3.readFileSync(credentialsPath, "utf8")), now());
27161
+ } catch {
27162
+ token = null;
27163
+ }
27164
+ if (token === null) return { status: "unavailable", reason: "missingCredentials" };
27165
+ let response;
27166
+ try {
27167
+ response = await input2.fetch(endpoint.usagesUrl, {
27168
+ headers: { Authorization: `Bearer ${token}`, Accept: "application/json" },
27169
+ signal: AbortSignal.any([
27170
+ ...request.signal === void 0 ? [] : [request.signal],
27171
+ AbortSignal.timeout(QUOTA_TIMEOUT_MS)
27172
+ ])
27173
+ });
27174
+ } catch {
27175
+ return { status: "unavailable", reason: "unreachable" };
27176
+ }
27177
+ if (response.status === 401 || response.status === 403) {
27178
+ return { status: "unavailable", reason: "authRejected" };
27179
+ }
27180
+ if (response.status === 429) {
27181
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"), now());
27182
+ return {
27183
+ status: "unavailable",
27184
+ reason: "rateLimited",
27185
+ ...retryAfterMs === null ? {} : { retryAfterMs }
27186
+ };
27187
+ }
27188
+ if (!response.ok) return { status: "unavailable", reason: "unreachable" };
27189
+ let body;
27190
+ try {
27191
+ body = parseJson(await response.text());
27192
+ } catch {
27193
+ return { status: "unavailable", reason: "unreachable" };
27194
+ }
27195
+ if (asObject(body) === null) return { status: "unavailable", reason: "unreachable" };
27196
+ const plan = kimiPlanName(body);
27197
+ return {
27198
+ status: "available",
27199
+ ...plan === void 0 ? {} : { plan },
27200
+ windows: kimiQuotaWindows(body),
27201
+ balances: [],
27202
+ accountKey: credentialsPath
27203
+ };
27204
+ };
27205
+ return { readQuota };
27206
+ };
27207
+
27208
+ // src/kimiTokens.ts
27209
+ import * as NodeFS4 from "node:fs";
27210
+ import * as NodeOS3 from "node:os";
27211
+ import * as NodePath4 from "node:path";
27212
+ var MTIME_SLACK_MS = 36 * 60 * 60 * 1e3;
27213
+ var GUARD_LENGTH = 64;
27214
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
27215
+ var count = (value) => typeof value === "number" && Number.isFinite(value) && value > 0 ? Math.trunc(value) : 0;
27216
+ var parseKimiUsageLine = (line) => {
27217
+ if (!line.includes('"usage.record"')) return void 0;
27218
+ let parsed;
27219
+ try {
27220
+ parsed = JSON.parse(line);
27221
+ } catch {
27222
+ return "malformed";
27223
+ }
27224
+ if (!isRecord8(parsed) || parsed["type"] !== "usage.record") return void 0;
27225
+ const usage = parsed["usage"];
27226
+ const model = typeof parsed["model"] === "string" ? parsed["model"].trim() : "";
27227
+ const timeMs = parsed["time"];
27228
+ if (!isRecord8(usage) || model.length === 0 || typeof timeMs !== "number" || !Number.isFinite(timeMs)) {
27229
+ return "malformed";
27230
+ }
27231
+ const totals = {
27232
+ uncachedInputTokens: count(usage["inputOther"]),
27233
+ cachedInputTokens: count(usage["inputCacheRead"]),
27234
+ cacheCreationTokens: count(usage["inputCacheCreation"]),
27235
+ outputTokens: count(usage["output"]),
27236
+ reasoningTokens: 0
27237
+ };
27238
+ return {
27239
+ timeMs,
27240
+ model,
27241
+ totals,
27242
+ key: `${timeMs}\0${model}\0${totals.uncachedInputTokens}\0${totals.cachedInputTokens}\0${totals.cacheCreationTokens}\0${totals.outputTokens}`
27243
+ };
27244
+ };
27245
+ var hasTokens = (totals) => totals.uncachedInputTokens > 0 || totals.cachedInputTokens > 0 || totals.cacheCreationTokens > 0 || totals.outputTokens > 0;
27246
+ var readDirs = (dir) => {
27247
+ try {
27248
+ return NodeFS4.readdirSync(dir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
27249
+ } catch {
27250
+ return [];
27251
+ }
27252
+ };
27253
+ var listWireLogs = (sessionsRoot) => {
27254
+ const found = [];
27255
+ for (const workspace of readDirs(sessionsRoot)) {
27256
+ for (const session of readDirs(NodePath4.join(sessionsRoot, workspace))) {
27257
+ const agentsDir = NodePath4.join(sessionsRoot, workspace, session, "agents");
27258
+ for (const agent of readDirs(agentsDir)) {
27259
+ found.push({ file: NodePath4.join(agentsDir, agent, "wire.jsonl"), sessionId: session });
27260
+ }
27261
+ }
27262
+ }
27263
+ return found;
27264
+ };
27265
+ var readRange = (file2, from, to) => {
27266
+ const length = Math.max(0, to - from);
27267
+ const buffer = Buffer.alloc(length);
27268
+ const fd = NodeFS4.openSync(file2, "r");
27269
+ try {
27270
+ let read = 0;
27271
+ while (read < length) {
27272
+ const n = NodeFS4.readSync(fd, buffer, read, length - read, from + read);
27273
+ if (n === 0) break;
27274
+ read += n;
27275
+ }
27276
+ return read === length ? buffer : buffer.subarray(0, read);
27277
+ } finally {
27278
+ NodeFS4.closeSync(fd);
27279
+ }
27280
+ };
27281
+ var guardOf = (file2, size) => readRange(file2, 0, Math.min(size, GUARD_LENGTH)).toString("base64");
27282
+ var createKimiTokenReader = (input2) => {
27283
+ const home = input2.homePath.trim().length > 0 ? NodePath4.resolve(input2.homePath) : NodePath4.join(NodeOS3.homedir(), ".kimi-code");
27284
+ const sessionsRoot = NodePath4.join(home, "sessions");
27285
+ const memos = /* @__PURE__ */ new Map();
27286
+ const refresh = (file2, size) => {
27287
+ const guard = guardOf(file2, size);
27288
+ const memo2 = memos.get(file2);
27289
+ const resumable = memo2 !== void 0 && memo2.size <= size && memo2.guard === guard;
27290
+ if (resumable && memo2.size === size) return memo2;
27291
+ const from = resumable ? memo2.offset : 0;
27292
+ const chunk = readRange(file2, from, size);
27293
+ const lastNewline = chunk.lastIndexOf(10);
27294
+ const complete = lastNewline < 0 ? Buffer.alloc(0) : chunk.subarray(0, lastNewline + 1);
27295
+ const next = resumable ? { ...memo2, size, offset: from + complete.length } : { size, guard, offset: complete.length, malformed: 0, records: [] };
27296
+ for (const line of complete.toString("utf8").split("\n")) {
27297
+ if (line.length === 0) continue;
27298
+ const parsed = parseKimiUsageLine(line);
27299
+ if (parsed === void 0) continue;
27300
+ if (parsed === "malformed") next.malformed += 1;
27301
+ else next.records.push(parsed);
27302
+ }
27303
+ memos.set(file2, next);
27304
+ return next;
27305
+ };
27306
+ const readTokenUsage = async (request) => {
27307
+ const sinceMs = Date.parse(request.since);
27308
+ const untilMs = Date.parse(request.until);
27309
+ if (!NodeFS4.existsSync(sessionsRoot)) {
27310
+ return {
27311
+ records: [],
27312
+ source: {
27313
+ homePath: home,
27314
+ status: "missing",
27315
+ scannedFiles: 0,
27316
+ skippedFiles: 0,
27317
+ message: `No Kimi sessions directory at ${sessionsRoot}`
27318
+ }
27319
+ };
27320
+ }
27321
+ const records = [];
27322
+ const seen = /* @__PURE__ */ new Set();
27323
+ let scannedFiles = 0;
27324
+ let skippedFiles = 0;
27325
+ let failedFiles = 0;
27326
+ let malformedRecords = 0;
27327
+ for (const { file: file2, sessionId } of listWireLogs(sessionsRoot)) {
27328
+ let size;
27329
+ try {
27330
+ const stat = NodeFS4.statSync(file2);
27331
+ if (stat.mtimeMs < sinceMs - MTIME_SLACK_MS) {
27332
+ skippedFiles += 1;
27333
+ continue;
27334
+ }
27335
+ size = stat.size;
27336
+ } catch {
27337
+ continue;
27338
+ }
27339
+ let memo2;
27340
+ try {
27341
+ memo2 = refresh(file2, size);
27342
+ } catch {
27343
+ failedFiles += 1;
27344
+ continue;
27345
+ }
27346
+ scannedFiles += 1;
27347
+ malformedRecords += memo2.malformed;
27348
+ for (const record2 of memo2.records) {
27349
+ if (record2.timeMs < sinceMs || record2.timeMs >= untilMs) continue;
27350
+ if (!hasTokens(record2.totals) || seen.has(record2.key)) continue;
27351
+ seen.add(record2.key);
27352
+ records.push({
27353
+ at: new Date(record2.timeMs).toISOString(),
27354
+ model: record2.model,
27355
+ sessionId,
27356
+ totals: record2.totals
27357
+ });
27358
+ }
27359
+ }
27360
+ return {
27361
+ records,
27362
+ source: {
27363
+ homePath: home,
27364
+ status: failedFiles > 0 ? "partial" : "ok",
27365
+ scannedFiles,
27366
+ skippedFiles: skippedFiles + failedFiles,
27367
+ malformedRecords,
27368
+ ...failedFiles > 0 ? { message: `${failedFiles} session log(s) could not be read` } : {}
27369
+ }
27370
+ };
27371
+ };
27372
+ return { readTokenUsage };
27373
+ };
27374
+
26974
27375
  // src/kimiModels.ts
26975
27376
  import {
26976
27377
  describeExecutorSpawnFailure,
@@ -26985,10 +27386,10 @@ var probeFailureDetail = (error62) => (error62 instanceof Error ? error62.messag
26985
27386
  var PROBE_TIMEOUT_MS = 15e3;
26986
27387
  var MAX_OUTPUT_BYTES = 1024 * 1024;
26987
27388
  var KIMI_MODEL_LIST_ARGS = ["provider", "list", "--json"];
26988
- function isRecord8(value) {
27389
+ function isRecord9(value) {
26989
27390
  return typeof value === "object" && value !== null && !Array.isArray(value);
26990
27391
  }
26991
- function asString3(value) {
27392
+ function asString4(value) {
26992
27393
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
26993
27394
  }
26994
27395
  function positiveInt2(value) {
@@ -27001,14 +27402,14 @@ function kimiProbeModels(stdout) {
27001
27402
  } catch {
27002
27403
  return void 0;
27003
27404
  }
27004
- if (!isRecord8(parsed)) return void 0;
27405
+ if (!isRecord9(parsed)) return void 0;
27005
27406
  const models = parsed["models"];
27006
- if (!isRecord8(models)) return void 0;
27407
+ if (!isRecord9(models)) return void 0;
27007
27408
  const out = [];
27008
27409
  for (const [slug, entry] of Object.entries(models)) {
27009
27410
  const trimmed = slug.trim();
27010
- if (trimmed.length === 0 || !isRecord8(entry)) continue;
27011
- const name = asString3(entry["displayName"]);
27411
+ if (trimmed.length === 0 || !isRecord9(entry)) continue;
27412
+ const name = asString4(entry["displayName"]);
27012
27413
  const contextWindow = positiveInt2(entry["maxContextSize"]);
27013
27414
  out.push({
27014
27415
  slug: trimmed,
@@ -27107,8 +27508,8 @@ function acpSessionModeToApply(requestedNativeModeId, startedModeId) {
27107
27508
  // src/kimiInstance.ts
27108
27509
  var RESUME_SCHEMA_VERSION = 1;
27109
27510
  var CLIENT_INFO = { name: "ycodium-agent-kimi", version: "0.1.0" };
27110
- var yieldTick = () => new Promise((resolve2) => {
27111
- setImmediate(resolve2);
27511
+ var yieldTick = () => new Promise((resolve3) => {
27512
+ setImmediate(resolve3);
27112
27513
  });
27113
27514
  var EventBus = class {
27114
27515
  #queued = [];
@@ -27135,17 +27536,17 @@ var EventBus = class {
27135
27536
  const queued = this.#queued.shift();
27136
27537
  if (queued !== void 0) return Promise.resolve({ done: false, value: queued });
27137
27538
  if (this.#closed) return Promise.resolve({ done: true, value: void 0 });
27138
- return new Promise((resolve2) => {
27139
- this.#waiters.push(resolve2);
27539
+ return new Promise((resolve3) => {
27540
+ this.#waiters.push(resolve3);
27140
27541
  });
27141
27542
  }
27142
27543
  };
27143
27544
  }
27144
27545
  };
27145
27546
  function expandHomePath(value) {
27146
- if (value === "~") return NodeOS2.homedir();
27547
+ if (value === "~") return NodeOS4.homedir();
27147
27548
  if (value.startsWith("~/") || value.startsWith("~\\")) {
27148
- return NodePath3.join(NodeOS2.homedir(), value.slice(2));
27549
+ return NodePath5.join(NodeOS4.homedir(), value.slice(2));
27149
27550
  }
27150
27551
  return value;
27151
27552
  }
@@ -27170,7 +27571,7 @@ function nowIso() {
27170
27571
  }
27171
27572
  function kimiContinuationKey(recipe) {
27172
27573
  const homePath = recipe.homePath.trim();
27173
- const resolvedHomePath = homePath.length > 0 ? NodePath3.resolve(expandHomePath(homePath)) : "";
27574
+ const resolvedHomePath = homePath.length > 0 ? NodePath5.resolve(expandHomePath(homePath)) : "";
27174
27575
  return `kimiCode:home:${recipe.binaryPath.trim()}\0${resolvedHomePath}`;
27175
27576
  }
27176
27577
  function resolveHomePath(recipe) {
@@ -27199,6 +27600,7 @@ var createKimiInstance = (config2, host) => {
27199
27600
  const events = new EventBus();
27200
27601
  const threads = /* @__PURE__ */ new Map();
27201
27602
  let disposed = false;
27603
+ const usageHome = kimiEffectiveHome(resolveHomePath(recipe), process.env[recipe.homeEnvVar]);
27202
27604
  const requireThread = (threadId) => {
27203
27605
  const booked = threads.get(threadId);
27204
27606
  if (!booked) throw new Error(`unknown thread ${threadId}`);
@@ -27312,6 +27714,11 @@ var createKimiInstance = (config2, host) => {
27312
27714
  providerInstanceId: host.instanceId,
27313
27715
  clientInfo: CLIENT_INFO,
27314
27716
  log: host.log,
27717
+ // agent 自己宣告的斜杠命令按会话报给宿主,输入区的 / 菜单才有它们。
27718
+ onAvailableCommands: (commands) => host.reportSessionInventory?.({
27719
+ threadId: input2.threadId,
27720
+ slashCommands: acpCommandsToSlashCommands(commands)
27721
+ }),
27315
27722
  ...requestedModelId === void 0 ? {} : { requestedModelId },
27316
27723
  ...input2.bindingGeneration === void 0 ? {} : { originBindingGeneration: input2.bindingGeneration },
27317
27724
  hooks,
@@ -27469,6 +27876,15 @@ var createKimiInstance = (config2, host) => {
27469
27876
  return { threadId, turns: booked.turns };
27470
27877
  },
27471
27878
  events,
27879
+ // 额度与 token 都读这个实例自己的账号目录(与它的 CLI 同一个)。
27880
+ usage: {
27881
+ ...createKimiUsage({
27882
+ homePath: usageHome,
27883
+ environment: recipe.environment,
27884
+ fetch: host.fetch
27885
+ }),
27886
+ ...createKimiTokenReader({ homePath: usageHome })
27887
+ },
27472
27888
  continuationKey: kimiContinuationKey(recipe),
27473
27889
  dispose: async () => {
27474
27890
  if (disposed) return;
package/locales/en.json CHANGED
@@ -10,5 +10,11 @@
10
10
  "description": "Sessions and the `kimi login` credentials live here, so providers sharing this directory can pick up each other's threads and providers with different directories cannot. Leave empty to use the CLI's default.",
11
11
  "placeholder": "e.g. ~/.kimi-code-work"
12
12
  }
13
+ },
14
+ "usage": {
15
+ "window": {
16
+ "subscription": "Subscription",
17
+ "duration": "{{duration}} window"
18
+ }
13
19
  }
14
20
  }
@@ -10,5 +10,11 @@
10
10
  "description": "会话和 `kimi login` 的凭据都保存在这里,因此共用同一目录的 provider 可以接手彼此的会话,目录不同的则不能。留空则使用该命令行工具的默认目录。",
11
11
  "placeholder": "例如 ~/.kimi-code-work"
12
12
  }
13
+ },
14
+ "usage": {
15
+ "window": {
16
+ "subscription": "订阅",
17
+ "duration": "{{duration}} 窗口"
18
+ }
13
19
  }
14
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-kimi",
3
- "version": "0.2.2690",
3
+ "version": "0.2.2696",
4
4
  "description": "Kimi Code executor: runs `kimi acp` over Agent Client Protocol stdio.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -21,9 +21,9 @@
21
21
  "@types/node": "24.12.4",
22
22
  "esbuild": "0.28.1",
23
23
  "vite-plus": "0.2.2",
24
- "ycodium-acp-executor": "0.2.2690",
25
- "@ycodium-ai/plugin-api": "0.2.2690",
26
- "ycodium-executor-testing": "0.0.0"
24
+ "@ycodium-ai/plugin-api": "0.2.2696",
25
+ "ycodium-executor-testing": "0.0.0",
26
+ "ycodium-acp-executor": "0.2.2696"
27
27
  },
28
28
  "ycodium": {
29
29
  "plugin": "./dist/index.js",