@clwnd/opencode 0.15.7 → 0.16.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.
Files changed (2) hide show
  1. package/dist/index.js +97 -50
  2. 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
- "edit",
394
- "write",
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
- edit: { file_path: "filePath", old_string: "oldString", new_string: "newString", replace_all: "replaceAll" },
420
- write: { file_path: "filePath" },
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 && pendingPenny.auxModelRouted === 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(["edit", "write"])
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", "edit", "write", "bash", "glob", "grep", "webfetch"];
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 isEmptyTools = !opts.tools || opts.tools.length === 0;
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 = isEmptyTools;
982
- if (skipGraft) trace("graft.skip", { method: "doStream", sid, reason: "emptyTools", toolsLen: opts.tools?.length ?? "undefined" });
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
- if (prevPetalCount > 0 && priorPetals.length < prevPetalCount * 0.5) {
1050
- trace("compaction.detected", { sid, prev: prevPetalCount, now: priorPetals.length });
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 allToolNames = opts.tools ? opts.tools.filter((t) => t.type === "function").map((t) => t.name) : [];
1070
- trace("tools.available", { sid, count: allToolNames.length, names: allToolNames.join(",") });
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: effectiveModel,
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: effectiveModel,
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: allToolNames,
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
- // Permission tool provider emits clwnd_permission with providerExecuted: false.
1460
- // OC executes via resolveTools() ctx.ask() TUI permission dialog.
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 === "edit" || args.tool === "write" ? "edit" : 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") || 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clwnd/opencode",
3
- "version": "0.15.7",
3
+ "version": "0.16.0",
4
4
  "description": "clwnd for opencode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",