@shanepadgett/tau-agent 0.36.0 → 0.38.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.
@@ -0,0 +1,501 @@
1
+ import { parse } from "unbash";
2
+ import type { ArithmeticExpression, Command, Node, ParsedScript, Redirect, Word, WordPart } from "unbash";
3
+
4
+ const ALWAYS_SKIP = new Set([
5
+ "cat",
6
+ "head",
7
+ "tail",
8
+ "wc",
9
+ "ls",
10
+ "tree",
11
+ "pwd",
12
+ "echo",
13
+ "printf",
14
+ "true",
15
+ "false",
16
+ "test",
17
+ "[",
18
+ "grep",
19
+ "egrep",
20
+ "fgrep",
21
+ "cut",
22
+ "tr",
23
+ "uniq",
24
+ "comm",
25
+ "cmp",
26
+ "paste",
27
+ "join",
28
+ "column",
29
+ "fmt",
30
+ "fold",
31
+ "nl",
32
+ "rev",
33
+ "tac",
34
+ "expand",
35
+ "unexpand",
36
+ "tsort",
37
+ "pr",
38
+ "seq",
39
+ "expr",
40
+ "getconf",
41
+ "id",
42
+ "whoami",
43
+ "uname",
44
+ "printenv",
45
+ "type",
46
+ "which",
47
+ "dirname",
48
+ "basename",
49
+ "readlink",
50
+ "realpath",
51
+ "file",
52
+ "stat",
53
+ "du",
54
+ "df",
55
+ "diff",
56
+ "md5sum",
57
+ "sha1sum",
58
+ "sha256sum",
59
+ "shasum",
60
+ "ps",
61
+ "uptime",
62
+ "sleep",
63
+ "cd",
64
+ "bat",
65
+ "jq",
66
+ ]);
67
+
68
+ const FIND_VETO = new Set(["-exec", "-execdir", "-ok", "-okdir", "-delete", "-fls", "-fprint", "-fprint0", "-fprintf"]);
69
+
70
+ const GIT_READ_VERBS = new Set([
71
+ "status",
72
+ "log",
73
+ "diff",
74
+ "show",
75
+ "blame",
76
+ "rev-parse",
77
+ "describe",
78
+ "ls-files",
79
+ "ls-tree",
80
+ "cat-file",
81
+ "rev-list",
82
+ "name-rev",
83
+ "merge-base",
84
+ "reflog",
85
+ "grep",
86
+ "for-each-ref",
87
+ "shortlog",
88
+ "version",
89
+ "help",
90
+ "count-objects",
91
+ "whatchanged",
92
+ "cherry",
93
+ "range-diff",
94
+ "var",
95
+ ]);
96
+
97
+ const WRITE_REDIRECTS = new Set([">", ">>", ">|", "&>", "&>>", "<>"]);
98
+ const SED_PRINT = /^(?:\d+|\d+,\d+)p$/;
99
+
100
+ interface WalkState {
101
+ sawCd: boolean;
102
+ sawGit: boolean;
103
+ }
104
+
105
+ export function isAllowlistedBash(command: string): boolean {
106
+ if (!command.trim()) return false;
107
+ let script: ParsedScript;
108
+ try {
109
+ script = parse(command);
110
+ } catch {
111
+ return false;
112
+ }
113
+ const state: WalkState = { sawCd: false, sawGit: false };
114
+ return walkScript(script, state) && !(state.sawCd && state.sawGit);
115
+ }
116
+
117
+ function walkScript(script: ParsedScript, state: WalkState): boolean {
118
+ if (script.errors && script.errors.length > 0) return false;
119
+ if (script.shebang) return false;
120
+ if (script.commands.length === 0) return false;
121
+ return script.commands.every((statement) => walkNode(statement, state));
122
+ }
123
+
124
+ function walkNode(node: Node, state: WalkState): boolean {
125
+ switch (node.type) {
126
+ case "Statement":
127
+ if (node.background) return false;
128
+ return walkRedirects(node.redirects, state) && walkNode(node.command, state);
129
+ case "Command":
130
+ return walkCommand(node, state);
131
+ case "Pipeline":
132
+ if (node.time || node.negated) return false;
133
+ if (node.operators.includes("|&")) return false;
134
+ return node.commands.every((child) => walkNode(child, state));
135
+ case "AndOr":
136
+ return node.commands.every((child) => walkNode(child, state));
137
+ case "CompoundList":
138
+ return node.commands.every((child) => walkNode(child, state));
139
+ default:
140
+ return false;
141
+ }
142
+ }
143
+
144
+ function walkCommand(command: Command, state: WalkState): boolean {
145
+ if (command.prefix.length > 0) return false;
146
+ if (!command.name) return false;
147
+ if (!walkRedirects(command.redirects, state)) return false;
148
+ if (!walkWord(command.name, state)) return false;
149
+ if (!command.suffix.every((word) => walkWord(word, state))) return false;
150
+
151
+ const name = staticBareName(command.name);
152
+ if (!name) return false;
153
+ if (name === "cd") state.sawCd = true;
154
+ if (name === "git") state.sawGit = true;
155
+ if (state.sawCd && state.sawGit) return false;
156
+ if (ALWAYS_SKIP.has(name)) return true;
157
+
158
+ const args: string[] = [];
159
+ for (const word of command.suffix) {
160
+ const value = staticWordValue(word);
161
+ if (value === undefined) return false;
162
+ args.push(value);
163
+ }
164
+
165
+ switch (name) {
166
+ case "find":
167
+ return !args.some((arg) => FIND_VETO.has(arg));
168
+ case "fd":
169
+ return !hasFdVeto(args);
170
+ case "rg":
171
+ return !hasRgVeto(args);
172
+ case "sort":
173
+ case "base64":
174
+ case "iconv":
175
+ return !hasOutputFlag(args);
176
+ case "xxd":
177
+ return !hasShortOption(args, "r") && !hasLongOption(args, "revert");
178
+ case "yq":
179
+ return !hasYqInplace(args);
180
+ case "sed":
181
+ return isSafeSed(args);
182
+ case "hostname":
183
+ return !hasPositional(args);
184
+ case "date":
185
+ return !hasShortOption(args, "s") && !hasLongOption(args, "set");
186
+ case "git":
187
+ return isSafeGit(args);
188
+ default:
189
+ return false;
190
+ }
191
+ }
192
+
193
+ function walkRedirects(redirects: Redirect[], state: WalkState): boolean {
194
+ for (const redirect of redirects) {
195
+ if (redirect.target && !walkWord(redirect.target, state)) return false;
196
+ if (redirect.body && !walkWord(redirect.body, state)) return false;
197
+ if (WRITE_REDIRECTS.has(redirect.operator)) {
198
+ if (!redirect.target || staticWordValue(redirect.target) !== "/dev/null") return false;
199
+ continue;
200
+ }
201
+ if (redirect.operator === ">&" || redirect.operator === "<&") {
202
+ const target = redirect.target ? staticWordValue(redirect.target) : undefined;
203
+ if (target === "1" || target === "2" || target === "-") continue;
204
+ if (redirect.operator === "<&" && target === "0") continue;
205
+ return false;
206
+ }
207
+ if (
208
+ redirect.operator !== "<" &&
209
+ redirect.operator !== "<<" &&
210
+ redirect.operator !== "<<-" &&
211
+ redirect.operator !== "<<<"
212
+ ) {
213
+ return false;
214
+ }
215
+ }
216
+ return true;
217
+ }
218
+
219
+ function walkWord(word: Word, state: WalkState): boolean {
220
+ const parts = word.parts;
221
+ if (!parts) return true;
222
+ return parts.every((part) => walkPart(part, state));
223
+ }
224
+
225
+ function walkPart(part: WordPart, state: WalkState): boolean {
226
+ switch (part.type) {
227
+ case "Literal":
228
+ case "SingleQuoted":
229
+ case "AnsiCQuoted":
230
+ case "SimpleExpansion":
231
+ return true;
232
+ case "DoubleQuoted":
233
+ case "LocaleString":
234
+ return part.parts.every((child) => walkPart(child, state));
235
+ case "ParameterExpansion":
236
+ if (part.indexParts && !part.indexParts.every((child) => walkPart(child, state))) return false;
237
+ if (part.operand && !walkWord(part.operand, state)) return false;
238
+ if (part.slice) {
239
+ if (!walkWord(part.slice.offset, state)) return false;
240
+ if (part.slice.length && !walkWord(part.slice.length, state)) return false;
241
+ }
242
+ if (part.replace) {
243
+ if (!walkWord(part.replace.pattern, state)) return false;
244
+ if (!walkWord(part.replace.replacement, state)) return false;
245
+ }
246
+ return true;
247
+ case "CommandExpansion":
248
+ return walkNestedScript(part.script, state);
249
+ case "ProcessSubstitution":
250
+ return false;
251
+ case "ArithmeticExpansion":
252
+ return part.expression ? walkArithmetic(part.expression, state) : false;
253
+ case "ExtendedGlob":
254
+ case "BraceExpansion":
255
+ return !part.parts || part.parts.every((child) => walkPart(child, state));
256
+ }
257
+ }
258
+
259
+ function walkArithmetic(expression: ArithmeticExpression, state: WalkState): boolean {
260
+ switch (expression.type) {
261
+ case "ArithmeticBinary":
262
+ return walkArithmetic(expression.left, state) && walkArithmetic(expression.right, state);
263
+ case "ArithmeticUnary":
264
+ return walkArithmetic(expression.operand, state);
265
+ case "ArithmeticTernary":
266
+ return (
267
+ walkArithmetic(expression.test, state) &&
268
+ walkArithmetic(expression.consequent, state) &&
269
+ walkArithmetic(expression.alternate, state)
270
+ );
271
+ case "ArithmeticGroup":
272
+ return walkArithmetic(expression.expression, state);
273
+ case "ArithmeticWord": {
274
+ const parts = expression.parts;
275
+ return !parts || parts.every((part) => walkPart(part, state));
276
+ }
277
+ case "ArithmeticCommandExpansion":
278
+ return walkNestedScript(expression.script, state);
279
+ }
280
+ }
281
+
282
+ function walkNestedScript(script: ParsedScript | undefined, state: WalkState): boolean {
283
+ if (!script) return false;
284
+ return walkScript(script, state);
285
+ }
286
+
287
+ function staticBareName(word: Word): string | undefined {
288
+ const value = staticWordValue(word);
289
+ if (!value || value.includes("/")) return undefined;
290
+ return value;
291
+ }
292
+
293
+ function staticWordValue(word: Word): string | undefined {
294
+ const parts = word.parts;
295
+ if (!parts) return word.value;
296
+ if (!parts.every(isStaticPart)) return undefined;
297
+ return word.value;
298
+ }
299
+
300
+ function isStaticPart(part: WordPart): boolean {
301
+ switch (part.type) {
302
+ case "Literal":
303
+ case "SingleQuoted":
304
+ case "AnsiCQuoted":
305
+ return true;
306
+ case "DoubleQuoted":
307
+ case "LocaleString":
308
+ return part.parts.every((child) => child.type === "Literal");
309
+ default:
310
+ return false;
311
+ }
312
+ }
313
+
314
+ function hasFdVeto(args: readonly string[]): boolean {
315
+ return (
316
+ hasShortOption(args, "x") ||
317
+ hasShortOption(args, "X") ||
318
+ hasLongOption(args, "exec") ||
319
+ hasLongOption(args, "exec-batch")
320
+ );
321
+ }
322
+
323
+ function hasRgVeto(args: readonly string[]): boolean {
324
+ return (
325
+ hasShortOption(args, "z") ||
326
+ hasLongOption(args, "pre") ||
327
+ hasLongOption(args, "hostname-bin") ||
328
+ hasLongOption(args, "search-zip")
329
+ );
330
+ }
331
+
332
+ function hasOutputFlag(args: readonly string[]): boolean {
333
+ return hasShortOption(args, "o") || hasLongOption(args, "output");
334
+ }
335
+
336
+ function hasYqInplace(args: readonly string[]): boolean {
337
+ return hasShortOption(args, "i") || hasLongOption(args, "inplace") || hasLongOption(args, "in-place");
338
+ }
339
+
340
+ function isSafeSed(args: readonly string[]): boolean {
341
+ if (args.length < 2 || args[0] !== "-n") return false;
342
+ const script = args[1];
343
+ if (script === undefined || !SED_PRINT.test(script)) return false;
344
+ for (const arg of args.slice(2)) {
345
+ if (arg === "--") continue;
346
+ if (arg.startsWith("-")) return false;
347
+ }
348
+ return true;
349
+ }
350
+
351
+ function hasPositional(args: readonly string[]): boolean {
352
+ let endFlags = false;
353
+ for (const arg of args) {
354
+ if (!endFlags) {
355
+ if (arg === "--") {
356
+ endFlags = true;
357
+ continue;
358
+ }
359
+ if (arg.startsWith("-") && arg !== "-") continue;
360
+ }
361
+ return true;
362
+ }
363
+ return false;
364
+ }
365
+
366
+ function isSafeGit(args: readonly string[]): boolean {
367
+ if (hasGitWriteExecFlag(args)) return false;
368
+ const verbIndex = gitVerbIndex(args);
369
+ if (verbIndex === undefined) return false;
370
+ const verb = args[verbIndex];
371
+ if (verb === undefined) return false;
372
+ const rest = args.slice(verbIndex + 1);
373
+ if (GIT_READ_VERBS.has(verb)) return true;
374
+ if (verb === "branch") return isGitBranch(rest);
375
+ if (verb === "remote") return isGitRemote(rest);
376
+ if (verb === "tag") return isGitTag(rest);
377
+ return false;
378
+ }
379
+
380
+ function gitVerbIndex(args: readonly string[]): number | undefined {
381
+ for (let index = 0; index < args.length; index += 1) {
382
+ const arg = args[index];
383
+ if (arg === undefined || arg === "--") return undefined;
384
+ if (arg === "--no-pager" || arg === "-P" || arg === "--no-color") continue;
385
+ if (
386
+ arg === "-C" ||
387
+ arg === "-c" ||
388
+ arg === "-p" ||
389
+ arg === "--paginate" ||
390
+ isLongOption(arg, "config-env") ||
391
+ isLongOption(arg, "exec-path") ||
392
+ isLongOption(arg, "git-dir") ||
393
+ isLongOption(arg, "work-tree") ||
394
+ isLongOption(arg, "namespace") ||
395
+ isLongOption(arg, "super-prefix")
396
+ ) {
397
+ return undefined;
398
+ }
399
+ if (arg.startsWith("-")) return undefined;
400
+ return index;
401
+ }
402
+ return undefined;
403
+ }
404
+
405
+ function hasGitWriteExecFlag(args: readonly string[]): boolean {
406
+ for (const arg of args) {
407
+ if (arg === "--") break;
408
+ if (
409
+ arg === "--paginate" ||
410
+ isLongOption(arg, "output") ||
411
+ isLongOption(arg, "ext-diff") ||
412
+ isLongOption(arg, "textconv") ||
413
+ isLongOption(arg, "exec")
414
+ ) {
415
+ return true;
416
+ }
417
+ }
418
+ return false;
419
+ }
420
+
421
+ function isGitBranch(args: readonly string[]): boolean {
422
+ for (let index = 0; index < args.length; index += 1) {
423
+ const arg = args[index];
424
+ if (arg === undefined || arg === "--") return false;
425
+ if (
426
+ arg === "--list" ||
427
+ arg === "--all" ||
428
+ arg === "--remotes" ||
429
+ arg === "--verbose" ||
430
+ arg === "--show-current"
431
+ ) {
432
+ continue;
433
+ }
434
+ if (isLongOption(arg, "format")) {
435
+ if (arg === "--format") {
436
+ if (args[index + 1] === undefined) return false;
437
+ index += 1;
438
+ }
439
+ continue;
440
+ }
441
+ if (arg.startsWith("--")) return false;
442
+ if (!arg.startsWith("-") || arg.length < 2) return false;
443
+ for (const flag of arg.slice(1)) {
444
+ if (flag !== "a" && flag !== "l" && flag !== "r" && flag !== "v") return false;
445
+ }
446
+ }
447
+ return true;
448
+ }
449
+
450
+ function isGitRemote(args: readonly string[]): boolean {
451
+ let index = 0;
452
+ while (index < args.length) {
453
+ const arg = args[index];
454
+ if (arg !== "-v" && arg !== "--verbose") break;
455
+ index += 1;
456
+ }
457
+ if (index >= args.length) return true;
458
+ const verb = args[index];
459
+ return verb === "show" || verb === "get-url";
460
+ }
461
+
462
+ function isGitTag(args: readonly string[]): boolean {
463
+ let listing = false;
464
+ for (const arg of args) {
465
+ if (arg === "--") return false;
466
+ if (arg === "-l" || arg === "--list") {
467
+ listing = true;
468
+ continue;
469
+ }
470
+ if (arg.startsWith("-") && !arg.startsWith("--") && arg.length > 1) {
471
+ for (const flag of arg.slice(1)) {
472
+ if (flag !== "l") return false;
473
+ }
474
+ listing = true;
475
+ continue;
476
+ }
477
+ if (arg.startsWith("-") || !listing) return false;
478
+ }
479
+ return true;
480
+ }
481
+
482
+ function hasShortOption(args: readonly string[], letter: string): boolean {
483
+ for (const arg of args) {
484
+ if (arg === "--") break;
485
+ if (arg.length < 2 || !arg.startsWith("-") || arg.startsWith("--")) continue;
486
+ if (arg.slice(1).includes(letter)) return true;
487
+ }
488
+ return false;
489
+ }
490
+
491
+ function hasLongOption(args: readonly string[], name: string): boolean {
492
+ for (const arg of args) {
493
+ if (arg === "--") break;
494
+ if (isLongOption(arg, name)) return true;
495
+ }
496
+ return false;
497
+ }
498
+
499
+ function isLongOption(arg: string, name: string): boolean {
500
+ return arg === `--${name}` || arg.startsWith(`--${name}=`);
501
+ }
@@ -5,6 +5,7 @@ import {
5
5
  type ExtensionContext,
6
6
  type ToolCallEvent,
7
7
  } from "@earendil-works/pi-coding-agent";
8
+ import { Marker } from "@shanepadgett/tau-tui";
8
9
  import { Type, type Static } from "typebox";
9
10
  import { Value } from "typebox/value";
10
11
  import { emitAgentBlocked } from "../../shared/agent-blocked.ts";
@@ -12,10 +13,11 @@ import { resolveEffortCandidates } from "../../shared/model-effort.ts";
12
13
  import { generateToolValidated } from "../../shared/model-fallback/index.ts";
13
14
  import { errorText, truncAt } from "../../shared/text.ts";
14
15
  import { loadTauExtensionSettings } from "../../shared/settings/load.ts";
16
+ import { isAllowlistedBash } from "./allowlist.ts";
15
17
  import toolApprovalSettings from "./settings.ts";
16
18
 
17
19
  const STATUS_KEY = "tool-approval";
18
- const MAX_REVIEW_CHARS = 12_000;
20
+ const AUTO_APPROVED_TYPE = "tau.tool-approval.auto-approved";
19
21
 
20
22
  const SUMMARY_SCHEMA = Type.String({
21
23
  minLength: 1,
@@ -61,29 +63,6 @@ const REVIEW_SYSTEM_PROMPT = [
61
63
  "An approved review has no reason field. A review that requires user approval must give one concise reason naming the concrete risk without repeating the summary.",
62
64
  ].join("\n");
63
65
 
64
- const PLAIN_COMMAND_PATTERN = /^[A-Za-z0-9_./:@%+,=-]+(?: +[A-Za-z0-9_./:@%+,=-]+)*$/;
65
- const TRIVIAL_READ_ONLY_COMMANDS = new Set(["git diff", "git log", "git show", "git status", "pwd"]);
66
- const TRIVIAL_READ_ONLY_PROGRAMS = new Set([
67
- "basename",
68
- "cat",
69
- "comm",
70
- "cut",
71
- "dirname",
72
- "du",
73
- "echo",
74
- "grep",
75
- "head",
76
- "ls",
77
- "printf",
78
- "realpath",
79
- "rg",
80
- "tail",
81
- "test",
82
- "uniq",
83
- "wc",
84
- "which",
85
- ]);
86
-
87
66
  const REVIEW_TOOL = {
88
67
  name: "submit_tool_review",
89
68
  description: "Submit the complete safety review for the agent tool request.",
@@ -98,9 +77,24 @@ interface ToolApprovalRequest {
98
77
  input: Record<string, unknown>;
99
78
  }
100
79
 
80
+ interface AutoApprovedMarker {
81
+ toolName: ApprovalToolName;
82
+ }
83
+
101
84
  export default function toolApprovalExtension(pi: ExtensionAPI): void {
102
85
  let settings = toolApprovalSettings.defaults;
103
86
 
87
+ pi.registerEntryRenderer<AutoApprovedMarker>(AUTO_APPROVED_TYPE, (entry, _options, theme) => {
88
+ const marker = autoApprovedMarker(entry.data);
89
+ if (!marker) return undefined;
90
+ return new Marker({
91
+ theme,
92
+ state: "complete",
93
+ label: "Auto-approved",
94
+ parts: [toolLabel(marker.toolName)],
95
+ });
96
+ });
97
+
104
98
  async function refreshSettings(ctx: Pick<ExtensionContext, "cwd" | "isProjectTrusted">): Promise<void> {
105
99
  settings = await loadTauExtensionSettings(ctx, toolApprovalSettings);
106
100
  }
@@ -114,7 +108,8 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
114
108
  if (!settings.enabled) return undefined;
115
109
  return {
116
110
  systemPrompt: `${event.systemPrompt}\n\n${[
117
- "Agent bash and script_runner requests are reviewed by a separate quick-effort safety classifier before execution.",
111
+ "Known-safe read-only bash commands skip review.",
112
+ "Other bash and every script_runner request are reviewed by a separate quick-effort safety classifier before execution.",
118
113
  "Treat classifier approval as a gate, not as permission to hide command intent from the user.",
119
114
  "Routine local development requests can be approved automatically.",
120
115
  "Requests with destructive, system, production, privileged, or security-sensitive effects require human confirmation.",
@@ -143,9 +138,9 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
143
138
  return block("bash command is empty");
144
139
  }
145
140
  command = value;
141
+ if (isAllowlistedBash(command)) return undefined;
146
142
  }
147
143
 
148
- const readOnlyCommand = request.toolName === "bash" && isTriviallyReadOnly(command ?? "");
149
144
  ctx.ui.setStatus(STATUS_KEY, `reviewing ${toolLabel(request.toolName)}`);
150
145
  try {
151
146
  const review = await reviewToolRequest(ctx, request);
@@ -158,7 +153,10 @@ export default function toolApprovalExtension(pi: ExtensionAPI): void {
158
153
  formatApproval(review.summary, review.reason),
159
154
  );
160
155
  }
161
- if (settings.autoApprove || readOnlyCommand) return undefined;
156
+ if (settings.autoApprove) {
157
+ pi.appendEntry<AutoApprovedMarker>(AUTO_APPROVED_TYPE, { toolName: request.toolName });
158
+ return undefined;
159
+ }
162
160
  return requestToolApproval(
163
161
  pi,
164
162
  ctx,
@@ -194,21 +192,19 @@ function approvalRequest(event: ToolCallEvent): ToolApprovalRequest | undefined
194
192
  return undefined;
195
193
  }
196
194
 
197
- function isTriviallyReadOnly(command: string): boolean {
198
- const plainCommand = PLAIN_COMMAND_PATTERN.test(command);
199
- const separator = command.indexOf(" ");
200
- const program = separator === -1 ? command : command.slice(0, separator);
201
- return plainCommand && (TRIVIAL_READ_ONLY_COMMANDS.has(command) || TRIVIAL_READ_ONLY_PROGRAMS.has(program));
202
- }
203
-
204
195
  function toolLabel(toolName: ApprovalToolName): string {
205
196
  return toolName === "bash" ? "bash command" : "script_runner request";
206
197
  }
207
198
 
199
+ function autoApprovedMarker(value: unknown): AutoApprovedMarker | undefined {
200
+ if (!value || typeof value !== "object") return undefined;
201
+ const toolName = (value as AutoApprovedMarker).toolName;
202
+ if (toolName !== "bash" && toolName !== "script_runner") return undefined;
203
+ return { toolName };
204
+ }
205
+
208
206
  async function reviewToolRequest(ctx: ExtensionContext, request: ToolApprovalRequest): Promise<ToolReview> {
209
207
  const requestJson = JSON.stringify(request);
210
- if (!requestJson || requestJson.length > MAX_REVIEW_CHARS)
211
- throw new Error("tool request is too large to review safely");
212
208
  const candidates = await resolveEffortCandidates(ctx, "quick", {
213
209
  includeParentModel: false,
214
210
  preferredProvider: "xai",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.36.0",
3
+ "version": "0.38.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,10 +35,11 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.0",
38
- "@shanepadgett/tau-tui": "0.36.0",
38
+ "@shanepadgett/tau-tui": "0.38.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
41
  "smol-toml": "1.7.1",
42
+ "unbash": "4.0.10",
42
43
  "web-tree-sitter": "0.26.11"
43
44
  },
44
45
  "peerDependencies": {