mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 14:01:24 +08:00
fix(agents): avoid duplicate Claude CLI skill prompts
Fix Claude CLI skill prompt handling so native skill plugin materialization is prepared before prompt suppression, with the prompt fallback preserved when plugin args are unavailable. Also keeps direct prepared-run callers covered by an execute-time fallback. Fixes #87063. Co-authored-by: uday <udaymanish.thumma@gmail.com>
This commit is contained in:
@@ -161,13 +161,14 @@ told us OpenClaw-style Claude CLI usage is allowed again, so OpenClaw treats
|
||||
a new policy.
|
||||
</Note>
|
||||
|
||||
The bundled Anthropic `claude-cli` backend receives the OpenClaw skills snapshot
|
||||
two ways: the compact OpenClaw skills catalog in the appended system prompt, and
|
||||
a temporary Claude Code plugin passed with `--plugin-dir`. The plugin contains
|
||||
only the eligible skills for that agent/session, so Claude Code's native skill
|
||||
resolver sees the same filtered set that OpenClaw would otherwise advertise in
|
||||
the prompt. Skill env/API key overrides are still applied by OpenClaw to the
|
||||
child process environment for the run.
|
||||
The bundled Anthropic `claude-cli` backend prefers Claude Code's native skill
|
||||
resolver for OpenClaw skills. When the current skills snapshot includes at least
|
||||
one selected skill with a materialized path, OpenClaw passes a temporary Claude
|
||||
Code plugin with `--plugin-dir` and omits the duplicate OpenClaw skills catalog
|
||||
from the appended system prompt. If the snapshot has no materialized plugin
|
||||
skill, OpenClaw keeps the prompt catalog as a fallback. Skill env/API key
|
||||
overrides are still applied by OpenClaw to the child process environment for the
|
||||
run.
|
||||
|
||||
Claude CLI also has its own noninteractive permission mode. OpenClaw maps that
|
||||
to the existing exec policy instead of adding Claude-specific policy config.
|
||||
|
||||
@@ -203,7 +203,7 @@ vi.mock("../config/sessions.js", () => ({
|
||||
mergeSessionEntry: (a: unknown, b: unknown) => ({ ...(a as object), ...(b as object) }),
|
||||
updateSessionStore: vi.fn(
|
||||
async (_path: string, fn: (store: Record<string, unknown>) => unknown) => {
|
||||
const store: Record<string, unknown> = {};
|
||||
const store = (state.sessionStoreMock ?? {}) as Record<string, unknown>;
|
||||
return fn(store);
|
||||
},
|
||||
),
|
||||
@@ -1023,7 +1023,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
expect(attemptCalls[0]?.sessionEntry).toStrictEqual(visibleEntry);
|
||||
expect(state.persistSessionEntryMock).not.toHaveBeenCalled();
|
||||
expect(state.updateSessionStoreAfterAgentRunMock).not.toHaveBeenCalled();
|
||||
expect(sessionStore["agent:main:main"]).toBe(visibleEntry);
|
||||
expect(sessionStore["agent:main:main"]).toEqual(visibleEntry);
|
||||
});
|
||||
|
||||
it("does not duplicate finishing lifecycle when an attempt already emitted finishing", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { accessSync } from "node:fs";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js";
|
||||
@@ -30,6 +31,15 @@ function sanitizeSkillDirName(name: string, used: Set<string>): string {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function isClaudeCliSkillFileAccessible(skillFilePath: string): boolean {
|
||||
try {
|
||||
accessSync(skillFilePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function collectClaudePluginSkills(snapshot?: SkillSnapshot): Promise<MaterializedSkill[]> {
|
||||
const skills = snapshot?.resolvedSkills ?? [];
|
||||
if (skills.length === 0) {
|
||||
@@ -44,9 +54,7 @@ async function collectClaudePluginSkills(snapshot?: SkillSnapshot): Promise<Mate
|
||||
if (!name || !skillFilePath) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await fs.access(skillFilePath);
|
||||
} catch {
|
||||
if (!isClaudeCliSkillFileAccessible(skillFilePath)) {
|
||||
cliBackendLog.warn(`claude skill plugin skipped missing skill file: ${skillFilePath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -322,15 +322,18 @@ export async function executePreparedCliRun(
|
||||
const resolvedArgs = useResume
|
||||
? baseArgs.map((entry) => entry.replaceAll("{sessionId}", resolvedSessionId ?? ""))
|
||||
: baseArgs;
|
||||
const claudeSkillsPlugin = await prepareClaudeCliSkillsPlugin({
|
||||
backendId: context.backendResolved.id,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
});
|
||||
let claudeSkillsPluginCleanupOwned = false;
|
||||
const fallbackClaudeSkillsPlugin =
|
||||
context.claudeSkillsPluginArgs === undefined
|
||||
? await prepareClaudeCliSkillsPlugin({
|
||||
backendId: context.backendResolved.id,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
})
|
||||
: undefined;
|
||||
let fallbackClaudeSkillsPluginCleanupOwned = false;
|
||||
const claudeSkillsPluginArgs =
|
||||
context.claudeSkillsPluginArgs ?? fallbackClaudeSkillsPlugin?.args ?? [];
|
||||
const baseArgsWithSkills =
|
||||
claudeSkillsPlugin.args.length > 0
|
||||
? [...resolvedArgs, ...claudeSkillsPlugin.args]
|
||||
: resolvedArgs;
|
||||
claudeSkillsPluginArgs.length > 0 ? [...resolvedArgs, ...claudeSkillsPluginArgs] : resolvedArgs;
|
||||
const executionBaseArgs =
|
||||
context.backendResolved.resolveExecutionArgs?.({
|
||||
config: params.config,
|
||||
@@ -453,7 +456,7 @@ export async function executePreparedCliRun(
|
||||
model: context.modelId,
|
||||
backend: context.backendResolved.id,
|
||||
});
|
||||
claudeSkillsPluginCleanupOwned = true;
|
||||
fallbackClaudeSkillsPluginCleanupOwned = true;
|
||||
const ownedPreparedBackendCleanup = context.preparedBackend.cleanup;
|
||||
context.preparedBackend.cleanup = undefined;
|
||||
const liveResult = await runClaudeLiveSessionTurn({
|
||||
@@ -482,7 +485,7 @@ export async function executePreparedCliRun(
|
||||
},
|
||||
cleanup: async () => {
|
||||
try {
|
||||
await claudeSkillsPlugin.cleanup();
|
||||
await fallbackClaudeSkillsPlugin?.cleanup();
|
||||
} finally {
|
||||
await ownedPreparedBackendCleanup?.();
|
||||
}
|
||||
@@ -753,8 +756,8 @@ export async function executePreparedCliRun(
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
if (!claudeSkillsPluginCleanupOwned) {
|
||||
await claudeSkillsPlugin.cleanup();
|
||||
if (!fallbackClaudeSkillsPluginCleanupOwned) {
|
||||
await fallbackClaudeSkillsPlugin?.cleanup();
|
||||
}
|
||||
if (systemPromptFile) {
|
||||
await systemPromptFile.cleanup();
|
||||
|
||||
@@ -205,6 +205,10 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
),
|
||||
resolveMcpLoopbackScopedTools: vi.fn(() => ({ agentId: "main", tools: [] })),
|
||||
resolveOpenClawReferencePaths: vi.fn(async () => ({ docsPath: null, sourcePath: null })),
|
||||
prepareClaudeCliSkillsPlugin: vi.fn(async () => ({
|
||||
args: [],
|
||||
cleanup: vi.fn(async () => undefined),
|
||||
})),
|
||||
});
|
||||
mockGetGlobalHookRunner.mockReturnValue(null);
|
||||
getRuntimeConfigMock.mockReturnValue({});
|
||||
@@ -1094,7 +1098,6 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:main:test",
|
||||
@@ -1185,7 +1188,6 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:main:test",
|
||||
@@ -1246,7 +1248,6 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:main:telegram:group:chat123",
|
||||
@@ -1459,6 +1460,270 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("omits Claude CLI prompt skills when the native skills plugin can carry them", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
const skillDir = path.join(dir, "skills", "weather");
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
const skillFilePath = path.join(skillDir, "SKILL.md");
|
||||
fs.writeFileSync(
|
||||
skillFilePath,
|
||||
[
|
||||
"---",
|
||||
"name: weather",
|
||||
"description: Use weather tools for forecasts.",
|
||||
"---",
|
||||
"",
|
||||
"Read forecast data before replying.",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
try {
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolvePluginSetupCliBackend: () => undefined,
|
||||
resolveRuntimeCliBackends: () => [
|
||||
{
|
||||
id: "claude-cli",
|
||||
pluginId: "anthropic",
|
||||
bundleMcp: false,
|
||||
config: {
|
||||
command: "claude",
|
||||
args: ["--print"],
|
||||
output: "jsonl",
|
||||
input: "stdin",
|
||||
sessionMode: "existing",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
setCliRunnerPrepareTestDeps({
|
||||
prepareClaudeCliSkillsPlugin: vi.fn(async () => ({
|
||||
args: ["--plugin-dir", path.join(dir, "openclaw-skills")],
|
||||
cleanup: vi.fn(async () => undefined),
|
||||
pluginDir: path.join(dir, "openclaw-skills"),
|
||||
})),
|
||||
});
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
prompt: "latest ask",
|
||||
provider: "claude-cli",
|
||||
model: "opus",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-claude-plugin-skills-prompt",
|
||||
config: createCliBackendConfig({ systemPromptOverride: null }),
|
||||
skillsSnapshot: {
|
||||
prompt: [
|
||||
"<available_skills>",
|
||||
" <skill>",
|
||||
" <name>weather</name>",
|
||||
" <description>Use weather tools for forecasts.</description>",
|
||||
` <location>${skillFilePath}</location>`,
|
||||
" </skill>",
|
||||
"</available_skills>",
|
||||
].join("\n"),
|
||||
skills: [{ name: "weather" }],
|
||||
resolvedSkills: [
|
||||
{
|
||||
name: "weather",
|
||||
description: "Use weather tools for forecasts.",
|
||||
filePath: skillFilePath,
|
||||
baseDir: skillDir,
|
||||
source: "test",
|
||||
sourceInfo: {
|
||||
path: skillDir,
|
||||
source: "test",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: skillDir,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(context.systemPrompt).not.toContain("<available_skills>");
|
||||
expect(context.systemPrompt).not.toContain("<name>weather</name>");
|
||||
expect(context.systemPromptReport.skills.promptChars).toBe(0);
|
||||
expect(context.claudeSkillsPluginArgs).toEqual([
|
||||
"--plugin-dir",
|
||||
path.join(dir, "openclaw-skills"),
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps Claude CLI prompt skills when the snapshot has no materialized plugin skills", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
const missingSkillDir = path.join(dir, "skills", "missing");
|
||||
const missingSkillFilePath = path.join(missingSkillDir, "SKILL.md");
|
||||
|
||||
try {
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolvePluginSetupCliBackend: () => undefined,
|
||||
resolveRuntimeCliBackends: () => [
|
||||
{
|
||||
id: "claude-cli",
|
||||
pluginId: "anthropic",
|
||||
bundleMcp: false,
|
||||
config: {
|
||||
command: "claude",
|
||||
args: ["--print"],
|
||||
output: "jsonl",
|
||||
input: "stdin",
|
||||
sessionMode: "existing",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
prompt: "latest ask",
|
||||
provider: "claude-cli",
|
||||
model: "opus",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-claude-plugin-skills-prompt-fallback",
|
||||
config: createCliBackendConfig({ systemPromptOverride: null }),
|
||||
skillsSnapshot: {
|
||||
prompt: [
|
||||
"<available_skills>",
|
||||
" <skill>",
|
||||
" <name>weather</name>",
|
||||
" <description>Use weather tools for forecasts.</description>",
|
||||
` <location>${missingSkillFilePath}</location>`,
|
||||
" </skill>",
|
||||
"</available_skills>",
|
||||
].join("\n"),
|
||||
skills: [{ name: "weather" }],
|
||||
resolvedSkills: [
|
||||
{
|
||||
name: "weather",
|
||||
description: "Use weather tools for forecasts.",
|
||||
filePath: missingSkillFilePath,
|
||||
baseDir: missingSkillDir,
|
||||
source: "test",
|
||||
sourceInfo: {
|
||||
path: missingSkillDir,
|
||||
source: "test",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: missingSkillDir,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(context.systemPrompt).toContain("<available_skills>");
|
||||
expect(context.systemPrompt).toContain("<name>weather</name>");
|
||||
expect(context.systemPromptReport.skills.promptChars).toBeGreaterThan(0);
|
||||
expect(context.claudeSkillsPluginArgs).toEqual([]);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps Claude CLI prompt skills when plugin materialization produces no args", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
const skillDir = path.join(dir, "skills", "weather");
|
||||
fs.mkdirSync(skillDir, { recursive: true });
|
||||
const skillFilePath = path.join(skillDir, "SKILL.md");
|
||||
fs.writeFileSync(
|
||||
skillFilePath,
|
||||
[
|
||||
"---",
|
||||
"name: weather",
|
||||
"description: Use weather tools for forecasts.",
|
||||
"---",
|
||||
"",
|
||||
"Read forecast data before replying.",
|
||||
].join("\n"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
try {
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
resolvePluginSetupCliBackend: () => undefined,
|
||||
resolveRuntimeCliBackends: () => [
|
||||
{
|
||||
id: "claude-cli",
|
||||
pluginId: "anthropic",
|
||||
bundleMcp: false,
|
||||
config: {
|
||||
command: "claude",
|
||||
args: ["--print"],
|
||||
output: "jsonl",
|
||||
input: "stdin",
|
||||
sessionMode: "existing",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
setCliRunnerPrepareTestDeps({
|
||||
prepareClaudeCliSkillsPlugin: vi.fn(async () => ({
|
||||
args: [],
|
||||
cleanup: vi.fn(async () => undefined),
|
||||
})),
|
||||
});
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
prompt: "latest ask",
|
||||
provider: "claude-cli",
|
||||
model: "opus",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-claude-plugin-skills-prompt-materialization-fallback",
|
||||
config: createCliBackendConfig({ systemPromptOverride: null }),
|
||||
skillsSnapshot: {
|
||||
prompt: [
|
||||
"<available_skills>",
|
||||
" <skill>",
|
||||
" <name>weather</name>",
|
||||
" <description>Use weather tools for forecasts.</description>",
|
||||
` <location>${skillFilePath}</location>`,
|
||||
" </skill>",
|
||||
"</available_skills>",
|
||||
].join("\n"),
|
||||
skills: [{ name: "weather" }],
|
||||
resolvedSkills: [
|
||||
{
|
||||
name: "weather",
|
||||
description: "Use weather tools for forecasts.",
|
||||
filePath: skillFilePath,
|
||||
baseDir: skillDir,
|
||||
source: "test",
|
||||
sourceInfo: {
|
||||
path: skillDir,
|
||||
source: "test",
|
||||
scope: "project",
|
||||
origin: "top-level",
|
||||
baseDir: skillDir,
|
||||
},
|
||||
disableModelInvocation: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(context.systemPrompt).toContain("<available_skills>");
|
||||
expect(context.systemPrompt).toContain("<name>weather</name>");
|
||||
expect(context.systemPromptReport.skills.promptChars).toBeGreaterThan(0);
|
||||
expect(context.claudeSkillsPluginArgs).toEqual([]);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not probe the transcript for non-claude-cli providers", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
try {
|
||||
|
||||
@@ -59,6 +59,7 @@ import { buildSystemPromptReport } from "../system-prompt-report.js";
|
||||
import { appendModelIdentitySystemPrompt } from "../system-prompt.js";
|
||||
import { redactRunIdentifier, resolveRunWorkspaceDir } from "../workspace-run.js";
|
||||
import { prepareCliBundleMcpConfig } from "./bundle-mcp.js";
|
||||
import { prepareClaudeCliSkillsPlugin } from "./claude-skills-plugin.js";
|
||||
import { buildCliAgentSystemPrompt, normalizeCliModel } from "./helpers.js";
|
||||
import { cliBackendLog } from "./log.js";
|
||||
import {
|
||||
@@ -81,6 +82,7 @@ const prepareDeps = {
|
||||
resolveOpenClawReferencePaths: async (
|
||||
params: Parameters<typeof import("../docs-path.js").resolveOpenClawReferencePaths>[0],
|
||||
) => (await import("../docs-path.js")).resolveOpenClawReferencePaths(params),
|
||||
prepareClaudeCliSkillsPlugin,
|
||||
// Surfaced as a dep so tests can stub the on-disk Claude CLI transcript probe
|
||||
// without touching ~/.claude/projects.
|
||||
claudeCliSessionTranscriptHasContent,
|
||||
@@ -310,6 +312,20 @@ export async function prepareCliRunContext(
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
const claudeSkillsPlugin = await prepareDeps.prepareClaudeCliSkillsPlugin({
|
||||
backendId: backendResolved.id,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
});
|
||||
const preparedCleanup =
|
||||
preparedBackendCleanup || claudeSkillsPlugin.args.length > 0
|
||||
? async () => {
|
||||
try {
|
||||
await claudeSkillsPlugin.cleanup();
|
||||
} finally {
|
||||
await preparedBackendCleanup?.();
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
const preparedBackendClearEnv = [
|
||||
...(preparedBackend.backend.clearEnv ?? []),
|
||||
...(preparedExecution?.clearEnv ?? []),
|
||||
@@ -323,7 +339,7 @@ export async function prepareCliRunContext(
|
||||
: {}),
|
||||
},
|
||||
...(preparedBackendEnv ? { env: preparedBackendEnv } : {}),
|
||||
...(preparedBackendCleanup ? { cleanup: preparedBackendCleanup } : {}),
|
||||
...(preparedCleanup ? { cleanup: preparedCleanup } : {}),
|
||||
};
|
||||
const promptTools =
|
||||
bundleMcpEnabled && mcpLoopbackRuntime
|
||||
@@ -405,6 +421,7 @@ export async function prepareCliRunContext(
|
||||
config: params.config,
|
||||
agentId: sessionAgentId,
|
||||
});
|
||||
const systemPromptSkillsPrompt = claudeSkillsPlugin.args.length > 0 ? "" : skillsPrompt;
|
||||
const builtSystemPrompt =
|
||||
resolveSystemPromptOverride({
|
||||
config: params.config,
|
||||
@@ -421,7 +438,7 @@ export async function prepareCliRunContext(
|
||||
heartbeatPrompt,
|
||||
docsPath: openClawReferences.docsPath ?? undefined,
|
||||
sourcePath: openClawReferences.sourcePath ?? undefined,
|
||||
skillsPrompt,
|
||||
skillsPrompt: systemPromptSkillsPrompt,
|
||||
tools: promptTools,
|
||||
contextFiles,
|
||||
modelDisplay,
|
||||
@@ -531,7 +548,7 @@ export async function prepareCliRunContext(
|
||||
systemPrompt,
|
||||
bootstrapFiles,
|
||||
injectedFiles: contextFiles,
|
||||
skillsPrompt,
|
||||
skillsPrompt: systemPromptSkillsPrompt,
|
||||
tools: promptTools,
|
||||
currentTurn: {
|
||||
...(params.currentInboundEventKind ? { kind: params.currentInboundEventKind } : {}),
|
||||
@@ -595,6 +612,7 @@ export async function prepareCliRunContext(
|
||||
contextWindowInfo,
|
||||
systemPrompt,
|
||||
systemPromptReport,
|
||||
claudeSkillsPluginArgs: claudeSkillsPlugin.args,
|
||||
bootstrapPromptWarningLines: bootstrapPromptWarning.lines,
|
||||
...(openClawHistoryPrompt ? { openClawHistoryPrompt } : {}),
|
||||
heartbeatPrompt,
|
||||
|
||||
@@ -135,6 +135,7 @@ export type PreparedCliRunContext = {
|
||||
contextWindowInfo?: ContextWindowInfo;
|
||||
systemPrompt: string;
|
||||
systemPromptReport: SessionSystemPromptReport;
|
||||
claudeSkillsPluginArgs?: string[] | undefined;
|
||||
bootstrapPromptWarningLines: string[];
|
||||
openClawHistoryPrompt?: string;
|
||||
heartbeatPrompt?: string;
|
||||
|
||||
Reference in New Issue
Block a user