@iinm/plain-agent 1.7.19 → 1.7.20
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/README.md +53 -90
- package/config/config.predefined.json +15 -15
- package/package.json +1 -3
- package/src/agentLoop.mjs +3 -1
- package/src/cliCost.mjs +67 -32
- package/src/cliFormatter.mjs +1 -1
- package/src/cliInteractive.mjs +1 -1
- package/src/config.d.ts +2 -2
- package/src/config.mjs +1 -1
- package/src/costTracker.mjs +58 -19
- package/src/env.mjs +0 -6
- package/src/main.mjs +2 -6
- package/src/model.d.ts +1 -1
- package/src/tools/patchFile.mjs +11 -12
- package/src/utils/notify.mjs +3 -2
- package/src/voiceInputGemini.mjs +58 -210
- package/src/voiceInputOpenAI.mjs +63 -220
- package/src/voiceInputSession.mjs +295 -2
- package/bin/plain-notify-terminal-bell +0 -3
package/README.md
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
<p align="center">
|
|
2
|
-
<img src="https://pub-0bb49aa929f242d49c89ed8c297932b5.r2.dev/plain-agent/plain-agent-logo.png" alt="plain-agent logo" width="320">
|
|
3
|
-
</p>
|
|
4
|
-
|
|
5
1
|
# Plain Agent
|
|
6
2
|
|
|
7
|
-
A lightweight CLI-based coding agent
|
|
3
|
+
A lightweight CLI-based coding agent.
|
|
8
4
|
|
|
9
5
|
## Why Plain Agent?
|
|
10
6
|
|
|
@@ -23,6 +19,7 @@ A lightweight CLI-based coding agent with zero framework dependencies.
|
|
|
23
19
|
|
|
24
20
|
## Limitations
|
|
25
21
|
|
|
22
|
+
- **CLI only** — Plain Agent does not provide a terminal UI.
|
|
26
23
|
- **Sequential subagent execution** — Subagents run one at a time rather than
|
|
27
24
|
in parallel. The trade-off is full visibility: every step is streamed to
|
|
28
25
|
your terminal so you can follow exactly what each subagent is doing.
|
|
@@ -31,7 +28,7 @@ A lightweight CLI-based coding agent with zero framework dependencies.
|
|
|
31
28
|
|
|
32
29
|
- Node.js 22 or later
|
|
33
30
|
- LLM provider credentials
|
|
34
|
-
-
|
|
31
|
+
- Bash / Docker for sandboxed execution
|
|
35
32
|
- [ripgrep](https://github.com/burntsushi/ripgrep)
|
|
36
33
|
- [fd](https://github.com/sharkdp/fd)
|
|
37
34
|
|
|
@@ -52,8 +49,8 @@ Create the configuration.
|
|
|
52
49
|
```js
|
|
53
50
|
// ~/.config/plain-agent/config.local.json
|
|
54
51
|
{
|
|
55
|
-
"model": "
|
|
56
|
-
// "model": "
|
|
52
|
+
"model": "claude-sonnet-4-6+thinking-high",
|
|
53
|
+
// "model": "gpt-5.5+thinking-high",
|
|
57
54
|
|
|
58
55
|
// Configure the providers you want to use
|
|
59
56
|
"platforms": [
|
|
@@ -83,15 +80,11 @@ Create the configuration.
|
|
|
83
80
|
"provider": "gemini",
|
|
84
81
|
"apiKey": "<GEMINI_API_KEY>",
|
|
85
82
|
"model": "gemini-3-flash-preview"
|
|
86
|
-
// Optional
|
|
87
|
-
// "baseURL": "<proxy_url>"
|
|
88
83
|
|
|
89
84
|
// Or use Vertex AI (Requires gcloud CLI to get authentication token)
|
|
90
85
|
// "provider": "gemini-vertex-ai",
|
|
91
86
|
// "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
|
|
92
87
|
// "model": "gemini-3-flash-preview"
|
|
93
|
-
// Optional:
|
|
94
|
-
// "account": "<service_account_email>"
|
|
95
88
|
},
|
|
96
89
|
|
|
97
90
|
// askURL: Answers questions based on provided URL content.
|
|
@@ -100,15 +93,8 @@ Create the configuration.
|
|
|
100
93
|
"provider": "gemini",
|
|
101
94
|
"apiKey": "<GEMINI_API_KEY>"
|
|
102
95
|
"model": "gemini-3-flash-preview"
|
|
103
|
-
// Optional
|
|
104
|
-
// "baseURL": "<proxy_url>"
|
|
105
96
|
|
|
106
97
|
// Or use Vertex AI (Requires gcloud CLI to get authentication token)
|
|
107
|
-
// "provider": "gemini-vertex-ai",
|
|
108
|
-
// "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
|
|
109
|
-
// "model": "gemini-3-flash-preview"
|
|
110
|
-
// Optional:
|
|
111
|
-
// "account": "<service_account_email>"
|
|
112
98
|
}
|
|
113
99
|
},
|
|
114
100
|
|
|
@@ -166,6 +152,7 @@ Create the configuration.
|
|
|
166
152
|
```
|
|
167
153
|
|
|
168
154
|
```js
|
|
155
|
+
// OpenAI-compatible provider (Ollama) example with a custom model
|
|
169
156
|
{
|
|
170
157
|
"platforms": [
|
|
171
158
|
{
|
|
@@ -279,6 +266,9 @@ plain
|
|
|
279
266
|
plain -m <model+variant>
|
|
280
267
|
```
|
|
281
268
|
|
|
269
|
+
Interrupt the agent while it's running:
|
|
270
|
+
Press **Ctrl-C** to pause auto-approve. The agent will finish the current tool call, then return to the prompt.
|
|
271
|
+
|
|
282
272
|
(Optional) Set up a sandbox for your project with the `sandbox-configurator` agent.
|
|
283
273
|
|
|
284
274
|
```
|
|
@@ -292,7 +282,7 @@ After the agent finishes, run the generated setup script once to build the sandb
|
|
|
292
282
|
```
|
|
293
283
|
|
|
294
284
|
Run in batch mode (non-interactive).
|
|
295
|
-
In batch mode, config files are not loaded automatically. Only the files specified with
|
|
285
|
+
In batch mode, config files are not loaded automatically. Only the files specified with `-c` are loaded.
|
|
296
286
|
|
|
297
287
|
```sh
|
|
298
288
|
plain batch \
|
|
@@ -307,21 +297,15 @@ Display the help message.
|
|
|
307
297
|
/help
|
|
308
298
|
```
|
|
309
299
|
|
|
310
|
-
Show
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
current month through today; override it with `--from` / `--to`. Multiple
|
|
314
|
-
currencies (e.g., USD and JPY) are aggregated separately.
|
|
300
|
+
Show daily token costs across sessions. `plain cost` reads
|
|
301
|
+
`~/.local/share/plain-agent/usage.jsonl`; use `--from` / `--to` to set the
|
|
302
|
+
period. Currencies are shown separately.
|
|
315
303
|
|
|
316
304
|
```sh
|
|
317
305
|
plain cost
|
|
318
306
|
plain cost --from 2026-04-01 --to 2026-04-30
|
|
319
307
|
```
|
|
320
308
|
|
|
321
|
-
Interrupt the agent while it's running:
|
|
322
|
-
|
|
323
|
-
Press **Ctrl-C** to pause auto-approve. The agent will finish the current tool call, then return to the prompt.
|
|
324
|
-
|
|
325
309
|
## Available Tools
|
|
326
310
|
|
|
327
311
|
The agent can use the following tools to assist with tasks:
|
|
@@ -338,29 +322,24 @@ The agent can use the following tools to assist with tasks:
|
|
|
338
322
|
|
|
339
323
|
## Configuration
|
|
340
324
|
|
|
325
|
+
Files are loaded in the following order. Settings in later files override earlier ones.
|
|
326
|
+
|
|
341
327
|
```
|
|
342
328
|
~/.config/plain-agent/
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
329
|
+
├── (1) config.json # User configuration
|
|
330
|
+
├── (2) config.local.json # User local configuration (including secrets)
|
|
331
|
+
├── prompts/ # Global/User-defined prompts
|
|
332
|
+
└── agents/ # Global/User-defined agent roles
|
|
347
333
|
|
|
348
334
|
<project-root>
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
335
|
+
└── .plain-agent/
|
|
336
|
+
├── (3) config.json # Project-specific configuration
|
|
337
|
+
├── (4) config.local.json # Project-specific local configuration (including secrets)
|
|
338
|
+
├── memory/ # Task-specific memory files
|
|
339
|
+
├── prompts/ # Project-specific prompts
|
|
340
|
+
└── agents/ # Project-specific agent roles
|
|
355
341
|
```
|
|
356
342
|
|
|
357
|
-
The agent loads configuration files in the following order. Settings in later files will override those in earlier files.
|
|
358
|
-
|
|
359
|
-
- `~/.config/plain-agent/config.json`
|
|
360
|
-
- `~/.config/plain-agent/config.local.json`
|
|
361
|
-
- `.plain-agent/config.json`
|
|
362
|
-
- `.plain-agent/config.local.json`
|
|
363
|
-
|
|
364
343
|
### Example
|
|
365
344
|
|
|
366
345
|
<details>
|
|
@@ -423,19 +402,19 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
423
402
|
"patterns": [
|
|
424
403
|
{
|
|
425
404
|
"toolName": { "$regex": "^(write_file|patch_file)$" },
|
|
426
|
-
"input": { "filePath": { "$regex": "
|
|
405
|
+
"input": { "filePath": { "$regex": "^\\.plain-agent/memory/.+\\.md$" } },
|
|
427
406
|
"action": "allow"
|
|
428
407
|
},
|
|
429
408
|
{
|
|
430
409
|
"toolName": { "$regex": "^(write_file|patch_file)$" },
|
|
431
|
-
"input": { "filePath": { "$regex": "^
|
|
410
|
+
"input": { "filePath": { "$regex": "^src/" } },
|
|
432
411
|
"action": "allow"
|
|
433
412
|
},
|
|
434
413
|
|
|
435
414
|
// ⚠️ Arbitrary code execution can access unauthorized files and networks. Always use a sandbox.
|
|
436
415
|
{
|
|
437
416
|
"toolName": "exec_command",
|
|
438
|
-
"input": { "command": "npm", "args": ["run", { "$regex": "^(
|
|
417
|
+
"input": { "command": "npm", "args": ["run", { "$regex": "^(lint|test)$" }] },
|
|
439
418
|
"action": "allow"
|
|
440
419
|
},
|
|
441
420
|
|
|
@@ -511,8 +490,8 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
511
490
|
}
|
|
512
491
|
},
|
|
513
492
|
|
|
514
|
-
// Override default notification command
|
|
515
|
-
// "notifyCmd": "
|
|
493
|
+
// Override default notification command (falls back to terminal bell)
|
|
494
|
+
// "notifyCmd": { "command": "plain-notify-desktop", "args": [] }
|
|
516
495
|
|
|
517
496
|
// (Optional) Voice input. See "Voice Input" below.
|
|
518
497
|
// "voiceInput": {
|
|
@@ -527,6 +506,17 @@ The agent loads configuration files in the following order. Settings in later fi
|
|
|
527
506
|
|
|
528
507
|
You can define reusable prompts in Markdown files.
|
|
529
508
|
|
|
509
|
+
### Locations
|
|
510
|
+
|
|
511
|
+
The agent searches for prompts in the following directories:
|
|
512
|
+
|
|
513
|
+
- `~/.config/plain-agent/prompts/`
|
|
514
|
+
- `.plain-agent/prompts/`
|
|
515
|
+
- `.claude/commands/`
|
|
516
|
+
- `.claude/skills/`
|
|
517
|
+
|
|
518
|
+
The prompt ID is the relative path of the file without the `.md` extension. For example, `.plain-agent/prompts/commit.md` becomes `/prompts:commit`.
|
|
519
|
+
|
|
530
520
|
### Prompt File Format
|
|
531
521
|
|
|
532
522
|
```md
|
|
@@ -548,30 +538,8 @@ import: https://raw.githubusercontent.com/anthropics/claude-code/5cff78741f54a0d
|
|
|
548
538
|
- Parallel execution of subagents is not supported. Delegate to subagents sequentially.
|
|
549
539
|
```
|
|
550
540
|
|
|
551
|
-
```md
|
|
552
|
-
---
|
|
553
|
-
import: https://raw.githubusercontent.com/anthropics/claude-code/db8834ba1d72e9a26fba30ac85f3bc4316bb0689/plugins/code-review/commands/code-review.md
|
|
554
|
-
---
|
|
555
|
-
|
|
556
|
-
- Parallel execution of subagents is not supported. Delegate to subagents sequentially.
|
|
557
|
-
- If CLAUDE.md is not found, refer to AGENTS.md instead for project rules and conventions.
|
|
558
|
-
- If the PR branch is already checked out, review changes from local files instead of fetching from GitHub.
|
|
559
|
-
- After explaining the review results to the user, ask whether to post the comments to GitHub as well.
|
|
560
|
-
```
|
|
561
|
-
|
|
562
541
|
Remote prompts are fetched and cached locally. The local content will be appended to the imported content.
|
|
563
542
|
|
|
564
|
-
### Locations
|
|
565
|
-
|
|
566
|
-
The agent searches for prompts in the following directories:
|
|
567
|
-
|
|
568
|
-
- `~/.config/plain-agent/prompts/`
|
|
569
|
-
- `.plain-agent/prompts/`
|
|
570
|
-
- `.claude/commands/`
|
|
571
|
-
- `.claude/skills/`
|
|
572
|
-
|
|
573
|
-
The prompt ID is the relative path of the file without the `.md` extension. For example, `.plain-agent/prompts/commit.md` becomes `/prompts:commit`.
|
|
574
|
-
|
|
575
543
|
### Shortcuts
|
|
576
544
|
|
|
577
545
|
Prompts located in a `shortcuts/` subdirectory (e.g., `.plain-agent/prompts/shortcuts/commit.md`) can be invoked directly as a top-level command (e.g., `/commit`).
|
|
@@ -580,6 +548,14 @@ Prompts located in a `shortcuts/` subdirectory (e.g., `.plain-agent/prompts/shor
|
|
|
580
548
|
|
|
581
549
|
Subagents are specialized agents designed for specific tasks.
|
|
582
550
|
|
|
551
|
+
### Locations
|
|
552
|
+
|
|
553
|
+
The agent searches for subagent definitions in the following directories:
|
|
554
|
+
|
|
555
|
+
- `~/.config/plain-agent/agents/`
|
|
556
|
+
- `.plain-agent/agents/`
|
|
557
|
+
- `.claude/agents/`
|
|
558
|
+
|
|
583
559
|
### Subagent File Format
|
|
584
560
|
|
|
585
561
|
```md
|
|
@@ -602,14 +578,6 @@ Use AGENTS.md instead of CLAUDE.md in this project.
|
|
|
602
578
|
|
|
603
579
|
Remote subagents are fetched and cached locally. The local content will be appended to the imported content.
|
|
604
580
|
|
|
605
|
-
### Locations
|
|
606
|
-
|
|
607
|
-
The agent searches for subagent definitions in the following directories:
|
|
608
|
-
|
|
609
|
-
- `~/.config/plain-agent/agents/`
|
|
610
|
-
- `.plain-agent/agents/`
|
|
611
|
-
- `.claude/agents/`
|
|
612
|
-
|
|
613
581
|
## Claude Code Plugin Support
|
|
614
582
|
|
|
615
583
|
Example:
|
|
@@ -653,7 +621,7 @@ and send them like regular text.
|
|
|
653
621
|
|
|
654
622
|
### Providers
|
|
655
623
|
|
|
656
|
-
**OpenAI Realtime**
|
|
624
|
+
**OpenAI Realtime**
|
|
657
625
|
|
|
658
626
|
```js
|
|
659
627
|
{
|
|
@@ -666,7 +634,7 @@ and send them like regular text.
|
|
|
666
634
|
}
|
|
667
635
|
```
|
|
668
636
|
|
|
669
|
-
**Gemini Live**
|
|
637
|
+
**Gemini Live**
|
|
670
638
|
|
|
671
639
|
```js
|
|
672
640
|
{
|
|
@@ -683,8 +651,7 @@ and send them like regular text.
|
|
|
683
651
|
|
|
684
652
|
- `toggleKey` — Rebind the toggle. Accepts `"ctrl-<char>"` where `<char>`
|
|
685
653
|
is a letter (a-z) or one of `[ \ ] ^ _`. Defaults to `"ctrl-o"`.
|
|
686
|
-
- `recorder` — Override recorder auto-detection. Must write raw 16-bit
|
|
687
|
-
little-endian mono PCM to stdout at 24 kHz (OpenAI) or 16 kHz (Gemini).
|
|
654
|
+
- `recorder` — Override recorder auto-detection, e.g. `{ "command": "sox", "args": ["-q", "-d", "-b", "16", "-c", "1", "-r", "24000", "-e", "signed-integer", "-t", "raw", "-"] }`. Must write raw 16-bit little-endian mono PCM to stdout at 24 kHz (OpenAI) or 16 kHz (Gemini).
|
|
688
655
|
|
|
689
656
|
## Development
|
|
690
657
|
|
|
@@ -705,13 +672,9 @@ npx npm-check-updates -t minor -c 3 -u
|
|
|
705
672
|
## Release
|
|
706
673
|
|
|
707
674
|
```sh
|
|
708
|
-
npm run check
|
|
709
|
-
|
|
710
|
-
git commit -m "<message>"
|
|
711
|
-
|
|
712
675
|
npm version <major|minor|patch>
|
|
713
|
-
git push --follow-tags
|
|
714
676
|
|
|
677
|
+
git push --follow-tags
|
|
715
678
|
gh release create $(git describe --tags) --generate-notes
|
|
716
679
|
|
|
717
680
|
npm publish --access public
|
|
@@ -813,7 +813,7 @@
|
|
|
813
813
|
}
|
|
814
814
|
},
|
|
815
815
|
{
|
|
816
|
-
"name": "gpt-5.
|
|
816
|
+
"name": "gpt-5.5",
|
|
817
817
|
"variant": "thinking-medium",
|
|
818
818
|
"platform": {
|
|
819
819
|
"name": "openai",
|
|
@@ -823,7 +823,7 @@
|
|
|
823
823
|
"model": {
|
|
824
824
|
"format": "openai-responses",
|
|
825
825
|
"config": {
|
|
826
|
-
"model": "gpt-5.
|
|
826
|
+
"model": "gpt-5.5",
|
|
827
827
|
"reasoning": { "effort": "medium", "summary": "auto" },
|
|
828
828
|
"store": false,
|
|
829
829
|
"include": ["reasoning.encrypted_content"]
|
|
@@ -833,14 +833,14 @@
|
|
|
833
833
|
"currency": "USD",
|
|
834
834
|
"unit": "1M",
|
|
835
835
|
"costs": {
|
|
836
|
-
"input_tokens":
|
|
837
|
-
"input_tokens_details.cached_tokens": -
|
|
838
|
-
"output_tokens":
|
|
836
|
+
"input_tokens": 5,
|
|
837
|
+
"input_tokens_details.cached_tokens": -4.5,
|
|
838
|
+
"output_tokens": 30
|
|
839
839
|
}
|
|
840
840
|
}
|
|
841
841
|
},
|
|
842
842
|
{
|
|
843
|
-
"name": "gpt-5.
|
|
843
|
+
"name": "gpt-5.5",
|
|
844
844
|
"variant": "thinking-high",
|
|
845
845
|
"platform": {
|
|
846
846
|
"name": "openai",
|
|
@@ -850,7 +850,7 @@
|
|
|
850
850
|
"model": {
|
|
851
851
|
"format": "openai-responses",
|
|
852
852
|
"config": {
|
|
853
|
-
"model": "gpt-5.
|
|
853
|
+
"model": "gpt-5.5",
|
|
854
854
|
"reasoning": { "effort": "high", "summary": "auto" },
|
|
855
855
|
"store": false,
|
|
856
856
|
"include": ["reasoning.encrypted_content"]
|
|
@@ -860,14 +860,14 @@
|
|
|
860
860
|
"currency": "USD",
|
|
861
861
|
"unit": "1M",
|
|
862
862
|
"costs": {
|
|
863
|
-
"input_tokens":
|
|
864
|
-
"input_tokens_details.cached_tokens": -
|
|
865
|
-
"output_tokens":
|
|
863
|
+
"input_tokens": 5,
|
|
864
|
+
"input_tokens_details.cached_tokens": -4.5,
|
|
865
|
+
"output_tokens": 30
|
|
866
866
|
}
|
|
867
867
|
}
|
|
868
868
|
},
|
|
869
869
|
{
|
|
870
|
-
"name": "gpt-5.
|
|
870
|
+
"name": "gpt-5.5",
|
|
871
871
|
"variant": "thinking-xhigh",
|
|
872
872
|
"platform": {
|
|
873
873
|
"name": "openai",
|
|
@@ -877,7 +877,7 @@
|
|
|
877
877
|
"model": {
|
|
878
878
|
"format": "openai-responses",
|
|
879
879
|
"config": {
|
|
880
|
-
"model": "gpt-5.
|
|
880
|
+
"model": "gpt-5.5",
|
|
881
881
|
"reasoning": { "effort": "xhigh", "summary": "auto" },
|
|
882
882
|
"store": false,
|
|
883
883
|
"include": ["reasoning.encrypted_content"]
|
|
@@ -887,9 +887,9 @@
|
|
|
887
887
|
"currency": "USD",
|
|
888
888
|
"unit": "1M",
|
|
889
889
|
"costs": {
|
|
890
|
-
"input_tokens":
|
|
891
|
-
"input_tokens_details.cached_tokens": -
|
|
892
|
-
"output_tokens":
|
|
890
|
+
"input_tokens": 5,
|
|
891
|
+
"input_tokens_details.cached_tokens": -4.5,
|
|
892
|
+
"output_tokens": 30
|
|
893
893
|
}
|
|
894
894
|
}
|
|
895
895
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iinm/plain-agent",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.20",
|
|
4
4
|
"description": "A lightweight CLI-based coding agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,9 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
12
|
"plain": "./bin/plain",
|
|
13
|
-
"plain-interrupt": "./bin/plain-interrupt",
|
|
14
13
|
"plain-notify-desktop": "./bin/plain-notify-desktop",
|
|
15
|
-
"plain-notify-terminal-bell": "./bin/plain-notify-terminal-bell",
|
|
16
14
|
"plain-sandbox": "./sandbox/bin/plain-sandbox"
|
|
17
15
|
},
|
|
18
16
|
"files": [
|
package/src/agentLoop.mjs
CHANGED
|
@@ -128,7 +128,9 @@ export function createAgentLoop({
|
|
|
128
128
|
|
|
129
129
|
const { message: assistantMessage, providerTokenUsage } = modelOutput;
|
|
130
130
|
stateManager.appendMessages([assistantMessage]);
|
|
131
|
-
|
|
131
|
+
if (providerTokenUsage) {
|
|
132
|
+
agentEventEmitter.emit("providerTokenUsage", providerTokenUsage);
|
|
133
|
+
}
|
|
132
134
|
|
|
133
135
|
// Gemini may stop with "thinking" -> continue
|
|
134
136
|
const lastContent = assistantMessage.content.at(-1);
|
package/src/cliCost.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { styleText } from "node:util";
|
|
6
|
-
import
|
|
6
|
+
import * as usageStore from "./usageStore.mjs";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* @typedef {Object} CostPeriod
|
|
@@ -72,15 +72,15 @@ export function parseDateOnly(value) {
|
|
|
72
72
|
if (!match) {
|
|
73
73
|
throw new Error(`Invalid date: "${value}" (expected YYYY-MM-DD)`);
|
|
74
74
|
}
|
|
75
|
-
const [,
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const date = new Date(
|
|
75
|
+
const [, year, month, day] = match;
|
|
76
|
+
const y = Number(year);
|
|
77
|
+
const m = Number(month);
|
|
78
|
+
const d = Number(day);
|
|
79
|
+
const date = new Date(y, m - 1, d);
|
|
80
80
|
if (
|
|
81
|
-
date.getFullYear() !==
|
|
82
|
-
date.getMonth() !==
|
|
83
|
-
date.getDate() !==
|
|
81
|
+
date.getFullYear() !== y ||
|
|
82
|
+
date.getMonth() !== m - 1 ||
|
|
83
|
+
date.getDate() !== d
|
|
84
84
|
) {
|
|
85
85
|
throw new Error(`Invalid date: "${value}"`);
|
|
86
86
|
}
|
|
@@ -109,6 +109,10 @@ export function aggregateUsage(records, period) {
|
|
|
109
109
|
let excludedOutOfRange = 0;
|
|
110
110
|
|
|
111
111
|
for (const record of records) {
|
|
112
|
+
if (record.timestamp == null) {
|
|
113
|
+
excludedOutOfRange++;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
112
116
|
const recordedAt = new Date(record.timestamp);
|
|
113
117
|
if (Number.isNaN(recordedAt.getTime())) {
|
|
114
118
|
excludedOutOfRange++;
|
|
@@ -123,12 +127,13 @@ export function aggregateUsage(records, period) {
|
|
|
123
127
|
noPricingSessionCount++;
|
|
124
128
|
continue;
|
|
125
129
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
perDate = new Map();
|
|
130
|
-
byCurrency.set(record.currency, perDate);
|
|
130
|
+
if (!record.currency || typeof record.currency !== "string") {
|
|
131
|
+
excludedOutOfRange++;
|
|
132
|
+
continue;
|
|
131
133
|
}
|
|
134
|
+
|
|
135
|
+
const perDate = byCurrency.get(record.currency) ?? new Map();
|
|
136
|
+
byCurrency.set(record.currency, perDate);
|
|
132
137
|
const existing = perDate.get(localDate);
|
|
133
138
|
if (existing) {
|
|
134
139
|
existing.totalCost += record.totalCost;
|
|
@@ -146,13 +151,14 @@ export function aggregateUsage(records, period) {
|
|
|
146
151
|
const aggregations = [];
|
|
147
152
|
for (const [currency, perDate] of byCurrency) {
|
|
148
153
|
const daily = Array.from(perDate.values()).sort((a, b) =>
|
|
149
|
-
a.date
|
|
150
|
-
);
|
|
151
|
-
const totalCost = daily.reduce((sum, entry) => sum + entry.totalCost, 0);
|
|
152
|
-
const sessionCount = daily.reduce(
|
|
153
|
-
(sum, entry) => sum + entry.sessionCount,
|
|
154
|
-
0,
|
|
154
|
+
a.date.localeCompare(b.date),
|
|
155
155
|
);
|
|
156
|
+
let totalCost = 0;
|
|
157
|
+
let sessionCount = 0;
|
|
158
|
+
for (const entry of daily) {
|
|
159
|
+
totalCost += entry.totalCost;
|
|
160
|
+
sessionCount += entry.sessionCount;
|
|
161
|
+
}
|
|
156
162
|
aggregations.push({ currency, daily, totalCost, sessionCount });
|
|
157
163
|
}
|
|
158
164
|
aggregations.sort((a, b) => a.currency.localeCompare(b.currency));
|
|
@@ -166,6 +172,14 @@ export function aggregateUsage(records, period) {
|
|
|
166
172
|
};
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
/**
|
|
176
|
+
* @param {number} count
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function formatSessions(count) {
|
|
180
|
+
return `${count} session${count === 1 ? "" : "s"}`;
|
|
181
|
+
}
|
|
182
|
+
|
|
169
183
|
/**
|
|
170
184
|
* Render a cost report as a human-readable string.
|
|
171
185
|
*
|
|
@@ -175,10 +189,9 @@ export function aggregateUsage(records, period) {
|
|
|
175
189
|
*/
|
|
176
190
|
export function formatCostReport(report, options = {}) {
|
|
177
191
|
const color = options.color ?? true;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
text;
|
|
192
|
+
/** @param {string | string[]} _modifiers @param {string} text @returns {string} */
|
|
193
|
+
const plainStyle = (_modifiers, text) => text;
|
|
194
|
+
const style = color ? styleText : plainStyle;
|
|
182
195
|
|
|
183
196
|
const lines = [];
|
|
184
197
|
lines.push(
|
|
@@ -204,14 +217,14 @@ export function formatCostReport(report, options = {}) {
|
|
|
204
217
|
lines.push(style("bold", `Daily cost (${agg.currency}):`));
|
|
205
218
|
for (const entry of agg.daily) {
|
|
206
219
|
lines.push(
|
|
207
|
-
` ${entry.date} ${formatCost(entry.totalCost)} ${agg.currency} (${entry.sessionCount}
|
|
220
|
+
` ${entry.date} ${formatCost(entry.totalCost)} ${agg.currency} (${formatSessions(entry.sessionCount)})`,
|
|
208
221
|
);
|
|
209
222
|
}
|
|
210
223
|
lines.push("");
|
|
211
224
|
lines.push(
|
|
212
225
|
style(
|
|
213
226
|
"bold",
|
|
214
|
-
`Total: ${formatCost(agg.totalCost)} ${agg.currency} (${agg.sessionCount}
|
|
227
|
+
`Total: ${formatCost(agg.totalCost)} ${agg.currency} (${formatSessions(agg.sessionCount)})`,
|
|
215
228
|
),
|
|
216
229
|
);
|
|
217
230
|
}
|
|
@@ -242,28 +255,50 @@ function formatCost(value) {
|
|
|
242
255
|
* Run the `plain cost` subcommand.
|
|
243
256
|
*
|
|
244
257
|
* @param {{ from: string | null, to: string | null }} args
|
|
258
|
+
* @param {{ readUsageRecords?: typeof import("./usageStore.mjs").readUsageRecords }} [deps]
|
|
245
259
|
* @returns {Promise<number>} exit code
|
|
246
260
|
*/
|
|
247
|
-
export async function runCostCommand(args) {
|
|
248
|
-
|
|
261
|
+
export async function runCostCommand(args, deps = {}) {
|
|
262
|
+
let from;
|
|
263
|
+
let to;
|
|
264
|
+
try {
|
|
265
|
+
({ from, to } = resolvePeriod(args));
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (err instanceof Error) {
|
|
268
|
+
console.error(`Error: ${err.message}`);
|
|
269
|
+
} else {
|
|
270
|
+
console.error("Error: invalid period arguments");
|
|
271
|
+
}
|
|
272
|
+
return 1;
|
|
273
|
+
}
|
|
249
274
|
|
|
250
|
-
const { records, skipped } = await
|
|
275
|
+
const { records, skipped } = await (
|
|
276
|
+
deps.readUsageRecords ?? usageStore.readUsageRecords
|
|
277
|
+
)();
|
|
251
278
|
if (skipped.length > 0) {
|
|
279
|
+
const details = skipped
|
|
280
|
+
.slice(0, 3)
|
|
281
|
+
.map((s) => `line ${s.line}: ${s.reason}`)
|
|
282
|
+
.join(", ");
|
|
283
|
+
const ellipsis =
|
|
284
|
+
skipped.length > 3 ? `, and ${skipped.length - 3} more` : "";
|
|
252
285
|
console.error(
|
|
253
|
-
`Warning: skipped ${skipped.length} malformed line(s) in usage log.`,
|
|
286
|
+
`Warning: skipped ${skipped.length} malformed line(s) in usage log (${details}${ellipsis}).`,
|
|
254
287
|
);
|
|
255
288
|
}
|
|
256
289
|
|
|
257
290
|
const report = aggregateUsage(records, { from, to });
|
|
258
291
|
console.log(formatCostReport(report));
|
|
259
|
-
return 0;
|
|
292
|
+
return skipped.length > 0 ? 1 : 0;
|
|
260
293
|
}
|
|
261
294
|
|
|
262
295
|
/**
|
|
296
|
+
* Resolve a period from CLI arguments, falling back to the current month.
|
|
297
|
+
*
|
|
263
298
|
* @param {{ from: string | null, to: string | null }} args
|
|
264
299
|
* @returns {CostPeriod}
|
|
265
300
|
*/
|
|
266
|
-
function resolvePeriod(args) {
|
|
301
|
+
export function resolvePeriod(args) {
|
|
267
302
|
const fallback = defaultPeriod();
|
|
268
303
|
const from = args.from ?? fallback.from;
|
|
269
304
|
const to = args.to ?? fallback.to;
|
package/src/cliFormatter.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export function formatToolUse(toolUse) {
|
|
|
91
91
|
const diffs = [];
|
|
92
92
|
const matches = Array.from(
|
|
93
93
|
diff.matchAll(
|
|
94
|
-
|
|
94
|
+
/<<< [0-9a-z]{3} <<< SEARCH\n(.*?)\n=== [0-9a-z]{3} ===\n(.*?)\n?>>> [0-9a-z]{3} >>> REPLACE/gs,
|
|
95
95
|
),
|
|
96
96
|
);
|
|
97
97
|
for (const match of matches) {
|
package/src/cliInteractive.mjs
CHANGED
|
@@ -57,7 +57,7 @@ const HELP_MESSAGE = [
|
|
|
57
57
|
* @property {AgentCommands} agentCommands
|
|
58
58
|
* @property {string} sessionId
|
|
59
59
|
* @property {string} modelName
|
|
60
|
-
* @property {string} notifyCmd
|
|
60
|
+
* @property {{ command: string; args?: string[] } | undefined} notifyCmd
|
|
61
61
|
* @property {boolean} sandbox
|
|
62
62
|
* @property {() => Promise<void>} onStop
|
|
63
63
|
* @property {ClaudeCodePlugin[]} [claudeCodePlugins]
|
package/src/config.d.ts
CHANGED
|
@@ -21,14 +21,14 @@ export type AppConfig = {
|
|
|
21
21
|
askURL?: AskURLToolOptions;
|
|
22
22
|
};
|
|
23
23
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
24
|
-
notifyCmd?: string;
|
|
24
|
+
notifyCmd?: { command: string; args?: string[] };
|
|
25
25
|
voiceInput?: VoiceInputConfig;
|
|
26
26
|
claudeCodePlugins?: ClaudeCodePluginRepo[];
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
export type MCPServerConfig = {
|
|
30
30
|
command: string;
|
|
31
|
-
args
|
|
31
|
+
args?: string[];
|
|
32
32
|
env?: Record<string, string>;
|
|
33
33
|
options?: {
|
|
34
34
|
enabledTools?: string[];
|
package/src/config.mjs
CHANGED
|
@@ -93,7 +93,7 @@ export async function loadAppConfig(options = {}) {
|
|
|
93
93
|
...(merged.mcpServers ?? {}),
|
|
94
94
|
...(config.mcpServers ?? {}),
|
|
95
95
|
},
|
|
96
|
-
notifyCmd: config.notifyCmd
|
|
96
|
+
notifyCmd: config.notifyCmd ?? merged.notifyCmd,
|
|
97
97
|
claudeCodePlugins: [
|
|
98
98
|
...(merged.claudeCodePlugins ?? []),
|
|
99
99
|
...(config.claudeCodePlugins ?? []),
|