opencode-engram 0.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.
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/README.zh.md +257 -0
- package/README.zht.md +255 -0
- package/package.json +48 -0
- package/src/common/charting.ts +107 -0
- package/src/common/common.ts +74 -0
- package/src/common/config.ts +963 -0
- package/src/common/history-prompt.ts +72 -0
- package/src/common/plugin.ts +593 -0
- package/src/common/upstream-navigator-prompt.ts +131 -0
- package/src/core/index.ts +33 -0
- package/src/core/sdk-bridge.ts +73 -0
- package/src/core/session.ts +196 -0
- package/src/core/turn-index.ts +219 -0
- package/src/domain/adapter.ts +386 -0
- package/src/domain/clip-text.ts +86 -0
- package/src/domain/domain.ts +618 -0
- package/src/domain/preview.ts +132 -0
- package/src/domain/serialize.ts +409 -0
- package/src/domain/types.ts +321 -0
- package/src/runtime/charting.ts +73 -0
- package/src/runtime/debug.ts +155 -0
- package/src/runtime/logger.ts +34 -0
- package/src/runtime/message-io.ts +224 -0
- package/src/runtime/runtime.ts +1033 -0
- package/src/runtime/search.ts +1280 -0
- package/src/runtime/turn-resolve.ts +111 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts - Shared domain types and output contracts
|
|
3
|
+
*
|
|
4
|
+
* This module defines all domain model types for the upstream history refactor.
|
|
5
|
+
* It serves as the stable boundary between adapter, domain, and serialize layers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// Basic Types
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
export type MessageRole = "user" | "assistant";
|
|
13
|
+
|
|
14
|
+
export type ToolStatus = "pending" | "running" | "completed" | "error";
|
|
15
|
+
|
|
16
|
+
export type ToolOutcome = "completed" | "recovered" | "error" | "running";
|
|
17
|
+
|
|
18
|
+
export type ReadablePartType = "text" | "reasoning" | "tool";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// Normalized Message (Adapter Output -> Domain Input)
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
export interface NormalizedMessage {
|
|
25
|
+
id: string;
|
|
26
|
+
role: MessageRole;
|
|
27
|
+
time: number | undefined;
|
|
28
|
+
summary: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Tool Call Summary
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Aggregated statistics for a single tool name within a message.
|
|
37
|
+
*/
|
|
38
|
+
export interface ToolCallSummary {
|
|
39
|
+
tool: string;
|
|
40
|
+
total: number;
|
|
41
|
+
errors: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// =============================================================================
|
|
45
|
+
// Message Metadata
|
|
46
|
+
// =============================================================================
|
|
47
|
+
|
|
48
|
+
export interface MessageMetaBase {
|
|
49
|
+
id: string;
|
|
50
|
+
role: MessageRole;
|
|
51
|
+
turn: number;
|
|
52
|
+
time: number | undefined;
|
|
53
|
+
notes: string[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UserMessageMeta extends MessageMetaBase {
|
|
57
|
+
role: "user";
|
|
58
|
+
attachments: string[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface AssistantMessageMeta extends MessageMetaBase {
|
|
62
|
+
role: "assistant";
|
|
63
|
+
toolCalls: ToolCallSummary[];
|
|
64
|
+
toolOutcome?: ToolOutcome;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type AnyMessageMeta = UserMessageMeta | AssistantMessageMeta;
|
|
68
|
+
|
|
69
|
+
// =============================================================================
|
|
70
|
+
// Section Types (Domain Model)
|
|
71
|
+
// =============================================================================
|
|
72
|
+
|
|
73
|
+
export interface TextSection {
|
|
74
|
+
type: "text";
|
|
75
|
+
partId: string;
|
|
76
|
+
content: string;
|
|
77
|
+
truncated: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ReasoningSection {
|
|
81
|
+
type: "reasoning";
|
|
82
|
+
partId: string;
|
|
83
|
+
content: string;
|
|
84
|
+
truncated: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ToolSection {
|
|
88
|
+
type: "tool";
|
|
89
|
+
partId: string;
|
|
90
|
+
tool: string;
|
|
91
|
+
title: string | null;
|
|
92
|
+
status: ToolStatus;
|
|
93
|
+
input?: Record<string, unknown>;
|
|
94
|
+
content?: string;
|
|
95
|
+
truncated: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ImageSection {
|
|
99
|
+
type: "image";
|
|
100
|
+
partId: string;
|
|
101
|
+
mime: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface FileSection {
|
|
105
|
+
type: "file";
|
|
106
|
+
partId: string;
|
|
107
|
+
path: string;
|
|
108
|
+
mime: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type Section =
|
|
112
|
+
| TextSection
|
|
113
|
+
| ReasoningSection
|
|
114
|
+
| ToolSection
|
|
115
|
+
| ImageSection
|
|
116
|
+
| FileSection;
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// Normalized Part (Adapter Output -> Domain Input)
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
export type NormalizedTextPart = {
|
|
123
|
+
type: "text";
|
|
124
|
+
partId: string;
|
|
125
|
+
messageId: string;
|
|
126
|
+
text: string;
|
|
127
|
+
ignored: boolean;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type NormalizedReasoningPart = {
|
|
131
|
+
type: "reasoning";
|
|
132
|
+
partId: string;
|
|
133
|
+
messageId: string;
|
|
134
|
+
text: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type NormalizedToolPart = {
|
|
138
|
+
type: "tool";
|
|
139
|
+
partId: string;
|
|
140
|
+
messageId: string;
|
|
141
|
+
tool: string;
|
|
142
|
+
title: string | null;
|
|
143
|
+
status: ToolStatus;
|
|
144
|
+
input: Record<string, unknown>;
|
|
145
|
+
content?: string;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export type NormalizedImagePart = {
|
|
149
|
+
type: "image";
|
|
150
|
+
partId: string;
|
|
151
|
+
messageId: string;
|
|
152
|
+
mime: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type NormalizedFilePart = {
|
|
156
|
+
type: "file";
|
|
157
|
+
partId: string;
|
|
158
|
+
messageId: string;
|
|
159
|
+
path: string;
|
|
160
|
+
mime: string;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type NormalizedPart =
|
|
164
|
+
| NormalizedTextPart
|
|
165
|
+
| NormalizedReasoningPart
|
|
166
|
+
| NormalizedToolPart
|
|
167
|
+
| NormalizedImagePart
|
|
168
|
+
| NormalizedFilePart;
|
|
169
|
+
|
|
170
|
+
// =============================================================================
|
|
171
|
+
// Section Conversion Context
|
|
172
|
+
// =============================================================================
|
|
173
|
+
|
|
174
|
+
export interface SectionConvertContext {
|
|
175
|
+
maxTextLength: number;
|
|
176
|
+
maxReasoningLength: number;
|
|
177
|
+
maxToolOutputLength: number;
|
|
178
|
+
maxToolInputLength: number;
|
|
179
|
+
visibleToolInputs: ReadonlySet<string>;
|
|
180
|
+
visibleToolOutputs: ReadonlySet<string>;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface PreviewFallbackHints {
|
|
184
|
+
hasCompaction: boolean;
|
|
185
|
+
hasSubtask: boolean;
|
|
186
|
+
hasUnsupported: boolean;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Serialized Output Types (Final JSON Contracts)
|
|
191
|
+
// =============================================================================
|
|
192
|
+
|
|
193
|
+
export interface BrowseUserItemOutput {
|
|
194
|
+
role: "user";
|
|
195
|
+
turn_index: number;
|
|
196
|
+
message_id: string;
|
|
197
|
+
preview?: string;
|
|
198
|
+
attachment?: string[];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface BrowseAssistantItemOutput {
|
|
202
|
+
role: "assistant";
|
|
203
|
+
turn_index: number;
|
|
204
|
+
message_id: string;
|
|
205
|
+
preview?: string;
|
|
206
|
+
tool?: ToolBlockOutput;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export type BrowseItemOutput =
|
|
210
|
+
| BrowseUserItemOutput
|
|
211
|
+
| BrowseAssistantItemOutput;
|
|
212
|
+
|
|
213
|
+
export interface OverviewUserOutput {
|
|
214
|
+
message_id: string;
|
|
215
|
+
preview: string | null;
|
|
216
|
+
attachment?: string[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Tool activity summary block for assistant outputs. */
|
|
220
|
+
export interface ToolBlockOutput {
|
|
221
|
+
calls: string[];
|
|
222
|
+
outcome: ToolOutcome;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface OverviewAssistantOutput {
|
|
226
|
+
total_messages: number;
|
|
227
|
+
preview: string | null;
|
|
228
|
+
modified?: string[];
|
|
229
|
+
tool?: ToolBlockOutput;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Turn summary for overview output.
|
|
234
|
+
*/
|
|
235
|
+
export interface OverviewTurnOutput {
|
|
236
|
+
turn_index: number;
|
|
237
|
+
user: OverviewUserOutput | null;
|
|
238
|
+
assistant: OverviewAssistantOutput | null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface OverviewOutput {
|
|
242
|
+
turns: OverviewTurnOutput[];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface BrowseOutput {
|
|
246
|
+
before_message_id?: string | null;
|
|
247
|
+
messages: BrowseItemOutput[];
|
|
248
|
+
after_message_id?: string | null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export type SectionOutput =
|
|
252
|
+
| { type: "text"; part_id?: string; content: string | null }
|
|
253
|
+
| { type: "reasoning"; part_id?: string; content: string | null }
|
|
254
|
+
| {
|
|
255
|
+
type: "tool";
|
|
256
|
+
part_id?: string;
|
|
257
|
+
tool: string;
|
|
258
|
+
status: ToolStatus;
|
|
259
|
+
input?: Record<string, unknown>;
|
|
260
|
+
content?: string | null;
|
|
261
|
+
}
|
|
262
|
+
| { type: "image"; mime: string }
|
|
263
|
+
| { type: "file"; path: string; mime: string };
|
|
264
|
+
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// Search Output Types (Phase 1: Contracts Only)
|
|
267
|
+
// =============================================================================
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Searchable part type.
|
|
271
|
+
*
|
|
272
|
+
* - "text": user or assistant text content
|
|
273
|
+
* - "reasoning": assistant reasoning content
|
|
274
|
+
* - "tool": tool call output content
|
|
275
|
+
*/
|
|
276
|
+
export type SearchPartType = "text" | "reasoning" | "tool";
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* A single search hit within a message.
|
|
280
|
+
*
|
|
281
|
+
* - type: part type that matched
|
|
282
|
+
* - part_id: identifier for reading full content
|
|
283
|
+
* - tool_name: present only for tool hits (optional)
|
|
284
|
+
* - snippets: ordered array of highest-priority text contexts around matches
|
|
285
|
+
*/
|
|
286
|
+
export interface SearchHitOutput {
|
|
287
|
+
type: SearchPartType;
|
|
288
|
+
part_id: string;
|
|
289
|
+
tool_name?: string;
|
|
290
|
+
snippets: string[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Search result grouped by message.
|
|
295
|
+
*
|
|
296
|
+
* - role: message role
|
|
297
|
+
* - turn_index: message turn number
|
|
298
|
+
* - message_id: identifier for reading full message
|
|
299
|
+
* - hits: array of hits within this message
|
|
300
|
+
* - remain_hits: omitted when 0; number of omitted low-priority hits
|
|
301
|
+
*/
|
|
302
|
+
export interface SearchMessageOutput {
|
|
303
|
+
role: MessageRole;
|
|
304
|
+
turn_index: number;
|
|
305
|
+
message_id: string;
|
|
306
|
+
hits: SearchHitOutput[];
|
|
307
|
+
remain_hits?: number;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Full search response output.
|
|
312
|
+
*
|
|
313
|
+
* - messages: results grouped by message (omitted when no message has hits)
|
|
314
|
+
*/
|
|
315
|
+
export type SearchNoHitsOutput = Record<string, never>;
|
|
316
|
+
|
|
317
|
+
export interface SearchHitsOutput {
|
|
318
|
+
messages: SearchMessageOutput[];
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export type SearchOutput = SearchNoHitsOutput | SearchHitsOutput;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { PluginInput } from "../common/common.ts";
|
|
2
|
+
import type { EngramConfig } from "../common/config.ts";
|
|
3
|
+
import type { BrowseOutput, OverviewOutput } from "../domain/types.ts";
|
|
4
|
+
|
|
5
|
+
import { createBrowseContext, resolveSessionTarget } from "../core/index.ts";
|
|
6
|
+
import { log } from "./logger.ts";
|
|
7
|
+
import { browseData, loadOverviewState } from "./runtime.ts";
|
|
8
|
+
|
|
9
|
+
export interface ChartingData {
|
|
10
|
+
overview: OverviewOutput;
|
|
11
|
+
latestTurnDetail: BrowseOutput;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Output Assembly Helpers
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
function emptyLatestTurnDetail(): BrowseOutput {
|
|
19
|
+
return {
|
|
20
|
+
before_message_id: null,
|
|
21
|
+
messages: [],
|
|
22
|
+
after_message_id: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Charting Data Loading
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load the structured data used to assemble the chart block for the current session.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadChartingData(
|
|
34
|
+
input: PluginInput,
|
|
35
|
+
sessionID: string,
|
|
36
|
+
config: EngramConfig,
|
|
37
|
+
): Promise<ChartingData> {
|
|
38
|
+
const journal = log(input.client, sessionID);
|
|
39
|
+
const target = await resolveSessionTarget(input.client, sessionID, input.directory);
|
|
40
|
+
const overviewBrowse = createBrowseContext(target, true);
|
|
41
|
+
const overviewState = await loadOverviewState(input, overviewBrowse, config, journal);
|
|
42
|
+
const latestTurn = overviewState.turns.at(-1);
|
|
43
|
+
const minTurn = latestTurn
|
|
44
|
+
? latestTurn.turn - config.context_charting.recent_turns
|
|
45
|
+
: Number.POSITIVE_INFINITY;
|
|
46
|
+
const overview: OverviewOutput = {
|
|
47
|
+
turns: overviewState.turns
|
|
48
|
+
.filter((turn) => turn.turn >= minTurn)
|
|
49
|
+
.map((turn) => turn.output),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!latestTurn) {
|
|
53
|
+
return {
|
|
54
|
+
overview,
|
|
55
|
+
latestTurnDetail: emptyLatestTurnDetail(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
overview,
|
|
61
|
+
latestTurnDetail: await browseData(
|
|
62
|
+
input,
|
|
63
|
+
createBrowseContext(target, false),
|
|
64
|
+
config,
|
|
65
|
+
journal,
|
|
66
|
+
{
|
|
67
|
+
messageID: latestTurn.lastVisibleMessageId,
|
|
68
|
+
numBefore: config.context_charting.recent_messages,
|
|
69
|
+
numAfter: 0,
|
|
70
|
+
},
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
|
|
6
|
+
import { json } from "../common/common.ts";
|
|
7
|
+
import type { ResolvedDebugModeConfig } from "../common/config.ts";
|
|
8
|
+
|
|
9
|
+
export const transientSearchErrorMessage =
|
|
10
|
+
"Failed to query messages. Try again or use history_browse_turns to lookup instead.";
|
|
11
|
+
|
|
12
|
+
export type ToolCallRecord = {
|
|
13
|
+
tool: string;
|
|
14
|
+
sessionID: string;
|
|
15
|
+
messageID: string;
|
|
16
|
+
targetSessionID?: string;
|
|
17
|
+
args: Record<string, unknown>;
|
|
18
|
+
output?: unknown;
|
|
19
|
+
error?: string;
|
|
20
|
+
time: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if tool call logging is enabled.
|
|
25
|
+
*
|
|
26
|
+
* Respects `enable` as the highest priority:
|
|
27
|
+
* - If `enable` is false, logging is disabled regardless of other settings.
|
|
28
|
+
* - Otherwise, returns the value of `log_tool_calls`.
|
|
29
|
+
*/
|
|
30
|
+
export function isToolCallLoggingEnabled(debug: ResolvedDebugModeConfig): boolean {
|
|
31
|
+
if (!debug.enable) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return debug.log_tool_calls;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if any debug feature is enabled that requires directory setup.
|
|
39
|
+
*
|
|
40
|
+
* This checks if any feature that writes to `.engram/` is active,
|
|
41
|
+
* respecting `enable` as the highest priority.
|
|
42
|
+
*/
|
|
43
|
+
export function isDebugDirectoryNeeded(debug: ResolvedDebugModeConfig): boolean {
|
|
44
|
+
if (!debug.enable) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return debug.log_tool_calls;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function estimateSerializedTokens(value: unknown): number {
|
|
51
|
+
const bytes = Buffer.byteLength(json(value), "utf8");
|
|
52
|
+
return Math.max(1, Math.ceil(bytes / 3));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function estimateCallDurationMs(startedAt: number): number {
|
|
56
|
+
return Math.max(0, Math.round(performance.now() - startedAt));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getLoggedResponsePayload(record: ToolCallRecord): unknown {
|
|
60
|
+
if (record.output !== undefined) {
|
|
61
|
+
return record.output;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (record.error !== undefined) {
|
|
65
|
+
return { error: record.error };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatDebugTimestamp(date = new Date()) {
|
|
72
|
+
const year = String(date.getUTCFullYear() % 100).padStart(2, "0");
|
|
73
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
74
|
+
const day = String(date.getUTCDate()).padStart(2, "0");
|
|
75
|
+
const hour = String(date.getUTCHours()).padStart(2, "0");
|
|
76
|
+
const minute = String(date.getUTCMinutes()).padStart(2, "0");
|
|
77
|
+
const second = String(date.getUTCSeconds()).padStart(2, "0");
|
|
78
|
+
const millisecond = String(date.getUTCMilliseconds()).padStart(3, "0");
|
|
79
|
+
return `${year}${month}${day}_${hour}${minute}${second}${millisecond}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function debugFileName(tool: string) {
|
|
83
|
+
return `${formatDebugTimestamp()}-${tool}-${randomUUID().slice(0, 8)}.json`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getToolCallLogDirectory(projectRoot: string, sessionID: string) {
|
|
87
|
+
return join(projectRoot, ".engram", "log_tool_calls", sessionID);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function recordToolCall(
|
|
91
|
+
record: ToolCallRecord,
|
|
92
|
+
enabled: boolean,
|
|
93
|
+
durationMs: number,
|
|
94
|
+
projectRoot: string,
|
|
95
|
+
) {
|
|
96
|
+
if (!enabled) return;
|
|
97
|
+
|
|
98
|
+
const dir = getToolCallLogDirectory(projectRoot, record.sessionID);
|
|
99
|
+
const filePath = join(dir, debugFileName(record.tool));
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const loggedRecord = {
|
|
103
|
+
...record,
|
|
104
|
+
estimated_tokens: estimateSerializedTokens(getLoggedResponsePayload(record)),
|
|
105
|
+
duration_ms: durationMs,
|
|
106
|
+
};
|
|
107
|
+
await mkdir(dir, { recursive: true });
|
|
108
|
+
await writeFile(filePath, JSON.stringify(loggedRecord, null, 2), "utf8");
|
|
109
|
+
} catch {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function ensureDebugGitIgnore(projectRoot: string) {
|
|
115
|
+
const engramDir = join(projectRoot, ".engram");
|
|
116
|
+
const gitIgnorePath = join(engramDir, ".gitignore");
|
|
117
|
+
const requiredEntries = ["log_tool_calls", ".gitignore"];
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
await mkdir(engramDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
let existing = "";
|
|
123
|
+
try {
|
|
124
|
+
existing = await readFile(gitIgnorePath, "utf8");
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (
|
|
127
|
+
!(
|
|
128
|
+
error &&
|
|
129
|
+
typeof error === "object" &&
|
|
130
|
+
"code" in error &&
|
|
131
|
+
error.code === "ENOENT"
|
|
132
|
+
)
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const existingLines = existing
|
|
139
|
+
.replace(/\r\n/g, "\n")
|
|
140
|
+
.split("\n")
|
|
141
|
+
.map((line) => line.trim())
|
|
142
|
+
.filter((line) => line.length > 0);
|
|
143
|
+
const lineSet = new Set(existingLines);
|
|
144
|
+
const missing = requiredEntries.filter((entry) => !lineSet.has(entry));
|
|
145
|
+
|
|
146
|
+
if (missing.length === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
151
|
+
await appendFile(gitIgnorePath, `${prefix}${missing.join("\n")}\n`, "utf8");
|
|
152
|
+
} catch {
|
|
153
|
+
// Ignore errors; best effort
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
|
+
|
|
3
|
+
export type Logger = ReturnType<typeof log>;
|
|
4
|
+
|
|
5
|
+
export function log(client: Parameters<Plugin>[0]["client"], sessionID: string) {
|
|
6
|
+
const write = (
|
|
7
|
+
level: "debug" | "info" | "warn" | "error",
|
|
8
|
+
message: string,
|
|
9
|
+
extra?: Record<string, unknown>,
|
|
10
|
+
) =>
|
|
11
|
+
client.app
|
|
12
|
+
.log({
|
|
13
|
+
body: {
|
|
14
|
+
service: "engram-plugin",
|
|
15
|
+
level,
|
|
16
|
+
message,
|
|
17
|
+
extra: { sessionID, ...extra },
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
.catch(() => undefined);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
trace: (message: string, extra?: Record<string, unknown>) =>
|
|
24
|
+
write("debug", message, extra),
|
|
25
|
+
debug: (message: string, extra?: Record<string, unknown>) =>
|
|
26
|
+
write("debug", message, extra),
|
|
27
|
+
info: (message: string, extra?: Record<string, unknown>) =>
|
|
28
|
+
write("info", message, extra),
|
|
29
|
+
warn: (message: string, extra?: Record<string, unknown>) =>
|
|
30
|
+
write("warn", message, extra),
|
|
31
|
+
error: (message: string, extra?: Record<string, unknown>) =>
|
|
32
|
+
write("error", message, extra),
|
|
33
|
+
};
|
|
34
|
+
}
|