wave-agent-sdk 1.1.4 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +72 -24
- package/dist/agent.js +123 -31
- package/dist/builtin/plugins.js +17 -2
- package/dist/builtin/skills/settings.js +6 -8
- package/dist/core/plugin.d.ts +4 -0
- package/dist/core/plugin.js +7 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/managers/aiManager.d.ts +0 -24
- package/dist/managers/aiManager.js +23 -181
- package/dist/managers/bashModeManager.d.ts +33 -0
- package/dist/managers/bashModeManager.js +110 -0
- package/dist/managers/hookManager.d.ts +5 -0
- package/dist/managers/hookManager.js +7 -0
- package/dist/managers/liveConfigManager.js +3 -3
- package/dist/managers/mcpManager.d.ts +23 -0
- package/dist/managers/mcpManager.js +162 -14
- package/dist/managers/messageManager.d.ts +12 -13
- package/dist/managers/messageManager.js +63 -60
- package/dist/managers/permissionManager.d.ts +29 -0
- package/dist/managers/permissionManager.js +158 -70
- package/dist/managers/planManager.d.ts +9 -0
- package/dist/managers/planManager.js +19 -1
- package/dist/managers/skillManager.d.ts +31 -0
- package/dist/managers/skillManager.js +122 -12
- package/dist/managers/slashCommandManager.js +9 -28
- package/dist/managers/subagentManager.d.ts +7 -0
- package/dist/managers/subagentManager.js +61 -9
- package/dist/managers/workflowManager.js +6 -0
- package/dist/prompts/index.d.ts +0 -1
- package/dist/prompts/index.js +0 -4
- package/dist/services/MarketplaceService.js +36 -14
- package/dist/services/configurationService.d.ts +34 -2
- package/dist/services/configurationService.js +123 -16
- package/dist/services/initializationService.js +2 -2
- package/dist/services/jsonlHandler.d.ts +14 -0
- package/dist/services/jsonlHandler.js +44 -1
- package/dist/services/memory.d.ts +14 -0
- package/dist/services/memory.js +33 -0
- package/dist/services/officialMarketplaceMirror.d.ts +85 -0
- package/dist/services/officialMarketplaceMirror.js +289 -0
- package/dist/services/remoteSettingsService.js +4 -4
- package/dist/services/session.js +30 -13
- package/dist/services/worktreeHooks.js +6 -1
- package/dist/stdio/index.d.ts +10 -0
- package/dist/stdio/index.js +10 -0
- package/dist/stdio/notificationRouter.d.ts +38 -0
- package/dist/stdio/notificationRouter.js +96 -0
- package/dist/stdio/rpcClient.d.ts +18 -0
- package/dist/stdio/rpcClient.js +10 -0
- package/dist/stdio/stdioAgent.d.ts +222 -0
- package/dist/stdio/stdioAgent.js +341 -0
- package/dist/tools/bashTool.js +2 -0
- package/dist/tools/exitPlanMode.js +10 -2
- package/dist/types/agent.d.ts +8 -0
- package/dist/types/commands.d.ts +7 -0
- package/dist/types/configuration.d.ts +6 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.js +19 -0
- package/dist/types/mcp.d.ts +3 -0
- package/dist/types/messaging.d.ts +1 -8
- package/dist/types/skills.d.ts +11 -0
- package/dist/utils/bashParser.d.ts +17 -0
- package/dist/utils/bashParser.js +72 -0
- package/dist/utils/bashStructure/bashLexer.d.ts +96 -0
- package/dist/utils/bashStructure/bashLexer.js +676 -0
- package/dist/utils/bashStructure/bashParser.d.ts +144 -0
- package/dist/utils/bashStructure/bashParser.js +606 -0
- package/dist/utils/bashStructure/bashSemantics.d.ts +70 -0
- package/dist/utils/bashStructure/bashSemantics.js +477 -0
- package/dist/utils/bashStructure/index.d.ts +26 -0
- package/dist/utils/bashStructure/index.js +27 -0
- package/dist/utils/bashStructure/types.d.ts +62 -0
- package/dist/utils/bashStructure/types.js +47 -0
- package/dist/utils/container.d.ts +6 -0
- package/dist/utils/container.js +9 -0
- package/dist/utils/containerSetup.d.ts +11 -1
- package/dist/utils/containerSetup.js +26 -11
- package/dist/utils/fileUtils.d.ts +11 -0
- package/dist/utils/fileUtils.js +37 -0
- package/dist/utils/messageOperations.d.ts +0 -18
- package/dist/utils/messageOperations.js +0 -62
- package/dist/utils/subagentParser.js +9 -2
- package/dist/utils/tokenCalculation.js +0 -8
- package/dist/utils/worktreeUtils.d.ts +2 -1
- package/dist/utils/worktreeUtils.js +64 -34
- package/package.json +6 -1
- package/dist/managers/bangManager.d.ts +0 -26
- package/dist/managers/bangManager.js +0 -78
|
@@ -14,34 +14,6 @@ export class SlashCommandManager {
|
|
|
14
14
|
}
|
|
15
15
|
initialize() {
|
|
16
16
|
this.loadCustomCommands();
|
|
17
|
-
// Builtin /subtask: manual fork subagent (aligned with Claude Code's
|
|
18
|
-
// /subtask — inherits the parent's full context, runs in the background,
|
|
19
|
-
// result returns to the main conversation via a task notification).
|
|
20
|
-
this.registerCommand({
|
|
21
|
-
id: "subtask",
|
|
22
|
-
name: "subtask",
|
|
23
|
-
description: "Start a fork subagent with the current conversation context in the background",
|
|
24
|
-
handler: async (args, signal) => {
|
|
25
|
-
const taskDescription = args?.trim();
|
|
26
|
-
if (!taskDescription) {
|
|
27
|
-
this.messageManager.addErrorBlock("Usage: /subtask <task description>");
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
// The fork snapshots its context synchronously on entry, so the
|
|
32
|
-
// transcript echo below never duplicates the prompt inside the fork.
|
|
33
|
-
await this.aiManager.runForkSubagent(taskDescription, { description: taskDescription }, signal);
|
|
34
|
-
this.messageManager.addUserMessage({
|
|
35
|
-
content: `/subtask ${taskDescription}`,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
this.aiManager.setIsLoading(false);
|
|
40
|
-
logger?.error("Failed to execute /subtask:", error);
|
|
41
|
-
this.messageManager.addErrorBlock(`Failed to execute /subtask: ${error instanceof Error ? error.message : String(error)}`);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
17
|
// Listen for skill refreshes and update skill commands
|
|
46
18
|
const skillManager = this.container.get("SkillManager");
|
|
47
19
|
if (skillManager) {
|
|
@@ -103,6 +75,13 @@ export class SlashCommandManager {
|
|
|
103
75
|
this.unregisterCommand(commandId);
|
|
104
76
|
}
|
|
105
77
|
this.skillCommandIds.clear();
|
|
78
|
+
const skillSourceFor = (skill) => {
|
|
79
|
+
if (skill.pluginName)
|
|
80
|
+
return "plugin";
|
|
81
|
+
if (skill.type === "personal")
|
|
82
|
+
return "user";
|
|
83
|
+
return skill.type; // "project" | "builtin"
|
|
84
|
+
};
|
|
106
85
|
for (const skill of skills) {
|
|
107
86
|
if (skill.userInvocable === false) {
|
|
108
87
|
continue;
|
|
@@ -113,6 +92,8 @@ export class SlashCommandManager {
|
|
|
113
92
|
id: commandId,
|
|
114
93
|
name: skill.name,
|
|
115
94
|
description: `Skill: ${skill.description}`,
|
|
95
|
+
// UI source tag: plugin skills carry pluginName; personal → 用户
|
|
96
|
+
skillSource: skillSourceFor(skill),
|
|
116
97
|
handler: async (args, signal) => {
|
|
117
98
|
try {
|
|
118
99
|
// 1. Prepare skill content immediately
|
|
@@ -100,6 +100,13 @@ export declare class SubagentManager {
|
|
|
100
100
|
* for conditional subagent registration (e.g. WAVE_VISION_MODEL).
|
|
101
101
|
*/
|
|
102
102
|
private getMergedEnv;
|
|
103
|
+
/**
|
|
104
|
+
* Delete a user-level or project-level subagent by removing its markdown
|
|
105
|
+
* file. Builtin and plugin agents are read-only and cannot be deleted.
|
|
106
|
+
* @param name - The subagent name as shown in configurations
|
|
107
|
+
* @returns true if the subagent was deleted
|
|
108
|
+
*/
|
|
109
|
+
deleteSubagent(name: string): Promise<boolean>;
|
|
103
110
|
/**
|
|
104
111
|
* Get cached configurations synchronously (must call loadConfigurations first)
|
|
105
112
|
*/
|
|
@@ -107,6 +107,31 @@ export class SubagentManager {
|
|
|
107
107
|
return (this.configurationService?.getMergedEnv?.() ??
|
|
108
108
|
process.env);
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Delete a user-level or project-level subagent by removing its markdown
|
|
112
|
+
* file. Builtin and plugin agents are read-only and cannot be deleted.
|
|
113
|
+
* @param name - The subagent name as shown in configurations
|
|
114
|
+
* @returns true if the subagent was deleted
|
|
115
|
+
*/
|
|
116
|
+
async deleteSubagent(name) {
|
|
117
|
+
const configs = this.cachedConfigurations ?? [];
|
|
118
|
+
const config = configs.find((c) => c.name === name);
|
|
119
|
+
if (!config) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
// Builtin and plugin agents are read-only
|
|
123
|
+
if (config.scope !== "user" && config.scope !== "project") {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const { unlink } = await import("fs/promises");
|
|
127
|
+
await unlink(config.filePath);
|
|
128
|
+
// Drop the deleted agent from the cached configurations immediately.
|
|
129
|
+
// A later refreshConfigurations() re-scans the filesystem and reflects
|
|
130
|
+
// the deletion persistently.
|
|
131
|
+
this.cachedConfigurations = configs.filter((c) => c.name !== name);
|
|
132
|
+
logger?.debug(`Deleted subagent '${name}' at ${config.filePath}`);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
110
135
|
/**
|
|
111
136
|
* Get cached configurations synchronously (must call loadConfigurations first)
|
|
112
137
|
*/
|
|
@@ -339,8 +364,13 @@ export class SubagentManager {
|
|
|
339
364
|
const result = await this.internalExecute(instance, prompt, abortSignal);
|
|
340
365
|
const task = backgroundTaskManager?.getTask(taskId);
|
|
341
366
|
if (task) {
|
|
342
|
-
|
|
343
|
-
task
|
|
367
|
+
// internalExecute already recorded the terminal state; only fill
|
|
368
|
+
// in a "completed" status when the task was not concurrently
|
|
369
|
+
// killed (mirrors the catch branch below).
|
|
370
|
+
if (task.status !== "killed") {
|
|
371
|
+
task.status = "completed";
|
|
372
|
+
task.stdout = result;
|
|
373
|
+
}
|
|
344
374
|
task.endTime = Date.now();
|
|
345
375
|
task.runtime = task.endTime - startTime;
|
|
346
376
|
}
|
|
@@ -348,9 +378,13 @@ export class SubagentManager {
|
|
|
348
378
|
catch (error) {
|
|
349
379
|
const task = backgroundTaskManager?.getTask(taskId);
|
|
350
380
|
if (task) {
|
|
351
|
-
|
|
352
|
-
task.
|
|
353
|
-
|
|
381
|
+
// Preserve a "killed" terminal state (internalExecute already
|
|
382
|
+
// notified it) instead of flipping the in-map task to failed.
|
|
383
|
+
if (task.status !== "killed") {
|
|
384
|
+
task.status = "failed";
|
|
385
|
+
task.stderr =
|
|
386
|
+
error instanceof Error ? error.message : String(error);
|
|
387
|
+
}
|
|
354
388
|
task.endTime = Date.now();
|
|
355
389
|
task.runtime = task.endTime - startTime;
|
|
356
390
|
}
|
|
@@ -480,8 +514,13 @@ export class SubagentManager {
|
|
|
480
514
|
const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
|
|
481
515
|
if (task) {
|
|
482
516
|
const wasAlreadyKilled = task.status === "killed";
|
|
483
|
-
|
|
484
|
-
|
|
517
|
+
// Preserve a "killed" terminal state (stopTask already notified it)
|
|
518
|
+
// instead of overwriting it with "completed" — mirrors shell exit
|
|
519
|
+
// semantics in BackgroundTaskManager.onExit.
|
|
520
|
+
if (!wasAlreadyKilled) {
|
|
521
|
+
task.status = "completed";
|
|
522
|
+
task.stdout = response || "Agent completed with no text response";
|
|
523
|
+
}
|
|
485
524
|
task.endTime = Date.now();
|
|
486
525
|
if (task.startTime) {
|
|
487
526
|
task.runtime = task.endTime - task.startTime;
|
|
@@ -496,6 +535,11 @@ export class SubagentManager {
|
|
|
496
535
|
messageQueue.enqueueNotification(`<task-notification>\n<task-id>${instance.backgroundTaskId}</task-id>\n<task-type>agent</task-type>\n<status>completed</status>\n<summary>${summary}</summary>\n</task-notification>`);
|
|
497
536
|
}
|
|
498
537
|
}
|
|
538
|
+
// Push a terminal snapshot: SubagentManager mutates the task object in
|
|
539
|
+
// place, so without an explicit notify the UI/hosts keep the last
|
|
540
|
+
// snapshot — the task created with status "running" — and the IDE
|
|
541
|
+
// background-task gate (new chat / history restore) stays locked.
|
|
542
|
+
backgroundTaskManager.notifyTasksChange();
|
|
499
543
|
}
|
|
500
544
|
}
|
|
501
545
|
return response || "Agent completed with no text response";
|
|
@@ -512,12 +556,20 @@ export class SubagentManager {
|
|
|
512
556
|
const task = backgroundTaskManager.getTask(instance.backgroundTaskId);
|
|
513
557
|
if (task) {
|
|
514
558
|
const wasAlreadyKilled = task.status === "killed";
|
|
515
|
-
|
|
516
|
-
|
|
559
|
+
// Preserve a user/agent-initiated "killed" terminal state instead of
|
|
560
|
+
// overwriting it with "failed" (mirrors shell exit semantics) — the
|
|
561
|
+
// abort that stopped the task surfaces here as a thrown error.
|
|
562
|
+
if (!wasAlreadyKilled) {
|
|
563
|
+
task.status = "failed";
|
|
564
|
+
task.stderr =
|
|
565
|
+
error instanceof Error ? error.message : String(error);
|
|
566
|
+
}
|
|
517
567
|
task.endTime = Date.now();
|
|
518
568
|
if (task.startTime) {
|
|
519
569
|
task.runtime = task.endTime - task.startTime;
|
|
520
570
|
}
|
|
571
|
+
// Push a terminal snapshot (see the completion branch above).
|
|
572
|
+
backgroundTaskManager.notifyTasksChange();
|
|
521
573
|
// Skip notification if task was already stopped (e.g. by main agent shutdown)
|
|
522
574
|
if (!wasAlreadyKilled) {
|
|
523
575
|
const messageQueue = this.container.has("MessageQueue")
|
|
@@ -207,6 +207,10 @@ export class WorkflowManager {
|
|
|
207
207
|
task.status = "completed";
|
|
208
208
|
task.endTime = Date.now();
|
|
209
209
|
}
|
|
210
|
+
// Push the terminal snapshot: without it the last snapshot holds the
|
|
211
|
+
// "running" registration and UI consumers (background-task gate) never
|
|
212
|
+
// see the workflow finish.
|
|
213
|
+
this.backgroundTaskManager.notifyTasksChange();
|
|
210
214
|
// Enqueue completion notification
|
|
211
215
|
const journalPath = journal.filePath;
|
|
212
216
|
this.messageQueue.enqueueNotification(taskNotificationToXml({
|
|
@@ -244,6 +248,8 @@ export class WorkflowManager {
|
|
|
244
248
|
task.stderr = run.error || "";
|
|
245
249
|
task.endTime = Date.now();
|
|
246
250
|
}
|
|
251
|
+
// Push the terminal snapshot (see the completion branch above).
|
|
252
|
+
this.backgroundTaskManager.notifyTasksChange();
|
|
247
253
|
this.messageQueue.enqueueNotification(taskNotificationToXml({
|
|
248
254
|
type: "task_notification",
|
|
249
255
|
taskId: runId,
|
package/dist/prompts/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ToolPlugin } from "../tools/types.js";
|
|
2
2
|
import type { WorktreeSession } from "../utils/worktreeSession.js";
|
|
3
3
|
export declare const BASE_SYSTEM_PROMPT = "You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.";
|
|
4
|
-
export declare const MODEL_IDENTITY_PROMPT = "# Model identity\n\nIf asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.";
|
|
5
4
|
export declare const DOING_TASKS_PROMPT = "# Doing tasks\n- The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change \"methodName\" to snake case, do not reply with just \"method_name\", instead find the method in the code and modify the code.\n- You are highly capable and often allow users to complete ambitious tasks that would otherwise be too complex or take too long. You should defer to user judgement about whether a task is too large to attempt.\n- If you notice the user's request is based on a misconception, or spot a bug adjacent to what they asked about, say so. You're a collaborator, not just an executor\u2014users benefit from your judgment, not just your compliance.\n- In general, do not propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications.\n- Do not create files unless they're absolutely necessary for achieving your goal. Generally prefer editing an existing file to creating a new one, as this prevents file bloat and builds on existing work more effectively.\n- If an approach fails, diagnose why before switching tactics\u2014read the error, check your assumptions, try a focused fix. Don't retry the identical action blindly, but don't abandon a viable approach after a single failure either. Escalate to the user with AskUserQuestion only when you're genuinely stuck after investigation, not as a first response to friction.\n- Be careful not to introduce security vulnerabilities such as command injection, XSS, SQL injection, and other OWASP top 10 vulnerabilities. If you notice that you wrote insecure code, immediately fix it. Prioritize writing safe, secure, and correct code.\n- Avoid over-engineering. Only make changes that are directly requested or clearly necessary. Keep solutions simple and focused.\n - Don't add features, refactor code, or make \"improvements\" beyond what was asked. A bug fix doesn't need surrounding code cleaned up. A simple feature doesn't need extra configurability. Don't add docstrings, comments, or type annotations to code you didn't change. Only add comments where the logic isn't self-evident.\n - Don't add error handling, fallbacks, or validation for scenarios that can't happen. Trust internal code and framework guarantees. Only validate at system boundaries (user input, external APIs). Don't use feature flags or backwards-compatibility shims when you can just change the code.\n - Don't create helpers, utilities, or abstractions for one-time operations. Don't design for hypothetical future requirements. The right amount of complexity is what the task actually requires\u2014no speculative abstractions, but no half-finished implementations either. Three similar lines of code is better than a premature abstraction.\n- Avoid backwards-compatibility hacks like renaming unused _vars, re-exporting types, adding // removed comments for removed code, etc. If you are certain that something is unused, you can delete it completely.\n- Report outcomes faithfully: if tests fail, say so with the relevant output; if you did not run a verification step, say that rather than implying it succeeded. Never claim \"all tests pass\" when output shows failures, never suppress or simplify failing checks (tests, lints, type errors) to manufacture a green result, and never characterize incomplete or broken work as done. Equally, when a check did pass or a task is complete, state it plainly \u2014 do not hedge confirmed results with unnecessary disclaimers, downgrade finished work to \"partial,\" or re-verify things you already checked. The goal is an accurate report, not a defensive one.\n- Before reporting a task complete, verify it actually works: run the test, execute the script, check the output. Minimum complexity means no gold-plating, not skipping the finish line. If you can't verify (no test exists, can't run the code), say so explicitly rather than claiming success.";
|
|
6
5
|
export declare const EXECUTING_ACTIONS_PROMPT = "# Executing actions with care\n\nCarefully consider the reversibility and blast radius of actions. Generally you can freely take local, reversible actions like editing files or running tests. But for actions that are hard to reverse, affect shared systems beyond your local environment, or could otherwise be risky or destructive, check with the user before proceeding. The cost of pausing to confirm is low, while the cost of an unwanted action (lost work, unintended messages sent, deleted branches) can be very high. For actions like these, consider the context, the action, and user instructions, and by default transparently communicate the action and ask for confirmation before proceeding.\n\nExamples of the kind of risky actions that warrant user confirmation:\n- Destructive operations: deleting files/branches, dropping database tables, killing processes, rm -rf, overwriting uncommitted changes\n- Hard-to-reverse operations: force-pushing (can also overwrite upstream), git reset --hard, amending published commits, removing or downgrading packages/dependencies, modifying CI/CD pipelines\n- Actions visible to others or that affect shared state: pushing code, creating/closing/commenting on PRs or issues, sending messages (Slack, email, GitHub), posting to external services\n\nWhen you encounter an obstacle, do not use destructive actions as a shortcut to simply make it go away. For instance, try to identify root causes and fix underlying issues rather than bypassing safety checks (e.g. --no-verify). If you discover unexpected state like unfamiliar files, branches, or configuration, investigate before deleting or overwriting, as it may represent the user's in-progress work. For example, typically resolve merge conflicts rather than discarding changes. In short: only take risky actions carefully, and when in doubt, ask before acting. Follow both the spirit and letter of these instructions - measure twice, cut once.";
|
|
7
6
|
export declare const TOOL_POLICY = "# Using your tools\n\n- Do NOT use the Bash to run commands when a relevant dedicated tool is provided. Using dedicated tools allows the user to better understand and review your work. This is CRITICAL to assisting the user:\n - To read files use Read instead of cat, head, tail, or sed\n - To edit files use Edit instead of sed or awk\n - To create files use Write instead of cat or echo redirection\n - To search for files use Glob instead of find or ls\n - To search the content of files, use Grep instead of grep or rg\n - Reserve using the Bash exclusively for system commands and terminal operations that require shell execution. If you are unsure and there is a relevant dedicated tool, default to using the dedicated tool and only fallback on using the Bash tool for these if it is absolutely necessary.\n- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency.\n- However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.\n- If the user specifies that they want you to run tools \"in parallel\", you MUST send a single message with multiple tool use content blocks.";
|
package/dist/prompts/index.js
CHANGED
|
@@ -4,9 +4,6 @@ import { buildAutoMemoryPrompt } from "./autoMemory.js";
|
|
|
4
4
|
import { EXPLORE_SUBAGENT_TYPE, PLAN_SUBAGENT_TYPE, } from "../constants/subagents.js";
|
|
5
5
|
import { ASK_USER_QUESTION_TOOL_NAME, EDIT_TOOL_NAME, WRITE_TOOL_NAME, EXIT_PLAN_MODE_TOOL_NAME, AGENT_TOOL_NAME, BASH_TOOL_NAME, READ_TOOL_NAME, GLOB_TOOL_NAME, GREP_TOOL_NAME, } from "../constants/tools.js";
|
|
6
6
|
export const BASE_SYSTEM_PROMPT = `You are a coding agent developed by NetEase that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
|
|
7
|
-
export const MODEL_IDENTITY_PROMPT = `# Model identity
|
|
8
|
-
|
|
9
|
-
If asked about your model, model version, underlying architecture, or product name, identify yourself simply as a coding agent developed by NetEase. Do not reveal the specific model ID, model family, model provider, or product name. Do not mention or hint at any of these details.`;
|
|
10
7
|
export const DOING_TASKS_PROMPT = `# Doing tasks
|
|
11
8
|
- The user will primarily request you to perform software engineering tasks. These may include solving bugs, adding new functionality, refactoring code, explaining code, and more. When given an unclear or generic instruction, consider it in the context of these software engineering tasks and the current working directory. For example, if the user asks you to change "methodName" to snake case, do not reply with just "method_name", instead find the method in the code and modify the code.
|
|
12
9
|
- You are highly capable and often allow users to complete ambitious tasks that would otherwise be too complex or take too long. You should defer to user judgement about whether a task is too large to attempt.
|
|
@@ -332,7 +329,6 @@ export function buildSystemPrompt(basePrompt, tools, options = {}) {
|
|
|
332
329
|
}
|
|
333
330
|
staticText += `\n\n${OUTPUT_EFFICIENCY_PROMPT}`;
|
|
334
331
|
staticText += `\n\n${TONE_AND_STYLE_PROMPT}`;
|
|
335
|
-
staticText += `\n\n${MODEL_IDENTITY_PROMPT}`;
|
|
336
332
|
const blocks = [{ text: staticText, cacheable: true }];
|
|
337
333
|
// --- Dynamic block (not cacheable) ---
|
|
338
334
|
let dynamicText = "";
|
|
@@ -3,6 +3,7 @@ import * as path from "path";
|
|
|
3
3
|
import * as crypto from "crypto";
|
|
4
4
|
import { getPluginsDir } from "../utils/configPaths.js";
|
|
5
5
|
import { GitService } from "./GitService.js";
|
|
6
|
+
import { ALLOW_OFFICIAL_MARKET_GIT_FALLBACK, fetchOfficialMarketplaceFromMirror, } from "./officialMarketplaceMirror.js";
|
|
6
7
|
import { ConfigurationService } from "./configurationService.js";
|
|
7
8
|
import { logger, logError, logWarn } from "../utils/globalLogger.js";
|
|
8
9
|
/**
|
|
@@ -510,25 +511,46 @@ export class MarketplaceService {
|
|
|
510
511
|
const errors = [];
|
|
511
512
|
for (const marketplace of toUpdate) {
|
|
512
513
|
try {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
514
|
+
// Builtin official marketplace: prefer the zip-snapshot mirror
|
|
515
|
+
// (content-addressed zip over a plain HTTP base, no git/GitHub
|
|
516
|
+
// needed). Only the builtin is special-cased by name — its
|
|
517
|
+
// `source` stays "github" in settings/cache, so there is zero
|
|
518
|
+
// data migration. On mirror failure (e.g. prod URL not yet live)
|
|
519
|
+
// fall back to the git path unless the kill switch forbids it.
|
|
520
|
+
const isOfficialBuiltin = marketplace.name === MarketplaceService.BUILTIN_MARKETPLACE.name;
|
|
521
|
+
let mirrorUpdated = false;
|
|
522
|
+
if (isOfficialBuiltin) {
|
|
519
523
|
const targetPath = this.getMarketplacePath(marketplace.source);
|
|
520
|
-
|
|
521
|
-
|
|
524
|
+
const mirrorSha = await fetchOfficialMarketplaceFromMirror(targetPath, this.marketplacesDir);
|
|
525
|
+
if (mirrorSha !== null) {
|
|
526
|
+
mirrorUpdated = true;
|
|
522
527
|
}
|
|
523
|
-
else {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
528
|
+
else if (!ALLOW_OFFICIAL_MARKET_GIT_FALLBACK) {
|
|
529
|
+
logWarn(`Skipping update for official marketplace "${marketplace.name}": mirror fetch failed and git fallback is disabled.`);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (marketplace.source.source === "github" ||
|
|
534
|
+
marketplace.source.source === "git") {
|
|
535
|
+
if (!mirrorUpdated) {
|
|
536
|
+
if (!isGitAvailable) {
|
|
537
|
+
logWarn(`Skipping update for Git/GitHub marketplace "${marketplace.name}" because Git is not installed.`);
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const targetPath = this.getMarketplacePath(marketplace.source);
|
|
541
|
+
if (existsSync(targetPath)) {
|
|
542
|
+
await this.gitService.pull(targetPath);
|
|
527
543
|
}
|
|
528
544
|
else {
|
|
529
|
-
|
|
545
|
+
let url;
|
|
546
|
+
if (marketplace.source.source === "github") {
|
|
547
|
+
url = marketplace.source.repo;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
url = marketplace.source.url;
|
|
551
|
+
}
|
|
552
|
+
await this.gitService.clone(url, targetPath, marketplace.source.ref);
|
|
530
553
|
}
|
|
531
|
-
await this.gitService.clone(url, targetPath, marketplace.source.ref);
|
|
532
554
|
}
|
|
533
555
|
}
|
|
534
556
|
const manifest = await this.loadMarketplaceManifest(this.getMarketplacePath(marketplace.source));
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Centralized service for loading, validating, and managing Wave configuration files.
|
|
5
5
|
* Replaces distributed configuration logic previously embedded in hook.ts.
|
|
6
6
|
*/
|
|
7
|
+
import type { HookEvent, HookEventConfig } from "../types/hooks.js";
|
|
7
8
|
import type { ConfigurationLoadResult, ValidationResult, ConfigurationPaths, WaveConfiguration, Scope, MarketplaceConfig } from "../types/configuration.js";
|
|
8
9
|
import { type EnvironmentValidationResult, type MergedEnvironmentContext, type EnvironmentMergeOptions } from "../types/environment.js";
|
|
9
10
|
import { GatewayConfig, ModelConfig, PermissionMode, AgentOptions } from "../types/index.js";
|
|
@@ -107,7 +108,7 @@ export declare class ConfigurationService {
|
|
|
107
108
|
resolveLanguage(constructorLanguage?: string): string | undefined;
|
|
108
109
|
/**
|
|
109
110
|
* Resolves auto-memory enabled state with fallbacks
|
|
110
|
-
* Resolution priority: settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
|
|
111
|
+
* Resolution priority: session options (host settings-page value) > settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
|
|
111
112
|
* @returns Resolved auto-memory enabled state
|
|
112
113
|
*/
|
|
113
114
|
resolveAutoMemoryEnabled(): boolean;
|
|
@@ -119,7 +120,7 @@ export declare class ConfigurationService {
|
|
|
119
120
|
resolveWorktreeBaseRef(): "fresh" | "head";
|
|
120
121
|
/**
|
|
121
122
|
* Resolves auto-memory extraction frequency with fallbacks
|
|
122
|
-
* Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
|
|
123
|
+
* Resolution priority: session options (host settings-page value) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
|
|
123
124
|
* @returns Resolved auto-memory extraction frequency (turns)
|
|
124
125
|
*/
|
|
125
126
|
resolveAutoMemoryFrequency(): number;
|
|
@@ -160,6 +161,28 @@ export declare class ConfigurationService {
|
|
|
160
161
|
* Update the enabled state of a plugin in the specified scope
|
|
161
162
|
*/
|
|
162
163
|
updateEnabledPlugin(workdir: string, scope: Scope, pluginId: string, enabled: boolean): Promise<void>;
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the settings.json path for a hook scope.
|
|
166
|
+
* user → ~/.wave/settings.json; project → <workdir>/.wave/settings.json
|
|
167
|
+
*/
|
|
168
|
+
private resolveHookConfigPath;
|
|
169
|
+
/**
|
|
170
|
+
* Absolute path of the settings.json that holds hooks for `scope`
|
|
171
|
+
* (user → ~/.wave/settings.json, project → <workdir>/.wave/settings.json).
|
|
172
|
+
* Hosts surface it to the GUI so editors / file panels can open the file —
|
|
173
|
+
* they cannot expand a `~` prefix, so the path must already be absolute.
|
|
174
|
+
*/
|
|
175
|
+
getHookConfigPath(workdir: string, scope: "user" | "project"): string;
|
|
176
|
+
/**
|
|
177
|
+
* Get hook configurations at a specific scope (user or project settings.json)
|
|
178
|
+
*/
|
|
179
|
+
getHooksByScope(workdir: string, scope: Scope): Promise<Partial<Record<HookEvent, HookEventConfig[]>>>;
|
|
180
|
+
/**
|
|
181
|
+
* Delete a hook at a specific scope, identified by hookName of the form
|
|
182
|
+
* `Event:Matcher`. Removes the matching entry from the scope's settings.json
|
|
183
|
+
* hooks block.
|
|
184
|
+
*/
|
|
185
|
+
deleteHook(workdir: string, scope: Scope, hookName: string): Promise<void>;
|
|
163
186
|
/**
|
|
164
187
|
* Get merged marketplaces from all scopes
|
|
165
188
|
*/
|
|
@@ -216,3 +239,12 @@ export declare function loadUserConfigEnv(): Record<string, string>;
|
|
|
216
239
|
* Checks .local.json files first, then falls back to .json files
|
|
217
240
|
*/
|
|
218
241
|
export declare function loadMergedWaveConfig(workdir: string): WaveConfiguration | null;
|
|
242
|
+
/**
|
|
243
|
+
* Parse a hookName of the form `Event:Matcher` (e.g. `PreToolUse:Write`)
|
|
244
|
+
* into its event and matcher parts. Hooks without a matcher use the event
|
|
245
|
+
* name alone (e.g. `SessionStart`).
|
|
246
|
+
*/
|
|
247
|
+
export declare function parseHookName(hookName: string): {
|
|
248
|
+
event: HookEvent;
|
|
249
|
+
matcher: string;
|
|
250
|
+
};
|
|
@@ -119,8 +119,8 @@ export function validateConfigurationObject(config) {
|
|
|
119
119
|
result.errors.push("All permission deny rules must be strings");
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
-
// Validate
|
|
123
|
-
if (config.permissions.
|
|
122
|
+
// Validate defaultMode if present
|
|
123
|
+
if (config.permissions.defaultMode !== undefined) {
|
|
124
124
|
const validModes = [
|
|
125
125
|
"default",
|
|
126
126
|
"bypassPermissions",
|
|
@@ -128,9 +128,9 @@ export function validateConfigurationObject(config) {
|
|
|
128
128
|
"plan",
|
|
129
129
|
"dontAsk",
|
|
130
130
|
];
|
|
131
|
-
if (!validModes.includes(config.permissions.
|
|
131
|
+
if (!validModes.includes(config.permissions.defaultMode)) {
|
|
132
132
|
result.isValid = false;
|
|
133
|
-
result.errors.push(`Invalid
|
|
133
|
+
result.errors.push(`Invalid defaultMode: "${config.permissions.defaultMode}". Must be one of: ${validModes.join(", ")}`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
@@ -590,21 +590,26 @@ export class ConfigurationService {
|
|
|
590
590
|
}
|
|
591
591
|
/**
|
|
592
592
|
* Resolves auto-memory enabled state with fallbacks
|
|
593
|
-
* Resolution priority: settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
|
|
593
|
+
* Resolution priority: session options (host settings-page value) > settings.json > WAVE_DISABLE_AUTO_MEMORY > default (true)
|
|
594
594
|
* @returns Resolved auto-memory enabled state
|
|
595
595
|
*/
|
|
596
596
|
resolveAutoMemoryEnabled() {
|
|
597
|
-
// 1. settings
|
|
597
|
+
// 1. Per-session options override (hosts pass the settings-page toggle over
|
|
598
|
+
// stdio initialize/updateConfig; Agent.create → setOptions stores it)
|
|
599
|
+
if (this.options.autoMemoryEnabled !== undefined) {
|
|
600
|
+
return this.options.autoMemoryEnabled;
|
|
601
|
+
}
|
|
602
|
+
// 2. settings.json (merged)
|
|
598
603
|
if (this.currentConfiguration?.autoMemoryEnabled !== undefined) {
|
|
599
604
|
return this.currentConfiguration.autoMemoryEnabled;
|
|
600
605
|
}
|
|
601
|
-
//
|
|
606
|
+
// 3. WAVE_DISABLE_AUTO_MEMORY environment variable (settings snapshot > OS env)
|
|
602
607
|
const disableAutoMemory = this.envSnapshot.WAVE_DISABLE_AUTO_MEMORY ??
|
|
603
608
|
process.env.WAVE_DISABLE_AUTO_MEMORY;
|
|
604
609
|
if (disableAutoMemory === "1" || disableAutoMemory === "true") {
|
|
605
610
|
return false;
|
|
606
611
|
}
|
|
607
|
-
//
|
|
612
|
+
// 4. Default (true)
|
|
608
613
|
return true;
|
|
609
614
|
}
|
|
610
615
|
/**
|
|
@@ -621,15 +626,23 @@ export class ConfigurationService {
|
|
|
621
626
|
}
|
|
622
627
|
/**
|
|
623
628
|
* Resolves auto-memory extraction frequency with fallbacks
|
|
624
|
-
* Resolution priority: settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
|
|
629
|
+
* Resolution priority: session options (host settings-page value) > settings.json > WAVE_AUTO_MEMORY_FREQUENCY > default (1)
|
|
625
630
|
* @returns Resolved auto-memory extraction frequency (turns)
|
|
626
631
|
*/
|
|
627
632
|
resolveAutoMemoryFrequency() {
|
|
628
|
-
// 1. settings
|
|
633
|
+
// 1. Per-session options override (hosts pass the settings-page value over
|
|
634
|
+
// stdio initialize/updateConfig; Agent.create → setOptions stores it).
|
|
635
|
+
// Only positive values are honored so a malformed 0/negative never
|
|
636
|
+
// degenerates into per-turn extraction.
|
|
637
|
+
if (this.options.autoMemoryFrequency !== undefined &&
|
|
638
|
+
this.options.autoMemoryFrequency > 0) {
|
|
639
|
+
return this.options.autoMemoryFrequency;
|
|
640
|
+
}
|
|
641
|
+
// 2. settings.json (merged)
|
|
629
642
|
if (this.currentConfiguration?.autoMemoryFrequency !== undefined) {
|
|
630
643
|
return this.currentConfiguration.autoMemoryFrequency;
|
|
631
644
|
}
|
|
632
|
-
//
|
|
645
|
+
// 3. WAVE_AUTO_MEMORY_FREQUENCY environment variable (settings snapshot > OS env)
|
|
633
646
|
const envFrequency = this.envSnapshot.WAVE_AUTO_MEMORY_FREQUENCY ??
|
|
634
647
|
process.env.WAVE_AUTO_MEMORY_FREQUENCY;
|
|
635
648
|
if (envFrequency) {
|
|
@@ -638,7 +651,7 @@ export class ConfigurationService {
|
|
|
638
651
|
return parsed;
|
|
639
652
|
}
|
|
640
653
|
}
|
|
641
|
-
//
|
|
654
|
+
// 4. Default (1)
|
|
642
655
|
return 1;
|
|
643
656
|
}
|
|
644
657
|
/**
|
|
@@ -840,6 +853,86 @@ export class ConfigurationService {
|
|
|
840
853
|
config.enabledPlugins[pluginId] = enabled;
|
|
841
854
|
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
842
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* Resolve the settings.json path for a hook scope.
|
|
858
|
+
* user → ~/.wave/settings.json; project → <workdir>/.wave/settings.json
|
|
859
|
+
*/
|
|
860
|
+
resolveHookConfigPath(workdir, scope) {
|
|
861
|
+
const configPath = scope === "user"
|
|
862
|
+
? getUserConfigPaths()[0]
|
|
863
|
+
: getProjectConfigPaths(workdir)[1];
|
|
864
|
+
return { configPath, configDir: path.dirname(configPath) };
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Absolute path of the settings.json that holds hooks for `scope`
|
|
868
|
+
* (user → ~/.wave/settings.json, project → <workdir>/.wave/settings.json).
|
|
869
|
+
* Hosts surface it to the GUI so editors / file panels can open the file —
|
|
870
|
+
* they cannot expand a `~` prefix, so the path must already be absolute.
|
|
871
|
+
*/
|
|
872
|
+
getHookConfigPath(workdir, scope) {
|
|
873
|
+
return this.resolveHookConfigPath(workdir, scope).configPath;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* Get hook configurations at a specific scope (user or project settings.json)
|
|
877
|
+
*/
|
|
878
|
+
async getHooksByScope(workdir, scope) {
|
|
879
|
+
if (scope !== "user" && !existsSync(workdir)) {
|
|
880
|
+
return {};
|
|
881
|
+
}
|
|
882
|
+
const { configPath } = this.resolveHookConfigPath(workdir, scope);
|
|
883
|
+
if (!existsSync(configPath)) {
|
|
884
|
+
return {};
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
888
|
+
const config = JSON.parse(content);
|
|
889
|
+
return config.hooks || {};
|
|
890
|
+
}
|
|
891
|
+
catch {
|
|
892
|
+
return {};
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Delete a hook at a specific scope, identified by hookName of the form
|
|
897
|
+
* `Event:Matcher`. Removes the matching entry from the scope's settings.json
|
|
898
|
+
* hooks block.
|
|
899
|
+
*/
|
|
900
|
+
async deleteHook(workdir, scope, hookName) {
|
|
901
|
+
if (scope !== "user" && !existsSync(workdir)) {
|
|
902
|
+
throw new Error(`Working directory does not exist: ${workdir}`);
|
|
903
|
+
}
|
|
904
|
+
const { configPath, configDir } = this.resolveHookConfigPath(workdir, scope);
|
|
905
|
+
if (!existsSync(configDir)) {
|
|
906
|
+
await fs.mkdir(configDir, { recursive: true });
|
|
907
|
+
}
|
|
908
|
+
let config = {};
|
|
909
|
+
if (existsSync(configPath)) {
|
|
910
|
+
try {
|
|
911
|
+
const content = await fs.readFile(configPath, "utf-8");
|
|
912
|
+
config = JSON.parse(content);
|
|
913
|
+
}
|
|
914
|
+
catch {
|
|
915
|
+
// Start with empty config if file is corrupted
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
const hooks = config.hooks || {};
|
|
919
|
+
const { event, matcher } = parseHookName(hookName);
|
|
920
|
+
if (!isValidHookEvent(event)) {
|
|
921
|
+
throw new Error(`Invalid hook event: ${event}`);
|
|
922
|
+
}
|
|
923
|
+
const eventConfigs = hooks[event];
|
|
924
|
+
if (eventConfigs) {
|
|
925
|
+
const remaining = eventConfigs.filter((cfg) => (cfg.matcher || "") !== (matcher || ""));
|
|
926
|
+
if (remaining.length > 0) {
|
|
927
|
+
hooks[event] = remaining;
|
|
928
|
+
}
|
|
929
|
+
else {
|
|
930
|
+
delete hooks[event];
|
|
931
|
+
}
|
|
932
|
+
config.hooks = hooks;
|
|
933
|
+
await atomicWriteFile(configPath, JSON.stringify(config, null, 2));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
843
936
|
/**
|
|
844
937
|
* Get merged marketplaces from all scopes
|
|
845
938
|
*/
|
|
@@ -1195,10 +1288,9 @@ export function loadMergedWaveConfig(workdir) {
|
|
|
1195
1288
|
]),
|
|
1196
1289
|
];
|
|
1197
1290
|
}
|
|
1198
|
-
// Merge
|
|
1199
|
-
if (config.permissions.
|
|
1200
|
-
mergedConfig.permissions.
|
|
1201
|
-
config.permissions.permissionMode;
|
|
1291
|
+
// Merge defaultMode (last one wins)
|
|
1292
|
+
if (config.permissions.defaultMode !== undefined) {
|
|
1293
|
+
mergedConfig.permissions.defaultMode = config.permissions.defaultMode;
|
|
1202
1294
|
}
|
|
1203
1295
|
// Merge additionalDirectories
|
|
1204
1296
|
if (config.permissions.additionalDirectories) {
|
|
@@ -1294,3 +1386,18 @@ export function loadMergedWaveConfig(workdir) {
|
|
|
1294
1386
|
enableArtifact: mergedConfig.enableArtifact,
|
|
1295
1387
|
};
|
|
1296
1388
|
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Parse a hookName of the form `Event:Matcher` (e.g. `PreToolUse:Write`)
|
|
1391
|
+
* into its event and matcher parts. Hooks without a matcher use the event
|
|
1392
|
+
* name alone (e.g. `SessionStart`).
|
|
1393
|
+
*/
|
|
1394
|
+
export function parseHookName(hookName) {
|
|
1395
|
+
const separator = hookName.indexOf(":");
|
|
1396
|
+
if (separator === -1) {
|
|
1397
|
+
return { event: hookName, matcher: "" };
|
|
1398
|
+
}
|
|
1399
|
+
return {
|
|
1400
|
+
event: hookName.slice(0, separator),
|
|
1401
|
+
matcher: hookName.slice(separator + 1),
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
@@ -84,8 +84,8 @@ export class InitializationService {
|
|
|
84
84
|
if (configResult.configuration.permissions.deny) {
|
|
85
85
|
permissionManager.updateDeniedRules(configResult.configuration.permissions.deny);
|
|
86
86
|
}
|
|
87
|
-
if (configResult.configuration.permissions.
|
|
88
|
-
permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.
|
|
87
|
+
if (configResult.configuration.permissions.defaultMode) {
|
|
88
|
+
permissionManager.updateConfiguredPermissionMode(configResult.configuration.permissions.defaultMode);
|
|
89
89
|
}
|
|
90
90
|
if (configResult.configuration.permissions.additionalDirectories) {
|
|
91
91
|
permissionManager.updateAdditionalDirectories(configResult.configuration.permissions.additionalDirectories);
|
|
@@ -65,6 +65,20 @@ export declare class JsonlHandler {
|
|
|
65
65
|
* Get the last message from JSONL file using efficient file reading (simplified)
|
|
66
66
|
*/
|
|
67
67
|
getLastMessage(filePath: string): Promise<Message | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Latest context-usage total for a session file.
|
|
70
|
+
*
|
|
71
|
+
* Sessions accumulate usage-less messages at the end — SessionStart hook
|
|
72
|
+
* meta messages are appended (and persisted) on every resume — so the last
|
|
73
|
+
* line alone cannot answer "how much context did this conversation use".
|
|
74
|
+
* Scan backwards over the file's tail window and take the newest message
|
|
75
|
+
* that carries usage.
|
|
76
|
+
*
|
|
77
|
+
* @param filePath - Path to the session JSONL file
|
|
78
|
+
* @returns total_tokens of the newest usage-bearing message, or 0 when the
|
|
79
|
+
* tail holds none (empty/unreadable file, no completed request yet)
|
|
80
|
+
*/
|
|
81
|
+
getLatestTotalTokens(filePath: string): Promise<number>;
|
|
68
82
|
/**
|
|
69
83
|
* Read the creation-time metadata from the session file's header line.
|
|
70
84
|
*
|