@mortz.prk/opencode-auto-mode 1.0.1 → 1.1.1
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/CONTRIBUTING.md +234 -0
- package/README.llm.md +314 -0
- package/README.md +175 -166
- package/dist/gui-experimental.cjs +1 -0
- package/package.json +27 -16
- package/src/audit.ts +136 -38
- package/src/classifier.ts +45 -37
- package/src/code-mode.ts +55 -0
- package/src/config.ts +104 -98
- package/src/debug-events.ts +223 -0
- package/src/decision.ts +72 -0
- package/src/errors.ts +39 -0
- package/src/gates.ts +16 -0
- package/src/global-options.ts +85 -0
- package/src/gui-state.ts +269 -0
- package/src/gui.tsx +78 -0
- package/src/index.ts +510 -131
- package/src/mode.ts +237 -0
- package/src/reviewer.ts +71 -85
- package/src/rpc.ts +127 -0
- package/src/score/files.ts +49 -0
- package/src/score/history.ts +141 -0
- package/src/score/index.ts +37 -0
- package/src/score/limits.ts +24 -0
- package/src/score/shell-argv.ts +74 -0
- package/src/score/shell-baseline.ts +113 -0
- package/src/score/shell-safety.ts +54 -0
- package/src/score/shell.ts +30 -0
- package/src/score/types.ts +14 -0
- package/src/score.ts +9 -0
- package/src/shell-policy.ts +1979 -0
- package/src/shell.ts +508 -225
- package/src/status.ts +85 -0
- package/src/tiers.ts +286 -40
- package/src/timeout.ts +30 -0
- package/src/tui.tsx +44 -34
- package/src/user-files.ts +75 -0
- package/src/worktree.ts +30 -0
- package/src/decisions.ts +0 -213
- package/src/logger.ts +0 -26
- package/src/otlp.ts +0 -74
- package/src/policy.ts +0 -36
- package/src/state.ts +0 -135
- package/src/storage.ts +0 -26
- package/src/traces.ts +0 -84
- package/src/transcript.ts +0 -208
- package/src/tui-state.ts +0 -7
package/src/transcript.ts
DELETED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
const MAX_USERS = 10;
|
|
2
|
-
const MAX_TOOLS = 20;
|
|
3
|
-
const MAX_USER_CHARS = 8_000;
|
|
4
|
-
const MAX_ENTRY_CHARS = 500;
|
|
5
|
-
const MAX_TRANSCRIPT_BYTES = 12_000;
|
|
6
|
-
|
|
7
|
-
interface ToolEntry {
|
|
8
|
-
name: string;
|
|
9
|
-
input: unknown;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface Entry {
|
|
13
|
-
role: "user" | "tool";
|
|
14
|
-
text: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function truncate(value: string, max: number): string {
|
|
18
|
-
return value.length > max ? `${value.slice(0, max - 1)}…` : value;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
|
22
|
-
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
23
|
-
return value as Record<string, unknown>;
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function userText(message: unknown): string | undefined {
|
|
29
|
-
const record = asRecord(message);
|
|
30
|
-
if (record?.["type"] !== "user") return undefined;
|
|
31
|
-
const text = record["text"];
|
|
32
|
-
return typeof text === "string" && text.trim() ? text.trim() : undefined;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function assistantTools(message: unknown): ToolEntry[] {
|
|
36
|
-
const record = asRecord(message);
|
|
37
|
-
if (record?.["type"] !== "assistant") return [];
|
|
38
|
-
const content = record["content"];
|
|
39
|
-
if (!Array.isArray(content)) return [];
|
|
40
|
-
const tools: ToolEntry[] = [];
|
|
41
|
-
for (const part of content) {
|
|
42
|
-
const entry = asRecord(part);
|
|
43
|
-
if (entry?.["type"] !== "tool") continue;
|
|
44
|
-
const name = entry["name"];
|
|
45
|
-
if (typeof name !== "string" || !name) continue;
|
|
46
|
-
tools.push({ name, input: entry["state"] });
|
|
47
|
-
}
|
|
48
|
-
return tools;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function formatTool(tool: ToolEntry): { text: string; truncated: boolean } {
|
|
52
|
-
let detail = "";
|
|
53
|
-
let truncated = false;
|
|
54
|
-
try {
|
|
55
|
-
const state = asRecord(tool.input);
|
|
56
|
-
const input = state?.["input"] ?? tool.input;
|
|
57
|
-
const raw = typeof input === "string" ? input : JSON.stringify(input);
|
|
58
|
-
if (raw) {
|
|
59
|
-
truncated = raw.length > MAX_ENTRY_CHARS;
|
|
60
|
-
detail = truncate(raw, MAX_ENTRY_CHARS);
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
detail = "";
|
|
64
|
-
}
|
|
65
|
-
return { text: detail ? `${tool.name} ${detail}` : tool.name, truncated };
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function escapedSize(value: string): number {
|
|
69
|
-
return Buffer.byteLength(JSON.stringify(value));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function prefixByEscapedBytes(characters: string[], maxBytes: number): string {
|
|
73
|
-
let low = 0;
|
|
74
|
-
let high = characters.length;
|
|
75
|
-
while (low < high) {
|
|
76
|
-
const middle = Math.ceil((low + high) / 2);
|
|
77
|
-
if (escapedSize(characters.slice(0, middle).join("")) <= maxBytes) low = middle;
|
|
78
|
-
else high = middle - 1;
|
|
79
|
-
}
|
|
80
|
-
return characters.slice(0, low).join("");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function suffixByEscapedBytes(characters: string[], maxBytes: number): string {
|
|
84
|
-
let low = 0;
|
|
85
|
-
let high = characters.length;
|
|
86
|
-
while (low < high) {
|
|
87
|
-
const middle = Math.ceil((low + high) / 2);
|
|
88
|
-
if (escapedSize(characters.slice(characters.length - middle).join("")) <= maxBytes) low = middle;
|
|
89
|
-
else high = middle - 1;
|
|
90
|
-
}
|
|
91
|
-
return characters.slice(characters.length - low).join("");
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Clips the head and tail so escaping cannot hide a trailing constraint. */
|
|
95
|
-
function clipEscaped(value: string, maxBytes: number): string {
|
|
96
|
-
if (escapedSize(value) <= maxBytes) return value;
|
|
97
|
-
const characters = Array.from(value);
|
|
98
|
-
const marker = "…";
|
|
99
|
-
const budget = Math.max(0, maxBytes - escapedSize(marker));
|
|
100
|
-
const headBudget = Math.floor(budget * 0.6);
|
|
101
|
-
return `${prefixByEscapedBytes(characters, headBudget)}${marker}${suffixByEscapedBytes(characters, budget - headBudget)}`;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function clipChars(value: string, max: number): string {
|
|
105
|
-
if (value.length <= max) return value;
|
|
106
|
-
const characters = Array.from(value);
|
|
107
|
-
const head = Math.floor(max * 0.6);
|
|
108
|
-
return `${characters.slice(0, head).join("")}…${characters.slice(characters.length - (max - head)).join("")}`;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
interface TranscriptData {
|
|
112
|
-
transcript: string;
|
|
113
|
-
userEvidence: boolean;
|
|
114
|
-
missingUserText: boolean;
|
|
115
|
-
notes: string[];
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function renderTranscript(entries: Entry[], toolTruncated: boolean, userClipped: boolean): { transcript: string; notes: string[] } {
|
|
119
|
-
if (!entries.some((entry) => entry.role === "user")) return { transcript: "(no prior user messages in this session)", notes: [] };
|
|
120
|
-
let users = 0;
|
|
121
|
-
let tools = 0;
|
|
122
|
-
const recent = entries.slice().reverse().filter((entry) => entry.role === "user" ? ++users <= MAX_USERS : ++tools <= MAX_TOOLS).reverse();
|
|
123
|
-
const notes: string[] = [];
|
|
124
|
-
if (recent.length < entries.length) notes.push("older messages omitted");
|
|
125
|
-
if (toolTruncated) notes.push("some tool details truncated");
|
|
126
|
-
if (userClipped) notes.push("latest user message truncated");
|
|
127
|
-
const render = () => `User messages and prior tool calls (chronological):\n${JSON.stringify(recent)}` +
|
|
128
|
-
(notes.length ? `\n… partial session history (${notes.join("; ")})` : "");
|
|
129
|
-
|
|
130
|
-
while (Buffer.byteLength(render()) > MAX_TRANSCRIPT_BYTES && recent.length > 1) {
|
|
131
|
-
// Keep at least the latest user message.
|
|
132
|
-
const onlyOneUser = recent.filter((entry) => entry.role === "user").length <= 1;
|
|
133
|
-
const index = onlyOneUser && recent[0].role === "user" ? recent.findIndex((entry) => entry.role === "tool") : 0;
|
|
134
|
-
if (index < 0) break;
|
|
135
|
-
recent.splice(index, 1);
|
|
136
|
-
if (!notes.includes("older messages omitted")) notes.push("older messages omitted");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const latestUser = [...recent].reverse().find((entry) => entry.role === "user");
|
|
140
|
-
for (let attempt = 0; latestUser !== undefined && Buffer.byteLength(render()) > MAX_TRANSCRIPT_BYTES && attempt < 10; attempt++) {
|
|
141
|
-
if (!notes.includes("latest user message truncated")) notes.push("latest user message truncated");
|
|
142
|
-
const excess = Buffer.byteLength(render()) - MAX_TRANSCRIPT_BYTES;
|
|
143
|
-
latestUser.text = clipEscaped(latestUser.text, Math.max(128, escapedSize(latestUser.text) - excess - 64));
|
|
144
|
-
}
|
|
145
|
-
return { transcript: render(), notes };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function buildTranscript(messages: unknown[]): TranscriptData {
|
|
149
|
-
const entries: Entry[] = [];
|
|
150
|
-
let userEvidence = false;
|
|
151
|
-
let missingUserText = false;
|
|
152
|
-
let userClipped = false;
|
|
153
|
-
let toolTruncated = false;
|
|
154
|
-
for (const message of messages) {
|
|
155
|
-
const record = asRecord(message);
|
|
156
|
-
if (record?.type === "user") {
|
|
157
|
-
const text = userText(message);
|
|
158
|
-
if (text === undefined) {
|
|
159
|
-
missingUserText = true;
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
userEvidence = true;
|
|
163
|
-
const clipped = clipChars(text, MAX_USER_CHARS);
|
|
164
|
-
if (clipped !== text) userClipped = true;
|
|
165
|
-
entries.push({ role: "user", text: clipped });
|
|
166
|
-
}
|
|
167
|
-
for (const tool of assistantTools(message)) {
|
|
168
|
-
const formatted = formatTool(tool);
|
|
169
|
-
if (formatted.truncated) toolTruncated = true;
|
|
170
|
-
entries.push({ role: "tool", text: formatted.text });
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const rendered = renderTranscript(entries, toolTruncated, userClipped);
|
|
174
|
-
return { transcript: rendered.transcript, userEvidence, missingUserText, notes: rendered.notes };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export interface ReviewContext { transcript: string; complete: boolean; reason?: string; notes?: string[] }
|
|
178
|
-
|
|
179
|
-
export function extractReviewContext(messages: unknown[]): ReviewContext {
|
|
180
|
-
const attachment = messages.some((message) => {
|
|
181
|
-
const record = asRecord(message);
|
|
182
|
-
if (record?.type !== "user") return false;
|
|
183
|
-
// These attachments carry evidence that the text-only reviewer cannot see.
|
|
184
|
-
return ["files", "skills", "agents"].some((key) =>
|
|
185
|
-
Array.isArray(record[key]) && record[key].length > 0);
|
|
186
|
-
});
|
|
187
|
-
const data = buildTranscript(messages);
|
|
188
|
-
if (attachment)
|
|
189
|
-
return {
|
|
190
|
-
transcript: data.transcript, complete: false, notes: data.notes,
|
|
191
|
-
reason: "User authorization includes attachments that cannot be reviewed automatically",
|
|
192
|
-
};
|
|
193
|
-
if (data.missingUserText)
|
|
194
|
-
return {
|
|
195
|
-
transcript: data.transcript, complete: false, notes: data.notes,
|
|
196
|
-
reason: "A user message has no text that can be reviewed",
|
|
197
|
-
};
|
|
198
|
-
if (!data.userEvidence)
|
|
199
|
-
return {
|
|
200
|
-
transcript: data.transcript, complete: false, notes: data.notes,
|
|
201
|
-
reason: "No user authorization message is available",
|
|
202
|
-
};
|
|
203
|
-
return { transcript: data.transcript, complete: true, notes: data.notes };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
export function extractTranscript(messages: unknown[]): string {
|
|
207
|
-
return buildTranscript(messages).transcript;
|
|
208
|
-
}
|
package/src/tui-state.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import type { DecisionRecord } from "./state.js";
|
|
2
|
-
|
|
3
|
-
export function isReviewing(records: Record<string, DecisionRecord>, sessionID: string | undefined, now = Date.now()): boolean {
|
|
4
|
-
if (!sessionID) return false;
|
|
5
|
-
return Object.values(records).some((record) => record.sessionID === sessionID &&
|
|
6
|
-
record.decision === "reviewing" && now - record.at >= 0 && now - record.at < 90_000);
|
|
7
|
-
}
|