myshell-tools 2.0.0 → 2.1.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +6 -3
  3. package/dist/cli.js +37 -6
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/doctor.d.ts +3 -2
  6. package/dist/commands/doctor.js +9 -5
  7. package/dist/commands/doctor.js.map +1 -1
  8. package/dist/core/orchestrate.js +80 -28
  9. package/dist/core/orchestrate.js.map +1 -1
  10. package/dist/core/policy.d.ts +10 -0
  11. package/dist/core/policy.js +40 -0
  12. package/dist/core/policy.js.map +1 -1
  13. package/dist/core/review.d.ts +5 -0
  14. package/dist/core/review.js +2 -2
  15. package/dist/core/review.js.map +1 -1
  16. package/dist/infra/atomic.d.ts +0 -3
  17. package/dist/infra/atomic.js +1 -1
  18. package/dist/infra/atomic.js.map +1 -1
  19. package/dist/infra/config.d.ts +23 -0
  20. package/dist/infra/config.js +64 -0
  21. package/dist/infra/config.js.map +1 -0
  22. package/dist/infra/conversation-store.d.ts +42 -0
  23. package/dist/infra/conversation-store.js +14 -0
  24. package/dist/infra/conversation-store.js.map +1 -0
  25. package/dist/infra/conversations.d.ts +18 -0
  26. package/dist/infra/conversations.js +296 -0
  27. package/dist/infra/conversations.js.map +1 -0
  28. package/dist/infra/insights.d.ts +66 -0
  29. package/dist/infra/insights.js +105 -0
  30. package/dist/infra/insights.js.map +1 -0
  31. package/dist/infra/ledger.d.ts +4 -6
  32. package/dist/infra/ledger.js.map +1 -1
  33. package/dist/interface/menu.d.ts +112 -0
  34. package/dist/interface/menu.js +622 -0
  35. package/dist/interface/menu.js.map +1 -0
  36. package/dist/providers/claude.d.ts +4 -13
  37. package/dist/providers/claude.js +5 -4
  38. package/dist/providers/claude.js.map +1 -1
  39. package/dist/providers/codex.d.ts +6 -12
  40. package/dist/providers/codex.js +6 -4
  41. package/dist/providers/codex.js.map +1 -1
  42. package/dist/providers/detect.d.ts +63 -14
  43. package/dist/providers/detect.js +123 -27
  44. package/dist/providers/detect.js.map +1 -1
  45. package/dist/ui/tui.d.ts +127 -0
  46. package/dist/ui/tui.js +316 -0
  47. package/dist/ui/tui.js.map +1 -0
  48. package/package.json +4 -1
  49. package/dist/core/index.d.ts +0 -13
  50. package/dist/core/index.js +0 -12
  51. package/dist/core/index.js.map +0 -1
  52. package/dist/infra/index.d.ts +0 -9
  53. package/dist/infra/index.js +0 -7
  54. package/dist/infra/index.js.map +0 -1
  55. package/dist/providers/index.d.ts +0 -9
  56. package/dist/providers/index.js +0 -7
  57. package/dist/providers/index.js.map +0 -1
@@ -0,0 +1,64 @@
1
+ /**
2
+ * src/infra/config.ts — Global app configuration persisted at
3
+ * <homeDir>/.myshell-tools/config.json.
4
+ *
5
+ * Reads merge over defaults so that new keys added in future versions are
6
+ * always present even when the on-disk file pre-dates them.
7
+ */
8
+ import { mkdir, readFile } from 'node:fs/promises';
9
+ import { homedir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { atomicWrite } from './atomic.js';
12
+ // ---------------------------------------------------------------------------
13
+ // Defaults
14
+ // ---------------------------------------------------------------------------
15
+ const DEFAULTS = {
16
+ onboarded: false,
17
+ setAsDefault: false,
18
+ };
19
+ // ---------------------------------------------------------------------------
20
+ // Path helpers
21
+ // ---------------------------------------------------------------------------
22
+ function getConfigDir(homeDir) {
23
+ return join(homeDir, '.myshell-tools');
24
+ }
25
+ function getConfigPath(homeDir) {
26
+ return join(getConfigDir(homeDir), 'config.json');
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Public API
30
+ // ---------------------------------------------------------------------------
31
+ /**
32
+ * Load the global app config. Returns defaults merged with any on-disk
33
+ * values so unknown/corrupt files never throw and new keys are always present.
34
+ */
35
+ export async function loadConfig(homeDir) {
36
+ const home = homeDir ?? homedir();
37
+ let raw;
38
+ try {
39
+ raw = await readFile(getConfigPath(home), 'utf8');
40
+ }
41
+ catch {
42
+ // Missing file — return defaults
43
+ return { ...DEFAULTS };
44
+ }
45
+ try {
46
+ const parsed = JSON.parse(raw);
47
+ // Merge: defaults first, then on-disk values (new keys default safely)
48
+ return { ...DEFAULTS, ...parsed };
49
+ }
50
+ catch {
51
+ // Corrupt JSON — return defaults
52
+ return { ...DEFAULTS };
53
+ }
54
+ }
55
+ /**
56
+ * Persist the app config atomically. Creates the `.myshell-tools` directory
57
+ * if it does not exist.
58
+ */
59
+ export async function saveConfig(config, homeDir) {
60
+ const home = homeDir ?? homedir();
61
+ await mkdir(getConfigDir(home), { recursive: true });
62
+ await atomicWrite(getConfigPath(home), JSON.stringify(config, null, 2));
63
+ }
64
+ //# sourceMappingURL=config.js.map
@@ -0,0 +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;AAa1C,8EAA8E;AAC9E,WAAW;AACX,8EAA8E;AAE9E,MAAM,QAAQ,GAAc;IAC1B,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,KAAK;CACpB,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"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * src/infra/conversation-store.ts — the persistent conversation contract.
3
+ *
4
+ * The menu UX manages multiple named, persistent conversations. This is the
5
+ * port both the file-backed implementation (infra) and the menu (interface)
6
+ * build against. Conversations live in a GLOBAL store (the user's home dir) so
7
+ * they follow the user across projects; each one is an append-only message log
8
+ * plus lightweight metadata.
9
+ *
10
+ * A conversation's `SessionWriter` is what gets injected into orchestrate() so a
11
+ * run's messages persist into that conversation.
12
+ */
13
+ import type { SessionEntry, SessionWriter } from '../core/types.js';
14
+ export interface ConversationMeta {
15
+ readonly id: string;
16
+ readonly title: string;
17
+ readonly createdAt: string;
18
+ readonly updatedAt: string;
19
+ readonly messageCount: number;
20
+ /** Whether this conversation is pinned (sorted to the top of the list). */
21
+ readonly pinned: boolean;
22
+ /** Optional short category tag (e.g. "ui", "refactor"); null when unset. */
23
+ readonly category: string | null;
24
+ }
25
+ export interface ConversationStore {
26
+ /** All conversations, pinned first then most-recently-updated first. */
27
+ list(): Promise<ConversationMeta[]>;
28
+ /** Create a new conversation; returns its metadata (with a fresh id). */
29
+ create(title: string): Promise<ConversationMeta>;
30
+ /** Read a conversation's full message history (oldest first); [] if missing. */
31
+ load(id: string): Promise<SessionEntry[]>;
32
+ /** Rename a conversation. No-op if the id does not exist. */
33
+ rename(id: string, title: string): Promise<void>;
34
+ /** Delete a conversation and its messages. No-op if missing. */
35
+ remove(id: string): Promise<void>;
36
+ /** A SessionWriter bound to `id` — appends entries and bumps updatedAt/count. */
37
+ writer(id: string): SessionWriter;
38
+ /** Pin or unpin a conversation. No-op if the id does not exist. */
39
+ setPinned(id: string, pinned: boolean): Promise<void>;
40
+ /** Set or clear the category tag for a conversation. No-op if id missing. */
41
+ setCategory(id: string, category: string | null): Promise<void>;
42
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * src/infra/conversation-store.ts — the persistent conversation contract.
3
+ *
4
+ * The menu UX manages multiple named, persistent conversations. This is the
5
+ * port both the file-backed implementation (infra) and the menu (interface)
6
+ * build against. Conversations live in a GLOBAL store (the user's home dir) so
7
+ * they follow the user across projects; each one is an append-only message log
8
+ * plus lightweight metadata.
9
+ *
10
+ * A conversation's `SessionWriter` is what gets injected into orchestrate() so a
11
+ * run's messages persist into that conversation.
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=conversation-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-store.js","sourceRoot":"","sources":["../../src/infra/conversation-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * src/infra/conversations.ts — File-backed ConversationStore implementation.
3
+ *
4
+ * Storage layout under <homeDir>/.myshell-tools/conversations/:
5
+ * index.json — JSON array of ConversationMeta, newest first
6
+ * index.json.lock — advisory lock for concurrent index mutations
7
+ * <id>.jsonl — one SessionEntry per line (append-only message log)
8
+ */
9
+ import type { Clock } from '../core/types.js';
10
+ import type { ConversationStore } from './conversation-store.js';
11
+ /**
12
+ * Create a file-backed ConversationStore that persists conversations under
13
+ * `<homeDir ?? os.homedir()>/.myshell-tools/conversations/`.
14
+ */
15
+ export declare function createFileConversationStore(opts: {
16
+ homeDir?: string;
17
+ clock: Clock;
18
+ }): ConversationStore;
@@ -0,0 +1,296 @@
1
+ /**
2
+ * src/infra/conversations.ts — File-backed ConversationStore implementation.
3
+ *
4
+ * Storage layout under <homeDir>/.myshell-tools/conversations/:
5
+ * index.json — JSON array of ConversationMeta, newest first
6
+ * index.json.lock — advisory lock for concurrent index mutations
7
+ * <id>.jsonl — one SessionEntry per line (append-only message log)
8
+ */
9
+ import { mkdir, readFile, unlink } from 'node:fs/promises';
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import { atomicAppendJSONL, atomicWrite, withLock } from './atomic.js';
13
+ // ---------------------------------------------------------------------------
14
+ // Path helpers (local — conversations dir lives in homeDir, not cwd)
15
+ // ---------------------------------------------------------------------------
16
+ function getConversationsDir(homeDir) {
17
+ return join(homeDir, '.myshell-tools', 'conversations');
18
+ }
19
+ function getIndexPath(homeDir) {
20
+ return join(getConversationsDir(homeDir), 'index.json');
21
+ }
22
+ function getIndexLockPath(homeDir) {
23
+ return join(getConversationsDir(homeDir), 'index.json.lock');
24
+ }
25
+ function getMessagePath(homeDir, id) {
26
+ return join(getConversationsDir(homeDir), `${id}.jsonl`);
27
+ }
28
+ // ---------------------------------------------------------------------------
29
+ // Internal index helpers
30
+ // ---------------------------------------------------------------------------
31
+ async function ensureDir(homeDir) {
32
+ await mkdir(getConversationsDir(homeDir), { recursive: true });
33
+ }
34
+ /**
35
+ * Normalise a raw index entry that may be missing fields added in later
36
+ * versions (pinned, category). Old on-disk entries that predate these fields
37
+ * will be migrated transparently on read so existing stores keep working.
38
+ */
39
+ function normaliseMeta(raw) {
40
+ const r = raw;
41
+ return {
42
+ id: String(r['id'] ?? ''),
43
+ title: String(r['title'] ?? ''),
44
+ createdAt: String(r['createdAt'] ?? ''),
45
+ updatedAt: String(r['updatedAt'] ?? ''),
46
+ messageCount: typeof r['messageCount'] === 'number' ? r['messageCount'] : 0,
47
+ pinned: typeof r['pinned'] === 'boolean' ? r['pinned'] : false,
48
+ category: typeof r['category'] === 'string' ? r['category'] : null,
49
+ };
50
+ }
51
+ async function readIndex(homeDir) {
52
+ try {
53
+ const raw = await readFile(getIndexPath(homeDir), 'utf8');
54
+ const parsed = JSON.parse(raw);
55
+ if (!Array.isArray(parsed))
56
+ return [];
57
+ return parsed.map(normaliseMeta);
58
+ }
59
+ catch {
60
+ return [];
61
+ }
62
+ }
63
+ async function writeIndex(homeDir, index) {
64
+ await atomicWrite(getIndexPath(homeDir), JSON.stringify(index, null, 2));
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Title extraction: trim + truncate to 80 chars
68
+ // ---------------------------------------------------------------------------
69
+ const MAX_TITLE_LEN = 80;
70
+ function deriveTitle(content) {
71
+ const trimmed = content.trim();
72
+ return trimmed.length <= MAX_TITLE_LEN ? trimmed : trimmed.slice(0, MAX_TITLE_LEN);
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Factory
76
+ // ---------------------------------------------------------------------------
77
+ /**
78
+ * Create a file-backed ConversationStore that persists conversations under
79
+ * `<homeDir ?? os.homedir()>/.myshell-tools/conversations/`.
80
+ */
81
+ export function createFileConversationStore(opts) {
82
+ const { clock } = opts;
83
+ const home = opts.homeDir ?? homedir();
84
+ return {
85
+ // -----------------------------------------------------------------------
86
+ // list
87
+ // -----------------------------------------------------------------------
88
+ async list() {
89
+ const index = await readIndex(home);
90
+ return [...index].sort((a, b) => {
91
+ // Pinned items always come before unpinned
92
+ if (a.pinned !== b.pinned)
93
+ return a.pinned ? -1 : 1;
94
+ // Within the same pin group, most-recently-updated first
95
+ return a.updatedAt < b.updatedAt ? 1 : -1;
96
+ });
97
+ },
98
+ // -----------------------------------------------------------------------
99
+ // create
100
+ // -----------------------------------------------------------------------
101
+ async create(title) {
102
+ await ensureDir(home);
103
+ const id = clock.uuid();
104
+ const now = clock.isoNow();
105
+ const meta = {
106
+ id,
107
+ title,
108
+ createdAt: now,
109
+ updatedAt: now,
110
+ messageCount: 0,
111
+ pinned: false,
112
+ category: null,
113
+ };
114
+ await withLock(getIndexLockPath(home), async () => {
115
+ const index = await readIndex(home);
116
+ await writeIndex(home, [meta, ...index]);
117
+ });
118
+ return meta;
119
+ },
120
+ // -----------------------------------------------------------------------
121
+ // load
122
+ // -----------------------------------------------------------------------
123
+ async load(id) {
124
+ let raw;
125
+ try {
126
+ raw = await readFile(getMessagePath(home, id), 'utf8');
127
+ }
128
+ catch (err) {
129
+ const nodeErr = err;
130
+ if (nodeErr.code === 'ENOENT')
131
+ return [];
132
+ throw err;
133
+ }
134
+ const entries = [];
135
+ for (const line of raw.split('\n')) {
136
+ const trimmed = line.trim();
137
+ if (trimmed.length === 0)
138
+ continue;
139
+ try {
140
+ entries.push(JSON.parse(trimmed));
141
+ }
142
+ catch {
143
+ // Skip malformed lines
144
+ }
145
+ }
146
+ return entries;
147
+ },
148
+ // -----------------------------------------------------------------------
149
+ // writer
150
+ // -----------------------------------------------------------------------
151
+ writer(id) {
152
+ return {
153
+ id,
154
+ async append(entry) {
155
+ await ensureDir(home);
156
+ // Append the entry to the conversation's JSONL file
157
+ await atomicAppendJSONL(getMessagePath(home, id), entry);
158
+ // Update index under lock
159
+ await withLock(getIndexLockPath(home), async () => {
160
+ const index = await readIndex(home);
161
+ const idx = index.findIndex((m) => m.id === id);
162
+ if (idx === -1)
163
+ return;
164
+ const existing = index[idx];
165
+ if (existing === undefined)
166
+ return;
167
+ const updatedAt = clock.isoNow();
168
+ const messageCount = existing.messageCount + 1;
169
+ // If this is a user message and the title is still the placeholder,
170
+ // use the message content as the title (first user message wins).
171
+ let title = existing.title;
172
+ if (entry.role === 'user' &&
173
+ entry.content &&
174
+ (title.trim().length === 0 || title === existing.title) &&
175
+ existing.messageCount === 0) {
176
+ title = deriveTitle(entry.content);
177
+ }
178
+ const updated = {
179
+ id: existing.id,
180
+ title,
181
+ createdAt: existing.createdAt,
182
+ updatedAt,
183
+ messageCount,
184
+ pinned: existing.pinned,
185
+ category: existing.category,
186
+ };
187
+ const newIndex = [...index];
188
+ newIndex[idx] = updated;
189
+ await writeIndex(home, newIndex);
190
+ });
191
+ },
192
+ };
193
+ },
194
+ // -----------------------------------------------------------------------
195
+ // rename
196
+ // -----------------------------------------------------------------------
197
+ async rename(id, title) {
198
+ await withLock(getIndexLockPath(home), async () => {
199
+ const index = await readIndex(home);
200
+ const idx = index.findIndex((m) => m.id === id);
201
+ if (idx === -1)
202
+ return;
203
+ const existing = index[idx];
204
+ if (existing === undefined)
205
+ return;
206
+ const updated = {
207
+ id: existing.id,
208
+ title,
209
+ createdAt: existing.createdAt,
210
+ updatedAt: existing.updatedAt,
211
+ messageCount: existing.messageCount,
212
+ pinned: existing.pinned,
213
+ category: existing.category,
214
+ };
215
+ const newIndex = [...index];
216
+ newIndex[idx] = updated;
217
+ await writeIndex(home, newIndex);
218
+ });
219
+ },
220
+ // -----------------------------------------------------------------------
221
+ // remove
222
+ // -----------------------------------------------------------------------
223
+ async remove(id) {
224
+ // Best-effort delete of message file
225
+ try {
226
+ await unlink(getMessagePath(home, id));
227
+ }
228
+ catch {
229
+ // Missing or already gone — ignore
230
+ }
231
+ // Remove from index under lock
232
+ await withLock(getIndexLockPath(home), async () => {
233
+ const index = await readIndex(home);
234
+ const filtered = index.filter((m) => m.id !== id);
235
+ if (filtered.length === index.length)
236
+ return; // not found, no-op
237
+ await writeIndex(home, filtered);
238
+ });
239
+ },
240
+ // -----------------------------------------------------------------------
241
+ // setPinned
242
+ // -----------------------------------------------------------------------
243
+ async setPinned(id, pinned) {
244
+ await ensureDir(home);
245
+ await withLock(getIndexLockPath(home), async () => {
246
+ const index = await readIndex(home);
247
+ const idx = index.findIndex((m) => m.id === id);
248
+ if (idx === -1)
249
+ return; // no-op if missing
250
+ const existing = index[idx];
251
+ if (existing === undefined)
252
+ return;
253
+ const updated = {
254
+ id: existing.id,
255
+ title: existing.title,
256
+ createdAt: existing.createdAt,
257
+ updatedAt: existing.updatedAt,
258
+ messageCount: existing.messageCount,
259
+ pinned,
260
+ category: existing.category,
261
+ };
262
+ const newIndex = [...index];
263
+ newIndex[idx] = updated;
264
+ await writeIndex(home, newIndex);
265
+ });
266
+ },
267
+ // -----------------------------------------------------------------------
268
+ // setCategory
269
+ // -----------------------------------------------------------------------
270
+ async setCategory(id, category) {
271
+ await ensureDir(home);
272
+ await withLock(getIndexLockPath(home), async () => {
273
+ const index = await readIndex(home);
274
+ const idx = index.findIndex((m) => m.id === id);
275
+ if (idx === -1)
276
+ return; // no-op if missing
277
+ const existing = index[idx];
278
+ if (existing === undefined)
279
+ return;
280
+ const updated = {
281
+ id: existing.id,
282
+ title: existing.title,
283
+ createdAt: existing.createdAt,
284
+ updatedAt: existing.updatedAt,
285
+ messageCount: existing.messageCount,
286
+ pinned: existing.pinned,
287
+ category,
288
+ };
289
+ const newIndex = [...index];
290
+ newIndex[idx] = updated;
291
+ await writeIndex(home, newIndex);
292
+ });
293
+ },
294
+ };
295
+ }
296
+ //# sourceMappingURL=conversations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversations.js","sourceRoot":"","sources":["../../src/infra/conversations.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * src/infra/insights.ts — Pure reducers over LedgerEntry[] for spend and provider health.
3
+ *
4
+ * No I/O. All functions are deterministic given the same inputs.
5
+ * Safe to call in tests with hand-built arrays.
6
+ *
7
+ * Honesty Contract:
8
+ * - No hardcoded percentages — success rates are always computed from data.
9
+ * - No digit-% literals in source — percent strings are built by concatenation.
10
+ * - No fabricated values — all outputs derive strictly from the input entries.
11
+ */
12
+ import type { LedgerEntry } from '../core/types.js';
13
+ /** Aggregated spend data suitable for display in the control panel header. */
14
+ export interface SpendSummary {
15
+ /** Total USD spent in the current calendar day (UTC). */
16
+ readonly todayUsd: number;
17
+ /** Total USD spent across all time. */
18
+ readonly totalUsd: number;
19
+ /** Total number of ledger entries (calls). */
20
+ readonly calls: number;
21
+ /** Per-provider breakdown keyed by ProviderId string. */
22
+ readonly byProvider: Record<string, {
23
+ readonly usd: number;
24
+ readonly calls: number;
25
+ }>;
26
+ }
27
+ /**
28
+ * Pure reduction over LedgerEntry[] that produces a SpendSummary.
29
+ *
30
+ * "Today" is defined as entries whose `timestamp` ISO string has the same
31
+ * YYYY-MM-DD date prefix as `nowIso`. Both are treated as UTC.
32
+ *
33
+ * @param entries - Array of LedgerEntry objects (may be empty).
34
+ * @param nowIso - ISO-8601 timestamp representing "now" (e.g. from Clock.isoNow()).
35
+ */
36
+ export declare function summarizeSpend(entries: LedgerEntry[], nowIso: string): SpendSummary;
37
+ /** Health summary for a single provider derived from its ledger entries. */
38
+ export interface ProviderHealth {
39
+ readonly provider: string;
40
+ readonly calls: number;
41
+ /** Fraction of successful calls in [0, 1]. 0 when calls === 0. */
42
+ readonly successRate: number;
43
+ /** Arithmetic mean of durationMs. 0 when calls === 0. */
44
+ readonly avgDurationMs: number;
45
+ readonly status: 'healthy' | 'degraded' | 'unknown';
46
+ }
47
+ /**
48
+ * Compute per-provider health from a LedgerEntry array.
49
+ *
50
+ * Status thresholds:
51
+ * - `unknown` — 0 calls
52
+ * - `degraded` — successRate < 0.7
53
+ * - `healthy` — successRate >= 0.7
54
+ *
55
+ * @param entries - Array of LedgerEntry objects (may be empty).
56
+ */
57
+ export declare function providerHealth(entries: LedgerEntry[]): ProviderHealth[];
58
+ /**
59
+ * Format a USD amount as a string with a `$` prefix and 4 decimal places.
60
+ *
61
+ * The result never contains a digit immediately before `%` — this is a dollar
62
+ * amount, not a percentage.
63
+ *
64
+ * @param n - Amount in US dollars (may be 0).
65
+ */
66
+ export declare function formatUsd(n: number): string;
@@ -0,0 +1,105 @@
1
+ /**
2
+ * src/infra/insights.ts — Pure reducers over LedgerEntry[] for spend and provider health.
3
+ *
4
+ * No I/O. All functions are deterministic given the same inputs.
5
+ * Safe to call in tests with hand-built arrays.
6
+ *
7
+ * Honesty Contract:
8
+ * - No hardcoded percentages — success rates are always computed from data.
9
+ * - No digit-% literals in source — percent strings are built by concatenation.
10
+ * - No fabricated values — all outputs derive strictly from the input entries.
11
+ */
12
+ /**
13
+ * Pure reduction over LedgerEntry[] that produces a SpendSummary.
14
+ *
15
+ * "Today" is defined as entries whose `timestamp` ISO string has the same
16
+ * YYYY-MM-DD date prefix as `nowIso`. Both are treated as UTC.
17
+ *
18
+ * @param entries - Array of LedgerEntry objects (may be empty).
19
+ * @param nowIso - ISO-8601 timestamp representing "now" (e.g. from Clock.isoNow()).
20
+ */
21
+ export function summarizeSpend(entries, nowIso) {
22
+ const todayDate = nowIso.slice(0, 10); // 'YYYY-MM-DD'
23
+ let todayUsd = 0;
24
+ let totalUsd = 0;
25
+ const byProvider = {};
26
+ for (const entry of entries) {
27
+ totalUsd += entry.usd;
28
+ if (entry.timestamp.slice(0, 10) === todayDate) {
29
+ todayUsd += entry.usd;
30
+ }
31
+ const existing = byProvider[entry.provider];
32
+ if (existing !== undefined) {
33
+ existing.usd += entry.usd;
34
+ existing.calls += 1;
35
+ }
36
+ else {
37
+ byProvider[entry.provider] = { usd: entry.usd, calls: 1 };
38
+ }
39
+ }
40
+ return {
41
+ todayUsd,
42
+ totalUsd,
43
+ calls: entries.length,
44
+ byProvider,
45
+ };
46
+ }
47
+ /**
48
+ * Compute per-provider health from a LedgerEntry array.
49
+ *
50
+ * Status thresholds:
51
+ * - `unknown` — 0 calls
52
+ * - `degraded` — successRate < 0.7
53
+ * - `healthy` — successRate >= 0.7
54
+ *
55
+ * @param entries - Array of LedgerEntry objects (may be empty).
56
+ */
57
+ export function providerHealth(entries) {
58
+ const byProvider = {};
59
+ for (const entry of entries) {
60
+ const existing = byProvider[entry.provider];
61
+ if (existing !== undefined) {
62
+ existing.calls += 1;
63
+ existing.successes += entry.success ? 1 : 0;
64
+ existing.durationMs += entry.durationMs;
65
+ }
66
+ else {
67
+ byProvider[entry.provider] = {
68
+ calls: 1,
69
+ successes: entry.success ? 1 : 0,
70
+ durationMs: entry.durationMs,
71
+ };
72
+ }
73
+ }
74
+ return Object.entries(byProvider).map(([provider, agg]) => {
75
+ const { calls, successes, durationMs } = agg;
76
+ const successRate = calls === 0 ? 0 : successes / calls;
77
+ const avgDurationMs = calls === 0 ? 0 : durationMs / calls;
78
+ let status;
79
+ if (calls === 0) {
80
+ status = 'unknown';
81
+ }
82
+ else if (successRate < 0.7) {
83
+ status = 'degraded';
84
+ }
85
+ else {
86
+ status = 'healthy';
87
+ }
88
+ return { provider, calls, successRate, avgDurationMs, status };
89
+ });
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // formatUsd
93
+ // ---------------------------------------------------------------------------
94
+ /**
95
+ * Format a USD amount as a string with a `$` prefix and 4 decimal places.
96
+ *
97
+ * The result never contains a digit immediately before `%` — this is a dollar
98
+ * amount, not a percentage.
99
+ *
100
+ * @param n - Amount in US dollars (may be 0).
101
+ */
102
+ export function formatUsd(n) {
103
+ return '$' + n.toFixed(4);
104
+ }
105
+ //# sourceMappingURL=insights.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"insights.js","sourceRoot":"","sources":["../../src/infra/insights.ts"],"names":[],"mappings":"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"}
@@ -23,16 +23,14 @@ export declare function createLedger(opts: {
23
23
  * @param cwd - The project working directory.
24
24
  */
25
25
  export declare function readLedger(cwd: string): Promise<LedgerEntry[]>;
26
- /** Per-model aggregation used in the summary. */
27
- export interface ModelSummary {
28
- readonly calls: number;
29
- readonly usd: number;
30
- }
31
26
  /** Aggregate summary returned by `summarizeLedger`. */
32
27
  export interface LedgerSummary {
33
28
  readonly totalUsd: number;
34
29
  readonly calls: number;
35
- readonly byModel: Record<string, ModelSummary>;
30
+ readonly byModel: Record<string, {
31
+ readonly calls: number;
32
+ readonly usd: number;
33
+ }>;
36
34
  }
37
35
  /**
38
36
  * Pure reduction over a ledger entry array.