opencode-ultra 0.3.0 → 0.4.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/README.md +152 -264
- package/dist/config.d.ts +13 -0
- package/dist/hooks/fragment-injector.d.ts +9 -0
- package/dist/hooks/prompt-renderer.d.ts +14 -0
- package/dist/hooks/session-compaction.d.ts +1 -1
- package/dist/index.js +436 -21
- package/dist/shared/index.d.ts +2 -0
- package/dist/shared/ttl-map.d.ts +19 -0
- package/dist/shared/types.d.ts +93 -0
- package/dist/tools/ast-search.d.ts +3 -0
- package/dist/tools/batch-read.d.ts +2 -0
- package/dist/tools/continuity-ledger.d.ts +4 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14369,6 +14369,49 @@ function log(message, data) {
|
|
|
14369
14369
|
console.error(`${PREFIX} ${message}`);
|
|
14370
14370
|
}
|
|
14371
14371
|
}
|
|
14372
|
+
// src/shared/ttl-map.ts
|
|
14373
|
+
class TtlMap {
|
|
14374
|
+
map = new Map;
|
|
14375
|
+
maxSize;
|
|
14376
|
+
ttlMs;
|
|
14377
|
+
constructor(opts = {}) {
|
|
14378
|
+
this.maxSize = opts.maxSize ?? 1000;
|
|
14379
|
+
this.ttlMs = opts.ttlMs ?? 0;
|
|
14380
|
+
}
|
|
14381
|
+
get(key) {
|
|
14382
|
+
const entry = this.map.get(key);
|
|
14383
|
+
if (!entry)
|
|
14384
|
+
return;
|
|
14385
|
+
if (this.ttlMs > 0 && Date.now() - entry.createdAt > this.ttlMs) {
|
|
14386
|
+
this.map.delete(key);
|
|
14387
|
+
return;
|
|
14388
|
+
}
|
|
14389
|
+
return entry.value;
|
|
14390
|
+
}
|
|
14391
|
+
set(key, value) {
|
|
14392
|
+
if (this.map.has(key)) {
|
|
14393
|
+
this.map.delete(key);
|
|
14394
|
+
}
|
|
14395
|
+
if (this.map.size >= this.maxSize) {
|
|
14396
|
+
const oldest = this.map.keys().next().value;
|
|
14397
|
+
if (oldest !== undefined)
|
|
14398
|
+
this.map.delete(oldest);
|
|
14399
|
+
}
|
|
14400
|
+
this.map.set(key, { value, createdAt: Date.now() });
|
|
14401
|
+
}
|
|
14402
|
+
has(key) {
|
|
14403
|
+
return this.get(key) !== undefined;
|
|
14404
|
+
}
|
|
14405
|
+
delete(key) {
|
|
14406
|
+
return this.map.delete(key);
|
|
14407
|
+
}
|
|
14408
|
+
get size() {
|
|
14409
|
+
return this.map.size;
|
|
14410
|
+
}
|
|
14411
|
+
clear() {
|
|
14412
|
+
this.map.clear();
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
14372
14415
|
// src/config.ts
|
|
14373
14416
|
var AgentOverrideSchema = exports_external.object({
|
|
14374
14417
|
model: exports_external.string().optional(),
|
|
@@ -14396,6 +14439,11 @@ var CategoryConfigSchema = exports_external.object({
|
|
|
14396
14439
|
var PluginConfigSchema = exports_external.object({
|
|
14397
14440
|
agents: AgentOverridesSchema.optional(),
|
|
14398
14441
|
categories: exports_external.record(exports_external.string(), CategoryConfigSchema).optional(),
|
|
14442
|
+
fragments: exports_external.record(exports_external.string(), exports_external.array(exports_external.string())).optional(),
|
|
14443
|
+
prompt_renderer: exports_external.object({
|
|
14444
|
+
default: exports_external.enum(["markdown", "xml", "json"]).optional(),
|
|
14445
|
+
model_overrides: exports_external.record(exports_external.string(), exports_external.enum(["markdown", "xml", "json"])).optional()
|
|
14446
|
+
}).optional(),
|
|
14399
14447
|
disabled_agents: exports_external.array(exports_external.string()).optional(),
|
|
14400
14448
|
disabled_hooks: exports_external.array(exports_external.string()).optional(),
|
|
14401
14449
|
disabled_tools: exports_external.array(exports_external.string()).optional(),
|
|
@@ -14819,7 +14867,7 @@ var THINK_MESSAGE = `Extended thinking enabled. Take your time to reason thoroug
|
|
|
14819
14867
|
// src/hooks/rules-injector.ts
|
|
14820
14868
|
import * as fs2 from "fs";
|
|
14821
14869
|
import * as path2 from "path";
|
|
14822
|
-
var cache = new
|
|
14870
|
+
var cache = new TtlMap({ maxSize: 50, ttlMs: 10 * 60 * 1000 });
|
|
14823
14871
|
function loadCachedFile(filePath) {
|
|
14824
14872
|
try {
|
|
14825
14873
|
if (!fs2.existsSync(filePath))
|
|
@@ -14874,6 +14922,94 @@ function loadCodeStyle(projectDir) {
|
|
|
14874
14922
|
return hit.content;
|
|
14875
14923
|
}
|
|
14876
14924
|
|
|
14925
|
+
// src/hooks/fragment-injector.ts
|
|
14926
|
+
import * as fs3 from "fs";
|
|
14927
|
+
import * as path3 from "path";
|
|
14928
|
+
var cache2 = new TtlMap({ maxSize: 100, ttlMs: 10 * 60 * 1000 });
|
|
14929
|
+
async function readCached(absPath) {
|
|
14930
|
+
try {
|
|
14931
|
+
const st = await fs3.promises.stat(absPath);
|
|
14932
|
+
if (!st.isFile())
|
|
14933
|
+
return null;
|
|
14934
|
+
const prev = cache2.get(absPath);
|
|
14935
|
+
if (prev && prev.mtimeMs === st.mtimeMs)
|
|
14936
|
+
return prev.content;
|
|
14937
|
+
const content = await fs3.promises.readFile(absPath, "utf-8");
|
|
14938
|
+
cache2.set(absPath, { mtimeMs: st.mtimeMs, content });
|
|
14939
|
+
return content;
|
|
14940
|
+
} catch (err) {
|
|
14941
|
+
if (err?.code !== "ENOENT") {
|
|
14942
|
+
log("fragment read failed", { path: absPath, error: String(err) });
|
|
14943
|
+
}
|
|
14944
|
+
return null;
|
|
14945
|
+
}
|
|
14946
|
+
}
|
|
14947
|
+
function createFragmentInjector(ctx, internalSessions, fragments) {
|
|
14948
|
+
return async (input, output) => {
|
|
14949
|
+
if (input.sessionID && internalSessions.has(input.sessionID))
|
|
14950
|
+
return;
|
|
14951
|
+
const agent = input.agent;
|
|
14952
|
+
if (!agent || !fragments)
|
|
14953
|
+
return;
|
|
14954
|
+
const fragmentPaths = fragments[agent];
|
|
14955
|
+
if (!Array.isArray(fragmentPaths) || fragmentPaths.length === 0)
|
|
14956
|
+
return;
|
|
14957
|
+
for (const rel of fragmentPaths) {
|
|
14958
|
+
const abs = path3.resolve(ctx.directory, rel);
|
|
14959
|
+
const content = await readCached(abs);
|
|
14960
|
+
if (content === null) {
|
|
14961
|
+
log("fragment file not found; skipping", { agent, path: rel });
|
|
14962
|
+
continue;
|
|
14963
|
+
}
|
|
14964
|
+
output.system.push(content);
|
|
14965
|
+
}
|
|
14966
|
+
};
|
|
14967
|
+
}
|
|
14968
|
+
|
|
14969
|
+
// src/hooks/prompt-renderer.ts
|
|
14970
|
+
function escapeXml(s) {
|
|
14971
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\"/g, """).replace(/'/g, "'");
|
|
14972
|
+
}
|
|
14973
|
+
function resolvePromptFormat(inputModel, config2) {
|
|
14974
|
+
const def = config2?.default ?? "markdown";
|
|
14975
|
+
const overrides = config2?.model_overrides ?? {};
|
|
14976
|
+
const providerID = inputModel?.providerID;
|
|
14977
|
+
const modelID = inputModel?.modelID;
|
|
14978
|
+
const key = typeof providerID === "string" && typeof modelID === "string" ? `${providerID}/${modelID}` : undefined;
|
|
14979
|
+
if (key && overrides[key])
|
|
14980
|
+
return overrides[key];
|
|
14981
|
+
if (typeof modelID === "string" && overrides[modelID])
|
|
14982
|
+
return overrides[modelID];
|
|
14983
|
+
return def;
|
|
14984
|
+
}
|
|
14985
|
+
function formatSystemSection(format2, content) {
|
|
14986
|
+
if (format2 === "markdown")
|
|
14987
|
+
return content;
|
|
14988
|
+
if (format2 === "xml") {
|
|
14989
|
+
return `<section name="Rules">${escapeXml(content)}</section>`;
|
|
14990
|
+
}
|
|
14991
|
+
if (format2 === "json") {
|
|
14992
|
+
return JSON.stringify({ section: "Rules", content });
|
|
14993
|
+
}
|
|
14994
|
+
return content;
|
|
14995
|
+
}
|
|
14996
|
+
function createPromptRendererHook(internalSessions, config2) {
|
|
14997
|
+
return (input, output) => {
|
|
14998
|
+
if (input.sessionID && internalSessions.has(input.sessionID))
|
|
14999
|
+
return;
|
|
15000
|
+
if (!Array.isArray(output.system) || output.system.length === 0)
|
|
15001
|
+
return;
|
|
15002
|
+
const format2 = resolvePromptFormat(input.model, config2);
|
|
15003
|
+
if (format2 === "markdown")
|
|
15004
|
+
return;
|
|
15005
|
+
try {
|
|
15006
|
+
output.system = output.system.map((s) => formatSystemSection(format2, s));
|
|
15007
|
+
} catch (err) {
|
|
15008
|
+
log("prompt renderer failed", { error: String(err) });
|
|
15009
|
+
}
|
|
15010
|
+
};
|
|
15011
|
+
}
|
|
15012
|
+
|
|
14877
15013
|
// node_modules/@opencode-ai/plugin/node_modules/zod/v4/classic/external.js
|
|
14878
15014
|
var exports_external2 = {};
|
|
14879
15015
|
__export(exports_external2, {
|
|
@@ -15603,15 +15739,15 @@ function mergeDefs2(...defs) {
|
|
|
15603
15739
|
function cloneDef2(schema) {
|
|
15604
15740
|
return mergeDefs2(schema._zod.def);
|
|
15605
15741
|
}
|
|
15606
|
-
function getElementAtPath2(obj,
|
|
15607
|
-
if (!
|
|
15742
|
+
function getElementAtPath2(obj, path4) {
|
|
15743
|
+
if (!path4)
|
|
15608
15744
|
return obj;
|
|
15609
|
-
return
|
|
15745
|
+
return path4.reduce((acc, key) => acc?.[key], obj);
|
|
15610
15746
|
}
|
|
15611
15747
|
function promiseAllObject2(promisesObj) {
|
|
15612
15748
|
const keys = Object.keys(promisesObj);
|
|
15613
|
-
const
|
|
15614
|
-
return Promise.all(
|
|
15749
|
+
const promises2 = keys.map((key) => promisesObj[key]);
|
|
15750
|
+
return Promise.all(promises2).then((results) => {
|
|
15615
15751
|
const resolvedObj = {};
|
|
15616
15752
|
for (let i = 0;i < keys.length; i++) {
|
|
15617
15753
|
resolvedObj[keys[i]] = results[i];
|
|
@@ -15965,11 +16101,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
15965
16101
|
}
|
|
15966
16102
|
return false;
|
|
15967
16103
|
}
|
|
15968
|
-
function prefixIssues2(
|
|
16104
|
+
function prefixIssues2(path4, issues) {
|
|
15969
16105
|
return issues.map((iss) => {
|
|
15970
16106
|
var _a2;
|
|
15971
16107
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
15972
|
-
iss.path.unshift(
|
|
16108
|
+
iss.path.unshift(path4);
|
|
15973
16109
|
return iss;
|
|
15974
16110
|
});
|
|
15975
16111
|
}
|
|
@@ -16137,7 +16273,7 @@ function treeifyError2(error48, _mapper) {
|
|
|
16137
16273
|
return issue3.message;
|
|
16138
16274
|
};
|
|
16139
16275
|
const result = { errors: [] };
|
|
16140
|
-
const processError = (error49,
|
|
16276
|
+
const processError = (error49, path4 = []) => {
|
|
16141
16277
|
var _a2, _b;
|
|
16142
16278
|
for (const issue3 of error49.issues) {
|
|
16143
16279
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -16147,7 +16283,7 @@ function treeifyError2(error48, _mapper) {
|
|
|
16147
16283
|
} else if (issue3.code === "invalid_element") {
|
|
16148
16284
|
processError({ issues: issue3.issues }, issue3.path);
|
|
16149
16285
|
} else {
|
|
16150
|
-
const fullpath = [...
|
|
16286
|
+
const fullpath = [...path4, ...issue3.path];
|
|
16151
16287
|
if (fullpath.length === 0) {
|
|
16152
16288
|
result.errors.push(mapper(issue3));
|
|
16153
16289
|
continue;
|
|
@@ -16179,8 +16315,8 @@ function treeifyError2(error48, _mapper) {
|
|
|
16179
16315
|
}
|
|
16180
16316
|
function toDotPath2(_path) {
|
|
16181
16317
|
const segs = [];
|
|
16182
|
-
const
|
|
16183
|
-
for (const seg of
|
|
16318
|
+
const path4 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
16319
|
+
for (const seg of path4) {
|
|
16184
16320
|
if (typeof seg === "number")
|
|
16185
16321
|
segs.push(`[${seg}]`);
|
|
16186
16322
|
else if (typeof seg === "symbol")
|
|
@@ -27229,7 +27365,8 @@ function resolveCategory(categoryName, configCategories) {
|
|
|
27229
27365
|
|
|
27230
27366
|
// src/tools/spawn-agent.ts
|
|
27231
27367
|
function showToast(ctx, title, message, variant = "info") {
|
|
27232
|
-
ctx.client
|
|
27368
|
+
const client = ctx.client;
|
|
27369
|
+
client.tui?.showToast?.({
|
|
27233
27370
|
body: { title, message, variant, duration: 2000 }
|
|
27234
27371
|
})?.catch?.(() => {});
|
|
27235
27372
|
}
|
|
@@ -27334,6 +27471,18 @@ spawn_agent({
|
|
|
27334
27471
|
if (!agents || agents.length === 0) {
|
|
27335
27472
|
return "No agents specified.";
|
|
27336
27473
|
}
|
|
27474
|
+
for (let i = 0;i < agents.length; i++) {
|
|
27475
|
+
const t = agents[i];
|
|
27476
|
+
if (!t.agent || typeof t.agent !== "string" || t.agent.trim().length === 0) {
|
|
27477
|
+
return `Error: agents[${i}].agent is required (non-empty string)`;
|
|
27478
|
+
}
|
|
27479
|
+
if (!t.prompt || typeof t.prompt !== "string" || t.prompt.trim().length === 0) {
|
|
27480
|
+
return `Error: agents[${i}].prompt is required (non-empty string)`;
|
|
27481
|
+
}
|
|
27482
|
+
if (!t.description || typeof t.description !== "string") {
|
|
27483
|
+
return `Error: agents[${i}].description is required`;
|
|
27484
|
+
}
|
|
27485
|
+
}
|
|
27337
27486
|
const agentNames = agents.map((a) => a.agent);
|
|
27338
27487
|
log("spawn_agent", { count: agents.length, agents: agentNames });
|
|
27339
27488
|
showToast(ctx, "spawn_agent", `${agents.length} agents: ${agentNames.join(", ")}`);
|
|
@@ -27519,6 +27668,243 @@ Original task:
|
|
|
27519
27668
|
${original}`;
|
|
27520
27669
|
}
|
|
27521
27670
|
|
|
27671
|
+
// src/tools/batch-read.ts
|
|
27672
|
+
import * as fs4 from "fs";
|
|
27673
|
+
import * as path4 from "path";
|
|
27674
|
+
var MAX_PATHS = 20;
|
|
27675
|
+
function resolvePath(projectDir, inputPath) {
|
|
27676
|
+
return path4.isAbsolute(inputPath) ? inputPath : path4.join(projectDir, inputPath);
|
|
27677
|
+
}
|
|
27678
|
+
function isProbablyBinary(buf) {
|
|
27679
|
+
const sample = buf.subarray(0, Math.min(buf.length, 8192));
|
|
27680
|
+
for (const b of sample) {
|
|
27681
|
+
if (b === 0)
|
|
27682
|
+
return true;
|
|
27683
|
+
}
|
|
27684
|
+
return false;
|
|
27685
|
+
}
|
|
27686
|
+
async function readOne(projectDir, inputPath) {
|
|
27687
|
+
const abs = resolvePath(projectDir, inputPath);
|
|
27688
|
+
try {
|
|
27689
|
+
const stat = await fs4.promises.stat(abs);
|
|
27690
|
+
if (!stat.isFile()) {
|
|
27691
|
+
return `=== ${inputPath} ===
|
|
27692
|
+
[File not found]
|
|
27693
|
+
`;
|
|
27694
|
+
}
|
|
27695
|
+
const buf = await fs4.promises.readFile(abs);
|
|
27696
|
+
if (isProbablyBinary(buf)) {
|
|
27697
|
+
return `=== ${inputPath} ===
|
|
27698
|
+
[Binary file]
|
|
27699
|
+
`;
|
|
27700
|
+
}
|
|
27701
|
+
const content = buf.toString("utf-8");
|
|
27702
|
+
return `=== ${inputPath} ===
|
|
27703
|
+
${content}
|
|
27704
|
+
`;
|
|
27705
|
+
} catch (err) {
|
|
27706
|
+
if (err?.code === "ENOENT") {
|
|
27707
|
+
return `=== ${inputPath} ===
|
|
27708
|
+
[File not found]
|
|
27709
|
+
`;
|
|
27710
|
+
}
|
|
27711
|
+
log("batch_read failed to read file", { path: abs, error: String(err) });
|
|
27712
|
+
return `=== ${inputPath} ===
|
|
27713
|
+
[File not found]
|
|
27714
|
+
`;
|
|
27715
|
+
}
|
|
27716
|
+
}
|
|
27717
|
+
function createBatchReadTool(ctx) {
|
|
27718
|
+
return tool({
|
|
27719
|
+
description: "Read multiple files in parallel and concatenate contents.",
|
|
27720
|
+
args: {
|
|
27721
|
+
paths: tool.schema.array(tool.schema.string()).describe("File paths (max 20).")
|
|
27722
|
+
},
|
|
27723
|
+
execute: async ({ paths }) => {
|
|
27724
|
+
if (!Array.isArray(paths)) {
|
|
27725
|
+
return "Error: paths must be an array of strings";
|
|
27726
|
+
}
|
|
27727
|
+
if (paths.length > MAX_PATHS) {
|
|
27728
|
+
return `Error: maximum ${MAX_PATHS} paths allowed`;
|
|
27729
|
+
}
|
|
27730
|
+
const results = await Promise.all(paths.map((p) => readOne(ctx.directory, p)));
|
|
27731
|
+
return results.join("");
|
|
27732
|
+
}
|
|
27733
|
+
});
|
|
27734
|
+
}
|
|
27735
|
+
|
|
27736
|
+
// src/tools/continuity-ledger.ts
|
|
27737
|
+
import * as fs5 from "fs";
|
|
27738
|
+
import * as path5 from "path";
|
|
27739
|
+
function normalizeLedgerName(name) {
|
|
27740
|
+
const trimmed = (name ?? "").trim().replace(/\.md$/i, "");
|
|
27741
|
+
const spaced = trimmed.replace(/\s+/g, "-");
|
|
27742
|
+
const cleaned = spaced.replace(/[^A-Za-z0-9._-]/g, "");
|
|
27743
|
+
return cleaned.length > 0 ? cleaned : "ledger";
|
|
27744
|
+
}
|
|
27745
|
+
function ledgerDir(projectDir) {
|
|
27746
|
+
return path5.join(projectDir, ".opencode", "ledgers");
|
|
27747
|
+
}
|
|
27748
|
+
function ledgerPath(projectDir, name) {
|
|
27749
|
+
return path5.join(ledgerDir(projectDir), `${normalizeLedgerName(name)}.md`);
|
|
27750
|
+
}
|
|
27751
|
+
function createLedgerSaveTool(ctx) {
|
|
27752
|
+
return tool({
|
|
27753
|
+
description: "Save a continuity ledger markdown document to .opencode/ledgers/.",
|
|
27754
|
+
args: {
|
|
27755
|
+
name: tool.schema.string().describe('Ledger name (e.g. "auth-refactor").'),
|
|
27756
|
+
content: tool.schema.string().describe("Markdown content.")
|
|
27757
|
+
},
|
|
27758
|
+
execute: async ({ name, content }) => {
|
|
27759
|
+
const dir = ledgerDir(ctx.directory);
|
|
27760
|
+
const outPath = ledgerPath(ctx.directory, name);
|
|
27761
|
+
try {
|
|
27762
|
+
await fs5.promises.mkdir(dir, { recursive: true });
|
|
27763
|
+
await fs5.promises.writeFile(outPath, content, "utf-8");
|
|
27764
|
+
return `Saved to .opencode/ledgers/${path5.basename(outPath)}`;
|
|
27765
|
+
} catch (err) {
|
|
27766
|
+
log("ledger_save failed", { error: String(err) });
|
|
27767
|
+
return `Error: failed to save ledger`;
|
|
27768
|
+
}
|
|
27769
|
+
}
|
|
27770
|
+
});
|
|
27771
|
+
}
|
|
27772
|
+
function createLedgerLoadTool(ctx) {
|
|
27773
|
+
return tool({
|
|
27774
|
+
description: "Load a continuity ledger markdown document from .opencode/ledgers/.",
|
|
27775
|
+
args: {
|
|
27776
|
+
name: tool.schema.string().optional().describe("Ledger name (defaults to most recent).")
|
|
27777
|
+
},
|
|
27778
|
+
execute: async ({ name }) => {
|
|
27779
|
+
const dir = ledgerDir(ctx.directory);
|
|
27780
|
+
try {
|
|
27781
|
+
if (name && name.trim().length > 0) {
|
|
27782
|
+
const p = ledgerPath(ctx.directory, name);
|
|
27783
|
+
if (!fs5.existsSync(p))
|
|
27784
|
+
return "No ledgers found";
|
|
27785
|
+
return await fs5.promises.readFile(p, "utf-8");
|
|
27786
|
+
}
|
|
27787
|
+
if (!fs5.existsSync(dir))
|
|
27788
|
+
return "No ledgers found";
|
|
27789
|
+
const entries = await fs5.promises.readdir(dir);
|
|
27790
|
+
const files = entries.filter((e) => e.toLowerCase().endsWith(".md")).map((e) => path5.join(dir, e));
|
|
27791
|
+
if (files.length === 0)
|
|
27792
|
+
return "No ledgers found";
|
|
27793
|
+
let latest = files[0];
|
|
27794
|
+
let latestMtime = -1;
|
|
27795
|
+
for (const f of files) {
|
|
27796
|
+
try {
|
|
27797
|
+
const st = await fs5.promises.stat(f);
|
|
27798
|
+
if (st.mtimeMs > latestMtime) {
|
|
27799
|
+
latestMtime = st.mtimeMs;
|
|
27800
|
+
latest = f;
|
|
27801
|
+
}
|
|
27802
|
+
} catch {}
|
|
27803
|
+
}
|
|
27804
|
+
return await fs5.promises.readFile(latest, "utf-8");
|
|
27805
|
+
} catch (err) {
|
|
27806
|
+
log("ledger_load failed", { error: String(err) });
|
|
27807
|
+
return "No ledgers found";
|
|
27808
|
+
}
|
|
27809
|
+
}
|
|
27810
|
+
});
|
|
27811
|
+
}
|
|
27812
|
+
|
|
27813
|
+
// src/tools/ast-search.ts
|
|
27814
|
+
import * as fs6 from "fs";
|
|
27815
|
+
import * as path6 from "path";
|
|
27816
|
+
import { spawnSync } from "child_process";
|
|
27817
|
+
var MAX_MATCHES = 50;
|
|
27818
|
+
function stripAnsi(s) {
|
|
27819
|
+
return s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
27820
|
+
}
|
|
27821
|
+
function resolveSearchPath(projectDir, p) {
|
|
27822
|
+
return path6.isAbsolute(p) ? p : path6.join(projectDir, p);
|
|
27823
|
+
}
|
|
27824
|
+
function candidatesFor(bin) {
|
|
27825
|
+
const out = [bin];
|
|
27826
|
+
if (process.platform === "win32") {
|
|
27827
|
+
out.push(`${bin}.exe`, `${bin}.cmd`, `${bin}.bat`);
|
|
27828
|
+
}
|
|
27829
|
+
return out;
|
|
27830
|
+
}
|
|
27831
|
+
function findAstGrepBinary() {
|
|
27832
|
+
const pathEnv = process.env.PATH ?? "";
|
|
27833
|
+
const dirs = pathEnv.split(path6.delimiter).filter(Boolean);
|
|
27834
|
+
const bins = ["sg", "ast-grep"];
|
|
27835
|
+
for (const dir of dirs) {
|
|
27836
|
+
for (const b of bins) {
|
|
27837
|
+
for (const cand of candidatesFor(b)) {
|
|
27838
|
+
const full = path6.join(dir, cand);
|
|
27839
|
+
try {
|
|
27840
|
+
const st = fs6.statSync(full);
|
|
27841
|
+
if (!st.isFile())
|
|
27842
|
+
continue;
|
|
27843
|
+
fs6.accessSync(full, fs6.constants.X_OK);
|
|
27844
|
+
return full;
|
|
27845
|
+
} catch {}
|
|
27846
|
+
}
|
|
27847
|
+
}
|
|
27848
|
+
}
|
|
27849
|
+
return null;
|
|
27850
|
+
}
|
|
27851
|
+
function normalizeOutput(stdout) {
|
|
27852
|
+
const lines = stripAnsi(stdout).split(`
|
|
27853
|
+
`).map((l) => l.trimEnd()).filter((l) => l.length > 0);
|
|
27854
|
+
const out = [];
|
|
27855
|
+
for (const line of lines) {
|
|
27856
|
+
const m3 = line.match(/^(.*?):(\d+):(\d+):(.*)$/);
|
|
27857
|
+
if (m3) {
|
|
27858
|
+
out.push(`${m3[1]}:${m3[2]}: ${m3[4].trim()}`);
|
|
27859
|
+
continue;
|
|
27860
|
+
}
|
|
27861
|
+
const m2 = line.match(/^(.*?):(\d+):(.*)$/);
|
|
27862
|
+
if (m2) {
|
|
27863
|
+
out.push(`${m2[1]}:${m2[2]}: ${m2[3].trim()}`);
|
|
27864
|
+
continue;
|
|
27865
|
+
}
|
|
27866
|
+
out.push(line);
|
|
27867
|
+
}
|
|
27868
|
+
return out.slice(0, MAX_MATCHES).join(`
|
|
27869
|
+
`);
|
|
27870
|
+
}
|
|
27871
|
+
function createAstSearchTool(ctx, binaryPath) {
|
|
27872
|
+
return tool({
|
|
27873
|
+
description: "AST-aware code search using ast-grep (sg).",
|
|
27874
|
+
args: {
|
|
27875
|
+
pattern: tool.schema.string().describe('ast-grep pattern (e.g. "console.log($$$)").'),
|
|
27876
|
+
path: tool.schema.string().optional().describe("Directory to search (default: project root)."),
|
|
27877
|
+
lang: tool.schema.string().optional().describe('Language (e.g. "typescript", "python").')
|
|
27878
|
+
},
|
|
27879
|
+
execute: async ({ pattern, path: searchPath, lang }) => {
|
|
27880
|
+
const target = resolveSearchPath(ctx.directory, searchPath ?? ".");
|
|
27881
|
+
const args = ["--pattern", pattern, target];
|
|
27882
|
+
if (lang && lang.trim().length > 0) {
|
|
27883
|
+
args.push("--lang", lang);
|
|
27884
|
+
}
|
|
27885
|
+
const res = spawnSync(binaryPath, args, {
|
|
27886
|
+
cwd: ctx.directory,
|
|
27887
|
+
encoding: "utf-8"
|
|
27888
|
+
});
|
|
27889
|
+
if (res.error) {
|
|
27890
|
+
log("ast_search spawn failed", { error: String(res.error) });
|
|
27891
|
+
return `Error: ${String(res.error)}`;
|
|
27892
|
+
}
|
|
27893
|
+
const stdout = res.stdout ?? "";
|
|
27894
|
+
const stderr = res.stderr ?? "";
|
|
27895
|
+
if ((res.status ?? 0) === 1 && stdout.trim().length === 0) {
|
|
27896
|
+
return "No matches";
|
|
27897
|
+
}
|
|
27898
|
+
if ((res.status ?? 0) !== 0 && (res.status ?? 0) !== 1) {
|
|
27899
|
+
log("ast_search failed", { status: res.status, stderr });
|
|
27900
|
+
return stderr.trim().length > 0 ? stderr.trim() : "Error: ast_search failed";
|
|
27901
|
+
}
|
|
27902
|
+
const normalized = normalizeOutput(stdout);
|
|
27903
|
+
return normalized.length > 0 ? normalized : "No matches";
|
|
27904
|
+
}
|
|
27905
|
+
});
|
|
27906
|
+
}
|
|
27907
|
+
|
|
27522
27908
|
// src/hooks/todo-enforcer.ts
|
|
27523
27909
|
var DEFAULT_MAX_ENFORCEMENTS = 5;
|
|
27524
27910
|
var sessionState = new Map;
|
|
@@ -27601,7 +27987,7 @@ ${unfinished.map((t) => `- [ ] ${t.text}`).join(`
|
|
|
27601
27987
|
}
|
|
27602
27988
|
|
|
27603
27989
|
// src/hooks/comment-checker.ts
|
|
27604
|
-
import * as
|
|
27990
|
+
import * as fs7 from "fs";
|
|
27605
27991
|
var AI_SLOP_PATTERNS = [
|
|
27606
27992
|
/\/\/ .{80,}/,
|
|
27607
27993
|
/\/\/ (This|The|We|Here|Note:)/i,
|
|
@@ -27680,7 +28066,7 @@ function createCommentCheckerHook(internalSessions, maxRatio = 0.3, slopThreshol
|
|
|
27680
28066
|
if (!filePath || !isCodeFile(filePath))
|
|
27681
28067
|
return;
|
|
27682
28068
|
try {
|
|
27683
|
-
const content =
|
|
28069
|
+
const content = fs7.readFileSync(filePath, "utf-8");
|
|
27684
28070
|
const result = checkComments(content, filePath, maxRatio, slopThreshold);
|
|
27685
28071
|
if (result.shouldWarn) {
|
|
27686
28072
|
output.output += `
|
|
@@ -27852,14 +28238,16 @@ class Semaphore {
|
|
|
27852
28238
|
this.active++;
|
|
27853
28239
|
return;
|
|
27854
28240
|
}
|
|
27855
|
-
return new Promise((
|
|
28241
|
+
return new Promise((resolve2) => {
|
|
27856
28242
|
this.queue.push(() => {
|
|
27857
28243
|
this.active++;
|
|
27858
|
-
|
|
28244
|
+
resolve2();
|
|
27859
28245
|
});
|
|
27860
28246
|
});
|
|
27861
28247
|
}
|
|
27862
28248
|
release() {
|
|
28249
|
+
if (this.active <= 0)
|
|
28250
|
+
return;
|
|
27863
28251
|
this.active--;
|
|
27864
28252
|
const next = this.queue.shift();
|
|
27865
28253
|
if (next)
|
|
@@ -27949,7 +28337,8 @@ async function registerMcps(ctx, disabled, apiKeys) {
|
|
|
27949
28337
|
if (config3.env) {
|
|
27950
28338
|
body.env = config3.env;
|
|
27951
28339
|
}
|
|
27952
|
-
|
|
28340
|
+
const client = ctx.client;
|
|
28341
|
+
await client.mcp?.add?.({ body });
|
|
27953
28342
|
log(`MCP ${name} registered${apiKey ? " (with API key)" : ""}`);
|
|
27954
28343
|
} catch (err) {
|
|
27955
28344
|
log(`MCP ${name} registration failed: ${err}`);
|
|
@@ -27975,11 +28364,18 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
27975
28364
|
resolveAgentModel
|
|
27976
28365
|
});
|
|
27977
28366
|
const ralphTools = createRalphLoopTools(ctx, internalSessions);
|
|
28367
|
+
const batchRead = createBatchReadTool(ctx);
|
|
28368
|
+
const ledgerSave = createLedgerSaveTool(ctx);
|
|
28369
|
+
const ledgerLoad = createLedgerLoadTool(ctx);
|
|
28370
|
+
const astGrepBin = findAstGrepBinary();
|
|
28371
|
+
const astSearch = astGrepBin ? createAstSearchTool(ctx, astGrepBin) : null;
|
|
27978
28372
|
const todoEnforcer = createTodoEnforcer(ctx, internalSessions, pluginConfig.todo_enforcer?.maxEnforcements);
|
|
27979
28373
|
const commentCheckerHook = createCommentCheckerHook(internalSessions, pluginConfig.comment_checker?.maxRatio, pluginConfig.comment_checker?.slopThreshold);
|
|
27980
28374
|
const tokenTruncationHook = createTokenTruncationHook(internalSessions, pluginConfig.token_truncation?.maxChars);
|
|
27981
|
-
const sessionCompactionHook = createSessionCompactionHook(ctx, internalSessions);
|
|
27982
|
-
const
|
|
28375
|
+
const sessionCompactionHook = createSessionCompactionHook({ client: ctx.client }, internalSessions);
|
|
28376
|
+
const fragmentInjector = createFragmentInjector(ctx, internalSessions, pluginConfig.fragments);
|
|
28377
|
+
const promptRendererHook = createPromptRendererHook(internalSessions, pluginConfig.prompt_renderer);
|
|
28378
|
+
const pendingKeywords = new TtlMap({ maxSize: 200, ttlMs: 5 * 60 * 1000 });
|
|
27983
28379
|
log("Config loaded", {
|
|
27984
28380
|
agentCount: Object.keys(agents).length,
|
|
27985
28381
|
disabledHooks: [...disabledHooks],
|
|
@@ -27999,6 +28395,18 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
27999
28395
|
if (!disabledTools.has("cancel_ralph")) {
|
|
28000
28396
|
toolRegistry.cancel_ralph = ralphTools.cancel_ralph;
|
|
28001
28397
|
}
|
|
28398
|
+
if (!disabledTools.has("batch_read")) {
|
|
28399
|
+
toolRegistry.batch_read = batchRead;
|
|
28400
|
+
}
|
|
28401
|
+
if (!disabledTools.has("ledger_save")) {
|
|
28402
|
+
toolRegistry.ledger_save = ledgerSave;
|
|
28403
|
+
}
|
|
28404
|
+
if (!disabledTools.has("ledger_load")) {
|
|
28405
|
+
toolRegistry.ledger_load = ledgerLoad;
|
|
28406
|
+
}
|
|
28407
|
+
if (!disabledTools.has("ast_search") && astSearch) {
|
|
28408
|
+
toolRegistry.ast_search = astSearch;
|
|
28409
|
+
}
|
|
28002
28410
|
return {
|
|
28003
28411
|
tool: toolRegistry,
|
|
28004
28412
|
config: async (config3) => {
|
|
@@ -28048,7 +28456,8 @@ var OpenCodeUltra = async (ctx) => {
|
|
|
28048
28456
|
if (output.message.variant === undefined) {
|
|
28049
28457
|
output.message.variant = "max";
|
|
28050
28458
|
}
|
|
28051
|
-
ctx.client
|
|
28459
|
+
const client = ctx.client;
|
|
28460
|
+
client.tui?.showToast?.({
|
|
28052
28461
|
body: {
|
|
28053
28462
|
title: "ULTRAWORK MODE",
|
|
28054
28463
|
message: "Maximum precision engaged. All agents at your disposal.",
|
|
@@ -28107,6 +28516,12 @@ ${arch}`);
|
|
|
28107
28516
|
${style}`);
|
|
28108
28517
|
}
|
|
28109
28518
|
}
|
|
28519
|
+
if (!disabledHooks.has("fragment-injector")) {
|
|
28520
|
+
await fragmentInjector(input, output);
|
|
28521
|
+
}
|
|
28522
|
+
if (!disabledHooks.has("prompt-renderer")) {
|
|
28523
|
+
promptRendererHook(input, output);
|
|
28524
|
+
}
|
|
28110
28525
|
},
|
|
28111
28526
|
"experimental.session.compacting": async (input, output) => {
|
|
28112
28527
|
if (disabledHooks.has("session-compaction"))
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple Map with max size eviction (LRU-like: evicts oldest entry when full).
|
|
3
|
+
* Optionally supports TTL-based expiry.
|
|
4
|
+
*/
|
|
5
|
+
export declare class TtlMap<K, V> {
|
|
6
|
+
private map;
|
|
7
|
+
private maxSize;
|
|
8
|
+
private ttlMs;
|
|
9
|
+
constructor(opts?: {
|
|
10
|
+
maxSize?: number;
|
|
11
|
+
ttlMs?: number;
|
|
12
|
+
});
|
|
13
|
+
get(key: K): V | undefined;
|
|
14
|
+
set(key: K, value: V): void;
|
|
15
|
+
has(key: K): boolean;
|
|
16
|
+
delete(key: K): boolean;
|
|
17
|
+
get size(): number;
|
|
18
|
+
clear(): void;
|
|
19
|
+
}
|