mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 18:50:23 +00:00
perf(inbound): trim cold startup import graph (#52082)
* perf(inbound): trim cold startup import graph * chore(reply): drop redundant inline action type import * fix(inbound): restore warning and maintenance seams * fix(reply): restore type seam and secure forked transcripts
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
listChatCommandsForConfig,
|
||||
normalizeCommandBody,
|
||||
} from "./commands-registry.js";
|
||||
import { isAbortTrigger } from "./reply/abort.js";
|
||||
import { isAbortTrigger } from "./reply/abort-primitives.js";
|
||||
|
||||
export function hasControlCommand(
|
||||
text?: string,
|
||||
|
||||
33
src/auto-reply/reply/abort-cutoff.runtime.ts
Normal file
33
src/auto-reply/reply/abort-cutoff.runtime.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { updateSessionStore } from "../../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { applyAbortCutoffToSessionEntry, hasAbortCutoff } from "./abort-cutoff.js";
|
||||
|
||||
export async function clearAbortCutoffInSessionRuntime(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
}): Promise<boolean> {
|
||||
const { sessionEntry, sessionStore, sessionKey, storePath } = params;
|
||||
if (!sessionEntry || !sessionStore || !sessionKey || !hasAbortCutoff(sessionEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
applyAbortCutoffToSessionEntry(sessionEntry, undefined);
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
|
||||
if (storePath) {
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const existing = store[sessionKey] ?? sessionEntry;
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
applyAbortCutoffToSessionEntry(existing, undefined);
|
||||
existing.updatedAt = Date.now();
|
||||
store[sessionKey] = existing;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
|
||||
export type AbortCutoff = {
|
||||
@@ -51,36 +50,6 @@ export function applyAbortCutoffToSessionEntry(
|
||||
entry.abortCutoffTimestamp = cutoff?.timestamp;
|
||||
}
|
||||
|
||||
export async function clearAbortCutoffInSession(params: {
|
||||
sessionEntry?: SessionEntry;
|
||||
sessionStore?: Record<string, SessionEntry>;
|
||||
sessionKey?: string;
|
||||
storePath?: string;
|
||||
}): Promise<boolean> {
|
||||
const { sessionEntry, sessionStore, sessionKey, storePath } = params;
|
||||
if (!sessionEntry || !sessionStore || !sessionKey || !hasAbortCutoff(sessionEntry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
applyAbortCutoffToSessionEntry(sessionEntry, undefined);
|
||||
sessionEntry.updatedAt = Date.now();
|
||||
sessionStore[sessionKey] = sessionEntry;
|
||||
|
||||
if (storePath) {
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
const existing = store[sessionKey] ?? sessionEntry;
|
||||
if (!existing) {
|
||||
return;
|
||||
}
|
||||
applyAbortCutoffToSessionEntry(existing, undefined);
|
||||
existing.updatedAt = Date.now();
|
||||
store[sessionKey] = existing;
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function toNumericMessageSid(value: string | undefined): bigint | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed || !/^\d+$/.test(trimmed)) {
|
||||
|
||||
130
src/auto-reply/reply/abort-primitives.ts
Normal file
130
src/auto-reply/reply/abort-primitives.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { normalizeCommandBody, type CommandNormalizeOptions } from "../commands-registry.js";
|
||||
|
||||
const ABORT_TRIGGERS = new Set([
|
||||
"stop",
|
||||
"esc",
|
||||
"abort",
|
||||
"wait",
|
||||
"exit",
|
||||
"interrupt",
|
||||
"detente",
|
||||
"deten",
|
||||
"detén",
|
||||
"arrete",
|
||||
"arrête",
|
||||
"停止",
|
||||
"やめて",
|
||||
"止めて",
|
||||
"रुको",
|
||||
"توقف",
|
||||
"стоп",
|
||||
"остановись",
|
||||
"останови",
|
||||
"остановить",
|
||||
"прекрати",
|
||||
"halt",
|
||||
"anhalten",
|
||||
"aufhören",
|
||||
"hoer auf",
|
||||
"stopp",
|
||||
"pare",
|
||||
"stop openclaw",
|
||||
"openclaw stop",
|
||||
"stop action",
|
||||
"stop current action",
|
||||
"stop run",
|
||||
"stop current run",
|
||||
"stop agent",
|
||||
"stop the agent",
|
||||
"stop don't do anything",
|
||||
"stop dont do anything",
|
||||
"stop do not do anything",
|
||||
"stop doing anything",
|
||||
"do not do that",
|
||||
"please stop",
|
||||
"stop please",
|
||||
]);
|
||||
const ABORT_MEMORY = new Map<string, boolean>();
|
||||
const ABORT_MEMORY_MAX = 2000;
|
||||
const TRAILING_ABORT_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
|
||||
|
||||
function normalizeAbortTriggerText(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[’`]/g, "'")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(TRAILING_ABORT_PUNCTUATION_RE, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isAbortTrigger(text?: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeAbortTriggerText(text);
|
||||
return ABORT_TRIGGERS.has(normalized);
|
||||
}
|
||||
|
||||
export function isAbortRequestText(text?: string, options?: CommandNormalizeOptions): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeCommandBody(text, options).trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const normalizedLower = normalized.toLowerCase();
|
||||
return (
|
||||
normalizedLower === "/stop" ||
|
||||
normalizeAbortTriggerText(normalizedLower) === "/stop" ||
|
||||
isAbortTrigger(normalizedLower)
|
||||
);
|
||||
}
|
||||
|
||||
export function getAbortMemory(key: string): boolean | undefined {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return ABORT_MEMORY.get(normalized);
|
||||
}
|
||||
|
||||
function pruneAbortMemory(): void {
|
||||
if (ABORT_MEMORY.size <= ABORT_MEMORY_MAX) {
|
||||
return;
|
||||
}
|
||||
const excess = ABORT_MEMORY.size - ABORT_MEMORY_MAX;
|
||||
let removed = 0;
|
||||
for (const entryKey of ABORT_MEMORY.keys()) {
|
||||
ABORT_MEMORY.delete(entryKey);
|
||||
removed += 1;
|
||||
if (removed >= excess) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAbortMemory(key: string, value: boolean): void {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
return;
|
||||
}
|
||||
if (ABORT_MEMORY.has(normalized)) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
}
|
||||
ABORT_MEMORY.set(normalized, true);
|
||||
pruneAbortMemory();
|
||||
}
|
||||
|
||||
export function getAbortMemorySizeForTest(): number {
|
||||
return ABORT_MEMORY.size;
|
||||
}
|
||||
|
||||
export function resetAbortMemoryForTest(): void {
|
||||
ABORT_MEMORY.clear();
|
||||
}
|
||||
@@ -20,147 +20,32 @@ import {
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import { normalizeCommandBody, type CommandNormalizeOptions } from "../commands-registry.js";
|
||||
import type { FinalizedMsgContext, MsgContext } from "../templating.js";
|
||||
import {
|
||||
applyAbortCutoffToSessionEntry,
|
||||
resolveAbortCutoffFromContext,
|
||||
shouldPersistAbortCutoff,
|
||||
} from "./abort-cutoff.js";
|
||||
import {
|
||||
getAbortMemory,
|
||||
getAbortMemorySizeForTest,
|
||||
isAbortRequestText,
|
||||
isAbortTrigger,
|
||||
resetAbortMemoryForTest,
|
||||
setAbortMemory,
|
||||
} from "./abort-primitives.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import { clearSessionQueues } from "./queue.js";
|
||||
|
||||
export { resolveAbortCutoffFromContext, shouldSkipMessageByAbortCutoff } from "./abort-cutoff.js";
|
||||
|
||||
const ABORT_TRIGGERS = new Set([
|
||||
"stop",
|
||||
"esc",
|
||||
"abort",
|
||||
"wait",
|
||||
"exit",
|
||||
"interrupt",
|
||||
"detente",
|
||||
"deten",
|
||||
"detén",
|
||||
"arrete",
|
||||
"arrête",
|
||||
"停止",
|
||||
"やめて",
|
||||
"止めて",
|
||||
"रुको",
|
||||
"توقف",
|
||||
"стоп",
|
||||
"остановись",
|
||||
"останови",
|
||||
"остановить",
|
||||
"прекрати",
|
||||
"halt",
|
||||
"anhalten",
|
||||
"aufhören",
|
||||
"hoer auf",
|
||||
"stopp",
|
||||
"pare",
|
||||
"stop openclaw",
|
||||
"openclaw stop",
|
||||
"stop action",
|
||||
"stop current action",
|
||||
"stop run",
|
||||
"stop current run",
|
||||
"stop agent",
|
||||
"stop the agent",
|
||||
"stop don't do anything",
|
||||
"stop dont do anything",
|
||||
"stop do not do anything",
|
||||
"stop doing anything",
|
||||
"do not do that",
|
||||
"please stop",
|
||||
"stop please",
|
||||
]);
|
||||
const ABORT_MEMORY = new Map<string, boolean>();
|
||||
const ABORT_MEMORY_MAX = 2000;
|
||||
const TRAILING_ABORT_PUNCTUATION_RE = /[.!?…,,。;;::'"’”)\]}]+$/u;
|
||||
|
||||
function normalizeAbortTriggerText(text: string): string {
|
||||
return text
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[’`]/g, "'")
|
||||
.replace(/\s+/g, " ")
|
||||
.replace(TRAILING_ABORT_PUNCTUATION_RE, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
export function isAbortTrigger(text?: string): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeAbortTriggerText(text);
|
||||
return ABORT_TRIGGERS.has(normalized);
|
||||
}
|
||||
|
||||
export function isAbortRequestText(text?: string, options?: CommandNormalizeOptions): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = normalizeCommandBody(text, options).trim();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
const normalizedLower = normalized.toLowerCase();
|
||||
return (
|
||||
normalizedLower === "/stop" ||
|
||||
normalizeAbortTriggerText(normalizedLower) === "/stop" ||
|
||||
isAbortTrigger(normalizedLower)
|
||||
);
|
||||
}
|
||||
|
||||
export function getAbortMemory(key: string): boolean | undefined {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return undefined;
|
||||
}
|
||||
return ABORT_MEMORY.get(normalized);
|
||||
}
|
||||
|
||||
function pruneAbortMemory(): void {
|
||||
if (ABORT_MEMORY.size <= ABORT_MEMORY_MAX) {
|
||||
return;
|
||||
}
|
||||
const excess = ABORT_MEMORY.size - ABORT_MEMORY_MAX;
|
||||
let removed = 0;
|
||||
for (const entryKey of ABORT_MEMORY.keys()) {
|
||||
ABORT_MEMORY.delete(entryKey);
|
||||
removed += 1;
|
||||
if (removed >= excess) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setAbortMemory(key: string, value: boolean): void {
|
||||
const normalized = key.trim();
|
||||
if (!normalized) {
|
||||
return;
|
||||
}
|
||||
if (!value) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
return;
|
||||
}
|
||||
// Refresh insertion order so active keys are less likely to be evicted.
|
||||
if (ABORT_MEMORY.has(normalized)) {
|
||||
ABORT_MEMORY.delete(normalized);
|
||||
}
|
||||
ABORT_MEMORY.set(normalized, true);
|
||||
pruneAbortMemory();
|
||||
}
|
||||
|
||||
export function getAbortMemorySizeForTest(): number {
|
||||
return ABORT_MEMORY.size;
|
||||
}
|
||||
|
||||
export function resetAbortMemoryForTest(): void {
|
||||
ABORT_MEMORY.clear();
|
||||
}
|
||||
export {
|
||||
getAbortMemory,
|
||||
getAbortMemorySizeForTest,
|
||||
isAbortRequestText,
|
||||
isAbortTrigger,
|
||||
resetAbortMemoryForTest,
|
||||
setAbortMemory,
|
||||
};
|
||||
|
||||
export function formatAbortReplyText(stoppedSubagents?: number): string {
|
||||
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { SessionEntry } from "../../config/sessions.js";
|
||||
import { updateSessionStore } from "../../config/sessions.js";
|
||||
import { setAbortMemory } from "./abort.js";
|
||||
import { setAbortMemory } from "./abort-primitives.js";
|
||||
|
||||
export async function applySessionHints(params: {
|
||||
baseBody: string;
|
||||
|
||||
1
src/auto-reply/reply/commands-core.runtime.ts
Normal file
1
src/auto-reply/reply/commands-core.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { emitResetCommandHooks } from "./commands-core.js";
|
||||
1
src/auto-reply/reply/commands.runtime.ts
Normal file
1
src/auto-reply/reply/commands.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { buildStatusReply, handleCommands } from "./commands.js";
|
||||
27
src/auto-reply/reply/directive-handling.auth-profile.ts
Normal file
27
src/auto-reply/reply/directive-handling.auth-profile.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export function resolveProfileOverride(params: {
|
||||
rawProfile?: string;
|
||||
provider: string;
|
||||
cfg: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): { profileId?: string; error?: string } {
|
||||
const raw = params.rawProfile?.trim();
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[raw];
|
||||
if (!profile) {
|
||||
return { error: `Auth profile "${raw}" not found.` };
|
||||
}
|
||||
if (profile.provider !== params.provider) {
|
||||
return {
|
||||
error: `Auth profile "${raw}" is for ${profile.provider}, not ${params.provider}.`,
|
||||
};
|
||||
}
|
||||
return { profileId: raw };
|
||||
}
|
||||
@@ -217,27 +217,4 @@ export const formatAuthLabel = (auth: { label: string; source: string }) => {
|
||||
return `${auth.label} (${auth.source})`;
|
||||
};
|
||||
|
||||
export const resolveProfileOverride = (params: {
|
||||
rawProfile?: string;
|
||||
provider: string;
|
||||
cfg: OpenClawConfig;
|
||||
agentDir?: string;
|
||||
}): { profileId?: string; error?: string } => {
|
||||
const raw = params.rawProfile?.trim();
|
||||
if (!raw) {
|
||||
return {};
|
||||
}
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[raw];
|
||||
if (!profile) {
|
||||
return { error: `Auth profile "${raw}" not found.` };
|
||||
}
|
||||
if (profile.provider !== params.provider) {
|
||||
return {
|
||||
error: `Auth profile "${raw}" is for ${profile.provider}, not ${params.provider}.`,
|
||||
};
|
||||
}
|
||||
return { profileId: raw };
|
||||
};
|
||||
export { resolveProfileOverride } from "./directive-handling.auth-profile.js";
|
||||
|
||||
24
src/auto-reply/reply/directive-handling.defaults.ts
Normal file
24
src/auto-reply/reply/directive-handling.defaults.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export function resolveDefaultModel(params: { cfg: OpenClawConfig; agentId?: string }): {
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
} {
|
||||
const mainModel = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const defaultProvider = mainModel.provider;
|
||||
const defaultModel = mainModel.model;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider,
|
||||
});
|
||||
return { defaultProvider, defaultModel, aliasIndex };
|
||||
}
|
||||
@@ -13,10 +13,8 @@ import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import { formatThinkingLevels, formatXHighModelHint, supportsXHighThinking } from "../thinking.js";
|
||||
import type { ReplyPayload } from "../types.js";
|
||||
import {
|
||||
maybeHandleModelDirectiveInfo,
|
||||
resolveModelSelectionFromDirective,
|
||||
} from "./directive-handling.model.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
|
||||
import { maybeHandleModelDirectiveInfo } from "./directive-handling.model.js";
|
||||
import type { HandleDirectiveOnlyParams } from "./directive-handling.params.js";
|
||||
import { maybeHandleQueueDirective } from "./directive-handling.queue-validation.js";
|
||||
import {
|
||||
|
||||
159
src/auto-reply/reply/directive-handling.model-selection.ts
Normal file
159
src/auto-reply/reply/directive-handling.model-selection.ts
Normal file
@@ -0,0 +1,159 @@
|
||||
import { ensureAuthProfileStore } from "../../agents/auth-profiles.js";
|
||||
import {
|
||||
type ModelAliasIndex,
|
||||
modelKey,
|
||||
normalizeProviderIdForAuth,
|
||||
resolveModelRefFromString,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveProfileOverride } from "./directive-handling.auth-profile.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { type ModelDirectiveSelection, resolveModelDirectiveSelection } from "./model-selection.js";
|
||||
|
||||
function resolveStoredNumericProfileModelDirective(params: { raw: string; agentDir: string }): {
|
||||
modelRaw: string;
|
||||
profileId: string;
|
||||
profileProvider: string;
|
||||
} | null {
|
||||
const trimmed = params.raw.trim();
|
||||
const lastSlash = trimmed.lastIndexOf("/");
|
||||
const profileDelimiter = trimmed.indexOf("@", lastSlash + 1);
|
||||
if (profileDelimiter <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const profileId = trimmed.slice(profileDelimiter + 1).trim();
|
||||
if (!/^\d{8}$/.test(profileId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const modelRaw = trimmed.slice(0, profileDelimiter).trim();
|
||||
if (!modelRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const profile = store.profiles[profileId];
|
||||
if (!profile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { modelRaw, profileId, profileProvider: profile.provider };
|
||||
}
|
||||
|
||||
export function resolveModelSelectionFromDirective(params: {
|
||||
directives: InlineDirectives;
|
||||
cfg: OpenClawConfig;
|
||||
agentDir: string;
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
allowedModelKeys: Set<string>;
|
||||
allowedModelCatalog: Array<{ provider: string; id?: string; name?: string }>;
|
||||
provider: string;
|
||||
}): {
|
||||
modelSelection?: ModelDirectiveSelection;
|
||||
profileOverride?: string;
|
||||
errorText?: string;
|
||||
} {
|
||||
if (!params.directives.hasModelDirective || !params.directives.rawModelDirective) {
|
||||
if (params.directives.rawModelProfile) {
|
||||
return { errorText: "Auth profile override requires a model selection." };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
const raw = params.directives.rawModelDirective.trim();
|
||||
const storedNumericProfile =
|
||||
params.directives.rawModelProfile === undefined
|
||||
? resolveStoredNumericProfileModelDirective({
|
||||
raw,
|
||||
agentDir: params.agentDir,
|
||||
})
|
||||
: null;
|
||||
const storedNumericProfileSelection = storedNumericProfile
|
||||
? resolveModelDirectiveSelection({
|
||||
raw: storedNumericProfile.modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys: params.allowedModelKeys,
|
||||
})
|
||||
: null;
|
||||
const useStoredNumericProfile =
|
||||
Boolean(storedNumericProfileSelection?.selection) &&
|
||||
normalizeProviderIdForAuth(storedNumericProfileSelection?.selection?.provider ?? "") ===
|
||||
normalizeProviderIdForAuth(storedNumericProfile?.profileProvider ?? "");
|
||||
const modelRaw =
|
||||
useStoredNumericProfile && storedNumericProfile ? storedNumericProfile.modelRaw : raw;
|
||||
let modelSelection: ModelDirectiveSelection | undefined;
|
||||
|
||||
if (/^[0-9]+$/.test(raw)) {
|
||||
return {
|
||||
errorText: [
|
||||
"Numeric model selection is not supported in chat.",
|
||||
"",
|
||||
"Browse: /models or /models <provider>",
|
||||
"Switch: /model <provider/model>",
|
||||
].join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
const explicit = resolveModelRefFromString({
|
||||
raw: modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex: params.aliasIndex,
|
||||
});
|
||||
if (explicit) {
|
||||
const explicitKey = modelKey(explicit.ref.provider, explicit.ref.model);
|
||||
if (params.allowedModelKeys.size === 0 || params.allowedModelKeys.has(explicitKey)) {
|
||||
modelSelection = {
|
||||
provider: explicit.ref.provider,
|
||||
model: explicit.ref.model,
|
||||
isDefault:
|
||||
explicit.ref.provider === params.defaultProvider &&
|
||||
explicit.ref.model === params.defaultModel,
|
||||
...(explicit.alias ? { alias: explicit.alias } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!modelSelection) {
|
||||
const resolved = resolveModelDirectiveSelection({
|
||||
raw: modelRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
aliasIndex: params.aliasIndex,
|
||||
allowedModelKeys: params.allowedModelKeys,
|
||||
});
|
||||
|
||||
if (resolved.error) {
|
||||
return { errorText: resolved.error };
|
||||
}
|
||||
|
||||
if (resolved.selection) {
|
||||
modelSelection = resolved.selection;
|
||||
}
|
||||
}
|
||||
|
||||
let profileOverride: string | undefined;
|
||||
const rawProfile =
|
||||
params.directives.rawModelProfile ??
|
||||
(useStoredNumericProfile ? storedNumericProfile?.profileId : undefined);
|
||||
if (modelSelection && rawProfile) {
|
||||
const profileResolved = resolveProfileOverride({
|
||||
rawProfile,
|
||||
provider: modelSelection.provider,
|
||||
cfg: params.cfg,
|
||||
agentDir: params.agentDir,
|
||||
});
|
||||
if (profileResolved.error) {
|
||||
return { errorText: profileResolved.error };
|
||||
}
|
||||
profileOverride = profileResolved.profileId;
|
||||
}
|
||||
|
||||
return { modelSelection, profileOverride };
|
||||
}
|
||||
@@ -3,20 +3,16 @@ import {
|
||||
resolveDefaultAgentId,
|
||||
resolveSessionAgentId,
|
||||
} from "../../agents/agent-scope.js";
|
||||
import { lookupContextTokens } from "../../agents/context.js";
|
||||
import { lookupCachedContextTokens } from "../../agents/context-cache.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import {
|
||||
buildModelAliasIndex,
|
||||
type ModelAliasIndex,
|
||||
resolveDefaultModelForAgent,
|
||||
} from "../../agents/model-selection.js";
|
||||
import type { ModelAliasIndex } from "../../agents/model-selection.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { updateSessionStore } from "../../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { applyVerboseOverride } from "../../sessions/level-overrides.js";
|
||||
import { applyModelOverrideToSessionEntry } from "../../sessions/model-overrides.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model.js";
|
||||
import { resolveModelSelectionFromDirective } from "./directive-handling.model-selection.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { enqueueModeSwitchEvents } from "./directive-handling.shared.js";
|
||||
import type { ElevatedLevel, ReasoningLevel } from "./directives.js";
|
||||
@@ -203,24 +199,7 @@ export async function persistInlineDirectives(params: {
|
||||
return {
|
||||
provider,
|
||||
model,
|
||||
contextTokens: agentCfg?.contextTokens ?? lookupContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
|
||||
contextTokens:
|
||||
agentCfg?.contextTokens ?? lookupCachedContextTokens(model) ?? DEFAULT_CONTEXT_TOKENS,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveDefaultModel(params: { cfg: OpenClawConfig; agentId?: string }): {
|
||||
defaultProvider: string;
|
||||
defaultModel: string;
|
||||
aliasIndex: ModelAliasIndex;
|
||||
} {
|
||||
const mainModel = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
});
|
||||
const defaultProvider = mainModel.provider;
|
||||
const defaultModel = mainModel.model;
|
||||
const aliasIndex = buildModelAliasIndex({
|
||||
cfg: params.cfg,
|
||||
defaultProvider,
|
||||
});
|
||||
return { defaultProvider, defaultModel, aliasIndex };
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ export { applyInlineDirectivesFastLane } from "./directive-handling.fast-lane.js
|
||||
export * from "./directive-handling.impl.js";
|
||||
export type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
export { isDirectiveOnly, parseInlineDirectives } from "./directive-handling.parse.js";
|
||||
export { persistInlineDirectives, resolveDefaultModel } from "./directive-handling.persist.js";
|
||||
export { persistInlineDirectives } from "./directive-handling.persist.js";
|
||||
export { resolveDefaultModel } from "./directive-handling.defaults.js";
|
||||
export { formatDirectiveAck } from "./directive-handling.shared.js";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { collectTextContentBlocks } from "../../agents/content-blocks.js";
|
||||
import { createOpenClawTools } from "../../agents/openclaw-tools.js";
|
||||
import type { BlockReplyChunking } from "../../agents/pi-embedded-block-chunker.js";
|
||||
import type { SkillCommandSpec } from "../../agents/skills.js";
|
||||
import { applyOwnerOnlyToolPolicy } from "../../agents/tool-policy.js";
|
||||
@@ -11,20 +10,18 @@ import { generateSecureToken } from "../../infra/secure-random.js";
|
||||
import { resolveGatewayMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
listReservedChatSlashCommandNames,
|
||||
listSkillCommandsForWorkspace,
|
||||
resolveSkillCommandInvocation,
|
||||
} from "../skill-commands.js";
|
||||
} from "../skill-commands-base.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import type { ElevatedLevel, ReasoningLevel, ThinkLevel, VerboseLevel } from "../thinking.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
clearAbortCutoffInSession,
|
||||
readAbortCutoffFromSessionEntry,
|
||||
resolveAbortCutoffFromContext,
|
||||
shouldSkipMessageByAbortCutoff,
|
||||
} from "./abort-cutoff.js";
|
||||
import { getAbortMemory, isAbortRequestText } from "./abort.js";
|
||||
import { buildStatusReply, handleCommands } from "./commands.js";
|
||||
import { getAbortMemory, isAbortRequestText } from "./abort-primitives.js";
|
||||
import type { buildStatusReply, handleCommands } from "./commands.runtime.js";
|
||||
import type { InlineDirectives } from "./directive-handling.parse.js";
|
||||
import { isDirectiveOnly } from "./directive-handling.parse.js";
|
||||
import type { createModelSelectionState } from "./model-selection.js";
|
||||
@@ -191,7 +188,7 @@ export async function handleInlineActions(params: {
|
||||
shouldLoadSkillCommands && params.skillCommands
|
||||
? params.skillCommands
|
||||
: shouldLoadSkillCommands
|
||||
? listSkillCommandsForWorkspace({
|
||||
? (await import("../skill-commands.runtime.js")).listSkillCommandsForWorkspace({
|
||||
workspaceDir,
|
||||
cfg,
|
||||
skillFilter,
|
||||
@@ -222,6 +219,7 @@ export async function handleInlineActions(params: {
|
||||
resolveGatewayMessageChannel(ctx.Provider) ??
|
||||
undefined;
|
||||
|
||||
const { createOpenClawTools } = await import("../../agents/openclaw-tools.runtime.js");
|
||||
const tools = createOpenClawTools({
|
||||
agentSessionKey: sessionKey,
|
||||
agentChannel: channel,
|
||||
@@ -305,7 +303,9 @@ export async function handleInlineActions(params: {
|
||||
return { kind: "reply", reply: undefined };
|
||||
}
|
||||
if (cutoff) {
|
||||
await clearAbortCutoffInSession({
|
||||
await (
|
||||
await import("./abort-cutoff.runtime.js")
|
||||
).clearAbortCutoffInSessionRuntime({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey,
|
||||
@@ -335,6 +335,7 @@ export async function handleInlineActions(params: {
|
||||
isGroup,
|
||||
}) && inlineStatusRequested;
|
||||
if (handleInlineStatus) {
|
||||
const { buildStatusReply } = await import("./commands.runtime.js");
|
||||
const inlineStatusReply = await buildStatusReply({
|
||||
cfg,
|
||||
command,
|
||||
@@ -358,8 +359,9 @@ export async function handleInlineActions(params: {
|
||||
directives = { ...directives, hasStatusDirective: false };
|
||||
}
|
||||
|
||||
const runCommands = (commandInput: typeof command) =>
|
||||
handleCommands({
|
||||
const runCommands = async (commandInput: typeof command) => {
|
||||
const { handleCommands } = await import("./commands.runtime.js");
|
||||
return handleCommands({
|
||||
// Pass sessionCtx so command handlers can mutate stripped body for same-turn continuation.
|
||||
ctx: sessionCtx,
|
||||
// Keep original finalized context in sync when command handlers need outer-dispatch side effects.
|
||||
@@ -397,6 +399,7 @@ export async function handleInlineActions(params: {
|
||||
skillCommands,
|
||||
typing,
|
||||
});
|
||||
};
|
||||
|
||||
if (inlineCommand) {
|
||||
const inlineCommandContext = {
|
||||
|
||||
@@ -15,8 +15,7 @@ import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import { SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { emitResetCommandHooks, type ResetCommandAction } from "./commands-core.js";
|
||||
import { resolveDefaultModel } from "./directive-handling.persist.js";
|
||||
import { resolveDefaultModel } from "./directive-handling.defaults.js";
|
||||
import { resolveReplyDirectives } from "./get-reply-directives.js";
|
||||
import { handleInlineActions } from "./get-reply-inline-actions.js";
|
||||
import { runPreparedReply } from "./get-reply-run.js";
|
||||
@@ -31,6 +30,8 @@ function shouldLogCoreIngressTiming(): boolean {
|
||||
return process.env.OPENCLAW_DEBUG_INGRESS_TIMING === "1";
|
||||
}
|
||||
|
||||
type ResetCommandAction = "new" | "reset";
|
||||
|
||||
function mergeSkillFilters(channelFilter?: string[], agentFilter?: string[]): string[] | undefined {
|
||||
const normalize = (list?: string[]) => {
|
||||
if (!Array.isArray(list)) {
|
||||
@@ -355,6 +356,7 @@ export async function getReplyFromConfig(
|
||||
if (!resetMatch) {
|
||||
return;
|
||||
}
|
||||
const { emitResetCommandHooks } = await import("./commands-core.runtime.js");
|
||||
const action: ResetCommandAction = resetMatch[1] === "reset" ? "reset" : "new";
|
||||
await emitResetCommandHooks({
|
||||
action,
|
||||
|
||||
52
src/auto-reply/reply/session-fork.runtime.ts
Normal file
52
src/auto-reply/reply/session-fork.runtime.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { resolveSessionFilePath } from "../../config/sessions/paths.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
|
||||
export function forkSessionFromParentRuntime(params: {
|
||||
parentEntry: SessionEntry;
|
||||
agentId: string;
|
||||
sessionsDir: string;
|
||||
}): { sessionId: string; sessionFile: string } | null {
|
||||
const parentSessionFile = resolveSessionFilePath(
|
||||
params.parentEntry.sessionId,
|
||||
params.parentEntry,
|
||||
{ agentId: params.agentId, sessionsDir: params.sessionsDir },
|
||||
);
|
||||
if (!parentSessionFile || !fs.existsSync(parentSessionFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const manager = SessionManager.open(parentSessionFile);
|
||||
const leafId = manager.getLeafId();
|
||||
if (leafId) {
|
||||
const sessionFile = manager.createBranchedSession(leafId) ?? manager.getSessionFile();
|
||||
const sessionId = manager.getSessionId();
|
||||
if (sessionFile && sessionId) {
|
||||
return { sessionId, sessionFile };
|
||||
}
|
||||
}
|
||||
const sessionId = crypto.randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
const sessionFile = path.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: sessionId,
|
||||
timestamp,
|
||||
cwd: manager.getCwd(),
|
||||
parentSession: parentSessionFile,
|
||||
};
|
||||
fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`, {
|
||||
encoding: "utf-8",
|
||||
mode: 0o600,
|
||||
flag: "wx",
|
||||
});
|
||||
return { sessionId, sessionFile };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveSessionFilePath } from "../../config/sessions/paths.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
|
||||
/**
|
||||
@@ -21,44 +16,11 @@ export function resolveParentForkMaxTokens(cfg: OpenClawConfig): number {
|
||||
return DEFAULT_PARENT_FORK_MAX_TOKENS;
|
||||
}
|
||||
|
||||
export function forkSessionFromParent(params: {
|
||||
export async function forkSessionFromParent(params: {
|
||||
parentEntry: SessionEntry;
|
||||
agentId: string;
|
||||
sessionsDir: string;
|
||||
}): { sessionId: string; sessionFile: string } | null {
|
||||
const parentSessionFile = resolveSessionFilePath(
|
||||
params.parentEntry.sessionId,
|
||||
params.parentEntry,
|
||||
{ agentId: params.agentId, sessionsDir: params.sessionsDir },
|
||||
);
|
||||
if (!parentSessionFile || !fs.existsSync(parentSessionFile)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const manager = SessionManager.open(parentSessionFile);
|
||||
const leafId = manager.getLeafId();
|
||||
if (leafId) {
|
||||
const sessionFile = manager.createBranchedSession(leafId) ?? manager.getSessionFile();
|
||||
const sessionId = manager.getSessionId();
|
||||
if (sessionFile && sessionId) {
|
||||
return { sessionId, sessionFile };
|
||||
}
|
||||
}
|
||||
const sessionId = crypto.randomUUID();
|
||||
const timestamp = new Date().toISOString();
|
||||
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
||||
const sessionFile = path.join(manager.getSessionDir(), `${fileTimestamp}_${sessionId}.jsonl`);
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: sessionId,
|
||||
timestamp,
|
||||
cwd: manager.getCwd(),
|
||||
parentSession: parentSessionFile,
|
||||
};
|
||||
fs.writeFileSync(sessionFile, `${JSON.stringify(header)}\n`, "utf-8");
|
||||
return { sessionId, sessionFile };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}): Promise<{ sessionId: string; sessionFile: string } | null> {
|
||||
const runtime = await import("./session-fork.runtime.js");
|
||||
return runtime.forkSessionFromParentRuntime(params);
|
||||
}
|
||||
|
||||
@@ -501,7 +501,7 @@ export async function initSessionState(params: {
|
||||
`forking from parent session: parentKey=${parentSessionKey} → sessionKey=${sessionKey} ` +
|
||||
`parentTokens=${parentTokens}`,
|
||||
);
|
||||
const forked = forkSessionFromParent({
|
||||
const forked = await forkSessionFromParent({
|
||||
parentEntry: sessionStore[parentSessionKey],
|
||||
agentId,
|
||||
sessionsDir: path.dirname(storePath),
|
||||
|
||||
96
src/auto-reply/skill-commands-base.ts
Normal file
96
src/auto-reply/skill-commands-base.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import type { SkillCommandSpec } from "../agents/skills.js";
|
||||
import { getChatCommands } from "./commands-registry.data.js";
|
||||
|
||||
export function listReservedChatSlashCommandNames(extraNames: string[] = []): Set<string> {
|
||||
const reserved = new Set<string>();
|
||||
for (const command of getChatCommands()) {
|
||||
if (command.nativeName) {
|
||||
reserved.add(command.nativeName.toLowerCase());
|
||||
}
|
||||
for (const alias of command.textAliases) {
|
||||
const trimmed = alias.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
reserved.add(trimmed.slice(1).toLowerCase());
|
||||
}
|
||||
}
|
||||
for (const name of extraNames) {
|
||||
const trimmed = name.trim().toLowerCase();
|
||||
if (trimmed) {
|
||||
reserved.add(trimmed);
|
||||
}
|
||||
}
|
||||
return reserved;
|
||||
}
|
||||
|
||||
function normalizeSkillCommandLookup(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, "-");
|
||||
}
|
||||
|
||||
function findSkillCommand(
|
||||
skillCommands: SkillCommandSpec[],
|
||||
rawName: string,
|
||||
): SkillCommandSpec | undefined {
|
||||
const trimmed = rawName.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = trimmed.toLowerCase();
|
||||
const normalized = normalizeSkillCommandLookup(trimmed);
|
||||
return skillCommands.find((entry) => {
|
||||
if (entry.name.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
if (entry.skillName.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
normalizeSkillCommandLookup(entry.name) === normalized ||
|
||||
normalizeSkillCommandLookup(entry.skillName) === normalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSkillCommandInvocation(params: {
|
||||
commandBodyNormalized: string;
|
||||
skillCommands: SkillCommandSpec[];
|
||||
}): { command: SkillCommandSpec; args?: string } | null {
|
||||
const trimmed = params.commandBodyNormalized.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
const match = trimmed.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const commandName = match[1]?.trim().toLowerCase();
|
||||
if (!commandName) {
|
||||
return null;
|
||||
}
|
||||
if (commandName === "skill") {
|
||||
const remainder = match[2]?.trim();
|
||||
if (!remainder) {
|
||||
return null;
|
||||
}
|
||||
const skillMatch = remainder.match(/^([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!skillMatch) {
|
||||
return null;
|
||||
}
|
||||
const skillCommand = findSkillCommand(params.skillCommands, skillMatch[1] ?? "");
|
||||
if (!skillCommand) {
|
||||
return null;
|
||||
}
|
||||
const args = skillMatch[2]?.trim();
|
||||
return { command: skillCommand, args: args || undefined };
|
||||
}
|
||||
const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName);
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
const args = match[2]?.trim();
|
||||
return { command, args: args || undefined };
|
||||
}
|
||||
@@ -1,5 +1 @@
|
||||
export {
|
||||
listReservedChatSlashCommandNames,
|
||||
listSkillCommandsForWorkspace,
|
||||
resolveSkillCommandInvocation,
|
||||
} from "./skill-commands.js";
|
||||
export { listSkillCommandsForAgents, listSkillCommandsForWorkspace } from "./skill-commands.js";
|
||||
|
||||
@@ -8,30 +8,11 @@ import { buildWorkspaceSkillCommandSpecs, type SkillCommandSpec } from "../agent
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import { getRemoteSkillEligibility } from "../infra/skills-remote.js";
|
||||
import { listChatCommands } from "./commands-registry.js";
|
||||
|
||||
export function listReservedChatSlashCommandNames(extraNames: string[] = []): Set<string> {
|
||||
const reserved = new Set<string>();
|
||||
for (const command of listChatCommands()) {
|
||||
if (command.nativeName) {
|
||||
reserved.add(command.nativeName.toLowerCase());
|
||||
}
|
||||
for (const alias of command.textAliases) {
|
||||
const trimmed = alias.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
continue;
|
||||
}
|
||||
reserved.add(trimmed.slice(1).toLowerCase());
|
||||
}
|
||||
}
|
||||
for (const name of extraNames) {
|
||||
const trimmed = name.trim().toLowerCase();
|
||||
if (trimmed) {
|
||||
reserved.add(trimmed);
|
||||
}
|
||||
}
|
||||
return reserved;
|
||||
}
|
||||
import { listReservedChatSlashCommandNames } from "./skill-commands-base.js";
|
||||
export {
|
||||
listReservedChatSlashCommandNames,
|
||||
resolveSkillCommandInvocation,
|
||||
} from "./skill-commands-base.js";
|
||||
|
||||
export function listSkillCommandsForWorkspace(params: {
|
||||
workspaceDir: string;
|
||||
@@ -131,74 +112,3 @@ export function listSkillCommandsForAgents(params: {
|
||||
export const __testing = {
|
||||
dedupeBySkillName,
|
||||
};
|
||||
|
||||
function normalizeSkillCommandLookup(value: string): string {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[\s_]+/g, "-");
|
||||
}
|
||||
|
||||
function findSkillCommand(
|
||||
skillCommands: SkillCommandSpec[],
|
||||
rawName: string,
|
||||
): SkillCommandSpec | undefined {
|
||||
const trimmed = rawName.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const lowered = trimmed.toLowerCase();
|
||||
const normalized = normalizeSkillCommandLookup(trimmed);
|
||||
return skillCommands.find((entry) => {
|
||||
if (entry.name.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
if (entry.skillName.toLowerCase() === lowered) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
normalizeSkillCommandLookup(entry.name) === normalized ||
|
||||
normalizeSkillCommandLookup(entry.skillName) === normalized
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveSkillCommandInvocation(params: {
|
||||
commandBodyNormalized: string;
|
||||
skillCommands: SkillCommandSpec[];
|
||||
}): { command: SkillCommandSpec; args?: string } | null {
|
||||
const trimmed = params.commandBodyNormalized.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
return null;
|
||||
}
|
||||
const match = trimmed.match(/^\/([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const commandName = match[1]?.trim().toLowerCase();
|
||||
if (!commandName) {
|
||||
return null;
|
||||
}
|
||||
if (commandName === "skill") {
|
||||
const remainder = match[2]?.trim();
|
||||
if (!remainder) {
|
||||
return null;
|
||||
}
|
||||
const skillMatch = remainder.match(/^([^\s]+)(?:\s+([\s\S]+))?$/);
|
||||
if (!skillMatch) {
|
||||
return null;
|
||||
}
|
||||
const skillCommand = findSkillCommand(params.skillCommands, skillMatch[1] ?? "");
|
||||
if (!skillCommand) {
|
||||
return null;
|
||||
}
|
||||
const args = skillMatch[2]?.trim();
|
||||
return { command: skillCommand, args: args || undefined };
|
||||
}
|
||||
const command = params.skillCommands.find((entry) => entry.name.toLowerCase() === commandName);
|
||||
if (!command) {
|
||||
return null;
|
||||
}
|
||||
const args = match[2]?.trim();
|
||||
return { command, args: args || undefined };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user