@hasna/economy 0.5.6 → 0.5.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.7
4
+
5
+ ### Patch Changes
6
+
7
+ - Support first-phone QR setup through private owner-Terminal login or explicitly confirmed signup. Accept signup HTTP 201 and attempt cleanup on catchable Terminal shutdown while preserving supplied sessions.
8
+
3
9
  ## 0.5.6
4
10
 
5
11
  ### Patch Changes
@@ -4,7 +4,7 @@ import {
4
4
  } from "./index-w3bvv0ns.js";
5
5
  import {
6
6
  getStore
7
- } from "./index-zs5dba3t.js";
7
+ } from "./index-z2n5y111.js";
8
8
  import {
9
9
  formatReportedUsd,
10
10
  formatSummaryCoverage,
@@ -20,7 +20,7 @@ import {
20
20
  // package.json
21
21
  var package_default = {
22
22
  name: "@hasna/economy",
23
- version: "0.5.6",
23
+ version: "0.5.7",
24
24
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
25
25
  repository: {
26
26
  type: "git",
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  LOCAL_STORAGE_OPT_IN_KEYS
4
- } from "./index-m12xttw1.js";
4
+ } from "./index-b6c649mq.js";
5
5
  import {
6
6
  assessCostFromDb,
7
7
  getIngestState,
@@ -14,7 +14,7 @@ import {
14
14
  querySavingsSummary,
15
15
  requireCurrentBillingDiffSummary,
16
16
  requireCurrentSavingsReport
17
- } from "./index-m12xttw1.js";
17
+ } from "./index-b6c649mq.js";
18
18
  import {
19
19
  deleteBudget,
20
20
  deleteGoal,
@@ -5,7 +5,7 @@ import {
5
5
  } from "./index-w3bvv0ns.js";
6
6
  import {
7
7
  syncAll
8
- } from "./index-qvcafabj.js";
8
+ } from "./index-wq7c4wqs.js";
9
9
  import {
10
10
  buildBrief,
11
11
  packageMetadata,
@@ -16,7 +16,7 @@ import {
16
16
  queryProjectDetail,
17
17
  queryRangeStats,
18
18
  querySavingsSummary
19
- } from "./index-m12xttw1.js";
19
+ } from "./index-b6c649mq.js";
20
20
  import {
21
21
  AGENTS,
22
22
  INGEST_SCHEMA_SQL,
@@ -6,10 +6,10 @@ import {
6
6
  printStatusLine,
7
7
  printWaybarJson,
8
8
  runTui
9
- } from "./index-zqsx461z.js";
9
+ } from "./index-6yr4hb72.js";
10
10
  import"./index-w3bvv0ns.js";
11
- import"./index-zs5dba3t.js";
12
- import"./index-m12xttw1.js";
11
+ import"./index-z2n5y111.js";
12
+ import"./index-b6c649mq.js";
13
13
  import"./index-f5jv6q5q.js";
14
14
  import"./index-p35cyhc4.js";
15
15
  export {
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  agentPaths,
4
4
  syncAll
5
- } from "./index-qvcafabj.js";
5
+ } from "./index-wq7c4wqs.js";
6
6
  import {
7
7
  getStore,
8
8
  isCloudStore
9
- } from "./index-zs5dba3t.js";
10
- import"./index-m12xttw1.js";
9
+ } from "./index-z2n5y111.js";
10
+ import"./index-b6c649mq.js";
11
11
  import {
12
12
  formatSummaryValue
13
13
  } from "./index-f5jv6q5q.js";
package/dist/cli/index.js CHANGED
@@ -8,22 +8,22 @@ import {
8
8
  import {
9
9
  buildStatusLine,
10
10
  gatherStatusData
11
- } from "../chunks/index-zqsx461z.js";
11
+ } from "../chunks/index-6yr4hb72.js";
12
12
  import"../chunks/index-w3bvv0ns.js";
13
13
  import {
14
14
  agentPaths,
15
15
  syncAll
16
- } from "../chunks/index-qvcafabj.js";
16
+ } from "../chunks/index-wq7c4wqs.js";
17
17
  import {
18
18
  getStore,
19
19
  isCloudStore
20
- } from "../chunks/index-zs5dba3t.js";
20
+ } from "../chunks/index-z2n5y111.js";
21
21
  import {
22
22
  billingDriftCheck,
23
23
  economyCloudStorage,
24
24
  economyTransportReport,
25
25
  packageMetadata
26
- } from "../chunks/index-m12xttw1.js";
26
+ } from "../chunks/index-b6c649mq.js";
27
27
  import {
28
28
  AGENTS,
29
29
  MAX_INGEST_BYTES,
@@ -1357,23 +1357,23 @@ function registerExtendedCommands(program) {
1357
1357
  console.log();
1358
1358
  });
1359
1359
  program.command("tui").description("Terminal status dashboard (Linux/SSH friendly)").option("--watch", "Live refresh").option("--interval <seconds>", "Refresh interval", "30").action(async (opts) => {
1360
- const { runTui } = await import("../chunks/tui-4bh067a3.js");
1360
+ const { runTui } = await import("../chunks/tui-9t7340wd.js");
1361
1361
  await runTui({
1362
1362
  watch: Boolean(opts.watch),
1363
1363
  interval: Number(opts.interval ?? 30)
1364
1364
  });
1365
1365
  });
1366
1366
  program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
1367
- const { printWaybarJson } = await import("../chunks/tui-4bh067a3.js");
1367
+ const { printWaybarJson } = await import("../chunks/tui-9t7340wd.js");
1368
1368
  await printWaybarJson();
1369
1369
  });
1370
1370
  const barCmd = program.command("bar").description("Status bar helpers (Linux)");
1371
1371
  barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
1372
- const { runTui } = await import("../chunks/tui-4bh067a3.js");
1372
+ const { runTui } = await import("../chunks/tui-9t7340wd.js");
1373
1373
  await runTui({ watch: Boolean(opts.watch) });
1374
1374
  });
1375
1375
  barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
1376
- const { printWaybarJson } = await import("../chunks/tui-4bh067a3.js");
1376
+ const { printWaybarJson } = await import("../chunks/tui-9t7340wd.js");
1377
1377
  await printWaybarJson();
1378
1378
  });
1379
1379
  program.command("estimate").description("Estimate cost for token counts").requiredOption("--model <model>", "Model name").option("--input <n>", "Input tokens", "0").option("--output <n>", "Output tokens", "0").action(async (opts) => {
@@ -3198,7 +3198,7 @@ program.command("accounts [period]").description("List account usage by email ad
3198
3198
  console.log();
3199
3199
  });
3200
3200
  program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
3201
- const { watchCosts } = await import("../chunks/watch-227kxmra.js");
3201
+ const { watchCosts } = await import("../chunks/watch-t6ssnx30.js");
3202
3202
  await watchCosts({
3203
3203
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
3204
3204
  daemon: Boolean(opts.daemon),
@@ -3434,7 +3434,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
3434
3434
  });
3435
3435
  program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
3436
3436
  const port = parseCliPort(opts.port ?? "3456", "--port");
3437
- const { startServer } = await import("../chunks/serve-ept6cfbe.js");
3437
+ const { startServer } = await import("../chunks/serve-ms8dvf7e.js");
3438
3438
  startServer(port);
3439
3439
  });
3440
3440
  program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import { randomUUID } from "crypto";
62
62
  // package.json
63
63
  var package_default = {
64
64
  name: "@hasna/economy",
65
- version: "0.5.6",
65
+ version: "0.5.7",
66
66
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
67
67
  repository: {
68
68
  type: "git",
package/dist/mcp/index.js CHANGED
@@ -85,7 +85,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
85
85
  // package.json
86
86
  var package_default = {
87
87
  name: "@hasna/economy",
88
- version: "0.5.6",
88
+ version: "0.5.7",
89
89
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
90
90
  repository: {
91
91
  type: "git",
@@ -3480,7 +3480,7 @@ async function pushIngestRows(cloud, scratch) {
3480
3480
  // package.json
3481
3481
  var package_default = {
3482
3482
  name: "@hasna/economy",
3483
- version: "0.5.6",
3483
+ version: "0.5.7",
3484
3484
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
3485
3485
  repository: {
3486
3486
  type: "git",
@@ -5578,7 +5578,7 @@ function isAuthorizedRequest(req, path) {
5578
5578
  // package.json
5579
5579
  var package_default = {
5580
5580
  name: "@hasna/economy",
5581
- version: "0.5.6",
5581
+ version: "0.5.7",
5582
5582
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5583
5583
  repository: {
5584
5584
  type: "git",
@@ -7426,7 +7426,7 @@ import { randomUUID } from "crypto";
7426
7426
  // package.json
7427
7427
  var package_default = {
7428
7428
  name: "@hasna/economy",
7429
- version: "0.5.6",
7429
+ version: "0.5.7",
7430
7430
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
7431
7431
  repository: {
7432
7432
  type: "git",
package/native/README.md CHANGED
@@ -30,8 +30,28 @@ Never run real codes in a captured agent terminal. The helper obtains a
30
30
  single-use code, hands only that temporary code to the CoreImage renderer and
31
31
  clears the display within five minutes. `--self-test` decodes a synthetic QR;
32
32
  `test_pair_qr.py` proves transcript/agent-key refusal and credential confinement.
33
- The existing Identities human login flow remains the bootstrap when no human
34
- session exists; pairing does not create or assume an account.
33
+ Without a saved human session, use
34
+ `python3 native/Tools/pair-qr.py --tenant <workspace-id> --login-email <human-email>`.
35
+ The helper opens the owner's Terminal, asks for confirmation before requesting
36
+ one sign-in email, and reads the verification code there with echo disabled.
37
+ It verifies a human principal and the selected workspace before displaying the
38
+ one-use QR. The new setup session stays in memory. On renderer exit or catchable
39
+ Terminal shutdown (HUP, TERM or Ctrl-C), setup makes one timeout-limited logout
40
+ attempt and reports an unconfirmed result as a failure. Force-kill, a reboot,
41
+ network failure or an interrupted verification response can prevent confirmation;
42
+ check the account's sessions before retrying in those cases. An existing session
43
+ supplied with `--session-ref` is retained.
44
+ Only the temporary QR code reaches the renderer, and no response or verification
45
+ code is returned to the agent terminal.
46
+
47
+ For a first account, `--create-account` offers signup through the existing
48
+ Identities flow instead of login. It requires direct confirmation in the owner
49
+ Terminal before account creation or email delivery. It does not create an
50
+ organization, grant an owner role, alter memberships or configure Economy's
51
+ store binding. An existing-account refusal is not retried as login, and failed
52
+ delivery/verification is not retried automatically. Never type the owner's code
53
+ for them or copy it into an agent session. A source/fixture test does not prove
54
+ real email delivery, account authority or a successful phone connection.
35
55
 
36
56
  The adapter verifies Economy's `/auth/session` subject, tenant, effective scopes
37
57
  and integer expiry. Viewer sign-in never requests write access. Refresh and period
@@ -6,14 +6,17 @@ Pass an existing authorized human Identities session's Secrets reference using
6
6
  verifies the session and selected tenant; no station agent key works.
7
7
  """
8
8
  import argparse
9
+ import getpass
9
10
  import json
10
11
  import os
11
12
  from pathlib import Path
12
13
  import re
13
14
  import shlex
15
+ import signal
14
16
  import subprocess
15
17
  import sys
16
18
  import urllib.request
19
+ import warnings
17
20
 
18
21
 
19
22
  class NoRedirect(urllib.request.HTTPRedirectHandler):
@@ -55,14 +58,20 @@ def owner_terminal():
55
58
  return False
56
59
 
57
60
 
58
- def launch_owner_terminal(reference, tenant):
59
- if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_./-]{0,254}', reference):
61
+ def launch_owner_terminal(reference, tenant, email=None, create_account=False):
62
+ if reference is not None and not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_./-]{0,254}', reference):
60
63
  print('Invalid Secrets reference.', file=sys.stderr)
61
64
  return 2
62
65
  # AppleScript receives references and paths only. Its returned text never
63
66
  # contains Terminal output. No QR or credential is returned to this process.
64
- command = shlex.join(['secrets', 'exec', reference, '--as', 'ECONOMY_HUMAN_SESSION', '--',
65
- '/usr/bin/python3', str(Path(__file__).resolve()), '--owner-terminal', '--tenant', tenant])
67
+ arguments = ['/usr/bin/python3', str(Path(__file__).resolve()), '--owner-terminal', '--tenant', tenant]
68
+ if email is not None:
69
+ arguments += ['--login-email', email]
70
+ if create_account:
71
+ arguments.append('--create-account')
72
+ else:
73
+ arguments = ['secrets', 'exec', reference, '--as', 'ECONOMY_HUMAN_SESSION', '--', *arguments]
74
+ command = shlex.join(arguments)
66
75
  script = 'tell application "Terminal"\nactivate\ndo script ' + json.dumps(command) + '\nend tell\n'
67
76
  try:
68
77
  result = subprocess.run(['/usr/bin/osascript', '-'], input=script, text=True, capture_output=True, timeout=30)
@@ -72,15 +81,68 @@ def launch_owner_terminal(reference, tenant):
72
81
  if result.returncode:
73
82
  print('Could not open the owner Terminal window. No code was displayed by this process.', file=sys.stderr)
74
83
  return 1
75
- print('Opened the Economy pairing window on this Mac. Scan it with Economy on your iPhone.')
84
+ print('Opened Economy setup in Terminal on this Mac. Complete the steps there, then scan its QR code.')
76
85
  return 0
77
86
 
78
87
 
88
+ def identity_request(action, body, bearer=None):
89
+ if action not in ['signup', 'login', 'verify', 'logout', 'economy-pairing/issue']:
90
+ raise ValueError('Unsupported identity operation')
91
+ headers = {'Content-Type': 'application/json', 'Accept': 'application/json',
92
+ 'User-Agent': 'Hasna-Economy-Pairing/1.0'}
93
+ if bearer is not None:
94
+ headers['Authorization'] = 'Bearer ' + bearer
95
+ request = urllib.request.Request('https://api.hasna.com/identities/v1/auth/' + action,
96
+ data=json.dumps(body).encode(), headers=headers, method='POST')
97
+ with urllib.request.build_opener(NoRedirect).open(request, timeout=15) as response:
98
+ raw = response.read(16385)
99
+ if response.status != (201 if action == 'signup' else 200) or len(raw) > 16384:
100
+ raise ValueError('Invalid identity response')
101
+ value = json.loads(raw)
102
+ if not isinstance(value, dict):
103
+ raise ValueError('Invalid identity response')
104
+ return value
105
+
106
+
107
+ def login_in_owner_terminal(email, create_account):
108
+ action = 'Create your Hasna sign-in and send one verification email to' if create_account else 'Send one sign-in email to'
109
+ if input(action + ' ' + email + '? Type yes to continue: ').strip().lower() != 'yes':
110
+ print('Cancelled. No email was requested.')
111
+ return None
112
+ challenge = identity_request('signup' if create_account else 'login', {'email': email})
113
+ if challenge.get('challenge') is not True or challenge.get('email_sent') is False:
114
+ raise ValueError('Email challenge unavailable')
115
+ with warnings.catch_warnings():
116
+ warnings.simplefilter('error', getpass.GetPassWarning)
117
+ code = getpass.getpass('Verification code (hidden; enter only in this Terminal): ')
118
+ if not re.fullmatch(r'[0-9]{6}', code):
119
+ raise ValueError('Invalid verification code')
120
+ try:
121
+ return identity_request('verify', {'email': email, 'code': code})
122
+ finally:
123
+ code = None
124
+
125
+
126
+ def render_pairing_code(human, tenant, renderer):
127
+ value = identity_request('economy-pairing/issue', {'tenant_id': tenant, 'device_name': 'Your iPhone'}, human)
128
+ if not re.fullmatch(r'hec_[A-Za-z0-9_-]{43}', value.get('code', '')) or not 1 <= value.get('expires_in', 0) <= 300:
129
+ raise ValueError('Invalid pairing response')
130
+ environment = {k: v for k, v in os.environ.items() if k in
131
+ ['PATH', 'HOME', 'TMPDIR', 'LANG', 'TERM', 'DEVELOPER_DIR', 'CLANG_MODULE_CACHE_PATH']}
132
+ environment['ECONOMY_PAIR_CODE'] = value['code']
133
+ environment['ECONOMY_PAIR_SERVER'] = 'https://api.hasna.com/economy'
134
+ environment['ECONOMY_PAIR_TTL'] = str(value['expires_in'])
135
+ return subprocess.run(['/usr/bin/swift', str(renderer)], env=environment).returncode
136
+
137
+
79
138
  def main():
80
139
  parser = argparse.ArgumentParser(description=__doc__)
81
140
  parser.add_argument('--tenant', help='Exact workspace ID, resolved through Identities')
82
141
  parser.add_argument('--self-test', action='store_true')
83
- parser.add_argument('--session-ref', help='Authorized human-session reference in Secrets; never a value')
142
+ source = parser.add_mutually_exclusive_group()
143
+ source.add_argument('--session-ref', help='Authorized human-session reference in Secrets; never a value')
144
+ source.add_argument('--login-email', help='Sign in privately in the owner Terminal before displaying the QR code')
145
+ parser.add_argument('--create-account', action='store_true', help='Offer first-account signup in the owner Terminal; requires --login-email and direct confirmation')
84
146
  parser.add_argument('--owner-terminal', action='store_true', help=argparse.SUPPRESS)
85
147
  args = parser.parse_args()
86
148
  human = os.environ.pop('ECONOMY_HUMAN_SESSION', None)
@@ -90,41 +152,70 @@ def main():
90
152
  if not args.tenant or not re.fullmatch(r'[A-Za-z0-9_-]{1,255}', args.tenant):
91
153
  print('Provide the exact authorized workspace with --tenant.', file=sys.stderr)
92
154
  return 2
155
+ if args.create_account and not args.login_email:
156
+ print('--create-account requires --login-email.', file=sys.stderr)
157
+ return 2
158
+ if args.login_email is not None and (len(args.login_email) > 320 or not re.fullmatch(r'[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+', args.login_email)):
159
+ print('Provide a valid sign-in email address.', file=sys.stderr)
160
+ return 2
93
161
  if not args.owner_terminal:
94
- if not args.session_ref:
95
- print('Use --session-ref to open the owner Terminal. Credential values are not accepted here.', file=sys.stderr)
162
+ if not args.session_ref and not args.login_email:
163
+ print('Use --session-ref or --login-email to open the owner Terminal. Credential values are not accepted here.', file=sys.stderr)
96
164
  return 3
97
- return launch_owner_terminal(args.session_ref, args.tenant)
165
+ return launch_owner_terminal(args.session_ref, args.tenant, args.login_email, args.create_account)
98
166
  if args.session_ref or not owner_terminal():
99
167
  print('Refusing to show a connection code outside the direct owner Terminal path.', file=sys.stderr)
100
168
  return 3
101
- if not human or not re.fullmatch(r'hst_[A-Za-z0-9_-]{43}', human):
169
+ if args.login_email and human:
170
+ print('Choose either a saved session or interactive sign-in.', file=sys.stderr)
171
+ return 2
172
+ if not args.login_email and (not human or not re.fullmatch(r'hst_[A-Za-z0-9_-]{43}', human)):
102
173
  print('An existing human Identities session is required via ECONOMY_HUMAN_SESSION. Station keys are refused.', file=sys.stderr)
103
174
  return 2
104
- request = urllib.request.Request('https://api.hasna.com/identities/v1/auth/economy-pairing/issue',
105
- data=json.dumps({'tenant_id': args.tenant, 'device_name': 'Your iPhone'}).encode(),
106
- headers={'Authorization': 'Bearer ' + human, 'Content-Type': 'application/json',
107
- 'Accept': 'application/json', 'User-Agent': 'Hasna-Economy-Pairing/1.0'}, method='POST')
175
+ created_session = None
176
+ result = 1
177
+ previous_handlers = {}
178
+ cleaning_up = False
179
+ def shutdown(signum, frame):
180
+ if not cleaning_up:
181
+ raise KeyboardInterrupt
108
182
  try:
109
- with urllib.request.build_opener(NoRedirect).open(request, timeout=15) as response:
110
- raw = response.read(8193)
111
- if response.status != 200 or len(raw) > 8192:
112
- raise ValueError('response')
113
- value = json.loads(raw)
114
- if not re.fullmatch(r'hec_[A-Za-z0-9_-]{43}', value.get('code', '')) or not 1 <= value.get('expires_in', 0) <= 300:
115
- raise ValueError('response')
116
- except Exception:
117
- print('Unable to issue the connection code. Check the human session, workspace and hosted pairing service.', file=sys.stderr)
118
- return 1
119
- human = None
120
- # Only the temporary code goes to the renderer. The human session was
121
- # removed from the environment before any child was started.
122
- environment = {k: v for k, v in os.environ.items() if k in
123
- ['PATH', 'HOME', 'TMPDIR', 'LANG', 'TERM', 'DEVELOPER_DIR', 'CLANG_MODULE_CACHE_PATH']}
124
- environment['ECONOMY_PAIR_CODE'] = value['code']
125
- environment['ECONOMY_PAIR_SERVER'] = 'https://api.hasna.com/economy'
126
- environment['ECONOMY_PAIR_TTL'] = str(value['expires_in'])
127
- return subprocess.run(['/usr/bin/swift', str(renderer)], env=environment).returncode
183
+ for sig in [signal.SIGHUP, signal.SIGTERM, signal.SIGINT]:
184
+ previous_handlers[sig] = signal.signal(sig, shutdown)
185
+ if args.login_email:
186
+ identity = login_in_owner_terminal(args.login_email, args.create_account)
187
+ if identity is None:
188
+ return 0
189
+ human = identity.get('session')
190
+ if not isinstance(human, str) or not re.fullmatch(r'hst_[A-Za-z0-9_-]{43}', human):
191
+ raise ValueError('Invalid human session')
192
+ created_session = human
193
+ if identity.get('principal', {}).get('kind') != 'human' or not any(
194
+ isinstance(row, dict) and row.get('tenant_id') == args.tenant for row in identity.get('tenants', [])):
195
+ raise ValueError('Human or workspace mismatch')
196
+ identity = None
197
+ print('Scan this code before closing its window. Setup will then try to sign out its temporary session.')
198
+ result = render_pairing_code(human, args.tenant, renderer)
199
+ except (Exception, KeyboardInterrupt):
200
+ print('Unable to complete setup. No automatic email or sign-in retry was made. Check your account and workspace before trying again.', file=sys.stderr)
201
+ finally:
202
+ # Catchable Terminal shutdown unwinds into cleanup. Further catchable
203
+ # signals cannot interrupt the single timeout-limited logout attempt.
204
+ cleaning_up = True
205
+ try:
206
+ if created_session:
207
+ try:
208
+ if identity_request('logout', {}, created_session).get('signed_out') is not True:
209
+ raise ValueError('Sign-out unconfirmed')
210
+ except Exception:
211
+ print('Setup session sign-out could not be confirmed. Check your account sessions before retrying.', file=sys.stderr)
212
+ result = 1
213
+ finally:
214
+ human = None
215
+ created_session = None
216
+ for sig, handler in previous_handlers.items():
217
+ signal.signal(sig, handler)
218
+ return result
128
219
 
129
220
 
130
221
  if __name__ == '__main__':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/economy",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
5
5
  "repository": {
6
6
  "type": "git",