@gaunt-sloth/core 2.0.0-alpha.1 → 2.0.0-alpha.11

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 (126) hide show
  1. package/.gsloth.code.md +10 -0
  2. package/LICENSE +7 -0
  3. package/README.md +3 -4
  4. package/dist/config/defaults.d.ts +85 -0
  5. package/dist/config/defaults.js +101 -0
  6. package/dist/config/defaults.js.map +1 -0
  7. package/dist/config/jsonc.d.ts +12 -0
  8. package/dist/config/jsonc.js +41 -0
  9. package/dist/config/jsonc.js.map +1 -0
  10. package/dist/config/loader.d.ts +109 -0
  11. package/dist/config/loader.js +663 -0
  12. package/dist/config/loader.js.map +1 -0
  13. package/dist/config/schema.d.ts +509 -0
  14. package/dist/config/schema.js +349 -0
  15. package/dist/config/schema.js.map +1 -0
  16. package/dist/config/shell-policy.d.ts +212 -0
  17. package/dist/config/shell-policy.js +142 -0
  18. package/dist/config/shell-policy.js.map +1 -0
  19. package/dist/config/types.d.ts +490 -0
  20. package/dist/config/types.js +13 -0
  21. package/dist/config/types.js.map +1 -0
  22. package/dist/config.d.ts +18 -647
  23. package/dist/config.js +15 -516
  24. package/dist/config.js.map +1 -1
  25. package/dist/constants.d.ts +32 -0
  26. package/dist/constants.js +32 -0
  27. package/dist/constants.js.map +1 -1
  28. package/dist/core/GthAbstractAgent.d.ts +57 -1
  29. package/dist/core/GthAbstractAgent.js +171 -4
  30. package/dist/core/GthAbstractAgent.js.map +1 -1
  31. package/dist/core/GthAgentRunner.d.ts +158 -1
  32. package/dist/core/GthAgentRunner.js +367 -4
  33. package/dist/core/GthAgentRunner.js.map +1 -1
  34. package/dist/core/GthLangChainAgent.js +118 -5
  35. package/dist/core/GthLangChainAgent.js.map +1 -1
  36. package/dist/core/debugCapture.d.ts +59 -0
  37. package/dist/core/debugCapture.js +101 -0
  38. package/dist/core/debugCapture.js.map +1 -0
  39. package/dist/core/gthLeanAgentFactory.d.ts +9 -0
  40. package/dist/core/gthLeanAgentFactory.js +10 -0
  41. package/dist/core/gthLeanAgentFactory.js.map +1 -0
  42. package/dist/core/runStats.d.ts +41 -0
  43. package/dist/core/runStats.js +73 -0
  44. package/dist/core/runStats.js.map +1 -0
  45. package/dist/core/shell/ShellCommandFailedError.d.ts +54 -0
  46. package/dist/core/shell/ShellCommandFailedError.js +68 -0
  47. package/dist/core/shell/ShellCommandFailedError.js.map +1 -0
  48. package/dist/core/shell/allowlist.d.ts +75 -0
  49. package/dist/core/shell/allowlist.js +187 -0
  50. package/dist/core/shell/allowlist.js.map +1 -0
  51. package/dist/core/shell/arity.d.ts +75 -0
  52. package/dist/core/shell/arity.js +313 -0
  53. package/dist/core/shell/arity.js.map +1 -0
  54. package/dist/core/shell/judge.d.ts +161 -0
  55. package/dist/core/shell/judge.js +261 -0
  56. package/dist/core/shell/judge.js.map +1 -0
  57. package/dist/core/shell/normalize.d.ts +27 -0
  58. package/dist/core/shell/normalize.js +53 -0
  59. package/dist/core/shell/normalize.js.map +1 -0
  60. package/dist/core/types.d.ts +124 -0
  61. package/dist/core/types.js.map +1 -1
  62. package/dist/history/historyFormat.d.ts +28 -0
  63. package/dist/history/historyFormat.js +127 -0
  64. package/dist/history/historyFormat.js.map +1 -0
  65. package/dist/history/historyStore.d.ts +198 -0
  66. package/dist/history/historyStore.js +469 -0
  67. package/dist/history/historyStore.js.map +1 -0
  68. package/dist/history/recordSession.d.ts +37 -0
  69. package/dist/history/recordSession.js +56 -0
  70. package/dist/history/recordSession.js.map +1 -0
  71. package/dist/index.d.ts +4 -0
  72. package/dist/index.js +4 -0
  73. package/dist/index.js.map +1 -1
  74. package/dist/providers/anthropic.d.ts +1 -1
  75. package/dist/providers/anthropic.js +5 -10
  76. package/dist/providers/anthropic.js.map +1 -1
  77. package/dist/providers/deepseek.d.ts +1 -1
  78. package/dist/providers/deepseek.js +5 -10
  79. package/dist/providers/deepseek.js.map +1 -1
  80. package/dist/providers/google-genai.d.ts +1 -1
  81. package/dist/providers/google-genai.js +5 -10
  82. package/dist/providers/google-genai.js.map +1 -1
  83. package/dist/providers/groq.d.ts +1 -1
  84. package/dist/providers/groq.js +5 -10
  85. package/dist/providers/groq.js.map +1 -1
  86. package/dist/providers/huggingface.d.ts +25 -0
  87. package/dist/providers/huggingface.js +69 -0
  88. package/dist/providers/huggingface.js.map +1 -0
  89. package/dist/providers/modelCatalog.d.ts +109 -0
  90. package/dist/providers/modelCatalog.js +245 -0
  91. package/dist/providers/modelCatalog.js.map +1 -0
  92. package/dist/providers/modelDiscovery.d.ts +54 -1
  93. package/dist/providers/modelDiscovery.js +119 -8
  94. package/dist/providers/modelDiscovery.js.map +1 -1
  95. package/dist/providers/ollama.d.ts +1 -1
  96. package/dist/providers/ollama.js +5 -15
  97. package/dist/providers/ollama.js.map +1 -1
  98. package/dist/providers/openai.d.ts +1 -1
  99. package/dist/providers/openai.js +5 -10
  100. package/dist/providers/openai.js.map +1 -1
  101. package/dist/providers/openrouter.d.ts +1 -1
  102. package/dist/providers/openrouter.js +7 -12
  103. package/dist/providers/openrouter.js.map +1 -1
  104. package/dist/providers/vertexai.d.ts +1 -1
  105. package/dist/providers/vertexai.js +5 -10
  106. package/dist/providers/vertexai.js.map +1 -1
  107. package/dist/providers/xai.d.ts +1 -1
  108. package/dist/providers/xai.js +5 -10
  109. package/dist/providers/xai.js.map +1 -1
  110. package/dist/runtime/singleShot.d.ts +5 -2
  111. package/dist/runtime/singleShot.js +36 -3
  112. package/dist/runtime/singleShot.js.map +1 -1
  113. package/dist/utils/consoleUtils.d.ts +13 -0
  114. package/dist/utils/consoleUtils.js +31 -0
  115. package/dist/utils/consoleUtils.js.map +1 -1
  116. package/dist/utils/fileUtils.d.ts +24 -2
  117. package/dist/utils/fileUtils.js +54 -12
  118. package/dist/utils/fileUtils.js.map +1 -1
  119. package/dist/utils/systemPromptNotes.d.ts +89 -0
  120. package/dist/utils/systemPromptNotes.js +184 -0
  121. package/dist/utils/systemPromptNotes.js.map +1 -0
  122. package/dist/utils/systemUtils.d.ts +31 -0
  123. package/dist/utils/systemUtils.js +38 -0
  124. package/dist/utils/systemUtils.js.map +1 -1
  125. package/package.json +16 -11
  126. package/schema/gsloth-config.schema.json +1579 -0
@@ -1,4 +1,5 @@
1
1
  import type { GthConfig } from '#src/config.js';
2
+ import type { ShellSafetyVerdict } from '#src/core/shell/judge.js';
2
3
  import type { BaseMessage } from '@langchain/core/messages';
3
4
  import type { RunnableConfig } from '@langchain/core/runnables';
4
5
  import type { StructuredToolInterface } from '@langchain/core/tools';
@@ -21,6 +22,23 @@ export declare enum StatusLevel {
21
22
  STREAM = 6
22
23
  }
23
24
  export type GthCommand = 'ask' | 'pr' | 'review' | 'chat' | 'code' | 'api' | 'exec';
25
+ /**
26
+ * GS2-16 — per-run analytics harvested from a finished agent turn, threaded into the opt-in
27
+ * history recorder ({@link recordSessionSafe}) so `gth insights` reports real numbers instead of
28
+ * zeros. All fields are best-effort: token counts are only present when the provider actually
29
+ * reported `usage_metadata` (otherwise omitted so the recorder stores NULL and the insights
30
+ * formatter suppresses the misleading `0`), and `tools` lists the names of tools invoked during
31
+ * the run (deduplicated, order-insensitive). There is no `costUsd` — cost requires a reliable
32
+ * price table this project does not carry, so it is deliberately never invented here.
33
+ */
34
+ export interface GthRunStats {
35
+ /** Total prompt/input tokens across the run's LLM calls, when the provider reported usage. */
36
+ tokensInput?: number;
37
+ /** Total completion/output tokens across the run's LLM calls, when the provider reported usage. */
38
+ tokensOutput?: number;
39
+ /** Names of tools invoked during the run (deduplicated); empty when no tools were used. */
40
+ tools: string[];
41
+ }
24
42
  /**
25
43
  * Typed events emitted by the agent's {@link GthAgentInterface#streamWithEvents} path.
26
44
  * This is the renderer contract shared by every consumer of an agent run — the AG-UI
@@ -52,6 +70,13 @@ export type AgentStreamEvent = {
52
70
  type: 'tool_result';
53
71
  id: string;
54
72
  content: string;
73
+ /**
74
+ * True when the underlying `ToolMessage.status` is `'error'` (LangChain's real
75
+ * tool-result error signal). Absent/undefined means success — consumers must not
76
+ * sniff the result text to infer failure. Optional for backward compatibility with
77
+ * producers that predate the field.
78
+ */
79
+ isError?: boolean;
55
80
  };
56
81
  /**
57
82
  * The minimal structural surface of a compiled LangGraph agent that the shared agent
@@ -66,7 +91,61 @@ export interface GthCompiledGraph {
66
91
  messages: BaseMessage[];
67
92
  }>;
68
93
  stream(input: any, config?: any): Promise<IterableReadableStream<any>>;
94
+ /**
95
+ * Read the checkpointed graph state for a thread. Present on LangGraph compiled graphs
96
+ * (both `createAgent` and `createDeepAgent`); used to detect a graph suspended on a
97
+ * human-in-the-loop `interrupt()` (its pending {@link PendingToolInterrupt} lives in
98
+ * `state.tasks[].interrupts[].value`). Optional because the structural surface predates it.
99
+ */
100
+ getState?(config: RunnableConfig): Promise<any>;
69
101
  }
102
+ /**
103
+ * A single tool call a human-in-the-loop interrupt is waiting on, surfaced from the
104
+ * suspended graph state so a consumer (the interactive session) can render an approve/reject
105
+ * prompt. Mirrors LangChain's HITL `ActionRequest` (tool name + the args it would run with).
106
+ */
107
+ export interface PendingToolInterrupt {
108
+ name: string;
109
+ args: Record<string, unknown>;
110
+ /**
111
+ * EXT-10 — when the LLM-as-judge safety gate escalated this `run_shell_command` to the human
112
+ * (rather than auto-approving it), the judge's verdict is attached here so the approval surface
113
+ * can show a "safety judge flagged: <reason>" notice. Absent when the judge is disabled (the
114
+ * default) or when the command reached the human without going through the judge.
115
+ */
116
+ safetyVerdict?: ShellSafetyVerdict;
117
+ }
118
+ /**
119
+ * Persistence scope for an `approve` decision (EXT-9 Tier-2 allow-list ergonomics):
120
+ * - `once` — run this single invocation only; remember nothing (the default).
121
+ * - `session` — remember the command's classified prefix for the life of this runner
122
+ * instance, so flag-variants of the same operation auto-approve without re-prompting.
123
+ * - `always` — additionally persist the prefix to the project allow-list
124
+ * (`.gsloth/.gsloth-settings/shell-allowlist.json`) so it survives across runs.
125
+ */
126
+ export type ToolApprovalScope = 'once' | 'session' | 'always';
127
+ /**
128
+ * A consumer-supplied decision on a {@link PendingToolInterrupt}: approve runs the tool,
129
+ * reject feeds the model a tool-rejected message (with the optional reason).
130
+ *
131
+ * `approve` carries an optional {@link ToolApprovalScope}; when absent it means `once`
132
+ * (backward compatible — a bare `{ type: 'approve' }` still type-checks and behaves as
133
+ * a single-shot approval that persists nothing).
134
+ */
135
+ export type ToolApprovalDecision = {
136
+ type: 'approve';
137
+ scope?: ToolApprovalScope;
138
+ } | {
139
+ type: 'reject';
140
+ message?: string;
141
+ };
142
+ /**
143
+ * Callback the {@link GthAgentRunner} invokes when a run suspends on a tool-approval
144
+ * interrupt, once per pending tool call. Returns the human's decision. When no handler is
145
+ * wired (e.g. a non-interactive run), the runner defaults to reject so a run can never
146
+ * silently hang or auto-approve.
147
+ */
148
+ export type ToolApprovalCallback = (pending: PendingToolInterrupt) => Promise<ToolApprovalDecision> | ToolApprovalDecision;
70
149
  export interface GthAgentInterface {
71
150
  init(command: GthCommand | undefined, configIn: GthConfig, checkpointSaver?: BaseCheckpointSaver | undefined): Promise<void>;
72
151
  invoke(messages: Message[], runConfig: RunnableConfig): Promise<string>;
@@ -80,6 +159,31 @@ export interface GthAgentInterface {
80
159
  streamWithEvents(messages: Message[], runConfig: RunnableConfig, signal?: AbortSignal): AsyncGenerator<AgentStreamEvent>;
81
160
  /** Resume a graph suspended via `interrupt()` with the supplied value. */
82
161
  streamWithEventsResume(resumeValue: unknown, runConfig: RunnableConfig, queuedMessages?: BaseMessage[], signal?: AbortSignal): AsyncGenerator<AgentStreamEvent>;
162
+ /**
163
+ * Resume a graph suspended on a human-in-the-loop `interrupt()` and stream the continuation
164
+ * as text (the string counterpart to {@link streamWithEventsResume}, for the readline path).
165
+ * Optional: only implemented by agents that support tool-approval interrupts.
166
+ */
167
+ streamResume?(resumeValue: unknown, runConfig: RunnableConfig): Promise<IterableReadableStream<string>>;
168
+ /**
169
+ * Inspect the checkpointed state for the thread and return any tool calls currently pending
170
+ * human approval (empty when the run completed normally). Optional: only implemented by
171
+ * agents whose graph exposes `getState`. Used by {@link GthAgentRunner} to drive the
172
+ * approve/reject confirmation loop.
173
+ */
174
+ getPendingToolInterrupts?(runConfig: RunnableConfig): Promise<PendingToolInterrupt[]>;
175
+ /**
176
+ * GS2-16 — reset the per-run analytics accumulator so the NEXT turn's token/tool totals start
177
+ * from zero. Called by {@link GthAgentRunner} at each turn boundary (the runner is reused across
178
+ * turns in interactive sessions). Optional: agents that don't collect stats simply omit it.
179
+ */
180
+ resetRunStats?(): void;
181
+ /**
182
+ * GS2-16 — the analytics harvested from the run(s) since the last {@link resetRunStats}. Used by
183
+ * the runner to thread token/tool data into the opt-in history recorder. Optional; when absent
184
+ * the runner records no analytics for that turn. Reading must never throw.
185
+ */
186
+ getRunStats?(): GthRunStats;
83
187
  cleanup?(): Promise<void>;
84
188
  }
85
189
  /**
@@ -93,9 +197,29 @@ export type ToolsResolver = (config: GthConfig, command?: GthCommand) => Promise
93
197
  export type ToolsCleanup = () => Promise<void>;
94
198
  export type MiddlewareResolver = (middleware: any[] | undefined, config: GthConfig) => Promise<any[]>;
95
199
  export type MiddlewareCleanup = () => Promise<void>;
200
+ /**
201
+ * EXT-32 — one connected MCP server's discovery `instructions` string (from its MCP `initialize`
202
+ * handshake), paired with the server name it came from. Captured once during tool resolution and
203
+ * reused: injected (fenced + per-server-labelled) into the composed system prompt, and available
204
+ * for [[TUI-C20]]'s MCP debug tab to render the same captured text. Only servers that actually
205
+ * supplied non-empty instructions appear here.
206
+ */
207
+ export interface McpServerInstruction {
208
+ /** The configured MCP server name (the key under `config.mcpServers`). */
209
+ server: string;
210
+ /** The server-provided instructions text (trimmed, non-empty). */
211
+ instructions: string;
212
+ }
96
213
  export interface AgentResolvers {
97
214
  resolveTools?: ToolsResolver;
98
215
  cleanupTools?: ToolsCleanup;
99
216
  resolveMiddleware?: MiddlewareResolver;
100
217
  cleanupMiddleware?: MiddlewareCleanup;
218
+ /**
219
+ * EXT-32 — the per-server MCP discovery instructions captured during the most recent
220
+ * {@link ToolsResolver} call (empty when no MCP servers are configured or none supplied
221
+ * instructions). Optional: resolvers without MCP support simply omit it, and the prompt
222
+ * composition treats an absent accessor as "no instructions" (no MCP section is emitted).
223
+ */
224
+ getMcpServerInstructions?(): McpServerInstruction[];
101
225
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAWA;;;;GAIG;AACH,MAAM,CAAN,IAAY,WAQX;AARD,WAAY,WAAW;IACrB,+CAAS,CAAA;IACT,6CAAQ,CAAA;IACR,mDAAW,CAAA;IACX,mDAAW,CAAA;IACX,mDAAW,CAAA;IACX,+CAAS,CAAA;IACT,iDAAU,CAAA;AACZ,CAAC,EARW,WAAW,KAAX,WAAW,QAQtB"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAYA;;;;GAIG;AACH,MAAM,CAAN,IAAY,WAQX;AARD,WAAY,WAAW;IACrB,+CAAS,CAAA;IACT,6CAAQ,CAAA;IACR,mDAAW,CAAA;IACX,mDAAW,CAAA;IACX,mDAAW,CAAA;IACX,+CAAS,CAAA;IACT,iDAAU,CAAA;AACZ,CAAC,EARW,WAAW,KAAX,WAAW,QAQtB"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * GS2-7 (B20) — pure, presentation-only formatters for the history surfaces.
4
+ *
5
+ * Shared by the `gth history` / `gth insights` CLI commands AND the `/history` `/search`
6
+ * `/insights` TUI slash commands so both render identically and both are unit-testable without a
7
+ * DB or a terminal. Every function is a pure `data -> string[]` transform (one display line per
8
+ * element); no I/O, no colour codes.
9
+ */
10
+ import type { ConversationSummary, HistoryInsights, SessionRecord, SessionSearchResult } from '#src/history/historyStore.js';
11
+ /**
12
+ * Render FTS search hits: a header line per hit plus its snippet (or a prompt preview when the
13
+ * snippet is empty). Returns a friendly single line when there are none.
14
+ */
15
+ export declare function formatSearchResults(results: SessionSearchResult[]): string[];
16
+ /**
17
+ * GS2-19 — render a conversation-grained listing: one header + last-turn preview per conversation.
18
+ * The header carries the count / timespan / last message that make the conversation the top-level
19
+ * unit (`gth history list`), replacing the old flat per-turn list.
20
+ */
21
+ export declare function formatConversationList(conversations: ConversationSummary[]): string[];
22
+ /**
23
+ * GS2-19 — render one conversation's full thread (all turns in order) for `gth history show <id>`.
24
+ * Each turn shows its prompt and response preview so a search hit can be expanded into context.
25
+ */
26
+ export declare function formatConversationThread(turns: SessionRecord[]): string[];
27
+ /** Render the analytics summary: totals, top tools, per-command breakdown. */
28
+ export declare function formatInsightsSummary(insights: HistoryInsights): string[];
@@ -0,0 +1,127 @@
1
+ /** Collapse whitespace and clip to `max` chars with an ellipsis, for one-line previews. */
2
+ function oneLine(text, max = 80) {
3
+ const s = (text ?? '').replace(/\s+/g, ' ').trim();
4
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
5
+ }
6
+ /** A compact `#id <ts> [command] model (conversation #cid)` header line for one turn. */
7
+ function headerLine(r) {
8
+ const parts = [`#${r.id}`, r.ts];
9
+ if (r.command)
10
+ parts.push(`[${r.command}]`);
11
+ if (r.model)
12
+ parts.push(r.model);
13
+ // GS2-19: a search hit resolves to the conversation it belongs to, so the reader can pull up the
14
+ // whole thread (`gth history show <cid>`). Older rows migrated from GS2-7 always have one now.
15
+ if (r.conversationId != null)
16
+ parts.push(`(conversation #${r.conversationId})`);
17
+ return parts.join(' ');
18
+ }
19
+ /**
20
+ * Render FTS search hits: a header line per hit plus its snippet (or a prompt preview when the
21
+ * snippet is empty). Returns a friendly single line when there are none.
22
+ */
23
+ export function formatSearchResults(results) {
24
+ if (results.length === 0)
25
+ return ['No matching sessions found.'];
26
+ const lines = [];
27
+ for (const r of results) {
28
+ lines.push(headerLine(r));
29
+ const detail = r.snippet && r.snippet.trim().length > 0 ? r.snippet : r.prompt;
30
+ const preview = oneLine(detail, 100);
31
+ if (preview)
32
+ lines.push(` ${preview}`);
33
+ }
34
+ return lines;
35
+ }
36
+ /**
37
+ * GS2-19 — render a conversation-grained listing: one header + last-turn preview per conversation.
38
+ * The header carries the count / timespan / last message that make the conversation the top-level
39
+ * unit (`gth history list`), replacing the old flat per-turn list.
40
+ */
41
+ export function formatConversationList(conversations) {
42
+ if (conversations.length === 0) {
43
+ return [
44
+ 'No conversations recorded yet. Enable history with `history.enabled: true` in your config.',
45
+ ];
46
+ }
47
+ const lines = [];
48
+ for (const c of conversations) {
49
+ const parts = [`#${c.id}`];
50
+ // Timespan across the conversation's turns; a 1-turn (or not-yet-started) conversation collapses
51
+ // to a single instant, so show one timestamp rather than an `a → a` range.
52
+ if (c.firstTs && c.lastTs && c.firstTs !== c.lastTs) {
53
+ parts.push(`${c.firstTs} → ${c.lastTs}`);
54
+ }
55
+ else {
56
+ parts.push(c.lastTs ?? c.firstTs ?? c.startedTs);
57
+ }
58
+ if (c.command)
59
+ parts.push(`[${c.command}]`);
60
+ if (c.model)
61
+ parts.push(c.model);
62
+ parts.push(`(${c.turnCount} ${c.turnCount === 1 ? 'turn' : 'turns'})`);
63
+ lines.push(parts.join(' '));
64
+ const preview = oneLine(c.lastPrompt, 100);
65
+ if (preview)
66
+ lines.push(` ${preview}`);
67
+ }
68
+ return lines;
69
+ }
70
+ /**
71
+ * GS2-19 — render one conversation's full thread (all turns in order) for `gth history show <id>`.
72
+ * Each turn shows its prompt and response preview so a search hit can be expanded into context.
73
+ */
74
+ export function formatConversationThread(turns) {
75
+ if (turns.length === 0)
76
+ return ['No turns found for that conversation.'];
77
+ const lines = [];
78
+ turns.forEach((t, i) => {
79
+ const header = t.ts ? `Turn ${i + 1} ${t.ts}` : `Turn ${i + 1}`;
80
+ lines.push(header);
81
+ const prompt = oneLine(t.prompt, 200);
82
+ if (prompt)
83
+ lines.push(` > ${prompt}`);
84
+ const response = oneLine(t.response, 200);
85
+ if (response)
86
+ lines.push(` ${response}`);
87
+ });
88
+ return lines;
89
+ }
90
+ /** Render the analytics summary: totals, top tools, per-command breakdown. */
91
+ export function formatInsightsSummary(insights) {
92
+ if (insights.sessionCount === 0) {
93
+ return [
94
+ 'No sessions recorded yet. Enable history with `history.enabled: true` in your config.',
95
+ ];
96
+ }
97
+ const lines = [];
98
+ lines.push(`Sessions: ${insights.sessionCount}`);
99
+ if (insights.firstTs && insights.lastTs) {
100
+ lines.push(`Span: ${insights.firstTs} → ${insights.lastTs}`);
101
+ }
102
+ // GS2-16: only surface the token/cost/top-tool lines when there is real data behind them.
103
+ // Older records (and providers that report no usage) leave these zero/empty; printing
104
+ // `Tokens: 0` / `$0.0000` / `(none recorded)` reads as "the run used nothing", which is
105
+ // misleading, so omit the line entirely instead. Sessions / Span / By-command always show.
106
+ if (insights.totalTokens > 0) {
107
+ lines.push(`Tokens: ${insights.totalTokens} total ` +
108
+ `(${insights.totalTokensInput} in / ${insights.totalTokensOutput} out)`);
109
+ }
110
+ // Cost is only ever recorded when a reliable price was available (the recorder never invents
111
+ // one), so a positive total is the signal that a cost line is meaningful.
112
+ if (insights.totalCostUsd > 0) {
113
+ lines.push(`Estimated cost: $${insights.totalCostUsd.toFixed(4)}`);
114
+ }
115
+ if (insights.perCommand.length > 0) {
116
+ lines.push('By command:');
117
+ for (const c of insights.perCommand)
118
+ lines.push(` ${c.command}: ${c.count}`);
119
+ }
120
+ if (insights.topTools.length > 0) {
121
+ lines.push('Top tools:');
122
+ for (const t of insights.topTools)
123
+ lines.push(` ${t.tool}: ${t.count}`);
124
+ }
125
+ return lines;
126
+ }
127
+ //# sourceMappingURL=historyFormat.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"historyFormat.js","sourceRoot":"","sources":["../../src/history/historyFormat.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,198 @@
1
+ /** Filename of the global history DB inside `~/.gsloth`. */
2
+ export declare const HISTORY_DB_FILENAME = "history.db";
3
+ /** A single persisted session record (all analytics fields optional; populated when available). */
4
+ export interface SessionRecord {
5
+ /**
6
+ * GS2-19 — the parent conversation this turn belongs to. When omitted, {@link HistoryStore.record}
7
+ * opens a fresh single-turn conversation for the row (so a bare single-shot run = a 1-turn
8
+ * conversation). Interactive sessions {@link HistoryStore.openConversation | open} one conversation
9
+ * up-front and pass its id here on every turn, grouping the whole chat under one conversation.
10
+ */
11
+ conversationId?: number;
12
+ /** ISO-8601 timestamp; defaults to now when omitted. */
13
+ ts?: string;
14
+ /** Project / working directory the run happened in. */
15
+ project?: string;
16
+ /** Originating command (ask/chat/code/exec/…). */
17
+ command?: string;
18
+ /** Human-readable model/provider label. */
19
+ model?: string;
20
+ /** The user prompt / source that started the run (full-text indexed). */
21
+ prompt?: string;
22
+ /** The final assistant response text (full-text indexed). */
23
+ response?: string;
24
+ /** Prompt/input token count, when known. */
25
+ tokensInput?: number;
26
+ /** Completion/output token count, when known. */
27
+ tokensOutput?: number;
28
+ /** Estimated cost in USD, when known. */
29
+ costUsd?: number;
30
+ /** Names of tools invoked during the run, when known. */
31
+ tools?: string[];
32
+ /** Wall-clock duration of the run in milliseconds, when known. */
33
+ durationMs?: number;
34
+ }
35
+ /** A search hit: the stored record plus its id and a highlighted snippet. */
36
+ export interface SessionSearchResult extends SessionRecord {
37
+ id: number;
38
+ ts: string;
39
+ /** FTS5 snippet around the match (may be empty). */
40
+ snippet: string;
41
+ }
42
+ /**
43
+ * GS2-19 — metadata for a conversation (a group of turns). Passed to
44
+ * {@link HistoryStore.openConversation} at interactive-session start.
45
+ */
46
+ export interface ConversationMeta {
47
+ /** ISO-8601 start timestamp; defaults to now when omitted. */
48
+ ts?: string;
49
+ /** Project / working directory the conversation happened in. */
50
+ project?: string;
51
+ /** Originating command (chat/code/ask/exec/…). */
52
+ command?: string;
53
+ /** Human-readable model/provider label. */
54
+ model?: string;
55
+ }
56
+ /**
57
+ * GS2-19 — a conversation-grained listing row: the conversation's metadata plus the aggregate of
58
+ * its turns (how many, the timespan they cover, and a preview of the last one). This is the
59
+ * top-level unit `gth history list` shows, in place of isolated per-turn rows.
60
+ */
61
+ export interface ConversationSummary {
62
+ id: number;
63
+ /** When the conversation was opened. */
64
+ startedTs: string;
65
+ project?: string;
66
+ command?: string;
67
+ model?: string;
68
+ /** Number of turns recorded under this conversation (0 for a conversation with no turns). */
69
+ turnCount: number;
70
+ /** Timestamp of the first / last turn, when any turns exist. */
71
+ firstTs?: string;
72
+ lastTs?: string;
73
+ /** Prompt / response of the most recent turn (a one-line preview source). */
74
+ lastPrompt?: string;
75
+ lastResponse?: string;
76
+ }
77
+ /** Aggregate analytics over the whole store (local only). */
78
+ export interface HistoryInsights {
79
+ sessionCount: number;
80
+ totalTokensInput: number;
81
+ totalTokensOutput: number;
82
+ totalTokens: number;
83
+ totalCostUsd: number;
84
+ /** Tool-name → invocation count, most-used first. */
85
+ topTools: {
86
+ tool: string;
87
+ count: number;
88
+ }[];
89
+ /** Command → run count, most-used first. */
90
+ perCommand: {
91
+ command: string;
92
+ count: number;
93
+ }[];
94
+ firstTs?: string;
95
+ lastTs?: string;
96
+ }
97
+ /** Options for opening a store. */
98
+ export interface OpenHistoryStoreOptions {
99
+ /**
100
+ * When false (the default for read-only callers), a missing DB file yields `null` instead of
101
+ * creating an empty database. Read commands pass `create: false` so `gth insights` never
102
+ * materialises a DB as a side effect; the recorder passes `create: true`.
103
+ */
104
+ create?: boolean;
105
+ }
106
+ /**
107
+ * Turn arbitrary user text into a safe FTS5 MATCH expression: each whitespace-separated token is
108
+ * wrapped as a quoted string and AND-ed together. This avoids FTS5 syntax errors from stray
109
+ * operators (`AND`, `*`, `:`, parentheses, unbalanced quotes) in a user's query while still
110
+ * matching all of their words.
111
+ */
112
+ export declare function toFtsMatchQuery(query: string): string;
113
+ /**
114
+ * A thin, fail-soft wrapper over a `node:sqlite` connection holding the session history.
115
+ *
116
+ * Obtain one via {@link openHistoryStore} (which returns `null` if the DB can't be opened). Every
117
+ * method is defensive: on any SQLite error it returns a safe empty/zero result rather than
118
+ * throwing, so callers on a run's hot path never have to guard.
119
+ */
120
+ export declare class HistoryStore {
121
+ private db;
122
+ private constructor();
123
+ /**
124
+ * Open (and lazily initialise) the store at `dbPath`. Returns `null` on any failure — a missing
125
+ * file when `create` is false, an unopenable/locked/corrupt DB, or a schema-init error — so the
126
+ * caller can simply skip history without a try/catch.
127
+ */
128
+ static open(dbPath: string, options?: OpenHistoryStoreOptions): HistoryStore | null;
129
+ private initSchema;
130
+ /**
131
+ * GS2-19 — in-place, idempotent upgrade of a pre-existing GS2-7 DB (flat `sessions` rows, no
132
+ * grouping) to the conversation-grained model. Runs on every open (including read-only `list` /
133
+ * `search` opens, which open the file read-write): it adds the `conversation_id` column if an
134
+ * older `sessions` table lacks it, then back-fills each ungrouped turn into its own 1-turn
135
+ * conversation. After the first pass there are no ungrouped rows, so it is a cheap no-op.
136
+ *
137
+ * Fully fail-soft: a migration hiccup is swallowed here rather than nulling the store, so reads
138
+ * still work against whatever is already there and new turns just fall back to fresh 1-turn
139
+ * conversations. Never throws.
140
+ */
141
+ private migrate;
142
+ /**
143
+ * GS2-19 — open a new conversation and return its id (or `null` on any error). Interactive
144
+ * sessions call this once at start, then pass the id on every {@link record} so all the session's
145
+ * turns group under it. A single-shot run does not need this: {@link record} opens a 1-turn
146
+ * conversation itself when no `conversationId` is supplied.
147
+ */
148
+ openConversation(meta?: ConversationMeta): number | null;
149
+ /**
150
+ * Persist one session and its full-text index entry. Returns the new row id, or `null` on any
151
+ * error (the run continues regardless). The two inserts run in a transaction so a failure can't
152
+ * leave the FTS index out of sync with the base table.
153
+ */
154
+ record(rec: SessionRecord): number | null;
155
+ /**
156
+ * Full-text search over prompt/response/command/project, best match first (FTS5 `rank`). User
157
+ * text is sanitised via {@link toFtsMatchQuery}; an empty or all-punctuation query returns `[]`.
158
+ * Any SQLite error yields `[]` (fail-soft).
159
+ */
160
+ search(query: string, limit?: number): SessionSearchResult[];
161
+ /** Most recent sessions, newest first. Fail-soft ([] on error). */
162
+ listRecent(limit?: number): SessionSearchResult[];
163
+ /**
164
+ * GS2-19 — conversations newest-first, each with its turn count, timespan, and a preview of the
165
+ * last turn. This is the top-level unit for `gth history list` (the turn-grained {@link listRecent}
166
+ * remains for callers that want raw turns). Fail-soft ([] on error).
167
+ *
168
+ * Empty conversations are **excluded** (`HAVING COUNT(s.id) > 0`): a session opens its conversation
169
+ * at start (before any turn), so one that exits with zero turns would otherwise show as a
170
+ * contentless `turnCount: 0` row. The LEFT JOIN still keeps every conversation that has ≥1 turn —
171
+ * including back-filled 1-turn conversations, whose single turn satisfies the HAVING.
172
+ */
173
+ listConversations(limit?: number): ConversationSummary[];
174
+ /**
175
+ * GS2-19 — all turns of one conversation in chronological (insert) order, so a search hit can be
176
+ * expanded into the whole thread it belonged to. Fail-soft ([] on error / unknown id).
177
+ */
178
+ getConversationThread(conversationId: number): SessionRecord[];
179
+ /**
180
+ * Aggregate token/cost totals, a top-tool tally, and a per-command breakdown over the whole
181
+ * store. Tool tallying reads each row's JSON `tools` array in JS (robust to nulls). Fail-soft:
182
+ * returns a zeroed summary on any error.
183
+ */
184
+ insights(topN?: number): HistoryInsights;
185
+ /** Close the underlying connection (fail-soft). */
186
+ close(): void;
187
+ }
188
+ /**
189
+ * Resolve the on-disk path of the history DB. Honors an explicit `dbPath` (from `history.dbPath`
190
+ * or a `--db` flag); otherwise the global `~/.gsloth/history.db`. When `dbPath` is omitted and
191
+ * `ensureDir` is true, the global dir is created so the recorder can write.
192
+ */
193
+ export declare function resolveHistoryDbPath(dbPath?: string, ensureDir?: boolean): string;
194
+ /**
195
+ * Fail-soft open of the history store. Returns `null` (never throws) when the DB can't be opened
196
+ * or, for read-only callers (`create: false`, the default), when the file does not yet exist.
197
+ */
198
+ export declare function openHistoryStore(dbPath: string, options?: OpenHistoryStoreOptions): HistoryStore | null;