mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
refactor: extract shared dedupe helpers for runtime paths
This commit is contained in:
@@ -42,3 +42,27 @@ export async function requestExecApprovalDecision(
|
||||
: undefined;
|
||||
return typeof decisionValue === "string" ? decisionValue : null;
|
||||
}
|
||||
|
||||
export async function requestExecApprovalDecisionForHost(params: {
|
||||
approvalId: string;
|
||||
command: string;
|
||||
workdir: string;
|
||||
host: "gateway" | "node";
|
||||
security: ExecSecurity;
|
||||
ask: ExecAsk;
|
||||
agentId?: string;
|
||||
resolvedPath?: string;
|
||||
sessionKey?: string;
|
||||
}): Promise<string | null> {
|
||||
return await requestExecApprovalDecision({
|
||||
id: params.approvalId,
|
||||
command: params.command,
|
||||
cwd: params.workdir,
|
||||
host: params.host,
|
||||
security: params.security,
|
||||
ask: params.ask,
|
||||
agentId: params.agentId,
|
||||
resolvedPath: params.resolvedPath,
|
||||
sessionKey: params.sessionKey,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from "../infra/exec-approvals.js";
|
||||
import type { SafeBinProfile } from "../infra/exec-safe-bin-policy.js";
|
||||
import { markBackgrounded, tail } from "./bash-process-registry.js";
|
||||
import { requestExecApprovalDecision } from "./bash-tools.exec-approval-request.js";
|
||||
import { requestExecApprovalDecisionForHost } from "./bash-tools.exec-approval-request.js";
|
||||
import {
|
||||
DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
DEFAULT_NOTIFY_TAIL_CHARS,
|
||||
@@ -81,6 +81,19 @@ export async function processGatewayAllowlist(
|
||||
const analysisOk = allowlistEval.analysisOk;
|
||||
const allowlistSatisfied =
|
||||
hostSecurity === "allowlist" && analysisOk ? allowlistEval.allowlistSatisfied : false;
|
||||
const recordMatchedAllowlistUse = (resolvedPath?: string) => {
|
||||
if (allowlistMatches.length === 0) {
|
||||
return;
|
||||
}
|
||||
const seen = new Set<string>();
|
||||
for (const match of allowlistMatches) {
|
||||
if (seen.has(match.pattern)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(match.pattern);
|
||||
recordAllowlistUse(approvals.file, params.agentId, match, params.command, resolvedPath);
|
||||
}
|
||||
};
|
||||
const hasHeredocSegment = allowlistEval.segments.some((segment) =>
|
||||
segment.argv.some((token) => token.startsWith("<<")),
|
||||
);
|
||||
@@ -113,10 +126,10 @@ export async function processGatewayAllowlist(
|
||||
void (async () => {
|
||||
let decision: string | null = null;
|
||||
try {
|
||||
decision = await requestExecApprovalDecision({
|
||||
id: approvalId,
|
||||
decision = await requestExecApprovalDecisionForHost({
|
||||
approvalId,
|
||||
command: params.command,
|
||||
cwd: params.workdir,
|
||||
workdir: params.workdir,
|
||||
host: "gateway",
|
||||
security: hostSecurity,
|
||||
ask: hostAsk,
|
||||
@@ -186,22 +199,7 @@ export async function processGatewayAllowlist(
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowlistMatches.length > 0) {
|
||||
const seen = new Set<string>();
|
||||
for (const match of allowlistMatches) {
|
||||
if (seen.has(match.pattern)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(match.pattern);
|
||||
recordAllowlistUse(
|
||||
approvals.file,
|
||||
params.agentId,
|
||||
match,
|
||||
params.command,
|
||||
resolvedPath ?? undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
recordMatchedAllowlistUse(resolvedPath ?? undefined);
|
||||
|
||||
let run: Awaited<ReturnType<typeof runExecProcess>> | null = null;
|
||||
try {
|
||||
@@ -321,22 +319,7 @@ export async function processGatewayAllowlist(
|
||||
}
|
||||
}
|
||||
|
||||
if (allowlistMatches.length > 0) {
|
||||
const seen = new Set<string>();
|
||||
for (const match of allowlistMatches) {
|
||||
if (seen.has(match.pattern)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(match.pattern);
|
||||
recordAllowlistUse(
|
||||
approvals.file,
|
||||
params.agentId,
|
||||
match,
|
||||
params.command,
|
||||
allowlistEval.segments[0]?.resolution?.resolvedPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
recordMatchedAllowlistUse(allowlistEval.segments[0]?.resolution?.resolvedPath);
|
||||
|
||||
return { execCommandOverride };
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
resolveExecApprovalsFromFile,
|
||||
} from "../infra/exec-approvals.js";
|
||||
import { buildNodeShellCommand } from "../infra/node-shell.js";
|
||||
import { requestExecApprovalDecision } from "./bash-tools.exec-approval-request.js";
|
||||
import { requestExecApprovalDecisionForHost } from "./bash-tools.exec-approval-request.js";
|
||||
import {
|
||||
DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
createApprovalSlug,
|
||||
@@ -178,10 +178,10 @@ export async function executeNodeHostCommand(
|
||||
void (async () => {
|
||||
let decision: string | null = null;
|
||||
try {
|
||||
decision = await requestExecApprovalDecision({
|
||||
id: approvalId,
|
||||
decision = await requestExecApprovalDecisionForHost({
|
||||
approvalId,
|
||||
command: params.command,
|
||||
cwd: params.workdir,
|
||||
workdir: params.workdir,
|
||||
host: "node",
|
||||
security: hostSecurity,
|
||||
ask: hostAsk,
|
||||
|
||||
@@ -317,35 +317,10 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
|
||||
}
|
||||
return `\`\`\`txt\n${trimmed}\n\`\`\``;
|
||||
};
|
||||
const emitToolSummary = (toolName?: string, meta?: string) => {
|
||||
const emitToolResultMessage = (toolName: string | undefined, message: string) => {
|
||||
if (!params.onToolResult) {
|
||||
return;
|
||||
}
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
const { text: cleanedText, mediaUrls } = parseReplyDirectives(agg);
|
||||
const filteredMediaUrls = filterToolResultMediaUrls(toolName, mediaUrls ?? []);
|
||||
if (!cleanedText && filteredMediaUrls.length === 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
void params.onToolResult({
|
||||
text: cleanedText,
|
||||
mediaUrls: filteredMediaUrls.length ? filteredMediaUrls : undefined,
|
||||
});
|
||||
} catch {
|
||||
// ignore tool result delivery failures
|
||||
}
|
||||
};
|
||||
const emitToolOutput = (toolName?: string, meta?: string, output?: string) => {
|
||||
if (!params.onToolResult || !output) {
|
||||
return;
|
||||
}
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
const message = `${agg}\n${formatToolOutputBlock(output)}`;
|
||||
const { text: cleanedText, mediaUrls } = parseReplyDirectives(message);
|
||||
const filteredMediaUrls = filterToolResultMediaUrls(toolName, mediaUrls ?? []);
|
||||
if (!cleanedText && filteredMediaUrls.length === 0) {
|
||||
@@ -360,6 +335,22 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
|
||||
// ignore tool result delivery failures
|
||||
}
|
||||
};
|
||||
const emitToolSummary = (toolName?: string, meta?: string) => {
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
emitToolResultMessage(toolName, agg);
|
||||
};
|
||||
const emitToolOutput = (toolName?: string, meta?: string, output?: string) => {
|
||||
if (!output) {
|
||||
return;
|
||||
}
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
const message = `${agg}\n${formatToolOutputBlock(output)}`;
|
||||
emitToolResultMessage(toolName, message);
|
||||
};
|
||||
|
||||
const stripBlockTags = (
|
||||
text: string,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { evaluateEntryMetadataRequirementsForCurrentPlatform } from "../shared/entry-status.js";
|
||||
import { evaluateEntryRequirementsForCurrentPlatform } from "../shared/entry-status.js";
|
||||
import type { RequirementConfigCheck, Requirements } from "../shared/requirements.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
import {
|
||||
@@ -191,17 +191,15 @@ function buildSkillStatus(
|
||||
? bundledNames.has(entry.skill.name)
|
||||
: entry.skill.source === "openclaw-bundled";
|
||||
|
||||
const requirementStatus = evaluateEntryMetadataRequirementsForCurrentPlatform({
|
||||
always,
|
||||
metadata: entry.metadata,
|
||||
frontmatter: entry.frontmatter,
|
||||
hasLocalBin: hasBinary,
|
||||
remote: eligibility?.remote,
|
||||
isEnvSatisfied,
|
||||
isConfigSatisfied,
|
||||
});
|
||||
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } =
|
||||
requirementStatus;
|
||||
evaluateEntryRequirementsForCurrentPlatform({
|
||||
always,
|
||||
entry,
|
||||
hasLocalBin: hasBinary,
|
||||
remote: eligibility?.remote,
|
||||
isEnvSatisfied,
|
||||
isConfigSatisfied,
|
||||
});
|
||||
const eligible = !disabled && !blockedByAllowlist && requirementsSatisfied;
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
||||
import type { ResolvedTimeFormat } from "./date-time.js";
|
||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||
import type { EmbeddedSandboxInfo } from "./pi-embedded-runner/types.js";
|
||||
import { sanitizeForPromptLiteral } from "./sanitize-for-prompt.js";
|
||||
|
||||
/**
|
||||
@@ -229,20 +230,7 @@ export function buildAgentSystemPrompt(params: {
|
||||
repoRoot?: string;
|
||||
};
|
||||
messageToolHints?: string[];
|
||||
sandboxInfo?: {
|
||||
enabled: boolean;
|
||||
workspaceDir?: string;
|
||||
containerWorkspaceDir?: string;
|
||||
workspaceAccess?: "none" | "ro" | "rw";
|
||||
agentWorkspaceMount?: string;
|
||||
browserBridgeUrl?: string;
|
||||
browserNoVncUrl?: string;
|
||||
hostBrowserAllowed?: boolean;
|
||||
elevated?: {
|
||||
allowed: boolean;
|
||||
defaultLevel: "on" | "off" | "ask" | "full";
|
||||
};
|
||||
};
|
||||
sandboxInfo?: EmbeddedSandboxInfo;
|
||||
/** Reaction guidance for the agent (for Telegram minimal/extensive modes). */
|
||||
reactionGuidance?: {
|
||||
level: "minimal" | "extensive";
|
||||
|
||||
@@ -266,6 +266,46 @@ function processLine(
|
||||
return enhanced;
|
||||
}
|
||||
|
||||
type InteractiveSnapshotLine = NonNullable<ReturnType<typeof matchInteractiveSnapshotLine>>;
|
||||
|
||||
function buildInteractiveSnapshotLines(params: {
|
||||
lines: string[];
|
||||
options: RoleSnapshotOptions;
|
||||
resolveRef: (parsed: InteractiveSnapshotLine) => { ref: string; nth?: number } | null;
|
||||
recordRef: (parsed: InteractiveSnapshotLine, ref: string, nth?: number) => void;
|
||||
includeSuffix: (suffix: string) => boolean;
|
||||
}): string[] {
|
||||
const out: string[] = [];
|
||||
for (const line of params.lines) {
|
||||
const parsed = matchInteractiveSnapshotLine(line, params.options);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
if (!INTERACTIVE_ROLES.has(parsed.role)) {
|
||||
continue;
|
||||
}
|
||||
const resolved = params.resolveRef(parsed);
|
||||
if (!resolved?.ref) {
|
||||
continue;
|
||||
}
|
||||
params.recordRef(parsed, resolved.ref, resolved.nth);
|
||||
|
||||
let enhanced = `- ${parsed.roleRaw}`;
|
||||
if (parsed.name) {
|
||||
enhanced += ` "${parsed.name}"`;
|
||||
}
|
||||
enhanced += ` [ref=${resolved.ref}]`;
|
||||
if ((resolved.nth ?? 0) > 0) {
|
||||
enhanced += ` [nth=${resolved.nth}]`;
|
||||
}
|
||||
if (params.includeSuffix(parsed.suffix)) {
|
||||
enhanced += parsed.suffix;
|
||||
}
|
||||
out.push(enhanced);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseRoleRef(raw: string): string | null {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
@@ -294,39 +334,24 @@ export function buildRoleSnapshotFromAriaSnapshot(
|
||||
};
|
||||
|
||||
if (options.interactive) {
|
||||
const result: string[] = [];
|
||||
for (const line of lines) {
|
||||
const parsed = matchInteractiveSnapshotLine(line, options);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const { roleRaw, role, name, suffix } = parsed;
|
||||
if (!INTERACTIVE_ROLES.has(role)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const ref = nextRef();
|
||||
const nth = tracker.getNextIndex(role, name);
|
||||
tracker.trackRef(role, name, ref);
|
||||
refs[ref] = {
|
||||
role,
|
||||
name,
|
||||
nth,
|
||||
};
|
||||
|
||||
let enhanced = `- ${roleRaw}`;
|
||||
if (name) {
|
||||
enhanced += ` "${name}"`;
|
||||
}
|
||||
enhanced += ` [ref=${ref}]`;
|
||||
if (nth > 0) {
|
||||
enhanced += ` [nth=${nth}]`;
|
||||
}
|
||||
if (suffix.includes("[")) {
|
||||
enhanced += suffix;
|
||||
}
|
||||
result.push(enhanced);
|
||||
}
|
||||
const result = buildInteractiveSnapshotLines({
|
||||
lines,
|
||||
options,
|
||||
resolveRef: ({ role, name }) => {
|
||||
const ref = nextRef();
|
||||
const nth = tracker.getNextIndex(role, name);
|
||||
tracker.trackRef(role, name, ref);
|
||||
return { ref, nth };
|
||||
},
|
||||
recordRef: ({ role, name }, ref, nth) => {
|
||||
refs[ref] = {
|
||||
role,
|
||||
name,
|
||||
nth,
|
||||
};
|
||||
},
|
||||
includeSuffix: (suffix) => suffix.includes("["),
|
||||
});
|
||||
|
||||
removeNthFromNonDuplicates(refs, tracker);
|
||||
|
||||
@@ -370,23 +395,18 @@ export function buildRoleSnapshotFromAiSnapshot(
|
||||
const refs: RoleRefMap = {};
|
||||
|
||||
if (options.interactive) {
|
||||
const out: string[] = [];
|
||||
for (const line of lines) {
|
||||
const parsed = matchInteractiveSnapshotLine(line, options);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const { roleRaw, role, name, suffix } = parsed;
|
||||
if (!INTERACTIVE_ROLES.has(role)) {
|
||||
continue;
|
||||
}
|
||||
const ref = parseAiSnapshotRef(suffix);
|
||||
if (!ref) {
|
||||
continue;
|
||||
}
|
||||
refs[ref] = { role, ...(name ? { name } : {}) };
|
||||
out.push(`- ${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
||||
}
|
||||
const out = buildInteractiveSnapshotLines({
|
||||
lines,
|
||||
options,
|
||||
resolveRef: ({ suffix }) => {
|
||||
const ref = parseAiSnapshotRef(suffix);
|
||||
return ref ? { ref } : null;
|
||||
},
|
||||
recordRef: ({ role, name }, ref) => {
|
||||
refs[ref] = { role, ...(name ? { name } : {}) };
|
||||
},
|
||||
includeSuffix: () => true,
|
||||
});
|
||||
return {
|
||||
snapshot: out.join("\n") || "(no interactive elements)",
|
||||
refs,
|
||||
|
||||
@@ -200,6 +200,21 @@ function parseSessionKeyFromPayloadJSON(payloadJSON: string): string | null {
|
||||
return sessionKey.length > 0 ? sessionKey : null;
|
||||
}
|
||||
|
||||
function parsePayloadObject(payloadJSON?: string | null): Record<string, unknown> | null {
|
||||
if (!payloadJSON) {
|
||||
return null;
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(payloadJSON) as unknown;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return typeof payload === "object" && payload !== null
|
||||
? (payload as Record<string, unknown>)
|
||||
: null;
|
||||
}
|
||||
|
||||
async function sendReceiptAck(params: {
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
deps: NodeEventContext["deps"];
|
||||
@@ -232,17 +247,10 @@ async function sendReceiptAck(params: {
|
||||
export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt: NodeEvent) => {
|
||||
switch (evt.event) {
|
||||
case "voice.transcript": {
|
||||
if (!evt.payloadJSON) {
|
||||
const obj = parsePayloadObject(evt.payloadJSON);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(evt.payloadJSON) as unknown;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const obj =
|
||||
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
|
||||
const text = typeof obj.text === "string" ? obj.text.trim() : "";
|
||||
if (!text) {
|
||||
return;
|
||||
@@ -455,17 +463,10 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
|
||||
case "exec.started":
|
||||
case "exec.finished":
|
||||
case "exec.denied": {
|
||||
if (!evt.payloadJSON) {
|
||||
const obj = parsePayloadObject(evt.payloadJSON);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(evt.payloadJSON) as unknown;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const obj =
|
||||
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
|
||||
const sessionKey =
|
||||
typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : `node-${nodeId}`;
|
||||
if (!sessionKey) {
|
||||
@@ -519,17 +520,10 @@ export const handleNodeEvent = async (ctx: NodeEventContext, nodeId: string, evt
|
||||
return;
|
||||
}
|
||||
case "push.apns.register": {
|
||||
if (!evt.payloadJSON) {
|
||||
const obj = parsePayloadObject(evt.payloadJSON);
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = JSON.parse(evt.payloadJSON) as unknown;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const obj =
|
||||
typeof payload === "object" && payload !== null ? (payload as Record<string, unknown>) : {};
|
||||
const token = typeof obj.token === "string" ? obj.token : "";
|
||||
const topic = typeof obj.topic === "string" ? obj.topic : "";
|
||||
const environment = obj.environment;
|
||||
|
||||
@@ -1,23 +1,9 @@
|
||||
import { vi } from "vitest";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import { createEmptyPluginRegistry, type PluginRegistry } from "../plugins/registry.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
|
||||
export const registryState: { registry: PluginRegistry } = {
|
||||
registry: {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
hooks: [],
|
||||
typedHooks: [],
|
||||
channels: [],
|
||||
providers: [],
|
||||
gatewayHandlers: {},
|
||||
httpHandlers: [],
|
||||
httpRoutes: [],
|
||||
cliRegistrars: [],
|
||||
services: [],
|
||||
commands: [],
|
||||
diagnostics: [],
|
||||
} as PluginRegistry,
|
||||
registry: createEmptyPluginRegistry(),
|
||||
};
|
||||
|
||||
export function setRegistry(registry: PluginRegistry) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { evaluateEntryMetadataRequirementsForCurrentPlatform } from "../shared/entry-status.js";
|
||||
import { evaluateEntryRequirementsForCurrentPlatform } from "../shared/entry-status.js";
|
||||
import type { RequirementConfigCheck, Requirements } from "../shared/requirements.js";
|
||||
import { CONFIG_DIR } from "../utils.js";
|
||||
import { hasBinary, isConfigPathTruthy, resolveHookConfig } from "./config.js";
|
||||
@@ -91,17 +91,15 @@ function buildHookStatus(
|
||||
Boolean(process.env[envName] || hookConfig?.env?.[envName]);
|
||||
const isConfigSatisfied = (pathStr: string) => isConfigPathTruthy(config, pathStr);
|
||||
|
||||
const requirementStatus = evaluateEntryMetadataRequirementsForCurrentPlatform({
|
||||
always,
|
||||
metadata: entry.metadata,
|
||||
frontmatter: entry.frontmatter,
|
||||
hasLocalBin: hasBinary,
|
||||
remote: eligibility?.remote,
|
||||
isEnvSatisfied,
|
||||
isConfigSatisfied,
|
||||
});
|
||||
const { emoji, homepage, required, missing, requirementsSatisfied, configChecks } =
|
||||
requirementStatus;
|
||||
evaluateEntryRequirementsForCurrentPlatform({
|
||||
always,
|
||||
entry,
|
||||
hasLocalBin: hasBinary,
|
||||
remote: eligibility?.remote,
|
||||
isEnvSatisfied,
|
||||
isConfigSatisfied,
|
||||
});
|
||||
|
||||
const eligible = !disabled && requirementsSatisfied;
|
||||
|
||||
|
||||
@@ -346,30 +346,7 @@ export function unwrapDispatchWrappersForResolution(
|
||||
}
|
||||
|
||||
function extractPosixShellInlineCommand(argv: string[]): string | null {
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
continue;
|
||||
}
|
||||
const lower = token.toLowerCase();
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (POSIX_INLINE_COMMAND_FLAGS.has(lower)) {
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
if (/^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
if (inline) {
|
||||
return inline;
|
||||
}
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return extractInlineCommandByFlags(argv, POSIX_INLINE_COMMAND_FLAGS, { allowCombinedC: true });
|
||||
}
|
||||
|
||||
function extractCmdInlineCommand(argv: string[]): string | null {
|
||||
@@ -389,6 +366,14 @@ function extractCmdInlineCommand(argv: string[]): string | null {
|
||||
}
|
||||
|
||||
function extractPowerShellInlineCommand(argv: string[]): string | null {
|
||||
return extractInlineCommandByFlags(argv, POWERSHELL_INLINE_COMMAND_FLAGS);
|
||||
}
|
||||
|
||||
function extractInlineCommandByFlags(
|
||||
argv: string[],
|
||||
flags: ReadonlySet<string>,
|
||||
options: { allowCombinedC?: boolean } = {},
|
||||
): string | null {
|
||||
for (let i = 1; i < argv.length; i += 1) {
|
||||
const token = argv[i]?.trim();
|
||||
if (!token) {
|
||||
@@ -398,7 +383,16 @@ function extractPowerShellInlineCommand(argv: string[]): string | null {
|
||||
if (lower === "--") {
|
||||
break;
|
||||
}
|
||||
if (POWERSHELL_INLINE_COMMAND_FLAGS.has(lower)) {
|
||||
if (flags.has(lower)) {
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
if (options.allowCombinedC && /^-[^-]*c[^-]*$/i.test(token)) {
|
||||
const commandIndex = lower.indexOf("c");
|
||||
const inline = token.slice(commandIndex + 1).trim();
|
||||
if (inline) {
|
||||
return inline;
|
||||
}
|
||||
const cmd = argv[i + 1]?.trim();
|
||||
return cmd ? cmd : null;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { resolveRemoteEmbeddingBearerClient } from "./embeddings-remote-client.js";
|
||||
import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js";
|
||||
import {
|
||||
createRemoteEmbeddingProvider,
|
||||
resolveRemoteEmbeddingClient,
|
||||
} from "./embeddings-remote-provider.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
|
||||
|
||||
export type MistralEmbeddingClient = {
|
||||
@@ -28,31 +30,13 @@ export async function createMistralEmbeddingProvider(
|
||||
options: EmbeddingProviderOptions,
|
||||
): Promise<{ provider: EmbeddingProvider; client: MistralEmbeddingClient }> {
|
||||
const client = await resolveMistralEmbeddingClient(options);
|
||||
const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
|
||||
|
||||
const embed = async (input: string[]): Promise<number[][]> => {
|
||||
if (input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await fetchRemoteEmbeddingVectors({
|
||||
url,
|
||||
headers: client.headers,
|
||||
ssrfPolicy: client.ssrfPolicy,
|
||||
body: { model: client.model, input },
|
||||
errorPrefix: "mistral embeddings failed",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
provider: {
|
||||
provider: createRemoteEmbeddingProvider({
|
||||
id: "mistral",
|
||||
model: client.model,
|
||||
embedQuery: async (text) => {
|
||||
const [vec] = await embed([text]);
|
||||
return vec ?? [];
|
||||
},
|
||||
embedBatch: embed,
|
||||
},
|
||||
client,
|
||||
errorPrefix: "mistral embeddings failed",
|
||||
}),
|
||||
client,
|
||||
};
|
||||
}
|
||||
@@ -60,11 +44,10 @@ export async function createMistralEmbeddingProvider(
|
||||
export async function resolveMistralEmbeddingClient(
|
||||
options: EmbeddingProviderOptions,
|
||||
): Promise<MistralEmbeddingClient> {
|
||||
const { baseUrl, headers, ssrfPolicy } = await resolveRemoteEmbeddingBearerClient({
|
||||
return await resolveRemoteEmbeddingClient({
|
||||
provider: "mistral",
|
||||
options,
|
||||
defaultBaseUrl: DEFAULT_MISTRAL_BASE_URL,
|
||||
normalizeModel: normalizeMistralModel,
|
||||
});
|
||||
const model = normalizeMistralModel(options.model);
|
||||
return { baseUrl, headers, ssrfPolicy, model };
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import { resolveRemoteEmbeddingBearerClient } from "./embeddings-remote-client.js";
|
||||
import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js";
|
||||
import {
|
||||
createRemoteEmbeddingProvider,
|
||||
resolveRemoteEmbeddingClient,
|
||||
} from "./embeddings-remote-provider.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
|
||||
|
||||
export type OpenAiEmbeddingClient = {
|
||||
@@ -33,32 +35,14 @@ export async function createOpenAiEmbeddingProvider(
|
||||
options: EmbeddingProviderOptions,
|
||||
): Promise<{ provider: EmbeddingProvider; client: OpenAiEmbeddingClient }> {
|
||||
const client = await resolveOpenAiEmbeddingClient(options);
|
||||
const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
|
||||
|
||||
const embed = async (input: string[]): Promise<number[][]> => {
|
||||
if (input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await fetchRemoteEmbeddingVectors({
|
||||
url,
|
||||
headers: client.headers,
|
||||
ssrfPolicy: client.ssrfPolicy,
|
||||
body: { model: client.model, input },
|
||||
errorPrefix: "openai embeddings failed",
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
provider: {
|
||||
provider: createRemoteEmbeddingProvider({
|
||||
id: "openai",
|
||||
model: client.model,
|
||||
client,
|
||||
errorPrefix: "openai embeddings failed",
|
||||
maxInputTokens: OPENAI_MAX_INPUT_TOKENS[client.model],
|
||||
embedQuery: async (text) => {
|
||||
const [vec] = await embed([text]);
|
||||
return vec ?? [];
|
||||
},
|
||||
embedBatch: embed,
|
||||
},
|
||||
}),
|
||||
client,
|
||||
};
|
||||
}
|
||||
@@ -66,11 +50,10 @@ export async function createOpenAiEmbeddingProvider(
|
||||
export async function resolveOpenAiEmbeddingClient(
|
||||
options: EmbeddingProviderOptions,
|
||||
): Promise<OpenAiEmbeddingClient> {
|
||||
const { baseUrl, headers, ssrfPolicy } = await resolveRemoteEmbeddingBearerClient({
|
||||
return await resolveRemoteEmbeddingClient({
|
||||
provider: "openai",
|
||||
options,
|
||||
defaultBaseUrl: DEFAULT_OPENAI_BASE_URL,
|
||||
normalizeModel: normalizeOpenAiModel,
|
||||
});
|
||||
const model = normalizeOpenAiModel(options.model);
|
||||
return { baseUrl, headers, ssrfPolicy, model };
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import type { EmbeddingProviderOptions } from "./embeddings.js";
|
||||
import { buildRemoteBaseUrlPolicy } from "./remote-http.js";
|
||||
|
||||
type RemoteEmbeddingProviderId = "openai" | "voyage" | "mistral";
|
||||
export type RemoteEmbeddingProviderId = "openai" | "voyage" | "mistral";
|
||||
|
||||
export async function resolveRemoteEmbeddingBearerClient(params: {
|
||||
provider: RemoteEmbeddingProviderId;
|
||||
|
||||
63
src/memory/embeddings-remote-provider.ts
Normal file
63
src/memory/embeddings-remote-provider.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
import {
|
||||
resolveRemoteEmbeddingBearerClient,
|
||||
type RemoteEmbeddingProviderId,
|
||||
} from "./embeddings-remote-client.js";
|
||||
import { fetchRemoteEmbeddingVectors } from "./embeddings-remote-fetch.js";
|
||||
import type { EmbeddingProvider, EmbeddingProviderOptions } from "./embeddings.js";
|
||||
|
||||
export type RemoteEmbeddingClient = {
|
||||
baseUrl: string;
|
||||
headers: Record<string, string>;
|
||||
ssrfPolicy?: SsrFPolicy;
|
||||
model: string;
|
||||
};
|
||||
|
||||
export function createRemoteEmbeddingProvider(params: {
|
||||
id: string;
|
||||
client: RemoteEmbeddingClient;
|
||||
errorPrefix: string;
|
||||
maxInputTokens?: number;
|
||||
}): EmbeddingProvider {
|
||||
const { client } = params;
|
||||
const url = `${client.baseUrl.replace(/\/$/, "")}/embeddings`;
|
||||
|
||||
const embed = async (input: string[]): Promise<number[][]> => {
|
||||
if (input.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await fetchRemoteEmbeddingVectors({
|
||||
url,
|
||||
headers: client.headers,
|
||||
ssrfPolicy: client.ssrfPolicy,
|
||||
body: { model: client.model, input },
|
||||
errorPrefix: params.errorPrefix,
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
id: params.id,
|
||||
model: client.model,
|
||||
...(typeof params.maxInputTokens === "number" ? { maxInputTokens: params.maxInputTokens } : {}),
|
||||
embedQuery: async (text) => {
|
||||
const [vec] = await embed([text]);
|
||||
return vec ?? [];
|
||||
},
|
||||
embedBatch: embed,
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveRemoteEmbeddingClient(params: {
|
||||
provider: RemoteEmbeddingProviderId;
|
||||
options: EmbeddingProviderOptions;
|
||||
defaultBaseUrl: string;
|
||||
normalizeModel: (model: string) => string;
|
||||
}): Promise<RemoteEmbeddingClient> {
|
||||
const { baseUrl, headers, ssrfPolicy } = await resolveRemoteEmbeddingBearerClient({
|
||||
provider: params.provider,
|
||||
options: params.options,
|
||||
defaultBaseUrl: params.defaultBaseUrl,
|
||||
});
|
||||
const model = params.normalizeModel(params.options.model);
|
||||
return { baseUrl, headers, ssrfPolicy, model };
|
||||
}
|
||||
@@ -64,3 +64,30 @@ export function evaluateEntryMetadataRequirementsForCurrentPlatform(
|
||||
localPlatform: process.platform,
|
||||
});
|
||||
}
|
||||
|
||||
export function evaluateEntryRequirementsForCurrentPlatform(params: {
|
||||
always: boolean;
|
||||
entry: {
|
||||
metadata?: (RequirementsMetadata & { emoji?: string; homepage?: string }) | null;
|
||||
frontmatter?: {
|
||||
emoji?: string;
|
||||
homepage?: string;
|
||||
website?: string;
|
||||
url?: string;
|
||||
} | null;
|
||||
};
|
||||
hasLocalBin: (bin: string) => boolean;
|
||||
remote?: RequirementRemote;
|
||||
isEnvSatisfied: (envName: string) => boolean;
|
||||
isConfigSatisfied: (pathStr: string) => boolean;
|
||||
}): ReturnType<typeof evaluateEntryMetadataRequirements> {
|
||||
return evaluateEntryMetadataRequirementsForCurrentPlatform({
|
||||
always: params.always,
|
||||
metadata: params.entry.metadata,
|
||||
frontmatter: params.entry.frontmatter,
|
||||
hasLocalBin: params.hasLocalBin,
|
||||
remote: params.remote,
|
||||
isEnvSatisfied: params.isEnvSatisfied,
|
||||
isConfigSatisfied: params.isConfigSatisfied,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user