@clwnd/opencode 0.15.7 → 0.16.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/dist/index.js +97 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -390,11 +390,9 @@ function mapToolName(name) {
|
|
|
390
390
|
var BROKERED_TOOLS = /* @__PURE__ */ new Set(["webfetch", "websearch", "todowrite"]);
|
|
391
391
|
var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
392
392
|
"read",
|
|
393
|
-
"
|
|
394
|
-
"
|
|
393
|
+
"do_code",
|
|
394
|
+
"do_noncode",
|
|
395
395
|
"bash",
|
|
396
|
-
"glob",
|
|
397
|
-
"grep",
|
|
398
396
|
"webfetch",
|
|
399
397
|
"websearch",
|
|
400
398
|
"todowrite",
|
|
@@ -412,15 +410,18 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
|
|
|
412
410
|
"notebookedit",
|
|
413
411
|
"codesearch",
|
|
414
412
|
"applypatch",
|
|
415
|
-
"ls"
|
|
413
|
+
"ls",
|
|
414
|
+
// Replaced-and-banned. Do not forward. Do not re-enable.
|
|
415
|
+
"edit",
|
|
416
|
+
"write",
|
|
417
|
+
"glob",
|
|
418
|
+
"grep"
|
|
416
419
|
]);
|
|
417
420
|
var INPUT_FIELD_MAP = {
|
|
418
421
|
read: { file_path: "filePath" },
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
bash: {}
|
|
422
|
-
glob: {},
|
|
423
|
-
grep: {}
|
|
422
|
+
do_code: { file_path: "filePath", new_source: "newSource" },
|
|
423
|
+
do_noncode: { file_path: "filePath" },
|
|
424
|
+
bash: {}
|
|
424
425
|
};
|
|
425
426
|
function mapToolInput(toolName, input) {
|
|
426
427
|
const ocName = mapToolName(toolName);
|
|
@@ -748,14 +749,6 @@ function sanitizePrompt(text, word) {
|
|
|
748
749
|
while (kept.length > 0 && !kept[0].trim()) kept.shift();
|
|
749
750
|
return kept.join("");
|
|
750
751
|
}
|
|
751
|
-
function isAuxiliaryCall(opts) {
|
|
752
|
-
return opts.tools === void 0;
|
|
753
|
-
}
|
|
754
|
-
function pickAuxModel(primary) {
|
|
755
|
-
if (/^claude-opus/i.test(primary)) return "claude-sonnet-4-6";
|
|
756
|
-
if (/^claude-sonnet/i.test(primary)) return "claude-haiku-4-5";
|
|
757
|
-
return primary;
|
|
758
|
-
}
|
|
759
752
|
function isBrokeredToolReturn(prompt) {
|
|
760
753
|
if (prompt.length < 2) return false;
|
|
761
754
|
const last = prompt[prompt.length - 1];
|
|
@@ -769,6 +762,7 @@ function isBrokeredToolReturn(prompt) {
|
|
|
769
762
|
}
|
|
770
763
|
var sessionLastAgent = /* @__PURE__ */ new Map();
|
|
771
764
|
var sessionPetalCounts = /* @__PURE__ */ new Map();
|
|
765
|
+
var sessionJustCompacted = /* @__PURE__ */ new Map();
|
|
772
766
|
function detectAgent(sid, headers) {
|
|
773
767
|
const raw = headers?.["x-clwnd-agent"] ?? null;
|
|
774
768
|
if (!raw) return null;
|
|
@@ -839,18 +833,16 @@ var lastAllowedToolsHash = /* @__PURE__ */ new Map();
|
|
|
839
833
|
var pendingPenny = {
|
|
840
834
|
humDedup: 0,
|
|
841
835
|
reminderStripped: 0,
|
|
842
|
-
priorPetalsElided: 0
|
|
843
|
-
auxModelRouted: 0
|
|
836
|
+
priorPetalsElided: 0
|
|
844
837
|
};
|
|
845
838
|
function flushPenny() {
|
|
846
|
-
if (pendingPenny.humDedup === 0 && pendingPenny.reminderStripped === 0 && pendingPenny.priorPetalsElided === 0
|
|
839
|
+
if (pendingPenny.humDedup === 0 && pendingPenny.reminderStripped === 0 && pendingPenny.priorPetalsElided === 0) {
|
|
847
840
|
return void 0;
|
|
848
841
|
}
|
|
849
842
|
const snap = { ...pendingPenny };
|
|
850
843
|
pendingPenny.humDedup = 0;
|
|
851
844
|
pendingPenny.reminderStripped = 0;
|
|
852
845
|
pendingPenny.priorPetalsElided = 0;
|
|
853
|
-
pendingPenny.auxModelRouted = 0;
|
|
854
846
|
return snap;
|
|
855
847
|
}
|
|
856
848
|
function cheapHash(s) {
|
|
@@ -859,7 +851,7 @@ function cheapHash(s) {
|
|
|
859
851
|
return h.toString(36) + ":" + s.length;
|
|
860
852
|
}
|
|
861
853
|
var AGENT_DENY = {
|
|
862
|
-
plan: /* @__PURE__ */ new Set(["
|
|
854
|
+
plan: /* @__PURE__ */ new Set(["do_code", "do_noncode"])
|
|
863
855
|
};
|
|
864
856
|
function deriveAllowedTools(sid, opts) {
|
|
865
857
|
const agent = opts.headers?.["x-clwnd-agent"] ?? "";
|
|
@@ -870,7 +862,7 @@ function deriveAllowedTools(sid, opts) {
|
|
|
870
862
|
} catch {
|
|
871
863
|
}
|
|
872
864
|
const denied = AGENT_DENY[agentName] ?? /* @__PURE__ */ new Set();
|
|
873
|
-
const all = ["read", "
|
|
865
|
+
const all = ["read", "do_code", "do_noncode", "bash", "webfetch"];
|
|
874
866
|
const result = all.filter((t) => !denied.has(t));
|
|
875
867
|
const key = result.join(",");
|
|
876
868
|
const prev = lastAllowedTools.get(sid);
|
|
@@ -917,14 +909,6 @@ var ClwndModel = class {
|
|
|
917
909
|
warnings: []
|
|
918
910
|
};
|
|
919
911
|
}
|
|
920
|
-
if (isAuxiliaryCall(opts)) {
|
|
921
|
-
return {
|
|
922
|
-
content: [{ type: "text", text: "" }],
|
|
923
|
-
usage: zeroUsage(),
|
|
924
|
-
finishReason: { unified: "stop", raw: "stop" },
|
|
925
|
-
warnings: []
|
|
926
|
-
};
|
|
927
|
-
}
|
|
928
912
|
const { stream } = await this.doStream(opts);
|
|
929
913
|
const reader = stream.getReader();
|
|
930
914
|
const content = [];
|
|
@@ -966,7 +950,7 @@ var ClwndModel = class {
|
|
|
966
950
|
} catch {
|
|
967
951
|
}
|
|
968
952
|
const isTitleGen = agentName === "title";
|
|
969
|
-
const
|
|
953
|
+
const isCompaction = agentName === "compaction";
|
|
970
954
|
if (isTitleGen) {
|
|
971
955
|
trace("title.skip", { method: "doStream", sid });
|
|
972
956
|
return {
|
|
@@ -978,13 +962,8 @@ var ClwndModel = class {
|
|
|
978
962
|
})
|
|
979
963
|
};
|
|
980
964
|
}
|
|
981
|
-
const skipGraft =
|
|
982
|
-
if (skipGraft) trace("graft.skip", { method: "doStream", sid, reason: "
|
|
983
|
-
const effectiveModel = isEmptyTools ? pickAuxModel(self.modelId) : self.modelId;
|
|
984
|
-
if (effectiveModel !== self.modelId) {
|
|
985
|
-
pendingPenny.auxModelRouted++;
|
|
986
|
-
trace("aux.model.routed", { sid, primary: self.modelId, aux: effectiveModel });
|
|
987
|
-
}
|
|
965
|
+
const skipGraft = isCompaction;
|
|
966
|
+
if (skipGraft) trace("graft.skip", { method: "doStream", sid, reason: "compaction" });
|
|
988
967
|
const systemPromptHash = cheapHash(systemPrompt);
|
|
989
968
|
const permissionsHash = cheapHash(JSON.stringify(permissions));
|
|
990
969
|
const allowedToolsHash = cheapHash(allowedTools.join(","));
|
|
@@ -1046,15 +1025,21 @@ var ClwndModel = class {
|
|
|
1046
1025
|
if (!skipGraft) {
|
|
1047
1026
|
prevPetalCount = sessionPetalCounts.get(sid) ?? 0;
|
|
1048
1027
|
sessionPetalCounts.set(sid, priorPetals.length);
|
|
1049
|
-
|
|
1050
|
-
|
|
1028
|
+
const dropped = prevPetalCount - priorPetals.length;
|
|
1029
|
+
const justCompacted = sessionJustCompacted.get(sid) === true;
|
|
1030
|
+
if (justCompacted || prevPetalCount > 0 && dropped >= 2) {
|
|
1031
|
+
trace("compaction.detected", { sid, prev: prevPetalCount, now: priorPetals.length, dropped, reason: justCompacted ? "agent" : "petal-drop" });
|
|
1051
1032
|
hum({ chi: "cancel", sid, reason: "compaction" });
|
|
1033
|
+
sessionJustCompacted.delete(sid);
|
|
1052
1034
|
} else if (prevPetalCount === priorPetals.length && prevPetalCount > 0) {
|
|
1053
1035
|
elidePriorPetals = true;
|
|
1054
1036
|
pendingPenny.priorPetalsElided++;
|
|
1055
1037
|
trace("priorPetals.elided", { sid, count: priorPetals.length });
|
|
1056
1038
|
}
|
|
1057
1039
|
}
|
|
1040
|
+
if (isCompaction) {
|
|
1041
|
+
sessionJustCompacted.set(sid, true);
|
|
1042
|
+
}
|
|
1058
1043
|
const externalTools = [];
|
|
1059
1044
|
const externalToolNames = /* @__PURE__ */ new Set();
|
|
1060
1045
|
if (opts.tools) {
|
|
@@ -1066,9 +1051,10 @@ var ClwndModel = class {
|
|
|
1066
1051
|
externalToolNames.add(name);
|
|
1067
1052
|
}
|
|
1068
1053
|
}
|
|
1069
|
-
const
|
|
1070
|
-
trace("tools.available", { sid, count:
|
|
1054
|
+
const ocToolNames = opts.tools ? opts.tools.filter((t) => t.type === "function").map((t) => t.name) : [];
|
|
1055
|
+
trace("tools.available", { sid, count: ocToolNames.length, names: ocToolNames.join(",") });
|
|
1071
1056
|
if (externalTools.length > 0) trace("external.tools.detected", { sid, names: [...externalToolNames].join(",") });
|
|
1057
|
+
const visibleExternalNames = [...externalToolNames];
|
|
1072
1058
|
let promptSent = false;
|
|
1073
1059
|
if (listenOnly && humAlive) {
|
|
1074
1060
|
const pd = flushPenny();
|
|
@@ -1076,7 +1062,7 @@ var ClwndModel = class {
|
|
|
1076
1062
|
chi: "prompt",
|
|
1077
1063
|
sid,
|
|
1078
1064
|
cwd,
|
|
1079
|
-
modelId:
|
|
1065
|
+
modelId: self.modelId,
|
|
1080
1066
|
listenOnly: true,
|
|
1081
1067
|
...pd ? { pennyDelta: pd } : {},
|
|
1082
1068
|
dusk: duskIn(3e4)
|
|
@@ -1092,7 +1078,7 @@ var ClwndModel = class {
|
|
|
1092
1078
|
chi: "prompt",
|
|
1093
1079
|
sid,
|
|
1094
1080
|
cwd,
|
|
1095
|
-
modelId:
|
|
1081
|
+
modelId: self.modelId,
|
|
1096
1082
|
content,
|
|
1097
1083
|
text,
|
|
1098
1084
|
...sendSystemPrompt ? { systemPrompt } : {},
|
|
@@ -1104,7 +1090,7 @@ var ClwndModel = class {
|
|
|
1104
1090
|
...elidePriorPetals ? {} : { priorPetals },
|
|
1105
1091
|
externalTools: externalTools.length > 0 ? externalTools : void 0,
|
|
1106
1092
|
mcpServerConfigs: await getMcpServerConfigs(this.config.client),
|
|
1107
|
-
visibleTools:
|
|
1093
|
+
visibleTools: visibleExternalNames,
|
|
1108
1094
|
...pd ? { pennyDelta: pd } : {},
|
|
1109
1095
|
dusk: duskIn(3e4)
|
|
1110
1096
|
});
|
|
@@ -1456,8 +1442,11 @@ var clwndPlugin = async (input) => {
|
|
|
1456
1442
|
"chat.headers": async (ctx, output) => {
|
|
1457
1443
|
output.headers["x-clwnd-agent"] = typeof ctx.agent === "string" ? ctx.agent : ctx.agent?.name ?? JSON.stringify(ctx.agent);
|
|
1458
1444
|
},
|
|
1459
|
-
//
|
|
1460
|
-
//
|
|
1445
|
+
// Custom tools registered with OC's tool registry. Each one delegates
|
|
1446
|
+
// to clwnd's daemon via the same MCP HTTP endpoint that Claude CLI uses
|
|
1447
|
+
// — JSON-RPC POST to http://127.0.0.1:29147/s/<sid> with method
|
|
1448
|
+
// tools/call. No new transport, no new vocabulary: plugin tools and
|
|
1449
|
+
// Claude CLI tools share the exact same executeTool() dispatch.
|
|
1461
1450
|
tool: {
|
|
1462
1451
|
clwnd_permission: tool({
|
|
1463
1452
|
description: "Permission prompt for clwnd file system operations",
|
|
@@ -1470,7 +1459,7 @@ var clwndPlugin = async (input) => {
|
|
|
1470
1459
|
trace("permission.tool.invoked", { tool: args.tool, path: args.path });
|
|
1471
1460
|
const t0 = Date.now();
|
|
1472
1461
|
await ctx.ask({
|
|
1473
|
-
permission: args.tool === "
|
|
1462
|
+
permission: args.tool === "do_code" || args.tool === "do_noncode" ? "edit" : args.tool,
|
|
1474
1463
|
patterns: [args.path ?? "*"],
|
|
1475
1464
|
metadata: { tool: args.tool, filepath: args.path },
|
|
1476
1465
|
always: [args.path ?? "*"]
|
|
@@ -1480,10 +1469,68 @@ var clwndPlugin = async (input) => {
|
|
|
1480
1469
|
trace("permission.tool.approved", { tool: args.tool, askId, elapsed, autoAllowed: elapsed < 100 });
|
|
1481
1470
|
return JSON.stringify({ granted: true, tool: args.tool, askId });
|
|
1482
1471
|
}
|
|
1472
|
+
}),
|
|
1473
|
+
do_code: tool({
|
|
1474
|
+
description: `Author code in a code file via AST-grounded operations. Accepts: .ts/.tsx/.js/.jsx/.py/.go/.rs/.java/.cpp/... (code files only; non-code is rejected \u2014 use do_noncode for that). Five operations:
|
|
1475
|
+
- operation: 'create', new_source: '<code>' \u2014 create a new file with new_source as its content. Re-parsed for syntax, rejected if invalid.
|
|
1476
|
+
- operation: 'replace', symbol: 'Class.method', new_source: '<full new source of that symbol>' \u2014 byte-range splice replacing the symbol with new_source. Re-parsed.
|
|
1477
|
+
- operation: 'replace', new_source: '<whole file>' \u2014 full-file rewrite, re-parsed.
|
|
1478
|
+
- operation: 'insert_after' | 'insert_before', symbol: 'NAME', new_source: '<new code block>' \u2014 add a new symbol adjacent to an anchor. Re-parsed.
|
|
1479
|
+
- operation: 'delete', symbol: 'NAME' \u2014 remove a symbol. Re-parsed.
|
|
1480
|
+
Before calling do_code on an existing file, run read(file_path) or read(file_path, symbol: '...') first \u2014 clwnd's staleness guard rejects edits whose baseline is older than the current mtime. There is no old_string/new_string vocabulary here \u2014 this is NOT a string replace tool.`,
|
|
1481
|
+
args: {
|
|
1482
|
+
file_path: tool.schema.string().describe("Absolute path to the code file"),
|
|
1483
|
+
operation: tool.schema.enum(["create", "replace", "insert_before", "insert_after", "delete"]).optional().describe("Operation to perform (default: replace)"),
|
|
1484
|
+
symbol: tool.schema.string().optional().describe("Target symbol name (required for insert/delete, optional for replace)"),
|
|
1485
|
+
new_source: tool.schema.string().optional().describe("New source code (required for create/replace/insert)")
|
|
1486
|
+
},
|
|
1487
|
+
async execute(args, ctx) {
|
|
1488
|
+
return callClwndTool("do_code", args, ctx.sessionID);
|
|
1489
|
+
}
|
|
1490
|
+
}),
|
|
1491
|
+
do_noncode: tool({
|
|
1492
|
+
description: `Author non-code files (configs, docs, JSON, YAML, Markdown, txt, \u2026). Rejects any file with a code extension \u2014 use do_code for those. Modes:
|
|
1493
|
+
- mode: 'write' (default) \u2014 create or overwrite with content
|
|
1494
|
+
- mode: 'append' \u2014 add content to the end of an existing file
|
|
1495
|
+
- mode: 'prepend' \u2014 add content to the start of an existing file
|
|
1496
|
+
For existing files, read(file_path) first so clwnd's staleness guard knows your baseline.`,
|
|
1497
|
+
args: {
|
|
1498
|
+
file_path: tool.schema.string().describe("Absolute path to the non-code file"),
|
|
1499
|
+
content: tool.schema.string().describe("Content to write/append/prepend"),
|
|
1500
|
+
mode: tool.schema.enum(["write", "append", "prepend"]).optional().describe("Write mode (default: write)")
|
|
1501
|
+
},
|
|
1502
|
+
async execute(args, ctx) {
|
|
1503
|
+
return callClwndTool("do_noncode", args, ctx.sessionID);
|
|
1504
|
+
}
|
|
1483
1505
|
})
|
|
1484
1506
|
}
|
|
1485
1507
|
};
|
|
1486
1508
|
};
|
|
1509
|
+
var CLWND_MCP_PORT = parseInt(process.env.CLWND_MCP_PORT ?? "29147");
|
|
1510
|
+
var CLWND_MCP_HOST = process.env.CLWND_MCP_HOST ?? "127.0.0.1";
|
|
1511
|
+
async function callClwndTool(name, args, sessionID) {
|
|
1512
|
+
const url = `http://${CLWND_MCP_HOST}:${CLWND_MCP_PORT}/s/${sessionID}`;
|
|
1513
|
+
const body = {
|
|
1514
|
+
jsonrpc: "2.0",
|
|
1515
|
+
id: 1,
|
|
1516
|
+
method: "tools/call",
|
|
1517
|
+
params: { name, arguments: args }
|
|
1518
|
+
};
|
|
1519
|
+
try {
|
|
1520
|
+
const response = await fetch(url, {
|
|
1521
|
+
method: "POST",
|
|
1522
|
+
headers: { "Content-Type": "application/json" },
|
|
1523
|
+
body: JSON.stringify(body)
|
|
1524
|
+
});
|
|
1525
|
+
if (!response.ok) return `Error: clwnd MCP endpoint returned HTTP ${response.status}`;
|
|
1526
|
+
const data = await response.json();
|
|
1527
|
+
if (data.error) return `Error: ${data.error.message}`;
|
|
1528
|
+
const content = data.result?.content ?? [];
|
|
1529
|
+
return content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n") || "(no output)";
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
return `Error: failed to reach clwnd daemon at ${url} \u2014 ${e instanceof Error ? e.message : String(e)}`;
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1487
1534
|
export {
|
|
1488
1535
|
clwndPlugin,
|
|
1489
1536
|
createClwnd
|