opencode-clm 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.
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Model-facing protocol text and user-facing status.
3
+ *
4
+ * The protocol section is written for this package from the behaviour of
5
+ * src/context-document.ts (file layout, what each kind of change does, what is refused).
6
+ * Its scope follows pi-clm src/presentation.ts (MIT, Copyright 2026 Emanuel Casco): it
7
+ * states the editing protocol only; context-management strategy belongs in a steering
8
+ * document.
9
+ */
10
+
11
+ import { budgetSummaryLine, formatTokens, type BudgetReading } from "./budget.ts";
12
+ import type { EditGate } from "./policy.ts";
13
+ import { steeringStatusLine, type SteeringDocument } from "./steering.ts";
14
+
15
+ /** System-prompt section that teaches the mirror protocol. */
16
+ export function systemGuidance(path: string, budget: number | undefined): string {
17
+ const limit = budget
18
+ ? `\nThe configured limit is ${formatTokens(budget)} tokens; [CLM BUDGET] notes carry the running estimate.`
19
+ : "";
20
+ return `## Editable context
21
+
22
+ \`${path}\` holds this conversation as text: every message after the first user message.
23
+ The plugin regenerates it before each model request. The system prompt and the first user
24
+ message are not in it. When your step ends, the file's content takes the place of those
25
+ messages on your next request; OpenCode's saved session keeps the originals.${limit}
26
+
27
+ Layout: line 1 is \`[[LIVE_CONTEXT ...]]\` and names the revision and the document id.
28
+ Each message follows as a block: a header line
29
+ \`[[CTX_TURN document=<id> index=<n> role=<role> id=<block id> protected=<true|false>]]\`,
30
+ then the message text. \`grep -n '^\\[\\[CTX_TURN' ${path}\` prints every header with its
31
+ line number.
32
+
33
+ Effect of each change:
34
+ - Header and text deleted, or text emptied: the message is dropped.
35
+ - New text under a tool result header: it replaces that output; the tool call stays.
36
+ - New text under an assistant header: the reply becomes that text with no tool calls, and
37
+ the tool results it called become plain notes.
38
+ - Blocks marked \`protected=true\` come back unchanged if you alter them; a deleted one
39
+ returns at the end of the file.
40
+ - A block whose header you copied with \`id=new-NAME\` (NAME: letters, digits, hyphens;
41
+ unique) and a role label such as \`notes\` is inserted as a note where it stands. Notes are
42
+ context, with no system-prompt authority.
43
+ - Text above the first header is kept as a note at the top.
44
+ - A file with no headers at all, only text: everything becomes one notes block.
45
+
46
+ Refused outright (the context stays as it was):
47
+ - a line 1 that no longer parses or names a different document;
48
+ - a duplicated block id, an id absent from the current headers that lacks the \`new-\`
49
+ prefix, or a malformed header carrying the current document id;
50
+ - growth beyond what the size gate allows.
51
+
52
+ Tools: change the file through the bash tool (python3, sed -i). The edit or write tools
53
+ keep a copy that goes stale when the file is regenerated, so leave them off this file.
54
+
55
+ Receipts: a bash command that writes the file returns a verdict (accepted, or the reason
56
+ it is not); a [CLM] note on the next request reports what was applied. After a refusal,
57
+ correct the file and write it again.
58
+
59
+ Example: put a short note in place of block 4's text. Headers quoted inside message text
60
+ can look like real ones, so the script matches on the document id from line 1:
61
+
62
+ \`\`\`bash
63
+ python3 - <<'PY'
64
+ import re
65
+ path = "${path}"
66
+ text = open(path).read()
67
+ doc = re.match(r"\\[\\[LIVE_CONTEXT [^\\n]*?document=([0-9a-f]+)", text).group(1)
68
+ header = re.compile(r"^\\[\\[CTX_TURN document=" + doc + r" index=(\\d+) [^\\n]*$", re.M)
69
+ heads = list(header.finditer(text))
70
+ pos = next(i for i, h in enumerate(heads) if h.group(1) == "4")
71
+ stop = heads[pos + 1].start() if pos + 1 < len(heads) else len(text)
72
+ text = text[:heads[pos].end()] + "\\n[note: what block 4 established]\\n\\n" + text[stop:]
73
+ open(path, "w").write(text)
74
+ PY
75
+ \`\`\``;
76
+ }
77
+
78
+ /** Inputs for the `/clm` status text; block 8 fills these from the session. */
79
+ export interface ClmStatus {
80
+ sessionID: string;
81
+ mirrorPath: string;
82
+ revision: number;
83
+ accepted: number;
84
+ rejected: number;
85
+ gate: EditGate;
86
+ guard: "withhold" | "off";
87
+ /** Latest budget reading; undefined before the first request. */
88
+ reading?: BudgetReading;
89
+ /** Model context window in tokens, when known. */
90
+ modelWindow?: number;
91
+ /** The accepted revision now in effect, if any. */
92
+ checkpoint?: { revision: number; anchorCount: number; beforeEstimate: number; afterEstimate: number };
93
+ lastRequest?: { rawMessages: number; sentMessages: number; mirrorBlocks: number };
94
+ steering?: SteeringDocument;
95
+ }
96
+
97
+ /** One line for a toast. */
98
+ export function statusLine(status: ClmStatus): string {
99
+ const size = status.reading
100
+ ? ` · ${formatTokens(status.reading.estimated)} of ${formatTokens(status.reading.budget)} tok`
101
+ : "";
102
+ return `CLM r${status.revision} · ${status.accepted} accepted / ${status.rejected} rejected${size}`;
103
+ }
104
+
105
+ /** Multi-line status for `/clm`. */
106
+ export function statusText(status: ClmStatus): string {
107
+ const lines = [
108
+ `CLM status for session ${status.sessionID}`,
109
+ `mirror: ${status.mirrorPath}`,
110
+ `revision ${status.revision} · edits accepted ${status.accepted} · rejected ${status.rejected} · gate ${status.gate} · guard ${status.guard}`,
111
+ status.reading
112
+ ? `${budgetSummaryLine(status.reading)} · model window ${status.modelWindow ? formatTokens(status.modelWindow) : "unknown"}`
113
+ : "budget: unknown until the first request",
114
+ ];
115
+ const checkpoint = status.checkpoint;
116
+ lines.push(
117
+ checkpoint
118
+ ? `active revision ${checkpoint.revision}: covers ${checkpoint.anchorCount} raw messages; ~${formatTokens(checkpoint.beforeEstimate)}→${formatTokens(checkpoint.afterEstimate)} tokens when accepted`
119
+ : "no accepted edit: the model sees the raw history",
120
+ );
121
+ if (status.lastRequest) {
122
+ const request = status.lastRequest;
123
+ lines.push(`last request: ${request.rawMessages} raw messages → ${request.sentMessages} sent, ${request.mirrorBlocks} mirror blocks`);
124
+ }
125
+ lines.push(steeringStatusLine(status.steering));
126
+ return lines.join("\n");
127
+ }
@@ -0,0 +1,146 @@
1
+ // Adapted from pi-clm src/projection.ts (MIT, Copyright 2026 Emanuel Casco).
2
+ //
3
+ // A checkpoint records one accepted mirror edit: the flattened raw prefix it replaces
4
+ // (count + SHA-256 digest) and the projected messages that stand in for that prefix.
5
+ // Messages are the flat form `opencode.ts` produces from OpenCode's `{ info, parts }`
6
+ // history. `digestSourcePrefix` hashes a key-sorted JSON serialization, so a checkpoint saved
7
+ // by one process validates against the same history flattened by another.
8
+ //
9
+ // The anchor is prune-invariant. OpenCode's `compaction.prune` (opt-in) clears old tool
10
+ // outputs in place (`state.time.compacted`), exactly in the old prefix a checkpoint covers.
11
+ // The source digest therefore hashes each `toolResult` without its content, and the
12
+ // checkpoint also stores the `ocMessageID` of every flat source message. Revert removes
13
+ // whole messages (ids change) or truncates the parts of a kept message (its toolCall and
14
+ // toolResult disappear, so the digest changes); both still fail closed.
15
+ //
16
+ // pi-clm's `recoverProjectionFromRetryErrors` is not ported. Pi writes failed assistant
17
+ // responses to its session log but drops them from live state, so a resumed log hashes
18
+ // differently. OpenCode keeps the error on the assistant message (`info.error`), and the
19
+ // flattener drops failed assistant messages before digesting, both when the checkpoint is
20
+ // created and when it is applied. The mismatch that recovery repairs cannot occur.
21
+ import { digestMessages } from "./context-document.ts";
22
+ import type { ContextEditTrace, LiveContextMessage } from "./types.ts";
23
+
24
+ export const PROJECTION_PREFIX_MISMATCH_REASON =
25
+ "Raw context prefix no longer matches the projection checkpoint.";
26
+ export const PROJECTION_SOURCE_IDS_REASON =
27
+ "Raw context prefix message ids no longer match the projection checkpoint (revert or compaction).";
28
+ export const PROJECTION_SHORTER_REASON = "Raw context is shorter than the projection source prefix.";
29
+
30
+ export type EstimateUnit = "characters" | "tokens";
31
+
32
+ export interface ProjectionCheckpoint {
33
+ version: 1;
34
+ revision: number;
35
+ /** Number of flattened raw messages the projection replaces. */
36
+ sourceMessageCount: number;
37
+ /** `ocMessageID` of each of those messages ("" when absent); length is sourceMessageCount. */
38
+ sourceIds: string[];
39
+ /** `digestSourcePrefix` of those messages. */
40
+ sourceDigest: string;
41
+ projectedMessages: LiveContextMessage[];
42
+ beforeEstimate: number;
43
+ afterEstimate: number;
44
+ estimateUnit: EstimateUnit;
45
+ createdAt: string;
46
+ /** Provenance for the accepted edit that produced projectedMessages. */
47
+ editTrace?: ContextEditTrace;
48
+ }
49
+
50
+ export type ProjectionApplication =
51
+ | {
52
+ valid: true;
53
+ messages: LiveContextMessage[];
54
+ suffix: LiveContextMessage[];
55
+ }
56
+ | {
57
+ valid: false;
58
+ messages: LiveContextMessage[];
59
+ reason: string;
60
+ };
61
+
62
+ /** The OpenCode message id a flat message came from, or "" when it has none. */
63
+ export function sourceMessageId(message: LiveContextMessage): string {
64
+ return typeof message.ocMessageID === "string" ? message.ocMessageID : "";
65
+ }
66
+
67
+ /**
68
+ * Digest a raw source prefix in a form OpenCode's prune cannot change: every `toolResult`
69
+ * keeps only role, toolCallId, toolName, isError and ocMessageID. Content is dropped for
70
+ * every tool result, not only cleared ones: the output is intact when the checkpoint is
71
+ * created and may be cleared when it is validated.
72
+ */
73
+ export function digestSourcePrefix(messages: readonly LiveContextMessage[]): string {
74
+ return digestMessages(
75
+ messages.map((message) => {
76
+ if (message.role !== "toolResult") return message;
77
+ return {
78
+ role: message.role,
79
+ toolCallId: message.toolCallId,
80
+ toolName: message.toolName,
81
+ isError: message.isError,
82
+ ocMessageID: message.ocMessageID,
83
+ };
84
+ }),
85
+ );
86
+ }
87
+
88
+ export function createProjectionCheckpoint(options: {
89
+ revision: number;
90
+ sourceMessages: readonly LiveContextMessage[];
91
+ projectedMessages: readonly LiveContextMessage[];
92
+ beforeEstimate: number;
93
+ afterEstimate: number;
94
+ estimateUnit: EstimateUnit;
95
+ createdAt?: string;
96
+ editTrace?: ContextEditTrace;
97
+ }): ProjectionCheckpoint {
98
+ const checkpoint: ProjectionCheckpoint = {
99
+ version: 1,
100
+ revision: options.revision,
101
+ sourceMessageCount: options.sourceMessages.length,
102
+ sourceIds: options.sourceMessages.map(sourceMessageId),
103
+ sourceDigest: digestSourcePrefix(options.sourceMessages),
104
+ projectedMessages: [...options.projectedMessages],
105
+ beforeEstimate: options.beforeEstimate,
106
+ afterEstimate: options.afterEstimate,
107
+ estimateUnit: options.estimateUnit,
108
+ createdAt: options.createdAt ?? new Date().toISOString(),
109
+ };
110
+ if (options.editTrace) checkpoint.editTrace = options.editTrace;
111
+
112
+ // Fail at creation rather than when state.json is written.
113
+ JSON.stringify(checkpoint);
114
+ return checkpoint;
115
+ }
116
+
117
+ /**
118
+ * Replace the checkpoint's raw source prefix and keep every raw message appended after
119
+ * it. A mismatch fails closed and returns a copy of the untouched raw context.
120
+ */
121
+ export function applyProjection(
122
+ rawMessages: readonly LiveContextMessage[],
123
+ checkpoint: ProjectionCheckpoint | undefined,
124
+ ): ProjectionApplication {
125
+ if (!checkpoint) return { valid: true, messages: [...rawMessages], suffix: [] };
126
+ if (checkpoint.version !== 1) {
127
+ return { valid: false, messages: [...rawMessages], reason: `Unsupported projection version: ${checkpoint.version}.` };
128
+ }
129
+ if (rawMessages.length < checkpoint.sourceMessageCount) {
130
+ return { valid: false, messages: [...rawMessages], reason: PROJECTION_SHORTER_REASON };
131
+ }
132
+ const prefix = rawMessages.slice(0, checkpoint.sourceMessageCount);
133
+ const ids = checkpoint.sourceIds;
134
+ if (
135
+ !Array.isArray(ids) ||
136
+ ids.length !== prefix.length ||
137
+ prefix.some((message, index) => sourceMessageId(message) !== ids[index])
138
+ ) {
139
+ return { valid: false, messages: [...rawMessages], reason: PROJECTION_SOURCE_IDS_REASON };
140
+ }
141
+ if (digestSourcePrefix(prefix) !== checkpoint.sourceDigest) {
142
+ return { valid: false, messages: [...rawMessages], reason: PROJECTION_PREFIX_MISMATCH_REASON };
143
+ }
144
+ const suffix = rawMessages.slice(checkpoint.sourceMessageCount);
145
+ return { valid: true, messages: [...checkpoint.projectedMessages, ...suffix], suffix };
146
+ }
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Settings: plugin options (the `plugin` entry in opencode.json) override environment
3
+ * variables, which override the defaults. Parsed once when the plugin loads; an invalid
4
+ * value throws an error naming the setting, so OpenCode reports the plugin as failed
5
+ * instead of running with a guess.
6
+ *
7
+ * Setting set adapted from pi-clm src/settings.ts (MIT, Copyright 2026 Emanuel Casco).
8
+ * pi-clm's per-session overrides (`/clm config` panel) are not ported: OpenCode commands
9
+ * cannot open a panel.
10
+ */
11
+ import { join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ import { DEFAULT_BUDGET_POLICY, resolveBudgetPolicy, type BudgetPolicyConfig } from "./budget.ts";
15
+ import { parseObservationCap, resolveObservationCap, type ObservationCapPolicy } from "./observation.ts";
16
+ import { toEditGate, type EditGate } from "./policy.ts";
17
+
18
+ export interface ClmSettings {
19
+ enabled: boolean;
20
+ budget: BudgetPolicyConfig;
21
+ gate: EditGate;
22
+ guard: "withhold" | "off";
23
+ /** Per-result cap on tool output in the effective context; `maxCharacters` undefined = off. */
24
+ observationCap: ObservationCapPolicy;
25
+ /** Parent directory of the per-session mirror directories. */
26
+ mirrorDir: string;
27
+ /** Steering document path; undefined means protocol only. */
28
+ steeringPath?: string;
29
+ /** `/clm-compact` template path; undefined means the built-in prompt. */
30
+ compactPromptPath?: string;
31
+ /** Multiplier on the chars/4 token estimate (dense content: 1.5–2). */
32
+ estimateFactor: number;
33
+ /** Render assistant reasoning in the mirror. */
34
+ reasoning: boolean;
35
+ /** Register the clm-context skill directory with OpenCode. */
36
+ skill: boolean;
37
+ /** Register the /clm and /clm-compact commands. */
38
+ commands: boolean;
39
+ /** Write every transformed request to `<session dir>/requests/nN.json` (debugging). */
40
+ dumpRequests: boolean;
41
+ }
42
+
43
+ export const PACKAGE_DIR = resolve(fileURLToPath(new URL("..", import.meta.url)));
44
+ export const HOUSE_STEERING = join(PACKAGE_DIR, "steering", "house-brief.md");
45
+ export const SKILLS_DIR = join(PACKAGE_DIR, "skills");
46
+
47
+ type Env = Record<string, string | undefined>;
48
+
49
+ const OFF_WORDS = ["none", "off"];
50
+
51
+ function pick(options: Record<string, unknown>, key: string, env: Env, envKey: string): unknown {
52
+ if (options[key] !== undefined) return options[key];
53
+ const raw = env[envKey]?.trim();
54
+ return raw === undefined || raw === "" ? undefined : raw;
55
+ }
56
+
57
+ /** `32000`, `32k`, `1.5m`, `32_000`, `32,000`. */
58
+ export function parseTokens(value: unknown, name: string): number {
59
+ const text = String(value).trim().toLowerCase().replace(/[_,]/g, "");
60
+ const match = /^(\d+(?:\.\d+)?)(k|m)?$/.exec(text);
61
+ if (!match) throw new Error(`${name} must be a number of tokens (e.g. 32000, 32k), got ${JSON.stringify(value)}`);
62
+ const scale = match[2] === "k" ? 1_000 : match[2] === "m" ? 1_000_000 : 1;
63
+ return Math.round(Number(match[1]) * scale);
64
+ }
65
+
66
+ function parseFlag(value: unknown, name: string): boolean {
67
+ if (typeof value === "boolean") return value;
68
+ const key = String(value).trim().toLowerCase();
69
+ if (["1", "true", "on", "yes"].includes(key)) return true;
70
+ if (["0", "false", "off", "no"].includes(key)) return false;
71
+ throw new Error(`${name} must be true or false, got ${JSON.stringify(value)}`);
72
+ }
73
+
74
+ function parseFractions(value: unknown, name: string): number[] | "off" {
75
+ if (value === false) return "off";
76
+ const parts: unknown[] = Array.isArray(value)
77
+ ? value
78
+ : String(value).trim().toLowerCase() === "off" || String(value).trim().toLowerCase() === "none"
79
+ ? []
80
+ : String(value).split(/[\s,/]+/).filter(Boolean);
81
+ if (parts.length === 0) return "off";
82
+ const fractions = parts.map((part) => {
83
+ const number = Number(String(part).trim().replace(/%$/, ""));
84
+ return number > 1 ? number / 100 : number;
85
+ });
86
+ if (fractions.some((fraction) => !Number.isFinite(fraction))) {
87
+ throw new Error(`${name} must be fractions like 0.25,0.5,0.75 (or 25/50/75%), or off; got ${JSON.stringify(value)}`);
88
+ }
89
+ return [...new Set(fractions)];
90
+ }
91
+
92
+ function withName<T>(name: string, parse: () => T): T {
93
+ try {
94
+ return parse();
95
+ } catch (error) {
96
+ const message = error instanceof Error ? error.message : String(error);
97
+ throw new Error(message.startsWith(name) ? message : `${name}: ${message}`);
98
+ }
99
+ }
100
+
101
+ /** A path setting: off words mean unset; relative paths resolve against the project. */
102
+ function parsePath(value: unknown, directory: string, offWords: string[]): string | undefined {
103
+ const text = String(value).trim();
104
+ if (!text || offWords.includes(text.toLowerCase())) return undefined;
105
+ return resolve(directory, text);
106
+ }
107
+
108
+ export function resolveSettings(options: Record<string, unknown> = {}, env: Env = process.env, directory = process.cwd()): ClmSettings {
109
+ const enabledRaw = pick(options, "enabled", env, "CLM_ENABLED");
110
+ const budgetRaw = pick(options, "budget", env, "CLM_BUDGET");
111
+ const reserveRaw = pick(options, "reserve", env, "CLM_RESERVE");
112
+ const remindRaw = pick(options, "remindAt", env, "CLM_REMIND_AT");
113
+ const gateRaw = pick(options, "gate", env, "CLM_EDIT_GATE");
114
+ const guardRaw = pick(options, "guard", env, "CLM_OVERFLOW");
115
+ const capRaw = pick(options, "observationCap", env, "CLM_OBSERVATION_CAP");
116
+ const dirRaw = pick(options, "mirrorDir", env, "CLM_MIRROR_DIR");
117
+ const steeringRaw = pick(options, "steering", env, "CLM_STEERING");
118
+ const compactRaw = pick(options, "compactPrompt", env, "CLM_COMPACT_PROMPT");
119
+ const factorRaw = pick(options, "estimateFactor", env, "CLM_ESTIMATE_FACTOR");
120
+ const reasoningRaw = pick(options, "reasoning", env, "CLM_REASONING");
121
+ const dumpRaw = pick(options, "dumpRequests", env, "CLM_DUMP_REQUESTS");
122
+
123
+ const overrides: Partial<BudgetPolicyConfig> = {};
124
+ if (budgetRaw !== undefined) {
125
+ overrides.contextBudget = String(budgetRaw).trim().toLowerCase() === "window" ? undefined : parseTokens(budgetRaw, "budget");
126
+ if (overrides.contextBudget === 0) throw new Error(`budget must be a positive number of tokens or "window", got ${JSON.stringify(budgetRaw)}`);
127
+ }
128
+ if (reserveRaw !== undefined) overrides.reserve = parseTokens(reserveRaw, "reserve");
129
+ if (remindRaw !== undefined) {
130
+ const fractions = parseFractions(remindRaw, "remindAt");
131
+ if (fractions === "off") {
132
+ overrides.remindAtFractions = [];
133
+ overrides.remindAtReserve = false;
134
+ } else {
135
+ const outside = fractions.find((fraction) => fraction <= 0 || fraction >= 1);
136
+ if (outside !== undefined) {
137
+ throw new Error(`remindAt entries must lie strictly between 0 and 1 (or 0% and 100%), got ${JSON.stringify(remindRaw)}`);
138
+ }
139
+ overrides.remindAtFractions = fractions;
140
+ }
141
+ }
142
+ const budget = withName("budget", () => resolveBudgetPolicy({ ...DEFAULT_BUDGET_POLICY, ...overrides }));
143
+
144
+ let guard: ClmSettings["guard"] = "withhold";
145
+ if (guardRaw !== undefined) {
146
+ const text = String(guardRaw).trim().toLowerCase();
147
+ if (text === "withhold" || text === "on" || text === "true") guard = "withhold";
148
+ else if (text === "off" || text === "false") guard = "off";
149
+ else throw new Error(`guard must be withhold or off, got ${JSON.stringify(guardRaw)}`);
150
+ }
151
+
152
+ const observationCap = withName("observationCap", () =>
153
+ resolveObservationCap(capRaw === undefined || capRaw === false ? {} : parseObservationCap(String(capRaw), "observationCap")),
154
+ );
155
+
156
+ let steeringPath: string | undefined;
157
+ if (steeringRaw !== undefined) {
158
+ const text = String(steeringRaw).trim().toLowerCase();
159
+ steeringPath = ["house", "house-brief", "house-brief.md"].includes(text)
160
+ ? HOUSE_STEERING
161
+ : parsePath(steeringRaw, directory, OFF_WORDS);
162
+ }
163
+
164
+ const compactPromptPath = compactRaw === undefined ? undefined : parsePath(compactRaw, directory, ["default", ...OFF_WORDS]);
165
+
166
+ // Only a number or a numeric string: Number(true) and Number([2]) would pass silently.
167
+ const factorValid = factorRaw === undefined || typeof factorRaw === "number" ||
168
+ (typeof factorRaw === "string" && factorRaw.trim() !== "");
169
+ const estimateFactor = factorRaw === undefined ? 1 : factorValid ? Number(factorRaw) : Number.NaN;
170
+ if (!Number.isFinite(estimateFactor) || estimateFactor < 1 || estimateFactor > 4) {
171
+ throw new Error(`estimateFactor must be between 1 and 4, got ${JSON.stringify(factorRaw)}`);
172
+ }
173
+
174
+ return {
175
+ enabled: enabledRaw === undefined ? true : parseFlag(enabledRaw, "enabled"),
176
+ budget,
177
+ gate: gateRaw === undefined ? "fit" : withName("gate (CLM_EDIT_GATE)", () => toEditGate(gateRaw)),
178
+ guard,
179
+ observationCap,
180
+ mirrorDir: dirRaw === undefined ? join(directory, ".opencode", "clm") : resolve(directory, String(dirRaw)),
181
+ steeringPath,
182
+ compactPromptPath,
183
+ estimateFactor,
184
+ reasoning: reasoningRaw === undefined ? true : parseFlag(reasoningRaw, "reasoning"),
185
+ skill: options.skill === undefined ? true : parseFlag(options.skill, "skill"),
186
+ commands: options.commands === undefined ? true : parseFlag(options.commands, "commands"),
187
+ dumpRequests: dumpRaw === undefined ? false : parseFlag(dumpRaw, "dumpRequests"),
188
+ };
189
+ }