@onepointfour-packs/flaredeck-agent 0.2.151 → 0.2.153

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.
@@ -0,0 +1,35 @@
1
+ export type SpendStatus = {
2
+ daily_usd: number | null;
3
+ monthly_usd: number | null;
4
+ today: {
5
+ date: string;
6
+ usd: number;
7
+ };
8
+ month: {
9
+ month: string;
10
+ usd: number;
11
+ };
12
+ over: {
13
+ limit: 'daily' | 'monthly';
14
+ until: number;
15
+ message: string;
16
+ } | null;
17
+ };
18
+ export type Spent = {
19
+ model: string;
20
+ prompt: number;
21
+ cached: number;
22
+ output: number;
23
+ thinking: number;
24
+ };
25
+ export type SpendClient = {
26
+ status(): Promise<SpendStatus | null>;
27
+ record(s: Spent): Promise<{
28
+ usd: number;
29
+ known_price: boolean;
30
+ status: SpendStatus;
31
+ } | null>;
32
+ };
33
+ export declare function spendFor(mcpUrl: string, auth: () => Promise<string>): SpendClient | null;
34
+ /** "$0.42 · today $3.10 of $20 · month $41 of $300", for the pane. */
35
+ export declare function spendLine(usd: number, s: SpendStatus): string;
package/dist/spend.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The workspace's Gemini spend, as the harness sees it: every agy run's tokens are reported to
3
+ * FlareDeck, which prices them from one list, and the answer says whether a daily or monthly
4
+ * dollar limit has been reached. FlareDeck is where the limit is set (Integrations → Secrets) and
5
+ * where the money is counted; the harness only reports and asks.
6
+ *
7
+ * Not reaching FlareDeck never stops the work: a limit that cannot be read is treated as not
8
+ * reached, and said once, because an outage at FlareDeck must not idle every agy agent.
9
+ */
10
+ import { callerFor } from './subscriptions.js';
11
+ export function spendFor(mcpUrl, auth) {
12
+ let tenantId = '';
13
+ return {
14
+ async status() {
15
+ const call = await caller();
16
+ if (!call)
17
+ return null;
18
+ const r = await call('GET', 'spend/gemini/check');
19
+ return r.ok ? r.json : null;
20
+ },
21
+ async record(s) {
22
+ const call = await caller();
23
+ if (!call)
24
+ return null;
25
+ const r = await call('POST', 'spend/gemini', {
26
+ model: s.model,
27
+ usage: { prompt: s.prompt, cached: s.cached, output: s.output, thinking: s.thinking },
28
+ });
29
+ return r.ok ? r.json : null;
30
+ },
31
+ };
32
+ async function caller() {
33
+ if (!tenantId) {
34
+ try {
35
+ const part = (await auth()).split('.')[1] ?? '';
36
+ tenantId = String(JSON.parse(Buffer.from(part, 'base64url').toString('utf8')).tenantId ?? '');
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ if (!tenantId)
42
+ return null;
43
+ }
44
+ return callerFor({ tenantId, baseUrl: `${new URL(mcpUrl).origin}/api`, auth });
45
+ }
46
+ }
47
+ /** "$0.42 · today $3.10 of $20 · month $41 of $300", for the pane. */
48
+ export function spendLine(usd, s) {
49
+ const of = (n) => (n === null ? '' : ` of $${n.toFixed(0)}`);
50
+ return `gemini: $${usd.toFixed(2)} this run · today $${s.today.usd.toFixed(2)}${of(s.daily_usd)} · month $${s.month.usd.toFixed(2)}${of(s.monthly_usd)}`;
51
+ }
52
+ //# sourceMappingURL=spend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spend.js","sourceRoot":"","sources":["../src/spend.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAiB/C,MAAM,UAAU,QAAQ,CAAC,MAAc,EAAE,IAA2B;IAClE,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,OAAO;QACL,KAAK,CAAC,MAAM;YACV,MAAM,IAAI,GAAG,MAAM,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YACvB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,oBAAoB,CAAC,CAAC;YAClD,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,IAA+B,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1D,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,MAAM,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI;gBAAE,OAAO,IAAI,CAAC;YACvB,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE;gBAC3C,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,KAAK,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE;aACtF,CAAC,CAAC;YACH,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAE,CAAC,CAAC,IAA8E,CAAC,CAAC,CAAC,IAAI,CAAC;QACzG,CAAC;KACF,CAAC;IAEF,KAAK,UAAU,MAAM;QACnB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAChD,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;YAChG,CAAC;YAAC,MAAM,CAAC;gBAAC,OAAO,IAAI,CAAC;YAAC,CAAC;YACxB,IAAI,CAAC,QAAQ;gBAAE,OAAO,IAAI,CAAC;QAC7B,CAAC;QACD,OAAO,SAAS,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,MAAM,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACjF,CAAC;AACH,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,SAAS,CAAC,GAAW,EAAE,CAAc;IACnD,MAAM,EAAE,GAAG,CAAC,CAAgB,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC5E,OAAO,YAAY,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC;AAC3J,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onepointfour-packs/flaredeck-agent",
3
- "version": "0.2.151",
3
+ "version": "0.2.153",
4
4
  "description": "Run a FlareDeck agent on your own machine: it picks up cards assigned to it, works each one in a checkout of its own, and reports back on the card.",
5
5
  "type": "module",
6
6
  "bin": {
package/python/sidecar.py CHANGED
@@ -30,6 +30,18 @@ from google.antigravity.types import HookResult, McpStreamableHttpServer
30
30
  GUARD_URL = ""
31
31
 
32
32
 
33
+ def emit_spend(agent, model: str) -> None:
34
+ try:
35
+ u = agent.conversation.total_usage
36
+ except Exception: # noqa: BLE001 — no conversation, nothing was billed
37
+ return
38
+ if not (u.total_token_count or 0):
39
+ return
40
+ emit({"type": "event", "kind": "spend", "model": str(model or "unknown"),
41
+ "prompt": u.prompt_token_count or 0, "cached": u.cached_content_token_count or 0,
42
+ "output": u.candidates_token_count or 0, "thinking": u.thoughts_token_count or 0})
43
+
44
+
33
45
  def emit(obj: dict) -> None:
34
46
  """One JSON object per line, flushed — Node is reading this as a stream, not a file."""
35
47
  sys.stdout.write(json.dumps(obj) + "\n")
@@ -359,7 +371,12 @@ async def main() -> None:
359
371
  "apiKeySource": "vertex" if kwargs.get("vertex") else "gemini-api-key",
360
372
  "permissionMode": "harness-gated",
361
373
  "sessionId": str(getattr(agent, "conversation_id", "") or "")})
362
- text = await run_turn(agent, first["brief"], lines)
374
+ try:
375
+ text = await run_turn(agent, first["brief"], lines)
376
+ finally:
377
+ # Every token this run used, whether it finished or not — a failed turn is billed too.
378
+ # The harness turns it into dollars and checks the workspace's Gemini spend limit.
379
+ emit_spend(agent, first.get("model") or "")
363
380
  # The session id only exists AFTER the first turn — conversation_id is None until then —
364
381
  # so the ready event above could never carry it, and the harness, finding no session,
365
382
  # skipped the handover entirely: "no session to write a handover from". That handover is