@trops/dash-core 0.1.622 → 0.1.623

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.
@@ -972,10 +972,15 @@ const BOTS_RUN$1 = "bots-run";
972
972
  const BOTS_STOP$1 = "bots-stop";
973
973
  const BOTS_APPROVE$1 = "bots-approve";
974
974
  const BOTS_LIST_APPROVALS$1 = "bots-list-approvals";
975
+ const BOTS_GET_BUDGETS$1 = "bots-get-budgets";
976
+ const BOTS_SET_BUDGET$1 = "bots-set-budget";
977
+ const BOTS_GET_SPEND$1 = "bots-get-spend";
978
+ const BOTS_RESUME_BUDGET$1 = "bots-resume-budget";
975
979
 
976
980
  // --- Main → Renderer (send) ---
977
981
  const BOT_STREAM$2 = "bot-stream"; // { botId, event: BotEvent }
978
982
  const BOT_APPROVAL_PENDING$2 = "bot-approval-pending"; // { id, request }
983
+ const BOT_BUDGET_ALERT$2 = "bot-budget-alert"; // { botId, cost, estimated, status }
979
984
 
980
985
  var botEvents$1 = {
981
986
  BOTS_LIST: BOTS_LIST$1,
@@ -986,8 +991,13 @@ var botEvents$1 = {
986
991
  BOTS_STOP: BOTS_STOP$1,
987
992
  BOTS_APPROVE: BOTS_APPROVE$1,
988
993
  BOTS_LIST_APPROVALS: BOTS_LIST_APPROVALS$1,
994
+ BOTS_GET_BUDGETS: BOTS_GET_BUDGETS$1,
995
+ BOTS_SET_BUDGET: BOTS_SET_BUDGET$1,
996
+ BOTS_GET_SPEND: BOTS_GET_SPEND$1,
997
+ BOTS_RESUME_BUDGET: BOTS_RESUME_BUDGET$1,
989
998
  BOT_STREAM: BOT_STREAM$2,
990
999
  BOT_APPROVAL_PENDING: BOT_APPROVAL_PENDING$2,
1000
+ BOT_BUDGET_ALERT: BOT_BUDGET_ALERT$2,
991
1001
  };
992
1002
 
993
1003
  /**
@@ -40808,7 +40818,7 @@ function getProvider$2(providerId) {
40808
40818
  return PROVIDERS[providerId] || PROVIDERS[DEFAULT_PROVIDER$1];
40809
40819
  }
40810
40820
 
40811
- function getCuratedModels(providerId) {
40821
+ function getCuratedModels$1(providerId) {
40812
40822
  return getProvider$2(providerId).curatedModels.slice();
40813
40823
  }
40814
40824
 
@@ -40860,7 +40870,7 @@ var modelProviders = {
40860
40870
  PROVIDERS,
40861
40871
  DEFAULT_PROVIDER: DEFAULT_PROVIDER$1,
40862
40872
  getProvider: getProvider$2,
40863
- getCuratedModels,
40873
+ getCuratedModels: getCuratedModels$1,
40864
40874
  getDefaultModel: getDefaultModel$1,
40865
40875
  migrateModelId: migrateModelId$3,
40866
40876
  isKnownRetired,
@@ -73658,6 +73668,7 @@ const Store = require$$0$5;
73658
73668
  */
73659
73669
  function createElectronHost$1() {
73660
73670
  const store = new Store({ name: "dash-bots" });
73671
+ const budgetStore = new Store({ name: "dash-bot-budgets" });
73661
73672
  const botsRoot = path$3.join(app$2.getPath("userData"), "bots");
73662
73673
 
73663
73674
  return {
@@ -73669,6 +73680,12 @@ function createElectronHost$1() {
73669
73680
  read: () => store.get("store", { bots: {}, runs: {} }),
73670
73681
  write: (obj) => store.set("store", obj),
73671
73682
  },
73683
+ // Separate blob for budgets + monthly spend (Slice 5).
73684
+ budgetPersistence: {
73685
+ read: () =>
73686
+ budgetStore.get("store", { budgets: {}, spend: {}, overrides: {} }),
73687
+ write: (obj) => budgetStore.set("store", obj),
73688
+ },
73672
73689
  };
73673
73690
  }
73674
73691
 
@@ -74929,6 +74946,9 @@ let BotRunner$1 = class BotRunner {
74929
74946
  deps.makeRequestPermission || createRequestPermission;
74930
74947
  this._gate = deps.gate; // forwarded to PermissionGate ctx (optional)
74931
74948
  this._isPaused = deps.isPaused || (() => false);
74949
+ // Called after each run with the run's token usage so budgets (Slice 5)
74950
+ // can accrue cost. Optional — default no-op keeps the runner portable.
74951
+ this._onUsage = deps.onUsage || null;
74932
74952
  /** @type {Map<string, AbortController>} */
74933
74953
  this._active = new Map();
74934
74954
  }
@@ -74979,9 +74999,11 @@ let BotRunner$1 = class BotRunner {
74979
74999
  let status = "completed";
74980
75000
  let usage = null;
74981
75001
  let errorMessage = null;
75002
+ let runProfile = null;
74982
75003
 
74983
75004
  try {
74984
75005
  const profile = await this._resolveRunProfile(bot);
75006
+ runProfile = profile;
74985
75007
  const engine = this._engines.getEngine(profile.engineId);
74986
75008
  if (!engine) {
74987
75009
  throw new Error(`no engine registered for "${profile.engineId}"`);
@@ -75068,6 +75090,19 @@ let BotRunner$1 = class BotRunner {
75068
75090
  error: errorMessage,
75069
75091
  };
75070
75092
  this._store.appendRun(botId, runRecord);
75093
+
75094
+ // Feed usage to budgets (Slice 5). Optional hook; only when we captured
75095
+ // usage and know which provider/model produced it.
75096
+ if (usage && runProfile && this._onUsage) {
75097
+ this._onUsage({
75098
+ botId,
75099
+ workspaceId: bot.workspaceId,
75100
+ providerId: runProfile.providerId,
75101
+ model: runProfile.model,
75102
+ usage,
75103
+ });
75104
+ }
75105
+
75071
75106
  return runRecord;
75072
75107
  }
75073
75108
  };
@@ -75180,6 +75215,211 @@ let BotScheduler$1 = class BotScheduler {
75180
75215
 
75181
75216
  var BotScheduler_1 = BotScheduler$1;
75182
75217
 
75218
+ var BudgetController$2 = {exports: {}};
75219
+
75220
+ /**
75221
+ * BudgetController.js
75222
+ *
75223
+ * Per-run cost capture and monthly spend limits for bots (PRD US-019 / FR-017).
75224
+ * Records each run's token usage as a dollar cost (from the provider registry's
75225
+ * per-model pricing), accrues it to bot / workspace / global monthly buckets,
75226
+ * and reports status: ok, warn (≥80%), or exceeded (≥100%). A bot that has
75227
+ * exceeded any applicable budget is over-budget and should auto-pause; the user
75228
+ * may explicitly (and audibly) override to keep it running for the rest of the
75229
+ * month.
75230
+ *
75231
+ * Portable (NFR-006): persistence, pricing lookup, clock, and audit sink are
75232
+ * injected. No Electron.
75233
+ *
75234
+ * new BudgetController({ persistence, getPricing, clock?, audit? })
75235
+ * persistence: { read(): object, write(obj): void } // { budgets, spend, overrides }
75236
+ * getPricing: (providerId, model) => { input, output } | null // USD per 1M tokens
75237
+ * clock: () => number (epoch ms)
75238
+ * audit: (entry) => void
75239
+ */
75240
+
75241
+ const WARN_AT = 0.8;
75242
+
75243
+ function monthKey(ms) {
75244
+ const d = new Date(ms);
75245
+ return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
75246
+ }
75247
+
75248
+ function scopeKey(scope, id) {
75249
+ if (scope === "global") return "global";
75250
+ if (scope === "workspace") return `workspace:${id}`;
75251
+ if (scope === "bot") return `bot:${id}`;
75252
+ throw new Error(`BudgetController: unknown scope "${scope}"`);
75253
+ }
75254
+
75255
+ function stateFor(spent, budget) {
75256
+ if (!(budget > 0)) return "ok"; // no limit set
75257
+ const pct = spent / budget;
75258
+ if (pct >= 1) return "exceeded";
75259
+ if (pct >= WARN_AT) return "warn";
75260
+ return "ok";
75261
+ }
75262
+
75263
+ // Most-restrictive wins.
75264
+ const RANK = { ok: 0, warn: 1, exceeded: 2 };
75265
+
75266
+ let BudgetController$1 = class BudgetController {
75267
+ constructor({ persistence, getPricing, clock, audit } = {}) {
75268
+ if (!persistence || typeof persistence.read !== "function") {
75269
+ throw new Error(
75270
+ "BudgetController: persistence with read()/write() is required",
75271
+ );
75272
+ }
75273
+ this._persistence = persistence;
75274
+ this._getPricing = getPricing || (() => null);
75275
+ this._now = clock || (() => Date.now());
75276
+ this._audit = audit || (() => {});
75277
+ }
75278
+
75279
+ _load() {
75280
+ const d = this._persistence.read() || {};
75281
+ if (!d.budgets) d.budgets = {};
75282
+ if (!d.spend) d.spend = {};
75283
+ if (!d.overrides) d.overrides = {};
75284
+ return d;
75285
+ }
75286
+
75287
+ _save(d) {
75288
+ this._persistence.write(d);
75289
+ }
75290
+
75291
+ /** Set a monthly USD limit for a scope. @param scope "bot"|"workspace"|"global" */
75292
+ setBudget(scope, id, monthlyUsd) {
75293
+ const key = scopeKey(scope, id);
75294
+ const d = this._load();
75295
+ if (monthlyUsd == null) delete d.budgets[key];
75296
+ else d.budgets[key] = monthlyUsd;
75297
+ this._save(d);
75298
+ this._audit({ type: "budget-set", scope: key, monthlyUsd });
75299
+ return d.budgets[key];
75300
+ }
75301
+
75302
+ /** @returns {object} scopeKey → monthly USD limit */
75303
+ getBudgets() {
75304
+ return { ...this._load().budgets };
75305
+ }
75306
+
75307
+ /**
75308
+ * Record a run's usage. Accrues cost to the bot/workspace/global buckets for
75309
+ * the current month.
75310
+ * @returns {{ cost: number, estimated: boolean, status: object }}
75311
+ */
75312
+ recordUsage({ botId, workspaceId, providerId, model, usage } = {}) {
75313
+ const inTok = (usage && usage.inputTokens) || 0;
75314
+ const outTok = (usage && usage.outputTokens) || 0;
75315
+ const pricing = this._getPricing(providerId, model);
75316
+ let cost = 0;
75317
+ let estimated = false;
75318
+ if (pricing) {
75319
+ cost = (inTok / 1e6) * pricing.input + (outTok / 1e6) * pricing.output;
75320
+ } else {
75321
+ // Unknown pricing — track tokens only and flag the figure as estimated.
75322
+ estimated = true;
75323
+ }
75324
+
75325
+ const d = this._load();
75326
+ const mk = monthKey(this._now());
75327
+ if (!d.spend[mk]) d.spend[mk] = {};
75328
+
75329
+ const keys = [scopeKey("bot", botId), scopeKey("global")];
75330
+ if (workspaceId) keys.splice(1, 0, scopeKey("workspace", workspaceId));
75331
+ for (const key of keys) {
75332
+ const bucket = d.spend[mk][key] || {
75333
+ usd: 0,
75334
+ inputTokens: 0,
75335
+ outputTokens: 0,
75336
+ };
75337
+ bucket.usd += cost;
75338
+ bucket.inputTokens += inTok;
75339
+ bucket.outputTokens += outTok;
75340
+ d.spend[mk][key] = bucket;
75341
+ }
75342
+ this._save(d);
75343
+
75344
+ this._audit({
75345
+ type: "usage",
75346
+ botId,
75347
+ workspaceId,
75348
+ cost,
75349
+ estimated,
75350
+ inTok,
75351
+ outTok,
75352
+ });
75353
+ return { cost, estimated, status: this.status(botId, workspaceId) };
75354
+ }
75355
+
75356
+ _spent(mk, key) {
75357
+ const d = this._load();
75358
+ return (d.spend[mk] && d.spend[mk][key] && d.spend[mk][key].usd) || 0;
75359
+ }
75360
+
75361
+ /**
75362
+ * Budget status for a bot across its applicable scopes.
75363
+ * @returns {{ scopes: object[], overall: "ok"|"warn"|"exceeded" }}
75364
+ */
75365
+ status(botId, workspaceId) {
75366
+ const d = this._load();
75367
+ const mk = monthKey(this._now());
75368
+ const applicable = [scopeKey("bot", botId)];
75369
+ if (workspaceId) applicable.push(scopeKey("workspace", workspaceId));
75370
+ applicable.push(scopeKey("global"));
75371
+
75372
+ const scopes = [];
75373
+ let overall = "ok";
75374
+ for (const key of applicable) {
75375
+ const budget = d.budgets[key];
75376
+ if (budget == null) continue; // no limit for this scope
75377
+ const spent = this._spent(mk, key);
75378
+ const state = stateFor(spent, budget);
75379
+ scopes.push({ scope: key, spent, budget, pct: spent / budget, state });
75380
+ if (RANK[state] > RANK[overall]) overall = state;
75381
+ }
75382
+ return { scopes, overall };
75383
+ }
75384
+
75385
+ /**
75386
+ * Whether a bot should be paused for budget. True when any applicable budget
75387
+ * is exceeded AND the user hasn't overridden for this month.
75388
+ */
75389
+ isOverBudget(botId, workspaceId) {
75390
+ const mk = monthKey(this._now());
75391
+ const d = this._load();
75392
+ if (d.overrides[mk] && d.overrides[mk][botId]) return false;
75393
+ return this.status(botId, workspaceId).overall === "exceeded";
75394
+ }
75395
+
75396
+ /**
75397
+ * Explicit, audited override to keep a budget-exceeded bot running for the
75398
+ * rest of the current month (US-019 AC6). Cleared automatically next month
75399
+ * (overrides are keyed by month).
75400
+ */
75401
+ resumeOverBudget(botId) {
75402
+ const d = this._load();
75403
+ const mk = monthKey(this._now());
75404
+ if (!d.overrides[mk]) d.overrides[mk] = {};
75405
+ d.overrides[mk][botId] = true;
75406
+ this._save(d);
75407
+ this._audit({ type: "budget-override", botId, month: mk });
75408
+ return true;
75409
+ }
75410
+
75411
+ /** @returns {object} spend buckets for a month (default current) */
75412
+ getSpend(mk) {
75413
+ const d = this._load();
75414
+ return d.spend[mk || monthKey(this._now())] || {};
75415
+ }
75416
+ };
75417
+
75418
+ BudgetController$2.exports = BudgetController$1;
75419
+ BudgetController$2.exports.WARN_AT = WARN_AT;
75420
+
75421
+ var BudgetControllerExports = BudgetController$2.exports;
75422
+
75183
75423
  /**
75184
75424
  * mcpResult.js
75185
75425
  *
@@ -75244,14 +75484,26 @@ const ApprovalRegistry = approvalsExports;
75244
75484
  const engines = engines$1;
75245
75485
  const BotRunner = BotRunner_1;
75246
75486
  const BotScheduler = BotScheduler_1;
75487
+ const BudgetController = BudgetControllerExports;
75247
75488
  const { normalizeMcpResult } = mcpResult;
75248
75489
  const {
75249
75490
  getProvider: getProvider$1,
75250
75491
  getDefaultModel,
75492
+ getCuratedModels,
75251
75493
  migrateModelId,
75252
75494
  DEFAULT_PROVIDER,
75253
75495
  } = modelProviders;
75254
- const { BOT_STREAM: BOT_STREAM$1, BOT_APPROVAL_PENDING: BOT_APPROVAL_PENDING$1 } = botEvents$1;
75496
+ const {
75497
+ BOT_STREAM: BOT_STREAM$1,
75498
+ BOT_APPROVAL_PENDING: BOT_APPROVAL_PENDING$1,
75499
+ BOT_BUDGET_ALERT: BOT_BUDGET_ALERT$1,
75500
+ } = botEvents$1;
75501
+
75502
+ /** Pricing lookup for BudgetController: curated-model pricing by provider. */
75503
+ function pricingFor(providerId, model) {
75504
+ const m = getCuratedModels(providerId).find((c) => c.value === model);
75505
+ return (m && m.pricing) || null;
75506
+ }
75255
75507
 
75256
75508
  const botController$1 = {
75257
75509
  _ready: false,
@@ -75299,6 +75551,12 @@ const botController$1 = {
75299
75551
  },
75300
75552
  };
75301
75553
 
75554
+ // Budgets: accrue per-run cost, auto-pause bots over their monthly cap.
75555
+ this._budgets = new BudgetController({
75556
+ persistence: host.budgetPersistence,
75557
+ getPricing: pricingFor,
75558
+ });
75559
+
75302
75560
  this._runner = new BotRunner({
75303
75561
  engines,
75304
75562
  store: this._store,
@@ -75307,6 +75565,24 @@ const botController$1 = {
75307
75565
  resolveTools: (bot) => this._resolveTools(bot),
75308
75566
  callTool: (serverName, toolName, args, o) =>
75309
75567
  this._callTool(serverName, toolName, args, o),
75568
+ // A bot over any applicable budget is paused at the permission gate.
75569
+ isPaused: (botId) =>
75570
+ this._budgets.isOverBudget(
75571
+ botId,
75572
+ (this._store.get(botId) || {}).workspaceId,
75573
+ ),
75574
+ // After each run, accrue cost and alert on warn/exceeded.
75575
+ onUsage: (u) => {
75576
+ const r = this._budgets.recordUsage(u);
75577
+ if (r.status.overall !== "ok") {
75578
+ this._broadcast(BOT_BUDGET_ALERT$1, {
75579
+ botId: u.botId,
75580
+ cost: r.cost,
75581
+ estimated: r.estimated,
75582
+ status: r.status,
75583
+ });
75584
+ }
75585
+ },
75310
75586
  });
75311
75587
 
75312
75588
  this._scheduler = new BotScheduler({
@@ -75374,6 +75650,26 @@ const botController$1 = {
75374
75650
  return this._approvals.list();
75375
75651
  },
75376
75652
 
75653
+ // ---- budgets (US-019) ---------------------------------------------------
75654
+
75655
+ getBudgets() {
75656
+ return this._budgets.getBudgets();
75657
+ },
75658
+
75659
+ setBudget(scope, id, monthlyUsd) {
75660
+ return this._budgets.setBudget(scope, id, monthlyUsd);
75661
+ },
75662
+
75663
+ /** Current-month (or given month) spend buckets by scope. */
75664
+ getSpend(month) {
75665
+ return this._budgets.getSpend(month);
75666
+ },
75667
+
75668
+ /** Explicit, audited override to keep a budget-exceeded bot running. */
75669
+ resumeBudget(botId) {
75670
+ return this._budgets.resumeOverBudget(botId);
75671
+ },
75672
+
75377
75673
  // ---- internals ----------------------------------------------------------
75378
75674
 
75379
75675
  _run(botId, opts) {
@@ -79367,8 +79663,13 @@ const {
79367
79663
  BOTS_STOP,
79368
79664
  BOTS_APPROVE,
79369
79665
  BOTS_LIST_APPROVALS,
79666
+ BOTS_GET_BUDGETS,
79667
+ BOTS_SET_BUDGET,
79668
+ BOTS_GET_SPEND,
79669
+ BOTS_RESUME_BUDGET,
79370
79670
  BOT_STREAM,
79371
79671
  BOT_APPROVAL_PENDING,
79672
+ BOT_BUDGET_ALERT,
79372
79673
  } = botEvents$1;
79373
79674
 
79374
79675
  let _nextListenerId = 0;
@@ -79397,10 +79698,20 @@ const botApi$1 = {
79397
79698
  ipcRenderer$3.invoke(BOTS_APPROVE, { approvalId, decision }),
79398
79699
  listApprovals: () => ipcRenderer$3.invoke(BOTS_LIST_APPROVALS),
79399
79700
 
79701
+ // --- Budgets (US-019) ---
79702
+ getBudgets: () => ipcRenderer$3.invoke(BOTS_GET_BUDGETS),
79703
+ /** scope: "bot"|"workspace"|"global" */
79704
+ setBudget: (scope, id, monthlyUsd) =>
79705
+ ipcRenderer$3.invoke(BOTS_SET_BUDGET, { scope, id, monthlyUsd }),
79706
+ getSpend: (month) => ipcRenderer$3.invoke(BOTS_GET_SPEND, { month }),
79707
+ resumeBudget: (botId) => ipcRenderer$3.invoke(BOTS_RESUME_BUDGET, { botId }),
79708
+
79400
79709
  /** Subscribe to streamed run events: { botId, event }. */
79401
79710
  onStream: (callback) => _addListener(BOT_STREAM, callback),
79402
79711
  /** Subscribe to new pending approvals: { id, request }. */
79403
79712
  onApprovalPending: (callback) => _addListener(BOT_APPROVAL_PENDING, callback),
79713
+ /** Subscribe to budget warn/exceeded alerts. */
79714
+ onBudgetAlert: (callback) => _addListener(BOT_BUDGET_ALERT, callback),
79404
79715
 
79405
79716
  removeListener: (id) => {
79406
79717
  const entry = _listenerMap.get(id);