opencode-blackhole 0.0.0-stage → 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 +35 -0
- package/LICENSE.vendor +21 -0
- package/README.md +377 -2
- package/dist/server.js +5025 -0
- package/index.ts +1 -0
- package/package.json +57 -4
- package/src/adapter/opencode-messages.ts +270 -0
- package/src/compaction/build.ts +264 -0
- package/src/compaction/handoff.ts +421 -0
- package/src/config.ts +316 -0
- package/src/index.ts +714 -0
- package/src/memory/ledger.ts +254 -0
- package/src/memory/manager.ts +659 -0
- package/src/memory/models.ts +172 -0
- package/src/memory/prompts.ts +145 -0
- package/src/recall/recall.ts +327 -0
- package/src/state/paths.ts +42 -0
- package/src/store/capture.ts +84 -0
- package/src/store/transcript.ts +279 -0
- package/src/vcc/README.md +51 -0
- package/src/vcc/core/brief.ts +407 -0
- package/src/vcc/core/build-sections.ts +81 -0
- package/src/vcc/core/compact-args.ts +45 -0
- package/src/vcc/core/content.ts +156 -0
- package/src/vcc/core/drill-down.ts +302 -0
- package/src/vcc/core/filter-noise.ts +41 -0
- package/src/vcc/core/format-recall.ts +100 -0
- package/src/vcc/core/format.ts +104 -0
- package/src/vcc/core/lineage.ts +26 -0
- package/src/vcc/core/normalize.ts +76 -0
- package/src/vcc/core/rank.ts +283 -0
- package/src/vcc/core/recall-request.ts +160 -0
- package/src/vcc/core/recall-scope.ts +30 -0
- package/src/vcc/core/render-entries.ts +55 -0
- package/src/vcc/core/sanitize.ts +5 -0
- package/src/vcc/core/search-entries.ts +594 -0
- package/src/vcc/core/skill-collapse.ts +35 -0
- package/src/vcc/core/summarize.ts +289 -0
- package/src/vcc/core/token-estimate.ts +100 -0
- package/src/vcc/core/tool-args.ts +16 -0
- package/src/vcc/details.ts +11 -0
- package/src/vcc/extract/commits.ts +96 -0
- package/src/vcc/extract/files.ts +87 -0
- package/src/vcc/extract/goals.ts +79 -0
- package/src/vcc/extract/preferences.ts +55 -0
- package/src/vcc/extract/tracked-commands.ts +232 -0
- package/src/vcc/load-messages.ts +42 -0
- package/src/vcc/pi-types.ts +53 -0
- package/src/vcc/sections.ts +12 -0
- package/src/vcc/types.ts +16 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NormalizedBlock } from "../types";
|
|
2
|
+
import { clip, nonEmptyLines } from "../core/content";
|
|
3
|
+
|
|
4
|
+
// Tightened patterns: require a clear preference construction, not bare keywords.
|
|
5
|
+
const PREF_PATTERNS = [
|
|
6
|
+
/\bprefer(?:s|red|ring)?\s+\w/i,
|
|
7
|
+
/\bdon'?t want\b/i,
|
|
8
|
+
/\balways (?:use|do|run|prefer|keep|make|format|write|add|set|put|prefix|start|include|append)\b/i,
|
|
9
|
+
/\bnever (?:use|do|run|push|commit|write|ignore|add|set|put|remove|delete|include|deploy)\b/i,
|
|
10
|
+
/\bplease (?:use|avoid|keep|make|don'?t|do not|format|write)\b/i,
|
|
11
|
+
/\b(?:style|format|language|naming)\s*[:=]\s*\S/i,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export const extractPreferences = (blocks: NormalizedBlock[]): string[] => {
|
|
15
|
+
const prefs: string[] = [];
|
|
16
|
+
const seen = new Set<string>();
|
|
17
|
+
|
|
18
|
+
for (const b of blocks) {
|
|
19
|
+
if (b.kind !== "user") continue;
|
|
20
|
+
|
|
21
|
+
let perBlock = 0;
|
|
22
|
+
for (const line of nonEmptyLines(b.text)) {
|
|
23
|
+
const trimmed = line.trim();
|
|
24
|
+
if (!trimmed || trimmed.length < 5) continue;
|
|
25
|
+
if (trimmed.length > 200) continue;
|
|
26
|
+
// Reject questions.
|
|
27
|
+
if (trimmed.endsWith("?") || trimmed.includes("?...")) continue;
|
|
28
|
+
if (!PREF_PATTERNS.some((p) => p.test(trimmed))) continue;
|
|
29
|
+
|
|
30
|
+
const clipped = clip(trimmed, 200);
|
|
31
|
+
const key = clipped.toLowerCase();
|
|
32
|
+
if (seen.has(key)) continue;
|
|
33
|
+
seen.add(key);
|
|
34
|
+
prefs.push(clipped);
|
|
35
|
+
|
|
36
|
+
// Cap per user block to avoid pasting long rule lists as many prefs.
|
|
37
|
+
if (++perBlock >= 1) break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return prefs.slice(0, 10);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Remove preferences that duplicate goals (case-insensitive, trimmed).
|
|
46
|
+
* Called by `buildSections` so that the two sections do not overlap.
|
|
47
|
+
*/
|
|
48
|
+
export const dedupPreferencesAgainstGoals = (
|
|
49
|
+
prefs: string[],
|
|
50
|
+
goals: string[],
|
|
51
|
+
): string[] => {
|
|
52
|
+
const norm = (s: string) => s.trim().toLowerCase();
|
|
53
|
+
const goalSet = new Set(goals.map(norm));
|
|
54
|
+
return prefs.filter((p) => !goalSet.has(norm(p)));
|
|
55
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { NormalizedBlock } from "../types";
|
|
2
|
+
import { capItems } from "../core/format";
|
|
3
|
+
|
|
4
|
+
/** Maximum characters kept per captured entry (a truncated one-liner, not a
|
|
5
|
+
* parsed structure -- deliberately shallow, see module docstring below). */
|
|
6
|
+
const MAX_ENTRY_CHARS = 80;
|
|
7
|
+
|
|
8
|
+
/** Single-pass quote-aware scan: returns the index of every UNQUOTED
|
|
9
|
+
* shell separator (`;`, `&`, `|`, newline). Quote state tracks ' and "
|
|
10
|
+
* plus backslash escapes; separators inside quotes are not boundaries.
|
|
11
|
+
* Deliberately stops there -- $( ) and backticks are not handled, keeping
|
|
12
|
+
* this a 20-line tokenizer rather than a shell parser. */
|
|
13
|
+
const unquotedSeparators = (cmd: string): number[] => {
|
|
14
|
+
const seps: number[] = [];
|
|
15
|
+
let quote: string | null = null;
|
|
16
|
+
for (let i = 0; i < cmd.length; i++) {
|
|
17
|
+
const c = cmd[i];
|
|
18
|
+
if (quote) {
|
|
19
|
+
// Backslash escapes inside "..." only; inside '...' it is literal.
|
|
20
|
+
if (c === "\\" && quote === '"') i++;
|
|
21
|
+
else if (c === quote) quote = null;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c === "\\") i++; // `it\'s` is not an opening quote
|
|
25
|
+
else if (c === "'" || c === '"') quote = c;
|
|
26
|
+
// A comment runs to the newline; its apostrophes (`# don't`) are not quotes.
|
|
27
|
+
else if (c === "#" && (i === 0 || /[\s;&|(]/.test(cmd[i - 1]))) {
|
|
28
|
+
while (i + 1 < cmd.length && cmd[i + 1] !== "\n") i++;
|
|
29
|
+
}
|
|
30
|
+
// `2>&1`, `<&3`, `&>file` are redirections, not separators.
|
|
31
|
+
else if (c === "&" && (cmd[i - 1] === ">" || cmd[i - 1] === "<" || cmd[i + 1] === ">")) continue;
|
|
32
|
+
else if (c === ";" || c === "&" || c === "|" || c === "\n") seps.push(i);
|
|
33
|
+
}
|
|
34
|
+
return seps;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Cut `text` at the next UNQUOTED separator. Quotes are kept as written:
|
|
38
|
+
* the cut never lands inside a quote, so they stay balanced. No truncation
|
|
39
|
+
* here -- dedup must see the full string (two distinct invocations sharing
|
|
40
|
+
* an 80-char prefix must not collapse); render-time truncation lives in
|
|
41
|
+
* formatTrackedCommands. */
|
|
42
|
+
const captureEntry = (text: string): string => {
|
|
43
|
+
const seps = unquotedSeparators(text);
|
|
44
|
+
const cut = seps.length ? text.slice(0, seps[0]) : text;
|
|
45
|
+
return cut.trim();
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Prefixes that hide the real command name: `VAR=val` assignments and a
|
|
49
|
+
* tiny, decades-stable wrapper list (sudo/env/nohup/time/command) — NOT a
|
|
50
|
+
* growing per-CLI taxonomy. They are skipped only to find the name; the
|
|
51
|
+
* entry keeps them (see findTopLevelInvocations). */
|
|
52
|
+
const WRAPPERS = new Set(["sudo", "env", "nohup", "time", "command"]);
|
|
53
|
+
const WRAPPER_VALUE_FLAGS: Record<string, RegExp> = {
|
|
54
|
+
sudo: /^-[ugpCDRT]$/, // -u user -g group -p prompt -C fd -D dir -R? -T
|
|
55
|
+
env: /^-[uS]$/, // -u name, -S split-string
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Next token from pos on the same line: a newline is a separator, so a
|
|
59
|
+
* lone `VAR=x` line must not borrow the command on the next one. */
|
|
60
|
+
const tokenAt = (cmd: string, pos: number): [string, number] => {
|
|
61
|
+
while (pos < cmd.length && (cmd[pos] === " " || cmd[pos] === "\t")) pos++;
|
|
62
|
+
const t = cmd.slice(pos).match(/^\S+/)?.[0] ?? "";
|
|
63
|
+
return [t, pos];
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** Skip VAR=val prefixes and wrapper-command invocations starting at pos;
|
|
67
|
+
* returns the position where the real command begins. */
|
|
68
|
+
const skipToCommand = (cmd: string, pos: number): number => {
|
|
69
|
+
for (;;) {
|
|
70
|
+
const [tok, at] = tokenAt(cmd, pos);
|
|
71
|
+
if (!tok) return at;
|
|
72
|
+
if (/^[A-Za-z_]\w*=/.test(tok)) { pos = at + tok.length; continue; }
|
|
73
|
+
if (!WRAPPERS.has(tok)) return at;
|
|
74
|
+
pos = at + tok.length;
|
|
75
|
+
for (;;) {
|
|
76
|
+
const [flag, fat] = tokenAt(cmd, pos);
|
|
77
|
+
if (!flag.startsWith("-")) break;
|
|
78
|
+
pos = fat + flag.length;
|
|
79
|
+
if (WRAPPER_VALUE_FLAGS[tok]?.test(flag)) {
|
|
80
|
+
const [value, vat] = tokenAt(cmd, pos); // consume the flag's value
|
|
81
|
+
pos = vat + value.length;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Positions where an invocation of `name` begins: position 0 or right
|
|
88
|
+
* after an UNQUOTED separator, so `echo "a; docker restart"` is not an
|
|
89
|
+
* invocation. Requires whitespace (or end of string) right after `name`, so
|
|
90
|
+
* `ssh-keygen`/`docker-compose` don't match `ssh`/`docker`. Returns the
|
|
91
|
+
* START of the segment (before any VAR=/wrapper prefixes), not the name
|
|
92
|
+
* itself, so the recorded entry stays faithful to what actually ran
|
|
93
|
+
* (`sudo -u root kubectl get pods` is recorded whole). */
|
|
94
|
+
const findTopLevelInvocations = (cmd: string, name: string): number[] => {
|
|
95
|
+
const bounds = [0, ...unquotedSeparators(cmd).map((i) => i + 1)];
|
|
96
|
+
const starts: number[] = [];
|
|
97
|
+
for (const bound of bounds) {
|
|
98
|
+
const pos = skipToCommand(cmd, bound);
|
|
99
|
+
if (!cmd.startsWith(name, pos)) continue;
|
|
100
|
+
const after = cmd[pos + name.length];
|
|
101
|
+
if (after === undefined || /\s/.test(after)) starts.push(bound);
|
|
102
|
+
}
|
|
103
|
+
return starts;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** ssh flags that take NO argument; every other leading `-flag` consumes
|
|
107
|
+
* the next token. Without this, `ssh -T host 'cmd'` misreads `host` as
|
|
108
|
+
* -T's value and the remote command is never found. Clusters (`-tt`, `-NT`)
|
|
109
|
+
* count as boolean when every letter is one. */
|
|
110
|
+
const SSH_BOOL_FLAG = /^-[46ACfGgKkMNTtVvXxYynaq]+$/;
|
|
111
|
+
|
|
112
|
+
/** Locate the SSH target by TOKEN POSITION (via matchAll's own `.index`,
|
|
113
|
+
* never a substring re-search, which would misfire on e.g.
|
|
114
|
+
* `ssh -i key-prod prod ...` finding "prod" inside "key-prod") and return
|
|
115
|
+
* everything after it, trimmed. */
|
|
116
|
+
const sshRemoteCommand = (afterSsh: string): string | undefined => {
|
|
117
|
+
const tokens = [...afterSsh.matchAll(/\S+/g)];
|
|
118
|
+
let i = 0;
|
|
119
|
+
while (i < tokens.length && tokens[i][0].startsWith("-")) {
|
|
120
|
+
i += SSH_BOOL_FLAG.test(tokens[i][0]) ? 1 : 2;
|
|
121
|
+
}
|
|
122
|
+
const target = tokens[i];
|
|
123
|
+
if (!target) return undefined;
|
|
124
|
+
const rest = afterSsh.slice(target.index! + target[0].length).trim();
|
|
125
|
+
return rest || undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export interface TrackedCommandActivity {
|
|
129
|
+
/** Command name (as configured in settings.trackCommands) -> entries seen. */
|
|
130
|
+
byCommand: Map<string, Set<string>>;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Scans bash tool-call commands for invocations of any command name in
|
|
135
|
+
* `trackCommands`, capturing a one-line snapshot per match --
|
|
136
|
+
* deliberately shallow (no per-command argument parsing) so this never
|
|
137
|
+
* needs updating as any given CLI's flags evolve, unlike a design that
|
|
138
|
+
* tries to extract structured fields (e.g. "the kubectl namespace" or "the
|
|
139
|
+
* docker container name") for each tool separately.
|
|
140
|
+
*
|
|
141
|
+
* When "ssh" is one of the tracked names, also scans inside its own
|
|
142
|
+
* remote-command argument (quoted or not) for other tracked names --
|
|
143
|
+
* running a command over SSH is as common as running it locally, and a
|
|
144
|
+
* top-level-only scan would otherwise miss it.
|
|
145
|
+
*
|
|
146
|
+
* Only matches literal command text, never tool_result output.
|
|
147
|
+
*/
|
|
148
|
+
export const extractTrackedCommands = (
|
|
149
|
+
blocks: NormalizedBlock[],
|
|
150
|
+
trackCommands: readonly string[],
|
|
151
|
+
): TrackedCommandActivity => {
|
|
152
|
+
const byCommand = new Map<string, Set<string>>();
|
|
153
|
+
if (trackCommands.length === 0) return { byCommand };
|
|
154
|
+
for (const name of trackCommands) byCommand.set(name, new Set());
|
|
155
|
+
|
|
156
|
+
const trackSsh = trackCommands.includes("ssh");
|
|
157
|
+
|
|
158
|
+
for (const b of blocks) {
|
|
159
|
+
if (b.kind !== "tool_call" || b.name !== "bash") continue;
|
|
160
|
+
const cmd = typeof b.args.command === "string" ? b.args.command : "";
|
|
161
|
+
if (!cmd) continue;
|
|
162
|
+
|
|
163
|
+
for (const name of trackCommands) {
|
|
164
|
+
for (const startAt of findTopLevelInvocations(cmd, name)) {
|
|
165
|
+
const item = captureEntry(cmd.slice(startAt).trim());
|
|
166
|
+
if (!item) continue;
|
|
167
|
+
const set = byCommand.get(name)!;
|
|
168
|
+
// re-running the same command refreshes it to the tail — the cap
|
|
169
|
+
// then keeps the N most-recently-USED, not first-seen.
|
|
170
|
+
if (set.has(item)) set.delete(item);
|
|
171
|
+
set.add(item);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (trackSsh) {
|
|
176
|
+
for (const bound of findTopLevelInvocations(cmd, "ssh")) {
|
|
177
|
+
// bounds are segment starts (incl. wrappers); the ssh token itself
|
|
178
|
+
// sits at skipToCommand — slice past it for the remote parser.
|
|
179
|
+
const seg = cmd.slice(bound);
|
|
180
|
+
const remote = sshRemoteCommand(seg.slice(skipToCommand(seg, 0) + 3));
|
|
181
|
+
if (!remote) continue;
|
|
182
|
+
// The remote string's quotes were the LOCAL shell's syntax, not
|
|
183
|
+
// the remote command's — a leftover stray ' would open a phantom
|
|
184
|
+
// quote in the rescanner and swallow the rest of the line. Strip
|
|
185
|
+
// them all; boundary detection stays shallow regardless.
|
|
186
|
+
const unquoted = remote.replace(/['"]/g, "");
|
|
187
|
+
for (const name of trackCommands) {
|
|
188
|
+
if (name === "ssh") continue;
|
|
189
|
+
for (const startAt2 of findTopLevelInvocations(unquoted, name)) {
|
|
190
|
+
const item = captureEntry(unquoted.slice(startAt2).trim());
|
|
191
|
+
if (!item) continue;
|
|
192
|
+
const set = byCommand.get(name)!;
|
|
193
|
+
if (set.has(item)) set.delete(item);
|
|
194
|
+
set.add(item);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { byCommand };
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
/** Line format of `[Tracked Commands]`, shared with the merge in
|
|
205
|
+
* core/summarize.ts: `- <name>: <entry> | <entry> ...`, newest
|
|
206
|
+
* COMMANDS_PER_NAME kept. " | " rather than "," since an entry (e.g.
|
|
207
|
+
* `kubectl get pods,svc`) can legitimately contain a comma. */
|
|
208
|
+
export const COMMAND_SEPARATOR = " | ";
|
|
209
|
+
export const COMMANDS_PER_NAME = 10;
|
|
210
|
+
|
|
211
|
+
/** Render form: one line (a quoted multi-line argument such as
|
|
212
|
+
* `python3 -c '...'` would otherwise break the line format), with an inner
|
|
213
|
+
* separator escaped as ` \| ` so the merge, which splits on
|
|
214
|
+
* COMMAND_SEPARATOR, keeps the entry whole. Then truncated. */
|
|
215
|
+
const renderEntry = (entry: string): string => {
|
|
216
|
+
const line = entry.replace(/\s+/g, " ").replaceAll(COMMAND_SEPARATOR, " \\| ");
|
|
217
|
+
return line.length > MAX_ENTRY_CHARS ? `${line.slice(0, MAX_ENTRY_CHARS)}…` : line;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/** Formats TrackedCommandActivity into `[Tracked Commands]` body lines, one per
|
|
221
|
+
* tracked command name that had at least one match. Keeps the NEWEST
|
|
222
|
+
* entries: a ledger of what was run recently must not freeze at its first
|
|
223
|
+
* ten while everything later drowns in a permanent "(+N more)". */
|
|
224
|
+
export const formatTrackedCommands = (act: TrackedCommandActivity): string[] => {
|
|
225
|
+
const lines: string[] = [];
|
|
226
|
+
for (const [name, entries] of act.byCommand) {
|
|
227
|
+
if (entries.size > 0) {
|
|
228
|
+
lines.push(`${name}: ${capItems([...entries].map(renderEntry), COMMANDS_PER_NAME, COMMAND_SEPARATOR, "tail")}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return lines;
|
|
232
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory session history loader.
|
|
3
|
+
*
|
|
4
|
+
* Upstream pi-vcc reads the session JSONL file from disk. OpenCode owns its
|
|
5
|
+
* history in the server database and exposes it over the API, so the loader
|
|
6
|
+
* here takes an already-materialized message list instead of a path.
|
|
7
|
+
*
|
|
8
|
+
* Upstream: https://github.com/sting8k/pi-vcc (MIT) — Copyright (c) 2026
|
|
9
|
+
* sting8k. See LICENSE.vendor in this package for the full text.
|
|
10
|
+
*/
|
|
11
|
+
import type { Message } from "./pi-types"
|
|
12
|
+
import { renderMessage, type RenderedEntry } from "./core/render-entries"
|
|
13
|
+
|
|
14
|
+
export interface LoadedMessages {
|
|
15
|
+
rendered: RenderedEntry[]
|
|
16
|
+
rawMessages: Message[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render every message into the entry shape the summary and recall pipeline
|
|
21
|
+
* consumes. `full` keeps the untruncated text (recall needs it); the summary
|
|
22
|
+
* path uses the clipped variant.
|
|
23
|
+
*/
|
|
24
|
+
export const loadMessages = (messages: Message[], full = false): LoadedMessages => {
|
|
25
|
+
const rendered: RenderedEntry[] = []
|
|
26
|
+
const rawMessages: Message[] = []
|
|
27
|
+
let messageIndex = 0
|
|
28
|
+
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
// System messages occupy an index but are not conversation content.
|
|
31
|
+
if (message.role !== "system") {
|
|
32
|
+
// A message that already carries a session-global index keeps it, so
|
|
33
|
+
// recall's `#N` refs line up with the ones in a summary.
|
|
34
|
+
const index = message.index ?? messageIndex
|
|
35
|
+
rendered.push(renderMessage(message, index, full))
|
|
36
|
+
rawMessages.push(message)
|
|
37
|
+
}
|
|
38
|
+
messageIndex++
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { rendered, rawMessages }
|
|
42
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal mirror of the `@earendil-works/pi-ai` message shape that the ported
|
|
3
|
+
* vcc core depends on.
|
|
4
|
+
*
|
|
5
|
+
* The vcc core was written against Pi's message model. Rather than rewrite
|
|
6
|
+
* its algorithms, the OpenCode adapter normalizes OpenCode messages into this
|
|
7
|
+
* shape and the core runs unchanged.
|
|
8
|
+
*
|
|
9
|
+
* Upstream: https://github.com/sting8k/pi-vcc (MIT) — Copyright (c) 2026
|
|
10
|
+
* sting8k. See LICENSE.vendor in this package for the full text.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface ToolCall {
|
|
14
|
+
type: "toolCall"
|
|
15
|
+
id: string
|
|
16
|
+
name: string
|
|
17
|
+
arguments: Record<string, unknown>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TextPart {
|
|
21
|
+
type: "text"
|
|
22
|
+
text: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ImagePart {
|
|
26
|
+
type: "image"
|
|
27
|
+
mimeType: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type ContentPart = TextPart | ToolCall | ImagePart
|
|
31
|
+
|
|
32
|
+
export type MessageRole = "user" | "assistant" | "toolResult" | "bashExecution" | "system"
|
|
33
|
+
|
|
34
|
+
export interface Message {
|
|
35
|
+
role: MessageRole
|
|
36
|
+
/** OpenCode's message id, when the message came from a session. */
|
|
37
|
+
id?: string
|
|
38
|
+
/** Session-global index, matching the `#N` references in summaries. */
|
|
39
|
+
index?: number
|
|
40
|
+
content?: string | ContentPart[]
|
|
41
|
+
/** Present on tool results. */
|
|
42
|
+
toolName?: string
|
|
43
|
+
/** Present on bash execution entries. */
|
|
44
|
+
command?: string
|
|
45
|
+
output?: string
|
|
46
|
+
exitCode?: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Pi persists `{ id, message }` entries; only the inner message is used here. */
|
|
50
|
+
export interface SessionMessageEntry {
|
|
51
|
+
id: string
|
|
52
|
+
message: Message
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface SectionData {
|
|
2
|
+
sessionGoal: string[];
|
|
3
|
+
outstandingContext: string[];
|
|
4
|
+
filesAndChanges: string[];
|
|
5
|
+
commits: string[];
|
|
6
|
+
userPreferences: string[];
|
|
7
|
+
/** Populated only when settings.trackCommands is non-empty -- see
|
|
8
|
+
* core/settings.ts. Empty array otherwise, same as any other section
|
|
9
|
+
* with nothing to report. */
|
|
10
|
+
trackedCommands: string[];
|
|
11
|
+
briefTranscript: string;
|
|
12
|
+
}
|
package/src/vcc/types.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Message } from "./pi-types";
|
|
2
|
+
|
|
3
|
+
export type CompactionReason = "manual" | "threshold" | "overflow";
|
|
4
|
+
|
|
5
|
+
export interface FileOps {
|
|
6
|
+
readFiles?: string[];
|
|
7
|
+
modifiedFiles?: string[];
|
|
8
|
+
createdFiles?: string[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type NormalizedBlock =
|
|
12
|
+
| { kind: "user"; text: string; sourceIndex?: number }
|
|
13
|
+
| { kind: "assistant"; text: string; sourceIndex?: number }
|
|
14
|
+
| { kind: "tool_call"; name: string; args: Record<string, unknown>; sourceIndex?: number }
|
|
15
|
+
| { kind: "tool_result"; name: string; text: string; sourceIndex?: number }
|
|
16
|
+
| { kind: "bash"; command: string; output: string; exitCode: number | undefined; sourceIndex?: number };
|