coding-tools 0.1.0

Declarative, agent-friendly CLI tools behind one 'ct' command: search, view, verifiable edits, and framed command tests.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
{
  "name": "ct",
  "description": "Umbrella launcher for the coding_tools suite. `ct <command> [args...]` runs the matching ct-<command> binary git-style, so `ct search` runs ct-search and `ct test` runs ct-test; any other ct-* tool installed beside ct or on PATH is reachable too. ct passes the child's stdout, stderr, and exit status through unchanged (0 success, 1 clean negative, 2 usage or runtime error). This definition also carries every suite tool's full tool-use definition under `tools`, so an agent can hoist them all in one call; for a single tool use `ct <command> --explain json` (e.g. `ct search --explain json`), and `ct --explain md` documents the suite for humans.",
  "input_schema": {
    "type": "object",
    "properties": {
      "command": {
        "type": "string",
        "description": "Subcommand to run; resolves to the ct-<command> binary. Built-in: search (ct-search), view (ct-view), tree (ct-tree), edit (ct-edit), patch (ct-patch), test (ct-test). Any other ct-* tool on PATH also works.",
        "enum": [
          "search",
          "view",
          "tree",
          "edit",
          "patch",
          "test"
        ]
      },
      "args": {
        "type": "array",
        "items": {
          "type": "string"
        },
        "description": "Arguments passed through verbatim to ct-<command> (e.g. [\"--name\", \"*.rs\", \"--grep\", \"TODO\"])."
      }
    },
    "required": [
      "command"
    ]
  },
  "tools": [
    {
      "name": "ct-search",
      "description": "Recursively find files by name, type, size, and content from a chosen root, replacing find|xargs|grep pipelines. An entry matches only when all supplied predicates hold. A search can also be posed as a pass/fail test: --question frames it, --expect sets an expectation over the match count, and --emit prints a templated verdict. The exit status follows the verdict = --expect applied to the count: 0 SUCCESS, 1 ERROR, 2 usage/runtime error; the default expectation 'any' makes this 0 if anything matched and 1 if not. Pattern arguments use substring->glob->regex promotion: text with no metacharacters is a literal substring; glob metacharacters (* ? [ ]) that are not a valid regex are treated as a glob; otherwise the pattern is used as a regex. Invoke as `ct search ...` or `ct-search ...`.",
      "input_schema": {
        "type": "object",
        "properties": {
          "base": {
            "type": "string",
            "description": "Search root, relative or absolute, independent of the current working directory.",
            "default": "."
          },
          "name": {
            "type": "string",
            "description": "File-name pattern. '|'-separated alternatives, each substring->glob->regex promoted and anchored to the whole name (e.g. '*.java|*.kt')."
          },
          "type": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "f",
                "d",
                "l"
              ]
            },
            "description": "Restrict to entry kinds: f=regular file, d=directory, l=symlink. May be repeated or comma-joined."
          },
          "grep": {
            "type": "string",
            "description": "Content pattern, substring->glob->regex promoted and searched unanchored against file contents. Implies regular files."
          },
          "size": {
            "type": "string",
            "description": "Size predicate [+|-]N[k|m|g]: +N larger than, -N smaller than, N at least N. Applies to regular files."
          },
          "hidden": {
            "type": "boolean",
            "description": "Include dot-entries (names starting with '.'). Default: skipped."
          },
          "follow": {
            "type": "boolean",
            "description": "Follow symlinks while traversing."
          },
          "limit": {
            "type": "integer",
            "description": "Stop after this many matches."
          },
          "question": {
            "type": "string",
            "description": "Question this search answers, framing it as a test; printed as a '== ... ==' banner unless quiet."
          },
          "expect": {
            "type": "string",
            "description": "Verdict expectation over the match count; default 'any'. One of: any (>=1), none (==0), N (>=N), =N (==N), +N (>N), -N (<N). 'none' inverts a search into a negative assertion that passes when nothing matches."
          },
          "emit": {
            "type": "string",
            "description": "Template written to stdout after the search (alias: emit-stdout). Tokens: {RESULT} {QUESTION} {COUNT} {LINES} {BASE} {MATCHES}."
          },
          "emit-stderr": {
            "type": "string",
            "description": "Template written to stderr after the search. Same tokens as emit."
          },
          "list": {
            "type": "boolean",
            "description": "Output mode: print one matching path per line. This is the default mode."
          },
          "summary": {
            "type": "boolean",
            "description": "Output mode: print counts only. Mutually exclusive with the other output modes."
          },
          "detail": {
            "type": "boolean",
            "description": "Output mode: print matches plus, for --grep, each hit as path:line:text. Mutually exclusive with the other output modes."
          },
          "quiet": {
            "type": "boolean",
            "description": "Output mode: print no per-match output and no --question banner; report via exit status (and --emit, which still fires). Mutually exclusive with the other output modes."
          }
        },
        "required": []
      }
    },
    {
      "name": "ct-view",
      "description": "Show one file's lines by range, or the regions around a pattern with context, instead of dumping the whole file. --range A:B (1-based inclusive; also A:, :B, A) prints a span; --match PATTERN prints the windows around matching lines with --context lines on each side (overlapping windows merge, like grep -C). Read-only, no allow-gate. With --json, emits {tool, path, total_lines, shown, lines:[{n,text}], matched?}. Exit status: 0 shown, 1 if --match matched nothing, 2 on a read or usage error. --match uses substring->glob->regex promotion, searched unanchored per line. Invoke as `ct view ...` or `ct-view ...`.",
      "input_schema": {
        "type": "object",
        "properties": {
          "path": {
            "type": "string",
            "description": "The file to view (positional, required)."
          },
          "range": {
            "type": "string",
            "description": "Line range A:B (1-based, inclusive); also A: (to end), :B (from start), or A (one line)."
          },
          "match": {
            "type": "string",
            "description": "Show only lines matching this pattern (substring->glob->regex promoted, searched unanchored), with --context lines around each hit."
          },
          "context": {
            "type": "integer",
            "description": "Lines of context shown around each --match hit. Default: 2.",
            "default": 2
          },
          "limit": {
            "type": "integer",
            "description": "Cap the number of lines emitted."
          },
          "plain": {
            "type": "boolean",
            "description": "Suppress the line-number gutter in text output."
          },
          "json": {
            "type": "boolean",
            "description": "Emit a structured JSON result instead of text."
          }
        },
        "required": [
          "path"
        ]
      }
    },
    {
      "name": "ct-tree",
      "description": "Walk a directory for chosen file types and report the effective file tree with per-file line, word, and character counts. Select files with --base/--name/--ext (--ext is a comma list of extensions added to --name as alternatives), filter with metric predicates (--min-lines/--max-lines/--min-words/--max-words/--min-chars/--max-chars) and per-folder predicates (--min-files-per-folder/--max-files-per-folder, counting matching files in a file's immediate directory), sort by path|name|lines|words|chars|ext (--desc for descending), and choose a summarisation level: --tree (indented tree with per-file counts and per-folder subtotals; default), --flat (one file per line: lines words chars path; best for ranked lists), or --summary (aggregates grouped by --group ext|dir|none). --json emits {tool, base, files:[{path,ext,lines,words,chars}], by_ext, totals}. Exit: 0 if any file is in the report, 1 if none, 2 on error. Read-only. Invoke as `ct tree ...` or `ct-tree ...`. Example: all *.rs files over 5000 lines sorted by line count descending = --ext rs --min-lines 5001 --flat --sort lines --desc.",
      "input_schema": {
        "type": "object",
        "properties": {
          "base": {
            "type": "string",
            "description": "Root directory to walk, relative or absolute. Default '.'.",
            "default": "."
          },
          "name": {
            "type": "string",
            "description": "File-name pattern; '|'-separated alternatives, each substring->glob->regex promoted and anchored to the whole name."
          },
          "ext": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Restrict to these extensions (no dots), e.g. ['rs','toml']. Added to --name as alternatives. May be comma-joined."
          },
          "hidden": {
            "type": "boolean",
            "description": "Include dot-entries (names starting with '.'). Default: skipped."
          },
          "follow": {
            "type": "boolean",
            "description": "Follow symlinks while traversing."
          },
          "min-lines": {
            "type": "integer",
            "description": "Only include files with at least N lines."
          },
          "max-lines": {
            "type": "integer",
            "description": "Only include files with at most N lines."
          },
          "min-words": {
            "type": "integer",
            "description": "Only include files with at least N words."
          },
          "max-words": {
            "type": "integer",
            "description": "Only include files with at most N words."
          },
          "min-chars": {
            "type": "integer",
            "description": "Only include files with at least N characters."
          },
          "max-chars": {
            "type": "integer",
            "description": "Only include files with at most N characters."
          },
          "min-files-per-folder": {
            "type": "integer",
            "description": "Only include folders that directly contain at least N matching files."
          },
          "max-files-per-folder": {
            "type": "integer",
            "description": "Only include folders that directly contain at most N matching files."
          },
          "sort": {
            "type": "string",
            "enum": [
              "path",
              "name",
              "lines",
              "words",
              "chars",
              "ext"
            ],
            "description": "Sort key. Default 'path'. In --flat the sort is global; in --tree it orders entries within each folder."
          },
          "desc": {
            "type": "boolean",
            "description": "Sort descending instead of ascending."
          },
          "tree": {
            "type": "boolean",
            "description": "Output mode: indented file tree with per-file counts and per-folder subtotals. This is the default mode."
          },
          "flat": {
            "type": "boolean",
            "description": "Output mode: one matching file per line with its counts. Mutually exclusive with the other modes."
          },
          "summary": {
            "type": "boolean",
            "description": "Output mode: aggregate counts only, grouped by --group. Mutually exclusive with the other modes."
          },
          "group": {
            "type": "string",
            "enum": [
              "ext",
              "dir",
              "none"
            ],
            "description": "Grouping for --summary: by extension (default), by immediate directory, or a single grand total."
          },
          "json": {
            "type": "boolean",
            "description": "Emit a structured JSON result instead of text."
          }
        },
        "required": []
      }
    },
    {
      "name": "ct-edit",
      "description": "Find/replace across files chosen by ct-search-style predicates, framed as a self-checking edit. It computes every replacement first, classifies the total against --expect into a SUCCESS/ERROR verdict, and writes ONLY when the verdict is SUCCESS and --dry-run is not set; otherwise nothing is written. Replacements preserve every untouched byte (line terminators, indentation, surrounding text). --find is substring->glob->regex promoted and matched per line; with a regex --find, $1/${name} expand in --replace (use $$ for literal $), with a literal or glob --find the replacement is literal. Only regular UTF-8 text files are edited. Not subject to the ct-test command allowlist (it runs no programs); safety is via --dry-run, --expect, and your VCS. With --json emits {tool, verdict, dry_run, applied, replacements, files_changed, sites:[{path,line,before,after}]}. Exit: 0 SUCCESS, 1 ERROR, 2 usage/runtime error. Invoke as `ct edit ...` or `ct-edit ...`.",
      "input_schema": {
        "type": "object",
        "properties": {
          "base": {
            "type": "string",
            "description": "Root to edit. A file edits just that file; a directory is descended. Default '.'.",
            "default": "."
          },
          "name": {
            "type": "string",
            "description": "Limit to files whose name matches; '|'-separated alternatives, each substring->glob->regex promoted and anchored to the whole name."
          },
          "hidden": {
            "type": "boolean",
            "description": "Include dot-entries (names starting with '.'). Default: skipped."
          },
          "follow": {
            "type": "boolean",
            "description": "Follow symlinks while traversing."
          },
          "find": {
            "type": "string",
            "description": "Pattern to find (substring->glob->regex promoted), matched per line. Required."
          },
          "replace": {
            "type": "string",
            "description": "Replacement text. With a regex --find, $1/${name} expand (use $$ for literal $); with a literal or glob --find, the replacement is literal. Required."
          },
          "expect": {
            "type": "string",
            "description": "Verdict expectation over the total replacement count; default 'any'. One of: any (>=1), none (==0), N (>=N), =N (==N), +N (>N), -N (<N). The edit is written only if the verdict is SUCCESS."
          },
          "dry-run": {
            "type": "boolean",
            "description": "Compute and show the change and verdict, but write nothing."
          },
          "quiet": {
            "type": "boolean",
            "description": "Suppress the per-site diff; print only the summary line."
          },
          "json": {
            "type": "boolean",
            "description": "Emit a structured JSON result instead of text."
          }
        },
        "required": [
          "find",
          "replace"
        ]
      }
    },
    {
      "name": "ct-patch",
      "description": "Make structured, format-preserving edits to JSON/JSONC/JSONL/YAML files: address a node by path and --set, --add, --delete, or --move-* it. For JSON/JSONC/JSONL, edits are byte-range splices against the parsed tree, so everything outside the changed node (comments, indentation, key order, blank lines, trailing commas) is preserved exactly; YAML uses the pure-Rust yaml-edit backend (comment-preserving, though a structural edit may relocate an adjacent comment). Format is detected from the extension (.json, .jsonc, .jsonl/.ndjson, .yaml/.yml) or forced with --format; for JSONL each op applies to every non-blank line. Paths are dot-separated keys with [N] array indices or [key=value] object predicates (leading dot optional, e.g. .servers[name=web].port). --set VALUE is parsed as JSON if possible else taken as a string (missing object keys are created and an index equal to the array length appends). --add appends VALUE to the array at PATH (no index needed). --delete removes the node and its separating comma (unresolved path is a no-op). --move-first/--move-last/--move-up/--move-down relocate the array element selected by PATH within its list. YAML currently supports --set (replace existing) and --delete only (both comment-preserving); --add, --move-*, and array-index/predicate paths are JSON-family-only for now and error clearly on YAML. The verdict is --expect applied to the total number of changes (default any); the edit is written only when the verdict is SUCCESS and --dry-run is not set. Not subject to the ct-test allowlist (it runs no programs). With --json emits {tool, verdict, dry_run, applied, changes, files_changed, files:[{path,changes}]}. Exit: 0 SUCCESS, 1 ERROR, 2 usage/runtime error. Invoke as `ct patch ...` or `ct-patch ...`.",
      "input_schema": {
        "type": "object",
        "properties": {
          "base": {
            "type": "string",
            "description": "Root to patch. A file patches just that file; a directory is descended. Default '.'.",
            "default": "."
          },
          "name": {
            "type": "string",
            "description": "Limit to files whose name matches; '|'-separated alternatives, each substring->glob->regex promoted and anchored to the whole name."
          },
          "hidden": {
            "type": "boolean",
            "description": "Include dot-entries (names starting with '.'). Default: skipped."
          },
          "follow": {
            "type": "boolean",
            "description": "Follow symlinks while traversing."
          },
          "set": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH=VALUE operations (repeatable). VALUE is parsed as JSON if possible, otherwise taken as a string. Missing object keys are created; an index equal to the array length appends. PATH may use [key=value] to select an array element. For YAML, --set replaces an existing key only."
          },
          "add": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH=VALUE operations (repeatable): append VALUE to the array at PATH, without computing an index. VALUE is parsed as JSON or taken as a string. JSON family only."
          },
          "delete": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH operations (repeatable): remove the node at PATH, taking its separating comma. PATH may use [key=value] to select an array element. An unresolved path is a no-op."
          },
          "move-first": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH operations (repeatable): move the array element selected by PATH (by index or [key=value]) to the front of its list. JSON family only."
          },
          "move-last": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH operations (repeatable): move the selected array element to the end of its list. JSON family only."
          },
          "move-up": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH operations (repeatable): move the selected array element one position earlier. JSON family only."
          },
          "move-down": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "PATH operations (repeatable): move the selected array element one position later. JSON family only."
          },
          "format": {
            "type": "string",
            "enum": [
              "json",
              "jsonc",
              "jsonl",
              "yaml"
            ],
            "description": "Force the document format instead of detecting from the file extension."
          },
          "expect": {
            "type": "string",
            "description": "Verdict expectation over the total number of changes; default 'any'. One of: any (>=1), none (==0), N (>=N), =N (==N), +N (>N), -N (<N). The edit is written only if the verdict is SUCCESS."
          },
          "dry-run": {
            "type": "boolean",
            "description": "Compute and report the changes and verdict, but write nothing."
          },
          "quiet": {
            "type": "boolean",
            "description": "Suppress the per-file lines; print only the summary."
          },
          "json": {
            "type": "boolean",
            "description": "Emit a structured JSON result instead of text."
          }
        },
        "required": []
      }
    },
    {
      "name": "ct-test",
      "description": "Run a command as a framed experiment: pose a question, classify the result from stdout/stderr pattern matches, and emit a templated verdict. Pass/fail is decided by what the command prints, not only its exit code. ct-test is fail-closed; {RESULT} resolves in order: (1) any err-match hit => ERROR (decisive, never overridden); (2) else any ok-match hit => SUCCESS (a supplied --ok-match is a REQUIRED proof of success — a clean exit does not substitute for it); (3) else the run is inconclusive and --otherwise decides (success|error|exit), defaulting to error when an --ok-match was supplied, otherwise exit. The -stdout/-stderr matcher variants search only that one stream (e.g. cargo test writes 'test result: ok' to stdout, so --ok-match-stderr would miss it). On ERROR a one-line reason explaining which rule fired is printed to stderr and is available as the {REASON} emit token. --focus distils the captured output to the lines matching a pattern (with --context around each), printed to stderr and available as {FOCUS}. Exit status is 0 when RESULT is SUCCESS, 1 when ERROR, 2 on a usage or runtime error. ct-test runs only a fixed, immutable built-in set of read-only commands (cat ct-search ct-tree ct-view echo false file grep head ls pwd stat tail true wc), so it is a ready conditional wrapper around the read-only ct-* tools; a command not on it is refused with exit 2 and nothing runs, and there is no run-time way to extend the list. Gating is by program name (basename of cmd, or 'sh' under --shell; since 'sh' is not on the list, --shell is currently unavailable). Pattern arguments use substring->glob->regex promotion and are searched unanchored. Invoke as `ct test ...` or `ct-test ...`.",
      "input_schema": {
        "type": "object",
        "properties": {
          "question": {
            "type": "string",
            "description": "The question this experiment answers; printed as a '== ... ==' banner unless --quiet."
          },
          "cmd": {
            "type": "string",
            "description": "Program to run. With --shell it is interpreted as a shell command line; otherwise trailing args (after --) are passed through to it."
          },
          "shell": {
            "type": "boolean",
            "description": "Interpret --cmd as a shell line via `sh -c`, enabling pipes and redirection."
          },
          "stdin": {
            "type": "string",
            "description": "Literal text written to the child's standard input."
          },
          "err-match": {
            "type": "string",
            "description": "Pattern that, if found in stdout OR stderr, forces RESULT=ERROR. Synonym for supplying both err-match-stdout and err-match-stderr."
          },
          "err-match-stdout": {
            "type": "string",
            "description": "Pattern that, if found in stdout, forces RESULT=ERROR."
          },
          "err-match-stderr": {
            "type": "string",
            "description": "Pattern that, if found in stderr, forces RESULT=ERROR."
          },
          "ok-match": {
            "type": "string",
            "description": "Pattern that, if found in stdout OR stderr, indicates RESULT=SUCCESS. Synonym for supplying both ok-match-stdout and ok-match-stderr."
          },
          "ok-match-stdout": {
            "type": "string",
            "description": "Pattern that, if found in stdout, indicates RESULT=SUCCESS."
          },
          "ok-match-stderr": {
            "type": "string",
            "description": "Pattern that, if found in stderr, indicates RESULT=SUCCESS."
          },
          "otherwise": {
            "type": "string",
            "enum": [
              "success",
              "error",
              "exit"
            ],
            "description": "Verdict when neither an --ok-match nor an --err-match matched (the inconclusive case). Default: error if any --ok-match was supplied, else exit (follow the exit code)."
          },
          "focus": {
            "type": "string",
            "description": "Distil the captured output to the lines matching this pattern, with --context lines around each (overlapping windows merge, line-numbered). Printed to stderr and available as the {FOCUS} emit token."
          },
          "context": {
            "type": "integer",
            "description": "Lines of context shown around each --focus match. Default: 2.",
            "default": 2
          },
          "emit": {
            "type": "string",
            "description": "Template written to stdout after the command finishes (alias: emit-stdout). Tokens: {RESULT} {CODE} {QUESTION} {CMD} {STDOUT} {STDERR} {REASON} {FOCUS}."
          },
          "emit-stderr": {
            "type": "string",
            "description": "Template written to stderr after the command finishes. Same tokens as emit."
          },
          "show-output": {
            "type": "boolean",
            "description": "Also pass the child's stdout/stderr through verbatim."
          },
          "quiet": {
            "type": "boolean",
            "description": "Suppress the question banner."
          },
          "args": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Arguments passed through to --cmd (supplied after `--`). Ignored when --shell is used."
          }
        },
        "required": [
          "cmd"
        ]
      }
    }
  ]
}