myshell-tools 2.0.0 → 2.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 +18 -0
- package/README.md +44 -7
- package/dist/cli.js +37 -6
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/doctor.js +9 -5
- package/dist/commands/doctor.js.map +1 -1
- package/dist/core/orchestrate.js +80 -28
- package/dist/core/orchestrate.js.map +1 -1
- package/dist/core/policy.d.ts +10 -0
- package/dist/core/policy.js +40 -0
- package/dist/core/policy.js.map +1 -1
- package/dist/core/review.d.ts +5 -0
- package/dist/core/review.js +2 -2
- package/dist/core/review.js.map +1 -1
- package/dist/infra/atomic.d.ts +0 -3
- package/dist/infra/atomic.js +1 -1
- package/dist/infra/atomic.js.map +1 -1
- package/dist/infra/config.d.ts +23 -0
- package/dist/infra/config.js +64 -0
- package/dist/infra/config.js.map +1 -0
- package/dist/infra/conversation-store.d.ts +42 -0
- package/dist/infra/conversation-store.js +14 -0
- package/dist/infra/conversation-store.js.map +1 -0
- package/dist/infra/conversations.d.ts +18 -0
- package/dist/infra/conversations.js +296 -0
- package/dist/infra/conversations.js.map +1 -0
- package/dist/infra/insights.d.ts +66 -0
- package/dist/infra/insights.js +105 -0
- package/dist/infra/insights.js.map +1 -0
- package/dist/infra/ledger.d.ts +4 -6
- package/dist/infra/ledger.js.map +1 -1
- package/dist/interface/menu.d.ts +112 -0
- package/dist/interface/menu.js +661 -0
- package/dist/interface/menu.js.map +1 -0
- package/dist/providers/claude.d.ts +4 -13
- package/dist/providers/claude.js +5 -4
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/codex.d.ts +6 -12
- package/dist/providers/codex.js +6 -4
- package/dist/providers/codex.js.map +1 -1
- package/dist/providers/detect.d.ts +63 -14
- package/dist/providers/detect.js +123 -27
- package/dist/providers/detect.js.map +1 -1
- package/dist/providers/install.d.ts +34 -0
- package/dist/providers/install.js +68 -0
- package/dist/providers/install.js.map +1 -0
- package/dist/ui/tui.d.ts +127 -0
- package/dist/ui/tui.js +316 -0
- package/dist/ui/tui.js.map +1 -0
- package/package.json +4 -1
- package/dist/core/index.d.ts +0 -13
- package/dist/core/index.js +0 -12
- package/dist/core/index.js.map +0 -1
- package/dist/infra/index.d.ts +0 -9
- package/dist/infra/index.js +0 -7
- package/dist/infra/index.js.map +0 -1
- package/dist/providers/index.d.ts +0 -9
- package/dist/providers/index.js +0 -7
- package/dist/providers/index.js.map +0 -1
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
export interface AppConfig {
|
|
9
|
+
onboarded: boolean;
|
|
10
|
+
setAsDefault: boolean;
|
|
11
|
+
/** Active routing mode. Absent → use DEFAULT_POLICY (same as 'balanced'). */
|
|
12
|
+
mode?: 'cost-saver' | 'balanced' | 'quality-first';
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Load the global app config. Returns defaults merged with any on-disk
|
|
16
|
+
* values so unknown/corrupt files never throw and new keys are always present.
|
|
17
|
+
*/
|
|
18
|
+
export declare function loadConfig(homeDir?: string): Promise<AppConfig>;
|
|
19
|
+
/**
|
|
20
|
+
* Persist the app config atomically. Creates the `.myshell-tools` directory
|
|
21
|
+
* if it does not exist.
|
|
22
|
+
*/
|
|
23
|
+
export declare function saveConfig(config: AppConfig, homeDir?: string): Promise<void>;
|
|
@@ -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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"}
|