@ycodium-ai/agent-dsh 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
@@ -22,6 +22,13 @@ Facts:
22
22
  disk format is unchanged.
23
23
  - Credentials come from the environment (`DEEPSEEK_API_KEY`); there is no login
24
24
  command and no device-code flow, so `ExecutorInstance.signIn` is not implemented.
25
+ - Usage (ADR-0034): DSH runs on a DeepSeek API key, so the quota is the account's remaining
26
+ balance, not windows. `ExecutorInstance.usage.readQuota` calls `GET https://api.deepseek.com/user/balance`
27
+ (through `host.fetch`) and maps each `balance_infos` entry to a money balance in the currency the reply
28
+ names. The key is found the way DSH finds it: instance environment table / process environment, then
29
+ `$DSH_HOME/.credentials.yaml` `refs.DEEPSEEK_API_KEY`, then `$DSH_HOME/.env`. A `DEEPSEEK_BASE_URL` that is
30
+ not `api.deepseek.com` reports `customEndpoint` and sends nothing. No `readTokenUsage`: DSH's ACP reply
31
+ carries no usage and its session log (`session.v4.jsonl.zstd`) has no per-event timestamps.
25
32
  - Manifest permission is `{ kind: "executor" }`.
26
33
  - Session model switching is `unsupported` on the card: the plugin does not switch
27
34
  models inside a running session.
package/dist/index.js CHANGED
@@ -24903,6 +24903,13 @@ function parseSessionModeState(sessionResponse) {
24903
24903
  availableModes
24904
24904
  };
24905
24905
  }
24906
+ function acpCommandsToSlashCommands(commands) {
24907
+ return commands.map((command) => ({
24908
+ name: command.name,
24909
+ ...command.description === void 0 ? {} : { description: command.description },
24910
+ ...command.inputHint === void 0 ? {} : { input: { hint: command.inputHint } }
24911
+ }));
24912
+ }
24906
24913
  function parseAvailableCommandsUpdate(params) {
24907
24914
  const update = params.update;
24908
24915
  if (update["sessionUpdate"] !== "available_commands_update") {
@@ -25354,24 +25361,7 @@ function filterMcpServersByAgentCapabilities(mcpServers, agentCapabilities) {
25354
25361
  });
25355
25362
  }
25356
25363
 
25357
- // ../../packages/ycodium-acp-executor/src/vendor-hooks.ts
25358
- var defaultVendorHooks = {
25359
- completedStopReason: (response) => response.stopReason
25360
- };
25361
-
25362
- // ../../packages/ycodium-acp-executor/src/executor.ts
25363
- var DEFAULT_CANCEL_GRACE_MS = 5e3;
25364
- var userInputAnswerOptionId = (question, answers) => {
25365
- const answer = answers[question.id];
25366
- const selected = typeof answer === "string" ? answer : Array.isArray(answer) && answer.length === 1 && typeof answer[0] === "string" ? answer[0] : void 0;
25367
- return selected !== void 0 && question.options.some((option) => option.id === selected) ? selected : void 0;
25368
- };
25369
- function defaultNowIso() {
25370
- return (/* @__PURE__ */ new Date()).toISOString();
25371
- }
25372
- function defaultRandomId() {
25373
- return NodeCrypto.randomUUID();
25374
- }
25364
+ // ../../packages/ycodium-acp-executor/src/model-selection.ts
25375
25365
  function normalizeModelId(value) {
25376
25366
  return value?.trim() || void 0;
25377
25367
  }
@@ -25390,6 +25380,25 @@ function modelSelectionStateFromSetup(setup) {
25390
25380
  advertisedModelValues: strategy === "config-option" ? modelConfigOption === void 0 ? [] : collectSessionConfigOptionValues(modelConfigOption) : collectSessionModelIds(sessionModels)
25391
25381
  };
25392
25382
  }
25383
+
25384
+ // ../../packages/ycodium-acp-executor/src/vendor-hooks.ts
25385
+ var defaultVendorHooks = {
25386
+ completedStopReason: (response) => response.stopReason
25387
+ };
25388
+
25389
+ // ../../packages/ycodium-acp-executor/src/executor.ts
25390
+ var DEFAULT_CANCEL_GRACE_MS = 5e3;
25391
+ var userInputAnswerOptionId = (question, answers) => {
25392
+ const answer = answers[question.id];
25393
+ const selected = typeof answer === "string" ? answer : Array.isArray(answer) && answer.length === 1 && typeof answer[0] === "string" ? answer[0] : void 0;
25394
+ return selected !== void 0 && question.options.some((option) => option.id === selected) ? selected : void 0;
25395
+ };
25396
+ function defaultNowIso() {
25397
+ return (/* @__PURE__ */ new Date()).toISOString();
25398
+ }
25399
+ function defaultRandomId() {
25400
+ return NodeCrypto.randomUUID();
25401
+ }
25393
25402
  function createAcpExecutor(options) {
25394
25403
  const hooks = { ...defaultVendorHooks, ...options.hooks };
25395
25404
  const cancelGraceMs = options.cancelGraceMs ?? DEFAULT_CANCEL_GRACE_MS;
@@ -25599,6 +25608,7 @@ function createAcpExecutor(options) {
25599
25608
  const commands = parseAvailableCommandsUpdate(params);
25600
25609
  if (commands !== void 0) {
25601
25610
  availableCommands = commands;
25611
+ options.onAvailableCommands?.(commands);
25602
25612
  }
25603
25613
  if (sessionLoadInFlight) {
25604
25614
  droppedReplayUpdates += parsed.events.filter((event) => event._tag !== "ModeChanged").length;
@@ -26760,6 +26770,164 @@ async function probeCliVersion(input2) {
26760
26770
  }
26761
26771
  }
26762
26772
 
26773
+ // src/dshBalance.ts
26774
+ import * as NodeCrypto2 from "node:crypto";
26775
+ import * as NodeFS from "node:fs";
26776
+ import * as NodeOS from "node:os";
26777
+ import * as NodePath from "node:path";
26778
+ import {
26779
+ parseRetryAfterMs
26780
+ } from "@ycodium-ai/plugin-api/usage";
26781
+ var BALANCE_URL = "https://api.deepseek.com/user/balance";
26782
+ var OFFICIAL_HOSTS = /* @__PURE__ */ new Set(["api.deepseek.com"]);
26783
+ var API_KEY_NAME = "DEEPSEEK_API_KEY";
26784
+ var BASE_URL_NAME = "DEEPSEEK_BASE_URL";
26785
+ var TIMEOUT_MS = 1e4;
26786
+ var asObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
26787
+ var parseJson = (raw) => {
26788
+ try {
26789
+ return JSON.parse(raw);
26790
+ } catch {
26791
+ return null;
26792
+ }
26793
+ };
26794
+ var readFile = (path) => {
26795
+ try {
26796
+ return NodeFS.readFileSync(path, "utf8");
26797
+ } catch {
26798
+ return null;
26799
+ }
26800
+ };
26801
+ var unquote = (raw) => {
26802
+ const trimmed = raw.trim();
26803
+ const quoted = /^(["'])(.*)\1/.exec(trimmed);
26804
+ if (quoted?.[2] !== void 0) return quoted[2];
26805
+ return (trimmed.split(/\s+#/)[0] ?? "").trim();
26806
+ };
26807
+ var readCredentialsYamlRef = (yaml, name) => {
26808
+ let inRefs = false;
26809
+ for (const line of yaml.split(/\r?\n/)) {
26810
+ if (line.trim().length === 0 || line.trimStart().startsWith("#")) continue;
26811
+ if (!/^\s/.test(line)) {
26812
+ inRefs = /^refs\s*:\s*(?:#.*)?$/.test(line);
26813
+ continue;
26814
+ }
26815
+ if (!inRefs) continue;
26816
+ const match = /^\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*(.*)$/.exec(line);
26817
+ if (match?.[1] === name) {
26818
+ const value = unquote(match[2] ?? "");
26819
+ return value.length > 0 ? value : null;
26820
+ }
26821
+ }
26822
+ return null;
26823
+ };
26824
+ var readDotenvValue = (dotenv, name) => {
26825
+ for (const line of dotenv.split(/\r?\n/)) {
26826
+ const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/.exec(line);
26827
+ if (match?.[1] !== name) continue;
26828
+ const value = unquote(match[2] ?? "");
26829
+ return value.length > 0 ? value : null;
26830
+ }
26831
+ return null;
26832
+ };
26833
+ var nonEmpty = (value) => {
26834
+ const trimmed = value?.trim() ?? "";
26835
+ return trimmed.length > 0 ? trimmed : null;
26836
+ };
26837
+ var asMoney = (value) => {
26838
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
26839
+ return Number.isFinite(parsed) ? parsed : null;
26840
+ };
26841
+ var dshBalances = (body) => {
26842
+ const infos = asObject(body)?.["balance_infos"];
26843
+ if (!Array.isArray(infos)) return [];
26844
+ const balances = [];
26845
+ for (const entry of infos) {
26846
+ const info = asObject(entry);
26847
+ const currency = typeof info?.["currency"] === "string" ? info["currency"].trim() : "";
26848
+ const total = asMoney(info?.["total_balance"]);
26849
+ if (info === null || !/^[A-Z]{3}$/.test(currency) || total === null) continue;
26850
+ balances.push({
26851
+ id: `balance-${currency}`,
26852
+ label: { key: "usage.balance.account" },
26853
+ remaining: total,
26854
+ used: null,
26855
+ limit: null,
26856
+ unit: { kind: "money", currency },
26857
+ resetsAt: null
26858
+ });
26859
+ }
26860
+ return balances;
26861
+ };
26862
+ var createDshUsage = (input2) => {
26863
+ const now = input2.now ?? Date.now;
26864
+ const processEnv = input2.processEnv ?? process.env;
26865
+ const envValue = (name) => nonEmpty(input2.environment[name]) ?? nonEmpty(processEnv[name]);
26866
+ const dshHome = () => nonEmpty(input2.environment[input2.homeEnvVar]) ?? nonEmpty(input2.homePath) ?? nonEmpty(processEnv[input2.homeEnvVar]) ?? NodePath.join(NodeOS.homedir(), ".dsh");
26867
+ const resolveKey = () => {
26868
+ const fromEnvironment = envValue(API_KEY_NAME);
26869
+ if (fromEnvironment !== null) return fromEnvironment;
26870
+ const home = dshHome();
26871
+ const yaml = readFile(NodePath.join(home, ".credentials.yaml"));
26872
+ const fromYaml = yaml === null ? null : readCredentialsYamlRef(yaml, API_KEY_NAME);
26873
+ if (fromYaml !== null) return fromYaml;
26874
+ const dotenv = readFile(NodePath.join(home, ".env"));
26875
+ return dotenv === null ? null : readDotenvValue(dotenv, API_KEY_NAME);
26876
+ };
26877
+ const readQuota = async (request) => {
26878
+ const baseUrl = envValue(BASE_URL_NAME);
26879
+ if (baseUrl !== null) {
26880
+ const host = URL.parse(baseUrl)?.hostname.toLowerCase();
26881
+ if (host === void 0 || !OFFICIAL_HOSTS.has(host)) {
26882
+ return { status: "unavailable", reason: "customEndpoint" };
26883
+ }
26884
+ }
26885
+ const key = resolveKey();
26886
+ if (key === null) return { status: "unavailable", reason: "missingCredentials" };
26887
+ let response;
26888
+ try {
26889
+ response = await input2.fetch(BALANCE_URL, {
26890
+ headers: { Authorization: `Bearer ${key}`, Accept: "application/json" },
26891
+ signal: AbortSignal.any([
26892
+ ...request.signal === void 0 ? [] : [request.signal],
26893
+ AbortSignal.timeout(TIMEOUT_MS)
26894
+ ])
26895
+ });
26896
+ } catch {
26897
+ return { status: "unavailable", reason: "unreachable" };
26898
+ }
26899
+ if (response.status === 401 || response.status === 403) {
26900
+ return { status: "unavailable", reason: "authRejected" };
26901
+ }
26902
+ if (response.status === 429) {
26903
+ const retryAfterMs = parseRetryAfterMs(response.headers.get("retry-after"), now());
26904
+ return {
26905
+ status: "unavailable",
26906
+ reason: "rateLimited",
26907
+ ...retryAfterMs === null ? {} : { retryAfterMs }
26908
+ };
26909
+ }
26910
+ if (!response.ok) return { status: "unavailable", reason: "unreachable" };
26911
+ let body;
26912
+ try {
26913
+ body = parseJson(await response.text());
26914
+ } catch {
26915
+ return { status: "unavailable", reason: "unreachable" };
26916
+ }
26917
+ if (asObject(body) === null || !Array.isArray(asObject(body)?.["balance_infos"])) {
26918
+ return { status: "unavailable", reason: "unreachable" };
26919
+ }
26920
+ return {
26921
+ status: "available",
26922
+ windows: [],
26923
+ balances: dshBalances(body),
26924
+ // 同一把 key 的两个实例是同一个账户:用 key 的指纹合成一行(指纹不可逆,不含 key 本身)。
26925
+ accountKey: `deepseek:${NodeCrypto2.createHash("sha256").update(key).digest("hex").slice(0, 12)}`
26926
+ };
26927
+ };
26928
+ return { readQuota };
26929
+ };
26930
+
26763
26931
  // src/config.ts
26764
26932
  var DSH_DEFAULT_LAUNCH_ARGS = "--profile acp";
26765
26933
  function isRecord6(value) {
@@ -26857,9 +27025,9 @@ function tokenizeLaunchArgs(value) {
26857
27025
  }
26858
27026
 
26859
27027
  // src/probeModels.ts
26860
- import * as NodeFS from "node:fs";
26861
- import * as NodeOS2 from "node:os";
26862
- import * as NodePath2 from "node:path";
27028
+ import * as NodeFS2 from "node:fs";
27029
+ import * as NodeOS3 from "node:os";
27030
+ import * as NodePath3 from "node:path";
26863
27031
  import {
26864
27032
  describeExecutorSpawnFailure,
26865
27033
  ExecutorSpawnRefusedError
@@ -26867,8 +27035,8 @@ import {
26867
27035
  import { nodeStreamsFromHandle } from "@ycodium-ai/plugin-api/process-stdin";
26868
27036
 
26869
27037
  // src/spawnEnv.ts
26870
- import * as NodeOS from "node:os";
26871
- import * as NodePath from "node:path";
27038
+ import * as NodeOS2 from "node:os";
27039
+ import * as NodePath2 from "node:path";
26872
27040
  import { mergeLaunchEnv } from "@ycodium-ai/plugin-api/executor";
26873
27041
 
26874
27042
  // src/permission.ts
@@ -26911,20 +27079,20 @@ function dshPermissionEnv(nativeModeId) {
26911
27079
 
26912
27080
  // src/spawnEnv.ts
26913
27081
  function expandHomePath(value) {
26914
- if (value === "~") return NodeOS.homedir();
27082
+ if (value === "~") return NodeOS2.homedir();
26915
27083
  if (value.startsWith("~/") || value.startsWith("~\\")) {
26916
- return NodePath.join(NodeOS.homedir(), value.slice(2));
27084
+ return NodePath2.join(NodeOS2.homedir(), value.slice(2));
26917
27085
  }
26918
27086
  return value;
26919
27087
  }
26920
27088
  function resolveHomePath(value) {
26921
27089
  const trimmed = value.trim();
26922
27090
  if (trimmed.length === 0) return "";
26923
- return NodePath.resolve(expandHomePath(trimmed));
27091
+ return NodePath2.resolve(expandHomePath(trimmed));
26924
27092
  }
26925
27093
  function dshContinuationKey(homePath) {
26926
27094
  const resolved = resolveHomePath(homePath);
26927
- return `dsh:home:${resolved.length > 0 ? resolved : NodePath.join(NodeOS.homedir(), ".dsh")}`;
27095
+ return `dsh:home:${resolved.length > 0 ? resolved : NodePath2.join(NodeOS2.homedir(), ".dsh")}`;
26928
27096
  }
26929
27097
  function buildSpawnEnv(recipe, resolvedHomePath, nativeModeId) {
26930
27098
  const homeEnvVar = recipe.homeEnvVar.trim() || DSH_CARD.homeEnvVar;
@@ -26942,7 +27110,7 @@ function buildSpawnEnv(recipe, resolvedHomePath, nativeModeId) {
26942
27110
  var PROBE_REQUEST_TIMEOUT_MS = 15e3;
26943
27111
  function dshProbeSessionCwd(instanceId) {
26944
27112
  const safe = instanceId.trim().replace(/[^A-Za-z0-9._-]/g, "-") || "instance";
26945
- return NodePath2.join(NodeOS2.tmpdir(), `ycodium-dsh-probe-${safe}`);
27113
+ return NodePath3.join(NodeOS3.tmpdir(), `ycodium-dsh-probe-${safe}`);
26946
27114
  }
26947
27115
  function isRecord7(value) {
26948
27116
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -27004,7 +27172,7 @@ async function probeDshModels(input2) {
27004
27172
  const { recipe, host } = input2;
27005
27173
  const probeCwd = dshProbeSessionCwd(host.instanceId);
27006
27174
  try {
27007
- NodeFS.mkdirSync(probeCwd, { recursive: true });
27175
+ NodeFS2.mkdirSync(probeCwd, { recursive: true });
27008
27176
  } catch (error62) {
27009
27177
  return { kind: "failed", detail: failureDetail(error62) };
27010
27178
  }
@@ -27191,6 +27359,11 @@ var createDshInstance = (config2, host) => {
27191
27359
  providerInstanceId: host.instanceId,
27192
27360
  clientInfo: DSH_CLIENT_INFO,
27193
27361
  log: host.log,
27362
+ // agent 自己宣告的斜杠命令按会话报给宿主,输入区的 / 菜单才有它们。
27363
+ onAvailableCommands: (commands) => host.reportSessionInventory?.({
27364
+ threadId: input2.threadId,
27365
+ slashCommands: acpCommandsToSlashCommands(commands)
27366
+ }),
27194
27367
  // 该 agent 不实现 session/load,只实现 session/resume——续接重新激活
27195
27368
  // 持久会话而不重放记录,发 load 会卡在永远不结算的重放门上。
27196
27369
  resumeMethod: "session/resume",
@@ -27323,6 +27496,13 @@ var createDshInstance = (config2, host) => {
27323
27496
  return { threadId, turns: booked.turns };
27324
27497
  },
27325
27498
  events,
27499
+ // 额度是 DeepSeek 账户的剩余金额(API key,没有订阅窗口);token 暂无来源,见 dshBalance.ts 与 README。
27500
+ usage: createDshUsage({
27501
+ environment: recipe.environment,
27502
+ homePath: resolveHomePath(recipe.homePath),
27503
+ homeEnvVar: recipe.homeEnvVar.trim() || DSH_CARD.homeEnvVar,
27504
+ fetch: host.fetch
27505
+ }),
27326
27506
  continuationKey: dshContinuationKey(recipe.homePath),
27327
27507
  dispose: async () => {
27328
27508
  if (disposed) return;
package/locales/en.json CHANGED
@@ -10,5 +10,10 @@
10
10
  "description": "Sessions 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 (~/.dsh).",
11
11
  "placeholder": "e.g. ~/.dsh-work"
12
12
  }
13
+ },
14
+ "usage": {
15
+ "balance": {
16
+ "account": "Account balance"
17
+ }
13
18
  }
14
19
  }
@@ -10,5 +10,10 @@
10
10
  "description": "会话保存在这里,因此共用同一目录的 provider 可以接手彼此的会话,目录不同的则不能。留空则使用该命令行工具的默认目录(~/.dsh)。",
11
11
  "placeholder": "例如 ~/.dsh-work"
12
12
  }
13
+ },
14
+ "usage": {
15
+ "balance": {
16
+ "account": "账户余额"
17
+ }
13
18
  }
14
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-dsh",
3
- "version": "0.2.2690",
3
+ "version": "0.2.2696",
4
4
  "description": "DeepSeek Harness executor: dsh CLI 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-executor-testing": "0.0.0",
26
- "@ycodium-ai/plugin-api": "0.2.2690"
24
+ "@ycodium-ai/plugin-api": "0.2.2696",
25
+ "ycodium-acp-executor": "0.2.2696",
26
+ "ycodium-executor-testing": "0.0.0"
27
27
  },
28
28
  "ycodium": {
29
29
  "plugin": "./dist/index.js",