opencode-ultra 0.2.2 → 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>>;
@@ -7,7 +7,7 @@ export interface CommentCheckResult {
7
7
  export declare function isCodeFile(filePath: string): boolean;
8
8
  export declare function checkComments(content: string, filePath: string, maxRatio?: number, slopThreshold?: number): CommentCheckResult;
9
9
  export declare function createCommentCheckerHook(internalSessions: Set<string>, maxRatio?: number, slopThreshold?: number): (input: {
10
- tool: {
10
+ tool: string | {
11
11
  name: string;
12
12
  };
13
13
  args: Record<string, unknown>;
@@ -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;
@@ -1,2 +1,4 @@
1
1
  export declare function loadRules(projectDir: string): string | null;
2
+ export declare function loadArchitecture(projectDir: string): string | null;
3
+ export declare function loadCodeStyle(projectDir: string): string | null;
2
4
  export declare function clearRulesCache(): void;
@@ -0,0 +1,21 @@
1
+ type MessageLike = {
2
+ role?: string;
3
+ content?: string;
4
+ info?: {
5
+ role?: string;
6
+ };
7
+ parts?: Array<{
8
+ type: string;
9
+ text?: string;
10
+ }>;
11
+ };
12
+ export declare function buildSessionSummary(messages: MessageLike[]): string;
13
+ export declare function createSessionCompactionHook(ctx: {
14
+ client: any;
15
+ }, internalSessions: Set<string>): (input: {
16
+ sessionID: string;
17
+ }, output: {
18
+ context: string[];
19
+ prompt?: string;
20
+ }) => Promise<void>;
21
+ export {};
@@ -0,0 +1,9 @@
1
+ export declare function truncateMiddle(input: string, maxChars: number): string;
2
+ export declare function createTokenTruncationHook(internalSessions: Set<string>, maxChars?: number): (input: {
3
+ tool: string | {
4
+ name: string;
5
+ };
6
+ sessionID: string;
7
+ }, output: {
8
+ output: string;
9
+ }) => 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(),
@@ -14819,28 +14824,148 @@ var THINK_MESSAGE = `Extended thinking enabled. Take your time to reason thoroug
14819
14824
  // src/hooks/rules-injector.ts
14820
14825
  import * as fs2 from "fs";
14821
14826
  import * as path2 from "path";
14822
- var cachedRules = null;
14823
- var cachedPath = null;
14824
- function loadRules(projectDir) {
14825
- const rulesPath = path2.join(projectDir, ".opencode", "rules.md");
14827
+ var cache = new Map;
14828
+ function loadCachedFile(filePath) {
14826
14829
  try {
14827
- if (!fs2.existsSync(rulesPath))
14830
+ if (!fs2.existsSync(filePath))
14828
14831
  return null;
14829
- const stat = fs2.statSync(rulesPath);
14832
+ const stat = fs2.statSync(filePath);
14830
14833
  const mtime = stat.mtimeMs;
14831
- if (cachedPath === rulesPath && cachedRules && cachedRules.mtime === mtime) {
14832
- return cachedRules.content;
14833
- }
14834
- const content = fs2.readFileSync(rulesPath, "utf-8");
14835
- cachedPath = rulesPath;
14836
- cachedRules = { content, mtime };
14837
- log(`Rules loaded from ${rulesPath}`);
14834
+ const cached2 = cache.get(filePath);
14835
+ if (cached2 && cached2.mtime === mtime)
14836
+ return cached2.content;
14837
+ const content = fs2.readFileSync(filePath, "utf-8");
14838
+ cache.set(filePath, { content, mtime });
14838
14839
  return content;
14839
14840
  } catch (err) {
14840
- log(`Error loading rules: ${err}`);
14841
+ log(`Error loading file: ${err}`, { filePath });
14842
+ return null;
14843
+ }
14844
+ }
14845
+ function loadFirstExisting(projectDir, relativePaths) {
14846
+ for (const rel of relativePaths) {
14847
+ const p = path2.join(projectDir, rel);
14848
+ const content = loadCachedFile(p);
14849
+ if (content !== null)
14850
+ return { path: p, content };
14851
+ }
14852
+ return null;
14853
+ }
14854
+ function loadRules(projectDir) {
14855
+ const hit = loadFirstExisting(projectDir, [path2.join(".opencode", "rules.md")]);
14856
+ if (!hit)
14857
+ return null;
14858
+ log(`Rules loaded from ${hit.path}`);
14859
+ return hit.content;
14860
+ }
14861
+ function loadArchitecture(projectDir) {
14862
+ const hit = loadFirstExisting(projectDir, [
14863
+ path2.join(".opencode", "ARCHITECTURE.md"),
14864
+ "ARCHITECTURE.md"
14865
+ ]);
14866
+ if (!hit)
14867
+ return null;
14868
+ log(`Architecture loaded from ${hit.path}`);
14869
+ return hit.content;
14870
+ }
14871
+ function loadCodeStyle(projectDir) {
14872
+ const hit = loadFirstExisting(projectDir, [
14873
+ path2.join(".opencode", "CODE_STYLE.md"),
14874
+ "CODE_STYLE.md"
14875
+ ]);
14876
+ if (!hit)
14877
+ return null;
14878
+ log(`Code style loaded from ${hit.path}`);
14879
+ return hit.content;
14880
+ }
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
+ }
14841
14901
  return null;
14842
14902
  }
14843
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
+ }
14844
14969
 
14845
14970
  // node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
14846
14971
  var exports_external2 = {};
@@ -15571,15 +15696,15 @@ function mergeDefs2(...defs) {
15571
15696
  function cloneDef2(schema) {
15572
15697
  return mergeDefs2(schema._zod.def);
15573
15698
  }
15574
- function getElementAtPath2(obj, path3) {
15575
- if (!path3)
15699
+ function getElementAtPath2(obj, path4) {
15700
+ if (!path4)
15576
15701
  return obj;
15577
- return path3.reduce((acc, key) => acc?.[key], obj);
15702
+ return path4.reduce((acc, key) => acc?.[key], obj);
15578
15703
  }
15579
15704
  function promiseAllObject2(promisesObj) {
15580
15705
  const keys = Object.keys(promisesObj);
15581
- const promises = keys.map((key) => promisesObj[key]);
15582
- return Promise.all(promises).then((results) => {
15706
+ const promises2 = keys.map((key) => promisesObj[key]);
15707
+ return Promise.all(promises2).then((results) => {
15583
15708
  const resolvedObj = {};
15584
15709
  for (let i = 0;i < keys.length; i++) {
15585
15710
  resolvedObj[keys[i]] = results[i];
@@ -15933,11 +16058,11 @@ function aborted2(x, startIndex = 0) {
15933
16058
  }
15934
16059
  return false;
15935
16060
  }
15936
- function prefixIssues2(path3, issues) {
16061
+ function prefixIssues2(path4, issues) {
15937
16062
  return issues.map((iss) => {
15938
16063
  var _a2;
15939
16064
  (_a2 = iss).path ?? (_a2.path = []);
15940
- iss.path.unshift(path3);
16065
+ iss.path.unshift(path4);
15941
16066
  return iss;
15942
16067
  });
15943
16068
  }
@@ -16105,7 +16230,7 @@ function treeifyError2(error48, _mapper) {
16105
16230
  return issue3.message;
16106
16231
  };
16107
16232
  const result = { errors: [] };
16108
- const processError = (error49, path3 = []) => {
16233
+ const processError = (error49, path4 = []) => {
16109
16234
  var _a2, _b;
16110
16235
  for (const issue3 of error49.issues) {
16111
16236
  if (issue3.code === "invalid_union" && issue3.errors.length) {
@@ -16115,7 +16240,7 @@ function treeifyError2(error48, _mapper) {
16115
16240
  } else if (issue3.code === "invalid_element") {
16116
16241
  processError({ issues: issue3.issues }, issue3.path);
16117
16242
  } else {
16118
- const fullpath = [...path3, ...issue3.path];
16243
+ const fullpath = [...path4, ...issue3.path];
16119
16244
  if (fullpath.length === 0) {
16120
16245
  result.errors.push(mapper(issue3));
16121
16246
  continue;
@@ -16147,8 +16272,8 @@ function treeifyError2(error48, _mapper) {
16147
16272
  }
16148
16273
  function toDotPath2(_path) {
16149
16274
  const segs = [];
16150
- const path3 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16151
- for (const seg of path3) {
16275
+ const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
16276
+ for (const seg of path4) {
16152
16277
  if (typeof seg === "number")
16153
16278
  segs.push(`[${seg}]`);
16154
16279
  else if (typeof seg === "symbol")
@@ -27487,6 +27612,243 @@ Original task:
27487
27612
  ${original}`;
27488
27613
  }
27489
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
+
27490
27852
  // src/hooks/todo-enforcer.ts
27491
27853
  var DEFAULT_MAX_ENFORCEMENTS = 5;
27492
27854
  var sessionState = new Map;
@@ -27569,7 +27931,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
27569
27931
  }
27570
27932
 
27571
27933
  // src/hooks/comment-checker.ts
27572
- import * as fs3 from "fs";
27934
+ import * as fs7 from "fs";
27573
27935
  var AI_SLOP_PATTERNS = [
27574
27936
  /\/\/ .{80,}/,
27575
27937
  /\/\/ (This|The|We|Here|Note:)/i,
@@ -27639,7 +28001,8 @@ function buildWarning(ratio, slopCount, maxRatio, slopThreshold) {
27639
28001
  }
27640
28002
  function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshold = 3) {
27641
28003
  return (input, output) => {
27642
- if (input.tool.name !== "Write" && input.tool.name !== "Edit")
28004
+ const toolName = typeof input.tool === "string" ? input.tool : input.tool.name;
28005
+ if (toolName !== "Write" && toolName !== "Edit")
27643
28006
  return;
27644
28007
  if (internalSessions.has(input.sessionID))
27645
28008
  return;
@@ -27647,7 +28010,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
27647
28010
  if (!filePath || !isCodeFile(filePath))
27648
28011
  return;
27649
28012
  try {
27650
- const content = fs3.readFileSync(filePath, "utf-8");
28013
+ const content = fs7.readFileSync(filePath, "utf-8");
27651
28014
  const result = checkComments(content, filePath, maxRatio, slopThreshold);
27652
28015
  if (result.shouldWarn) {
27653
28016
  output.output += `
@@ -27662,6 +28025,150 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
27662
28025
  };
27663
28026
  }
27664
28027
 
28028
+ // src/hooks/token-truncation.ts
28029
+ var DEFAULT_MAX_CHARS = 30000;
28030
+ function truncateMiddle(input, maxChars) {
28031
+ if (!Number.isFinite(maxChars) || maxChars <= 0)
28032
+ return input;
28033
+ if (input.length <= maxChars)
28034
+ return input;
28035
+ if (maxChars < 32)
28036
+ return input.slice(0, maxChars);
28037
+ const len = input.length;
28038
+ let head = Math.floor(maxChars * 0.4);
28039
+ let tail = Math.floor(maxChars * 0.4);
28040
+ head = Math.min(Math.max(head, 1), maxChars);
28041
+ const buildMarker = (removed2) => `
28042
+
28043
+ ... [truncated ${removed2} chars] ...
28044
+
28045
+ `;
28046
+ const finalize2 = (h, t) => {
28047
+ const removed2 = Math.max(0, len - h - t);
28048
+ const marker2 = buildMarker(removed2);
28049
+ const availableTail2 = maxChars - h - marker2.length;
28050
+ const finalTail = Math.max(0, Math.min(t, availableTail2));
28051
+ const finalRemoved = Math.max(0, len - h - finalTail);
28052
+ const finalMarker = buildMarker(finalRemoved);
28053
+ const availableTail22 = maxChars - h - finalMarker.length;
28054
+ const finalTail2 = Math.max(0, Math.min(finalTail, availableTail22));
28055
+ const removed22 = Math.max(0, len - h - finalTail2);
28056
+ const marker22 = buildMarker(removed22);
28057
+ const budget = h + marker22.length + finalTail2;
28058
+ if (budget > maxChars) {
28059
+ return input.slice(0, maxChars);
28060
+ }
28061
+ return input.slice(0, h) + marker22 + input.slice(len - finalTail2);
28062
+ };
28063
+ const removed = Math.max(0, len - head - tail);
28064
+ const marker = buildMarker(removed);
28065
+ const availableTail = maxChars - head - marker.length;
28066
+ if (availableTail < 0) {
28067
+ const shrinkHead = Math.max(0, maxChars - marker.length);
28068
+ return finalize2(shrinkHead, 0);
28069
+ }
28070
+ tail = Math.min(tail, availableTail);
28071
+ return finalize2(head, tail);
28072
+ }
28073
+ function createTokenTruncationHook(internalSessions, maxChars = DEFAULT_MAX_CHARS) {
28074
+ const budget = Number.isFinite(maxChars) ? Math.max(1, Math.floor(maxChars)) : DEFAULT_MAX_CHARS;
28075
+ return (input, output) => {
28076
+ if (internalSessions.has(input.sessionID))
28077
+ return;
28078
+ const before = output.output;
28079
+ const after = truncateMiddle(before, budget);
28080
+ if (after !== before) {
28081
+ output.output = after;
28082
+ const toolName = typeof input.tool === "string" ? input.tool : input.tool.name;
28083
+ log("Token truncation applied", { tool: toolName, before: before.length, after: after.length });
28084
+ }
28085
+ };
28086
+ }
28087
+
28088
+ // src/hooks/session-compaction.ts
28089
+ function partsToText(parts) {
28090
+ if (!parts)
28091
+ return "";
28092
+ return parts.filter((p) => p && p.type === "text" && typeof p.text === "string").map((p) => p.text).join("");
28093
+ }
28094
+ function getRole(m) {
28095
+ return (m.role ?? m.info?.role ?? "").toLowerCase();
28096
+ }
28097
+ function getText(m) {
28098
+ if (typeof m.content === "string")
28099
+ return m.content;
28100
+ return partsToText(m.parts);
28101
+ }
28102
+ function uniq(items) {
28103
+ const seen = new Set;
28104
+ const out = [];
28105
+ for (const it of items) {
28106
+ const k = typeof it === "string" ? it : JSON.stringify(it);
28107
+ if (seen.has(k))
28108
+ continue;
28109
+ seen.add(k);
28110
+ out.push(it);
28111
+ }
28112
+ return out;
28113
+ }
28114
+ var FILE_PATH_RE = /(?:(?:[A-Za-z]:\\)?[\w.-]+(?:[\\/][\w.-]+)+)\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|toml|yaml|yml|py|go|rs|java|kt|c|cpp|h|hpp|cs|swift|vue|svelte)/g;
28115
+ function buildSessionSummary(messages) {
28116
+ const assistantTexts = messages.filter((m) => getRole(m) === "assistant").map((m) => getText(m)).map((t) => t.trim()).filter(Boolean);
28117
+ const combined = assistantTexts.join(`
28118
+ `);
28119
+ const files = uniq((combined.match(FILE_PATH_RE) ?? []).map((s) => s.replace(/^[`"']|[`"']$/g, "")));
28120
+ const decisionLines = uniq(assistantTexts.flatMap((t) => t.split(`
28121
+ `)).map((l) => l.trim()).filter((l) => /(decid|we will|should|must|chosen|choice|\u65B9\u91DD|\u6C7A\u5B9A|\u63A1\u7528)/i.test(l)).slice(0, 10));
28122
+ const nextLines = uniq(assistantTexts.flatMap((t) => t.split(`
28123
+ `)).map((l) => l.trim()).filter((l) => /(next\s*steps?|todo|next:|\u6B21\s*\u306B|\u3084\u308B|TODO)/i.test(l)).slice(0, 10));
28124
+ const currentState = (assistantTexts[assistantTexts.length - 1] ?? "").slice(0, 800).trim();
28125
+ const lines = [];
28126
+ lines.push("## Session Summary");
28127
+ lines.push("");
28128
+ lines.push("### Key Decisions");
28129
+ if (decisionLines.length === 0)
28130
+ lines.push("- (none)");
28131
+ else
28132
+ for (const l of decisionLines)
28133
+ lines.push(`- ${l}`);
28134
+ lines.push("");
28135
+ lines.push("### Files Modified");
28136
+ if (files.length === 0)
28137
+ lines.push("- (none)");
28138
+ else
28139
+ for (const f of files)
28140
+ lines.push(`- ${f}`);
28141
+ lines.push("");
28142
+ lines.push("### Current State");
28143
+ lines.push(currentState ? `- ${currentState}` : "- (none)");
28144
+ lines.push("");
28145
+ lines.push("### Next Steps");
28146
+ if (nextLines.length === 0)
28147
+ lines.push("- (none)");
28148
+ else
28149
+ for (const l of nextLines)
28150
+ lines.push(`- ${l}`);
28151
+ lines.push("");
28152
+ return lines.join(`
28153
+ `);
28154
+ }
28155
+ function createSessionCompactionHook(ctx, internalSessions) {
28156
+ return async (input, output) => {
28157
+ if (internalSessions.has(input.sessionID))
28158
+ return;
28159
+ try {
28160
+ const res = await ctx.client?.session?.listMessages?.({ id: input.sessionID });
28161
+ const msgs = res?.messages ?? res ?? [];
28162
+ const summary = buildSessionSummary(msgs);
28163
+ output.context.push(summary);
28164
+ output.prompt = "Output a compact session summary in the EXACT format below. " + "Use the provided '## Session Summary' scaffold from context as your base; do not add extra sections.";
28165
+ } catch (err) {
28166
+ log("Session compaction hook failed", { error: err });
28167
+ output.prompt = "Summarize the session using this exact template: ## Session Summary; ### Key Decisions; ### Files Modified; ### Current State; ### Next Steps.";
28168
+ }
28169
+ };
28170
+ }
28171
+
27665
28172
  // src/concurrency/semaphore.ts
27666
28173
  class Semaphore {
27667
28174
  max;
@@ -27675,10 +28182,10 @@ class Semaphore {
27675
28182
  this.active++;
27676
28183
  return;
27677
28184
  }
27678
- return new Promise((resolve) => {
28185
+ return new Promise((resolve2) => {
27679
28186
  this.queue.push(() => {
27680
28187
  this.active++;
27681
- resolve();
28188
+ resolve2();
27682
28189
  });
27683
28190
  });
27684
28191
  }
@@ -27798,8 +28305,17 @@ var OpenCodeUltra = async (ctx) => {
27798
28305
  resolveAgentModel
27799
28306
  });
27800
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;
27801
28313
  const todoEnforcer = createTodoEnforcer(ctx, internalSessions, pluginConfig.todo_enforcer?.maxEnforcements);
27802
28314
  const commentCheckerHook = createCommentCheckerHook(internalSessions, pluginConfig.comment_checker?.maxRatio, pluginConfig.comment_checker?.slopThreshold);
28315
+ const tokenTruncationHook = createTokenTruncationHook(internalSessions, pluginConfig.token_truncation?.maxChars);
28316
+ const sessionCompactionHook = createSessionCompactionHook(ctx, internalSessions);
28317
+ const fragmentInjector = createFragmentInjector(ctx, internalSessions, pluginConfig.fragments);
28318
+ const promptRendererHook = createPromptRendererHook(internalSessions, pluginConfig.prompt_renderer);
27803
28319
  const pendingKeywords = new Map;
27804
28320
  log("Config loaded", {
27805
28321
  agentCount: Object.keys(agents).length,
@@ -27820,6 +28336,18 @@ var OpenCodeUltra = async (ctx) => {
27820
28336
  if (!disabledTools.has("cancel_ralph")) {
27821
28337
  toolRegistry.cancel_ralph = ralphTools.cancel_ralph;
27822
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
+ }
27823
28351
  return {
27824
28352
  tool: toolRegistry,
27825
28353
  config: async (config3) => {
@@ -27844,6 +28372,15 @@ var OpenCodeUltra = async (ctx) => {
27844
28372
  ...existingAgents,
27845
28373
  ...agentConfigs
27846
28374
  };
28375
+ if (pluginConfig.demote_builtin !== false) {
28376
+ const agentMap = config3.agent;
28377
+ for (const name of ["build", "plan", "triage", "docs"]) {
28378
+ const cur = agentMap[name];
28379
+ if (cur && typeof cur === "object") {
28380
+ agentMap[name] = { ...cur, mode: "subagent" };
28381
+ }
28382
+ }
28383
+ }
27847
28384
  log("Agents registered", { agents: Object.keys(agentConfigs) });
27848
28385
  },
27849
28386
  "chat.message": async (input, output) => {
@@ -27905,11 +28442,39 @@ var OpenCodeUltra = async (ctx) => {
27905
28442
  ${rules}`);
27906
28443
  }
27907
28444
  }
28445
+ if (!disabledHooks.has("context-injector")) {
28446
+ const arch = loadArchitecture(ctx.directory);
28447
+ if (arch) {
28448
+ output.system.push(`## Architecture
28449
+
28450
+ ${arch}`);
28451
+ }
28452
+ const style = loadCodeStyle(ctx.directory);
28453
+ if (style) {
28454
+ output.system.push(`## Code Style
28455
+
28456
+ ${style}`);
28457
+ }
28458
+ }
28459
+ if (!disabledHooks.has("fragment-injector")) {
28460
+ await fragmentInjector(input, output);
28461
+ }
28462
+ if (!disabledHooks.has("prompt-renderer")) {
28463
+ promptRendererHook(input, output);
28464
+ }
28465
+ },
28466
+ "experimental.session.compacting": async (input, output) => {
28467
+ if (disabledHooks.has("session-compaction"))
28468
+ return;
28469
+ await sessionCompactionHook(input, output);
27908
28470
  },
27909
28471
  "tool.execute.after": async (input, output) => {
27910
28472
  if (!disabledHooks.has("comment-checker")) {
27911
28473
  commentCheckerHook(input, output);
27912
28474
  }
28475
+ if (!disabledHooks.has("token-truncation")) {
28476
+ tokenTruncationHook(input, output);
28477
+ }
27913
28478
  },
27914
28479
  event: async ({ event }) => {
27915
28480
  if (event.type === "session.deleted") {
@@ -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.2.2",
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",