myshell-tools 2.15.0 → 3.2.0
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 +83 -1
- package/README.md +39 -44
- package/dist/cli.js +33 -22
- package/dist/cli.js.map +1 -1
- package/dist/commands/cost.js +44 -21
- package/dist/commands/cost.js.map +1 -1
- package/dist/commands/doctor.d.ts +17 -4
- package/dist/commands/doctor.js +62 -35
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/login.d.ts +18 -10
- package/dist/commands/login.js +81 -109
- package/dist/commands/login.js.map +1 -1
- package/dist/core/json-envelope.js +31 -3
- package/dist/core/json-envelope.js.map +1 -1
- package/dist/core/native-session.d.ts +57 -0
- package/dist/core/native-session.js +68 -0
- package/dist/core/native-session.js.map +1 -0
- package/dist/core/orchestrate.js +106 -43
- package/dist/core/orchestrate.js.map +1 -1
- package/dist/core/types.d.ts +26 -0
- package/dist/infra/atomic.d.ts +9 -1
- package/dist/infra/atomic.js +12 -2
- package/dist/infra/atomic.js.map +1 -1
- package/dist/infra/config.d.ts +8 -0
- package/dist/infra/config.js.map +1 -1
- package/dist/infra/credentials.d.ts +69 -3
- package/dist/infra/credentials.js +118 -9
- package/dist/infra/credentials.js.map +1 -1
- package/dist/infra/health.d.ts +57 -0
- package/dist/infra/health.js +96 -0
- package/dist/infra/health.js.map +1 -0
- package/dist/infra/insights.d.ts +14 -0
- package/dist/infra/insights.js +31 -0
- package/dist/infra/insights.js.map +1 -1
- package/dist/interface/menu.d.ts +70 -5
- package/dist/interface/menu.js +292 -88
- package/dist/interface/menu.js.map +1 -1
- package/dist/interface/render.js +20 -13
- package/dist/interface/render.js.map +1 -1
- package/dist/providers/claude.d.ts +24 -8
- package/dist/providers/claude.js +77 -15
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/codex-parse.js +14 -2
- package/dist/providers/codex-parse.js.map +1 -1
- package/dist/providers/codex.d.ts +13 -1
- package/dist/providers/codex.js +19 -8
- package/dist/providers/codex.js.map +1 -1
- package/dist/providers/detect.js +22 -1
- package/dist/providers/detect.js.map +1 -1
- package/dist/providers/port.d.ts +15 -0
- package/dist/providers/registry.d.ts +8 -4
- package/dist/providers/registry.js +7 -6
- package/dist/providers/registry.js.map +1 -1
- package/dist/ui/help.d.ts +17 -0
- package/dist/ui/help.js +106 -0
- package/dist/ui/help.js.map +1 -0
- package/package.json +1 -1
package/dist/infra/config.d.ts
CHANGED
|
@@ -17,6 +17,14 @@ export interface AppConfig {
|
|
|
17
17
|
* disable auto-update and show only the notification banner instead.
|
|
18
18
|
*/
|
|
19
19
|
autoUpdate?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* EXPERIMENTAL (default off). When true, a conversation that stays on the same
|
|
22
|
+
* provider reuses that provider's native session (Claude `--session-id`/
|
|
23
|
+
* `--resume`) instead of replaying a compacted history block into each prompt
|
|
24
|
+
* — better context fidelity and less re-sent context. Scoped to Claude for
|
|
25
|
+
* now. Verify live behavior with `npm run test:integration` before relying on it.
|
|
26
|
+
*/
|
|
27
|
+
nativeSessions?: boolean;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Load the global app config. Returns defaults merged with any on-disk
|
package/dist/infra/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/infra/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/infra/config.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AA4B1C,8EAA8E;AAC9E,WAAW;AACX,8EAA8E;AAE9E,MAAM,QAAQ,GAAc;IAC1B,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,KAAK;IACnB,UAAU,EAAE,IAAI;CACjB,CAAC;AAEF,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E,SAAS,YAAY,CAAC,OAAe;IACnC,OAAO,IAAI,CAAC,OAAO,EAAE,gBAAgB,CAAC,CAAC;AACzC,CAAC;AAED,SAAS,aAAa,CAAC,OAAe;IACpC,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC,CAAC;AACpD,CAAC;AAED,8EAA8E;AAC9E,aAAa;AACb,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAgB;IAC/C,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;IAClC,IAAI,GAAW,CAAC;IAChB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;QACjC,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;IACzB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;QACrD,uEAAuE;QACvE,OAAO,EAAE,GAAG,QAAQ,EAAE,GAAG,MAAM,EAAE,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;QACjC,OAAO,EAAE,GAAG,QAAQ,EAAE,CAAC;IACzB,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAiB,EAAE,OAAgB;IAClE,MAAM,IAAI,GAAG,OAAO,IAAI,OAAO,EAAE,CAAC;IAClC,MAAM,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,MAAM,WAAW,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC1E,CAAC"}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* shape `{ claudeOauthToken?: string }` so the Claude OAuth token captured
|
|
6
6
|
* during `myshell-tools login claude --code` is available across restarts.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
8
|
+
* Token scoping: instead of injecting the token into the global `process.env`
|
|
9
|
+
* at startup (which would expose it to every child process), callers use
|
|
10
|
+
* `loadClaudeToken()` + `claudeEnv()` to build a scoped env object that is
|
|
11
|
+
* passed only to Claude CLI invocations. Other providers (codex, opencode,
|
|
12
|
+
* npm) never see the token.
|
|
11
13
|
*
|
|
12
14
|
* Security: the file is written with mode 0o600 (owner-read-only) on POSIX
|
|
13
15
|
* systems. The chmod is best-effort — a failure is silently ignored so the
|
|
@@ -15,16 +17,77 @@
|
|
|
15
17
|
*/
|
|
16
18
|
export interface Credentials {
|
|
17
19
|
claudeOauthToken?: string;
|
|
20
|
+
claudeTokenCapturedAt?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ClaudeTokenStatus {
|
|
23
|
+
/** ISO string of when the token was saved. */
|
|
24
|
+
readonly capturedAt: string;
|
|
25
|
+
/** ISO string of the computed expiry (capturedAt + lifetimeDays). */
|
|
26
|
+
readonly expiresAt: string;
|
|
27
|
+
/** Whole days remaining until expiry (floor). 0 when expiring today, negative when expired. */
|
|
28
|
+
readonly daysLeft: number;
|
|
29
|
+
/** True when daysLeft <= 0. */
|
|
30
|
+
readonly expired: boolean;
|
|
31
|
+
/** True when 0 < daysLeft <= warnWithinDays. */
|
|
32
|
+
readonly nearExpiry: boolean;
|
|
18
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Compute token lifetime status from a stored ISO capture timestamp.
|
|
36
|
+
*
|
|
37
|
+
* Pure — no I/O, no Date.now() inside. Pass `nowMs` for deterministic testing.
|
|
38
|
+
* Never throws.
|
|
39
|
+
*
|
|
40
|
+
* @param capturedAtIso - ISO string stored at save time, or undefined/null.
|
|
41
|
+
* @param nowMs - Current epoch-ms (e.g. Date.now() from the caller).
|
|
42
|
+
* @param lifetimeDays - Total token lifetime in days (default 365).
|
|
43
|
+
* @param warnWithinDays - Warn when this many days or fewer remain (default 14).
|
|
44
|
+
* @returns Status object, or null when capturedAtIso is missing or unparseable.
|
|
45
|
+
*/
|
|
46
|
+
export declare function claudeTokenStatus(capturedAtIso: string | undefined, nowMs: number, lifetimeDays?: number, warnWithinDays?: number): ClaudeTokenStatus | null;
|
|
19
47
|
/**
|
|
20
48
|
* Load stored credentials. Never throws — missing or corrupt files return `{}`.
|
|
21
49
|
*/
|
|
22
50
|
export declare function loadCredentials(homeDir?: string): Promise<Credentials>;
|
|
51
|
+
/**
|
|
52
|
+
* Load the stored Claude OAuth token. Returns `null` when no token is stored
|
|
53
|
+
* or when the credentials file is missing/corrupt. Never throws.
|
|
54
|
+
*
|
|
55
|
+
* This is a thin convenience wrapper over `loadCredentials` that returns the
|
|
56
|
+
* token string directly so callers don't need to destructure `Credentials`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function loadClaudeToken(homeDir?: string): Promise<string | null>;
|
|
59
|
+
/**
|
|
60
|
+
* Build a child-process environment that injects the Claude OAuth token into
|
|
61
|
+
* ONLY the `CLAUDE_CODE_OAUTH_TOKEN` variable, leaving all other variables
|
|
62
|
+
* from `baseEnv` intact.
|
|
63
|
+
*
|
|
64
|
+
* Rules (pure — no I/O):
|
|
65
|
+
* - Returns `baseEnv` unchanged when `token` is `null` (nothing stored).
|
|
66
|
+
* - Returns `baseEnv` unchanged when `baseEnv.CLAUDE_CODE_OAUTH_TOKEN` is
|
|
67
|
+
* already set — the user's explicitly-exported value always wins.
|
|
68
|
+
* - Otherwise returns `{ ...baseEnv, CLAUDE_CODE_OAUTH_TOKEN: token }`.
|
|
69
|
+
*
|
|
70
|
+
* Pass the result as the `env` option to execa for Claude CLI spawns only.
|
|
71
|
+
* Never pass it to codex/opencode/npm children — they do not need it and
|
|
72
|
+
* should not see the token.
|
|
73
|
+
*
|
|
74
|
+
* Pure / never throws.
|
|
75
|
+
*/
|
|
76
|
+
export declare function claudeEnv(baseEnv: NodeJS.ProcessEnv, token: string | null): NodeJS.ProcessEnv;
|
|
23
77
|
/**
|
|
24
78
|
* Persist the Claude OAuth token atomically to
|
|
25
79
|
* `~/.myshell-tools/credentials.json` with restrictive permissions (0o600).
|
|
80
|
+
*
|
|
81
|
+
* Records `claudeTokenCapturedAt` (ISO timestamp) so the token's age can be
|
|
82
|
+
* tracked for expiry warnings.
|
|
26
83
|
*/
|
|
27
84
|
export declare function saveClaudeToken(token: string, homeDir?: string): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Load the ISO timestamp recorded when the Claude OAuth token was last saved.
|
|
87
|
+
* Returns `undefined` when no token has been saved, or when the stored value
|
|
88
|
+
* is missing from an older credential file. Never throws.
|
|
89
|
+
*/
|
|
90
|
+
export declare function loadClaudeTokenCapturedAt(homeDir?: string): Promise<string | undefined>;
|
|
28
91
|
/**
|
|
29
92
|
* Remove the stored Claude OAuth token. Writes the file back without the
|
|
30
93
|
* token key so any future credential fields are preserved.
|
|
@@ -75,6 +138,8 @@ export declare function stripPastedSecretWrapper(raw: string): string;
|
|
|
75
138
|
* - `'api-key'` — starts with `sk-ant-api` (a raw Anthropic API key, NOT what we want).
|
|
76
139
|
* - `'none'` — neither; blank or unrecognised.
|
|
77
140
|
*
|
|
141
|
+
* Uses `startsWith` semantics: mid-string occurrences of `sk-ant-oat` or
|
|
142
|
+
* `sk-ant-api` do NOT classify as oauth-token or api-key respectively.
|
|
78
143
|
* Input is pre-normalised (trimmed, quotes stripped) by the caller.
|
|
79
144
|
* Pure / never throws.
|
|
80
145
|
*
|
|
@@ -82,5 +147,6 @@ export declare function stripPastedSecretWrapper(raw: string): string;
|
|
|
82
147
|
* classifyPastedSecret('sk-ant-oat01-abc-XYZ') // → 'oauth-token'
|
|
83
148
|
* classifyPastedSecret('sk-ant-api03-abc-XYZ') // → 'api-key'
|
|
84
149
|
* classifyPastedSecret('not-a-token') // → 'none'
|
|
150
|
+
* classifyPastedSecret('prefix sk-ant-oat01-x') // → 'none'
|
|
85
151
|
*/
|
|
86
152
|
export declare function classifyPastedSecret(s: string): 'oauth-token' | 'api-key' | 'none';
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* shape `{ claudeOauthToken?: string }` so the Claude OAuth token captured
|
|
6
6
|
* during `myshell-tools login claude --code` is available across restarts.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `
|
|
8
|
+
* Token scoping: instead of injecting the token into the global `process.env`
|
|
9
|
+
* at startup (which would expose it to every child process), callers use
|
|
10
|
+
* `loadClaudeToken()` + `claudeEnv()` to build a scoped env object that is
|
|
11
|
+
* passed only to Claude CLI invocations. Other providers (codex, opencode,
|
|
12
|
+
* npm) never see the token.
|
|
11
13
|
*
|
|
12
14
|
* Security: the file is written with mode 0o600 (owner-read-only) on POSIX
|
|
13
15
|
* systems. The chmod is best-effort — a failure is silently ignored so the
|
|
@@ -17,6 +19,40 @@ import { mkdir, readFile, chmod } from 'node:fs/promises';
|
|
|
17
19
|
import { homedir } from 'node:os';
|
|
18
20
|
import { join } from 'node:path';
|
|
19
21
|
import { atomicWrite } from './atomic.js';
|
|
22
|
+
/**
|
|
23
|
+
* Compute token lifetime status from a stored ISO capture timestamp.
|
|
24
|
+
*
|
|
25
|
+
* Pure — no I/O, no Date.now() inside. Pass `nowMs` for deterministic testing.
|
|
26
|
+
* Never throws.
|
|
27
|
+
*
|
|
28
|
+
* @param capturedAtIso - ISO string stored at save time, or undefined/null.
|
|
29
|
+
* @param nowMs - Current epoch-ms (e.g. Date.now() from the caller).
|
|
30
|
+
* @param lifetimeDays - Total token lifetime in days (default 365).
|
|
31
|
+
* @param warnWithinDays - Warn when this many days or fewer remain (default 14).
|
|
32
|
+
* @returns Status object, or null when capturedAtIso is missing or unparseable.
|
|
33
|
+
*/
|
|
34
|
+
export function claudeTokenStatus(capturedAtIso, nowMs, lifetimeDays = 365, warnWithinDays = 14) {
|
|
35
|
+
try {
|
|
36
|
+
if (capturedAtIso === undefined || capturedAtIso === null || capturedAtIso.length === 0) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const capturedMs = new Date(capturedAtIso).getTime();
|
|
40
|
+
if (!Number.isFinite(capturedMs)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const lifetimeMs = lifetimeDays * 24 * 60 * 60 * 1000;
|
|
44
|
+
const expiresMs = capturedMs + lifetimeMs;
|
|
45
|
+
const expiresAt = new Date(expiresMs).toISOString();
|
|
46
|
+
const msLeft = expiresMs - nowMs;
|
|
47
|
+
const daysLeft = Math.floor(msLeft / (24 * 60 * 60 * 1000));
|
|
48
|
+
const expired = daysLeft <= 0;
|
|
49
|
+
const nearExpiry = !expired && daysLeft <= warnWithinDays;
|
|
50
|
+
return { capturedAt: capturedAtIso, expiresAt, daysLeft, expired, nearExpiry };
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
20
56
|
// ---------------------------------------------------------------------------
|
|
21
57
|
// Path helpers (pure)
|
|
22
58
|
// ---------------------------------------------------------------------------
|
|
@@ -44,6 +80,9 @@ function parseCredentials(raw) {
|
|
|
44
80
|
if (typeof obj['claudeOauthToken'] === 'string' && obj['claudeOauthToken'].length > 0) {
|
|
45
81
|
result.claudeOauthToken = obj['claudeOauthToken'];
|
|
46
82
|
}
|
|
83
|
+
if (typeof obj['claudeTokenCapturedAt'] === 'string' && obj['claudeTokenCapturedAt'].length > 0) {
|
|
84
|
+
result.claudeTokenCapturedAt = obj['claudeTokenCapturedAt'];
|
|
85
|
+
}
|
|
47
86
|
return result;
|
|
48
87
|
}
|
|
49
88
|
catch {
|
|
@@ -66,20 +105,73 @@ export async function loadCredentials(homeDir) {
|
|
|
66
105
|
return {};
|
|
67
106
|
}
|
|
68
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Load the stored Claude OAuth token. Returns `null` when no token is stored
|
|
110
|
+
* or when the credentials file is missing/corrupt. Never throws.
|
|
111
|
+
*
|
|
112
|
+
* This is a thin convenience wrapper over `loadCredentials` that returns the
|
|
113
|
+
* token string directly so callers don't need to destructure `Credentials`.
|
|
114
|
+
*/
|
|
115
|
+
export async function loadClaudeToken(homeDir) {
|
|
116
|
+
try {
|
|
117
|
+
const creds = await loadCredentials(homeDir);
|
|
118
|
+
return creds.claudeOauthToken ?? null;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Build a child-process environment that injects the Claude OAuth token into
|
|
126
|
+
* ONLY the `CLAUDE_CODE_OAUTH_TOKEN` variable, leaving all other variables
|
|
127
|
+
* from `baseEnv` intact.
|
|
128
|
+
*
|
|
129
|
+
* Rules (pure — no I/O):
|
|
130
|
+
* - Returns `baseEnv` unchanged when `token` is `null` (nothing stored).
|
|
131
|
+
* - Returns `baseEnv` unchanged when `baseEnv.CLAUDE_CODE_OAUTH_TOKEN` is
|
|
132
|
+
* already set — the user's explicitly-exported value always wins.
|
|
133
|
+
* - Otherwise returns `{ ...baseEnv, CLAUDE_CODE_OAUTH_TOKEN: token }`.
|
|
134
|
+
*
|
|
135
|
+
* Pass the result as the `env` option to execa for Claude CLI spawns only.
|
|
136
|
+
* Never pass it to codex/opencode/npm children — they do not need it and
|
|
137
|
+
* should not see the token.
|
|
138
|
+
*
|
|
139
|
+
* Pure / never throws.
|
|
140
|
+
*/
|
|
141
|
+
export function claudeEnv(baseEnv, token) {
|
|
142
|
+
if (token === null) {
|
|
143
|
+
return baseEnv;
|
|
144
|
+
}
|
|
145
|
+
if (baseEnv['CLAUDE_CODE_OAUTH_TOKEN'] !== undefined) {
|
|
146
|
+
// User's explicitly-exported env wins — do not overwrite.
|
|
147
|
+
return baseEnv;
|
|
148
|
+
}
|
|
149
|
+
return { ...baseEnv, CLAUDE_CODE_OAUTH_TOKEN: token };
|
|
150
|
+
}
|
|
69
151
|
/**
|
|
70
152
|
* Persist the Claude OAuth token atomically to
|
|
71
153
|
* `~/.myshell-tools/credentials.json` with restrictive permissions (0o600).
|
|
154
|
+
*
|
|
155
|
+
* Records `claudeTokenCapturedAt` (ISO timestamp) so the token's age can be
|
|
156
|
+
* tracked for expiry warnings.
|
|
72
157
|
*/
|
|
73
158
|
export async function saveClaudeToken(token, homeDir) {
|
|
74
159
|
const home = homeDir ?? homedir();
|
|
75
160
|
const dir = getCredentialsDir(home);
|
|
76
161
|
const path = getCredentialsPath(home);
|
|
77
|
-
|
|
162
|
+
// Create the directory with restrictive permissions (0o700) so it is never
|
|
163
|
+
// world-readable. recursive:true is a no-op when it already exists.
|
|
164
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
78
165
|
// Load existing credentials so we only replace the token key, preserving others.
|
|
79
166
|
const existing = await loadCredentials(homeDir);
|
|
80
|
-
const updated = {
|
|
81
|
-
|
|
82
|
-
|
|
167
|
+
const updated = {
|
|
168
|
+
...existing,
|
|
169
|
+
claudeOauthToken: token,
|
|
170
|
+
claudeTokenCapturedAt: new Date().toISOString(),
|
|
171
|
+
};
|
|
172
|
+
// atomicWrite with mode 0o600 guarantees the temp file is never more permissive
|
|
173
|
+
// than the final destination — no world-readable window before the rename.
|
|
174
|
+
await atomicWrite(path, JSON.stringify(updated, null, 2), 0o600);
|
|
83
175
|
// Best-effort: restrict to owner-read-only. Silently ignored on Windows or
|
|
84
176
|
// unusual filesystems where chmod is unavailable or unsupported.
|
|
85
177
|
try {
|
|
@@ -89,6 +181,20 @@ export async function saveClaudeToken(token, homeDir) {
|
|
|
89
181
|
// Cross-platform best-effort only — never throws
|
|
90
182
|
}
|
|
91
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Load the ISO timestamp recorded when the Claude OAuth token was last saved.
|
|
186
|
+
* Returns `undefined` when no token has been saved, or when the stored value
|
|
187
|
+
* is missing from an older credential file. Never throws.
|
|
188
|
+
*/
|
|
189
|
+
export async function loadClaudeTokenCapturedAt(homeDir) {
|
|
190
|
+
try {
|
|
191
|
+
const creds = await loadCredentials(homeDir);
|
|
192
|
+
return creds.claudeTokenCapturedAt;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
92
198
|
/**
|
|
93
199
|
* Remove the stored Claude OAuth token. Writes the file back without the
|
|
94
200
|
* token key so any future credential fields are preserved.
|
|
@@ -200,6 +306,8 @@ export function stripPastedSecretWrapper(raw) {
|
|
|
200
306
|
* - `'api-key'` — starts with `sk-ant-api` (a raw Anthropic API key, NOT what we want).
|
|
201
307
|
* - `'none'` — neither; blank or unrecognised.
|
|
202
308
|
*
|
|
309
|
+
* Uses `startsWith` semantics: mid-string occurrences of `sk-ant-oat` or
|
|
310
|
+
* `sk-ant-api` do NOT classify as oauth-token or api-key respectively.
|
|
203
311
|
* Input is pre-normalised (trimmed, quotes stripped) by the caller.
|
|
204
312
|
* Pure / never throws.
|
|
205
313
|
*
|
|
@@ -207,12 +315,13 @@ export function stripPastedSecretWrapper(raw) {
|
|
|
207
315
|
* classifyPastedSecret('sk-ant-oat01-abc-XYZ') // → 'oauth-token'
|
|
208
316
|
* classifyPastedSecret('sk-ant-api03-abc-XYZ') // → 'api-key'
|
|
209
317
|
* classifyPastedSecret('not-a-token') // → 'none'
|
|
318
|
+
* classifyPastedSecret('prefix sk-ant-oat01-x') // → 'none'
|
|
210
319
|
*/
|
|
211
320
|
export function classifyPastedSecret(s) {
|
|
212
321
|
try {
|
|
213
|
-
if (s.
|
|
322
|
+
if (s.startsWith('sk-ant-oat'))
|
|
214
323
|
return 'oauth-token';
|
|
215
|
-
if (s.
|
|
324
|
+
if (s.startsWith('sk-ant-api'))
|
|
216
325
|
return 'api-key';
|
|
217
326
|
return 'none';
|
|
218
327
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../../src/infra/credentials.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/infra/health.ts — Self-health evaluation for myshell-tools.
|
|
3
|
+
*
|
|
4
|
+
* The product goal is "it just works": the user should never run a diagnostic
|
|
5
|
+
* command. Instead the control panel evaluates its own environment health at
|
|
6
|
+
* startup and surfaces a short, actionable warning ONLY when something is
|
|
7
|
+
* actually wrong. No problems → nothing shown (silence == healthy).
|
|
8
|
+
*
|
|
9
|
+
* This module covers the diagnostics that are NOT already visible elsewhere in
|
|
10
|
+
* the UI: Node version, state-directory writability, and pricing-table
|
|
11
|
+
* staleness. Provider install/auth status and Claude-token expiry are already
|
|
12
|
+
* rendered in the header, so they are intentionally not duplicated here.
|
|
13
|
+
*
|
|
14
|
+
* `evaluateHealth` is PURE (no I/O) so it is trivially testable. The one piece
|
|
15
|
+
* that needs I/O — probing whether the state directory is writable — is a
|
|
16
|
+
* separate function the caller runs once at startup and feeds in.
|
|
17
|
+
*/
|
|
18
|
+
type HealthSeverity = 'warn' | 'error';
|
|
19
|
+
export interface HealthIssue {
|
|
20
|
+
/** Stable identifier for the issue (useful for tests and de-duplication). */
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly severity: HealthSeverity;
|
|
23
|
+
/** One-line, human-readable message that already includes the fix. */
|
|
24
|
+
readonly message: string;
|
|
25
|
+
}
|
|
26
|
+
export interface HealthInputs {
|
|
27
|
+
/** process.version, e.g. "v20.20.0". */
|
|
28
|
+
readonly nodeVersion: string;
|
|
29
|
+
/** Whether the .myshell-tools state directory could be written to. */
|
|
30
|
+
readonly stateWritable: boolean;
|
|
31
|
+
/** Whether the bundled pricing seed is past its staleness window. */
|
|
32
|
+
readonly pricingStale: boolean;
|
|
33
|
+
/** Minimum supported Node major version (defaults to 20). */
|
|
34
|
+
readonly minNodeMajor?: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse the major version from a Node version string ("v20.20.0" → 20).
|
|
38
|
+
* Returns null when the string can't be parsed (so callers can skip the check
|
|
39
|
+
* rather than emit a bogus warning). Pure; never throws.
|
|
40
|
+
*/
|
|
41
|
+
export declare function nodeMajor(version: string): number | null;
|
|
42
|
+
/**
|
|
43
|
+
* Evaluate environment health and return the issues worth surfacing.
|
|
44
|
+
*
|
|
45
|
+
* Pure — takes a snapshot of inputs and returns zero or more issues, most
|
|
46
|
+
* severe first (errors before warnings). An empty array means "all healthy,
|
|
47
|
+
* show nothing".
|
|
48
|
+
*/
|
|
49
|
+
export declare function evaluateHealth(inputs: HealthInputs): HealthIssue[];
|
|
50
|
+
/**
|
|
51
|
+
* Probe whether the .myshell-tools state directory under `cwd` is writable.
|
|
52
|
+
*
|
|
53
|
+
* Creates the directory if needed, writes and removes a temp file. Returns true
|
|
54
|
+
* on success, false on any I/O error. Never throws.
|
|
55
|
+
*/
|
|
56
|
+
export declare function probeStateWritable(cwd: string): Promise<boolean>;
|
|
57
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/infra/health.ts — Self-health evaluation for myshell-tools.
|
|
3
|
+
*
|
|
4
|
+
* The product goal is "it just works": the user should never run a diagnostic
|
|
5
|
+
* command. Instead the control panel evaluates its own environment health at
|
|
6
|
+
* startup and surfaces a short, actionable warning ONLY when something is
|
|
7
|
+
* actually wrong. No problems → nothing shown (silence == healthy).
|
|
8
|
+
*
|
|
9
|
+
* This module covers the diagnostics that are NOT already visible elsewhere in
|
|
10
|
+
* the UI: Node version, state-directory writability, and pricing-table
|
|
11
|
+
* staleness. Provider install/auth status and Claude-token expiry are already
|
|
12
|
+
* rendered in the header, so they are intentionally not duplicated here.
|
|
13
|
+
*
|
|
14
|
+
* `evaluateHealth` is PURE (no I/O) so it is trivially testable. The one piece
|
|
15
|
+
* that needs I/O — probing whether the state directory is writable — is a
|
|
16
|
+
* separate function the caller runs once at startup and feeds in.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdir, writeFile, rm, access } from 'node:fs/promises';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Pure helpers
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
/**
|
|
24
|
+
* Parse the major version from a Node version string ("v20.20.0" → 20).
|
|
25
|
+
* Returns null when the string can't be parsed (so callers can skip the check
|
|
26
|
+
* rather than emit a bogus warning). Pure; never throws.
|
|
27
|
+
*/
|
|
28
|
+
export function nodeMajor(version) {
|
|
29
|
+
const m = /^v?(\d+)\./.exec(version.trim());
|
|
30
|
+
if (m === null)
|
|
31
|
+
return null;
|
|
32
|
+
const n = parseInt(m[1] ?? '', 10);
|
|
33
|
+
return Number.isFinite(n) ? n : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Evaluate environment health and return the issues worth surfacing.
|
|
37
|
+
*
|
|
38
|
+
* Pure — takes a snapshot of inputs and returns zero or more issues, most
|
|
39
|
+
* severe first (errors before warnings). An empty array means "all healthy,
|
|
40
|
+
* show nothing".
|
|
41
|
+
*/
|
|
42
|
+
export function evaluateHealth(inputs) {
|
|
43
|
+
const minMajor = inputs.minNodeMajor ?? 20;
|
|
44
|
+
const issues = [];
|
|
45
|
+
// State directory not writable — the most serious: nothing persists.
|
|
46
|
+
if (!inputs.stateWritable) {
|
|
47
|
+
issues.push({
|
|
48
|
+
id: 'state-not-writable',
|
|
49
|
+
severity: 'error',
|
|
50
|
+
message: "Can't write to the .myshell-tools state directory — conversations and " +
|
|
51
|
+
'cost tracking will not be saved. Check the directory permissions.',
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// Node below the supported floor — the compiled CLI targets Node >= minMajor.
|
|
55
|
+
const major = nodeMajor(inputs.nodeVersion);
|
|
56
|
+
if (major !== null && major < minMajor) {
|
|
57
|
+
issues.push({
|
|
58
|
+
id: 'node-too-old',
|
|
59
|
+
severity: 'warn',
|
|
60
|
+
message: `Node ${inputs.nodeVersion} is below the supported v${minMajor} — upgrade Node if you hit errors.`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Stale pricing seed — cost estimates may drift; updating refreshes them.
|
|
64
|
+
if (inputs.pricingStale) {
|
|
65
|
+
issues.push({
|
|
66
|
+
id: 'pricing-stale',
|
|
67
|
+
severity: 'warn',
|
|
68
|
+
message: 'Cost estimates may be out of date — update to refresh them: npm install -g myshell-tools@latest',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return issues;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// I/O probe (run once at startup; result fed into evaluateHealth)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Probe whether the .myshell-tools state directory under `cwd` is writable.
|
|
78
|
+
*
|
|
79
|
+
* Creates the directory if needed, writes and removes a temp file. Returns true
|
|
80
|
+
* on success, false on any I/O error. Never throws.
|
|
81
|
+
*/
|
|
82
|
+
export async function probeStateWritable(cwd) {
|
|
83
|
+
const stateDir = join(cwd, '.myshell-tools');
|
|
84
|
+
const probe = join(stateDir, '.health-probe');
|
|
85
|
+
try {
|
|
86
|
+
await mkdir(stateDir, { recursive: true });
|
|
87
|
+
await writeFile(probe, '');
|
|
88
|
+
await rm(probe, { force: true });
|
|
89
|
+
await access(stateDir);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=health.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"health.js","sourceRoot":"","sources":["../../src/infra/health.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AA2BjC,8EAA8E;AAC9E,eAAe;AACf,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,MAAM,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAC5C,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACvC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,MAAoB;IACjD,MAAM,QAAQ,GAAG,MAAM,CAAC,YAAY,IAAI,EAAE,CAAC;IAC3C,MAAM,MAAM,GAAkB,EAAE,CAAC;IAEjC,qEAAqE;IACrE,IAAI,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC;QAC1B,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,oBAAoB;YACxB,QAAQ,EAAE,OAAO;YACjB,OAAO,EACL,wEAAwE;gBACxE,mEAAmE;SACtE,CAAC,CAAC;IACL,CAAC;IAED,8EAA8E;IAC9E,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,GAAG,QAAQ,EAAE,CAAC;QACvC,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,cAAc;YAClB,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,QAAQ,MAAM,CAAC,WAAW,4BAA4B,QAAQ,oCAAoC;SAC5G,CAAC,CAAC;IACL,CAAC;IAED,0EAA0E;IAC1E,IAAI,MAAM,CAAC,YAAY,EAAE,CAAC;QACxB,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,eAAe;YACnB,QAAQ,EAAE,MAAM;YAChB,OAAO,EACL,iGAAiG;SACpG,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8EAA8E;AAC9E,kEAAkE;AAClE,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,GAAW;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3C,MAAM,SAAS,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAC3B,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjC,MAAM,MAAM,CAAC,QAAQ,CAAC,CAAC;QACvB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/infra/insights.d.ts
CHANGED
|
@@ -18,6 +18,11 @@ export interface SpendSummary {
|
|
|
18
18
|
readonly totalUsd: number;
|
|
19
19
|
/** Total number of ledger entries (calls). */
|
|
20
20
|
readonly calls: number;
|
|
21
|
+
/** Real tokens (input + output) used today (UTC). The transparent primary
|
|
22
|
+
* signal — measured, not an estimate, unlike the USD figures. */
|
|
23
|
+
readonly todayTokens: number;
|
|
24
|
+
/** Real tokens (input + output) used across all time. */
|
|
25
|
+
readonly totalTokens: number;
|
|
21
26
|
/** Per-provider breakdown keyed by ProviderId string. */
|
|
22
27
|
readonly byProvider: Record<string, {
|
|
23
28
|
readonly usd: number;
|
|
@@ -64,3 +69,12 @@ export declare function providerHealth(entries: LedgerEntry[]): ProviderHealth[]
|
|
|
64
69
|
* @param n - Amount in US dollars (may be 0).
|
|
65
70
|
*/
|
|
66
71
|
export declare function formatUsd(n: number): string;
|
|
72
|
+
/**
|
|
73
|
+
* Format a token count compactly: 942 → "942", 12_400 → "12.4k", 3_000_000 → "3M".
|
|
74
|
+
*
|
|
75
|
+
* Tokens are REAL measured values (unlike the USD estimates), so this is the
|
|
76
|
+
* primary live signal. Pure; never throws. Negative/NaN inputs clamp to "0".
|
|
77
|
+
*
|
|
78
|
+
* @param n - Token count.
|
|
79
|
+
*/
|
|
80
|
+
export declare function formatTokens(n: number): string;
|
package/dist/infra/insights.js
CHANGED
|
@@ -22,11 +22,16 @@ export function summarizeSpend(entries, nowIso) {
|
|
|
22
22
|
const todayDate = nowIso.slice(0, 10); // 'YYYY-MM-DD'
|
|
23
23
|
let todayUsd = 0;
|
|
24
24
|
let totalUsd = 0;
|
|
25
|
+
let todayTokens = 0;
|
|
26
|
+
let totalTokens = 0;
|
|
25
27
|
const byProvider = {};
|
|
26
28
|
for (const entry of entries) {
|
|
27
29
|
totalUsd += entry.usd;
|
|
30
|
+
const entryTokens = entry.inputTokens + entry.outputTokens;
|
|
31
|
+
totalTokens += entryTokens;
|
|
28
32
|
if (entry.timestamp.slice(0, 10) === todayDate) {
|
|
29
33
|
todayUsd += entry.usd;
|
|
34
|
+
todayTokens += entryTokens;
|
|
30
35
|
}
|
|
31
36
|
const existing = byProvider[entry.provider];
|
|
32
37
|
if (existing !== undefined) {
|
|
@@ -41,6 +46,8 @@ export function summarizeSpend(entries, nowIso) {
|
|
|
41
46
|
todayUsd,
|
|
42
47
|
totalUsd,
|
|
43
48
|
calls: entries.length,
|
|
49
|
+
todayTokens,
|
|
50
|
+
totalTokens,
|
|
44
51
|
byProvider,
|
|
45
52
|
};
|
|
46
53
|
}
|
|
@@ -102,4 +109,28 @@ export function providerHealth(entries) {
|
|
|
102
109
|
export function formatUsd(n) {
|
|
103
110
|
return '$' + n.toFixed(4);
|
|
104
111
|
}
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// formatTokens
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
/**
|
|
116
|
+
* Format a token count compactly: 942 → "942", 12_400 → "12.4k", 3_000_000 → "3M".
|
|
117
|
+
*
|
|
118
|
+
* Tokens are REAL measured values (unlike the USD estimates), so this is the
|
|
119
|
+
* primary live signal. Pure; never throws. Negative/NaN inputs clamp to "0".
|
|
120
|
+
*
|
|
121
|
+
* @param n - Token count.
|
|
122
|
+
*/
|
|
123
|
+
export function formatTokens(n) {
|
|
124
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
125
|
+
return '0';
|
|
126
|
+
if (n < 1000)
|
|
127
|
+
return String(Math.round(n));
|
|
128
|
+
if (n < 1_000_000) {
|
|
129
|
+
const k = n / 1000;
|
|
130
|
+
// One decimal, but drop a trailing ".0" (12000 → "12k", 12400 → "12.4k").
|
|
131
|
+
return (Number.isInteger(k) ? String(k) : k.toFixed(1)) + 'k';
|
|
132
|
+
}
|
|
133
|
+
const m = n / 1_000_000;
|
|
134
|
+
return (Number.isInteger(m) ? String(m) : m.toFixed(1)) + 'M';
|
|
135
|
+
}
|
|
105
136
|
//# sourceMappingURL=insights.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"insights.js","sourceRoot":"","sources":["../../src/infra/insights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"insights.js","sourceRoot":"","sources":["../../src/infra/insights.ts"],"names":[],"mappings":"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"}
|