@iinm/plain-agent 1.9.3 → 1.10.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/README.md +40 -12
- package/package.json +1 -1
- package/src/agent.d.ts +14 -2
- package/src/agent.mjs +67 -55
- package/src/cliArgs.mjs +46 -1
- package/src/cliBatch.mjs +1 -0
- package/src/cliCommands.mjs +0 -12
- package/src/cliCompleter.mjs +0 -2
- package/src/cliCost.mjs +21 -1
- package/src/cliFormatter.mjs +19 -12
- package/src/cliInteractive.mjs +3 -2
- package/src/config.d.ts +64 -4
- package/src/config.mjs +8 -8
- package/src/costTracker.mjs +29 -0
- package/src/env.mjs +1 -5
- package/src/main.mjs +229 -39
- package/src/sessionStore.mjs +164 -0
- package/src/subagent.mjs +56 -0
- package/src/tool.d.ts +2 -0
- package/src/toolUseApprover.mjs +25 -0
- package/src/tools/execCommand.mjs +1 -0
- package/src/tools/webFetch.mjs +442 -0
- package/src/tools/webSearch.mjs +503 -0
- package/src/tools/askURL.mjs +0 -209
- package/src/tools/askWeb.mjs +0 -208
package/README.md
CHANGED
|
@@ -62,7 +62,6 @@ A lightweight, capable coding agent for the terminal.
|
|
|
62
62
|
## Limitations
|
|
63
63
|
|
|
64
64
|
- **Path validation only covers tool arguments** — Path validation restricts only paths explicitly passed as tool-use arguments; it cannot control file access inside arbitrary scripts. Always use sandboxed execution when allowing arbitrary script execution.
|
|
65
|
-
- **No session persistence** — Sessions are not persisted. Start a fresh session and use memory files (`.plain-agent/memory/`) instead.
|
|
66
65
|
- **Sequential subagent execution** — Subagents run one at a time rather than
|
|
67
66
|
in parallel. The trade-off is full visibility: every step is streamed to
|
|
68
67
|
your terminal so you can follow exactly what each subagent is doing.
|
|
@@ -115,25 +114,35 @@ Create the configuration.
|
|
|
115
114
|
}
|
|
116
115
|
],
|
|
117
116
|
|
|
118
|
-
// (Optional) Enable web
|
|
117
|
+
// (Optional) Enable web tools
|
|
119
118
|
"tools": {
|
|
120
|
-
|
|
121
|
-
"askWeb": {
|
|
119
|
+
"webSearch": {
|
|
122
120
|
"provider": "gemini",
|
|
123
121
|
"apiKey": "<GEMINI_API_KEY>",
|
|
124
122
|
"model": "gemini-3-flash-preview"
|
|
123
|
+
|
|
125
124
|
// Or use Vertex AI (Requires gcloud CLI to get authentication token)
|
|
126
125
|
// "provider": "gemini-vertex-ai",
|
|
127
126
|
// "baseURL": "https://aiplatform.googleapis.com/v1beta1/projects/<project_id>/locations/<location>",
|
|
128
127
|
// "model": "gemini-3-flash-preview"
|
|
128
|
+
|
|
129
|
+
// Or use a custom command
|
|
130
|
+
// "provider": "command",
|
|
131
|
+
// "command": "bash",
|
|
132
|
+
// "args": ["-c", "w3m -dump -o display_link_number=1 \"https://lite.duckduckgo.com/lite?q=$*\"", "-"]
|
|
129
133
|
},
|
|
130
134
|
|
|
131
|
-
|
|
132
|
-
"askURL": {
|
|
135
|
+
"webFetch": {
|
|
133
136
|
"provider": "gemini",
|
|
134
137
|
"apiKey": "<GEMINI_API_KEY>",
|
|
135
138
|
"model": "gemini-3-flash-preview"
|
|
139
|
+
|
|
136
140
|
// Or use Vertex AI (Requires gcloud CLI to get authentication token)
|
|
141
|
+
|
|
142
|
+
// Or use a custom command
|
|
143
|
+
// "provider": "command",
|
|
144
|
+
// "command": "w3m",
|
|
145
|
+
// "args": ["-dump", "-o", "display_link_number=1"]
|
|
137
146
|
}
|
|
138
147
|
}
|
|
139
148
|
}
|
|
@@ -338,6 +347,21 @@ plain cost
|
|
|
338
347
|
plain cost --from 2026-04-01 --to 2026-04-30
|
|
339
348
|
```
|
|
340
349
|
|
|
350
|
+
Resume a previously interrupted interactive session. Sessions are
|
|
351
|
+
auto-saved to `.plain-agent/sessions/` and can be removed with `rm` when
|
|
352
|
+
no longer needed. Without an argument, the most recently updated session
|
|
353
|
+
is resumed. Use `--list` to see resumable sessions. Switching models is
|
|
354
|
+
not supported (`-m` is rejected).
|
|
355
|
+
|
|
356
|
+
```sh
|
|
357
|
+
plain resume
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
plain resume --list
|
|
362
|
+
plain resume 2026-05-10-0803-a7k
|
|
363
|
+
```
|
|
364
|
+
|
|
341
365
|
Configure plain-agent for your project.
|
|
342
366
|
|
|
343
367
|
```
|
|
@@ -388,7 +412,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
388
412
|
"action": "allow"
|
|
389
413
|
},
|
|
390
414
|
{
|
|
391
|
-
"toolName": { "$regex": "^(
|
|
415
|
+
"toolName": { "$regex": "^(web_search|web_fetch)$" },
|
|
392
416
|
"action": "allow"
|
|
393
417
|
}
|
|
394
418
|
// ⚠️ Never do this. mcp run outside the sandbox, so they can send anything externally.
|
|
@@ -447,7 +471,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
447
471
|
},
|
|
448
472
|
|
|
449
473
|
{
|
|
450
|
-
"toolName": { "$regex": "^(
|
|
474
|
+
"toolName": { "$regex": "^(web_search|web_fetch)$" },
|
|
451
475
|
"action": "allow"
|
|
452
476
|
},
|
|
453
477
|
|
|
@@ -539,8 +563,8 @@ The agent can use the following tools to assist with tasks:
|
|
|
539
563
|
- **patch_file**: Patch a file.
|
|
540
564
|
- **exec_command**: Run a command without shell interpretation.
|
|
541
565
|
- **tmux_command**: Run a tmux command.
|
|
542
|
-
- **
|
|
543
|
-
- **
|
|
566
|
+
- **web_search**: Search the web with one or more keyword sets and answer a question based on the combined results (requires Google API key, Vertex AI configuration, or the `command` provider with a local search command).
|
|
567
|
+
- **web_fetch**: Fetch the contents of a single URL and answer a question based on it (requires Google API key, Vertex AI configuration, or the `command` provider with a local fetch command such as `w3m`, `curl`, or `lynx`).
|
|
544
568
|
- **switch_to_subagent**: Switch to a subagent role within the same conversation, focusing on the specified goal.
|
|
545
569
|
- **switch_to_main_agent**: Switch back to the main agent role and report the result. After reporting, the subagent's conversation history is removed from the context.
|
|
546
570
|
- **compact_context**: Compact the conversation context by discarding prior messages and reloading task state from a memory file. Use when the context has grown large but the task is not yet complete. Can also be invoked via the `/compact` slash command.
|
|
@@ -591,10 +615,14 @@ The agent searches for subagent definitions in the following directories:
|
|
|
591
615
|
|
|
592
616
|
```md
|
|
593
617
|
---
|
|
594
|
-
description:
|
|
618
|
+
description: Fetches a web page and answers questions about its content
|
|
595
619
|
---
|
|
596
620
|
|
|
597
|
-
You are a
|
|
621
|
+
You are a web content reader and analyzer. Given a URL and a question, you:
|
|
622
|
+
|
|
623
|
+
1. Fetch the page content using `w3m -dump <URL>`.
|
|
624
|
+
2. Read and understand the fetched content.
|
|
625
|
+
3. Answer the user's question based on the content.
|
|
598
626
|
```
|
|
599
627
|
|
|
600
628
|
## Claude Code Plugin Support
|
package/package.json
CHANGED
package/src/agent.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
PartialMessageContent,
|
|
10
10
|
ProviderTokenUsage,
|
|
11
11
|
} from "./model";
|
|
12
|
+
import type { SessionState } from "./sessionStore.mjs";
|
|
12
13
|
import type { Tool, ToolUseApprover } from "./tool";
|
|
13
14
|
|
|
14
15
|
export type Agent = {
|
|
@@ -18,10 +19,12 @@ export type Agent = {
|
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type AgentCommands = {
|
|
21
|
-
dumpMessages: () => Promise<void>;
|
|
22
|
-
loadMessages: () => Promise<void>;
|
|
23
22
|
getCostSummary: () => CostSummary;
|
|
24
23
|
pauseAutoApprove: () => void;
|
|
24
|
+
/** Subagent currently active for this session, or null. */
|
|
25
|
+
getActiveSubagent: () => { name: string } | null;
|
|
26
|
+
/** Wait for any pending session-state writes to flush to disk. */
|
|
27
|
+
flushSessionPersistence: () => Promise<void>;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
30
|
type UserEventMap = {
|
|
@@ -49,4 +52,13 @@ export type AgentConfig = {
|
|
|
49
52
|
toolUseApprover: ToolUseApprover;
|
|
50
53
|
agentRoles: Map<string, AgentRole>;
|
|
51
54
|
modelCostConfig?: CostConfig;
|
|
55
|
+
/** Metadata used when persisting session state. */
|
|
56
|
+
sessionMetadata: {
|
|
57
|
+
sessionId: string;
|
|
58
|
+
modelName: string;
|
|
59
|
+
workingDir: string;
|
|
60
|
+
startTime: Date;
|
|
61
|
+
};
|
|
62
|
+
/** When provided, the agent restores its state from this snapshot. */
|
|
63
|
+
initialState?: SessionState | null;
|
|
52
64
|
};
|
package/src/agent.mjs
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { EventEmitter } from "node:events";
|
|
10
|
-
import
|
|
10
|
+
import { styleText } from "node:util";
|
|
11
11
|
import { createAgentLoop } from "./agentLoop.mjs";
|
|
12
12
|
import { createStateManager } from "./agentState.mjs";
|
|
13
13
|
import { createCostTracker } from "./costTracker.mjs";
|
|
14
|
-
import {
|
|
14
|
+
import { SESSION_FILE_VERSION, saveSession } from "./sessionStore.mjs";
|
|
15
15
|
import { createSubagentManager } from "./subagent.mjs";
|
|
16
16
|
import { createToolExecutor } from "./toolExecutor.mjs";
|
|
17
17
|
import {
|
|
@@ -32,6 +32,8 @@ export function createAgent({
|
|
|
32
32
|
toolUseApprover,
|
|
33
33
|
agentRoles,
|
|
34
34
|
modelCostConfig,
|
|
35
|
+
sessionMetadata,
|
|
36
|
+
initialState,
|
|
35
37
|
}) {
|
|
36
38
|
/** @type {UserEventEmitter} */
|
|
37
39
|
const userEventEmitter = new EventEmitter();
|
|
@@ -44,23 +46,27 @@ export function createAgent({
|
|
|
44
46
|
costTracker.recordUsage(usage);
|
|
45
47
|
});
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
],
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
49
|
+
// Build the initial message list. When resuming, replace messages[0] with
|
|
50
|
+
// the freshly built system prompt (today/agent roles/skills may have
|
|
51
|
+
// changed) but keep the rest of the saved conversation verbatim.
|
|
52
|
+
/** @type {import("./model").SystemMessage} */
|
|
53
|
+
const systemMessage = {
|
|
54
|
+
role: "system",
|
|
55
|
+
content: [{ type: "text", text: prompt }],
|
|
56
|
+
};
|
|
57
|
+
const baseMessages = initialState?.messages?.length
|
|
58
|
+
? [systemMessage, ...initialState.messages.slice(1)]
|
|
59
|
+
: [systemMessage];
|
|
60
|
+
|
|
61
|
+
const stateManager = createStateManager(baseMessages, {
|
|
62
|
+
onMessagesAppended: (newMessages) => {
|
|
63
|
+
const lastMessage = newMessages.at(-1);
|
|
64
|
+
if (lastMessage) {
|
|
60
65
|
agentEventEmitter.emit("message", lastMessage);
|
|
61
|
-
}
|
|
66
|
+
}
|
|
67
|
+
schedulePersist();
|
|
62
68
|
},
|
|
63
|
-
);
|
|
69
|
+
});
|
|
64
70
|
|
|
65
71
|
const subagentManager = createSubagentManager(agentRoles, {
|
|
66
72
|
onSubagentSwitched: (subagent) => {
|
|
@@ -68,6 +74,46 @@ export function createAgent({
|
|
|
68
74
|
},
|
|
69
75
|
});
|
|
70
76
|
|
|
77
|
+
// Restore the rest of the session state. Subagent restoration is silent
|
|
78
|
+
// (no event), since CLI listeners aren't attached yet — the CLI consults
|
|
79
|
+
// getActiveSubagent() at startup instead.
|
|
80
|
+
if (initialState) {
|
|
81
|
+
subagentManager.restoreState(initialState.subagentState);
|
|
82
|
+
toolUseApprover.restoreAllowedToolUseInSession(
|
|
83
|
+
initialState.allowedToolUseInSession,
|
|
84
|
+
);
|
|
85
|
+
costTracker.restoreUsageHistory(initialState.tokenUsageHistory);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type {Promise<void>} */
|
|
89
|
+
let persistChain = Promise.resolve();
|
|
90
|
+
function schedulePersist() {
|
|
91
|
+
persistChain = persistChain.then(async () => {
|
|
92
|
+
try {
|
|
93
|
+
await saveSession({
|
|
94
|
+
version: SESSION_FILE_VERSION,
|
|
95
|
+
sessionId: sessionMetadata.sessionId,
|
|
96
|
+
modelName: sessionMetadata.modelName,
|
|
97
|
+
workingDir: sessionMetadata.workingDir,
|
|
98
|
+
startTime: sessionMetadata.startTime.toISOString(),
|
|
99
|
+
lastUpdatedAt: new Date().toISOString(),
|
|
100
|
+
messages: stateManager.getMessages(),
|
|
101
|
+
subagentState: subagentManager.getState(),
|
|
102
|
+
allowedToolUseInSession: toolUseApprover.getAllowedToolUseInSession(),
|
|
103
|
+
tokenUsageHistory: costTracker.getUsageHistory(),
|
|
104
|
+
});
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
107
|
+
console.error(
|
|
108
|
+
styleText(
|
|
109
|
+
"yellow",
|
|
110
|
+
`Warning: failed to persist session state: ${message}`,
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
/**
|
|
72
118
|
* @param {SwitchToSubagentInput} input
|
|
73
119
|
*/
|
|
@@ -130,42 +176,6 @@ export function createAgent({
|
|
|
130
176
|
exclusiveToolNames: [switchToSubagentToolName, switchToMainAgentToolName],
|
|
131
177
|
});
|
|
132
178
|
|
|
133
|
-
async function dumpMessages() {
|
|
134
|
-
const filePath = MESSAGES_DUMP_FILE_PATH;
|
|
135
|
-
try {
|
|
136
|
-
await fs.writeFile(
|
|
137
|
-
filePath,
|
|
138
|
-
JSON.stringify(stateManager.getMessages(), null, 2),
|
|
139
|
-
);
|
|
140
|
-
console.log(`Messages dumped to ${filePath}`);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
143
|
-
console.error(`Error dumping messages: ${message}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
async function loadMessages() {
|
|
148
|
-
const filePath = MESSAGES_DUMP_FILE_PATH;
|
|
149
|
-
try {
|
|
150
|
-
const data = await fs.readFile(filePath, "utf-8");
|
|
151
|
-
const loadedMessages = JSON.parse(data);
|
|
152
|
-
if (Array.isArray(loadedMessages)) {
|
|
153
|
-
// Keep the system message (index 0) and replace the rest
|
|
154
|
-
stateManager.setMessages([
|
|
155
|
-
stateManager.getMessageAt(0),
|
|
156
|
-
...loadedMessages.slice(1),
|
|
157
|
-
]);
|
|
158
|
-
console.log(`Messages loaded from ${filePath}`);
|
|
159
|
-
} else {
|
|
160
|
-
console.error("Error loading messages: Invalid format in file.");
|
|
161
|
-
}
|
|
162
|
-
} catch (error) {
|
|
163
|
-
if (error instanceof Error) {
|
|
164
|
-
console.error(`Error loading messages: ${error.message}`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
179
|
// Pause signal: set by Ctrl-C during agent execution, checked after each tool batch completes
|
|
170
180
|
let paused = false;
|
|
171
181
|
/** @type {import("./agentLoop.mjs").PauseSignal} */
|
|
@@ -193,12 +203,14 @@ export function createAgent({
|
|
|
193
203
|
userEventEmitter,
|
|
194
204
|
agentEventEmitter,
|
|
195
205
|
agentCommands: {
|
|
196
|
-
dumpMessages,
|
|
197
|
-
loadMessages,
|
|
198
206
|
getCostSummary: () => costTracker.calculateCost(),
|
|
199
207
|
pauseAutoApprove: () => {
|
|
200
208
|
paused = true;
|
|
201
209
|
},
|
|
210
|
+
getActiveSubagent: () => subagentManager.getActiveSubagent(),
|
|
211
|
+
flushSessionPersistence: async () => {
|
|
212
|
+
await persistChain;
|
|
213
|
+
},
|
|
202
214
|
},
|
|
203
215
|
};
|
|
204
216
|
}
|
package/src/cliArgs.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @typedef {HelpSubcommand | InteractiveSubcommand | BatchSubcommand | ListModelsSubcommand | InstallClaudeCodePluginsSubcommand | CostSubcommand} Subcommand
|
|
2
|
+
* @typedef {HelpSubcommand | InteractiveSubcommand | BatchSubcommand | ListModelsSubcommand | InstallClaudeCodePluginsSubcommand | CostSubcommand | ResumeSubcommand} Subcommand
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -26,6 +26,13 @@
|
|
|
26
26
|
* @typedef {{ type: 'cost', from: string | null, to: string | null }} CostSubcommand
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Resume a previously interrupted interactive session.
|
|
31
|
+
* - `sessionId === null` and `list === false`: resume the most recently updated session.
|
|
32
|
+
* - `list === true`: print the resumable sessions and exit.
|
|
33
|
+
* @typedef {{ type: 'resume', sessionId: string | null, list: boolean, config: string[] }} ResumeSubcommand
|
|
34
|
+
*/
|
|
35
|
+
|
|
29
36
|
/**
|
|
30
37
|
* @typedef {Object} CliArgs
|
|
31
38
|
* @property {Subcommand} subcommand - The subcommand to execute
|
|
@@ -110,6 +117,38 @@ export function parseCliArgs(argv) {
|
|
|
110
117
|
};
|
|
111
118
|
}
|
|
112
119
|
|
|
120
|
+
if (subcommandName === "resume") {
|
|
121
|
+
const resumeArgs = args.slice(1);
|
|
122
|
+
/** @type {string | null} */
|
|
123
|
+
let sessionId = null;
|
|
124
|
+
let list = false;
|
|
125
|
+
/** @type {string[]} */
|
|
126
|
+
const config = [];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < resumeArgs.length; i++) {
|
|
129
|
+
const arg = resumeArgs[i];
|
|
130
|
+
if (arg === "--list") {
|
|
131
|
+
list = true;
|
|
132
|
+
} else if (arg === "-c" || arg === "--config") {
|
|
133
|
+
if (resumeArgs[i + 1]) {
|
|
134
|
+
config.push(resumeArgs[i + 1]);
|
|
135
|
+
i++;
|
|
136
|
+
}
|
|
137
|
+
} else if (arg === "-m" || arg === "--model") {
|
|
138
|
+
// Switching models on resume is not supported by design.
|
|
139
|
+
return {
|
|
140
|
+
subcommand: { type: "help" },
|
|
141
|
+
};
|
|
142
|
+
} else if (!arg.startsWith("-") && sessionId === null) {
|
|
143
|
+
sessionId = arg;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
subcommand: { type: "resume", sessionId, list, config },
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
113
152
|
if (subcommandName === "cost") {
|
|
114
153
|
const costArgs = args.slice(1);
|
|
115
154
|
let from = null;
|
|
@@ -145,6 +184,7 @@ export function printHelp(exitCode = 0) {
|
|
|
145
184
|
console.log(`
|
|
146
185
|
Usage: plain [options]
|
|
147
186
|
plain batch [options] <task>
|
|
187
|
+
plain resume [<sessionId>] [--list] [-c <file>]
|
|
148
188
|
plain cost [--from YYYY-MM-DD] [--to YYYY-MM-DD]
|
|
149
189
|
plain list-models
|
|
150
190
|
plain install-claude-code-plugins
|
|
@@ -158,6 +198,11 @@ Subcommands:
|
|
|
158
198
|
batch <task> Run in batch mode with the given task instruction.
|
|
159
199
|
Config files are NOT auto-loaded in batch mode;
|
|
160
200
|
use -c to specify config files explicitly.
|
|
201
|
+
resume Resume an interactive session that was
|
|
202
|
+
interrupted. With no sessionId, resumes the
|
|
203
|
+
most recently updated session. Use --list to
|
|
204
|
+
see resumable sessions. Switching models is
|
|
205
|
+
not supported (-m is rejected).
|
|
161
206
|
cost Show aggregated token cost per day for a period.
|
|
162
207
|
Defaults to the first day of the current month
|
|
163
208
|
through today.
|
package/src/cliBatch.mjs
CHANGED
package/src/cliCommands.mjs
CHANGED
|
@@ -131,18 +131,6 @@ export function createCommandHandler({
|
|
|
131
131
|
return "continue";
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
// /dump
|
|
135
|
-
if (inputTrimmed.toLowerCase() === "/dump") {
|
|
136
|
-
await agentCommands.dumpMessages();
|
|
137
|
-
return "prompt";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// /load
|
|
141
|
-
if (inputTrimmed.toLowerCase() === "/load") {
|
|
142
|
-
await agentCommands.loadMessages();
|
|
143
|
-
return "prompt";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
134
|
// /cost
|
|
147
135
|
if (inputTrimmed.toLowerCase() === "/cost") {
|
|
148
136
|
const summary = agentCommands.getCostSummary();
|
package/src/cliCompleter.mjs
CHANGED
|
@@ -32,8 +32,6 @@ export const SLASH_COMMANDS = [
|
|
|
32
32
|
name: "/resume",
|
|
33
33
|
description: "Resume conversation after an LLM provider error",
|
|
34
34
|
},
|
|
35
|
-
{ name: "/dump", description: "Save current messages to a JSON file" },
|
|
36
|
-
{ name: "/load", description: "Load messages from a JSON file" },
|
|
37
35
|
{ name: "/cost", description: "Display session cost and token usage" },
|
|
38
36
|
{
|
|
39
37
|
name: "/compact",
|
package/src/cliCost.mjs
CHANGED
|
@@ -87,6 +87,24 @@ export function parseDateOnly(value) {
|
|
|
87
87
|
return date;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Deduplicate usage records by sessionId, keeping only the last record for each session.
|
|
92
|
+
* This prevents double-counting when a session is resumed and exited multiple times.
|
|
93
|
+
*
|
|
94
|
+
* @param {UsageRecord[]} records - Records in chronological order (oldest first)
|
|
95
|
+
* @returns {UsageRecord[]} Deduplicated records (one per sessionId)
|
|
96
|
+
*/
|
|
97
|
+
function deduplicateBySessionId(records) {
|
|
98
|
+
/** @type {Map<string, UsageRecord>} */
|
|
99
|
+
const bySessionId = new Map();
|
|
100
|
+
for (const record of records) {
|
|
101
|
+
if (record.sessionId) {
|
|
102
|
+
bySessionId.set(record.sessionId, record);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return Array.from(bySessionId.values());
|
|
106
|
+
}
|
|
107
|
+
|
|
90
108
|
/**
|
|
91
109
|
* Aggregate usage records into a cost report.
|
|
92
110
|
*
|
|
@@ -103,12 +121,14 @@ export function aggregateUsage(records, period) {
|
|
|
103
121
|
);
|
|
104
122
|
}
|
|
105
123
|
|
|
124
|
+
const deduplicated = deduplicateBySessionId(records);
|
|
125
|
+
|
|
106
126
|
/** @type {Map<string, Map<string, DailyEntry>>} */
|
|
107
127
|
const byCurrency = new Map();
|
|
108
128
|
let noPricingSessionCount = 0;
|
|
109
129
|
let excludedOutOfRange = 0;
|
|
110
130
|
|
|
111
|
-
for (const record of
|
|
131
|
+
for (const record of deduplicated) {
|
|
112
132
|
if (record.timestamp == null) {
|
|
113
133
|
excludedOutOfRange++;
|
|
114
134
|
continue;
|
package/src/cliFormatter.mjs
CHANGED
|
@@ -152,20 +152,27 @@ export async function formatToolUse(toolUse) {
|
|
|
152
152
|
].join("\n");
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
if (toolName === "
|
|
156
|
-
/** @type {Partial<import("./tools/
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
"
|
|
160
|
-
|
|
155
|
+
if (toolName === "web_search") {
|
|
156
|
+
/** @type {Partial<import("./tools/webSearch.mjs").WebSearchInput>} */
|
|
157
|
+
const webSearchInput = input;
|
|
158
|
+
const searchesLine = webSearchInput.searches
|
|
159
|
+
? webSearchInput.searches.map((s) => s.keywords.join(" ")).join(" | ")
|
|
160
|
+
: "";
|
|
161
|
+
return [
|
|
162
|
+
`tool: ${toolName}`,
|
|
163
|
+
`searches: ${searchesLine}`,
|
|
164
|
+
`question: ${webSearchInput.question}`,
|
|
165
|
+
].join("\n");
|
|
161
166
|
}
|
|
162
167
|
|
|
163
|
-
if (toolName === "
|
|
164
|
-
/** @type {Partial<import("./tools/
|
|
165
|
-
const
|
|
166
|
-
return [
|
|
167
|
-
|
|
168
|
-
|
|
168
|
+
if (toolName === "web_fetch") {
|
|
169
|
+
/** @type {Partial<import("./tools/webFetch.mjs").WebFetchInput>} */
|
|
170
|
+
const webFetchInput = input;
|
|
171
|
+
return [
|
|
172
|
+
`tool: ${toolName}`,
|
|
173
|
+
`url: ${webFetchInput.url}`,
|
|
174
|
+
`question: ${webFetchInput.question}`,
|
|
175
|
+
].join("\n");
|
|
169
176
|
}
|
|
170
177
|
|
|
171
178
|
const { provider: _, ...filteredToolUse } = toolUse;
|
package/src/cliInteractive.mjs
CHANGED
|
@@ -112,7 +112,7 @@ export function startInteractiveSession({
|
|
|
112
112
|
const state = {
|
|
113
113
|
turn: true,
|
|
114
114
|
multiLineBuffer: null,
|
|
115
|
-
subagentName: "",
|
|
115
|
+
subagentName: agentCommands.getActiveSubagent()?.name ?? "",
|
|
116
116
|
};
|
|
117
117
|
|
|
118
118
|
/**
|
|
@@ -157,6 +157,7 @@ export function startInteractiveSession({
|
|
|
157
157
|
console.log();
|
|
158
158
|
console.log(formatCostSummary(summary));
|
|
159
159
|
await persistUsage(summary, { sessionId, modelName, startTime });
|
|
160
|
+
await agentCommands.flushSessionPersistence();
|
|
160
161
|
await onStop();
|
|
161
162
|
process.exit(0);
|
|
162
163
|
};
|
|
@@ -351,7 +352,7 @@ export function startInteractiveSession({
|
|
|
351
352
|
process.stdout.write("\x1b[?2004h");
|
|
352
353
|
}
|
|
353
354
|
|
|
354
|
-
let currentCliPrompt = getCliPrompt();
|
|
355
|
+
let currentCliPrompt = getCliPrompt(state.subagentName);
|
|
355
356
|
cli = readline.createInterface({
|
|
356
357
|
input: paste.transform,
|
|
357
358
|
output: process.stdout,
|
package/src/config.d.ts
CHANGED
|
@@ -1,11 +1,71 @@
|
|
|
1
1
|
import { ClaudeCodePluginRepo } from "./claudeCodePlugin.mjs";
|
|
2
2
|
import { ModelDefinition, PlatformConfig } from "./modelDefinition";
|
|
3
3
|
import { ToolUsePattern } from "./tool";
|
|
4
|
-
import { AskURLToolOptions } from "./tools/askURL.mjs";
|
|
5
|
-
import { AskWebToolOptions } from "./tools/askWeb.mjs";
|
|
6
4
|
import { ExecCommandSanboxConfig } from "./tools/execCommand";
|
|
5
|
+
import {
|
|
6
|
+
WebFetchToolGeminiOptions,
|
|
7
|
+
WebFetchToolGeminiVertexAIOptions,
|
|
8
|
+
} from "./tools/webFetch.mjs";
|
|
9
|
+
import {
|
|
10
|
+
WebSearchToolGeminiOptions,
|
|
11
|
+
WebSearchToolGeminiVertexAIOptions,
|
|
12
|
+
} from "./tools/webSearch.mjs";
|
|
7
13
|
import { VoiceInputConfig } from "./voiceInput.mjs";
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* JSON-serializable webFetch configuration.
|
|
17
|
+
*
|
|
18
|
+
* The `command` provider runs an arbitrary local command per fetch to
|
|
19
|
+
* download a URL's content; the agent's main model is then used to answer
|
|
20
|
+
* based on the dumped output. The runtime tool factory receives a resolved
|
|
21
|
+
* `modelCaller` instead — see `WebFetchToolOptions` in `tools/webFetch.mjs`.
|
|
22
|
+
*/
|
|
23
|
+
export type WebFetchToolConfig =
|
|
24
|
+
| WebFetchToolGeminiOptions
|
|
25
|
+
| WebFetchToolGeminiVertexAIOptions
|
|
26
|
+
| WebFetchToolCommandJsonConfig;
|
|
27
|
+
|
|
28
|
+
export type WebFetchToolCommandJsonConfig = {
|
|
29
|
+
provider: "command";
|
|
30
|
+
/** Executable used to fetch the URL (e.g., `"w3m"`, `"curl"`). */
|
|
31
|
+
command: string;
|
|
32
|
+
/** Arguments passed before the URL (e.g., `["-dump"]`). The URL is appended automatically. */
|
|
33
|
+
args: string[];
|
|
34
|
+
/** Per-call timeout in milliseconds (default 30000). */
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/** Extra environment variables, merged on top of PATH / HOME / LANG. */
|
|
37
|
+
env?: Record<string, string>;
|
|
38
|
+
maxLength?: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* JSON-serializable webSearch configuration.
|
|
43
|
+
*
|
|
44
|
+
* The `command` provider runs an arbitrary local command per keyword set
|
|
45
|
+
* to perform a search; the agent's main model is then used to filter the
|
|
46
|
+
* combined results down to entries relevant to the question. The runtime
|
|
47
|
+
* tool factory receives a resolved `modelCaller` instead — see
|
|
48
|
+
* `WebSearchToolOptions` in `tools/webSearch.mjs`.
|
|
49
|
+
*/
|
|
50
|
+
export type WebSearchToolConfig =
|
|
51
|
+
| WebSearchToolGeminiOptions
|
|
52
|
+
| WebSearchToolGeminiVertexAIOptions
|
|
53
|
+
| WebSearchToolCommandJsonConfig;
|
|
54
|
+
|
|
55
|
+
export type WebSearchToolCommandJsonConfig = {
|
|
56
|
+
provider: "command";
|
|
57
|
+
/** Executable used to perform each search (e.g., a wrapper around a search API). */
|
|
58
|
+
command: string;
|
|
59
|
+
/** Arguments passed before each keyword set (e.g., `["-n", "5"]`). Keywords are appended automatically. */
|
|
60
|
+
args: string[];
|
|
61
|
+
/** Per-search timeout in milliseconds (default 30000). */
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
/** Extra environment variables, merged on top of PATH / HOME / LANG. */
|
|
64
|
+
env?: Record<string, string>;
|
|
65
|
+
maxLengthPerSearch?: number;
|
|
66
|
+
maxTotalLength?: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
9
69
|
export type AppConfig = {
|
|
10
70
|
model?: string;
|
|
11
71
|
models?: ModelDefinition[];
|
|
@@ -17,8 +77,8 @@ export type AppConfig = {
|
|
|
17
77
|
};
|
|
18
78
|
sandbox?: ExecCommandSanboxConfig;
|
|
19
79
|
tools?: {
|
|
20
|
-
|
|
21
|
-
|
|
80
|
+
webSearch?: WebSearchToolConfig;
|
|
81
|
+
webFetch?: WebFetchToolConfig;
|
|
22
82
|
};
|
|
23
83
|
mcpServers?: Record<string, MCPServerConfig>;
|
|
24
84
|
notifyCmd?: { command: string; args?: string[] };
|
package/src/config.mjs
CHANGED
|
@@ -76,18 +76,18 @@ export async function loadAppConfig(options = {}) {
|
|
|
76
76
|
},
|
|
77
77
|
sandbox: config.sandbox ?? merged.sandbox,
|
|
78
78
|
tools: {
|
|
79
|
-
|
|
79
|
+
webSearch: config.tools?.webSearch
|
|
80
80
|
? {
|
|
81
|
-
...(merged.tools?.
|
|
82
|
-
...config.tools.
|
|
81
|
+
...(merged.tools?.webSearch ?? {}),
|
|
82
|
+
...config.tools.webSearch,
|
|
83
83
|
}
|
|
84
|
-
: merged.tools?.
|
|
85
|
-
|
|
84
|
+
: merged.tools?.webSearch,
|
|
85
|
+
webFetch: config.tools?.webFetch
|
|
86
86
|
? {
|
|
87
|
-
...(merged.tools?.
|
|
88
|
-
...config.tools.
|
|
87
|
+
...(merged.tools?.webFetch ?? {}),
|
|
88
|
+
...config.tools.webFetch,
|
|
89
89
|
}
|
|
90
|
-
: merged.tools?.
|
|
90
|
+
: merged.tools?.webFetch,
|
|
91
91
|
},
|
|
92
92
|
mcpServers: {
|
|
93
93
|
...(merged.mcpServers ?? {}),
|