wave-agent-sdk 1.0.4 → 1.0.6
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 +23 -1
- package/dist/agent.js +34 -4
- package/dist/managers/aiManager.d.ts +36 -0
- package/dist/managers/aiManager.js +240 -8
- package/dist/managers/messageManager.d.ts +1 -3
- package/dist/managers/messageManager.js +19 -9
- package/dist/managers/slashCommandManager.js +28 -0
- package/dist/managers/subagentManager.d.ts +0 -2
- package/dist/services/session.js +2 -9
- package/dist/telemetry/instrumentation.d.ts +1 -1
- package/dist/telemetry/instrumentation.js +57 -28
- package/dist/tools/bashTool.js +16 -6
- package/dist/tools/grepTool.js +11 -3
- package/dist/types/index.d.ts +3 -0
- package/dist/types/messaging.d.ts +2 -0
- package/dist/utils/containerSetup.js +0 -3
- package/dist/utils/messageOperations.d.ts +10 -1
- package/dist/utils/messageOperations.js +19 -1
- package/dist/utils/notificationXml.js +5 -0
- package/dist/utils/ripgrep.d.ts +4 -4
- package/dist/utils/ripgrep.js +4 -27
- package/package.json +20 -8
- package/bin/rg +0 -79
- package/dist/managers/forkedAgentManager.d.ts +0 -50
- package/dist/managers/forkedAgentManager.js +0 -130
- package/scripts/install_ripgrep.js +0 -111
- package/scripts/postinstall.js +0 -38
- package/src/agent.ts +0 -1271
- package/src/constants/subagents.ts +0 -4
- package/src/constants/toolLimits.ts +0 -15
- package/src/constants/tools.ts +0 -24
- package/src/core/plugin.ts +0 -237
- package/src/core/session.ts +0 -9
- package/src/index.ts +0 -38
- package/src/managers/MemoryRuleManager.ts +0 -198
- package/src/managers/aiManager.ts +0 -2429
- package/src/managers/backgroundTaskManager.ts +0 -480
- package/src/managers/bangManager.ts +0 -119
- package/src/managers/cronManager.ts +0 -401
- package/src/managers/foregroundTaskManager.ts +0 -30
- package/src/managers/forkedAgentManager.ts +0 -216
- package/src/managers/hookManager.ts +0 -1089
- package/src/managers/liveConfigManager.ts +0 -445
- package/src/managers/lspManager.ts +0 -470
- package/src/managers/mcpManager.ts +0 -1010
- package/src/managers/messageManager.ts +0 -1180
- package/src/managers/messageQueue.ts +0 -157
- package/src/managers/permissionManager.ts +0 -1140
- package/src/managers/planManager.ts +0 -112
- package/src/managers/pluginManager.ts +0 -330
- package/src/managers/pluginScopeManager.ts +0 -124
- package/src/managers/reversionManager.ts +0 -178
- package/src/managers/skillManager.ts +0 -578
- package/src/managers/slashCommandManager.ts +0 -564
- package/src/managers/subagentManager.ts +0 -915
- package/src/managers/toolManager.ts +0 -489
- package/src/managers/workflowManager.ts +0 -493
- package/src/prompts/autoMemory.ts +0 -33
- package/src/prompts/autoMemoryExtraction.ts +0 -146
- package/src/prompts/index.ts +0 -506
- package/src/prompts/planModeReminders.ts +0 -126
- package/src/services/GitService.ts +0 -131
- package/src/services/MarketplaceService.ts +0 -1052
- package/src/services/MemoryRuleService.ts +0 -71
- package/src/services/aiService.ts +0 -919
- package/src/services/authService.ts +0 -565
- package/src/services/autoMemoryService.ts +0 -280
- package/src/services/configurationService.ts +0 -1520
- package/src/services/fileWatcher.ts +0 -319
- package/src/services/hook.ts +0 -348
- package/src/services/initializationService.ts +0 -357
- package/src/services/interactionService.ts +0 -229
- package/src/services/jsonlHandler.ts +0 -295
- package/src/services/memory.ts +0 -234
- package/src/services/pluginLoader.ts +0 -252
- package/src/services/remoteSettingsService.ts +0 -366
- package/src/services/reversionService.ts +0 -122
- package/src/services/session.ts +0 -922
- package/src/services/taskManager.ts +0 -321
- package/src/telemetry/events.ts +0 -65
- package/src/telemetry/instrumentation.ts +0 -499
- package/src/telemetry/sessionTracing.ts +0 -348
- package/src/tools/agentTool.ts +0 -322
- package/src/tools/askUserQuestion.ts +0 -160
- package/src/tools/bashTool.ts +0 -621
- package/src/tools/buildTool.ts +0 -61
- package/src/tools/cronCreateTool.ts +0 -161
- package/src/tools/cronDeleteTool.ts +0 -51
- package/src/tools/cronListTool.ts +0 -47
- package/src/tools/editTool.ts +0 -313
- package/src/tools/enterPlanMode.ts +0 -124
- package/src/tools/enterWorktreeTool.ts +0 -190
- package/src/tools/exitPlanMode.ts +0 -139
- package/src/tools/exitWorktreeTool.ts +0 -236
- package/src/tools/globTool.ts +0 -167
- package/src/tools/grepTool.ts +0 -399
- package/src/tools/lspTool.ts +0 -883
- package/src/tools/readTool.ts +0 -426
- package/src/tools/skillTool.ts +0 -248
- package/src/tools/taskManagementTools.ts +0 -574
- package/src/tools/taskStopTool.ts +0 -78
- package/src/tools/types.ts +0 -137
- package/src/tools/webFetchTool.ts +0 -373
- package/src/tools/workflowTool.ts +0 -205
- package/src/tools/writeTool.ts +0 -267
- package/src/types/agent.ts +0 -128
- package/src/types/auth.ts +0 -19
- package/src/types/commands.ts +0 -30
- package/src/types/config.ts +0 -43
- package/src/types/configuration.ts +0 -168
- package/src/types/core.ts +0 -105
- package/src/types/cron.ts +0 -13
- package/src/types/environment.ts +0 -106
- package/src/types/fileSearch.ts +0 -4
- package/src/types/history.ts +0 -7
- package/src/types/hooks.ts +0 -256
- package/src/types/index.ts +0 -41
- package/src/types/lsp.ts +0 -98
- package/src/types/marketplace.ts +0 -71
- package/src/types/mcp.ts +0 -43
- package/src/types/memoryRule.ts +0 -31
- package/src/types/messaging.ts +0 -114
- package/src/types/permissions.ts +0 -80
- package/src/types/plugins.ts +0 -39
- package/src/types/processes.ts +0 -99
- package/src/types/reversion.ts +0 -29
- package/src/types/session.ts +0 -12
- package/src/types/skills.ts +0 -108
- package/src/types/tasks.ts +0 -13
- package/src/types/telemetry.ts +0 -98
- package/src/types/tools.ts +0 -44
- package/src/types/workflow.ts +0 -6
- package/src/utils/abortUtils.ts +0 -118
- package/src/utils/atomicWrite.ts +0 -61
- package/src/utils/bashParser.ts +0 -899
- package/src/utils/cacheControlUtils.ts +0 -426
- package/src/utils/commandArgumentParser.ts +0 -161
- package/src/utils/commandPathResolver.ts +0 -88
- package/src/utils/configPaths.ts +0 -220
- package/src/utils/configValidator.ts +0 -100
- package/src/utils/constants.ts +0 -39
- package/src/utils/container.ts +0 -92
- package/src/utils/containerSetup.ts +0 -371
- package/src/utils/convertMessagesForAPI.ts +0 -326
- package/src/utils/cronTasks.ts +0 -128
- package/src/utils/cronTasksLock.ts +0 -182
- package/src/utils/cronToHuman.ts +0 -99
- package/src/utils/customCommands.ts +0 -95
- package/src/utils/editUtils.ts +0 -20
- package/src/utils/fileFormat.ts +0 -40
- package/src/utils/fileSearch.ts +0 -151
- package/src/utils/fileUtils.ts +0 -223
- package/src/utils/gitUtils.ts +0 -315
- package/src/utils/globalLogger.ts +0 -128
- package/src/utils/groupMessagesByApiRound.ts +0 -121
- package/src/utils/hookMatcher.ts +0 -176
- package/src/utils/markdownParser.ts +0 -317
- package/src/utils/mcpUtils.ts +0 -138
- package/src/utils/messageOperations.ts +0 -641
- package/src/utils/modelCapabilities.ts +0 -30
- package/src/utils/nameGenerator.ts +0 -99
- package/src/utils/notificationXml.ts +0 -57
- package/src/utils/openaiClient.ts +0 -251
- package/src/utils/parseCronExpression.ts +0 -78
- package/src/utils/path.ts +0 -72
- package/src/utils/pathEncoder.ts +0 -400
- package/src/utils/pathSafety.ts +0 -40
- package/src/utils/promptHistory.ts +0 -168
- package/src/utils/ripgrep.ts +0 -35
- package/src/utils/shellResolver.ts +0 -190
- package/src/utils/skillParser.ts +0 -246
- package/src/utils/stringUtils.ts +0 -156
- package/src/utils/subagentParser.ts +0 -279
- package/src/utils/taskReminder.ts +0 -96
- package/src/utils/tokenCalculation.ts +0 -43
- package/src/utils/tokenEstimate.ts +0 -34
- package/src/utils/toolResultStorage.ts +0 -117
- package/src/utils/worktreeSession.ts +0 -26
- package/src/utils/worktreeUtils.ts +0 -786
- package/src/workflow/budgetTracker.ts +0 -34
- package/src/workflow/concurrencyLimiter.ts +0 -47
- package/src/workflow/journal.ts +0 -95
- package/src/workflow/progressReporter.ts +0 -141
- package/src/workflow/runState.ts +0 -65
- package/src/workflow/scriptRuntime.ts +0 -274
- package/src/workflow/structuredOutput.ts +0 -123
- package/src/workflow/types.ts +0 -101
- package/src/workflow/workflowApis.ts +0 -410
- package/vendor/ripgrep/linux-aarch64/rg +0 -0
- package/vendor/ripgrep/linux-x86_64/rg +0 -0
- package/vendor/ripgrep/macos-aarch64/rg +0 -0
- package/vendor/ripgrep/macos-x86_64/rg +0 -0
- package/vendor/ripgrep/windows-aarch64/rg.exe +0 -0
- package/vendor/ripgrep/windows-x86_64/rg.exe +0 -0
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
2
|
-
import { CRON_CREATE_TOOL_NAME } from "../constants/tools.js";
|
|
3
|
-
import { cronToHuman } from "../utils/cronToHuman.js";
|
|
4
|
-
import { parseCronExpression } from "../utils/parseCronExpression.js";
|
|
5
|
-
|
|
6
|
-
const DEFAULT_MAX_AGE_DAYS = 7;
|
|
7
|
-
const MAX_JOBS = 50;
|
|
8
|
-
|
|
9
|
-
const CRON_CREATE_DESCRIPTION = `Schedule a prompt to run at a future time within this Wave session — either recurring on a cron schedule, or once at a specific time.`;
|
|
10
|
-
|
|
11
|
-
const CRON_CREATE_PROMPT = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders.
|
|
12
|
-
|
|
13
|
-
Uses standard 5-field cron in the user's local timezone: minute hour day-of-month month day-of-week. "0 9 * * *" means 9am local — no timezone conversion needed.
|
|
14
|
-
|
|
15
|
-
## One-shot tasks (recurring: false)
|
|
16
|
-
|
|
17
|
-
For "remind me at X" or "at <time>, do Y" requests — fire once then auto-delete.
|
|
18
|
-
Pin minute/hour/day-of-month/month to specific values:
|
|
19
|
-
"remind me at 2:30pm today to check the deploy" → cron: "30 14 <today_dom> <today_month> *", recurring: false
|
|
20
|
-
"tomorrow morning, run the smoke test" → cron: "57 8 <tomorrow_dom> <tomorrow_month> *", recurring: false
|
|
21
|
-
|
|
22
|
-
## Recurring jobs (recurring: true, the default)
|
|
23
|
-
|
|
24
|
-
For "every N minutes" / "every hour" / "weekdays at 9am" requests:
|
|
25
|
-
"*/5 * * * *" (every 5 min), "0 * * * *" (hourly), "0 9 * * 1-5" (weekdays at 9am local)
|
|
26
|
-
|
|
27
|
-
## Avoid the :00 and :30 minute marks when the task allows it
|
|
28
|
-
|
|
29
|
-
Every user who asks for "9am" gets \`0 9\`, and every user who asks for "hourly" gets \`0 *\` — which means requests from across the planet land on the API at the same instant. When the user's request is approximate, pick a minute that is NOT 0 or 30:
|
|
30
|
-
"every morning around 9" → "57 8 * * *" or "3 9 * * *" (not "0 9 * * *")
|
|
31
|
-
"hourly" → "7 * * * *" (not "0 * * * *")
|
|
32
|
-
"in an hour or so, remind me to..." → pick whatever minute you land on, don't round
|
|
33
|
-
|
|
34
|
-
Only use minute 0 or 30 when the user names that exact time and clearly means it ("at 9:00 sharp", "at half past", coordinating with a meeting). When in doubt, nudge a few minutes early or late — the user will not notice, and the fleet will.
|
|
35
|
-
|
|
36
|
-
## Session-only vs durable
|
|
37
|
-
|
|
38
|
-
By default, jobs live only in this Wave session — nothing is written to disk, and the job is gone when Wave exits.
|
|
39
|
-
Set \`durable: true\` to persist the job to \`.wave/scheduled_tasks.json\` so it survives Wave restarts (requires the session to hold the scheduler lock).
|
|
40
|
-
|
|
41
|
-
## Runtime behavior
|
|
42
|
-
|
|
43
|
-
Jobs only fire while the REPL is idle (not mid-query). The scheduler adds a small deterministic jitter on top of whatever you pick: recurring tasks fire up to 10% of their period late (max 15 min); one-shot tasks landing on :00 or :30 fire up to 90s early. Picking an off-minute is still the bigger lever.
|
|
44
|
-
|
|
45
|
-
Recurring tasks auto-expire after ${DEFAULT_MAX_AGE_DAYS} days — they fire one final time, then are deleted. This bounds session lifetime. Tell the user about the ${DEFAULT_MAX_AGE_DAYS}-day limit when scheduling recurring jobs.
|
|
46
|
-
|
|
47
|
-
Returns a job ID you can pass to CronDelete.`;
|
|
48
|
-
|
|
49
|
-
export const cronCreateTool: ToolPlugin = {
|
|
50
|
-
name: CRON_CREATE_TOOL_NAME,
|
|
51
|
-
config: {
|
|
52
|
-
type: "function",
|
|
53
|
-
function: {
|
|
54
|
-
name: CRON_CREATE_TOOL_NAME,
|
|
55
|
-
description: CRON_CREATE_DESCRIPTION,
|
|
56
|
-
parameters: {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: {
|
|
59
|
-
cron: {
|
|
60
|
-
type: "string",
|
|
61
|
-
description:
|
|
62
|
-
'Standard 5-field cron expression in local time: "M H DoM Mon DoW" (e.g. "*/5 * * * *" = every 5 minutes, "30 14 28 2 *" = Feb 28 at 2:30pm local once).',
|
|
63
|
-
},
|
|
64
|
-
prompt: {
|
|
65
|
-
type: "string",
|
|
66
|
-
description: "The prompt to enqueue at each fire time.",
|
|
67
|
-
},
|
|
68
|
-
recurring: {
|
|
69
|
-
type: "boolean",
|
|
70
|
-
description: `true (default) = fire on every cron match until deleted or auto-expired after ${DEFAULT_MAX_AGE_DAYS} days. false = fire once at the next match, then auto-delete. Use false for "remind me at X" one-shot requests with pinned minute/hour/dom/month.`,
|
|
71
|
-
default: true,
|
|
72
|
-
},
|
|
73
|
-
durable: {
|
|
74
|
-
type: "boolean",
|
|
75
|
-
description:
|
|
76
|
-
"true = persist job to .wave/scheduled_tasks.json so it survives Wave restarts. false (default) = session-only, removed when Wave exits.",
|
|
77
|
-
default: false,
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
required: ["cron", "prompt"],
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
prompt: () => CRON_CREATE_PROMPT,
|
|
85
|
-
execute: async (
|
|
86
|
-
args: Record<string, unknown>,
|
|
87
|
-
context: ToolContext,
|
|
88
|
-
): Promise<ToolResult> => {
|
|
89
|
-
const {
|
|
90
|
-
cron,
|
|
91
|
-
prompt,
|
|
92
|
-
recurring = true,
|
|
93
|
-
durable = false,
|
|
94
|
-
} = args as {
|
|
95
|
-
cron: string;
|
|
96
|
-
prompt: string;
|
|
97
|
-
recurring?: boolean;
|
|
98
|
-
durable?: boolean;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
if (!context.cronManager) {
|
|
102
|
-
return {
|
|
103
|
-
success: false,
|
|
104
|
-
content: "",
|
|
105
|
-
error: "CronManager not available",
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Validate cron expression
|
|
110
|
-
if (!parseCronExpression(cron)) {
|
|
111
|
-
return {
|
|
112
|
-
success: false,
|
|
113
|
-
content: "",
|
|
114
|
-
error: `Invalid cron expression '${cron}'. Expected 5 fields: M H DoM Mon DoW.`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Check max jobs limit
|
|
119
|
-
const existingJobs = context.cronManager.listJobs();
|
|
120
|
-
if (existingJobs.length >= MAX_JOBS) {
|
|
121
|
-
return {
|
|
122
|
-
success: false,
|
|
123
|
-
content: "",
|
|
124
|
-
error: `Too many scheduled jobs (max ${MAX_JOBS}). Cancel one first.`,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const job = context.cronManager.createJob({
|
|
130
|
-
cron,
|
|
131
|
-
prompt,
|
|
132
|
-
recurring,
|
|
133
|
-
durable,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
const humanSchedule = cronToHuman(cron);
|
|
137
|
-
const where = durable
|
|
138
|
-
? "Persisted to .wave/scheduled_tasks.json"
|
|
139
|
-
: "Session-only (not written to disk, dies when Wave exits)";
|
|
140
|
-
const resultMessage = recurring
|
|
141
|
-
? `Scheduled recurring job ${job.id} (${humanSchedule}). ${where}. Auto-expires after ${DEFAULT_MAX_AGE_DAYS} days. Use CronDelete to cancel sooner.`
|
|
142
|
-
: `Scheduled one-shot task ${job.id} (${humanSchedule}). ${where}. It will fire once then auto-delete.`;
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
success: true,
|
|
146
|
-
content: JSON.stringify(
|
|
147
|
-
{ id: job.id, humanSchedule, recurring, durable },
|
|
148
|
-
null,
|
|
149
|
-
2,
|
|
150
|
-
),
|
|
151
|
-
shortResult: resultMessage,
|
|
152
|
-
};
|
|
153
|
-
} catch (error) {
|
|
154
|
-
return {
|
|
155
|
-
success: false,
|
|
156
|
-
content: "",
|
|
157
|
-
error: error instanceof Error ? error.message : String(error),
|
|
158
|
-
};
|
|
159
|
-
}
|
|
160
|
-
},
|
|
161
|
-
};
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
2
|
-
import { CRON_DELETE_TOOL_NAME } from "../constants/tools.js";
|
|
3
|
-
|
|
4
|
-
const CRON_DELETE_DESCRIPTION = "Cancel a scheduled cron job by ID";
|
|
5
|
-
|
|
6
|
-
const CRON_DELETE_PROMPT = `Cancel a cron job previously scheduled with CronCreate. Removes it from both the in-memory session store and durable file storage (if applicable).`;
|
|
7
|
-
|
|
8
|
-
export const cronDeleteTool: ToolPlugin = {
|
|
9
|
-
name: CRON_DELETE_TOOL_NAME,
|
|
10
|
-
config: {
|
|
11
|
-
type: "function",
|
|
12
|
-
function: {
|
|
13
|
-
name: CRON_DELETE_TOOL_NAME,
|
|
14
|
-
description: CRON_DELETE_DESCRIPTION,
|
|
15
|
-
parameters: {
|
|
16
|
-
type: "object",
|
|
17
|
-
properties: {
|
|
18
|
-
id: {
|
|
19
|
-
type: "string",
|
|
20
|
-
description: "Job ID returned by CronCreate",
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
required: ["id"],
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
prompt: () => CRON_DELETE_PROMPT,
|
|
28
|
-
execute: async (
|
|
29
|
-
args: Record<string, unknown>,
|
|
30
|
-
context: ToolContext,
|
|
31
|
-
): Promise<ToolResult> => {
|
|
32
|
-
const { id } = args as { id: string };
|
|
33
|
-
|
|
34
|
-
if (!context.cronManager) {
|
|
35
|
-
return {
|
|
36
|
-
success: false,
|
|
37
|
-
content: "",
|
|
38
|
-
error: "CronManager not available",
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const success = context.cronManager.deleteJob(id);
|
|
43
|
-
|
|
44
|
-
return {
|
|
45
|
-
success,
|
|
46
|
-
content: JSON.stringify({ success }, null, 2),
|
|
47
|
-
shortResult: success ? `Deleted job ${id}` : `Job ${id} not found`,
|
|
48
|
-
error: success ? undefined : `Job ${id} not found`,
|
|
49
|
-
};
|
|
50
|
-
},
|
|
51
|
-
};
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
2
|
-
import { CRON_LIST_TOOL_NAME } from "../constants/tools.js";
|
|
3
|
-
|
|
4
|
-
const CRON_LIST_DESCRIPTION = "List scheduled cron jobs";
|
|
5
|
-
|
|
6
|
-
const CRON_LIST_PROMPT = `List all cron jobs scheduled via CronCreate in this session.`;
|
|
7
|
-
|
|
8
|
-
export const cronListTool: ToolPlugin = {
|
|
9
|
-
name: CRON_LIST_TOOL_NAME,
|
|
10
|
-
config: {
|
|
11
|
-
type: "function",
|
|
12
|
-
function: {
|
|
13
|
-
name: CRON_LIST_TOOL_NAME,
|
|
14
|
-
description: CRON_LIST_DESCRIPTION,
|
|
15
|
-
parameters: {
|
|
16
|
-
type: "object",
|
|
17
|
-
properties: {},
|
|
18
|
-
},
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
prompt: () => CRON_LIST_PROMPT,
|
|
22
|
-
execute: async (
|
|
23
|
-
_args: Record<string, unknown>,
|
|
24
|
-
context: ToolContext,
|
|
25
|
-
): Promise<ToolResult> => {
|
|
26
|
-
if (!context.cronManager) {
|
|
27
|
-
return {
|
|
28
|
-
success: false,
|
|
29
|
-
content: "",
|
|
30
|
-
error: "CronManager not available",
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const jobs = context.cronManager.listJobs();
|
|
35
|
-
|
|
36
|
-
const annotatedJobs = jobs.map((job) => ({
|
|
37
|
-
...job,
|
|
38
|
-
scope: job.durable ? "durable" : "session-only",
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
success: true,
|
|
43
|
-
content: JSON.stringify({ jobs: annotatedJobs }, null, 2),
|
|
44
|
-
shortResult: `Found ${jobs.length} jobs`,
|
|
45
|
-
};
|
|
46
|
-
},
|
|
47
|
-
};
|
package/src/tools/editTool.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, stat } from "fs/promises";
|
|
2
|
-
import { createHash } from "crypto";
|
|
3
|
-
import { logger } from "../utils/globalLogger.js";
|
|
4
|
-
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
5
|
-
import { resolvePath, getDisplayPath } from "../utils/path.js";
|
|
6
|
-
import { escapeRegExp, analyzeEditMismatch } from "../utils/editUtils.js";
|
|
7
|
-
import { EDIT_TOOL_NAME, READ_TOOL_NAME } from "../constants/tools.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Format compact parameter display
|
|
11
|
-
*/
|
|
12
|
-
function formatCompactParams(
|
|
13
|
-
args: Record<string, unknown>,
|
|
14
|
-
context: ToolContext,
|
|
15
|
-
): string {
|
|
16
|
-
const filePath = args.file_path as string;
|
|
17
|
-
return getDisplayPath(filePath || "", context.workdir);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Single file edit tool plugin
|
|
22
|
-
*/
|
|
23
|
-
export const editTool: ToolPlugin = {
|
|
24
|
-
name: EDIT_TOOL_NAME,
|
|
25
|
-
isConcurrencySafe: false,
|
|
26
|
-
formatCompactParams,
|
|
27
|
-
prompt: () =>
|
|
28
|
-
`Performs exact string replacements in files.
|
|
29
|
-
|
|
30
|
-
Usage:
|
|
31
|
-
- You must use your \`${READ_TOOL_NAME}\` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
|
|
32
|
-
- When editing text from ${READ_TOOL_NAME} tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
|
|
33
|
-
- ALWAYS prefer editing existing files in the codebase. NEVER write new files unless explicitly required.
|
|
34
|
-
- Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
|
|
35
|
-
- Use the smallest \`old_string\` that's clearly unique — usually 2-4 adjacent lines is sufficient. Avoid including 10+ lines of context when less uniquely identifies the target. Shorter matches are less likely to contain reproduction errors.
|
|
36
|
-
- The edit will FAIL if \`old_string\` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use \`replace_all\` to change every instance of \`old_string\`.
|
|
37
|
-
- Use \`replace_all\` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance.`,
|
|
38
|
-
config: {
|
|
39
|
-
type: "function",
|
|
40
|
-
function: {
|
|
41
|
-
name: EDIT_TOOL_NAME,
|
|
42
|
-
description: "A tool for editing files",
|
|
43
|
-
parameters: {
|
|
44
|
-
type: "object",
|
|
45
|
-
properties: {
|
|
46
|
-
file_path: {
|
|
47
|
-
type: "string",
|
|
48
|
-
description: "The absolute path to the file to modify",
|
|
49
|
-
},
|
|
50
|
-
old_string: {
|
|
51
|
-
type: "string",
|
|
52
|
-
description: "The text to replace",
|
|
53
|
-
},
|
|
54
|
-
new_string: {
|
|
55
|
-
type: "string",
|
|
56
|
-
description:
|
|
57
|
-
"The text to replace it with (must be different from old_string)",
|
|
58
|
-
},
|
|
59
|
-
replace_all: {
|
|
60
|
-
type: "boolean",
|
|
61
|
-
default: false,
|
|
62
|
-
description: "Replace all occurences of old_string (default false)",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
required: ["file_path", "old_string", "new_string"],
|
|
66
|
-
additionalProperties: false,
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
execute: async (
|
|
71
|
-
args: Record<string, unknown>,
|
|
72
|
-
context: ToolContext,
|
|
73
|
-
): Promise<ToolResult> => {
|
|
74
|
-
const filePath = args.file_path as string;
|
|
75
|
-
const oldString = args.old_string as string;
|
|
76
|
-
const newString = args.new_string as string;
|
|
77
|
-
const replaceAll = (args.replace_all as boolean) || false;
|
|
78
|
-
|
|
79
|
-
// Validate required parameters
|
|
80
|
-
if (!filePath || typeof filePath !== "string") {
|
|
81
|
-
return {
|
|
82
|
-
success: false,
|
|
83
|
-
content: "",
|
|
84
|
-
error: "file_path parameter is required and must be a string",
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (typeof oldString !== "string") {
|
|
89
|
-
return {
|
|
90
|
-
success: false,
|
|
91
|
-
content: "",
|
|
92
|
-
error: "old_string parameter is required and must be a string",
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (typeof newString !== "string") {
|
|
97
|
-
return {
|
|
98
|
-
success: false,
|
|
99
|
-
content: "",
|
|
100
|
-
error: "new_string parameter is required and must be a string",
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (oldString === newString) {
|
|
105
|
-
return {
|
|
106
|
-
success: false,
|
|
107
|
-
content: "",
|
|
108
|
-
error: "old_string and new_string must be different",
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Trigger conditional rule loading for this file
|
|
113
|
-
context.messageManager?.triggerFileRead(filePath);
|
|
114
|
-
|
|
115
|
-
// Enforce read-before-edit: the file must have been read or written first.
|
|
116
|
-
// readFileState is populated by Read, Write, and Edit tools — single source
|
|
117
|
-
// of truth, aligned with Claude Code's readFileState approach. Skipped in
|
|
118
|
-
// plan mode: permissionManager enforces a plan-file-only gate whose denial
|
|
119
|
-
// message must surface instead of being masked by a read-state rejection.
|
|
120
|
-
const resolvedPath = resolvePath(filePath, context.workdir);
|
|
121
|
-
if (
|
|
122
|
-
context.permissionMode !== "plan" &&
|
|
123
|
-
!context.readFileState?.has(resolvedPath)
|
|
124
|
-
) {
|
|
125
|
-
return {
|
|
126
|
-
success: false,
|
|
127
|
-
content: "",
|
|
128
|
-
error: `You must read the file with the ${READ_TOOL_NAME} tool before editing it. Use ${READ_TOOL_NAME} on ${filePath} first.`,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
try {
|
|
133
|
-
// Read file content
|
|
134
|
-
let originalContent: string;
|
|
135
|
-
try {
|
|
136
|
-
originalContent = await readFile(resolvedPath, "utf-8");
|
|
137
|
-
} catch (readError) {
|
|
138
|
-
return {
|
|
139
|
-
success: false,
|
|
140
|
-
content: "",
|
|
141
|
-
error: `Failed to read file: ${readError instanceof Error ? readError.message : String(readError)}`,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Staleness check (aligned with Claude Code): only flag when the file got
|
|
146
|
-
// newer since last read. For full reads, a content-hash fallback avoids
|
|
147
|
-
// false positives when mtime changed but content didn't (git checkout,
|
|
148
|
-
// editor round-trip save, cloud sync, antivirus). Partial reads get no
|
|
149
|
-
// fallback since only a slice was cached. Skipped in plan mode (see
|
|
150
|
-
// read-before-edit note above) so the plan-file-only denial wins.
|
|
151
|
-
if (context.permissionMode !== "plan" && context.readFileState) {
|
|
152
|
-
const state = context.readFileState.get(resolvedPath);
|
|
153
|
-
if (state) {
|
|
154
|
-
const currentStats = await stat(resolvedPath);
|
|
155
|
-
if (currentStats.mtime.getTime() > state.mtime) {
|
|
156
|
-
const isFullRead =
|
|
157
|
-
state.offset === undefined && state.limit === undefined;
|
|
158
|
-
const contentUnchanged =
|
|
159
|
-
isFullRead &&
|
|
160
|
-
createHash("sha256").update(originalContent).digest("hex") ===
|
|
161
|
-
state.hash;
|
|
162
|
-
if (!contentUnchanged) {
|
|
163
|
-
return {
|
|
164
|
-
success: false,
|
|
165
|
-
content: "",
|
|
166
|
-
error:
|
|
167
|
-
"File has been unexpectedly modified since last read. Read it again before editing it.",
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Normalize line endings for matching
|
|
175
|
-
const normalizedContent = originalContent.replace(/\r\n/g, "\n");
|
|
176
|
-
const normalizedOldString = oldString.replace(/\r\n/g, "\n");
|
|
177
|
-
|
|
178
|
-
// Check if old_string exists
|
|
179
|
-
const index = normalizedContent.indexOf(normalizedOldString);
|
|
180
|
-
const matchedOldString = index !== -1 ? normalizedOldString : null;
|
|
181
|
-
const startLineNumber =
|
|
182
|
-
index !== -1
|
|
183
|
-
? normalizedContent.substring(0, index).split("\n").length
|
|
184
|
-
: undefined;
|
|
185
|
-
|
|
186
|
-
if (!matchedOldString) {
|
|
187
|
-
return {
|
|
188
|
-
success: false,
|
|
189
|
-
content: "",
|
|
190
|
-
error: analyzeEditMismatch(normalizedOldString),
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let newContent: string;
|
|
195
|
-
let replacementCount: number;
|
|
196
|
-
|
|
197
|
-
if (replaceAll) {
|
|
198
|
-
// Replace all matches
|
|
199
|
-
const regex = new RegExp(escapeRegExp(matchedOldString), "g");
|
|
200
|
-
newContent = normalizedContent.replace(regex, newString);
|
|
201
|
-
replacementCount = (normalizedContent.match(regex) || []).length;
|
|
202
|
-
} else {
|
|
203
|
-
// Replace only the first match, but first check if it's unique
|
|
204
|
-
const matches = normalizedContent.split(matchedOldString).length - 1;
|
|
205
|
-
if (matches > 1) {
|
|
206
|
-
return {
|
|
207
|
-
success: false,
|
|
208
|
-
content: "",
|
|
209
|
-
error: `old_string appears ${matches} times in the file. Either provide a larger string with more surrounding context to make it unique or use replace_all=true to change every instance.`,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
newContent = normalizedContent.replace(matchedOldString, newString);
|
|
214
|
-
replacementCount = 1;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Permission check after validation but before real operation
|
|
218
|
-
if (context.permissionManager) {
|
|
219
|
-
try {
|
|
220
|
-
const permissionContext = context.permissionManager.createContext(
|
|
221
|
-
EDIT_TOOL_NAME,
|
|
222
|
-
context.permissionMode || "default",
|
|
223
|
-
context.canUseToolCallback,
|
|
224
|
-
{
|
|
225
|
-
file_path: filePath,
|
|
226
|
-
old_string: oldString,
|
|
227
|
-
new_string: newString,
|
|
228
|
-
replace_all: replaceAll,
|
|
229
|
-
startLineNumber,
|
|
230
|
-
},
|
|
231
|
-
context.toolCallId,
|
|
232
|
-
);
|
|
233
|
-
const permissionResult =
|
|
234
|
-
await context.permissionManager.checkPermission(permissionContext);
|
|
235
|
-
|
|
236
|
-
if (permissionResult.behavior === "deny") {
|
|
237
|
-
return {
|
|
238
|
-
success: false,
|
|
239
|
-
content: "",
|
|
240
|
-
error: `${EDIT_TOOL_NAME} operation denied by user, reason: ${permissionResult.message || "No reason provided"}`,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
} catch {
|
|
244
|
-
return {
|
|
245
|
-
success: false,
|
|
246
|
-
content: "",
|
|
247
|
-
error: "Permission check failed",
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// Record snapshot for reversion
|
|
253
|
-
let snapshotId: string | undefined;
|
|
254
|
-
if (context.reversionManager && context.messageId) {
|
|
255
|
-
snapshotId = await context.reversionManager.recordSnapshot(
|
|
256
|
-
context.messageId,
|
|
257
|
-
resolvedPath,
|
|
258
|
-
"modify",
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Write file
|
|
263
|
-
try {
|
|
264
|
-
await writeFile(resolvedPath, newContent, "utf-8");
|
|
265
|
-
// Commit snapshot on success
|
|
266
|
-
if (context.reversionManager && snapshotId) {
|
|
267
|
-
await context.reversionManager.commitSnapshot(snapshotId);
|
|
268
|
-
}
|
|
269
|
-
} catch (writeError) {
|
|
270
|
-
return {
|
|
271
|
-
success: false,
|
|
272
|
-
content: "",
|
|
273
|
-
error: `Failed to write file: ${writeError instanceof Error ? writeError.message : String(writeError)}`,
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Update readFileState so subsequent serialized edits see fresh state
|
|
278
|
-
if (context.readFileState) {
|
|
279
|
-
const newStats = await stat(resolvedPath);
|
|
280
|
-
const hash = createHash("sha256").update(newContent).digest("hex");
|
|
281
|
-
context.readFileState.set(resolvedPath, {
|
|
282
|
-
mtime: newStats.mtime.getTime(),
|
|
283
|
-
hash,
|
|
284
|
-
offset: undefined,
|
|
285
|
-
limit: undefined,
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const shortResult = replaceAll
|
|
290
|
-
? `Replaced ${replacementCount} instances`
|
|
291
|
-
: "Text replaced successfully";
|
|
292
|
-
|
|
293
|
-
logger.debug(`Edit tool: ${shortResult}`);
|
|
294
|
-
|
|
295
|
-
return {
|
|
296
|
-
success: true,
|
|
297
|
-
content: shortResult,
|
|
298
|
-
shortResult,
|
|
299
|
-
filePath: resolvedPath,
|
|
300
|
-
startLineNumber,
|
|
301
|
-
};
|
|
302
|
-
} catch (error) {
|
|
303
|
-
const errorMessage =
|
|
304
|
-
error instanceof Error ? error.message : String(error);
|
|
305
|
-
logger.error(`Edit tool error: ${errorMessage}`);
|
|
306
|
-
return {
|
|
307
|
-
success: false,
|
|
308
|
-
content: "",
|
|
309
|
-
error: errorMessage,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
},
|
|
313
|
-
};
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { logger } from "../utils/globalLogger.js";
|
|
2
|
-
import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
|
|
3
|
-
import { ENTER_PLAN_MODE_TOOL_NAME } from "../constants/tools.js";
|
|
4
|
-
import { OPERATION_CANCELLED_BY_USER } from "../types/permissions.js";
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Enter Plan Mode Tool Plugin
|
|
8
|
-
*/
|
|
9
|
-
export const enterPlanModeTool: ToolPlugin = {
|
|
10
|
-
name: ENTER_PLAN_MODE_TOOL_NAME,
|
|
11
|
-
config: {
|
|
12
|
-
type: "function",
|
|
13
|
-
function: {
|
|
14
|
-
name: ENTER_PLAN_MODE_TOOL_NAME,
|
|
15
|
-
description:
|
|
16
|
-
"Request to enter plan mode for complex tasks requiring user approval before coding",
|
|
17
|
-
parameters: {
|
|
18
|
-
type: "object",
|
|
19
|
-
properties: {},
|
|
20
|
-
required: [],
|
|
21
|
-
additionalProperties: false,
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
prompt:
|
|
26
|
-
() => `Use this tool to proactively request entering plan mode when a task is non-trivial and benefits from planning before implementation.
|
|
27
|
-
|
|
28
|
-
## When to Use This Tool
|
|
29
|
-
- Multi-file changes or refactoring that requires architectural decisions
|
|
30
|
-
- New features with multiple implementation steps or design trade-offs
|
|
31
|
-
- Tasks where you want user approval on approach before writing code
|
|
32
|
-
- Complex bug fixes that span multiple components
|
|
33
|
-
|
|
34
|
-
## When NOT to Use This Tool
|
|
35
|
-
- Simple fixes (typos, small logic changes in a single file)
|
|
36
|
-
- Research tasks (searching files, reading code, gathering information)
|
|
37
|
-
- Tasks where the implementation approach is straightforward and unambiguous
|
|
38
|
-
- When already in plan mode
|
|
39
|
-
|
|
40
|
-
## Examples
|
|
41
|
-
|
|
42
|
-
1. Task: "Refactor the authentication system to use OAuth2" - Use EnterPlanMode to propose an approach before implementation.
|
|
43
|
-
2. Task: "Add input validation to the login form" - Do NOT use EnterPlanMode, just implement it.
|
|
44
|
-
3. Task: "Migrate the database schema to support multi-tenancy" - Use EnterPlanMode to plan the migration steps.
|
|
45
|
-
4. Task: "Fix the off-by-one error in the pagination logic" - Do NOT use EnterPlanMode, just fix the bug.
|
|
46
|
-
`,
|
|
47
|
-
execute: async (
|
|
48
|
-
_args: Record<string, unknown>,
|
|
49
|
-
context: ToolContext,
|
|
50
|
-
): Promise<ToolResult> => {
|
|
51
|
-
try {
|
|
52
|
-
// Runtime guard: tool is always visible but only works outside plan mode
|
|
53
|
-
if (context.permissionMode === "plan") {
|
|
54
|
-
return {
|
|
55
|
-
success: false,
|
|
56
|
-
content: "Already in plan mode. Use ExitPlanMode to exit.",
|
|
57
|
-
error: "Already in plan mode",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!context.permissionManager) {
|
|
62
|
-
return {
|
|
63
|
-
success: false,
|
|
64
|
-
content: "",
|
|
65
|
-
error: "Permission manager is not available",
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const permissionContext = context.permissionManager.createContext(
|
|
70
|
-
ENTER_PLAN_MODE_TOOL_NAME,
|
|
71
|
-
context.permissionMode || "default",
|
|
72
|
-
context.canUseToolCallback,
|
|
73
|
-
{},
|
|
74
|
-
context.toolCallId,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// No "allow always" option for plan mode transitions (matching Claude Code behavior)
|
|
78
|
-
permissionContext.hidePersistentOption = true;
|
|
79
|
-
|
|
80
|
-
const permissionResult =
|
|
81
|
-
await context.permissionManager.checkPermission(permissionContext);
|
|
82
|
-
|
|
83
|
-
if (permissionResult.behavior === "deny") {
|
|
84
|
-
if (permissionResult.message === OPERATION_CANCELLED_BY_USER) {
|
|
85
|
-
return {
|
|
86
|
-
success: false,
|
|
87
|
-
content: OPERATION_CANCELLED_BY_USER,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
return {
|
|
91
|
-
success: false,
|
|
92
|
-
content: `User declined to enter plan mode. Proceed in current mode.`,
|
|
93
|
-
error: permissionResult.message
|
|
94
|
-
? undefined
|
|
95
|
-
: "Operation declined by user",
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Ensure the full plan mode transition happens (planManager + UI callback).
|
|
100
|
-
// In bypass mode the canUseTool callback is not invoked, so the transition
|
|
101
|
-
// never occurs. In normal mode, the callback already handled it
|
|
102
|
-
// (permissionResult.newPermissionMode will be set).
|
|
103
|
-
if (!permissionResult.newPermissionMode && context.toolManager) {
|
|
104
|
-
context.toolManager.requestPermissionModeChange("plan");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
success: true,
|
|
109
|
-
content:
|
|
110
|
-
"Plan mode entered successfully. Please write your plan to the plan file and use ExitPlanMode when ready for approval.",
|
|
111
|
-
shortResult: "Plan mode entered",
|
|
112
|
-
};
|
|
113
|
-
} catch (error) {
|
|
114
|
-
const errorMessage =
|
|
115
|
-
error instanceof Error ? error.message : String(error);
|
|
116
|
-
logger.error(`EnterPlanMode tool error: ${errorMessage}`);
|
|
117
|
-
return {
|
|
118
|
-
success: false,
|
|
119
|
-
content: "",
|
|
120
|
-
error: errorMessage,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
},
|
|
124
|
-
};
|