opencode-lcm 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/archive-transform.d.ts +45 -0
- package/dist/archive-transform.js +81 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +16 -0
- package/dist/doctor.d.ts +22 -0
- package/dist/doctor.js +44 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +306 -0
- package/dist/logging.d.ts +14 -0
- package/dist/logging.js +28 -0
- package/dist/options.d.ts +3 -0
- package/dist/options.js +217 -0
- package/dist/preview-providers.d.ts +20 -0
- package/dist/preview-providers.js +246 -0
- package/dist/privacy.d.ts +16 -0
- package/dist/privacy.js +92 -0
- package/dist/search-ranking.d.ts +12 -0
- package/dist/search-ranking.js +98 -0
- package/dist/sql-utils.d.ts +31 -0
- package/dist/sql-utils.js +80 -0
- package/dist/store-artifacts.d.ts +50 -0
- package/dist/store-artifacts.js +374 -0
- package/dist/store-retention.d.ts +39 -0
- package/dist/store-retention.js +90 -0
- package/dist/store-search.d.ts +37 -0
- package/dist/store-search.js +298 -0
- package/dist/store-snapshot.d.ts +133 -0
- package/dist/store-snapshot.js +325 -0
- package/dist/store-types.d.ts +14 -0
- package/dist/store-types.js +5 -0
- package/dist/store.d.ts +316 -0
- package/dist/store.js +3673 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +35 -0
- package/dist/utils.js +414 -0
- package/dist/workspace-path.d.ts +1 -0
- package/dist/workspace-path.js +15 -0
- package/dist/worktree-key.d.ts +1 -0
- package/dist/worktree-key.js +6 -0
- package/package.json +61 -0
- package/src/archive-transform.ts +147 -0
- package/src/bun-sqlite.d.ts +18 -0
- package/src/constants.ts +20 -0
- package/src/doctor.ts +83 -0
- package/src/index.ts +330 -0
- package/src/logging.ts +41 -0
- package/src/options.ts +297 -0
- package/src/preview-providers.ts +298 -0
- package/src/privacy.ts +122 -0
- package/src/search-ranking.ts +145 -0
- package/src/sql-utils.ts +107 -0
- package/src/store-artifacts.ts +666 -0
- package/src/store-retention.ts +152 -0
- package/src/store-search.ts +440 -0
- package/src/store-snapshot.ts +582 -0
- package/src/store-types.ts +16 -0
- package/src/store.ts +4926 -0
- package/src/types.ts +132 -0
- package/src/utils.ts +444 -0
- package/src/workspace-path.ts +20 -0
- package/src/worktree-key.ts +5 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { ConversationMessage, SearchResult } from './types.js';
|
|
2
|
+
import { shortNodeID, truncate } from './utils.js';
|
|
3
|
+
|
|
4
|
+
export type AutomaticRetrievalHit = {
|
|
5
|
+
kind: 'message' | 'summary' | 'artifact';
|
|
6
|
+
id: string;
|
|
7
|
+
label: string;
|
|
8
|
+
sessionID?: string;
|
|
9
|
+
snippet: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ArchiveSummaryRoot = {
|
|
13
|
+
nodeID: string;
|
|
14
|
+
summaryText: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ArchiveTransformWindow = {
|
|
18
|
+
anchor: ConversationMessage;
|
|
19
|
+
archived: ConversationMessage[];
|
|
20
|
+
recent: ConversationMessage[];
|
|
21
|
+
recentStart: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type AutomaticRetrievalTelemetry = {
|
|
25
|
+
queries: string[];
|
|
26
|
+
rawResults: number;
|
|
27
|
+
stopReason: string;
|
|
28
|
+
scopeStats: Array<{
|
|
29
|
+
scope: string;
|
|
30
|
+
budget: number;
|
|
31
|
+
rawResults: number;
|
|
32
|
+
selectedHits: number;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type AutomaticRetrievalQuotas = {
|
|
37
|
+
message: number;
|
|
38
|
+
summary: number;
|
|
39
|
+
artifact: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function pluralize(count: number, singular: string, plural = `${singular}s`): string {
|
|
43
|
+
return count === 1 ? singular : plural;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function formatAutomaticRetrievalHit(hit: AutomaticRetrievalHit): string {
|
|
47
|
+
const session = hit.sessionID ? ` session=${hit.sessionID}` : '';
|
|
48
|
+
const id = hit.kind === 'summary' ? shortNodeID(hit.id) : hit.id;
|
|
49
|
+
const label = hit.label !== hit.kind ? ` (${hit.label})` : '';
|
|
50
|
+
return `${hit.kind}${session} id=${id}${label}: ${truncate(hit.snippet, 180)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveArchiveTransformWindow(
|
|
54
|
+
messages: ConversationMessage[],
|
|
55
|
+
freshTailMessages: number,
|
|
56
|
+
): ArchiveTransformWindow | undefined {
|
|
57
|
+
let latestUserIndex = -1;
|
|
58
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
59
|
+
if (messages[index]?.info.role === 'user') {
|
|
60
|
+
latestUserIndex = index;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (latestUserIndex < 0) return undefined;
|
|
65
|
+
|
|
66
|
+
let recentStart = Math.max(0, messages.length - Math.max(0, freshTailMessages));
|
|
67
|
+
if (latestUserIndex < recentStart) {
|
|
68
|
+
recentStart = latestUserIndex;
|
|
69
|
+
}
|
|
70
|
+
if (recentStart <= 0) return undefined;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
anchor: messages[latestUserIndex],
|
|
74
|
+
archived: messages.slice(0, recentStart),
|
|
75
|
+
recent: messages.slice(recentStart),
|
|
76
|
+
recentStart,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function selectAutomaticRetrievalHits(input: {
|
|
81
|
+
recent: ConversationMessage[];
|
|
82
|
+
tokens: string[];
|
|
83
|
+
results: SearchResult[];
|
|
84
|
+
quotas: AutomaticRetrievalQuotas;
|
|
85
|
+
isFreshResult: (result: SearchResult, freshMessageIDs: Set<string>) => boolean;
|
|
86
|
+
}): AutomaticRetrievalHit[] {
|
|
87
|
+
const freshMessageIDs = new Set(input.recent.map((message) => message.info.id));
|
|
88
|
+
const quotas = { ...input.quotas };
|
|
89
|
+
// With few tokens each one is critical — require at least 2 token matches when possible
|
|
90
|
+
const minSnippetMatches = input.tokens.length >= 2 ? 2 : 1;
|
|
91
|
+
const hits: AutomaticRetrievalHit[] = [];
|
|
92
|
+
|
|
93
|
+
for (const result of input.results) {
|
|
94
|
+
const kind =
|
|
95
|
+
result.type === 'summary'
|
|
96
|
+
? 'summary'
|
|
97
|
+
: result.type.startsWith('artifact:')
|
|
98
|
+
? 'artifact'
|
|
99
|
+
: 'message';
|
|
100
|
+
if (quotas[kind] <= 0) continue;
|
|
101
|
+
if (input.isFreshResult(result, freshMessageIDs)) continue;
|
|
102
|
+
|
|
103
|
+
const lowerSnippet = result.snippet.toLowerCase();
|
|
104
|
+
const matchedTokens = input.tokens.filter((token) => lowerSnippet.includes(token)).length;
|
|
105
|
+
if (matchedTokens < minSnippetMatches && input.tokens.length > 1) continue;
|
|
106
|
+
|
|
107
|
+
hits.push({
|
|
108
|
+
kind,
|
|
109
|
+
id: result.id,
|
|
110
|
+
label: result.type,
|
|
111
|
+
sessionID: result.sessionID,
|
|
112
|
+
snippet: result.snippet,
|
|
113
|
+
});
|
|
114
|
+
quotas[kind] -= 1;
|
|
115
|
+
if (quotas.message <= 0 && quotas.summary <= 0 && quotas.artifact <= 0) break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return hits;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function renderAutomaticRetrievalContext(
|
|
122
|
+
scopes: string | string[],
|
|
123
|
+
hits: AutomaticRetrievalHit[],
|
|
124
|
+
maxChars: number,
|
|
125
|
+
_telemetry?: AutomaticRetrievalTelemetry,
|
|
126
|
+
): string {
|
|
127
|
+
const scopeLabel = Array.isArray(scopes) ? scopes.join(' -> ') : scopes;
|
|
128
|
+
const lines = [
|
|
129
|
+
`[Archived by opencode-lcm: recalled ${hits.length} archived ${pluralize(hits.length, 'hit')} for this turn (scope=${scopeLabel}).]`,
|
|
130
|
+
`Archived hits: ${hits.map((hit) => formatAutomaticRetrievalHit(hit)).join(' | ')}`,
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
return truncate(lines.join('\n'), maxChars);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildActiveSummaryText(
|
|
137
|
+
roots: ArchiveSummaryRoot[],
|
|
138
|
+
archivedCount: number,
|
|
139
|
+
maxChars: number,
|
|
140
|
+
): string {
|
|
141
|
+
const lines = [
|
|
142
|
+
`[Archived by opencode-lcm: compacted ${archivedCount} older conversation ${pluralize(archivedCount, 'turn')} into ${roots.length} archived summary ${pluralize(roots.length, 'node')}.]`,
|
|
143
|
+
`Summary roots: ${roots.map((node) => `${node.nodeID}: ${truncate(node.summaryText, 140)}`).join(' | ')}`,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
return truncate(lines.join('\n'), maxChars);
|
|
147
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
declare module 'bun:sqlite' {
|
|
2
|
+
export class Database {
|
|
3
|
+
constructor(path: string, opts?: { create: boolean });
|
|
4
|
+
exec(sql: string): void;
|
|
5
|
+
close(): void;
|
|
6
|
+
prepare(sql: string): {
|
|
7
|
+
run(...args: unknown[]): void;
|
|
8
|
+
get(...args: unknown[]): Record<string, unknown>;
|
|
9
|
+
all(...args: unknown[]): Record<string, unknown>[];
|
|
10
|
+
values(...args: unknown[]): unknown[][];
|
|
11
|
+
};
|
|
12
|
+
query(sql: string): {
|
|
13
|
+
run(...args: unknown[]): void;
|
|
14
|
+
get(...args: unknown[]): Record<string, unknown>;
|
|
15
|
+
all(...args: unknown[]): Record<string, unknown>[];
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store-level constants used across the SQLite LCM store.
|
|
3
|
+
* These are configuration-like values that control store behavior.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Summary DAG configuration
|
|
7
|
+
export const SUMMARY_LEAF_MESSAGES = 6;
|
|
8
|
+
export const SUMMARY_BRANCH_FACTOR = 3;
|
|
9
|
+
export const SUMMARY_NODE_CHAR_LIMIT = 260;
|
|
10
|
+
|
|
11
|
+
// Store schema
|
|
12
|
+
export const STORE_SCHEMA_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
// Message retrieval limits
|
|
15
|
+
export const EXPAND_MESSAGE_LIMIT = 6;
|
|
16
|
+
|
|
17
|
+
// Automatic retrieval configuration
|
|
18
|
+
export const AUTOMATIC_RETRIEVAL_QUERY_TOKENS = 8;
|
|
19
|
+
export const AUTOMATIC_RETRIEVAL_RECENT_MESSAGES = 3;
|
|
20
|
+
export const AUTOMATIC_RETRIEVAL_QUERY_VARIANTS = 8;
|
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type DoctorSessionIssue = {
|
|
2
|
+
sessionID: string;
|
|
3
|
+
issues: string[];
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type DoctorCountCheck = {
|
|
7
|
+
expected: number;
|
|
8
|
+
actual: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DoctorReport = {
|
|
12
|
+
scope: string;
|
|
13
|
+
checkedSessions: number;
|
|
14
|
+
summarySessionsNeedingRebuild: DoctorSessionIssue[];
|
|
15
|
+
lineageSessionsNeedingRefresh: string[];
|
|
16
|
+
orphanSummaryEdges: number;
|
|
17
|
+
messageFts: DoctorCountCheck;
|
|
18
|
+
summaryFts: DoctorCountCheck;
|
|
19
|
+
artifactFts: DoctorCountCheck;
|
|
20
|
+
orphanArtifactBlobs: number;
|
|
21
|
+
status: 'clean' | 'issues-found' | 'repaired';
|
|
22
|
+
appliedActions?: string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function formatCountCheck(label: string, value: DoctorCountCheck): string[] {
|
|
26
|
+
return [
|
|
27
|
+
`${label}_expected=${value.expected}`,
|
|
28
|
+
`${label}_actual=${value.actual}`,
|
|
29
|
+
`${label}_delta=${value.expected - value.actual}`,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function formatDoctorReport(report: DoctorReport, limit: number): string {
|
|
34
|
+
const issueCount =
|
|
35
|
+
report.summarySessionsNeedingRebuild.length +
|
|
36
|
+
report.lineageSessionsNeedingRefresh.length +
|
|
37
|
+
report.orphanSummaryEdges +
|
|
38
|
+
Math.abs(report.messageFts.expected - report.messageFts.actual) +
|
|
39
|
+
Math.abs(report.summaryFts.expected - report.summaryFts.actual) +
|
|
40
|
+
Math.abs(report.artifactFts.expected - report.artifactFts.actual) +
|
|
41
|
+
report.orphanArtifactBlobs;
|
|
42
|
+
|
|
43
|
+
const lines = [
|
|
44
|
+
`checked_scope=${report.scope}`,
|
|
45
|
+
`checked_sessions=${report.checkedSessions}`,
|
|
46
|
+
`summary_sessions_needing_rebuild=${report.summarySessionsNeedingRebuild.length}`,
|
|
47
|
+
`lineage_sessions_needing_refresh=${report.lineageSessionsNeedingRefresh.length}`,
|
|
48
|
+
`orphan_summary_edges=${report.orphanSummaryEdges}`,
|
|
49
|
+
...formatCountCheck('message_fts', report.messageFts),
|
|
50
|
+
...formatCountCheck('summary_fts', report.summaryFts),
|
|
51
|
+
...formatCountCheck('artifact_fts', report.artifactFts),
|
|
52
|
+
`orphan_artifact_blobs=${report.orphanArtifactBlobs}`,
|
|
53
|
+
`issues=${issueCount}`,
|
|
54
|
+
`status=${report.status}`,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
if (report.summarySessionsNeedingRebuild.length > 0) {
|
|
58
|
+
lines.push(
|
|
59
|
+
'summary_session_preview:',
|
|
60
|
+
...report.summarySessionsNeedingRebuild
|
|
61
|
+
.slice(0, limit)
|
|
62
|
+
.map((issue) => `- ${issue.sessionID}: ${issue.issues.join(', ')}`),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (report.lineageSessionsNeedingRefresh.length > 0) {
|
|
67
|
+
lines.push(
|
|
68
|
+
'lineage_session_preview:',
|
|
69
|
+
...report.lineageSessionsNeedingRefresh.slice(0, limit).map((sessionID) => `- ${sessionID}`),
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (report.appliedActions && report.appliedActions.length > 0) {
|
|
74
|
+
lines.push(
|
|
75
|
+
'applied_actions:',
|
|
76
|
+
...report.appliedActions.slice(0, limit).map((action) => `- ${action}`),
|
|
77
|
+
);
|
|
78
|
+
} else if (report.status === 'issues-found') {
|
|
79
|
+
lines.push('Re-run with apply=true to repair the issues above.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { type Hooks, type PluginInput, tool } from '@opencode-ai/plugin';
|
|
2
|
+
|
|
3
|
+
import { resolveOptions } from './options.js';
|
|
4
|
+
import { SqliteLcmStore } from './store.js';
|
|
5
|
+
|
|
6
|
+
type PluginWithOptions = (ctx: PluginInput, rawOptions?: unknown) => Promise<Hooks>;
|
|
7
|
+
|
|
8
|
+
export const OpencodeLcmPlugin: PluginWithOptions = async (ctx, rawOptions) => {
|
|
9
|
+
const options = resolveOptions(rawOptions);
|
|
10
|
+
const store = new SqliteLcmStore(ctx.directory, options);
|
|
11
|
+
|
|
12
|
+
await store.init();
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
event: async ({ event }) => {
|
|
16
|
+
await store.captureDeferred(event);
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
tool: {
|
|
20
|
+
lcm_status: tool({
|
|
21
|
+
description: 'Show archived LCM capture stats',
|
|
22
|
+
args: {},
|
|
23
|
+
async execute() {
|
|
24
|
+
const stats = await store.stats();
|
|
25
|
+
const lines = [
|
|
26
|
+
`schema_version=${stats.schemaVersion}`,
|
|
27
|
+
`total_events=${stats.totalEvents}`,
|
|
28
|
+
`session_count=${stats.sessionCount}`,
|
|
29
|
+
`root_sessions=${stats.rootSessionCount}`,
|
|
30
|
+
`branched_sessions=${stats.branchedSessionCount}`,
|
|
31
|
+
`pinned_sessions=${stats.pinnedSessionCount}`,
|
|
32
|
+
`worktrees=${stats.worktreeCount}`,
|
|
33
|
+
`latest_event_at=${stats.latestEventAt ?? 'n/a'}`,
|
|
34
|
+
`summary_nodes=${stats.summaryNodeCount}`,
|
|
35
|
+
`summary_states=${stats.summaryStateCount}`,
|
|
36
|
+
`artifacts=${stats.artifactCount}`,
|
|
37
|
+
`artifact_blobs=${stats.artifactBlobCount}`,
|
|
38
|
+
`shared_artifact_blobs=${stats.sharedArtifactBlobCount}`,
|
|
39
|
+
`orphan_artifact_blobs=${stats.orphanArtifactBlobCount}`,
|
|
40
|
+
`default_grep_scope=${options.scopeDefaults.grep}`,
|
|
41
|
+
`default_describe_scope=${options.scopeDefaults.describe}`,
|
|
42
|
+
`scope_profiles=${options.scopeProfiles.length}`,
|
|
43
|
+
`retention_stale_session_days=${options.retention.staleSessionDays ?? 'disabled'}`,
|
|
44
|
+
`retention_deleted_session_days=${options.retention.deletedSessionDays ?? 'disabled'}`,
|
|
45
|
+
`retention_orphan_blob_days=${options.retention.orphanBlobDays ?? 'disabled'}`,
|
|
46
|
+
`automatic_retrieval_enabled=${options.automaticRetrieval.enabled}`,
|
|
47
|
+
`automatic_retrieval_max_chars=${options.automaticRetrieval.maxChars}`,
|
|
48
|
+
`automatic_retrieval_min_tokens=${options.automaticRetrieval.minTokens}`,
|
|
49
|
+
`automatic_retrieval_message_hits=${options.automaticRetrieval.maxMessageHits}`,
|
|
50
|
+
`automatic_retrieval_summary_hits=${options.automaticRetrieval.maxSummaryHits}`,
|
|
51
|
+
`automatic_retrieval_artifact_hits=${options.automaticRetrieval.maxArtifactHits}`,
|
|
52
|
+
`automatic_retrieval_scope_order=${options.automaticRetrieval.scopeOrder.join(',')}`,
|
|
53
|
+
`automatic_retrieval_scope_budgets=session:${options.automaticRetrieval.scopeBudgets.session},root:${options.automaticRetrieval.scopeBudgets.root},worktree:${options.automaticRetrieval.scopeBudgets.worktree},all:${options.automaticRetrieval.scopeBudgets.all}`,
|
|
54
|
+
`automatic_retrieval_stop_target_hits=${options.automaticRetrieval.stop.targetHits}`,
|
|
55
|
+
`automatic_retrieval_stop_on_first_scope_with_hits=${options.automaticRetrieval.stop.stopOnFirstScopeWithHits}`,
|
|
56
|
+
`fresh_tail_messages=${options.freshTailMessages}`,
|
|
57
|
+
`min_messages_for_transform=${options.minMessagesForTransform}`,
|
|
58
|
+
`large_content_threshold=${options.largeContentThreshold}`,
|
|
59
|
+
`binary_preview_providers=${options.binaryPreviewProviders.join(',')}`,
|
|
60
|
+
`preview_byte_peek=${options.previewBytePeek}`,
|
|
61
|
+
`privacy_exclude_tool_prefixes=${options.privacy.excludeToolPrefixes.join(',')}`,
|
|
62
|
+
`privacy_exclude_path_patterns=${options.privacy.excludePathPatterns.length}`,
|
|
63
|
+
`privacy_redact_patterns=${options.privacy.redactPatterns.length}`,
|
|
64
|
+
...Object.entries(stats.eventTypes)
|
|
65
|
+
.sort((a, b) => b[1] - a[1])
|
|
66
|
+
.slice(0, 10)
|
|
67
|
+
.map(([type, count]) => `${type}=${count}`),
|
|
68
|
+
];
|
|
69
|
+
return lines.join('\n');
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
|
|
73
|
+
lcm_resume: tool({
|
|
74
|
+
description: 'Show the latest archived resume note',
|
|
75
|
+
args: {
|
|
76
|
+
sessionID: tool.schema.string().optional(),
|
|
77
|
+
},
|
|
78
|
+
async execute(args) {
|
|
79
|
+
return await store.resume(args.sessionID);
|
|
80
|
+
},
|
|
81
|
+
}),
|
|
82
|
+
|
|
83
|
+
lcm_grep: tool({
|
|
84
|
+
description: 'Search archived LCM capture with scope',
|
|
85
|
+
args: {
|
|
86
|
+
query: tool.schema.string().min(1),
|
|
87
|
+
sessionID: tool.schema.string().optional(),
|
|
88
|
+
scope: tool.schema.string().optional(),
|
|
89
|
+
limit: tool.schema.number().int().min(1).max(20).optional(),
|
|
90
|
+
},
|
|
91
|
+
async execute(args) {
|
|
92
|
+
const results = await store.grep({
|
|
93
|
+
query: args.query,
|
|
94
|
+
sessionID: args.sessionID,
|
|
95
|
+
scope: args.scope,
|
|
96
|
+
limit: args.limit ?? 5,
|
|
97
|
+
});
|
|
98
|
+
if (results.length === 0) return 'No archived matches found.';
|
|
99
|
+
|
|
100
|
+
return results
|
|
101
|
+
.map((result) => {
|
|
102
|
+
const suffix = result.sessionID ? ` session=${result.sessionID}` : '';
|
|
103
|
+
return `[${result.type}]${suffix} ${result.snippet}`;
|
|
104
|
+
})
|
|
105
|
+
.join('\n\n');
|
|
106
|
+
},
|
|
107
|
+
}),
|
|
108
|
+
|
|
109
|
+
lcm_describe: tool({
|
|
110
|
+
description: 'Summarize archived session capture with scope',
|
|
111
|
+
args: {
|
|
112
|
+
sessionID: tool.schema.string().optional(),
|
|
113
|
+
scope: tool.schema.string().optional(),
|
|
114
|
+
},
|
|
115
|
+
async execute(args) {
|
|
116
|
+
return await store.describe({
|
|
117
|
+
sessionID: args.sessionID,
|
|
118
|
+
scope: args.scope,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
lcm_lineage: tool({
|
|
124
|
+
description: 'Show archived branch lineage for a session',
|
|
125
|
+
args: {
|
|
126
|
+
sessionID: tool.schema.string().optional(),
|
|
127
|
+
},
|
|
128
|
+
async execute(args) {
|
|
129
|
+
return await store.lineage(args.sessionID);
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
lcm_pin_session: tool({
|
|
134
|
+
description: 'Pin a session so retention pruning will skip it',
|
|
135
|
+
args: {
|
|
136
|
+
sessionID: tool.schema.string().optional(),
|
|
137
|
+
reason: tool.schema.string().optional(),
|
|
138
|
+
},
|
|
139
|
+
async execute(args) {
|
|
140
|
+
return await store.pinSession({
|
|
141
|
+
sessionID: args.sessionID,
|
|
142
|
+
reason: args.reason,
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
lcm_unpin_session: tool({
|
|
148
|
+
description: 'Remove a session retention pin',
|
|
149
|
+
args: {
|
|
150
|
+
sessionID: tool.schema.string().optional(),
|
|
151
|
+
},
|
|
152
|
+
async execute(args) {
|
|
153
|
+
return await store.unpinSession({
|
|
154
|
+
sessionID: args.sessionID,
|
|
155
|
+
});
|
|
156
|
+
},
|
|
157
|
+
}),
|
|
158
|
+
|
|
159
|
+
lcm_expand: tool({
|
|
160
|
+
description: 'Expand archived summary nodes into targeted descendants or raw messages',
|
|
161
|
+
args: {
|
|
162
|
+
sessionID: tool.schema.string().optional(),
|
|
163
|
+
nodeID: tool.schema.string().optional(),
|
|
164
|
+
query: tool.schema.string().optional(),
|
|
165
|
+
depth: tool.schema.number().int().min(1).max(4).optional(),
|
|
166
|
+
messageLimit: tool.schema.number().int().min(1).max(20).optional(),
|
|
167
|
+
includeRaw: tool.schema.boolean().optional(),
|
|
168
|
+
},
|
|
169
|
+
async execute(args) {
|
|
170
|
+
return await store.expand({
|
|
171
|
+
sessionID: args.sessionID,
|
|
172
|
+
nodeID: args.nodeID,
|
|
173
|
+
query: args.query,
|
|
174
|
+
depth: args.depth,
|
|
175
|
+
messageLimit: args.messageLimit,
|
|
176
|
+
includeRaw: args.includeRaw,
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
}),
|
|
180
|
+
|
|
181
|
+
lcm_artifact: tool({
|
|
182
|
+
description: 'View externalized archived content by artifact ID',
|
|
183
|
+
args: {
|
|
184
|
+
artifactID: tool.schema.string().min(1),
|
|
185
|
+
chars: tool.schema.number().int().min(200).max(20000).optional(),
|
|
186
|
+
},
|
|
187
|
+
async execute(args) {
|
|
188
|
+
return await store.artifact({
|
|
189
|
+
artifactID: args.artifactID,
|
|
190
|
+
chars: args.chars,
|
|
191
|
+
});
|
|
192
|
+
},
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
lcm_blob_stats: tool({
|
|
196
|
+
description: 'Show deduplicated artifact blob stats',
|
|
197
|
+
args: {
|
|
198
|
+
limit: tool.schema.number().int().min(1).max(20).optional(),
|
|
199
|
+
},
|
|
200
|
+
async execute(args) {
|
|
201
|
+
return await store.blobStats({
|
|
202
|
+
limit: args.limit,
|
|
203
|
+
});
|
|
204
|
+
},
|
|
205
|
+
}),
|
|
206
|
+
|
|
207
|
+
lcm_blob_gc: tool({
|
|
208
|
+
description: 'Preview or delete orphaned artifact blobs',
|
|
209
|
+
args: {
|
|
210
|
+
apply: tool.schema.boolean().optional(),
|
|
211
|
+
limit: tool.schema.number().int().min(1).max(50).optional(),
|
|
212
|
+
},
|
|
213
|
+
async execute(args) {
|
|
214
|
+
return await store.gcBlobs({
|
|
215
|
+
apply: args.apply,
|
|
216
|
+
limit: args.limit,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
}),
|
|
220
|
+
|
|
221
|
+
lcm_doctor: tool({
|
|
222
|
+
description: 'Inspect or repair archive summaries and indexes',
|
|
223
|
+
args: {
|
|
224
|
+
apply: tool.schema.boolean().optional(),
|
|
225
|
+
sessionID: tool.schema.string().optional(),
|
|
226
|
+
limit: tool.schema.number().int().min(1).max(50).optional(),
|
|
227
|
+
},
|
|
228
|
+
async execute(args) {
|
|
229
|
+
return await store.doctor({
|
|
230
|
+
apply: args.apply,
|
|
231
|
+
sessionID: args.sessionID,
|
|
232
|
+
limit: args.limit,
|
|
233
|
+
});
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
|
|
237
|
+
lcm_retention_report: tool({
|
|
238
|
+
description: 'Preview stale-session and orphan-blob retention candidates',
|
|
239
|
+
args: {
|
|
240
|
+
staleSessionDays: tool.schema.number().min(0).optional(),
|
|
241
|
+
deletedSessionDays: tool.schema.number().min(0).optional(),
|
|
242
|
+
orphanBlobDays: tool.schema.number().min(0).optional(),
|
|
243
|
+
limit: tool.schema.number().int().min(1).max(50).optional(),
|
|
244
|
+
},
|
|
245
|
+
async execute(args) {
|
|
246
|
+
return await store.retentionReport({
|
|
247
|
+
staleSessionDays: args.staleSessionDays,
|
|
248
|
+
deletedSessionDays: args.deletedSessionDays,
|
|
249
|
+
orphanBlobDays: args.orphanBlobDays,
|
|
250
|
+
limit: args.limit,
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
}),
|
|
254
|
+
|
|
255
|
+
lcm_retention_prune: tool({
|
|
256
|
+
description: 'Preview or apply stale-session and orphan-blob retention pruning',
|
|
257
|
+
args: {
|
|
258
|
+
apply: tool.schema.boolean().optional(),
|
|
259
|
+
staleSessionDays: tool.schema.number().min(0).optional(),
|
|
260
|
+
deletedSessionDays: tool.schema.number().min(0).optional(),
|
|
261
|
+
orphanBlobDays: tool.schema.number().min(0).optional(),
|
|
262
|
+
limit: tool.schema.number().int().min(1).max(50).optional(),
|
|
263
|
+
},
|
|
264
|
+
async execute(args) {
|
|
265
|
+
return await store.retentionPrune({
|
|
266
|
+
apply: args.apply,
|
|
267
|
+
staleSessionDays: args.staleSessionDays,
|
|
268
|
+
deletedSessionDays: args.deletedSessionDays,
|
|
269
|
+
orphanBlobDays: args.orphanBlobDays,
|
|
270
|
+
limit: args.limit,
|
|
271
|
+
});
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
|
|
275
|
+
lcm_export_snapshot: tool({
|
|
276
|
+
description: 'Export a portable long-memory snapshot to a JSON file',
|
|
277
|
+
args: {
|
|
278
|
+
filePath: tool.schema.string().min(1),
|
|
279
|
+
sessionID: tool.schema.string().optional(),
|
|
280
|
+
scope: tool.schema.string().optional(),
|
|
281
|
+
},
|
|
282
|
+
async execute(args) {
|
|
283
|
+
return await store.exportSnapshot({
|
|
284
|
+
filePath: args.filePath,
|
|
285
|
+
sessionID: args.sessionID,
|
|
286
|
+
scope: args.scope,
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
}),
|
|
290
|
+
|
|
291
|
+
lcm_import_snapshot: tool({
|
|
292
|
+
description: 'Import a portable long-memory snapshot from a JSON file',
|
|
293
|
+
args: {
|
|
294
|
+
filePath: tool.schema.string().min(1),
|
|
295
|
+
mode: tool.schema.string().optional(),
|
|
296
|
+
worktreeMode: tool.schema.string().optional(),
|
|
297
|
+
},
|
|
298
|
+
async execute(args) {
|
|
299
|
+
return await store.importSnapshot({
|
|
300
|
+
filePath: args.filePath,
|
|
301
|
+
mode: args.mode === 'merge' ? 'merge' : 'replace',
|
|
302
|
+
worktreeMode:
|
|
303
|
+
args.worktreeMode === 'preserve' || args.worktreeMode === 'current'
|
|
304
|
+
? args.worktreeMode
|
|
305
|
+
: 'auto',
|
|
306
|
+
});
|
|
307
|
+
},
|
|
308
|
+
}),
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
'experimental.chat.messages.transform': async (_input, output) => {
|
|
312
|
+
await store.transformMessages(output.messages);
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
'experimental.chat.system.transform': async (_input, output) => {
|
|
316
|
+
const hint = store.systemHint();
|
|
317
|
+
if (!hint) return;
|
|
318
|
+
output.system.push(hint);
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
'experimental.session.compacting': async (input, output) => {
|
|
322
|
+
const note = await store.buildCompactionContext(input.sessionID);
|
|
323
|
+
if (!note) return;
|
|
324
|
+
if (output.context.some((entry) => entry.includes('LCM prototype resume note'))) return;
|
|
325
|
+
output.context.push(note);
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export default OpencodeLcmPlugin;
|
package/src/logging.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight structured logging interface for opencode-lcm.
|
|
3
|
+
* Silent by default so plugin logs do not corrupt the host terminal UI.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
7
|
+
|
|
8
|
+
export interface Logger {
|
|
9
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
10
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
11
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
12
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isTruthyEnvFlag(value: string | undefined): boolean {
|
|
16
|
+
if (!value) return false;
|
|
17
|
+
const normalized = value.trim().toLowerCase();
|
|
18
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const silentLogger: Logger = {
|
|
22
|
+
debug() {},
|
|
23
|
+
info() {},
|
|
24
|
+
warn() {},
|
|
25
|
+
error() {},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let currentLogger: Logger = silentLogger;
|
|
29
|
+
|
|
30
|
+
export function setLogger(logger: Logger): void {
|
|
31
|
+
currentLogger = logger;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getLogger(): Logger {
|
|
35
|
+
return currentLogger;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isStartupLoggingEnabled(): boolean {
|
|
39
|
+
if (typeof process !== 'object' || !process?.env) return false;
|
|
40
|
+
return isTruthyEnvFlag(process.env.OPENCODE_LCM_STARTUP_LOG);
|
|
41
|
+
}
|