@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 +6 -0
- package/dist/chunks/{index-zqsx461z.js → index-6yr4hb72.js} +1 -1
- package/dist/chunks/{index-m12xttw1.js → index-b6c649mq.js} +1 -1
- package/dist/chunks/{index-qvcafabj.js → index-wq7c4wqs.js} +1 -1
- package/dist/chunks/{index-zs5dba3t.js → index-z2n5y111.js} +1 -1
- package/dist/chunks/{serve-ept6cfbe.js → serve-ms8dvf7e.js} +2 -2
- package/dist/chunks/{tui-4bh067a3.js → tui-9t7340wd.js} +3 -3
- package/dist/chunks/{watch-227kxmra.js → watch-t6ssnx30.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/otel/index.js +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/service-worker.js +1 -1
- package/native/README.md +22 -2
- package/native/Tools/pair-qr.py +124 -33
- package/package.json +1 -1
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
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
// package.json
|
|
21
21
|
var package_default = {
|
|
22
22
|
name: "@hasna/economy",
|
|
23
|
-
version: "0.5.
|
|
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",
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./index-w3bvv0ns.js";
|
|
6
6
|
import {
|
|
7
7
|
syncAll
|
|
8
|
-
} from "./index-
|
|
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-
|
|
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-
|
|
9
|
+
} from "./index-6yr4hb72.js";
|
|
10
10
|
import"./index-w3bvv0ns.js";
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
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-
|
|
5
|
+
} from "./index-wq7c4wqs.js";
|
|
6
6
|
import {
|
|
7
7
|
getStore,
|
|
8
8
|
isCloudStore
|
|
9
|
-
} from "./index-
|
|
10
|
-
import"./index-
|
|
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-
|
|
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-
|
|
16
|
+
} from "../chunks/index-wq7c4wqs.js";
|
|
17
17
|
import {
|
|
18
18
|
getStore,
|
|
19
19
|
isCloudStore
|
|
20
|
-
} from "../chunks/index-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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.
|
|
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",
|
package/dist/otel/index.js
CHANGED
|
@@ -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.
|
|
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",
|
package/dist/server/index.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
34
|
-
|
|
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
|
package/native/Tools/pair-qr.py
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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.
|
|
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",
|