opencode-ultra 0.3.0 → 0.4.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/dist/config.d.ts CHANGED
@@ -34,6 +34,19 @@ declare const PluginConfigSchema: z.ZodObject<{
34
34
  model: z.ZodOptional<z.ZodString>;
35
35
  variant: z.ZodOptional<z.ZodString>;
36
36
  }, z.core.$loose>>>;
37
+ fragments: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
38
+ prompt_renderer: z.ZodOptional<z.ZodObject<{
39
+ default: z.ZodOptional<z.ZodEnum<{
40
+ markdown: "markdown";
41
+ xml: "xml";
42
+ json: "json";
43
+ }>>;
44
+ model_overrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
45
+ markdown: "markdown";
46
+ xml: "xml";
47
+ json: "json";
48
+ }>>>;
49
+ }, z.core.$strip>>;
37
50
  disabled_agents: z.ZodOptional<z.ZodArray<z.ZodString>>;
38
51
  disabled_hooks: z.ZodOptional<z.ZodArray<z.ZodString>>;
39
52
  disabled_tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -0,0 +1,9 @@
1
+ export type FragmentsConfig = Record<string, string[]>;
2
+ export declare function createFragmentInjector(ctx: {
3
+ directory: string;
4
+ }, internalSessions: Set<string>, fragments?: FragmentsConfig): (input: {
5
+ sessionID?: string;
6
+ agent?: string;
7
+ }, output: {
8
+ system: string[];
9
+ }) => Promise<void>;
@@ -0,0 +1,13 @@
1
+ export type PromptFormat = "markdown" | "xml" | "json";
2
+ export type PromptRendererConfig = {
3
+ default: PromptFormat;
4
+ model_overrides: Record<string, PromptFormat>;
5
+ };
6
+ export declare function resolvePromptFormat(inputModel: any, config?: Partial<PromptRendererConfig>): PromptFormat;
7
+ export declare function formatSystemSection(format: PromptFormat, content: string): string;
8
+ export declare function createPromptRendererHook(internalSessions: Set<string>, config?: Partial<PromptRendererConfig>): (input: {
9
+ sessionID?: string;
10
+ model?: unknown;
11
+ }, output: {
12
+ system: string[];
13
+ }) => void;
package/dist/index.js CHANGED
@@ -14396,6 +14396,11 @@ var CategoryConfigSchema = exports_external.object({
14396
14396
  var PluginConfigSchema = exports_external.object({
14397
14397
  agents: AgentOverridesSchema.optional(),
14398
14398
  categories: exports_external.record(exports_external.string(), CategoryConfigSchema).optional(),
14399
+ fragments: exports_external.record(exports_external.string(), exports_external.array(exports_external.string())).optional(),
14400
+ prompt_renderer: exports_external.object({
14401
+ default: exports_external.enum(["markdown", "xml", "json"]).optional(),
14402
+ model_overrides: exports_external.record(exports_external.string(), exports_external.enum(["markdown", "xml", "json"])).optional()
14403
+ }).optional(),
14399
14404
  disabled_agents: exports_external.array(exports_external.string()).optional(),
14400
14405
  disabled_hooks: exports_external.array(exports_external.string()).optional(),
14401
14406
  disabled_tools: exports_external.array(exports_external.string()).optional(),
@@ -14874,6 +14879,94 @@ function loadCodeStyle(projectDir) {
14874
14879
  return hit.content;
14875
14880
  }
14876
14881
 
14882
+ // src/hooks/fragment-injector.ts
14883
+ import * as fs3 from "fs";
14884
+ import * as path3 from "path";
14885
+ var cache2 = new Map;
14886
+ async function readCached(absPath) {
14887
+ try {
14888
+ const st = await fs3.promises.stat(absPath);
14889
+ if (!st.isFile())
14890
+ return null;
14891
+ const prev = cache2.get(absPath);
14892
+ if (prev && prev.mtimeMs === st.mtimeMs)
14893
+ return prev.content;
14894
+ const content = await fs3.promises.readFile(absPath, "utf-8");
14895
+ cache2.set(absPath, { mtimeMs: st.mtimeMs, content });
14896
+ return content;
14897
+ } catch (err) {
14898
+ if (err?.code !== "ENOENT") {
14899
+ log("fragment read failed", { path: absPath, error: String(err) });
14900
+ }
14901
+ return null;
14902
+ }
14903
+ }
14904
+ function createFragmentInjector(ctx, internalSessions, fragments) {
14905
+ return async (input, output) => {
14906
+ if (input.sessionID && internalSessions.has(input.sessionID))
14907
+ return;
14908
+ const agent = input.agent;
14909
+ if (!agent || !fragments)
14910
+ return;
14911
+ const fragmentPaths = fragments[agent];
14912
+ if (!Array.isArray(fragmentPaths) || fragmentPaths.length === 0)
14913
+ return;
14914
+ for (const rel of fragmentPaths) {
14915
+ const abs = path3.resolve(ctx.directory, rel);
14916
+ const content = await readCached(abs);
14917
+ if (content === null) {
14918
+ log("fragment file not found; skipping", { agent, path: rel });
14919
+ continue;
14920
+ }
14921
+ output.system.push(content);
14922
+ }
14923
+ };
14924
+ }
14925
+
14926
+ // src/hooks/prompt-renderer.ts
14927
+ function escapeXml(s) {
14928
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\"/g, "&quot;").replace(/'/g, "&apos;");
14929
+ }
14930
+ function resolvePromptFormat(inputModel, config2) {
14931
+ const def = config2?.default ?? "markdown";
14932
+ const overrides = config2?.model_overrides ?? {};
14933
+ const providerID = inputModel?.providerID;
14934
+ const modelID = inputModel?.modelID;
14935
+ const key = typeof providerID === "string" && typeof modelID === "string" ? `${providerID}/${modelID}` : undefined;
14936
+ if (key && overrides[key])
14937
+ return overrides[key];
14938
+ if (typeof modelID === "string" && overrides[modelID])
14939
+ return overrides[modelID];
14940
+ return def;
14941
+ }
14942
+ function formatSystemSection(format2, content) {
14943
+ if (format2 === "markdown")
14944
+ return content;
14945
+ if (format2 === "xml") {
14946
+ return `<section name="Rules">${escapeXml(content)}</section>`;
14947
+ }
14948
+ if (format2 === "json") {
14949
+ return JSON.stringify({ section: "Rules", content });
14950
+ }
14951
+ return content;
14952
+ }
14953
+ function createPromptRendererHook(internalSessions, config2) {
14954
+ return (input, output) => {
14955
+ if (input.sessionID && internalSessions.has(input.sessionID))
14956
+ return;
14957
+ if (!Array.isArray(output.system) || output.system.length === 0)
14958
+ return;
14959
+ const format2 = resolvePromptFormat(input.model, config2);
14960
+ if (format2 === "markdown")
14961
+ return;
14962
+ try {
14963
+ output.system = output.system.map((s) => formatSystemSection(format2, s));
14964
+ } catch (err) {
14965
+ log("prompt renderer failed", { error: String(err) });
14966
+ }
14967
+ };
14968
+ }
14969
+
14877
14970
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
14878
14971
  var exports_external2 = {};
14879
14972
  __export(exports_external2, {
@@ -15603,15 +15696,15 @@ function mergeDefs2(...defs) {
15603
15696
  function cloneDef2(schema) {
15604
15697
  return mergeDefs2(schema._zod.def);
15605
15698
  }
15606
- function getElementAtPath2(obj, path3) {
15607
- if (!path3)
15699
+ function getElementAtPath2(obj, path4) {
15700
+ if (!path4)
15608
15701
  return obj;
15609
- return path3.reduce((acc, key) => acc?.[key], obj);
15702
+ return path4.reduce((acc, key) => acc?.[key], obj);
15610
15703
  }
15611
15704
  function promiseAllObject2(promisesObj) {
15612
15705
  const keys = Object.keys(promisesObj);
15613
- const promises = keys.map((key) => promisesObj[key]);
15614
- return Promise.all(promises).then((results) => {
15706
+ const promises2 = keys.map((key) => promisesObj[key]);
15707
+ return Promise.all(promises2).then((results) => {
15615
15708
  const resolvedObj = {};
15616
15709
  for (let i = 0;i < keys.length; i++) {
15617
15710
  resolvedObj[keys[i]] = results[i];
@@ -15965,11 +16058,11 @@ function aborted2(x, startIndex = 0) {
15965
16058
  }
15966
16059
  return false;
15967
16060
  }
15968
- function prefixIssues2(path3, issues) {
16061
+ function prefixIssues2(path4, issues) {
15969
16062
  return issues.map((iss) => {
15970
16063
  var _a2;
15971
16064
  (_a2 = iss).path ?? (_a2.path = []);
15972
- iss.path.unshift(path3);
16065
+ iss.path.unshift(path4);
15973
16066
  return iss;
15974
16067
  });
15975
16068
  }
@@ -16137,7 +16230,7 @@ function treeifyError2(error48, _mapper) {
16137
16230
  return issue3.message;
16138
16231
  };
16139
16232
  const result = { errors: [] };
16140
- const processError = (error49, path3 = []) => {
16233
+ const processError = (error49, path4 = []) => {
16141
16234
  var _a2, _b;
16142
16235
  for (const issue3 of error49.issues) {
16143
16236
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -16147,7 +16240,7 @@ function treeifyError2(error48, _mapper) {
16147
16240
  } else if (issue3.code === "invalid_element") {
16148
16241
  processError({ issues: issue3.issues }, issue3.path);
16149
16242
  } else {
16150
- const fullpath = [...path3, ...issue3.path];
16243
+ const fullpath = [...path4, ...issue3.path];
16151
16244
  if (fullpath.length === 0) {
16152
16245
  result.errors.push(mapper(issue3));
16153
16246
  continue;
@@ -16179,8 +16272,8 @@ function treeifyError2(error48, _mapper) {
16179
16272
  }
16180
16273
  function toDotPath2(_path) {
16181
16274
  const segs = [];
16182
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16183
- for (const seg of path3) {
16275
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16276
+ for (const seg of path4) {
16184
16277
  if (typeof seg === "number")
16185
16278
  segs.push(`[${seg}]`);
16186
16279
  else if (typeof seg === "symbol")
@@ -27519,6 +27612,243 @@ Original task:
27519
27612
  ${original}`;
27520
27613
  }
27521
27614
 
27615
+ // src/tools/batch-read.ts
27616
+ import * as fs4 from "fs";
27617
+ import * as path4 from "path";
27618
+ var MAX_PATHS = 20;
27619
+ function resolvePath(projectDir, inputPath) {
27620
+ return path4.isAbsolute(inputPath) ? inputPath : path4.join(projectDir, inputPath);
27621
+ }
27622
+ function isProbablyBinary(buf) {
27623
+ const sample = buf.subarray(0, Math.min(buf.length, 8192));
27624
+ for (const b of sample) {
27625
+ if (b === 0)
27626
+ return true;
27627
+ }
27628
+ return false;
27629
+ }
27630
+ async function readOne(projectDir, inputPath) {
27631
+ const abs = resolvePath(projectDir, inputPath);
27632
+ try {
27633
+ const stat = await fs4.promises.stat(abs);
27634
+ if (!stat.isFile()) {
27635
+ return `=== ${inputPath} ===
27636
+ [File not found]
27637
+ `;
27638
+ }
27639
+ const buf = await fs4.promises.readFile(abs);
27640
+ if (isProbablyBinary(buf)) {
27641
+ return `=== ${inputPath} ===
27642
+ [Binary file]
27643
+ `;
27644
+ }
27645
+ const content = buf.toString("utf-8");
27646
+ return `=== ${inputPath} ===
27647
+ ${content}
27648
+ `;
27649
+ } catch (err) {
27650
+ if (err?.code === "ENOENT") {
27651
+ return `=== ${inputPath} ===
27652
+ [File not found]
27653
+ `;
27654
+ }
27655
+ log("batch_read failed to read file", { path: abs, error: String(err) });
27656
+ return `=== ${inputPath} ===
27657
+ [File not found]
27658
+ `;
27659
+ }
27660
+ }
27661
+ function createBatchReadTool(ctx) {
27662
+ return tool({
27663
+ description: "Read multiple files in parallel and concatenate contents.",
27664
+ args: {
27665
+ paths: tool.schema.array(tool.schema.string()).describe("File paths (max 20).")
27666
+ },
27667
+ execute: async ({ paths }) => {
27668
+ if (!Array.isArray(paths)) {
27669
+ return "Error: paths must be an array of strings";
27670
+ }
27671
+ if (paths.length > MAX_PATHS) {
27672
+ return `Error: maximum ${MAX_PATHS} paths allowed`;
27673
+ }
27674
+ const results = await Promise.all(paths.map((p) => readOne(ctx.directory, p)));
27675
+ return results.join("");
27676
+ }
27677
+ });
27678
+ }
27679
+
27680
+ // src/tools/continuity-ledger.ts
27681
+ import * as fs5 from "fs";
27682
+ import * as path5 from "path";
27683
+ function normalizeLedgerName(name) {
27684
+ const trimmed = (name ?? "").trim().replace(/\.md$/i, "");
27685
+ const spaced = trimmed.replace(/\s+/g, "-");
27686
+ const cleaned = spaced.replace(/[^A-Za-z0-9._-]/g, "");
27687
+ return cleaned.length > 0 ? cleaned : "ledger";
27688
+ }
27689
+ function ledgerDir(projectDir) {
27690
+ return path5.join(projectDir, ".opencode", "ledgers");
27691
+ }
27692
+ function ledgerPath(projectDir, name) {
27693
+ return path5.join(ledgerDir(projectDir), `${normalizeLedgerName(name)}.md`);
27694
+ }
27695
+ function createLedgerSaveTool(ctx) {
27696
+ return tool({
27697
+ description: "Save a continuity ledger markdown document to .opencode/ledgers/.",
27698
+ args: {
27699
+ name: tool.schema.string().describe('Ledger name (e.g. "auth-refactor").'),
27700
+ content: tool.schema.string().describe("Markdown content.")
27701
+ },
27702
+ execute: async ({ name, content }) => {
27703
+ const dir = ledgerDir(ctx.directory);
27704
+ const outPath = ledgerPath(ctx.directory, name);
27705
+ try {
27706
+ await fs5.promises.mkdir(dir, { recursive: true });
27707
+ await fs5.promises.writeFile(outPath, content, "utf-8");
27708
+ return `Saved to .opencode/ledgers/${path5.basename(outPath)}`;
27709
+ } catch (err) {
27710
+ log("ledger_save failed", { error: String(err) });
27711
+ return `Error: failed to save ledger`;
27712
+ }
27713
+ }
27714
+ });
27715
+ }
27716
+ function createLedgerLoadTool(ctx) {
27717
+ return tool({
27718
+ description: "Load a continuity ledger markdown document from .opencode/ledgers/.",
27719
+ args: {
27720
+ name: tool.schema.string().optional().describe("Ledger name (defaults to most recent).")
27721
+ },
27722
+ execute: async ({ name }) => {
27723
+ const dir = ledgerDir(ctx.directory);
27724
+ try {
27725
+ if (name && name.trim().length > 0) {
27726
+ const p = ledgerPath(ctx.directory, name);
27727
+ if (!fs5.existsSync(p))
27728
+ return "No ledgers found";
27729
+ return await fs5.promises.readFile(p, "utf-8");
27730
+ }
27731
+ if (!fs5.existsSync(dir))
27732
+ return "No ledgers found";
27733
+ const entries = await fs5.promises.readdir(dir);
27734
+ const files = entries.filter((e) => e.toLowerCase().endsWith(".md")).map((e) => path5.join(dir, e));
27735
+ if (files.length === 0)
27736
+ return "No ledgers found";
27737
+ let latest = files[0];
27738
+ let latestMtime = -1;
27739
+ for (const f of files) {
27740
+ try {
27741
+ const st = await fs5.promises.stat(f);
27742
+ if (st.mtimeMs > latestMtime) {
27743
+ latestMtime = st.mtimeMs;
27744
+ latest = f;
27745
+ }
27746
+ } catch {}
27747
+ }
27748
+ return await fs5.promises.readFile(latest, "utf-8");
27749
+ } catch (err) {
27750
+ log("ledger_load failed", { error: String(err) });
27751
+ return "No ledgers found";
27752
+ }
27753
+ }
27754
+ });
27755
+ }
27756
+
27757
+ // src/tools/ast-search.ts
27758
+ import * as fs6 from "fs";
27759
+ import * as path6 from "path";
27760
+ import { spawnSync } from "child_process";
27761
+ var MAX_MATCHES = 50;
27762
+ function stripAnsi(s) {
27763
+ return s.replace(/\u001b\[[0-9;]*m/g, "");
27764
+ }
27765
+ function resolveSearchPath(projectDir, p) {
27766
+ return path6.isAbsolute(p) ? p : path6.join(projectDir, p);
27767
+ }
27768
+ function candidatesFor(bin) {
27769
+ const out = [bin];
27770
+ if (process.platform === "win32") {
27771
+ out.push(`${bin}.exe`, `${bin}.cmd`, `${bin}.bat`);
27772
+ }
27773
+ return out;
27774
+ }
27775
+ function findAstGrepBinary() {
27776
+ const pathEnv = process.env.PATH ?? "";
27777
+ const dirs = pathEnv.split(path6.delimiter).filter(Boolean);
27778
+ const bins = ["sg", "ast-grep"];
27779
+ for (const dir of dirs) {
27780
+ for (const b of bins) {
27781
+ for (const cand of candidatesFor(b)) {
27782
+ const full = path6.join(dir, cand);
27783
+ try {
27784
+ const st = fs6.statSync(full);
27785
+ if (!st.isFile())
27786
+ continue;
27787
+ fs6.accessSync(full, fs6.constants.X_OK);
27788
+ return full;
27789
+ } catch {}
27790
+ }
27791
+ }
27792
+ }
27793
+ return null;
27794
+ }
27795
+ function normalizeOutput(stdout) {
27796
+ const lines = stripAnsi(stdout).split(`
27797
+ `).map((l) => l.trimEnd()).filter((l) => l.length > 0);
27798
+ const out = [];
27799
+ for (const line of lines) {
27800
+ const m3 = line.match(/^(.*?):(\d+):(\d+):(.*)$/);
27801
+ if (m3) {
27802
+ out.push(`${m3[1]}:${m3[2]}: ${m3[4].trim()}`);
27803
+ continue;
27804
+ }
27805
+ const m2 = line.match(/^(.*?):(\d+):(.*)$/);
27806
+ if (m2) {
27807
+ out.push(`${m2[1]}:${m2[2]}: ${m2[3].trim()}`);
27808
+ continue;
27809
+ }
27810
+ out.push(line);
27811
+ }
27812
+ return out.slice(0, MAX_MATCHES).join(`
27813
+ `);
27814
+ }
27815
+ function createAstSearchTool(ctx, binaryPath) {
27816
+ return tool({
27817
+ description: "AST-aware code search using ast-grep (sg).",
27818
+ args: {
27819
+ pattern: tool.schema.string().describe('ast-grep pattern (e.g. "console.log($$$)").'),
27820
+ path: tool.schema.string().optional().describe("Directory to search (default: project root)."),
27821
+ lang: tool.schema.string().optional().describe('Language (e.g. "typescript", "python").')
27822
+ },
27823
+ execute: async ({ pattern, path: searchPath, lang }) => {
27824
+ const target = resolveSearchPath(ctx.directory, searchPath ?? ".");
27825
+ const args = ["--pattern", pattern, target];
27826
+ if (lang && lang.trim().length > 0) {
27827
+ args.push("--lang", lang);
27828
+ }
27829
+ const res = spawnSync(binaryPath, args, {
27830
+ cwd: ctx.directory,
27831
+ encoding: "utf-8"
27832
+ });
27833
+ if (res.error) {
27834
+ log("ast_search spawn failed", { error: String(res.error) });
27835
+ return `Error: ${String(res.error)}`;
27836
+ }
27837
+ const stdout = res.stdout ?? "";
27838
+ const stderr = res.stderr ?? "";
27839
+ if ((res.status ?? 0) === 1 && stdout.trim().length === 0) {
27840
+ return "No matches";
27841
+ }
27842
+ if ((res.status ?? 0) !== 0 && (res.status ?? 0) !== 1) {
27843
+ log("ast_search failed", { status: res.status, stderr });
27844
+ return stderr.trim().length > 0 ? stderr.trim() : "Error: ast_search failed";
27845
+ }
27846
+ const normalized = normalizeOutput(stdout);
27847
+ return normalized.length > 0 ? normalized : "No matches";
27848
+ }
27849
+ });
27850
+ }
27851
+
27522
27852
  // src/hooks/todo-enforcer.ts
27523
27853
  var DEFAULT_MAX_ENFORCEMENTS = 5;
27524
27854
  var sessionState = new Map;
@@ -27601,7 +27931,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
27601
27931
  }
27602
27932
 
27603
27933
  // src/hooks/comment-checker.ts
27604
- import * as fs3 from "fs";
27934
+ import * as fs7 from "fs";
27605
27935
  var AI_SLOP_PATTERNS = [
27606
27936
  /\/\/ .{80,}/,
27607
27937
  /\/\/ (This|The|We|Here|Note:)/i,
@@ -27680,7 +28010,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
27680
28010
  if (!filePath || !isCodeFile(filePath))
27681
28011
  return;
27682
28012
  try {
27683
- const content = fs3.readFileSync(filePath, "utf-8");
28013
+ const content = fs7.readFileSync(filePath, "utf-8");
27684
28014
  const result = checkComments(content, filePath, maxRatio, slopThreshold);
27685
28015
  if (result.shouldWarn) {
27686
28016
  output.output += `
@@ -27852,10 +28182,10 @@ class Semaphore {
27852
28182
  this.active++;
27853
28183
  return;
27854
28184
  }
27855
- return new Promise((resolve) => {
28185
+ return new Promise((resolve2) => {
27856
28186
  this.queue.push(() => {
27857
28187
  this.active++;
27858
- resolve();
28188
+ resolve2();
27859
28189
  });
27860
28190
  });
27861
28191
  }
@@ -27975,10 +28305,17 @@ var OpenCodeUltra = async (ctx) => {
27975
28305
  resolveAgentModel
27976
28306
  });
27977
28307
  const ralphTools = createRalphLoopTools(ctx, internalSessions);
28308
+ const batchRead = createBatchReadTool(ctx);
28309
+ const ledgerSave = createLedgerSaveTool(ctx);
28310
+ const ledgerLoad = createLedgerLoadTool(ctx);
28311
+ const astGrepBin = findAstGrepBinary();
28312
+ const astSearch = astGrepBin ? createAstSearchTool(ctx, astGrepBin) : null;
27978
28313
  const todoEnforcer = createTodoEnforcer(ctx, internalSessions, pluginConfig.todo_enforcer?.maxEnforcements);
27979
28314
  const commentCheckerHook = createCommentCheckerHook(internalSessions, pluginConfig.comment_checker?.maxRatio, pluginConfig.comment_checker?.slopThreshold);
27980
28315
  const tokenTruncationHook = createTokenTruncationHook(internalSessions, pluginConfig.token_truncation?.maxChars);
27981
28316
  const sessionCompactionHook = createSessionCompactionHook(ctx, internalSessions);
28317
+ const fragmentInjector = createFragmentInjector(ctx, internalSessions, pluginConfig.fragments);
28318
+ const promptRendererHook = createPromptRendererHook(internalSessions, pluginConfig.prompt_renderer);
27982
28319
  const pendingKeywords = new Map;
27983
28320
  log("Config loaded", {
27984
28321
  agentCount: Object.keys(agents).length,
@@ -27999,6 +28336,18 @@ var OpenCodeUltra = async (ctx) => {
27999
28336
  if (!disabledTools.has("cancel_ralph")) {
28000
28337
  toolRegistry.cancel_ralph = ralphTools.cancel_ralph;
28001
28338
  }
28339
+ if (!disabledTools.has("batch_read")) {
28340
+ toolRegistry.batch_read = batchRead;
28341
+ }
28342
+ if (!disabledTools.has("ledger_save")) {
28343
+ toolRegistry.ledger_save = ledgerSave;
28344
+ }
28345
+ if (!disabledTools.has("ledger_load")) {
28346
+ toolRegistry.ledger_load = ledgerLoad;
28347
+ }
28348
+ if (!disabledTools.has("ast_search") && astSearch) {
28349
+ toolRegistry.ast_search = astSearch;
28350
+ }
28002
28351
  return {
28003
28352
  tool: toolRegistry,
28004
28353
  config: async (config3) => {
@@ -28107,6 +28456,12 @@ ${arch}`);
28107
28456
  ${style}`);
28108
28457
  }
28109
28458
  }
28459
+ if (!disabledHooks.has("fragment-injector")) {
28460
+ await fragmentInjector(input, output);
28461
+ }
28462
+ if (!disabledHooks.has("prompt-renderer")) {
28463
+ promptRendererHook(input, output);
28464
+ }
28110
28465
  },
28111
28466
  "experimental.session.compacting": async (input, output) => {
28112
28467
  if (disabledHooks.has("session-compaction"))
@@ -0,0 +1,3 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ export declare function findAstGrepBinary(): string | null;
3
+ export declare function createAstSearchTool(ctx: PluginInput, binaryPath: string): any;
@@ -0,0 +1,2 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ export declare function createBatchReadTool(ctx: PluginInput): any;
@@ -0,0 +1,4 @@
1
+ import type { PluginInput } from "@opencode-ai/plugin";
2
+ export declare function normalizeLedgerName(name: string): string;
3
+ export declare function createLedgerSaveTool(ctx: PluginInput): any;
4
+ export declare function createLedgerLoadTool(ctx: PluginInput): any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-ultra",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Lightweight OpenCode 1.2.x plugin — ultrawork mode, multi-agent orchestration, rules injection",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",