@happycastle/oh-my-openclaw 0.10.0 → 0.12.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.
@@ -0,0 +1,30 @@
1
+ export type ContextPriority = 'critical' | 'high' | 'normal' | 'low';
2
+ export type ContextSourceType = 'persona' | 'todo-enforcer' | 'system' | 'plugin';
3
+ export interface ContextEntry {
4
+ id: string;
5
+ content: string;
6
+ priority: ContextPriority;
7
+ source: ContextSourceType;
8
+ oneShot?: boolean;
9
+ }
10
+ export interface RegisterContextOptions {
11
+ id: string;
12
+ content: string;
13
+ priority?: ContextPriority;
14
+ source?: ContextSourceType;
15
+ oneShot?: boolean;
16
+ }
17
+ export declare class ContextCollector {
18
+ private sessions;
19
+ register(sessionKey: string, options: RegisterContextOptions): void;
20
+ unregister(sessionKey: string, entryId: string): void;
21
+ collect(sessionKey: string): ContextEntry[];
22
+ collectAsString(sessionKey: string, separator?: string): string;
23
+ clear(sessionKey: string): void;
24
+ clearAll(): void;
25
+ getEntries(sessionKey: string): ContextEntry[];
26
+ hasEntries(sessionKey: string): boolean;
27
+ private getOrCreateSession;
28
+ private sortEntries;
29
+ }
30
+ export declare const contextCollector: ContextCollector;
@@ -0,0 +1,85 @@
1
+ const PRIORITY_ORDER = {
2
+ critical: 0,
3
+ high: 1,
4
+ normal: 2,
5
+ low: 3,
6
+ };
7
+ export class ContextCollector {
8
+ sessions = new Map();
9
+ register(sessionKey, options) {
10
+ const sessionEntries = this.getOrCreateSession(sessionKey);
11
+ const entry = {
12
+ id: options.id,
13
+ content: options.content,
14
+ priority: options.priority ?? 'normal',
15
+ source: options.source ?? 'plugin',
16
+ oneShot: options.oneShot ?? false,
17
+ };
18
+ sessionEntries.set(options.id, entry);
19
+ }
20
+ unregister(sessionKey, entryId) {
21
+ const sessionEntries = this.sessions.get(sessionKey);
22
+ if (!sessionEntries) {
23
+ return;
24
+ }
25
+ sessionEntries.delete(entryId);
26
+ if (sessionEntries.size === 0) {
27
+ this.sessions.delete(sessionKey);
28
+ }
29
+ }
30
+ collect(sessionKey) {
31
+ const sessionEntries = this.sessions.get(sessionKey);
32
+ if (!sessionEntries) {
33
+ return [];
34
+ }
35
+ const entries = this.sortEntries([...sessionEntries.values()]);
36
+ for (const entry of entries) {
37
+ if (entry.oneShot) {
38
+ sessionEntries.delete(entry.id);
39
+ }
40
+ }
41
+ if (sessionEntries.size === 0) {
42
+ this.sessions.delete(sessionKey);
43
+ }
44
+ return entries;
45
+ }
46
+ collectAsString(sessionKey, separator = '\n\n') {
47
+ const entries = this.collect(sessionKey);
48
+ return entries.map((entry) => entry.content).join(separator);
49
+ }
50
+ clear(sessionKey) {
51
+ this.sessions.delete(sessionKey);
52
+ }
53
+ clearAll() {
54
+ this.sessions.clear();
55
+ }
56
+ getEntries(sessionKey) {
57
+ const sessionEntries = this.sessions.get(sessionKey);
58
+ if (!sessionEntries) {
59
+ return [];
60
+ }
61
+ return this.sortEntries([...sessionEntries.values()]);
62
+ }
63
+ hasEntries(sessionKey) {
64
+ const sessionEntries = this.sessions.get(sessionKey);
65
+ return Boolean(sessionEntries && sessionEntries.size > 0);
66
+ }
67
+ getOrCreateSession(sessionKey) {
68
+ let sessionEntries = this.sessions.get(sessionKey);
69
+ if (!sessionEntries) {
70
+ sessionEntries = new Map();
71
+ this.sessions.set(sessionKey, sessionEntries);
72
+ }
73
+ return sessionEntries;
74
+ }
75
+ sortEntries(entries) {
76
+ return entries.sort((a, b) => {
77
+ const priorityDiff = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
78
+ if (priorityDiff !== 0) {
79
+ return priorityDiff;
80
+ }
81
+ return a.id.localeCompare(b.id);
82
+ });
83
+ }
84
+ }
85
+ export const contextCollector = new ContextCollector();
@@ -0,0 +1,2 @@
1
+ import { OmocPluginApi } from '../types.js';
2
+ export declare function registerContextInjector(api: OmocPluginApi): void;
@@ -0,0 +1,20 @@
1
+ import { contextCollector } from '../features/context-collector.js';
2
+ export function registerContextInjector(api) {
3
+ api.registerHook('before_prompt_build', (event) => {
4
+ const sessionKey = event.agentId || 'default';
5
+ if (!contextCollector.hasEntries(sessionKey)) {
6
+ return event;
7
+ }
8
+ const entryCount = contextCollector.getEntries(sessionKey).length;
9
+ const collectedContext = contextCollector.collectAsString(sessionKey);
10
+ if (!collectedContext) {
11
+ return event;
12
+ }
13
+ event.prependContext = collectedContext;
14
+ api.logger.info(`[omoc] Context injected: ${entryCount} entries for ${sessionKey}`);
15
+ return event;
16
+ }, {
17
+ name: 'oh-my-openclaw.context-injector',
18
+ description: 'Unified context injection from ContextCollector into prependContext',
19
+ });
20
+ }
@@ -1,4 +1,5 @@
1
1
  import { OmocPluginApi } from '../types.js';
2
+ export declare function resetPersonaContextEntries(): void;
2
3
  export declare function resetPersonaInjectorState(): void;
3
4
  export declare function getPersonaInjectorState(): {
4
5
  lastInjectedPersonaId: string | null;
@@ -1,8 +1,22 @@
1
1
  import { getActivePersona } from '../utils/persona-state.js';
2
2
  import { readPersonaPromptSync } from '../agents/persona-prompts.js';
3
+ import { contextCollector } from '../features/context-collector.js';
3
4
  let lastInjectedPersonaId = null;
5
+ const personaSessionKeys = new Set();
6
+ export function resetPersonaContextEntries() {
7
+ for (const sessionKey of personaSessionKeys) {
8
+ const entries = contextCollector.getEntries(sessionKey);
9
+ for (const entry of entries) {
10
+ if (entry.source === 'persona') {
11
+ contextCollector.unregister(sessionKey, entry.id);
12
+ }
13
+ }
14
+ }
15
+ personaSessionKeys.clear();
16
+ }
4
17
  export function resetPersonaInjectorState() {
5
18
  lastInjectedPersonaId = null;
19
+ resetPersonaContextEntries();
6
20
  }
7
21
  export function getPersonaInjectorState() {
8
22
  return { lastInjectedPersonaId };
@@ -10,30 +24,30 @@ export function getPersonaInjectorState() {
10
24
  export function registerPersonaInjector(api) {
11
25
  api.registerHook('agent:bootstrap', (event) => {
12
26
  const personaId = getActivePersona();
27
+ const sessionKey = event.context.agentId || 'default';
13
28
  if (!personaId) {
14
29
  return;
15
30
  }
16
- if (!event.context.bootstrapFiles) {
17
- event.context.bootstrapFiles = [];
18
- }
19
- const alreadyInFiles = event.context.bootstrapFiles.some((f) => f.path.startsWith('omoc://persona/'));
20
- if (alreadyInFiles) {
21
- return;
22
- }
23
31
  if (lastInjectedPersonaId === personaId) {
24
32
  return;
25
33
  }
26
34
  try {
35
+ if (lastInjectedPersonaId) {
36
+ contextCollector.unregister(sessionKey, `persona/${lastInjectedPersonaId}`);
37
+ }
27
38
  const content = readPersonaPromptSync(personaId);
28
- event.context.bootstrapFiles.push({
29
- path: `omoc://persona/${personaId}`,
39
+ contextCollector.register(sessionKey, {
40
+ id: `persona/${personaId}`,
30
41
  content,
42
+ priority: 'high',
43
+ source: 'persona',
31
44
  });
45
+ personaSessionKeys.add(sessionKey);
32
46
  lastInjectedPersonaId = personaId;
33
- api.logger.info(`[omoc] Persona injected: ${personaId}`);
47
+ api.logger.info(`[omoc] Persona context registered: ${personaId}`);
34
48
  }
35
49
  catch (err) {
36
- api.logger.error(`[omoc] Failed to inject persona ${personaId}:`, err);
50
+ api.logger.error(`[omoc] Failed to register persona context ${personaId}:`, err);
37
51
  }
38
52
  }, {
39
53
  name: 'oh-my-openclaw.persona-injector',
@@ -1,4 +1,5 @@
1
1
  import { getConfig } from '../utils/config.js';
2
+ import { contextCollector } from '../features/context-collector.js';
2
3
  const ORCHESTRATOR_IDS = new Set([
3
4
  'omoc_prometheus',
4
5
  'omoc_atlas',
@@ -50,25 +51,21 @@ export function registerTodoEnforcer(api) {
50
51
  }
51
52
  const role = classifyAgentRole(event.context.agentId);
52
53
  const directive = DIRECTIVES[role];
54
+ const sessionKey = event.context.agentId || 'default';
53
55
  if (!directive) {
54
56
  return;
55
57
  }
56
- if (!event.context.bootstrapFiles) {
57
- event.context.bootstrapFiles = [];
58
- }
59
- const alreadyInjected = event.context.bootstrapFiles.some((f) => f.path === 'omoc://todo-enforcer');
60
- if (alreadyInjected) {
61
- return;
62
- }
63
58
  try {
64
- event.context.bootstrapFiles.push({
65
- path: 'omoc://todo-enforcer',
59
+ contextCollector.register(sessionKey, {
60
+ id: 'todo-enforcer',
66
61
  content: directive,
62
+ priority: 'normal',
63
+ source: 'todo-enforcer',
67
64
  });
68
- api.logger.info(`[omoc] Todo enforcer injected (role: ${role})`);
65
+ api.logger.info(`[omoc] Todo enforcer context registered (role: ${role})`);
69
66
  }
70
67
  catch (err) {
71
- api.logger.error('[omoc] Todo enforcer injection failed:', err);
68
+ api.logger.error('[omoc] Todo enforcer context registration failed:', err);
72
69
  }
73
70
  }, {
74
71
  name: 'oh-my-openclaw.todo-enforcer',
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { registerRalphCommands } from './commands/ralph-commands.js';
14
14
  import { registerStatusCommands } from './commands/status-commands.js';
15
15
  import { registerPersonaCommands } from './commands/persona-commands.js';
16
16
  import { registerPersonaInjector } from './hooks/persona-injector.js';
17
+ import { registerContextInjector } from './hooks/context-injector.js';
17
18
  import { registerSetupCli } from './cli/setup.js';
18
19
  const registry = {
19
20
  hooks: [],
@@ -65,6 +66,14 @@ export default function register(api) {
65
66
  catch (err) {
66
67
  api.logger.error(`[${PLUGIN_ID}] Failed to register Persona Injector:`, err);
67
68
  }
69
+ try {
70
+ registerContextInjector(api);
71
+ registry.hooks.push('context-injector');
72
+ api.logger.info(`[${PLUGIN_ID}] Context injector hook registered (before_prompt_build)`);
73
+ }
74
+ catch (err) {
75
+ api.logger.error(`[${PLUGIN_ID}] Failed to register Context Injector:`, err);
76
+ }
68
77
  try {
69
78
  registerRalphLoop(api);
70
79
  registry.services.push('ralph-loop');
@@ -1,3 +1,5 @@
1
+ export declare function initPersonaState(filePath?: string): void;
1
2
  export declare function setActivePersona(id: string | null): void;
2
3
  export declare function getActivePersona(): string | null;
3
4
  export declare function resetPersonaState(): void;
5
+ export declare function resetPersonaStateForTesting(): void;
@@ -1,10 +1,48 @@
1
+ import { readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { dirname, join } from 'path';
1
3
  let activePersonaId = null;
4
+ let loaded = false;
5
+ let stateFilePath = join('workspace', '.omoc-state', 'active-persona');
6
+ export function initPersonaState(filePath) {
7
+ if (filePath)
8
+ stateFilePath = filePath;
9
+ loadFromDisk();
10
+ }
2
11
  export function setActivePersona(id) {
3
12
  activePersonaId = id;
13
+ loaded = true;
14
+ saveToDisk();
4
15
  }
5
16
  export function getActivePersona() {
17
+ if (!loaded)
18
+ loadFromDisk();
6
19
  return activePersonaId;
7
20
  }
8
21
  export function resetPersonaState() {
9
22
  activePersonaId = null;
23
+ loaded = true;
24
+ saveToDisk();
25
+ }
26
+ export function resetPersonaStateForTesting() {
27
+ activePersonaId = null;
28
+ loaded = true;
29
+ }
30
+ function loadFromDisk() {
31
+ try {
32
+ const content = readFileSync(stateFilePath, 'utf-8').trim();
33
+ activePersonaId = content || null;
34
+ }
35
+ catch {
36
+ activePersonaId = null;
37
+ }
38
+ loaded = true;
39
+ }
40
+ function saveToDisk() {
41
+ try {
42
+ mkdirSync(dirname(stateFilePath), { recursive: true });
43
+ writeFileSync(stateFilePath, activePersonaId ?? '', 'utf-8');
44
+ }
45
+ catch {
46
+ // silent fail — in-memory state still works
47
+ }
10
48
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happycastle/oh-my-openclaw",
3
- "version": "0.10.0",
3
+ "version": "0.12.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",