@clwnd/opencode 0.15.6 → 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 +115 -60
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@ var DEFAULTS = {
17
17
  smallModel: "",
18
18
  permissionDusk: 6e4,
19
19
  droned: false,
20
- droneModel: { providerID: "opencode-clwnd", modelID: "claude-haiku-4-5" }
20
+ droneModel: { providerID: "opencode-clwnd", modelID: "claude-haiku-4-5" },
21
+ ocCompaction: false
21
22
  };
22
23
  var CONFIG_PATHS = [
23
24
  join(process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? "/", ".config"), "clwnd", "clwnd.json")
@@ -34,7 +35,8 @@ function loadConfig() {
34
35
  smallModel: raw.smallModel ?? DEFAULTS.smallModel,
35
36
  permissionDusk: raw.permissionDusk ?? DEFAULTS.permissionDusk,
36
37
  droned: raw.droned ?? DEFAULTS.droned,
37
- droneModel: raw.droneModel ?? DEFAULTS.droneModel
38
+ droneModel: raw.droneModel ?? DEFAULTS.droneModel,
39
+ ocCompaction: raw.ocCompaction ?? DEFAULTS.ocCompaction
38
40
  };
39
41
  return cached;
40
42
  } catch {
@@ -388,11 +390,9 @@ function mapToolName(name) {
388
390
  var BROKERED_TOOLS = /* @__PURE__ */ new Set(["webfetch", "websearch", "todowrite"]);
389
391
  var KNOWN_TOOLS = /* @__PURE__ */ new Set([
390
392
  "read",
391
- "edit",
392
- "write",
393
+ "do_code",
394
+ "do_noncode",
393
395
  "bash",
394
- "glob",
395
- "grep",
396
396
  "webfetch",
397
397
  "websearch",
398
398
  "todowrite",
@@ -410,15 +410,18 @@ var KNOWN_TOOLS = /* @__PURE__ */ new Set([
410
410
  "notebookedit",
411
411
  "codesearch",
412
412
  "applypatch",
413
- "ls"
413
+ "ls",
414
+ // Replaced-and-banned. Do not forward. Do not re-enable.
415
+ "edit",
416
+ "write",
417
+ "glob",
418
+ "grep"
414
419
  ]);
415
420
  var INPUT_FIELD_MAP = {
416
421
  read: { file_path: "filePath" },
417
- edit: { file_path: "filePath", old_string: "oldString", new_string: "newString", replace_all: "replaceAll" },
418
- write: { file_path: "filePath" },
419
- bash: {},
420
- glob: {},
421
- grep: {}
422
+ do_code: { file_path: "filePath", new_source: "newSource" },
423
+ do_noncode: { file_path: "filePath" },
424
+ bash: {}
422
425
  };
423
426
  function mapToolInput(toolName, input) {
424
427
  const ocName = mapToolName(toolName);
@@ -474,7 +477,7 @@ function defaultSocketPath() {
474
477
  var HUM_PATH = (process.env.CLWND_SOCKET ?? defaultSocketPath()) + ".hum";
475
478
  var humSocket = null;
476
479
  var humEcho = "";
477
- var humHearer = null;
480
+ var humHearers = /* @__PURE__ */ new Map();
478
481
  var humAlive = false;
479
482
  var humReady = null;
480
483
  var humAwaken = awakenHum();
@@ -552,7 +555,11 @@ async function awakenHum() {
552
555
  trace("hum.pulse", { kind: msg.kind, sid: msg.sid });
553
556
  continue;
554
557
  }
555
- if (humHearer) humHearer(msg);
558
+ const msgSid = typeof msg.sid === "string" ? msg.sid : void 0;
559
+ if (msgSid) {
560
+ const h = humHearers.get(msgSid);
561
+ if (h) h(msg);
562
+ }
556
563
  } catch {
557
564
  }
558
565
  }
@@ -594,15 +601,15 @@ function hum(msg) {
594
601
  writeLog("trace", "hum.send.failed", { err: String(e) });
595
602
  }
596
603
  }
597
- function humHear(onMessage) {
604
+ function humHear(sid, onMessage) {
598
605
  return new Promise((resolve) => {
599
- humHearer = (incoming) => {
606
+ humHearers.set(sid, (incoming) => {
600
607
  onMessage(incoming);
601
608
  if (incoming.chi === "finish" || incoming.chi === "error") {
602
- humHearer = null;
609
+ humHearers.delete(sid);
603
610
  resolve();
604
611
  }
605
- };
612
+ });
606
613
  });
607
614
  }
608
615
  function extractContent(prompt, sessionId) {
@@ -742,14 +749,6 @@ function sanitizePrompt(text, word) {
742
749
  while (kept.length > 0 && !kept[0].trim()) kept.shift();
743
750
  return kept.join("");
744
751
  }
745
- function isAuxiliaryCall(opts) {
746
- return opts.tools === void 0;
747
- }
748
- function pickAuxModel(primary) {
749
- if (/^claude-opus/i.test(primary)) return "claude-sonnet-4-6";
750
- if (/^claude-sonnet/i.test(primary)) return "claude-haiku-4-5";
751
- return primary;
752
- }
753
752
  function isBrokeredToolReturn(prompt) {
754
753
  if (prompt.length < 2) return false;
755
754
  const last = prompt[prompt.length - 1];
@@ -763,6 +762,7 @@ function isBrokeredToolReturn(prompt) {
763
762
  }
764
763
  var sessionLastAgent = /* @__PURE__ */ new Map();
765
764
  var sessionPetalCounts = /* @__PURE__ */ new Map();
765
+ var sessionJustCompacted = /* @__PURE__ */ new Map();
766
766
  function detectAgent(sid, headers) {
767
767
  const raw = headers?.["x-clwnd-agent"] ?? null;
768
768
  if (!raw) return null;
@@ -833,18 +833,16 @@ var lastAllowedToolsHash = /* @__PURE__ */ new Map();
833
833
  var pendingPenny = {
834
834
  humDedup: 0,
835
835
  reminderStripped: 0,
836
- priorPetalsElided: 0,
837
- auxModelRouted: 0
836
+ priorPetalsElided: 0
838
837
  };
839
838
  function flushPenny() {
840
- if (pendingPenny.humDedup === 0 && pendingPenny.reminderStripped === 0 && pendingPenny.priorPetalsElided === 0 && pendingPenny.auxModelRouted === 0) {
839
+ if (pendingPenny.humDedup === 0 && pendingPenny.reminderStripped === 0 && pendingPenny.priorPetalsElided === 0) {
841
840
  return void 0;
842
841
  }
843
842
  const snap = { ...pendingPenny };
844
843
  pendingPenny.humDedup = 0;
845
844
  pendingPenny.reminderStripped = 0;
846
845
  pendingPenny.priorPetalsElided = 0;
847
- pendingPenny.auxModelRouted = 0;
848
846
  return snap;
849
847
  }
850
848
  function cheapHash(s) {
@@ -853,7 +851,7 @@ function cheapHash(s) {
853
851
  return h.toString(36) + ":" + s.length;
854
852
  }
855
853
  var AGENT_DENY = {
856
- plan: /* @__PURE__ */ new Set(["edit", "write"])
854
+ plan: /* @__PURE__ */ new Set(["do_code", "do_noncode"])
857
855
  };
858
856
  function deriveAllowedTools(sid, opts) {
859
857
  const agent = opts.headers?.["x-clwnd-agent"] ?? "";
@@ -864,7 +862,7 @@ function deriveAllowedTools(sid, opts) {
864
862
  } catch {
865
863
  }
866
864
  const denied = AGENT_DENY[agentName] ?? /* @__PURE__ */ new Set();
867
- const all = ["read", "edit", "write", "bash", "glob", "grep", "webfetch"];
865
+ const all = ["read", "do_code", "do_noncode", "bash", "webfetch"];
868
866
  const result = all.filter((t) => !denied.has(t));
869
867
  const key = result.join(",");
870
868
  const prev = lastAllowedTools.get(sid);
@@ -911,14 +909,6 @@ var ClwndModel = class {
911
909
  warnings: []
912
910
  };
913
911
  }
914
- if (isAuxiliaryCall(opts)) {
915
- return {
916
- content: [{ type: "text", text: "" }],
917
- usage: zeroUsage(),
918
- finishReason: { unified: "stop", raw: "stop" },
919
- warnings: []
920
- };
921
- }
922
912
  const { stream } = await this.doStream(opts);
923
913
  const reader = stream.getReader();
924
914
  const content = [];
@@ -960,7 +950,7 @@ var ClwndModel = class {
960
950
  } catch {
961
951
  }
962
952
  const isTitleGen = agentName === "title";
963
- const isEmptyTools = !opts.tools || opts.tools.length === 0;
953
+ const isCompaction = agentName === "compaction";
964
954
  if (isTitleGen) {
965
955
  trace("title.skip", { method: "doStream", sid });
966
956
  return {
@@ -972,13 +962,8 @@ var ClwndModel = class {
972
962
  })
973
963
  };
974
964
  }
975
- const skipGraft = isEmptyTools;
976
- if (skipGraft) trace("graft.skip", { method: "doStream", sid, reason: "emptyTools", toolsLen: opts.tools?.length ?? "undefined" });
977
- const effectiveModel = isEmptyTools ? pickAuxModel(self.modelId) : self.modelId;
978
- if (effectiveModel !== self.modelId) {
979
- pendingPenny.auxModelRouted++;
980
- trace("aux.model.routed", { sid, primary: self.modelId, aux: effectiveModel });
981
- }
965
+ const skipGraft = isCompaction;
966
+ if (skipGraft) trace("graft.skip", { method: "doStream", sid, reason: "compaction" });
982
967
  const systemPromptHash = cheapHash(systemPrompt);
983
968
  const permissionsHash = cheapHash(JSON.stringify(permissions));
984
969
  const allowedToolsHash = cheapHash(allowedTools.join(","));
@@ -996,7 +981,8 @@ var ClwndModel = class {
996
981
  if (!lt || !Array.isArray(lt.content)) return false;
997
982
  for (const p of lt.content) {
998
983
  if (p.type === "tool-result" && p.toolCallId?.startsWith("perm-")) {
999
- const rawOutput = p.output ?? p.result;
984
+ const loose = p;
985
+ const rawOutput = loose.output ?? loose.result;
1000
986
  try {
1001
987
  const outer = typeof rawOutput === "string" ? JSON.parse(rawOutput) : rawOutput;
1002
988
  const inner = outer?.value ?? outer;
@@ -1039,15 +1025,21 @@ var ClwndModel = class {
1039
1025
  if (!skipGraft) {
1040
1026
  prevPetalCount = sessionPetalCounts.get(sid) ?? 0;
1041
1027
  sessionPetalCounts.set(sid, priorPetals.length);
1042
- if (prevPetalCount > 0 && priorPetals.length < prevPetalCount * 0.5) {
1043
- 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" });
1044
1032
  hum({ chi: "cancel", sid, reason: "compaction" });
1033
+ sessionJustCompacted.delete(sid);
1045
1034
  } else if (prevPetalCount === priorPetals.length && prevPetalCount > 0) {
1046
1035
  elidePriorPetals = true;
1047
1036
  pendingPenny.priorPetalsElided++;
1048
1037
  trace("priorPetals.elided", { sid, count: priorPetals.length });
1049
1038
  }
1050
1039
  }
1040
+ if (isCompaction) {
1041
+ sessionJustCompacted.set(sid, true);
1042
+ }
1051
1043
  const externalTools = [];
1052
1044
  const externalToolNames = /* @__PURE__ */ new Set();
1053
1045
  if (opts.tools) {
@@ -1059,9 +1051,10 @@ var ClwndModel = class {
1059
1051
  externalToolNames.add(name);
1060
1052
  }
1061
1053
  }
1062
- const allToolNames = opts.tools ? opts.tools.filter((t) => t.type === "function").map((t) => t.name) : [];
1063
- 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(",") });
1064
1056
  if (externalTools.length > 0) trace("external.tools.detected", { sid, names: [...externalToolNames].join(",") });
1057
+ const visibleExternalNames = [...externalToolNames];
1065
1058
  let promptSent = false;
1066
1059
  if (listenOnly && humAlive) {
1067
1060
  const pd = flushPenny();
@@ -1069,7 +1062,7 @@ var ClwndModel = class {
1069
1062
  chi: "prompt",
1070
1063
  sid,
1071
1064
  cwd,
1072
- modelId: effectiveModel,
1065
+ modelId: self.modelId,
1073
1066
  listenOnly: true,
1074
1067
  ...pd ? { pennyDelta: pd } : {},
1075
1068
  dusk: duskIn(3e4)
@@ -1085,7 +1078,7 @@ var ClwndModel = class {
1085
1078
  chi: "prompt",
1086
1079
  sid,
1087
1080
  cwd,
1088
- modelId: effectiveModel,
1081
+ modelId: self.modelId,
1089
1082
  content,
1090
1083
  text,
1091
1084
  ...sendSystemPrompt ? { systemPrompt } : {},
@@ -1097,7 +1090,7 @@ var ClwndModel = class {
1097
1090
  ...elidePriorPetals ? {} : { priorPetals },
1098
1091
  externalTools: externalTools.length > 0 ? externalTools : void 0,
1099
1092
  mcpServerConfigs: await getMcpServerConfigs(this.config.client),
1100
- visibleTools: allToolNames,
1093
+ visibleTools: visibleExternalNames,
1101
1094
  ...pd ? { pennyDelta: pd } : {},
1102
1095
  dusk: duskIn(3e4)
1103
1096
  });
@@ -1125,6 +1118,7 @@ var ClwndModel = class {
1125
1118
  function wilt() {
1126
1119
  if (done) return;
1127
1120
  done = true;
1121
+ humHearers.delete(sid);
1128
1122
  try {
1129
1123
  controller.close();
1130
1124
  } catch {
@@ -1145,7 +1139,7 @@ var ClwndModel = class {
1145
1139
  return;
1146
1140
  }
1147
1141
  petal({ type: "stream-start", warnings: [] });
1148
- const humFade = humHear(onHummin);
1142
+ const humFade = humHear(sid, onHummin);
1149
1143
  if (!promptSent) {
1150
1144
  hum({
1151
1145
  chi: "prompt",
@@ -1448,8 +1442,11 @@ var clwndPlugin = async (input) => {
1448
1442
  "chat.headers": async (ctx, output) => {
1449
1443
  output.headers["x-clwnd-agent"] = typeof ctx.agent === "string" ? ctx.agent : ctx.agent?.name ?? JSON.stringify(ctx.agent);
1450
1444
  },
1451
- // Permission tool provider emits clwnd_permission with providerExecuted: false.
1452
- // 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.
1453
1450
  tool: {
1454
1451
  clwnd_permission: tool({
1455
1452
  description: "Permission prompt for clwnd file system operations",
@@ -1462,7 +1459,7 @@ var clwndPlugin = async (input) => {
1462
1459
  trace("permission.tool.invoked", { tool: args.tool, path: args.path });
1463
1460
  const t0 = Date.now();
1464
1461
  await ctx.ask({
1465
- permission: args.tool === "edit" || args.tool === "write" ? "edit" : args.tool,
1462
+ permission: args.tool === "do_code" || args.tool === "do_noncode" ? "edit" : args.tool,
1466
1463
  patterns: [args.path ?? "*"],
1467
1464
  metadata: { tool: args.tool, filepath: args.path },
1468
1465
  always: [args.path ?? "*"]
@@ -1472,10 +1469,68 @@ var clwndPlugin = async (input) => {
1472
1469
  trace("permission.tool.approved", { tool: args.tool, askId, elapsed, autoAllowed: elapsed < 100 });
1473
1470
  return JSON.stringify({ granted: true, tool: args.tool, askId });
1474
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
+ }
1475
1505
  })
1476
1506
  }
1477
1507
  };
1478
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
+ }
1479
1534
  export {
1480
1535
  clwndPlugin,
1481
1536
  createClwnd
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clwnd/opencode",
3
- "version": "0.15.6",
3
+ "version": "0.16.0",
4
4
  "description": "clwnd for opencode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",