@multiclawcli/core 0.6.3 → 0.7.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/index.js CHANGED
@@ -241,6 +241,36 @@ function isPermanent(err) {
241
241
  return err instanceof PermanentError;
242
242
  }
243
243
 
244
+ // src/utils/toolSummary.ts
245
+ var MAX_SUMMARY_COLUMNS = 120;
246
+ function stringifySafe(value) {
247
+ if (typeof value === "string") return value;
248
+ if (value === void 0 || value === null) return "";
249
+ try {
250
+ return JSON.stringify(value) ?? "";
251
+ } catch {
252
+ return String(value);
253
+ }
254
+ }
255
+ function firstLineTruncated(value) {
256
+ const text = stringifySafe(value);
257
+ const newline = text.indexOf("\n");
258
+ const firstLine = newline === -1 ? text : text.slice(0, newline);
259
+ if (firstLine.length <= MAX_SUMMARY_COLUMNS) return firstLine;
260
+ return `${firstLine.slice(0, MAX_SUMMARY_COLUMNS - 1)}\u2026`;
261
+ }
262
+ function hasToolArgs(input) {
263
+ if (input === void 0 || input === null) return false;
264
+ if (typeof input !== "object" || Array.isArray(input)) return true;
265
+ return Object.keys(input).length > 0;
266
+ }
267
+ function summarizeToolInput(input) {
268
+ return hasToolArgs(input) ? firstLineTruncated(input) : "";
269
+ }
270
+ function summarizeToolOutput(output) {
271
+ return firstLineTruncated(output);
272
+ }
273
+
244
274
  // src/runtime/RuntimeBase.ts
245
275
  var RuntimeBase = class {
246
276
  };
@@ -305,16 +335,26 @@ var KIND_TO_TOOLS = {
305
335
  search: ["Grep", "Glob"],
306
336
  fetch: ["WebFetch"]
307
337
  };
338
+ function matchesTool(pattern, name) {
339
+ if (!pattern.includes("*")) return pattern === name;
340
+ const source = pattern.split("*").map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*");
341
+ return new RegExp(`^${source}$`).test(name);
342
+ }
308
343
  function toolPermitted(params, tools) {
309
- const allowed = new Set(tools.map((tool) => tool.toLowerCase()));
344
+ const allowed = tools.map((tool) => tool.toLowerCase());
310
345
  const name = params.toolCall.name ?? void 0;
311
- return name !== void 0 ? allowed.has(name.toLowerCase()) : (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some(
312
- (tool) => allowed.has(tool.toLowerCase())
313
- );
346
+ if (name !== void 0) {
347
+ const candidate = name.toLowerCase();
348
+ return allowed.some((pattern) => matchesTool(pattern, candidate));
349
+ }
350
+ return (KIND_TO_TOOLS[params.toolCall.kind ?? ""] ?? []).some((tool) => {
351
+ const candidate = tool.toLowerCase();
352
+ return allowed.some((pattern) => matchesTool(pattern, candidate));
353
+ });
314
354
  }
315
355
  function decidePermission(params, tools, sessionId) {
316
356
  if (sessionId !== void 0 && params.sessionId !== sessionId) {
317
- return { response: { outcome: { outcome: "cancelled" } } };
357
+ return { response: { outcome: { outcome: "cancelled" } }, approved: false };
318
358
  }
319
359
  const name = params.toolCall.name ?? void 0;
320
360
  const kind = params.toolCall.kind ?? void 0;
@@ -323,9 +363,10 @@ function decidePermission(params, tools, sessionId) {
323
363
  (o) => approved ? o.kind === "allow_once" : o.kind === "reject_once"
324
364
  );
325
365
  const response = option ? { outcome: { outcome: "selected", optionId: option.optionId } } : { outcome: { outcome: "cancelled" } };
326
- if (approved) return { response };
366
+ if (approved) return { response, approved };
327
367
  return {
328
368
  response,
369
+ approved,
329
370
  rejected: {
330
371
  toolCallId: params.toolCall.toolCallId,
331
372
  name,
@@ -506,6 +547,8 @@ var AcpRuntime = class extends RuntimeBase {
506
547
  let closeResolve;
507
548
  let closeReject;
508
549
  let connection;
550
+ const argsSurfaced = /* @__PURE__ */ new Set();
551
+ const toolNames = /* @__PURE__ */ new Map();
509
552
  const killTree = (signal) => {
510
553
  if (child.pid !== void 0) {
511
554
  try {
@@ -640,15 +683,23 @@ var AcpRuntime = class extends RuntimeBase {
640
683
  }
641
684
  break;
642
685
  }
643
- case "tool_call":
644
- emit({
645
- type: "tool_call",
646
- id: update.toolCallId,
647
- name: update.name ?? update.title,
648
- input: update.rawInput
649
- });
686
+ case "tool_call": {
687
+ const name = update.name ?? update.title;
688
+ toolNames.set(update.toolCallId, name);
689
+ if (hasToolArgs(update.rawInput)) argsSurfaced.add(update.toolCallId);
690
+ emit({ type: "tool_call", id: update.toolCallId, name, input: update.rawInput });
650
691
  break;
651
- case "tool_call_update":
692
+ }
693
+ case "tool_call_update": {
694
+ if (!argsSurfaced.has(update.toolCallId) && hasToolArgs(update.rawInput)) {
695
+ argsSurfaced.add(update.toolCallId);
696
+ emit({
697
+ type: "tool_call",
698
+ id: update.toolCallId,
699
+ name: update.name ?? toolNames.get(update.toolCallId) ?? update.title ?? "",
700
+ input: update.rawInput
701
+ });
702
+ }
652
703
  emit({
653
704
  type: "tool_result",
654
705
  id: update.toolCallId,
@@ -656,6 +707,7 @@ var AcpRuntime = class extends RuntimeBase {
656
707
  isError: update.status === "failed"
657
708
  });
658
709
  break;
710
+ }
659
711
  case "plan":
660
712
  emit({ type: "plan", entries: update.entries.map((entry3) => entry3.content) });
661
713
  break;
@@ -690,22 +742,26 @@ var AcpRuntime = class extends RuntimeBase {
690
742
  if (state !== "running" || !turn || params.sessionId !== sessionId || context.signal.aborted)
691
743
  return cancelled;
692
744
  const mode = turn.options.permissionMode ?? "auto";
693
- const { response, rejected } = decidePermission(params, options.tools, sessionId);
694
- if (rejected) {
695
- warn("Tool permission denied by the configured tools policy.");
696
- emit({
697
- type: "raw",
698
- vendor: "acp",
699
- payload: { type: "permission_rejected", ...rejected }
700
- });
701
- return response;
745
+ const { response, approved, rejected } = decidePermission(params, options.tools, sessionId);
746
+ if (approved) {
747
+ if (mode !== "deny") return response;
748
+ warn("Tool permission denied by the configured approval mode.");
749
+ return cancelled;
702
750
  }
703
- if (mode === "auto") return response;
704
- if (mode !== "ask" || !turn.options.onPermission) {
751
+ const askable = mode === "ask" && turn.options.onPermission !== void 0;
752
+ if (!askable) {
753
+ const label = rejected?.name ?? rejected?.kind ?? rejected?.toolCallId ?? "an unnamed tool";
705
754
  warn(
706
- mode === "ask" ? "Tool permission cancelled: no approval handler is available." : "Tool permission denied by the configured approval mode."
755
+ mode === "ask" ? `Tool permission cancelled: no approval handler is available (${label}).` : `Tool permission denied by the configured tools policy: ${label}.`
707
756
  );
708
- return cancelled;
757
+ if (rejected) {
758
+ emit({
759
+ type: "raw",
760
+ vendor: "acp",
761
+ payload: { type: "permission_rejected", ...rejected }
762
+ });
763
+ }
764
+ return mode === "deny" ? cancelled : response;
709
765
  }
710
766
  const signal = AbortSignal.any([turn.decisions.signal, context.signal]);
711
767
  const offered = new Set(params.options.map((option) => option.optionId));
@@ -736,7 +792,18 @@ var AcpRuntime = class extends RuntimeBase {
736
792
  aborted
737
793
  ]);
738
794
  if (signal.aborted || active !== turn || state !== "running") return cancelled;
739
- return optionId !== void 0 && offered.has(optionId) ? { outcome: { outcome: "selected", optionId } } : cancelled;
795
+ const chosen = params.options.find((option) => option.optionId === optionId);
796
+ if (optionId === void 0 || chosen === void 0 || !offered.has(optionId)) {
797
+ return cancelled;
798
+ }
799
+ if (chosen.kind.startsWith("reject") && rejected) {
800
+ emit({
801
+ type: "raw",
802
+ vendor: "acp",
803
+ payload: { type: "permission_rejected", ...rejected, source: "user" }
804
+ });
805
+ }
806
+ return { outcome: { outcome: "selected", optionId } };
740
807
  } catch {
741
808
  return cancelled;
742
809
  } finally {
@@ -2366,31 +2433,6 @@ function withTimeout(factory, ms, label, signal, options) {
2366
2433
  });
2367
2434
  }
2368
2435
 
2369
- // src/utils/toolSummary.ts
2370
- var MAX_SUMMARY_COLUMNS = 120;
2371
- function stringifySafe(value) {
2372
- if (typeof value === "string") return value;
2373
- if (value === void 0 || value === null) return "";
2374
- try {
2375
- return JSON.stringify(value) ?? "";
2376
- } catch {
2377
- return String(value);
2378
- }
2379
- }
2380
- function firstLineTruncated(value) {
2381
- const text = stringifySafe(value);
2382
- const newline = text.indexOf("\n");
2383
- const firstLine = newline === -1 ? text : text.slice(0, newline);
2384
- if (firstLine.length <= MAX_SUMMARY_COLUMNS) return firstLine;
2385
- return `${firstLine.slice(0, MAX_SUMMARY_COLUMNS - 1)}\u2026`;
2386
- }
2387
- function summarizeToolInput(input) {
2388
- return firstLineTruncated(input);
2389
- }
2390
- function summarizeToolOutput(output) {
2391
- return firstLineTruncated(output);
2392
- }
2393
-
2394
2436
  // src/orchestrator/AgentRunner.ts
2395
2437
  var AgentRunner = class {
2396
2438
  constructor(eventBus, contextManager, globalWorkDir, config, sessionManager) {
@@ -2451,6 +2493,16 @@ ${prompt}`;
2451
2493
  if (instruction) prompt += `
2452
2494
 
2453
2495
  ${instruction}`;
2496
+ const toolRows = /* @__PURE__ */ new Set();
2497
+ const toolNames = /* @__PURE__ */ new Map();
2498
+ const printToolRow = (id, name, input) => {
2499
+ if (toolRows.has(id)) return;
2500
+ toolRows.add(id);
2501
+ const summary = summarizeToolInput(input);
2502
+ this.emitOutput(agent.id, `
2503
+ \u203A ${name}${summary ? ` ${summary}` : ""}
2504
+ `, "tool");
2505
+ };
2454
2506
  const task = {
2455
2507
  agentId: agent.id,
2456
2508
  systemPrompt: agent.systemPrompt,
@@ -2492,17 +2544,13 @@ ${instruction}`;
2492
2544
  return;
2493
2545
  }
2494
2546
  if (e.type === "tool_call") {
2495
- const summary = summarizeToolInput(e.input);
2496
- this.emitOutput(
2497
- agent.id,
2498
- `
2499
- \u203A ${e.name}${summary ? ` ${summary}` : ""}
2500
- `,
2501
- "tool"
2502
- );
2547
+ toolNames.set(e.id, e.name);
2548
+ if (summarizeToolInput(e.input)) printToolRow(e.id, e.name, e.input);
2503
2549
  return;
2504
2550
  }
2505
2551
  if (e.type === "tool_result") {
2552
+ const name = toolNames.get(e.id);
2553
+ if (name !== void 0) printToolRow(e.id, name, void 0);
2506
2554
  const summary = summarizeToolOutput(e.output);
2507
2555
  this.emitOutput(
2508
2556
  agent.id,
@@ -28,7 +28,14 @@ export type RuntimeEvent = (RuntimeEventBase & {
28
28
  type: "message";
29
29
  role: "agent" | "thought" | "user";
30
30
  text: string;
31
- }) | (RuntimeEventBase & {
31
+ })
32
+ /**
33
+ * A tool call. The same `id` may arrive more than once: providers announce a
34
+ * call before its arguments have streamed, so the first event can carry an
35
+ * empty `input` and a later one the completed arguments. Consumers should key
36
+ * on `id` and render a row once.
37
+ */
38
+ | (RuntimeEventBase & {
32
39
  type: "tool_call";
33
40
  id: string;
34
41
  name: string;
@@ -7,6 +7,15 @@
7
7
  * truncated line and are deliberately total — a summary is diagnostics and must
8
8
  * never be the reason a run fails.
9
9
  */
10
+ /**
11
+ * Whether a tool call was given any arguments.
12
+ *
13
+ * "No arguments yet" reaches us as `{}` (or missing) rather than as
14
+ * `undefined`: ACP announces a call before its input has finished streaming.
15
+ * Both mean the same thing to a reader — there is nothing to show yet — so the
16
+ * two are collapsed here instead of at every call site.
17
+ */
18
+ export declare function hasToolArgs(input: unknown): boolean;
10
19
  /** One-line summary of a tool call's input. `input` is untrusted and may be circular. */
11
20
  export declare function summarizeToolInput(input: unknown): string;
12
21
  /** One-line summary of a tool result's output, which may be a string or a value. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiclawcli/core",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",