fix: adding login and fixing the plugin to follow the openclaw plugin standards (#4686)

This commit is contained in:
Kartik
2026-04-03 20:59:23 +05:30
committed by GitHub
parent 66230b3f1f
commit cfb3f58e4a
33 changed files with 5254 additions and 1376 deletions
+129 -45
View File
@@ -14,19 +14,27 @@ import type { SkillsConfig, CategoryConfig } from "./types.ts";
const DEFAULT_CATEGORIES: Record<string, CategoryConfig> = {
configuration: { importance: 0.95, ttl: null },
rule: { importance: 0.90, ttl: null },
rule: { importance: 0.9, ttl: null },
identity: { importance: 0.95, ttl: null, immutable: true },
preference: { importance: 0.85, ttl: null },
decision: { importance: 0.80, ttl: null },
technical: { importance: 0.80, ttl: null },
decision: { importance: 0.8, ttl: null },
technical: { importance: 0.8, ttl: null },
relationship: { importance: 0.75, ttl: null },
project: { importance: 0.75, ttl: "90d" },
operational: { importance: 0.60, ttl: "7d" },
operational: { importance: 0.6, ttl: "7d" },
};
const DEFAULT_CREDENTIAL_PATTERNS = [
"sk-", "m0-", "ghp_", "AKIA", "ak_", "Bearer ",
"bot\\d+:AA", "password=", "token=", "secret=",
"sk-",
"m0-",
"ghp_",
"AKIA",
"ak_",
"Bearer ",
"bot\\d+:AA",
"password=",
"token=",
"secret=",
];
// ============================================================================
@@ -91,7 +99,9 @@ function resolveSkillsDir(): string {
const metaDir = path.dirname(fileURLToPath(import.meta.url));
candidates.push(path.join(metaDir, "skills"));
candidates.push(path.join(metaDir, "..", "skills"));
} catch { /* import.meta.url may not be available */ }
} catch {
/* import.meta.url may not be available */
}
// Strategy 2: __dirname (works in CJS / jiti)
if (typeof __dirname !== "undefined") {
@@ -120,8 +130,10 @@ const RESOLVED_SKILLS_DIR = path.resolve(SKILLS_DIR);
*/
export function safePath(...segments: string[]): string | null {
const resolved = path.resolve(SKILLS_DIR, ...segments);
if (resolved !== RESOLVED_SKILLS_DIR &&
!resolved.startsWith(RESOLVED_SKILLS_DIR + path.sep)) {
if (
resolved !== RESOLVED_SKILLS_DIR &&
!resolved.startsWith(RESOLVED_SKILLS_DIR + path.sep)
) {
return null;
}
return resolved;
@@ -163,12 +175,18 @@ function readDomainOverlay(domain: string, targetSkill: string): string | null {
// Config Injection — render user-configured knobs into prompt text
// ============================================================================
function renderCategoriesBlock(categories: Record<string, CategoryConfig>): string {
const lines: string[] = ["\n## Active Category Configuration (overrides defaults above)\n"];
function renderCategoriesBlock(
categories: Record<string, CategoryConfig>,
): string {
const lines: string[] = [
"\n## Active Category Configuration (overrides defaults above)\n",
];
for (const [name, cat] of Object.entries(categories)) {
const ttlLabel = cat.ttl ? `expires: ${cat.ttl}` : "permanent";
const immLabel = cat.immutable ? ", immutable" : "";
lines.push(`- **${name.toUpperCase()}** (importance: ${cat.importance} | ${ttlLabel}${immLabel})`);
lines.push(
`- **${name.toUpperCase()}** (importance: ${cat.importance} | ${ttlLabel}${immLabel})`,
);
}
return lines.join("\n");
}
@@ -180,7 +198,9 @@ function renderTriageKnobs(config: SkillsConfig): string {
const lines: string[] = [];
if (triage.importanceThreshold !== undefined) {
lines.push(`- Only store facts with importance >= ${triage.importanceThreshold}`);
lines.push(
`- Only store facts with importance >= ${triage.importanceThreshold}`,
);
}
const patterns = resolveCredentialPatterns(config);
@@ -297,19 +317,29 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
// are not part of the SKILL.md (tool format, batching, search protocol).
const parts: string[] = [];
parts.push("<memory-system>");
parts.push("IMPORTANT: Use `memory_store` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).");
parts.push(
"IMPORTANT: Use `memory_store` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
);
parts.push("");
parts.push(triage.prompt);
parts.push("");
parts.push("## Tool Usage");
parts.push("");
parts.push("Batch facts by CATEGORY. All facts in one memory_store call must share the same category because category determines retention policy (TTL, immutability). If a turn has facts in different categories, make one call per category.");
parts.push(
"Batch facts by CATEGORY. All facts in one memory_store call must share the same category because category determines retention policy (TTL, immutability). If a turn has facts in different categories, make one call per category.",
);
parts.push("");
parts.push("FORMAT (single category):");
parts.push(' memory_store(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")');
parts.push(
' memory_store(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
);
parts.push("FORMAT (mixed categories in one turn, separate calls):");
parts.push(' memory_store(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")');
parts.push(' memory_store(facts: ["As of 2026-04-01, migrating from Postgres to CockroachDB"], category: "decision")');
parts.push(
' memory_store(facts: ["User is Alex, backend engineer at Stripe, PST timezone"], category: "identity")',
);
parts.push(
' memory_store(facts: ["As of 2026-04-01, migrating from Postgres to CockroachDB"], category: "decision")',
);
// Only include search instructions if recall is enabled
if (config.recall?.enabled !== false) {
const strategy = config.recall?.strategy ?? "smart";
@@ -319,39 +349,79 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
// In manual mode, the agent is fully responsible for all search
if (strategy === "manual") {
parts.push("You control all memory search. No automatic recall happens. Use memory_search proactively:");
parts.push("- At the start of a new conversation, search for user identity and context.");
parts.push("- When the user references something you do not have context for.");
parts.push(
"You control all memory search. No automatic recall happens. Use memory_search proactively:",
);
parts.push(
"- At the start of a new conversation, search for user identity and context.",
);
parts.push(
"- When the user references something you do not have context for.",
);
parts.push("- When the conversation topic shifts to a new domain.");
parts.push("- Before updating a memory, search to find the existing version.");
parts.push(
"- Before updating a memory, search to find the existing version.",
);
parts.push("");
}
parts.push("When calling memory_search, ALWAYS rewrite the query. NEVER pass the user's raw message.");
parts.push("Stored memories are third-person factual statements. Write a query that matches storage language, not conversation language.");
parts.push("Process: (1) Name your target. (2) Extract signal: proper nouns, technical terms, domain concepts. (3) Bridge to storage language: add terms the stored memory contains (user, decided, prefers, rule, configured, based in). (4) Compose 3-6 keywords.");
parts.push('WRONG: memory_search("Who was that nutritionist my wife recommended?")');
parts.push('RIGHT: memory_search("nutritionist wife recommended relationship")');
parts.push(
"When calling memory_search, ALWAYS rewrite the query. NEVER pass the user's raw message.",
);
parts.push(
"Stored memories are third-person factual statements. Write a query that matches storage language, not conversation language.",
);
parts.push(
"Process: (1) Name your target. (2) Extract signal: proper nouns, technical terms, domain concepts. (3) Bridge to storage language: add terms the stored memory contains (user, decided, prefers, rule, configured, based in). (4) Compose 3-6 keywords.",
);
parts.push(
'WRONG: memory_search("Who was that nutritionist my wife recommended?")',
);
parts.push(
'RIGHT: memory_search("nutritionist wife recommended relationship")',
);
parts.push('WRONG: memory_search("What timezone am I in?")');
parts.push('RIGHT: memory_search("user timezone location based")');
parts.push("");
parts.push("ENTITY SCOPING: Memories are scoped by user_id, agent_id, and run_id. You do not need to pass these in most cases. The plugin handles scoping automatically based on the current session.");
parts.push("- Default behavior: all memory operations use the configured userId and current session. You do not need to pass userId or agentId.");
parts.push("- Use agentId only when you need to read or write memories for a DIFFERENT agent (e.g., querying what the 'researcher' agent knows). This accesses a separate namespace.");
parts.push("- Use userId only when explicitly instructed to operate on a different user's memories.");
parts.push("- Do not pass run_id directly. The plugin manages session scoping through the scope parameter.");
parts.push("- In multi-agent setups, each agent has isolated memory. The main agent's memories are separate from subagent memories.");
parts.push(
"ENTITY SCOPING: Memories are scoped by user_id, agent_id, and run_id. You do not need to pass these in most cases. The plugin handles scoping automatically based on the current session.",
);
parts.push(
"- Default behavior: all memory operations use the configured userId and current session. You do not need to pass userId or agentId.",
);
parts.push(
"- Use agentId only when you need to read or write memories for a DIFFERENT agent (e.g., querying what the 'researcher' agent knows). This accesses a separate namespace.",
);
parts.push(
"- Use userId only when explicitly instructed to operate on a different user's memories.",
);
parts.push(
"- Do not pass run_id directly. The plugin manages session scoping through the scope parameter.",
);
parts.push(
"- In multi-agent setups, each agent has isolated memory. The main agent's memories are separate from subagent memories.",
);
parts.push("");
parts.push("SEARCH SCOPE: Choose the right scope for each search:");
parts.push('- scope: "long-term" for user context, identity, preferences, decisions (default, most common)');
parts.push(
'- scope: "long-term" for user context, identity, preferences, decisions (default, most common)',
);
parts.push('- scope: "session" for facts from this conversation only');
parts.push('- scope: "all" only when you truly need both scopes combined');
parts.push(
'- scope: "all" only when you truly need both scopes combined',
);
parts.push("Using a specific scope avoids unnecessary backend fan-out.");
parts.push("");
parts.push("SEARCH FILTERS: When the user's intent implies a time range or category constraint, pass a `filters` object alongside your rewritten query.");
parts.push('- Time: "last week" -> filters: {"created_at": {"gte": "2026-03-24"}}');
parts.push(
"SEARCH FILTERS: When the user's intent implies a time range or category constraint, pass a `filters` object alongside your rewritten query.",
);
parts.push(
'- Time: "last week" -> filters: {"created_at": {"gte": "2026-03-24"}}',
);
parts.push('- Category: "my preferences" -> categories: ["preference"]');
parts.push("- Available operators: eq, ne, gt, gte, lt, lte, in, contains. Logical: AND, OR, NOT.");
parts.push(
"- Available operators: eq, ne, gt, gte, lt, lte, in, contains. Logical: AND, OR, NOT.",
);
}
parts.push("</memory-system>");
return parts.join("\n");
@@ -360,15 +430,29 @@ export function loadTriagePrompt(config: SkillsConfig = {}): string {
// Fallback: SKILL.md not found. Minimal inline protocol.
const parts: string[] = [];
parts.push("<memory-system>");
parts.push("You have persistent long-term memory via mem0. After EVERY response, evaluate the turn for facts worth storing.");
parts.push("Use `memory_store` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).");
parts.push(
"You have persistent long-term memory via mem0. After EVERY response, evaluate the turn for facts worth storing.",
);
parts.push(
"Use `memory_store` tool for ALL user facts. NEVER write user info to workspace files (USER.md, memory/).",
);
parts.push("Most turns produce ZERO memory operations. That is correct.");
parts.push("Only store facts a new agent would need days later: identity, preferences, decisions, rules, projects, configs.");
parts.push("Batch facts by CATEGORY. All facts in one call must share the same category.");
parts.push('Format: memory_store(facts: ["fact text"], category: "identity")');
parts.push("NEVER store credentials (sk-, m0-, ghp_, AKIA, Bearer tokens, passwords).");
parts.push(
"Only store facts a new agent would need days later: identity, preferences, decisions, rules, projects, configs.",
);
parts.push(
"Batch facts by CATEGORY. All facts in one call must share the same category.",
);
parts.push(
'Format: memory_store(facts: ["fact text"], category: "identity")',
);
parts.push(
"NEVER store credentials (sk-, m0-, ghp_, AKIA, Bearer tokens, passwords).",
);
if (config.recall?.enabled !== false) {
parts.push("When searching, rewrite queries for retrieval. Do not pass raw user messages.");
parts.push(
"When searching, rewrite queries for retrieval. Do not pass raw user messages.",
);
}
parts.push("</memory-system>");
return parts.join("\n");