@happycastle/oh-my-openclaw 0.13.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import { LOG_PREFIX } from '../constants.js';
2
- import { getActivePersona, setActivePersonaId, resetPersonaState } from '../utils/persona-state.js';
3
- import { resolvePersonaId, listPersonas, DEFAULT_PERSONA_ID } from '../agents/persona-prompts.js';
2
+ import { getActivePersona, setActivePersonaId, resetPersonaState, replaceAgentsMd, restoreAgentsMdToDefault } from '../utils/persona-state.js';
3
+ import { resolvePersonaId, listPersonas, readPersonaPrompt, DEFAULT_PERSONA_ID } from '../agents/persona-prompts.js';
4
4
  function getDisplayName(personaId) {
5
5
  const persona = listPersonas().find((p) => p.id === personaId);
6
6
  return persona ? `${persona.emoji} ${persona.displayName}` : personaId;
@@ -17,22 +17,25 @@ export function registerPersonaCommands(api) {
17
17
  if (!args) {
18
18
  const previousId = await getActivePersona();
19
19
  await setActivePersonaId(DEFAULT_PERSONA_ID);
20
+ const content = await readPersonaPrompt(DEFAULT_PERSONA_ID);
21
+ await replaceAgentsMd(content);
20
22
  const name = getDisplayName(DEFAULT_PERSONA_ID);
21
23
  const switchNote = previousId && previousId !== DEFAULT_PERSONA_ID
22
24
  ? `\n\nSwitched from **${getDisplayName(previousId)}**.`
23
25
  : '';
24
26
  return {
25
- text: `# OmOC Mode: ON\n\nActive persona: **${name}**${switchNote}\n\nApplied immediately your next message will use this persona.\n\nUse \`/omoc list\` to see available personas, or \`/omoc <name>\` to switch.`,
27
+ text: `# OmOC Mode: ON\n\nActive persona: **${name}**${switchNote}\n\nAGENTS.md replaced with persona prompt. Your next message will use this persona.\n\nUse \`/omoc list\` to see available personas, or \`/omoc <name>\` to switch.`,
26
28
  };
27
29
  }
28
30
  if (args === 'off') {
29
31
  const wasActive = await getActivePersona();
30
32
  const wasName = wasActive ? getDisplayName(wasActive) : null;
31
33
  await resetPersonaState();
34
+ await restoreAgentsMdToDefault();
32
35
  return {
33
36
  text: wasName
34
- ? `# OmOC Mode: OFF\n\nPersona **${wasName}** deactivated. Applied immediately your next message will use default behavior.`
35
- : '# OmOC Mode: OFF\n\nNo persona was active.',
37
+ ? `# OmOC Mode: OFF\n\nPersona **${wasName}** deactivated. AGENTS.md restored to default.`
38
+ : '# OmOC Mode: OFF\n\nNo persona was active. AGENTS.md restored to default.',
36
39
  };
37
40
  }
38
41
  if (args === 'list') {
@@ -66,13 +69,15 @@ export function registerPersonaCommands(api) {
66
69
  }
67
70
  const previousId = await getActivePersona();
68
71
  await setActivePersonaId(resolvedId);
72
+ const content = await readPersonaPrompt(resolvedId);
73
+ await replaceAgentsMd(content);
69
74
  const displayName = getDisplayName(resolvedId);
70
75
  const switched = listPersonas().find((p) => p.id === resolvedId);
71
76
  const switchNote = previousId && previousId !== resolvedId
72
77
  ? `\n\nSwitched from **${getDisplayName(previousId)}**.`
73
78
  : '';
74
79
  return {
75
- text: `# Persona Switched\n\nActive persona: **${displayName}**${switchNote}\n\nApplied immediately your next message will use the ${switched?.theme ?? 'persona'} prompt.`,
80
+ text: `# Persona Switched\n\nActive persona: **${displayName}**${switchNote}\n\nAGENTS.md replaced. Your next message will use the ${switched?.theme ?? 'persona'} prompt.`,
76
81
  };
77
82
  },
78
83
  });
@@ -0,0 +1,3 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ /** session_start hook: re-sync AGENTS.md from `.omoc-state` (source of truth). */
3
+ export declare function registerSessionSync(api: OmocPluginApi): void;
@@ -0,0 +1,33 @@
1
+ import { LOG_PREFIX } from '../constants.js';
2
+ import { getActivePersona, resolveAgentsMdPath, replaceAgentsMd } from '../utils/persona-state.js';
3
+ import { readPersonaPromptSync } from '../agents/persona-prompts.js';
4
+ import { readFileSync } from 'fs';
5
+ /** session_start hook: re-sync AGENTS.md from `.omoc-state` (source of truth). */
6
+ export function registerSessionSync(api) {
7
+ api.on('session_start', async (_event, _ctx) => {
8
+ try {
9
+ const activePersona = await getActivePersona();
10
+ if (!activePersona)
11
+ return;
12
+ const personaContent = readPersonaPromptSync(activePersona);
13
+ if (personaContent.startsWith('[OmOC]')) {
14
+ api.logger.warn(`${LOG_PREFIX} Session sync: persona file issue for ${activePersona}`);
15
+ return;
16
+ }
17
+ const agentsPath = resolveAgentsMdPath();
18
+ try {
19
+ const current = readFileSync(agentsPath, 'utf-8');
20
+ if (current.includes(personaContent.slice(0, 100)))
21
+ return;
22
+ }
23
+ catch {
24
+ // AGENTS.md missing or unreadable — needs sync
25
+ }
26
+ await replaceAgentsMd(personaContent);
27
+ api.logger.info(`${LOG_PREFIX} Session sync: AGENTS.md re-synced with .omoc-state (persona=${activePersona})`);
28
+ }
29
+ catch (err) {
30
+ api.logger.error(`${LOG_PREFIX} Session sync failed:`, err);
31
+ }
32
+ }, { priority: 200 });
33
+ }
@@ -0,0 +1,3 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ /** before_tool_call hook: block sessions_spawn without agentId when OmOC persona is active. */
3
+ export declare function registerSpawnGuard(api: OmocPluginApi): void;
@@ -0,0 +1,33 @@
1
+ import { LOG_PREFIX } from '../constants.js';
2
+ import { getActivePersona } from '../utils/persona-state.js';
3
+ import { ALL_AGENT_IDS } from '../agents/agent-ids.js';
4
+ const SPAWN_TOOL_NAME = 'sessions_spawn';
5
+ const AVAILABLE_AGENTS = ALL_AGENT_IDS.map((id) => id.replace('omoc_', '')).join(', ');
6
+ /** before_tool_call hook: block sessions_spawn without agentId when OmOC persona is active. */
7
+ export function registerSpawnGuard(api) {
8
+ api.on('before_tool_call', async (event, _ctx) => {
9
+ if (event.toolName !== SPAWN_TOOL_NAME) {
10
+ return;
11
+ }
12
+ let activePersona;
13
+ try {
14
+ activePersona = await getActivePersona();
15
+ }
16
+ catch {
17
+ return;
18
+ }
19
+ if (!activePersona)
20
+ return;
21
+ const agentId = event.params.agentId;
22
+ if (typeof agentId === 'string' && agentId.trim().length > 0)
23
+ return;
24
+ api.logger.info(`${LOG_PREFIX} Spawn guard: blocked sessions_spawn without agentId (active persona=${activePersona})`);
25
+ return {
26
+ block: true,
27
+ blockReason: `[OmOC] Sub-agent spawn BLOCKED: agentId is required when OmOC persona is active (current: ${activePersona}). ` +
28
+ `You MUST provide the agentId parameter in your sessions_spawn call. ` +
29
+ `Available agents: ${AVAILABLE_AGENTS}. ` +
30
+ `Retry with agentId set to the appropriate agent for this task.`,
31
+ };
32
+ }, { priority: 150 });
33
+ }
package/dist/index.js CHANGED
@@ -14,8 +14,9 @@ import { registerWorkflowCommands } from './commands/workflow-commands.js';
14
14
  import { registerRalphCommands } from './commands/ralph-commands.js';
15
15
  import { registerStatusCommands } from './commands/status-commands.js';
16
16
  import { registerPersonaCommands } from './commands/persona-commands.js';
17
- import { registerPersonaInjector } from './hooks/persona-injector.js';
18
17
  import { registerContextInjector } from './hooks/context-injector.js';
18
+ import { registerSessionSync } from './hooks/session-sync.js';
19
+ import { registerSpawnGuard } from './hooks/spawn-guard.js';
19
20
  import { registerSetupCli } from './cli/setup.js';
20
21
  /**
21
22
  * Generation counter for multi-registration handling.
@@ -77,16 +78,21 @@ export default function register(api) {
77
78
  registry.hooks.push('gateway-startup');
78
79
  api.logger.info(`[${PLUGIN_ID}] Gateway startup hook registered`);
79
80
  });
80
- safeRegister(api, 'persona-injector', 'hook', () => {
81
- registerPersonaInjector(guarded);
82
- registry.hooks.push('persona-injector');
83
- api.logger.info(`[${PLUGIN_ID}] Persona injector hook registered`);
84
- });
85
81
  safeRegister(api, 'context-injector', 'hook', () => {
86
82
  registerContextInjector(guarded);
87
83
  registry.hooks.push('context-injector');
88
84
  api.logger.info(`[${PLUGIN_ID}] Context injector hook registered (before_prompt_build)`);
89
85
  });
86
+ safeRegister(api, 'session-sync', 'hook', () => {
87
+ registerSessionSync(api);
88
+ registry.hooks.push('session-sync');
89
+ api.logger.info(`[${PLUGIN_ID}] Session sync hook registered (session_start)`);
90
+ });
91
+ safeRegister(api, 'spawn-guard', 'hook', () => {
92
+ registerSpawnGuard(api);
93
+ registry.hooks.push('spawn-guard');
94
+ api.logger.info(`[${PLUGIN_ID}] Spawn guard hook registered (before_tool_call)`);
95
+ });
90
96
  safeRegister(api, 'ralph-loop', 'service', () => {
91
97
  registerRalphLoop(api);
92
98
  registry.services.push('ralph-loop');
package/dist/types.d.ts CHANGED
@@ -118,4 +118,5 @@ export interface BeforePromptBuildResult {
118
118
  export interface BeforePromptBuildEvent {
119
119
  prompt?: string;
120
120
  messages?: unknown[];
121
+ systemPrompt?: string;
121
122
  }
@@ -4,3 +4,6 @@ export declare function setActivePersonaId(id: string | null): Promise<void>;
4
4
  export declare function setActivePersona(id: string | null): Promise<void>;
5
5
  export declare function getActivePersona(): Promise<string | null>;
6
6
  export declare function resetPersonaState(): Promise<void>;
7
+ export declare function resolveAgentsMdPath(): string;
8
+ export declare function replaceAgentsMd(personaContent: string): Promise<void>;
9
+ export declare function restoreAgentsMdToDefault(): Promise<void>;
@@ -1,8 +1,10 @@
1
1
  import { readFile, writeFile, mkdir } from 'fs/promises';
2
2
  import { dirname, join } from 'path';
3
+ import { homedir } from 'os';
3
4
  let activePersonaId = null;
4
5
  let loaded = false;
5
- let stateFilePath = join('workspace', '.omoc-state', 'active-persona');
6
+ const stateDir = join('workspace', '.omoc-state');
7
+ let stateFilePath = join(stateDir, 'active-persona');
6
8
  export async function initPersonaState(_api) {
7
9
  try {
8
10
  await mkdir(dirname(stateFilePath), { recursive: true });
@@ -50,7 +52,67 @@ async function saveToDisk() {
50
52
  await writeFile(stateFilePath, activePersonaId ?? '', 'utf-8');
51
53
  }
52
54
  catch (error) {
53
- // silent fail — in-memory state still works, but log for debugging
54
55
  console.warn('[omoc] Failed to persist persona state to disk:', error);
55
56
  }
56
57
  }
58
+ export function resolveAgentsMdPath() {
59
+ const profile = process.env.OPENCLAW_PROFILE?.trim();
60
+ const wsDir = (profile && profile.toLowerCase() !== 'default')
61
+ ? join(homedir(), '.openclaw', `workspace-${profile}`)
62
+ : join(homedir(), '.openclaw', 'workspace');
63
+ return join(wsDir, 'AGENTS.md');
64
+ }
65
+ export async function replaceAgentsMd(personaContent) {
66
+ const agentsPath = resolveAgentsMdPath();
67
+ await mkdir(dirname(agentsPath), { recursive: true });
68
+ const merged = `${DEFAULT_AGENTS_MD}\n---\n\n${personaContent}`;
69
+ await writeFile(agentsPath, merged, 'utf-8');
70
+ }
71
+ export async function restoreAgentsMdToDefault() {
72
+ const agentsPath = resolveAgentsMdPath();
73
+ await mkdir(dirname(agentsPath), { recursive: true });
74
+ await writeFile(agentsPath, DEFAULT_AGENTS_MD, 'utf-8');
75
+ }
76
+ const DEFAULT_AGENTS_MD = `# AGENTS.md - Your Workspace
77
+
78
+ This folder is home. Treat it that way.
79
+
80
+ ## First Run
81
+
82
+ If \`BOOTSTRAP.md\` exists, that's your birth certificate. Follow it, figure out who you are, then delete it. You won't need it again.
83
+
84
+ ## Every Session
85
+
86
+ Before doing anything else:
87
+
88
+ 1. Read \`SOUL.md\` — this is who you are
89
+ 2. Read \`USER.md\` — this is who you're helping
90
+ 3. Read \`memory/YYYY-MM-DD.md\` (today + yesterday) for recent context
91
+ 4. **If in MAIN SESSION** (direct chat with your human): Also read \`MEMORY.md\`
92
+
93
+ Don't ask permission. Just do it.
94
+
95
+ ## Memory
96
+
97
+ You wake up fresh each session. These files are your continuity:
98
+
99
+ - **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed) — raw logs of what happened
100
+ - **Long-term:** \`MEMORY.md\` — your curated memories, like a human's long-term memory
101
+
102
+ Capture what matters. Decisions, context, things to remember. Skip the secrets unless asked to keep them.
103
+
104
+ ## Safety
105
+
106
+ - Don't exfiltrate private data. Ever.
107
+ - Don't run destructive commands without asking.
108
+ - \`trash\` > \`rm\` (recoverable beats gone forever)
109
+ - When in doubt, ask.
110
+
111
+ ## Tools
112
+
113
+ Skills provide your tools. When you need one, check its \`SKILL.md\`. Keep local notes in \`TOOLS.md\`.
114
+
115
+ ## Make It Yours
116
+
117
+ This is a starting point. Add your own conventions, style, and rules as you figure out what works.
118
+ `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.13.4",
3
+ "version": "0.14.0",
4
4
  "description": "Oh-My-OpenClaw plugin — multi-agent orchestration, todo enforcer, ralph loop, and custom tools for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,10 +0,0 @@
1
- import { OmocPluginApi } from '../types.js';
2
- /**
3
- * Check whether persona content is already present in session history.
4
- *
5
- * OpenClaw's `prependContext` is merged into the user prompt and persisted
6
- * in session history. Without this check the persona text accumulates —
7
- * appearing once per historical user message sent to the model.
8
- */
9
- export declare function isPersonaAlreadyInHistory(messages: unknown[] | undefined, personaContent: string): boolean;
10
- export declare function registerPersonaInjector(api: OmocPluginApi): void;
@@ -1,87 +0,0 @@
1
- import { LOG_PREFIX } from '../constants.js';
2
- import { getActivePersona } from '../utils/persona-state.js';
3
- import { readPersonaPromptSync, resolvePersonaId } from '../agents/persona-prompts.js';
4
- /**
5
- * Resolve the effective persona ID.
6
- *
7
- * Priority:
8
- * 1. Manually set persona via /omoc command (getActivePersona())
9
- * 2. agentId from the hook context (set by OpenClaw core)
10
- * 3. null — no persona to inject
11
- */
12
- async function resolveEffectivePersona(ctx) {
13
- const manual = await getActivePersona();
14
- if (manual) {
15
- const resolved = resolvePersonaId(manual);
16
- if (resolved)
17
- return { personaId: resolved, source: 'manual' };
18
- }
19
- const agentId = ctx.agentId;
20
- if (!agentId)
21
- return null;
22
- const resolved = resolvePersonaId(agentId);
23
- if (!resolved)
24
- return null;
25
- return { personaId: resolved, source: 'auto' };
26
- }
27
- const FINGERPRINT_LENGTH = 200;
28
- /**
29
- * Check whether persona content is already present in session history.
30
- *
31
- * OpenClaw's `prependContext` is merged into the user prompt and persisted
32
- * in session history. Without this check the persona text accumulates —
33
- * appearing once per historical user message sent to the model.
34
- */
35
- export function isPersonaAlreadyInHistory(messages, personaContent) {
36
- if (!messages || messages.length === 0)
37
- return false;
38
- const fingerprint = personaContent.slice(0, FINGERPRINT_LENGTH).trim();
39
- if (!fingerprint)
40
- return false;
41
- for (const msg of messages) {
42
- if (!msg || typeof msg !== 'object')
43
- continue;
44
- const record = msg;
45
- if (record['role'] !== 'user')
46
- continue;
47
- const content = record['content'];
48
- if (typeof content === 'string' && content.includes(fingerprint)) {
49
- return true;
50
- }
51
- }
52
- return false;
53
- }
54
- export function registerPersonaInjector(api) {
55
- // Use the typed hook system (api.on) for before_prompt_build.
56
- // This directly injects into the system prompt via prependContext,
57
- // which is more reliable than bootstrapFiles via agent:bootstrap.
58
- //
59
- // api.registerHook('before_prompt_build', ...) registers into the internal
60
- // hook system which does NOT trigger before_prompt_build — only hookRunner
61
- // (typed hooks via api.on) does.
62
- api.on('before_prompt_build', async (event, ctx) => {
63
- const result = await resolveEffectivePersona(ctx);
64
- if (!result) {
65
- const manual = await getActivePersona();
66
- api.logger.info(`${LOG_PREFIX} Persona injector: no persona resolved (agentId=${ctx.agentId ?? 'none'}, manual=${manual ?? 'none'})`);
67
- return;
68
- }
69
- const { personaId, source } = result;
70
- try {
71
- const content = readPersonaPromptSync(personaId);
72
- if (isPersonaAlreadyInHistory(event.messages, content)) {
73
- api.logger.info(`${LOG_PREFIX} Persona already in history, skipping injection: ${personaId} (${source})`);
74
- return;
75
- }
76
- api.logger.info(`${LOG_PREFIX} Persona injected via before_prompt_build: ${personaId} (${source}, agentId=${ctx.agentId ?? 'none'})`);
77
- return {
78
- prependContext: content,
79
- };
80
- }
81
- catch (err) {
82
- api.logger.error(`${LOG_PREFIX} Failed to inject persona ${personaId}:`, err);
83
- return;
84
- }
85
- }, { priority: 100 } // High priority — persona prompt should be prepended first
86
- );
87
- }