@sayknow-cli/coding-agent 0.3.1 → 0.3.4
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/bin/skc.js +4 -0
- package/dist/types/cli/mcp-cli.d.ts +25 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/cli.d.ts +6 -0
- package/dist/types/commands/mcp.d.ts +70 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/config/model-profiles.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/models-config-schema.d.ts +0 -5
- package/dist/types/config/settings-schema.d.ts +79 -68
- package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
- package/dist/types/i18n/messages/en.d.ts +0 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/model-selector.d.ts +8 -0
- package/dist/types/modes/components/settings-defs.d.ts +0 -1
- package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
- package/dist/types/modes/theme/defaults/index.d.ts +99 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/operator-runtime.d.ts +52 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +114 -16
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +12 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
- package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
- package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
- package/dist/types/tools/composer-bash-policy.d.ts +14 -0
- package/dist/types/tools/computer-gc.d.ts +23 -0
- package/dist/types/tools/cron.d.ts +36 -61
- package/dist/types/tools/index.d.ts +3 -2
- package/dist/types/tools/resource-gc.d.ts +54 -0
- package/dist/types/utils/changelog.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +6 -3
- package/dist/types/web/scrapers/types.d.ts +5 -0
- package/dist/types/web/scrapers/utils.d.ts +7 -1
- package/dist/types/web/search/index.d.ts +1 -0
- package/dist/types/web/search/providers/utils.d.ts +11 -4
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +180 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/args.ts +0 -1
- package/src/cli/fast-help.ts +29 -2
- package/src/cli/mcp-cli.ts +272 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli/web-search-cli.ts +5 -0
- package/src/cli.ts +30 -12
- package/src/commands/mcp.ts +117 -0
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +62 -8
- package/src/config/model-profiles.ts +3 -4
- package/src/config/model-registry.ts +3 -6
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +88 -103
- package/src/deep-interview/plaintext-gate-guard.ts +94 -0
- package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
- package/src/defaults/skc/skills/team/SKILL.md +5 -3
- package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +0 -12
- package/src/extensibility/extensions/runner.ts +1 -0
- package/src/extensibility/skc-plugins/compiler.ts +351 -0
- package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/skc-plugins/index.ts +9 -0
- package/src/extensibility/skc-plugins/injection.ts +109 -0
- package/src/extensibility/skc-plugins/installer.ts +434 -0
- package/src/extensibility/skc-plugins/loader.ts +3 -1
- package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/skc-plugins/observability.ts +84 -0
- package/src/extensibility/skc-plugins/paths.ts +1 -1
- package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/skc-plugins/registry.ts +180 -0
- package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/skc-plugins/schema.ts +250 -20
- package/src/extensibility/skc-plugins/session-validation.ts +147 -0
- package/src/extensibility/skc-plugins/types.ts +199 -3
- package/src/extensibility/skc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/goals/tools/goal-tool.ts +14 -1
- package/src/hooks/skill-state.ts +57 -0
- package/src/i18n/messages/de.ts +0 -1
- package/src/i18n/messages/en.ts +0 -1
- package/src/i18n/messages/es.ts +0 -1
- package/src/i18n/messages/fr.ts +0 -1
- package/src/i18n/messages/ja.ts +0 -1
- package/src/i18n/messages/ko.ts +0 -1
- package/src/i18n/messages/zh.ts +0 -1
- package/src/internal-urls/docs-index.generated.ts +14 -11
- package/src/main.ts +14 -3
- package/src/modes/bridge/bridge-mode.ts +11 -0
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/custom-editor.ts +2 -0
- package/src/modes/components/footer.ts +2 -3
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +65 -12
- package/src/modes/components/settings-defs.ts +1 -2
- package/src/modes/components/settings-selector.ts +2 -7
- package/src/modes/components/status-line/git-utils.ts +25 -0
- package/src/modes/components/status-line.ts +10 -11
- package/src/modes/components/welcome.ts +2 -3
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +59 -11
- package/src/modes/interactive-mode.ts +4 -1
- package/src/modes/shared/agent-wire/scopes.ts +1 -1
- package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
- package/src/modes/theme/defaults/index.ts +2 -0
- package/src/modes/theme/theme.ts +0 -4
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +238 -0
- package/src/notifications/lifecycle-control-runtime.ts +405 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/operator-runtime.ts +171 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +778 -257
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +23 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/prompts/system/system-prompt.md +2 -2
- package/src/prompts/tools/cron.md +5 -3
- package/src/prompts/tools/read.md +1 -1
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +162 -10
- package/src/session/agent-session.ts +210 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +762 -12
- package/src/skc-runtime/launch-tmux.ts +68 -19
- package/src/skc-runtime/psmux-detect.ts +239 -0
- package/src/skc-runtime/state-renderer.ts +13 -0
- package/src/skc-runtime/team-runtime.ts +56 -23
- package/src/skc-runtime/tmux-common.ts +88 -4
- package/src/skc-runtime/tmux-sessions.ts +111 -9
- package/src/skc-runtime/ultragoal-guard.ts +192 -8
- package/src/skc-runtime/ultragoal-runtime.ts +296 -16
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/slash-commands/helpers/parse.ts +2 -1
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/bash.ts +9 -0
- package/src/tools/browser/tab-supervisor.ts +86 -2
- package/src/tools/composer-bash-policy.ts +96 -0
- package/src/tools/computer-gc.ts +66 -0
- package/src/tools/computer.ts +2 -0
- package/src/tools/cron.ts +75 -112
- package/src/tools/fetch.ts +18 -2
- package/src/tools/index.ts +5 -9
- package/src/tools/read.ts +25 -55
- package/src/tools/renderers.ts +0 -2
- package/src/tools/resource-gc.ts +291 -0
- package/src/tools/ultragoal-ask-guard.ts +7 -1
- package/src/utils/changelog.ts +8 -0
- package/src/web/insane/url-guard.ts +18 -14
- package/src/web/scrapers/types.ts +143 -45
- package/src/web/scrapers/utils.ts +70 -19
- package/src/web/search/index.ts +1 -0
- package/src/web/search/providers/utils.ts +22 -5
- package/vendor/insane-search/MANIFEST.json +3 -1
- package/vendor/insane-search/engine/__init__.py +14 -0
- package/vendor/insane-search/engine/content_safety.py +151 -0
- package/vendor/insane-search/engine/fetch_chain.py +32 -0
- package/vendor/insane-search/engine/tests/test_u8.py +216 -0
- package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
- package/dist/types/tools/inspect-image.d.ts +0 -31
- package/src/prompts/tools/inspect-image-system.md +0 -20
- package/src/prompts/tools/inspect-image.md +0 -32
- package/src/tools/inspect-image-renderer.ts +0 -103
- package/src/tools/inspect-image.ts +0 -172
package/src/tools/ask.ts
CHANGED
|
@@ -260,7 +260,11 @@ async function askSingleQuestion(
|
|
|
260
260
|
? "up/down navigate enter select ←/→ question esc cancel"
|
|
261
261
|
: "up/down navigate enter select esc cancel";
|
|
262
262
|
const helpText =
|
|
263
|
-
scrollTitleRows === undefined
|
|
263
|
+
scrollTitleRows === undefined
|
|
264
|
+
? baseHelpText
|
|
265
|
+
: navigation
|
|
266
|
+
? "↑/↓ select enter ←/→ question esc PgUp/PgDn/Ctrl+u/d: question · Wheel: transcript"
|
|
267
|
+
: "↑/↓ select enter esc PgUp/PgDn/Ctrl+u/d: question · Wheel: transcript";
|
|
264
268
|
const dialogOptions = {
|
|
265
269
|
initialIndex,
|
|
266
270
|
timeout,
|
package/src/tools/bash.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-intera
|
|
|
25
25
|
import { checkBashInterception } from "./bash-interceptor";
|
|
26
26
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
27
27
|
import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
|
|
28
|
+
import { checkComposerBashPolicy } from "./composer-bash-policy";
|
|
28
29
|
import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
|
|
29
30
|
import { resolveToCwd } from "./path-utils";
|
|
30
31
|
import { formatToolWorkingDirectory, replaceTabs } from "./render-utils";
|
|
@@ -570,6 +571,14 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
573
|
|
|
574
|
+
const composerPolicy = checkComposerBashPolicy({
|
|
575
|
+
modelId: this.session.getActiveModelString?.() ?? this.session.getModelString?.() ?? this.session.model?.id,
|
|
576
|
+
commands: rawCommand === command ? [command] : [rawCommand, command],
|
|
577
|
+
});
|
|
578
|
+
if (!composerPolicy.allowed) {
|
|
579
|
+
throw new ToolError(composerPolicy.message);
|
|
580
|
+
}
|
|
581
|
+
|
|
573
582
|
const internalUrlOptions: InternalUrlExpansionOptions = {
|
|
574
583
|
skills: this.session.skills ?? [],
|
|
575
584
|
internalRouter: InternalUrlRouter.instance(),
|
|
@@ -57,6 +57,10 @@ export interface TabSession {
|
|
|
57
57
|
kindTag: BrowserKindTag;
|
|
58
58
|
/** Session that acquired this tab; used for session-scoped teardown (F13). */
|
|
59
59
|
ownerId?: string;
|
|
60
|
+
/** Unix-ms timestamp of the last acquire/run activity; drives GC idle + LRU ordering. */
|
|
61
|
+
lastUsedAt: number;
|
|
62
|
+
/** Set synchronously by the shared begin-release guard so concurrent release is a no-op. */
|
|
63
|
+
releasing?: boolean;
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
export interface AcquireTabOptions {
|
|
@@ -94,6 +98,76 @@ export function getTab(name: string): TabSession | undefined {
|
|
|
94
98
|
return tabs.get(name);
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Shared synchronous begin-release guard. Returns true for the first caller (which then
|
|
103
|
+
* owns teardown) and false for any concurrent/repeat caller, so `releaseBrowser` and the
|
|
104
|
+
* `BrowserHandle.refCount` decrement happen exactly once even when the idle sweep, the RSS
|
|
105
|
+
* sweep, a manual close, and `forceKillTab` race on the same tab.
|
|
106
|
+
*/
|
|
107
|
+
function beginRelease(tab: TabSession): boolean {
|
|
108
|
+
if (tab.releasing) return false;
|
|
109
|
+
tab.releasing = true;
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Read-only, GC-facing projection of a live tab. Never exposes the mutable `tabs` map. */
|
|
114
|
+
export interface TabGcSnapshot {
|
|
115
|
+
name: string;
|
|
116
|
+
ownerId?: string;
|
|
117
|
+
state: TabSession["state"];
|
|
118
|
+
pendingCount: number;
|
|
119
|
+
kindTag: BrowserKindTag;
|
|
120
|
+
lastUsedAt: number;
|
|
121
|
+
browserRefCount: number;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface BrowserGcEligibilityPolicy {
|
|
125
|
+
now: () => number;
|
|
126
|
+
idleMs: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Snapshot every live tab for GC ordering/diagnostics. */
|
|
130
|
+
export function listTabsForGc(): TabGcSnapshot[] {
|
|
131
|
+
return [...tabs.values()].map(tab => ({
|
|
132
|
+
name: tab.name,
|
|
133
|
+
ownerId: tab.ownerId,
|
|
134
|
+
state: tab.state,
|
|
135
|
+
pendingCount: tab.pending.size,
|
|
136
|
+
kindTag: tab.kindTag,
|
|
137
|
+
lastUsedAt: tab.lastUsedAt,
|
|
138
|
+
browserRefCount: tab.browser.refCount,
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Evict a tab only if it is still GC-eligible against the LIVE supervisor state. The full
|
|
144
|
+
* predicate is checked synchronously and `releaseTab` is invoked with no intervening await,
|
|
145
|
+
* so a tab that became busy after a GC snapshot cannot be closed (its run rejected). Returns
|
|
146
|
+
* true only when the tab was actually released.
|
|
147
|
+
*/
|
|
148
|
+
export async function releaseTabIfGcEligible(name: string, policy: BrowserGcEligibilityPolicy): Promise<boolean> {
|
|
149
|
+
const tab = tabs.get(name);
|
|
150
|
+
if (!tab) return false;
|
|
151
|
+
if (tab.releasing) return false;
|
|
152
|
+
if (tab.state !== "alive") return false;
|
|
153
|
+
if (tab.pending.size !== 0) return false;
|
|
154
|
+
if (tab.kindTag !== "headless" && tab.kindTag !== "spawned") return false;
|
|
155
|
+
if (policy.now() - tab.lastUsedAt <= policy.idleMs) return false;
|
|
156
|
+
// All predicates passed synchronously; releaseTab applies the shared begin-release guard
|
|
157
|
+
// on the same microtask, so no other code runs between the check and the state transition.
|
|
158
|
+
return await releaseTab(name, { kill: false });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Test-only: install a fabricated tab session. */
|
|
162
|
+
export function setTabForTest(tab: TabSession): void {
|
|
163
|
+
tabs.set(tab.name, tab);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Test-only: clear the tab registry between cases. */
|
|
167
|
+
export function clearTabsForTest(): void {
|
|
168
|
+
tabs.clear();
|
|
169
|
+
}
|
|
170
|
+
|
|
97
171
|
export async function acquireTab(
|
|
98
172
|
name: string,
|
|
99
173
|
browser: BrowserHandle,
|
|
@@ -105,6 +179,7 @@ export async function acquireTab(
|
|
|
105
179
|
if (opts.dialogs !== undefined && opts.dialogs !== existing.dialogPolicy) {
|
|
106
180
|
await releaseTab(name, { kill: false });
|
|
107
181
|
} else {
|
|
182
|
+
existing.lastUsedAt = Date.now();
|
|
108
183
|
if (opts.url) {
|
|
109
184
|
await runInTabWithSnapshot(
|
|
110
185
|
name,
|
|
@@ -166,6 +241,7 @@ export async function acquireTab(
|
|
|
166
241
|
dialogPolicy: opts.dialogs,
|
|
167
242
|
kindTag: browser.kind.kind,
|
|
168
243
|
ownerId: opts.ownerId,
|
|
244
|
+
lastUsedAt: Date.now(),
|
|
169
245
|
};
|
|
170
246
|
worker.onMessage(msg => handleTabMessage(tab, msg));
|
|
171
247
|
tabs.set(name, tab);
|
|
@@ -188,6 +264,7 @@ async function runInTabWithSnapshot(
|
|
|
188
264
|
const tab = tabs.get(name);
|
|
189
265
|
if (!tab || tab.state === "dead") throw new ToolError(`Tab ${JSON.stringify(name)} is not alive. Reopen it.`);
|
|
190
266
|
if (tab.pending.size > 0) throw new ToolError(`Tab ${JSON.stringify(name)} is busy`);
|
|
267
|
+
tab.lastUsedAt = Date.now();
|
|
191
268
|
const id = Snowflake.next();
|
|
192
269
|
const { promise, resolve, reject } = Promise.withResolvers<RunResultOk>();
|
|
193
270
|
const pending: PendingRun = {
|
|
@@ -224,6 +301,10 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
224
301
|
logger.debug("releaseTab: unknown tab", { name });
|
|
225
302
|
return false;
|
|
226
303
|
}
|
|
304
|
+
if (!beginRelease(tab)) {
|
|
305
|
+
logger.debug("releaseTab: already releasing", { name });
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
227
308
|
const wasAlive = tab.state === "alive";
|
|
228
309
|
tab.state = "dead";
|
|
229
310
|
const closeError = new ToolError(`Tab ${JSON.stringify(name)} was closed`);
|
|
@@ -246,7 +327,9 @@ export async function releaseTab(name: string, opts: ReleaseTabOptions = {}): Pr
|
|
|
246
327
|
await tab.worker.terminate().catch(() => undefined);
|
|
247
328
|
if (forced && tab.kindTag === "headless") await closeOrphanTarget(tab);
|
|
248
329
|
await releaseBrowser(tab.browser, { kill: opts.kill ?? false });
|
|
249
|
-
|
|
330
|
+
// Only delete if the map still holds THIS tab: a same-name reacquire during our async
|
|
331
|
+
// teardown may have installed a fresh tab that we must not evict.
|
|
332
|
+
if (tabs.get(name) === tab) tabs.delete(name);
|
|
250
333
|
return true;
|
|
251
334
|
}
|
|
252
335
|
|
|
@@ -392,6 +475,7 @@ function toErrorPayload(error: unknown): RunErrorPayload {
|
|
|
392
475
|
async function forceKillTab(name: string, reason: string): Promise<void> {
|
|
393
476
|
const tab = tabs.get(name);
|
|
394
477
|
if (!tab) return;
|
|
478
|
+
if (!beginRelease(tab)) return;
|
|
395
479
|
tab.state = "dead";
|
|
396
480
|
const error = new ToolError(reason);
|
|
397
481
|
for (const pending of tab.pending.values()) pending.reject(error);
|
|
@@ -399,7 +483,7 @@ async function forceKillTab(name: string, reason: string): Promise<void> {
|
|
|
399
483
|
await tab.worker.terminate().catch(() => undefined);
|
|
400
484
|
if (tab.kindTag === "headless") await closeOrphanTarget(tab);
|
|
401
485
|
await releaseBrowser(tab.browser, { kill: false });
|
|
402
|
-
tabs.delete(name);
|
|
486
|
+
if (tabs.get(name) === tab) tabs.delete(name);
|
|
403
487
|
}
|
|
404
488
|
|
|
405
489
|
async function closeOrphanTarget(tab: TabSession): Promise<void> {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { isComposerHarnessModel } from "@sayknow-cli/ai/providers/composer-discipline";
|
|
2
|
+
|
|
3
|
+
export const COMPOSER_BASH_POLICY_ERROR =
|
|
4
|
+
"Composer bash policy blocked repository file I/O. Use find, search, read, and edit tools for file discovery, file inspection, and file mutation.";
|
|
5
|
+
|
|
6
|
+
type ComposerBashPolicyResult =
|
|
7
|
+
| { allowed: true }
|
|
8
|
+
| {
|
|
9
|
+
allowed: false;
|
|
10
|
+
reason: string;
|
|
11
|
+
message: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const BLOCK_PATTERNS: Array<{ id: string; pattern: RegExp }> = [
|
|
15
|
+
{ id: "pipe", pattern: /\|/ },
|
|
16
|
+
{ id: "process-substitution", pattern: /<[>(]/ },
|
|
17
|
+
{ id: "heredoc", pattern: /<<[-~]?/ },
|
|
18
|
+
{ id: "command-substitution", pattern: /\$\(|`/ },
|
|
19
|
+
{ id: "redirection", pattern: /(^|[^<>])(?:>>?|<)(?!=)/ },
|
|
20
|
+
{ id: "tee", pattern: /(?:^|[;&|\s])tee(?:\s|$)/ },
|
|
21
|
+
{
|
|
22
|
+
id: "shell-file-read-discovery",
|
|
23
|
+
pattern: /(?:^|[;&|()\s])(?:\S*\/)?(?:cat|head|tail|less|more|grep|rg|find|fd|tree|ls)\b/,
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "shell-file-mutation",
|
|
27
|
+
pattern: /(?:^|[;&|()\s])(?:\S*\/)?(?:cp|mv|rm|touch|mkdir|chmod|chown|ln)\b/,
|
|
28
|
+
},
|
|
29
|
+
{ id: "sed-print", pattern: /(?:^|[;&|()\s])sed\s+(?:-[^\s]*n\b|.*\bp\b)/ },
|
|
30
|
+
{ id: "awk-print", pattern: /(?:^|[;&|()\s])awk\b/ },
|
|
31
|
+
{ id: "git-ls-files", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+ls-files\b/ },
|
|
32
|
+
{ id: "git-grep", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+grep\b/ },
|
|
33
|
+
{ id: "git-show-path", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+show\s+\S+:\S+/ },
|
|
34
|
+
{ id: "git-diff", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+diff(?:\s|$)/ },
|
|
35
|
+
{ id: "git-cat-file", pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+cat-file\b/ },
|
|
36
|
+
{
|
|
37
|
+
id: "git-show-discovery",
|
|
38
|
+
pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+show\b.*(?:--name-only|--name-status|--stat)/,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "git-log-path-discovery",
|
|
42
|
+
pattern: /(?:^|[;&|()\s])git(?:\s+-C\s+\S+)?\s+log\b.*(?:--name-only|--name-status|--stat)/,
|
|
43
|
+
},
|
|
44
|
+
{ id: "sed-in-place", pattern: /(?:^|[;&|()\s])sed\s+-[^\s]*i\b/ },
|
|
45
|
+
{ id: "perl-in-place", pattern: /(?:^|[;&|()\s])perl\s+-[^\s]*p[^\s]*i\b/ },
|
|
46
|
+
{
|
|
47
|
+
id: "script-file-io",
|
|
48
|
+
pattern:
|
|
49
|
+
/(?:^|[;&|()\s])(?:python3?|node|bun)\s+(?:-\s*<<|-c\b|-e\b|--eval\b).*?(?:read_text|read_bytes|write_text|iterdir|listdir|glob\.glob|readFile|readFileSync|writeFile|writeFileSync|readdir|readdirSync|stat|statSync|cpSync|rmSync|mkdirSync|createReadStream|createWriteStream|Bun\.file|Bun\.write|fs\.readFile|fs\.writeFile|fs\.readdir|fs\.stat|fs\.cp|fs\.rm|fs\.mkdir|open\s*\()/s,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "contaminated-command",
|
|
53
|
+
pattern: /```|^\s*(?:I\s+(?:will|need|am going)|We\s+(?:need|will)|First[, ]|Now[, ]|Let's)\b/im,
|
|
54
|
+
},
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const ALLOWED_TERMINAL_PATTERNS: RegExp[] = [
|
|
58
|
+
/^bun\s+test(?:\s+[\w./:@=-]+)*$/,
|
|
59
|
+
/^bun\s+run\s+(?:check(?::[\w-]+)?|test(?::[\w-]+)?|build(?::[\w-]+)?)(?:\s+[\w./:@=-]+)*$/,
|
|
60
|
+
/^bun\s+--version$/,
|
|
61
|
+
/^mise\s+x\s+bun@\d+\.\d+\.\d+\s+--\s+bun\s+test(?:\s+[\w./:@=-]+)*$/,
|
|
62
|
+
/^mise\s+x\s+bun@\d+\.\d+\.\d+\s+--\s+bun\s+run\s+(?:check(?::[\w-]+)?|test(?::[\w-]+)?|build(?::[\w-]+)?)(?:\s+[\w./:@=-]+)*$/,
|
|
63
|
+
/^cargo\s+(?:test|check|build)(?:\s+[\w./:@=-]+)*$/,
|
|
64
|
+
/^git\s+status(?:\s+--short)?(?:\s+--branch)?$/,
|
|
65
|
+
/^git\s+rev-parse\s+HEAD$/,
|
|
66
|
+
/^npm\s+--version$/,
|
|
67
|
+
/^pnpm\s+--version$/,
|
|
68
|
+
/^yarn\s+--version$/,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function isAllowedComposerTerminalCommand(command: string): boolean {
|
|
72
|
+
const normalized = command.trim().replace(/\s+/g, " ");
|
|
73
|
+
return ALLOWED_TERMINAL_PATTERNS.some(pattern => pattern.test(normalized));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function isComposerBashPolicyModel(modelId: string | undefined): boolean {
|
|
77
|
+
return Boolean(modelId && isComposerHarnessModel(modelId));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function checkComposerBashPolicy(input: {
|
|
81
|
+
modelId?: string;
|
|
82
|
+
commands: readonly string[];
|
|
83
|
+
}): ComposerBashPolicyResult {
|
|
84
|
+
if (!isComposerBashPolicyModel(input.modelId)) return { allowed: true };
|
|
85
|
+
for (const command of input.commands) {
|
|
86
|
+
for (const block of BLOCK_PATTERNS) {
|
|
87
|
+
if (block.pattern.test(command)) {
|
|
88
|
+
return { allowed: false, reason: block.id, message: COMPOSER_BASH_POLICY_ERROR };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!isAllowedComposerTerminalCommand(command)) {
|
|
92
|
+
return { allowed: false, reason: "not-allowlisted", message: COMPOSER_BASH_POLICY_ERROR };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { allowed: true };
|
|
96
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { logger } from "@sayknow-cli/utils";
|
|
6
|
+
|
|
7
|
+
/** Prefix of every computer-use screenshot fallback directory created under the OS temp dir. */
|
|
8
|
+
export const SCREENSHOT_FALLBACK_DIR_PREFIX = "skc-computer-screenshots-";
|
|
9
|
+
|
|
10
|
+
let screenshotFallbackDirsCreated = false;
|
|
11
|
+
|
|
12
|
+
/** Lazy-arm marker: called when the first screenshot fallback dir is created this process. */
|
|
13
|
+
export function markScreenshotFallbackDirCreatedForGc(): void {
|
|
14
|
+
screenshotFallbackDirsCreated = true;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Whether any screenshot fallback dir has been created this process (GC lazy-arm gate). */
|
|
18
|
+
export function hasCreatedScreenshotFallbackDir(): boolean {
|
|
19
|
+
return screenshotFallbackDirsCreated;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Test-only: reset the lazy-arm marker between cases. */
|
|
23
|
+
export function resetScreenshotFallbackGcForTest(): void {
|
|
24
|
+
screenshotFallbackDirsCreated = false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ScreenshotGcOptions {
|
|
28
|
+
now: () => number;
|
|
29
|
+
staleMs: number;
|
|
30
|
+
/** Base dir to scan; defaults to os.tmpdir(). Injectable for tests. */
|
|
31
|
+
tmpDir?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Disk-only GC for stale computer-use screenshot fallback directories. Scans the temp dir for
|
|
36
|
+
* `skc-computer-screenshots-*` directories and removes those whose mtime is older than `staleMs`.
|
|
37
|
+
* Never throws on a per-directory failure; the whole sweep is best-effort.
|
|
38
|
+
*/
|
|
39
|
+
export async function cleanupStaleScreenshotFallbackDirs(
|
|
40
|
+
options: ScreenshotGcOptions,
|
|
41
|
+
): Promise<{ scanned: number; removed: number }> {
|
|
42
|
+
const base = options.tmpDir ?? os.tmpdir();
|
|
43
|
+
const entries: Dirent[] = await fs.readdir(base, { withFileTypes: true }).catch(err => {
|
|
44
|
+
logger.debug("screenshot GC: failed to read temp dir", { base, error: (err as Error).message });
|
|
45
|
+
return [] as Dirent[];
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const now = options.now();
|
|
49
|
+
let scanned = 0;
|
|
50
|
+
let removed = 0;
|
|
51
|
+
for (const entry of entries) {
|
|
52
|
+
if (!entry.isDirectory()) continue;
|
|
53
|
+
if (!entry.name.startsWith(SCREENSHOT_FALLBACK_DIR_PREFIX)) continue;
|
|
54
|
+
scanned++;
|
|
55
|
+
const dir = path.join(base, entry.name);
|
|
56
|
+
try {
|
|
57
|
+
const stat = await fs.stat(dir);
|
|
58
|
+
if (now - stat.mtimeMs <= options.staleMs) continue;
|
|
59
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
60
|
+
removed++;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
logger.debug("screenshot GC: failed to remove stale dir", { dir, error: (err as Error).message });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { scanned, removed };
|
|
66
|
+
}
|
package/src/tools/computer.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { prompt } from "@sayknow-cli/utils";
|
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
8
|
import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
|
|
9
9
|
import { resizeImage } from "../utils/image-resize";
|
|
10
|
+
import { markScreenshotFallbackDirCreatedForGc } from "./computer-gc";
|
|
10
11
|
import type { ToolSession } from "./index";
|
|
11
12
|
import type { OutputMeta } from "./output-meta";
|
|
12
13
|
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
@@ -547,6 +548,7 @@ function getScreenshotFallbackDir(session: ToolSession): Promise<string> {
|
|
|
547
548
|
async function createScreenshotFallbackDir(): Promise<string> {
|
|
548
549
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "skc-computer-screenshots-"));
|
|
549
550
|
await fs.chmod(dir, 0o700);
|
|
551
|
+
markScreenshotFallbackDirCreatedForGc();
|
|
550
552
|
return dir;
|
|
551
553
|
}
|
|
552
554
|
|
package/src/tools/cron.ts
CHANGED
|
@@ -19,34 +19,36 @@ const MAX_RECURRING_JITTER_MS = 30 * 60 * 1000;
|
|
|
19
19
|
const MAX_ONE_SHOT_EARLY_JITTER_MS = 90 * 1000;
|
|
20
20
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
})
|
|
49
|
-
|
|
22
|
+
const cronSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
op: z
|
|
25
|
+
.enum(["create", "list", "delete"])
|
|
26
|
+
.describe(
|
|
27
|
+
"operation: 'create' schedules a prompt on a cron expression, 'list' enumerates scheduled tasks, 'delete' cancels a task by id",
|
|
28
|
+
),
|
|
29
|
+
cron_expression: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
"(op=create, required) Standard 5-field cron expression in the user's local timezone: 'minute hour day-of-month month day-of-week'. Examples: '*/5 * * * *' (every 5 min), '0 9 * * *' (9am daily), '0 9 * * 1-5' (weekdays at 9am). Day-of-week uses 0/7 for Sunday through 6 for Saturday. When both day-of-month and day-of-week are constrained, a date matches if either field matches (vixie-cron semantics).",
|
|
34
|
+
),
|
|
35
|
+
prompt: z
|
|
36
|
+
.string()
|
|
37
|
+
.optional()
|
|
38
|
+
.describe(
|
|
39
|
+
"(op=create, required) Prompt to inject between turns when the cron fires. May reference slash commands (e.g. '/review-pr 1234') or natural-language instructions.",
|
|
40
|
+
),
|
|
41
|
+
recurring: z
|
|
42
|
+
.boolean()
|
|
43
|
+
.optional()
|
|
44
|
+
.describe(
|
|
45
|
+
"(op=create) true to fire on every match of the cron expression (recurring, auto-expires after 7 days); false to fire once at the next match and then self-delete.",
|
|
46
|
+
),
|
|
47
|
+
id: z.string().optional().describe("(op=delete, required) The 8-character job ID returned by op=create."),
|
|
48
|
+
})
|
|
49
|
+
.strict();
|
|
50
|
+
|
|
51
|
+
export type CronParams = z.infer<typeof cronSchema>;
|
|
50
52
|
|
|
51
53
|
export interface CronJobSnapshot {
|
|
52
54
|
id: string;
|
|
@@ -68,20 +70,14 @@ export interface CronListJobDetails {
|
|
|
68
70
|
humanSchedule: string;
|
|
69
71
|
}
|
|
70
72
|
|
|
71
|
-
export interface
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
export interface CronToolDetails {
|
|
74
|
+
op: "create" | "list" | "delete";
|
|
75
|
+
id?: string;
|
|
76
|
+
cron_expression?: string;
|
|
77
|
+
recurring?: boolean;
|
|
75
78
|
nextFireAt?: number;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export interface CronListToolDetails {
|
|
79
|
-
jobs: CronListJobDetails[];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export interface CronDeleteToolDetails {
|
|
83
|
-
id: string;
|
|
84
|
-
deleted: boolean;
|
|
79
|
+
jobs?: CronListJobDetails[];
|
|
80
|
+
deleted?: boolean;
|
|
85
81
|
}
|
|
86
82
|
|
|
87
83
|
interface CronTimerHandle {
|
|
@@ -543,12 +539,12 @@ function isCronDisabled(): boolean {
|
|
|
543
539
|
return process.env.CLAUDE_CODE_DISABLE_CRON === "1";
|
|
544
540
|
}
|
|
545
541
|
|
|
546
|
-
export class
|
|
547
|
-
readonly name = "
|
|
548
|
-
readonly label = "
|
|
549
|
-
readonly summary = "Schedule
|
|
542
|
+
export class CronTool implements AgentTool<typeof cronSchema, CronToolDetails> {
|
|
543
|
+
readonly name = "cron";
|
|
544
|
+
readonly label = "Cron";
|
|
545
|
+
readonly summary = "Schedule, list, and cancel cron-style prompts (op: create | list | delete)";
|
|
550
546
|
readonly description: string;
|
|
551
|
-
readonly parameters =
|
|
547
|
+
readonly parameters = cronSchema;
|
|
552
548
|
readonly strict = true;
|
|
553
549
|
readonly loadMode = "discoverable";
|
|
554
550
|
|
|
@@ -556,19 +552,30 @@ export class CronCreateTool implements AgentTool<typeof cronCreateSchema, CronCr
|
|
|
556
552
|
this.description = prompt.render(cronDescription);
|
|
557
553
|
}
|
|
558
554
|
|
|
559
|
-
static createIf(session: ToolSession):
|
|
555
|
+
static createIf(session: ToolSession): CronTool | null {
|
|
560
556
|
if (!isBackgroundJobSupportEnabled(session.settings)) return null;
|
|
561
557
|
if (isCronDisabled()) return null;
|
|
562
|
-
return new
|
|
558
|
+
return new CronTool(session);
|
|
563
559
|
}
|
|
564
560
|
|
|
565
561
|
async execute(
|
|
566
562
|
_toolCallId: string,
|
|
567
|
-
params:
|
|
563
|
+
params: CronParams,
|
|
568
564
|
_signal?: AbortSignal,
|
|
569
|
-
_onUpdate?: AgentToolUpdateCallback<
|
|
565
|
+
_onUpdate?: AgentToolUpdateCallback<CronToolDetails>,
|
|
570
566
|
_context?: AgentToolContext,
|
|
571
|
-
): Promise<AgentToolResult<
|
|
567
|
+
): Promise<AgentToolResult<CronToolDetails>> {
|
|
568
|
+
switch (params.op) {
|
|
569
|
+
case "create":
|
|
570
|
+
return this.#create(params);
|
|
571
|
+
case "list":
|
|
572
|
+
return this.#list();
|
|
573
|
+
case "delete":
|
|
574
|
+
return this.#delete(params);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async #create(params: CronParams): Promise<AgentToolResult<CronToolDetails>> {
|
|
572
579
|
const manager = AsyncJobManager.instance();
|
|
573
580
|
if (!manager) {
|
|
574
581
|
throw new ToolError("Async execution is disabled; cron is unavailable in this session.");
|
|
@@ -576,26 +583,30 @@ export class CronCreateTool implements AgentTool<typeof cronCreateSchema, CronCr
|
|
|
576
583
|
if (isCronDisabled()) {
|
|
577
584
|
throw new ToolError("Cron is disabled by CLAUDE_CODE_DISABLE_CRON=1.");
|
|
578
585
|
}
|
|
586
|
+
if (!params.cron_expression || !params.prompt) {
|
|
587
|
+
throw new ToolError("cron op=create requires both 'cron_expression' and 'prompt'.");
|
|
588
|
+
}
|
|
579
589
|
validateCronExpression(params.cron_expression);
|
|
580
590
|
|
|
581
591
|
const ownerId = this.session.getAgentId?.() ?? undefined;
|
|
582
592
|
const state = getOrCreateOwnerState(ownerId);
|
|
583
593
|
if (state.jobs.size >= MAX_CRON_TASKS_PER_OWNER) {
|
|
584
594
|
throw new ToolError(
|
|
585
|
-
`Cron task limit reached (${MAX_CRON_TASKS_PER_OWNER}). Cancel an existing task with
|
|
595
|
+
`Cron task limit reached (${MAX_CRON_TASKS_PER_OWNER}). Cancel an existing task with cron op=delete first.`,
|
|
586
596
|
);
|
|
587
597
|
}
|
|
588
598
|
ensureOwnerCleanup(ownerId, manager, state);
|
|
589
599
|
|
|
590
600
|
const id = generateCronId(new Set(state.jobs.keys()));
|
|
591
601
|
const now = Date.now();
|
|
602
|
+
const recurring = params.recurring ?? true;
|
|
592
603
|
const snapshot: CronJobSnapshot = {
|
|
593
604
|
id,
|
|
594
605
|
cron_expression: params.cron_expression.trim(),
|
|
595
606
|
prompt: params.prompt,
|
|
596
|
-
recurring
|
|
607
|
+
recurring,
|
|
597
608
|
createdAt: now,
|
|
598
|
-
expiresAt:
|
|
609
|
+
expiresAt: recurring ? now + CRON_RECURRING_MAX_AGE_MS : undefined,
|
|
599
610
|
humanSchedule: humanizeCronExpression(params.cron_expression.trim()),
|
|
600
611
|
ownerId,
|
|
601
612
|
};
|
|
@@ -604,7 +615,7 @@ export class CronCreateTool implements AgentTool<typeof cronCreateSchema, CronCr
|
|
|
604
615
|
scheduleExpiry(ownerId, record);
|
|
605
616
|
scheduleRecord(ownerId, state, record);
|
|
606
617
|
|
|
607
|
-
logger.debug("
|
|
618
|
+
logger.debug("cron op=create: scheduled task", {
|
|
608
619
|
id,
|
|
609
620
|
ownerId,
|
|
610
621
|
cron: snapshot.cron_expression,
|
|
@@ -614,6 +625,7 @@ export class CronCreateTool implements AgentTool<typeof cronCreateSchema, CronCr
|
|
|
614
625
|
return {
|
|
615
626
|
content: [{ type: "text", text: `Scheduled ${id} (${snapshot.humanSchedule})` }],
|
|
616
627
|
details: {
|
|
628
|
+
op: "create",
|
|
617
629
|
id,
|
|
618
630
|
cron_expression: snapshot.cron_expression,
|
|
619
631
|
recurring: snapshot.recurring,
|
|
@@ -621,34 +633,8 @@ export class CronCreateTool implements AgentTool<typeof cronCreateSchema, CronCr
|
|
|
621
633
|
},
|
|
622
634
|
};
|
|
623
635
|
}
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
export class CronListTool implements AgentTool<typeof cronListSchema, CronListToolDetails> {
|
|
627
|
-
readonly name = "CronList";
|
|
628
|
-
readonly label = "CronList";
|
|
629
|
-
readonly summary = "List scheduled cron jobs";
|
|
630
|
-
readonly description: string;
|
|
631
|
-
readonly parameters = cronListSchema;
|
|
632
|
-
readonly strict = true;
|
|
633
|
-
readonly loadMode = "discoverable";
|
|
634
|
-
|
|
635
|
-
constructor(private readonly session: ToolSession) {
|
|
636
|
-
this.description = prompt.render(cronDescription);
|
|
637
|
-
}
|
|
638
636
|
|
|
639
|
-
|
|
640
|
-
if (!isBackgroundJobSupportEnabled(session.settings)) return null;
|
|
641
|
-
if (isCronDisabled()) return null;
|
|
642
|
-
return new CronListTool(session);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
async execute(
|
|
646
|
-
_toolCallId: string,
|
|
647
|
-
_params: CronListParams,
|
|
648
|
-
_signal?: AbortSignal,
|
|
649
|
-
_onUpdate?: AgentToolUpdateCallback<CronListToolDetails>,
|
|
650
|
-
_context?: AgentToolContext,
|
|
651
|
-
): Promise<AgentToolResult<CronListToolDetails>> {
|
|
637
|
+
async #list(): Promise<AgentToolResult<CronToolDetails>> {
|
|
652
638
|
const ownerId = this.session.getAgentId?.() ?? undefined;
|
|
653
639
|
const state = schedulesByOwner.get(ownerKey(ownerId));
|
|
654
640
|
const records = state
|
|
@@ -658,7 +644,7 @@ export class CronListTool implements AgentTool<typeof cronListSchema, CronListTo
|
|
|
658
644
|
if (jobs.length === 0) {
|
|
659
645
|
return {
|
|
660
646
|
content: [{ type: "text", text: "No scheduled jobs" }],
|
|
661
|
-
details: { jobs: [] },
|
|
647
|
+
details: { op: "list", jobs: [] },
|
|
662
648
|
};
|
|
663
649
|
}
|
|
664
650
|
const lines = jobs.map(job => {
|
|
@@ -667,43 +653,20 @@ export class CronListTool implements AgentTool<typeof cronListSchema, CronListTo
|
|
|
667
653
|
});
|
|
668
654
|
return {
|
|
669
655
|
content: [{ type: "text", text: lines.join("\n") }],
|
|
670
|
-
details: { jobs },
|
|
656
|
+
details: { op: "list", jobs },
|
|
671
657
|
};
|
|
672
658
|
}
|
|
673
|
-
}
|
|
674
659
|
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
readonly description: string;
|
|
680
|
-
readonly parameters = cronDeleteSchema;
|
|
681
|
-
readonly strict = true;
|
|
682
|
-
readonly loadMode = "discoverable";
|
|
683
|
-
|
|
684
|
-
constructor(private readonly session: ToolSession) {
|
|
685
|
-
this.description = prompt.render(cronDescription);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
static createIf(session: ToolSession): CronDeleteTool | null {
|
|
689
|
-
if (!isBackgroundJobSupportEnabled(session.settings)) return null;
|
|
690
|
-
if (isCronDisabled()) return null;
|
|
691
|
-
return new CronDeleteTool(session);
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
async execute(
|
|
695
|
-
_toolCallId: string,
|
|
696
|
-
params: CronDeleteParams,
|
|
697
|
-
_signal?: AbortSignal,
|
|
698
|
-
_onUpdate?: AgentToolUpdateCallback<CronDeleteToolDetails>,
|
|
699
|
-
_context?: AgentToolContext,
|
|
700
|
-
): Promise<AgentToolResult<CronDeleteToolDetails>> {
|
|
660
|
+
async #delete(params: CronParams): Promise<AgentToolResult<CronToolDetails>> {
|
|
661
|
+
if (!params.id) {
|
|
662
|
+
throw new ToolError("cron op=delete requires 'id'.");
|
|
663
|
+
}
|
|
701
664
|
const ownerId = this.session.getAgentId?.() ?? undefined;
|
|
702
665
|
const deleted = deleteRecord(ownerId, params.id);
|
|
703
666
|
const text = deleted ? `Cancelled ${params.id}` : `No scheduled task '${params.id}' found; nothing to cancel.`;
|
|
704
667
|
return {
|
|
705
668
|
content: [{ type: "text", text }],
|
|
706
|
-
details: { id: params.id, deleted },
|
|
669
|
+
details: { op: "delete", id: params.id, deleted },
|
|
707
670
|
};
|
|
708
671
|
}
|
|
709
672
|
}
|