@dyyz1993/pi-coding-agent 0.69.12 → 0.69.13
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.
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -11
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +5 -4
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +73 -25
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/server-channel.d.ts +27 -0
- package/dist/core/extensions/server-channel.d.ts.map +1 -0
- package/dist/core/extensions/server-channel.js +40 -0
- package/dist/core/extensions/server-channel.js.map +1 -0
- package/dist/core/extensions/types.d.ts +19 -29
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/file-store/internal-git.d.ts +31 -0
- package/dist/core/file-store/internal-git.d.ts.map +1 -0
- package/dist/core/file-store/internal-git.js +176 -0
- package/dist/core/file-store/internal-git.js.map +1 -0
- package/dist/core/session-manager.d.ts +1 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +7 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +25 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +26 -3
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +33 -2
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +22 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/rules-engine/cache.d.ts +4 -0
- package/dist/rules-engine/cache.d.ts.map +1 -0
- package/dist/rules-engine/cache.js +32 -0
- package/dist/rules-engine/cache.js.map +1 -0
- package/dist/rules-engine/config.d.ts +8 -0
- package/dist/rules-engine/config.d.ts.map +1 -0
- package/dist/rules-engine/config.js +56 -0
- package/dist/rules-engine/config.js.map +1 -0
- package/dist/rules-engine/index.d.ts +10 -0
- package/dist/rules-engine/index.d.ts.map +1 -0
- package/dist/rules-engine/index.js +404 -0
- package/dist/rules-engine/index.js.map +1 -0
- package/dist/rules-engine/injector.d.ts +5 -0
- package/dist/rules-engine/injector.d.ts.map +1 -0
- package/dist/rules-engine/injector.js +57 -0
- package/dist/rules-engine/injector.js.map +1 -0
- package/dist/rules-engine/loader.d.ts +8 -0
- package/dist/rules-engine/loader.d.ts.map +1 -0
- package/dist/rules-engine/loader.js +190 -0
- package/dist/rules-engine/loader.js.map +1 -0
- package/dist/rules-engine/matcher.d.ts +3 -0
- package/dist/rules-engine/matcher.d.ts.map +1 -0
- package/dist/rules-engine/matcher.js +48 -0
- package/dist/rules-engine/matcher.js.map +1 -0
- package/dist/rules-engine/types.d.ts +150 -0
- package/dist/rules-engine/types.d.ts.map +1 -0
- package/dist/rules-engine/types.js +2 -0
- package/dist/rules-engine/types.js.map +1 -0
- package/docs/extensions.md +56 -56
- package/docs/file-rollback-design.md +287 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/file-snapshot.ts +407 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc-types.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * RPC protocol types for headless operation.\n *\n * Commands are sent as JSON lines on stdin.\n * Responses and events are emitted as JSON lines on stdout.\n */\n\nimport type { AgentMessage, ThinkingLevel } from \"@dyyz1993/pi-agent-core\";\nimport type { ImageContent, Model } from \"@dyyz1993/pi-ai\";\nimport type { SessionStats } from \"../../core/agent-session.js\";\nimport type { BashResult } from \"../../core/bash-executor.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport type { Settings } from \"../../core/settings-manager.js\";\nimport type { SourceInfo } from \"../../core/source-info.js\";\n\n// ============================================================================\n// RPC Commands (stdin)\n// ============================================================================\n\nexport type RpcCommand =\n\t// Prompting\n\t| { id?: string; type: \"prompt\"; message: string; images?: ImageContent[]; streamingBehavior?: \"steer\" | \"followUp\" }\n\t| { id?: string; type: \"steer\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"follow_up\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"abort\" }\n\t| { id?: string; type: \"new_session\"; parentSession?: string }\n\n\t// State\n\t| { id?: string; type: \"get_state\" }\n\n\t// Model\n\t| { id?: string; type: \"set_model\"; provider: string; modelId: string }\n\t| { id?: string; type: \"cycle_model\" }\n\t| { id?: string; type: \"get_available_models\" }\n\n\t// Thinking\n\t| { id?: string; type: \"set_thinking_level\"; level: ThinkingLevel }\n\t| { id?: string; type: \"cycle_thinking_level\" }\n\n\t// Queue modes\n\t| { id?: string; type: \"set_steering_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\t| { id?: string; type: \"set_follow_up_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\n\t// Compaction\n\t| { id?: string; type: \"compact\"; customInstructions?: string }\n\t| { id?: string; type: \"set_auto_compaction\"; enabled: boolean }\n\n\t// Retry\n\t| { id?: string; type: \"set_auto_retry\"; enabled: boolean }\n\t| { id?: string; type: \"abort_retry\" }\n\n\t// Bash\n\t| { id?: string; type: \"bash\"; command: string }\n\t| { id?: string; type: \"abort_bash\" }\n\n\t// Session\n\t| { id?: string; type: \"get_session_stats\" }\n\t| { id?: string; type: \"export_html\"; outputPath?: string }\n\t| { id?: string; type: \"switch_session\"; sessionPath: string }\n\t| { id?: string; type: \"fork\"; entryId: string }\n\t| { id?: string; type: \"clone\" }\n\t| { id?: string; type: \"get_fork_messages\" }\n\t| { id?: string; type: \"get_last_assistant_text\" }\n\t| { id?: string; type: \"set_session_name\"; name: string }\n\n\t// Messages\n\t| { id?: string; type: \"get_messages\" }\n\n\t// Commands (available for invocation via prompt)\n\t| { id?: string; type: \"get_commands\" }\n\n\t// Resources\n\t| { id?: string; type: \"get_skills\" }\n\t| { id?: string; type: \"get_extensions\" }\n\t| { id?: string; type: \"get_tools\" }\n\n\t// Settings\n\t| { id?: string; type: \"get_settings\"; scope?: \"global\" | \"project\" }\n\t| { id?: string; type: \"set_settings\"; settings: Partial<Settings>; scope?: \"global\" | \"project\" }\n\n\t// Context usage\n\t| { id?: string; type: \"get_context_usage\" }\n\n\t// System prompt\n\t| { id?: string; type: \"get_system_prompt\" }\n\n\t// Active tools\n\t| { id?: string; type: \"get_active_tools\" }\n\t| { id?: string; type: \"set_active_tools\"; toolNames: string[] }\n\n\t// Queue\n\t| { id?: string; type: \"get_queue\" }\n\t| { id?: string; type: \"clear_queue\" }\n\n\t// Flags\n\t| { id?: string; type: \"get_flags\" }\n\t| { id?: string; type: \"get_flag_values\" }\n\t| { id?: string; type: \"set_flag\"; name: string; value: boolean | string }\n\n\t// Reload\n\t| { id?: string; type: \"reload\" }\n\n\t// Agents files\n\t| { id?: string; type: \"get_agents_files\" };\n\n// ============================================================================\n// RPC Slash Command (for get_commands response)\n// ============================================================================\n\n/** A command available for invocation via prompt */\nexport interface RpcSlashCommand {\n\t/** Command name (without leading slash) */\n\tname: string;\n\t/** Human-readable description */\n\tdescription?: string;\n\t/** What kind of command this is */\n\tsource: \"extension\" | \"prompt\" | \"skill\";\n\t/** Source metadata for the owning resource */\n\tsourceInfo: SourceInfo;\n}\n\n/** A loaded skill */\nexport interface RpcSkill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsourceInfo: SourceInfo;\n\tdisableModelInvocation: boolean;\n}\n\n/** A loaded extension */\nexport interface RpcExtension {\n\tpath: string;\n\tresolvedPath: string;\n\tsourceInfo: SourceInfo;\n\ttoolNames: string[];\n\tcommandNames: string[];\n}\n\n/** A registered tool */\nexport interface RpcTool {\n\tname: string;\n\tlabel: string;\n\tdescription: string;\n\tsourceInfo: SourceInfo;\n}\n\n// ============================================================================\n// RPC Types for new commands\n// ============================================================================\n\nexport interface RpcContextUsage {\n\ttokens: number | null;\n\tcontextWindow: number;\n\tpercent: number | null;\n}\n\nexport interface RpcExtensionFlag {\n\tname: string;\n\tdescription?: string;\n\ttype: \"boolean\" | \"string\";\n\tdefault?: boolean | string;\n\textensionPath: string;\n}\n\n// ============================================================================\n// RPC State\n// ============================================================================\n\nexport interface RpcSessionState {\n\tmodel?: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\tisStreaming: boolean;\n\tisCompacting: boolean;\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\tsessionFile?: string;\n\tsessionId: string;\n\tsessionName?: string;\n\tautoCompactionEnabled: boolean;\n\tmessageCount: number;\n\tpendingMessageCount: number;\n}\n\n// ============================================================================\n// RPC Responses (stdout)\n// ============================================================================\n\n// Success responses with data\nexport type RpcResponse =\n\t// Prompting (async - events follow)\n\t| { id?: string; type: \"response\"; command: \"prompt\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"steer\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"follow_up\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"new_session\"; success: true; data: { cancelled: boolean } }\n\n\t// State\n\t| { id?: string; type: \"response\"; command: \"get_state\"; success: true; data: RpcSessionState }\n\n\t// Model\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"set_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: Model<any>;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: { model: Model<any>; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_available_models\";\n\t\t\tsuccess: true;\n\t\t\tdata: { models: Model<any>[] };\n\t }\n\n\t// Thinking\n\t| { id?: string; type: \"response\"; command: \"set_thinking_level\"; success: true }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_thinking_level\";\n\t\t\tsuccess: true;\n\t\t\tdata: { level: ThinkingLevel } | null;\n\t }\n\n\t// Queue modes\n\t| { id?: string; type: \"response\"; command: \"set_steering_mode\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"set_follow_up_mode\"; success: true }\n\n\t// Compaction\n\t| { id?: string; type: \"response\"; command: \"compact\"; success: true; data: CompactionResult }\n\t| { id?: string; type: \"response\"; command: \"set_auto_compaction\"; success: true }\n\n\t// Retry\n\t| { id?: string; type: \"response\"; command: \"set_auto_retry\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort_retry\"; success: true }\n\n\t// Bash\n\t| { id?: string; type: \"response\"; command: \"bash\"; success: true; data: BashResult }\n\t| { id?: string; type: \"response\"; command: \"abort_bash\"; success: true }\n\n\t// Session\n\t| { id?: string; type: \"response\"; command: \"get_session_stats\"; success: true; data: SessionStats }\n\t| { id?: string; type: \"response\"; command: \"export_html\"; success: true; data: { path: string } }\n\t| { id?: string; type: \"response\"; command: \"switch_session\"; success: true; data: { cancelled: boolean } }\n\t| { id?: string; type: \"response\"; command: \"fork\"; success: true; data: { text: string; cancelled: boolean } }\n\t| { id?: string; type: \"response\"; command: \"clone\"; success: true; data: { cancelled: boolean } }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_fork_messages\";\n\t\t\tsuccess: true;\n\t\t\tdata: { messages: Array<{ entryId: string; text: string }> };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_last_assistant_text\";\n\t\t\tsuccess: true;\n\t\t\tdata: { text: string | null };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_session_name\"; success: true }\n\n\t// Messages\n\t| { id?: string; type: \"response\"; command: \"get_messages\"; success: true; data: { messages: AgentMessage[] } }\n\n\t// Commands\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_commands\";\n\t\t\tsuccess: true;\n\t\t\tdata: { commands: RpcSlashCommand[] };\n\t }\n\n\t// Skills\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_skills\";\n\t\t\tsuccess: true;\n\t\t\tdata: { skills: RpcSkill[] };\n\t }\n\n\t// Extensions\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_extensions\";\n\t\t\tsuccess: true;\n\t\t\tdata: { extensions: RpcExtension[] };\n\t }\n\n\t// Tools\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_tools\";\n\t\t\tsuccess: true;\n\t\t\tdata: { tools: RpcTool[] };\n\t }\n\n\t// Settings\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_settings\";\n\t\t\tsuccess: true;\n\t\t\tdata: Settings;\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_settings\"; success: true }\n\n\t// Context usage\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_context_usage\";\n\t\t\tsuccess: true;\n\t\t\tdata: RpcContextUsage;\n\t }\n\n\t// System prompt\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_system_prompt\";\n\t\t\tsuccess: true;\n\t\t\tdata: { systemPrompt: string; appendSystemPrompt: string[] };\n\t }\n\n\t// Active tools\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_active_tools\";\n\t\t\tsuccess: true;\n\t\t\tdata: { toolNames: string[] };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_active_tools\"; success: true }\n\n\t// Queue\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_queue\";\n\t\t\tsuccess: true;\n\t\t\tdata: { steering: string[]; followUp: string[] };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"clear_queue\";\n\t\t\tsuccess: true;\n\t\t\tdata: { steering: string[]; followUp: string[] };\n\t }\n\n\t// Flags\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_flags\";\n\t\t\tsuccess: true;\n\t\t\tdata: { flags: RpcExtensionFlag[] };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_flag_values\";\n\t\t\tsuccess: true;\n\t\t\tdata: { values: Record<string, boolean | string> };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_flag\"; success: true }\n\n\t// Reload\n\t| { id?: string; type: \"response\"; command: \"reload\"; success: true }\n\n\t// Agents files\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_agents_files\";\n\t\t\tsuccess: true;\n\t\t\tdata: { agentsFiles: Array<{ path: string; content: string }> };\n\t }\n\n\t// Error response (any command can fail)\n\t| { id?: string; type: \"response\"; command: string; success: false; error: string };\n\n// ============================================================================\n// Extension UI Events (stdout)\n// ============================================================================\n\n/** Emitted when an extension needs user input */\nexport type RpcExtensionUIRequest =\n\t| { type: \"extension_ui_request\"; id: string; method: \"select\"; title: string; options: string[]; timeout?: number }\n\t| { type: \"extension_ui_request\"; id: string; method: \"confirm\"; title: string; message: string; timeout?: number }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"input\";\n\t\t\ttitle: string;\n\t\t\tplaceholder?: string;\n\t\t\ttimeout?: number;\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"editor\"; title: string; prefill?: string }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"notify\";\n\t\t\tmessage: string;\n\t\t\tnotifyType?: \"info\" | \"warning\" | \"error\";\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setStatus\";\n\t\t\tstatusKey: string;\n\t\t\tstatusText: string | undefined;\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setWidget\";\n\t\t\twidgetKey: string;\n\t\t\twidgetLines: string[] | undefined;\n\t\t\twidgetPlacement?: \"aboveEditor\" | \"belowEditor\";\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"setTitle\"; title: string }\n\t| { type: \"extension_ui_request\"; id: string; method: \"set_editor_text\"; text: string };\n\n// ============================================================================\n// Extension UI Commands (stdin)\n// ============================================================================\n\n/** Response to an extension UI request */\nexport type RpcExtensionUIResponse =\n\t| { type: \"extension_ui_response\"; id: string; value: string }\n\t| { type: \"extension_ui_response\"; id: string; confirmed: boolean }\n\t| { type: \"extension_ui_response\"; id: string; cancelled: true };\n\n// ============================================================================\n// Helper type for extracting command types\n// ============================================================================\n\nexport type RpcCommandType = RpcCommand[\"type\"];\n"]}
|
|
1
|
+
{"version":3,"file":"rpc-types.js","sourceRoot":"","sources":["../../../src/modes/rpc/rpc-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG","sourcesContent":["/**\n * RPC protocol types for headless operation.\n *\n * Commands are sent as JSON lines on stdin.\n * Responses and events are emitted as JSON lines on stdout.\n */\n\nimport type { AgentMessage, ThinkingLevel } from \"@dyyz1993/pi-agent-core\";\nimport type { ImageContent, Model } from \"@dyyz1993/pi-ai\";\nimport type { SessionStats } from \"../../core/agent-session.js\";\nimport type { BashResult } from \"../../core/bash-executor.js\";\nimport type { CompactionResult } from \"../../core/compaction/index.js\";\nimport type { Settings } from \"../../core/settings-manager.js\";\nimport type { SourceInfo } from \"../../core/source-info.js\";\n\n// ============================================================================\n// RPC Commands (stdin)\n// ============================================================================\n\nexport type RpcCommand =\n\t// Prompting\n\t| { id?: string; type: \"prompt\"; message: string; images?: ImageContent[]; streamingBehavior?: \"steer\" | \"followUp\" }\n\t| { id?: string; type: \"steer\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"follow_up\"; message: string; images?: ImageContent[] }\n\t| { id?: string; type: \"abort\" }\n\t| { id?: string; type: \"new_session\"; parentSession?: string }\n\n\t// State\n\t| { id?: string; type: \"get_state\" }\n\n\t// Model\n\t| { id?: string; type: \"set_model\"; provider: string; modelId: string }\n\t| { id?: string; type: \"cycle_model\" }\n\t| { id?: string; type: \"get_available_models\" }\n\n\t// Thinking\n\t| { id?: string; type: \"set_thinking_level\"; level: ThinkingLevel }\n\t| { id?: string; type: \"cycle_thinking_level\" }\n\n\t// Queue modes\n\t| { id?: string; type: \"set_steering_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\t| { id?: string; type: \"set_follow_up_mode\"; mode: \"all\" | \"one-at-a-time\" }\n\n\t// Compaction\n\t| { id?: string; type: \"compact\"; customInstructions?: string }\n\t| { id?: string; type: \"set_auto_compaction\"; enabled: boolean }\n\n\t// Retry\n\t| { id?: string; type: \"set_auto_retry\"; enabled: boolean }\n\t| { id?: string; type: \"abort_retry\" }\n\n\t// Bash\n\t| { id?: string; type: \"bash\"; command: string }\n\t| { id?: string; type: \"abort_bash\" }\n\n\t// Session\n\t| { id?: string; type: \"get_session_stats\" }\n\t| { id?: string; type: \"export_html\"; outputPath?: string }\n\t| { id?: string; type: \"switch_session\"; sessionPath: string }\n\t| { id?: string; type: \"fork\"; entryId: string; position?: \"before\" | \"at\" }\n\t| { id?: string; type: \"navigate_tree\"; targetId: string; summarize?: boolean }\n\t| { id?: string; type: \"clone\" }\n\t| { id?: string; type: \"get_fork_messages\" }\n\t| { id?: string; type: \"get_last_assistant_text\" }\n\t| { id?: string; type: \"set_session_name\"; name: string }\n\n\t// Messages\n\t| { id?: string; type: \"get_messages\" }\n\t| { id?: string; type: \"get_tree\" }\n\n\t// Commands (available for invocation via prompt)\n\t| { id?: string; type: \"get_commands\" }\n\n\t// Resources\n\t| { id?: string; type: \"get_skills\" }\n\t| { id?: string; type: \"get_extensions\" }\n\t| { id?: string; type: \"get_tools\" }\n\n\t// Settings\n\t| { id?: string; type: \"get_settings\"; scope?: \"global\" | \"project\" }\n\t| { id?: string; type: \"set_settings\"; settings: Partial<Settings>; scope?: \"global\" | \"project\" }\n\n\t// Context usage\n\t| { id?: string; type: \"get_context_usage\" }\n\n\t// System prompt\n\t| { id?: string; type: \"get_system_prompt\" }\n\n\t// Active tools\n\t| { id?: string; type: \"get_active_tools\" }\n\t| { id?: string; type: \"set_active_tools\"; toolNames: string[] }\n\n\t// Queue\n\t| { id?: string; type: \"get_queue\" }\n\t| { id?: string; type: \"clear_queue\" }\n\n\t// Flags\n\t| { id?: string; type: \"get_flags\" }\n\t| { id?: string; type: \"get_flag_values\" }\n\t| { id?: string; type: \"set_flag\"; name: string; value: boolean | string }\n\n\t// Reload\n\t| { id?: string; type: \"reload\" }\n\n\t// Agents files\n\t| { id?: string; type: \"get_agents_files\" };\n\n// ============================================================================\n// RPC Slash Command (for get_commands response)\n// ============================================================================\n\n/** A command available for invocation via prompt */\nexport interface RpcSlashCommand {\n\t/** Command name (without leading slash) */\n\tname: string;\n\t/** Human-readable description */\n\tdescription?: string;\n\t/** What kind of command this is */\n\tsource: \"extension\" | \"prompt\" | \"skill\";\n\t/** Source metadata for the owning resource */\n\tsourceInfo: SourceInfo;\n}\n\n/** A loaded skill */\nexport interface RpcSkill {\n\tname: string;\n\tdescription: string;\n\tfilePath: string;\n\tbaseDir: string;\n\tsourceInfo: SourceInfo;\n\tdisableModelInvocation: boolean;\n}\n\n/** A loaded extension */\nexport interface RpcExtension {\n\tpath: string;\n\tresolvedPath: string;\n\tsourceInfo: SourceInfo;\n\ttoolNames: string[];\n\tcommandNames: string[];\n}\n\n/** A registered tool */\nexport interface RpcTool {\n\tname: string;\n\tlabel: string;\n\tdescription: string;\n\tsourceInfo: SourceInfo;\n}\n\n// ============================================================================\n// RPC Types for new commands\n// ============================================================================\n\nexport interface RpcContextUsage {\n\ttokens: number | null;\n\tcontextWindow: number;\n\tpercent: number | null;\n}\n\nexport interface RpcExtensionFlag {\n\tname: string;\n\tdescription?: string;\n\ttype: \"boolean\" | \"string\";\n\tdefault?: boolean | string;\n\textensionPath: string;\n}\n\n// ============================================================================\n// RPC State\n// ============================================================================\n\nexport interface RpcSessionState {\n\tmodel?: Model<any>;\n\tthinkingLevel: ThinkingLevel;\n\tisStreaming: boolean;\n\tisCompacting: boolean;\n\tsteeringMode: \"all\" | \"one-at-a-time\";\n\tfollowUpMode: \"all\" | \"one-at-a-time\";\n\tsessionFile?: string;\n\tsessionId: string;\n\tsessionName?: string;\n\tautoCompactionEnabled: boolean;\n\tmessageCount: number;\n\tpendingMessageCount: number;\n}\n\n// ============================================================================\n// RPC Responses (stdout)\n// ============================================================================\n\n// Success responses with data\nexport type RpcResponse =\n\t// Prompting (async - events follow)\n\t| { id?: string; type: \"response\"; command: \"prompt\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"steer\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"follow_up\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"new_session\"; success: true; data: { cancelled: boolean } }\n\n\t// State\n\t| { id?: string; type: \"response\"; command: \"get_state\"; success: true; data: RpcSessionState }\n\n\t// Model\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"set_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: Model<any>;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_model\";\n\t\t\tsuccess: true;\n\t\t\tdata: { model: Model<any>; thinkingLevel: ThinkingLevel; isScoped: boolean } | null;\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_available_models\";\n\t\t\tsuccess: true;\n\t\t\tdata: { models: Model<any>[] };\n\t }\n\n\t// Thinking\n\t| { id?: string; type: \"response\"; command: \"set_thinking_level\"; success: true }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"cycle_thinking_level\";\n\t\t\tsuccess: true;\n\t\t\tdata: { level: ThinkingLevel } | null;\n\t }\n\n\t// Queue modes\n\t| { id?: string; type: \"response\"; command: \"set_steering_mode\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"set_follow_up_mode\"; success: true }\n\n\t// Compaction\n\t| { id?: string; type: \"response\"; command: \"compact\"; success: true; data: CompactionResult }\n\t| { id?: string; type: \"response\"; command: \"set_auto_compaction\"; success: true }\n\n\t// Retry\n\t| { id?: string; type: \"response\"; command: \"set_auto_retry\"; success: true }\n\t| { id?: string; type: \"response\"; command: \"abort_retry\"; success: true }\n\n\t// Bash\n\t| { id?: string; type: \"response\"; command: \"bash\"; success: true; data: BashResult }\n\t| { id?: string; type: \"response\"; command: \"abort_bash\"; success: true }\n\n\t// Session\n\t| { id?: string; type: \"response\"; command: \"get_session_stats\"; success: true; data: SessionStats }\n\t| { id?: string; type: \"response\"; command: \"export_html\"; success: true; data: { path: string } }\n\t| { id?: string; type: \"response\"; command: \"switch_session\"; success: true; data: { cancelled: boolean } }\n\t| { id?: string; type: \"response\"; command: \"fork\"; success: true; data: { text: string; cancelled: boolean } }\n\t| { id?: string; type: \"response\"; command: \"clone\"; success: true; data: { cancelled: boolean } }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_fork_messages\";\n\t\t\tsuccess: true;\n\t\t\tdata: { messages: Array<{ entryId: string; text: string }> };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_last_assistant_text\";\n\t\t\tsuccess: true;\n\t\t\tdata: { text: string | null };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_session_name\"; success: true }\n\n\t// Messages\n\t| { id?: string; type: \"response\"; command: \"get_messages\"; success: true; data: { messages: AgentMessage[] } }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_tree\";\n\t\t\tsuccess: true;\n\t\t\tdata: { entries: Array<{ id: string; parentId: string | null; type: string; label?: string }> };\n\t }\n\n\t// Commands\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_commands\";\n\t\t\tsuccess: true;\n\t\t\tdata: { commands: RpcSlashCommand[] };\n\t }\n\n\t// Skills\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_skills\";\n\t\t\tsuccess: true;\n\t\t\tdata: { skills: RpcSkill[] };\n\t }\n\n\t// Extensions\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_extensions\";\n\t\t\tsuccess: true;\n\t\t\tdata: { extensions: RpcExtension[] };\n\t }\n\n\t// Tools\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_tools\";\n\t\t\tsuccess: true;\n\t\t\tdata: { tools: RpcTool[] };\n\t }\n\n\t// Settings\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_settings\";\n\t\t\tsuccess: true;\n\t\t\tdata: Settings;\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_settings\"; success: true }\n\n\t// Context usage\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_context_usage\";\n\t\t\tsuccess: true;\n\t\t\tdata: RpcContextUsage;\n\t }\n\n\t// System prompt\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_system_prompt\";\n\t\t\tsuccess: true;\n\t\t\tdata: { systemPrompt: string; appendSystemPrompt: string[] };\n\t }\n\n\t// Active tools\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_active_tools\";\n\t\t\tsuccess: true;\n\t\t\tdata: { toolNames: string[] };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_active_tools\"; success: true }\n\n\t// Queue\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_queue\";\n\t\t\tsuccess: true;\n\t\t\tdata: { steering: string[]; followUp: string[] };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"clear_queue\";\n\t\t\tsuccess: true;\n\t\t\tdata: { steering: string[]; followUp: string[] };\n\t }\n\n\t// Flags\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_flags\";\n\t\t\tsuccess: true;\n\t\t\tdata: { flags: RpcExtensionFlag[] };\n\t }\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_flag_values\";\n\t\t\tsuccess: true;\n\t\t\tdata: { values: Record<string, boolean | string> };\n\t }\n\t| { id?: string; type: \"response\"; command: \"set_flag\"; success: true }\n\n\t// Reload\n\t| { id?: string; type: \"response\"; command: \"reload\"; success: true }\n\n\t// Agents files\n\t| {\n\t\t\tid?: string;\n\t\t\ttype: \"response\";\n\t\t\tcommand: \"get_agents_files\";\n\t\t\tsuccess: true;\n\t\t\tdata: { agentsFiles: Array<{ path: string; content: string }> };\n\t }\n\n\t// Error response (any command can fail)\n\t| { id?: string; type: \"response\"; command: string; success: false; error: string };\n\n// ============================================================================\n// Extension UI Events (stdout)\n// ============================================================================\n\n/** Emitted when an extension needs user input */\nexport type RpcExtensionUIRequest =\n\t| { type: \"extension_ui_request\"; id: string; method: \"select\"; title: string; options: string[]; timeout?: number }\n\t| { type: \"extension_ui_request\"; id: string; method: \"confirm\"; title: string; message: string; timeout?: number }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"input\";\n\t\t\ttitle: string;\n\t\t\tplaceholder?: string;\n\t\t\ttimeout?: number;\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"editor\"; title: string; prefill?: string }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"notify\";\n\t\t\tmessage: string;\n\t\t\tnotifyType?: \"info\" | \"warning\" | \"error\";\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setStatus\";\n\t\t\tstatusKey: string;\n\t\t\tstatusText: string | undefined;\n\t }\n\t| {\n\t\t\ttype: \"extension_ui_request\";\n\t\t\tid: string;\n\t\t\tmethod: \"setWidget\";\n\t\t\twidgetKey: string;\n\t\t\twidgetLines: string[] | undefined;\n\t\t\twidgetPlacement?: \"aboveEditor\" | \"belowEditor\";\n\t }\n\t| { type: \"extension_ui_request\"; id: string; method: \"setTitle\"; title: string }\n\t| { type: \"extension_ui_request\"; id: string; method: \"set_editor_text\"; text: string };\n\n// ============================================================================\n// Extension UI Commands (stdin)\n// ============================================================================\n\n/** Response to an extension UI request */\nexport type RpcExtensionUIResponse =\n\t| { type: \"extension_ui_response\"; id: string; value: string }\n\t| { type: \"extension_ui_response\"; id: string; confirmed: boolean }\n\t| { type: \"extension_ui_response\"; id: string; cancelled: true };\n\n// ============================================================================\n// Helper type for extracting command types\n// ============================================================================\n\nexport type RpcCommandType = RpcCommand[\"type\"];\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/rules-engine/cache.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAI1D,wBAAsB,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,EAAE,CAAC,CAQ7F;AAED,wBAAgB,eAAe,IAAI,IAAI,CAEtC","sourcesContent":["import { resolveDirs } from \"./config.js\";\nimport { loadRules } from \"./loader.js\";\nimport type { ParsedRule, RulesConfig } from \"./types.js\";\n\nlet cache: { rules: ParsedRule[]; loadedAt: number } | null = null;\n\nexport async function getRules(projectDir: string, config: RulesConfig): Promise<ParsedRule[]> {\n\tif (cache && Date.now() - cache.loadedAt < config.cacheTTL) {\n\t\treturn cache.rules;\n\t}\n\n\tconst rules = await loadAllRules(projectDir, config);\n\tcache = { rules, loadedAt: Date.now() };\n\treturn rules;\n}\n\nexport function invalidateCache(): void {\n\tcache = null;\n}\n\nasync function loadAllRules(projectDir: string, config: RulesConfig): Promise<ParsedRule[]> {\n\tconst sources = resolveDirs(projectDir, config);\n\tconst result: ParsedRule[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const { scope, dir, source } of sources) {\n\t\tconst ruleCache = loadRules(dir);\n\t\tfor (const rule of ruleCache.rules) {\n\t\t\tif (seen.has(rule.filePath)) continue;\n\t\t\tseen.add(rule.filePath);\n\t\t\trule.scope = scope as ParsedRule[\"scope\"];\n\t\t\trule.source = source;\n\t\t\tresult.push(rule);\n\t\t}\n\t}\n\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { resolveDirs } from "./config.js";
|
|
2
|
+
import { loadRules } from "./loader.js";
|
|
3
|
+
let cache = null;
|
|
4
|
+
export async function getRules(projectDir, config) {
|
|
5
|
+
if (cache && Date.now() - cache.loadedAt < config.cacheTTL) {
|
|
6
|
+
return cache.rules;
|
|
7
|
+
}
|
|
8
|
+
const rules = await loadAllRules(projectDir, config);
|
|
9
|
+
cache = { rules, loadedAt: Date.now() };
|
|
10
|
+
return rules;
|
|
11
|
+
}
|
|
12
|
+
export function invalidateCache() {
|
|
13
|
+
cache = null;
|
|
14
|
+
}
|
|
15
|
+
async function loadAllRules(projectDir, config) {
|
|
16
|
+
const sources = resolveDirs(projectDir, config);
|
|
17
|
+
const result = [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
for (const { scope, dir, source } of sources) {
|
|
20
|
+
const ruleCache = loadRules(dir);
|
|
21
|
+
for (const rule of ruleCache.rules) {
|
|
22
|
+
if (seen.has(rule.filePath))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(rule.filePath);
|
|
25
|
+
rule.scope = scope;
|
|
26
|
+
rule.source = source;
|
|
27
|
+
result.push(rule);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/rules-engine/cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,IAAI,KAAK,GAAqD,IAAI,CAAC;AAEnE,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,UAAkB,EAAE,MAAmB,EAAyB;IAC9F,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAC5D,OAAO,KAAK,CAAC,KAAK,CAAC;IACpB,CAAC;IAED,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IACrD,KAAK,GAAG,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC;IACxC,OAAO,KAAK,CAAC;AAAA,CACb;AAED,MAAM,UAAU,eAAe,GAAS;IACvC,KAAK,GAAG,IAAI,CAAC;AAAA,CACb;AAED,KAAK,UAAU,YAAY,CAAC,UAAkB,EAAE,MAAmB,EAAyB;IAC3F,MAAM,OAAO,GAAG,WAAW,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAiB,EAAE,CAAC;IAChC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,KAAK,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;QAC9C,MAAM,SAAS,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QACjC,KAAK,MAAM,IAAI,IAAI,SAAS,CAAC,KAAK,EAAE,CAAC;YACpC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACtC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACxB,IAAI,CAAC,KAAK,GAAG,KAA4B,CAAC;YAC1C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;YACrB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import { resolveDirs } from \"./config.js\";\nimport { loadRules } from \"./loader.js\";\nimport type { ParsedRule, RulesConfig } from \"./types.js\";\n\nlet cache: { rules: ParsedRule[]; loadedAt: number } | null = null;\n\nexport async function getRules(projectDir: string, config: RulesConfig): Promise<ParsedRule[]> {\n\tif (cache && Date.now() - cache.loadedAt < config.cacheTTL) {\n\t\treturn cache.rules;\n\t}\n\n\tconst rules = await loadAllRules(projectDir, config);\n\tcache = { rules, loadedAt: Date.now() };\n\treturn rules;\n}\n\nexport function invalidateCache(): void {\n\tcache = null;\n}\n\nasync function loadAllRules(projectDir: string, config: RulesConfig): Promise<ParsedRule[]> {\n\tconst sources = resolveDirs(projectDir, config);\n\tconst result: ParsedRule[] = [];\n\tconst seen = new Set<string>();\n\n\tfor (const { scope, dir, source } of sources) {\n\t\tconst ruleCache = loadRules(dir);\n\t\tfor (const rule of ruleCache.rules) {\n\t\t\tif (seen.has(rule.filePath)) continue;\n\t\t\tseen.add(rule.filePath);\n\t\t\trule.scope = scope as ParsedRule[\"scope\"];\n\t\t\trule.source = source;\n\t\t\tresult.push(rule);\n\t\t}\n\t}\n\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RulesConfig } from "./types.js";
|
|
2
|
+
export declare function loadConfig(projectDir: string): Promise<RulesConfig>;
|
|
3
|
+
export declare function resolveDirs(projectDir: string, config: RulesConfig): Array<{
|
|
4
|
+
scope: string;
|
|
5
|
+
dir: string;
|
|
6
|
+
source: string;
|
|
7
|
+
}>;
|
|
8
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/rules-engine/config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAmB9C,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAwBzE;AAED,wBAAgB,WAAW,CAC1B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,WAAW,GACjB,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAevD","sourcesContent":["import * as fs from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport * as path from \"node:path\";\nimport type { RulesConfig } from \"./types.js\";\n\nconst DEFAULT_CACHE_TTL = 30_000;\n\nconst DEFAULT_DIRS = {\n\tmanaged: [\"/etc/claude-code/.claude/rules\"],\n\tuser: [path.join(homedir(), \".claude\", \"rules\"), path.join(homedir(), \".config\", \"opencode\", \"rules\")],\n\tpi: [\".pi/rules\"],\n\tproject: [\".claude/rules\", \".opencode/rules\", \".trae/rules\"],\n};\n\nfunction defaultConfig(): RulesConfig {\n\treturn {\n\t\tcacheTTL: DEFAULT_CACHE_TTL,\n\t\tnotifyOnLoad: true,\n\t\tnotifyOnMatch: true,\n\t};\n}\n\nexport async function loadConfig(projectDir: string): Promise<RulesConfig> {\n\tconst configFiles = [\n\t\t\".rules-config.json\",\n\t\t\".pi/rules-config.json\",\n\t\t\".claude/rules-config.json\",\n\t\t\".opencode/rules-config.json\",\n\t];\n\n\tfor (const name of configFiles) {\n\t\tconst fp = path.resolve(projectDir, name);\n\t\ttry {\n\t\t\tconst raw = fs.readFileSync(fp, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(raw);\n\t\t\treturn {\n\t\t\t\t...defaultConfig(),\n\t\t\t\t...parsed,\n\t\t\t\tcacheTTL: parsed.cacheTTL ?? DEFAULT_CACHE_TTL,\n\t\t\t\tnotifyOnLoad: parsed.notifyOnLoad ?? true,\n\t\t\t\tnotifyOnMatch: parsed.notifyOnMatch ?? true,\n\t\t\t};\n\t\t} catch {}\n\t}\n\n\treturn defaultConfig();\n}\n\nexport function resolveDirs(\n\tprojectDir: string,\n\tconfig: RulesConfig,\n): Array<{ scope: string; dir: string; source: string }> {\n\tconst sources = config.dirs || DEFAULT_DIRS;\n\tconst result: Array<{ scope: string; dir: string; source: string }> = [];\n\n\tfor (const [scope, paths] of Object.entries(sources)) {\n\t\tfor (const p of paths) {\n\t\t\tresult.push({\n\t\t\t\tscope,\n\t\t\t\tdir: p.startsWith(\"/\") || p.startsWith(\"~\") ? p.replace(/^~/, homedir()) : path.resolve(projectDir, p),\n\t\t\t\tsource: p,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
const DEFAULT_CACHE_TTL = 30_000;
|
|
5
|
+
const DEFAULT_DIRS = {
|
|
6
|
+
managed: ["/etc/claude-code/.claude/rules"],
|
|
7
|
+
user: [path.join(homedir(), ".claude", "rules"), path.join(homedir(), ".config", "opencode", "rules")],
|
|
8
|
+
pi: [".pi/rules"],
|
|
9
|
+
project: [".claude/rules", ".opencode/rules", ".trae/rules"],
|
|
10
|
+
};
|
|
11
|
+
function defaultConfig() {
|
|
12
|
+
return {
|
|
13
|
+
cacheTTL: DEFAULT_CACHE_TTL,
|
|
14
|
+
notifyOnLoad: true,
|
|
15
|
+
notifyOnMatch: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function loadConfig(projectDir) {
|
|
19
|
+
const configFiles = [
|
|
20
|
+
".rules-config.json",
|
|
21
|
+
".pi/rules-config.json",
|
|
22
|
+
".claude/rules-config.json",
|
|
23
|
+
".opencode/rules-config.json",
|
|
24
|
+
];
|
|
25
|
+
for (const name of configFiles) {
|
|
26
|
+
const fp = path.resolve(projectDir, name);
|
|
27
|
+
try {
|
|
28
|
+
const raw = fs.readFileSync(fp, "utf-8");
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
return {
|
|
31
|
+
...defaultConfig(),
|
|
32
|
+
...parsed,
|
|
33
|
+
cacheTTL: parsed.cacheTTL ?? DEFAULT_CACHE_TTL,
|
|
34
|
+
notifyOnLoad: parsed.notifyOnLoad ?? true,
|
|
35
|
+
notifyOnMatch: parsed.notifyOnMatch ?? true,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
}
|
|
40
|
+
return defaultConfig();
|
|
41
|
+
}
|
|
42
|
+
export function resolveDirs(projectDir, config) {
|
|
43
|
+
const sources = config.dirs || DEFAULT_DIRS;
|
|
44
|
+
const result = [];
|
|
45
|
+
for (const [scope, paths] of Object.entries(sources)) {
|
|
46
|
+
for (const p of paths) {
|
|
47
|
+
result.push({
|
|
48
|
+
scope,
|
|
49
|
+
dir: p.startsWith("/") || p.startsWith("~") ? p.replace(/^~/, homedir()) : path.resolve(projectDir, p),
|
|
50
|
+
source: p,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/rules-engine/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAGlC,MAAM,iBAAiB,GAAG,MAAM,CAAC;AAEjC,MAAM,YAAY,GAAG;IACpB,OAAO,EAAE,CAAC,gCAAgC,CAAC;IAC3C,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,CAAC,CAAC;IACtG,EAAE,EAAE,CAAC,WAAW,CAAC;IACjB,OAAO,EAAE,CAAC,eAAe,EAAE,iBAAiB,EAAE,aAAa,CAAC;CAC5D,CAAC;AAEF,SAAS,aAAa,GAAgB;IACrC,OAAO;QACN,QAAQ,EAAE,iBAAiB;QAC3B,YAAY,EAAE,IAAI;QAClB,aAAa,EAAE,IAAI;KACnB,CAAC;AAAA,CACF;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAkB,EAAwB;IAC1E,MAAM,WAAW,GAAG;QACnB,oBAAoB;QACpB,uBAAuB;QACvB,2BAA2B;QAC3B,6BAA6B;KAC7B,CAAC;IAEF,KAAK,MAAM,IAAI,IAAI,WAAW,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC;YACJ,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC/B,OAAO;gBACN,GAAG,aAAa,EAAE;gBAClB,GAAG,MAAM;gBACT,QAAQ,EAAE,MAAM,CAAC,QAAQ,IAAI,iBAAiB;gBAC9C,YAAY,EAAE,MAAM,CAAC,YAAY,IAAI,IAAI;gBACzC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,IAAI;aAC3C,CAAC;QACH,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACX,CAAC;IAED,OAAO,aAAa,EAAE,CAAC;AAAA,CACvB;AAED,MAAM,UAAU,WAAW,CAC1B,UAAkB,EAClB,MAAmB,EACqC;IACxD,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,IAAI,YAAY,CAAC;IAC5C,MAAM,MAAM,GAA0D,EAAE,CAAC;IAEzE,KAAK,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACtD,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACvB,MAAM,CAAC,IAAI,CAAC;gBACX,KAAK;gBACL,GAAG,EAAE,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC;gBACtG,MAAM,EAAE,CAAC;aACT,CAAC,CAAC;QACJ,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["import * as fs from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport * as path from \"node:path\";\nimport type { RulesConfig } from \"./types.js\";\n\nconst DEFAULT_CACHE_TTL = 30_000;\n\nconst DEFAULT_DIRS = {\n\tmanaged: [\"/etc/claude-code/.claude/rules\"],\n\tuser: [path.join(homedir(), \".claude\", \"rules\"), path.join(homedir(), \".config\", \"opencode\", \"rules\")],\n\tpi: [\".pi/rules\"],\n\tproject: [\".claude/rules\", \".opencode/rules\", \".trae/rules\"],\n};\n\nfunction defaultConfig(): RulesConfig {\n\treturn {\n\t\tcacheTTL: DEFAULT_CACHE_TTL,\n\t\tnotifyOnLoad: true,\n\t\tnotifyOnMatch: true,\n\t};\n}\n\nexport async function loadConfig(projectDir: string): Promise<RulesConfig> {\n\tconst configFiles = [\n\t\t\".rules-config.json\",\n\t\t\".pi/rules-config.json\",\n\t\t\".claude/rules-config.json\",\n\t\t\".opencode/rules-config.json\",\n\t];\n\n\tfor (const name of configFiles) {\n\t\tconst fp = path.resolve(projectDir, name);\n\t\ttry {\n\t\t\tconst raw = fs.readFileSync(fp, \"utf-8\");\n\t\t\tconst parsed = JSON.parse(raw);\n\t\t\treturn {\n\t\t\t\t...defaultConfig(),\n\t\t\t\t...parsed,\n\t\t\t\tcacheTTL: parsed.cacheTTL ?? DEFAULT_CACHE_TTL,\n\t\t\t\tnotifyOnLoad: parsed.notifyOnLoad ?? true,\n\t\t\t\tnotifyOnMatch: parsed.notifyOnMatch ?? true,\n\t\t\t};\n\t\t} catch {}\n\t}\n\n\treturn defaultConfig();\n}\n\nexport function resolveDirs(\n\tprojectDir: string,\n\tconfig: RulesConfig,\n): Array<{ scope: string; dir: string; source: string }> {\n\tconst sources = config.dirs || DEFAULT_DIRS;\n\tconst result: Array<{ scope: string; dir: string; source: string }> = [];\n\n\tfor (const [scope, paths] of Object.entries(sources)) {\n\t\tfor (const p of paths) {\n\t\t\tresult.push({\n\t\t\t\tscope,\n\t\t\t\tdir: p.startsWith(\"/\") || p.startsWith(\"~\") ? p.replace(/^~/, homedir()) : path.resolve(projectDir, p),\n\t\t\t\tsource: p,\n\t\t\t});\n\t\t}\n\t}\n\n\treturn result;\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ExtensionAPI } from "@dyyz1993/pi-coding-agent";
|
|
2
|
+
export { ServerChannel } from "../core/extensions/server-channel.js";
|
|
3
|
+
export { getRules, invalidateCache } from "./cache.js";
|
|
4
|
+
export { loadConfig, resolveDirs } from "./config.js";
|
|
5
|
+
export { buildCompactContext, buildSystemPromptSection, buildToolContextSection } from "./injector.js";
|
|
6
|
+
export { loadRules, parseFrontmatter, parseRuleFile } from "./loader.js";
|
|
7
|
+
export { matchesAnyGlob, matchGlob } from "./matcher.js";
|
|
8
|
+
export type { InjectedPayload, LifecycleEntry, MatchedPayload, MatchRecord, ParsedRule, ReloadedPayload, RuleDetail, RuleFrontmatter, RuleScope, RuleSeverity, RulesChannelContract, RulesChannelEvent, RulesConfig, ScannedDir, SnapshotPayload, UnloadedPayload, } from "./types.js";
|
|
9
|
+
export default function rulesEnginePlugin(pi: ExtensionAPI): void;
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rules-engine/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAc,KAAK,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAqB1E,OAAO,EAAE,aAAa,EAAE,MAAM,sCAAsC,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,wBAAwB,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACvG,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzD,YAAY,EACX,eAAe,EACf,cAAc,EACd,cAAc,EACd,WAAW,EACX,UAAU,EACV,eAAe,EACf,UAAU,EACV,eAAe,EACf,SAAS,EACT,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,eAAe,EACf,eAAe,GACf,MAAM,YAAY,CAAC;AAIpB,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,EAAE,YAAY,QAiczD","sourcesContent":["import { Type } from \"@dyyz1993/pi-ai\";\nimport { defineTool, type ExtensionAPI } from \"@dyyz1993/pi-coding-agent\";\nimport { ServerChannel } from \"../core/extensions/server-channel.js\";\nimport { getRules, invalidateCache } from \"./cache.js\";\nimport { loadConfig } from \"./config.js\";\nimport { buildSystemPromptSection, buildToolContextSection } from \"./injector.js\";\nimport { matchesAnyGlob } from \"./matcher.js\";\nimport type {\n\tInjectedPayload,\n\tLifecycleEntry,\n\tMatchedRuleDetail,\n\tMatchRecord,\n\tParsedRule,\n\tRuleDetail,\n\tRuleSeverity,\n\tRulesChannelContract,\n\tRulesChannelEvent,\n\tRulesConfig,\n\tScannedDir,\n\tSnapshotPayload,\n} from \"./types.js\";\n\nexport { ServerChannel } from \"../core/extensions/server-channel.js\";\nexport { getRules, invalidateCache } from \"./cache.js\";\nexport { loadConfig, resolveDirs } from \"./config.js\";\nexport { buildCompactContext, buildSystemPromptSection, buildToolContextSection } from \"./injector.js\";\nexport { loadRules, parseFrontmatter, parseRuleFile } from \"./loader.js\";\nexport { matchesAnyGlob, matchGlob } from \"./matcher.js\";\nexport type {\n\tInjectedPayload,\n\tLifecycleEntry,\n\tMatchedPayload,\n\tMatchRecord,\n\tParsedRule,\n\tReloadedPayload,\n\tRuleDetail,\n\tRuleFrontmatter,\n\tRuleScope,\n\tRuleSeverity,\n\tRulesChannelContract,\n\tRulesChannelEvent,\n\tRulesConfig,\n\tScannedDir,\n\tSnapshotPayload,\n\tUnloadedPayload,\n} from \"./types.js\";\n\nconst READ_TOOLS = new Set([\"read\", \"grep\", \"glob\"]);\n\nexport default function rulesEnginePlugin(pi: ExtensionAPI) {\n\tlet config: RulesConfig | null = null;\n\tlet rules: ParsedRule[] = [];\n\tlet cachedMatchHash = \"\";\n\tlet hasSentSnapshot = false;\n\tlet _lastCwd = \"\";\n\tlet lastMessages: unknown[] = [];\n\n\tfunction rebuildMatchHistory(messages: unknown[]): MatchRecord[] {\n\t\tconst history: MatchRecord[] = [];\n\t\tfor (const msg of messages) {\n\t\t\tif ((msg as Record<string, unknown>).role !== \"toolResult\") continue;\n\t\t\tconst details = (msg as { details?: Record<string, unknown> }).details;\n\t\t\tif (!details?.rulesMatched) continue;\n\t\t\tconst rulesMatched = details.rulesMatched as MatchedRuleDetail[];\n\t\t\thistory.push({\n\t\t\t\tfilePath: (details.matchedFilePath as string) || \"\",\n\t\t\t\truleNames: rulesMatched.map((r) => r.name),\n\t\t\t\ttoolName: (msg as { toolName?: string }).toolName || \"\",\n\t\t\t\ttoolCallId: (msg as { toolCallId?: string }).toolCallId || \"\",\n\t\t\t\tseverity: rulesMatched.some((r) => r.severity === \"critical\" || r.severity === \"high\") ? \"warning\" : \"info\",\n\t\t\t\ttimestamp: (msg as { timestamp?: number }).timestamp || 0,\n\t\t\t\tmatchedRuleDetails: rulesMatched,\n\t\t\t});\n\t\t}\n\t\treturn history;\n\t}\n\n\tconst rawChannel = pi.registerChannel(\"rules-engine\");\n\tconst channel = new ServerChannel<RulesChannelContract>(rawChannel);\n\n\tchannel.handle(\"getSnapshot\", (params) => {\n\t\tconst unconditional = getUnconditionalRules();\n\t\tconst conditional = getConditionalRules();\n\t\tconst matchHistory = rebuildMatchHistory(lastMessages);\n\t\treturn {\n\t\t\ttype: \"snapshot\" as const,\n\t\t\trules: rules.map(toRuleDetail),\n\t\t\tinjectedRuleNames: unconditional.map((r) => r.name),\n\t\t\ttotalRules: rules.length,\n\t\t\tunconditionalCount: unconditional.length,\n\t\t\tconditionalCount: conditional.length,\n\t\t\tmatchHistory,\n\t\t\tlifecycleLog: [] as LifecycleEntry[],\n\t\t\tloadedAt: Date.now(),\n\t\t\tcacheTTL: config?.cacheTTL || 30000,\n\t\t};\n\t});\n\n\tfunction getUnconditionalRules(): ParsedRule[] {\n\t\treturn rules.filter((r) => r.isUnconditional);\n\t}\n\n\tfunction getConditionalRules(): ParsedRule[] {\n\t\treturn rules.filter((r) => !r.isUnconditional);\n\t}\n\n\tfunction getMatchingRules(targetPath: string): ParsedRule[] {\n\t\treturn getConditionalRules().filter((rule) => {\n\t\t\tconst globs = rule.frontmatter.paths;\n\t\t\tif (!globs || globs.length === 0) return false;\n\t\t\treturn matchesAnyGlob(globs, targetPath);\n\t\t});\n\t}\n\n\tasync function refreshRules(cwd: string): Promise<void> {\n\t\tconfig = await loadConfig(cwd);\n\t\trules = await getRules(cwd, config);\n\t}\n\n\tfunction toRuleDetail(r: ParsedRule): RuleDetail {\n\t\treturn {\n\t\t\tname: r.name,\n\t\t\ttitle: r.title,\n\t\t\tfilePath: r.filePath,\n\t\t\tscope: r.scope,\n\t\t\tsource: r.source,\n\t\t\tseverity: r.frontmatter.severity || (\"medium\" as RuleSeverity),\n\t\t\tisUnconditional: r.isUnconditional,\n\t\t\tpaths: r.frontmatter.paths || [],\n\t\t\tdescription: r.frontmatter.description,\n\t\t};\n\t}\n\n\tfunction buildSnapshot(matchHistory: MatchRecord[], lifecycleLog: LifecycleEntry[]): RulesChannelEvent {\n\t\tconst unconditional = getUnconditionalRules();\n\t\tconst conditional = getConditionalRules();\n\t\treturn {\n\t\t\ttype: \"snapshot\",\n\t\t\trules: rules.map(toRuleDetail),\n\t\t\tinjectedRuleNames: unconditional.map((r) => r.name),\n\t\t\ttotalRules: rules.length,\n\t\t\tunconditionalCount: unconditional.length,\n\t\t\tconditionalCount: conditional.length,\n\t\t\tmatchHistory,\n\t\t\tlifecycleLog,\n\t\t\tloadedAt: Date.now(),\n\t\t\tcacheTTL: config?.cacheTTL || 30000,\n\t\t};\n\t}\n\n\tfunction extractTargetPath(args: Record<string, unknown>): string | undefined {\n\t\tif (\"filePath\" in args && typeof args.filePath === \"string\") return args.filePath;\n\t\tif (\"path\" in args && typeof args.path === \"string\") return args.path;\n\t\tif (\"pattern\" in args && typeof args.pattern === \"string\") return args.pattern;\n\t\treturn undefined;\n\t}\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"rules_list\",\n\t\t\tlabel: \"List Rules\",\n\t\t\tdescription: \"List all discovered rules from all configured directories across all scopes\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute() {\n\t\t\t\tconst unconditional = getUnconditionalRules();\n\t\t\t\tconst conditional = getConditionalRules();\n\n\t\t\t\tconst byScope: Record<string, number> = {};\n\t\t\t\tfor (const r of rules) {\n\t\t\t\t\tbyScope[r.scope] = (byScope[r.scope] || 0) + 1;\n\t\t\t\t}\n\n\t\t\t\tlet output = `# Loaded Rules (${rules.length})\\n\\n`;\n\t\t\t\toutput += `Scopes: ${Object.entries(byScope)\n\t\t\t\t\t.map(([k, v]) => `${k}: ${v}`)\n\t\t\t\t\t.join(\", \")}\\n\\n`;\n\n\t\t\t\toutput += `**Unconditional** (${unconditional.length}):\\n`;\n\t\t\t\tfor (const rule of unconditional) {\n\t\t\t\t\toutput += `- ${rule.title} (${rule.source})\\n`;\n\t\t\t\t}\n\n\t\t\t\toutput += `\\n**Conditional** (${conditional.length}):\\n`;\n\t\t\t\tfor (const rule of conditional) {\n\t\t\t\t\toutput += `- ${rule.title} [${rule.frontmatter.paths?.join(\", \")}] (${rule.source})\\n`;\n\t\t\t\t}\n\n\t\t\t\treturn { content: [{ type: \"text\", text: output }], details: undefined };\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"rules_match\",\n\t\t\tlabel: \"Match Rules\",\n\t\t\tdescription: \"Find conditional rules that match a given file path by glob pattern\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tfilePath: Type.String({ description: \"File path to match\" }),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst matching = getMatchingRules(params.filePath);\n\t\t\t\tconst unconditional = getUnconditionalRules();\n\n\t\t\t\tlet output = `# Rule Match: ${params.filePath}\\n\\n`;\n\t\t\t\toutput += `**Unconditional** (always active, ${unconditional.length}):\\n`;\n\t\t\t\tfor (const r of unconditional) {\n\t\t\t\t\toutput += `- ${r.title}\\n`;\n\t\t\t\t}\n\t\t\t\toutput += `\\n**Conditional matches** (${matching.length}):\\n`;\n\t\t\t\tfor (const r of matching) {\n\t\t\t\t\tconst sev = r.frontmatter.severity || \"medium\";\n\t\t\t\t\toutput += `- [${sev}] ${r.title} (${r.frontmatter.paths?.join(\", \")})\\n`;\n\t\t\t\t}\n\n\t\t\t\treturn { content: [{ type: \"text\", text: output }], details: undefined };\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"rules_reload\",\n\t\t\tlabel: \"Reload Rules\",\n\t\t\tdescription: \"Force reload all rules from disk (clears cache and re-reads config)\",\n\t\t\tparameters: Type.Object({}),\n\t\t\tasync execute(_id, _params, _signal, _onUpdate, ctx) {\n\t\t\t\tinvalidateCache();\n\t\t\t\tawait refreshRules(ctx.cwd);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Rules reloaded: ${rules.length} total (${getUnconditionalRules().length} unconditional, ${getConditionalRules().length} conditional)`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: undefined,\n\t\t\t\t};\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerTool(\n\t\tdefineTool({\n\t\t\tname: \"rules_show\",\n\t\t\tlabel: \"Show Rule\",\n\t\t\tdescription: \"Show the full content of a specific rule by name\",\n\t\t\tparameters: Type.Object({\n\t\t\t\tname: Type.String({ description: \"Rule name (filename without .md)\" }),\n\t\t\t}),\n\t\t\tasync execute(_id, params) {\n\t\t\t\tconst rule = rules.find((r) => r.name === params.name);\n\t\t\t\tif (!rule) {\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [{ type: \"text\", text: `Rule '${params.name}' not found` }],\n\t\t\t\t\t\tisError: true,\n\t\t\t\t\t\tdetails: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\n\t\t\t\tlet output = `# ${rule.title}\\n\\n`;\n\t\t\t\toutput += `- **Name**: ${rule.name}\\n`;\n\t\t\t\toutput += `- **Scope**: ${rule.scope}\\n`;\n\t\t\t\toutput += `- **Source**: ${rule.source}\\n`;\n\t\t\t\toutput += `- **File**: ${rule.filePath}\\n`;\n\t\t\t\tif (rule.frontmatter.paths?.length) {\n\t\t\t\t\toutput += `- **Paths**: ${rule.frontmatter.paths.join(\", \")}\\n`;\n\t\t\t\t}\n\t\t\t\tif (rule.frontmatter.description) {\n\t\t\t\t\toutput += `- **Description**: ${rule.frontmatter.description}\\n`;\n\t\t\t\t}\n\t\t\t\toutput += `\\n${rule.content}`;\n\n\t\t\t\treturn { content: [{ type: \"text\", text: output }], details: undefined };\n\t\t\t},\n\t\t}),\n\t);\n\n\tpi.registerCommand(\"rules\", {\n\t\tdescription: \"Rules management (list, reload, check <path>, active)\",\n\t\thandler: async (args, ctx) => {\n\t\t\tconst parts = args.trim().split(/\\s+/);\n\t\t\tconst sub = parts[0] || \"list\";\n\n\t\t\tif (sub === \"list\" || sub === \"ls\") {\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`${rules.length} rules loaded (${getUnconditionalRules().length} unconditional, ${getConditionalRules().length} conditional)`,\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t} else if (sub === \"reload\") {\n\t\t\t\tinvalidateCache();\n\t\t\t\tawait refreshRules(ctx.cwd);\n\t\t\t\tctx.ui.notify(`Rules reloaded: ${rules.length} total`, \"info\");\n\t\t\t} else if (sub === \"check\" && parts[1]) {\n\t\t\t\tconst target = parts.slice(1).join(\" \");\n\t\t\t\tconst matching = getMatchingRules(target);\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\tmatching.length > 0\n\t\t\t\t\t\t? `${matching.length} rules match ${target}: ${matching.map((r) => r.title).join(\", \")}`\n\t\t\t\t\t\t: `No conditional rules match ${target}`,\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t} else if (sub === \"active\") {\n\t\t\t\tconst active = getUnconditionalRules();\n\t\t\t\tctx.ui.notify(\n\t\t\t\t\t`Active: ${active.length} unconditional (in system prompt), ${getConditionalRules().length} conditional (on file match)`,\n\t\t\t\t\t\"info\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tctx.ui.notify(\"Usage: /rules [list|reload|check <path>|active]\", \"info\");\n\t\t\t}\n\t\t},\n\t});\n\n\tpi.on(\"session_start\", async (_event, ctx) => {\n\t\t_lastCwd = ctx.cwd;\n\t\tawait refreshRules(ctx.cwd);\n\t\tctx.ui.setStatus(\"rules-engine\", `Rules: ${rules.length}`);\n\n\t\tconst unconditional = getUnconditionalRules();\n\t\tconst conditional = getConditionalRules();\n\n\t\tif (!hasSentSnapshot) {\n\t\t\thasSentSnapshot = true;\n\n\t\t\tconst scopeGroups = new Map<string, ParsedRule[]>();\n\t\t\tfor (const r of rules) {\n\t\t\t\tconst list = scopeGroups.get(r.scope) || [];\n\t\t\t\tlist.push(r);\n\t\t\t\tscopeGroups.set(r.scope, list);\n\t\t\t}\n\n\t\t\tconst scannedDirs: ScannedDir[] = [...scopeGroups.entries()].map(([scope, scopeRules]) => ({\n\t\t\t\tdir: scopeRules[0]?.source || scope,\n\t\t\t\tfileCount: scopeRules.length,\n\t\t\t\truleNames: scopeRules.map((r) => r.name),\n\t\t\t}));\n\n\t\t\tchannel.emit(\n\t\t\t\t\"snapshot\",\n\t\t\t\tbuildSnapshot(\n\t\t\t\t\t[],\n\t\t\t\t\t[\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tevent: \"loaded\",\n\t\t\t\t\t\t\tmessage: `Loaded ${rules.length} rules (${unconditional.length} unconditional, ${conditional.length} conditional)`,\n\t\t\t\t\t\t\truleCount: rules.length,\n\t\t\t\t\t\t\ttimestamp: Date.now(),\n\t\t\t\t\t\t\tdetails: {\n\t\t\t\t\t\t\t\tscannedDirs,\n\t\t\t\t\t\t\t\tconfigSource: config ? \".rules-config.json\" : \"default\",\n\t\t\t\t\t\t\t\tcacheHit: false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t),\n\t\t\t);\n\n\t\t\tpi.appendEntry(\"rules\", {\n\t\t\t\taction: \"loaded\",\n\t\t\t\truleCount: rules.length,\n\t\t\t\tscannedDirs,\n\t\t\t\ttimestamp: Date.now(),\n\t\t\t});\n\t\t}\n\t});\n\n\tpi.on(\"before_agent_start\", async (event) => {\n\t\tconst unconditional = getUnconditionalRules();\n\n\t\tif (unconditional.length === 0) {\n\t\t\tchannel.emit(\"injected\", {\n\t\t\t\ttype: \"injected\",\n\t\t\t\truleNames: [],\n\t\t\t\tsystemPromptLength: event.systemPrompt.length,\n\t\t\t});\n\t\t\treturn undefined;\n\t\t}\n\n\t\tconst sources = [...new Set(unconditional.map((r) => r.source))];\n\t\tconst section = buildSystemPromptSection(unconditional, sources);\n\t\tconst newPrompt = event.systemPrompt + section;\n\n\t\tchannel.emit(\"injected\", {\n\t\t\ttype: \"injected\",\n\t\t\truleNames: unconditional.map((r) => r.name),\n\t\t\tsystemPromptLength: newPrompt.length,\n\t\t});\n\n\t\treturn {\n\t\t\tsystemPrompt: newPrompt,\n\t\t};\n\t});\n\n\tpi.on(\"tool_result\", async (event) => {\n\t\tif (!READ_TOOLS.has(event.toolName)) return undefined;\n\n\t\tconst targetPath = extractTargetPath(event.input);\n\t\tif (!targetPath) return undefined;\n\n\t\tconst matching = getMatchingRules(targetPath);\n\t\tif (matching.length === 0) return undefined;\n\n\t\tconst matchedRuleDetails: MatchedRuleDetail[] = matching.map((r) => ({\n\t\t\tname: r.name,\n\t\t\ttitle: r.title,\n\t\t\tseverity: r.frontmatter.severity || (\"medium\" as RuleSeverity),\n\t\t\tmatchedGlob:\n\t\t\t\tr.frontmatter.paths?.find((p) => matchesAnyGlob([p], targetPath)) || r.frontmatter.paths?.[0] || \"\",\n\t\t}));\n\n\t\tconst contextSection = buildToolContextSection(matching, targetPath);\n\n\t\tconst hasCritical = matching.some((r) => r.frontmatter.severity === \"critical\");\n\t\tconst hasHigh = matching.some((r) => r.frontmatter.severity === \"high\");\n\n\t\tchannel.emit(\"matched\", {\n\t\t\ttype: \"matched\",\n\t\t\tfilePath: targetPath,\n\t\t\tmatchedRules: matchedRuleDetails,\n\t\t\ttoolName: event.toolName,\n\t\t\ttoolCallId: event.toolCallId,\n\t\t\tseverity: hasCritical ? \"warning\" : hasHigh ? \"warning\" : \"info\",\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\n\t\treturn {\n\t\t\tcontent: [...event.content, { type: \"text\" as const, text: `\\n\\n${contextSection}` }],\n\t\t\tdetails: {\n\t\t\t\t...((event.details as Record<string, unknown>) || {}),\n\t\t\t\trulesMatched: matchedRuleDetails,\n\t\t\t\tmatchedFilePath: targetPath,\n\t\t\t},\n\t\t};\n\t});\n\n\tpi.on(\"context\", async (event) => {\n\t\tlastMessages = event.messages;\n\t\tconst matchHistory = rebuildMatchHistory(event.messages);\n\n\t\tconst hash = JSON.stringify(matchHistory.map((r) => `${r.toolCallId}:${r.filePath}`));\n\t\tif (hash !== cachedMatchHash) {\n\t\t\tcachedMatchHash = hash;\n\t\t\tconst unconditional = getUnconditionalRules();\n\t\t\tconst conditional = getConditionalRules();\n\t\t\tchannel.emit(\"snapshot\", {\n\t\t\t\ttype: \"snapshot\",\n\t\t\t\trules: rules.map(toRuleDetail),\n\t\t\t\tinjectedRuleNames: unconditional.map((r) => r.name),\n\t\t\t\ttotalRules: rules.length,\n\t\t\t\tunconditionalCount: unconditional.length,\n\t\t\t\tconditionalCount: conditional.length,\n\t\t\t\tmatchHistory,\n\t\t\t\tlifecycleLog: [],\n\t\t\t\tloadedAt: Date.now(),\n\t\t\t\tcacheTTL: config?.cacheTTL || 30000,\n\t\t\t});\n\t\t}\n\n\t\treturn undefined;\n\t});\n\n\tpi.on(\"session_compact\", async (_event, ctx) => {\n\t\tcachedMatchHash = \"\";\n\t\tlastMessages = [];\n\t\tctx.ui.setStatus(\"rules-engine\", `Rules: ${rules.length} (re-injected after compact)`);\n\t});\n\n\tpi.on(\"turn_end\", async () => {\n\t\tif (lastMessages.length === 0 && rules.length > 0) return;\n\t\tconst matchHistory = rebuildMatchHistory(lastMessages);\n\t\tconst hash = JSON.stringify(matchHistory.map((r) => `${r.toolCallId}:${r.filePath}`));\n\t\tif (hash !== cachedMatchHash) {\n\t\t\tcachedMatchHash = hash;\n\t\t\tchannel.emit(\"snapshot\", {\n\t\t\t\ttype: \"snapshot\",\n\t\t\t\trules: rules.map(toRuleDetail),\n\t\t\t\tinjectedRuleNames: getUnconditionalRules().map((r) => r.name),\n\t\t\t\ttotalRules: rules.length,\n\t\t\t\tunconditionalCount: getUnconditionalRules().length,\n\t\t\t\tconditionalCount: getConditionalRules().length,\n\t\t\t\tmatchHistory,\n\t\t\t\tlifecycleLog: [],\n\t\t\t\tloadedAt: Date.now(),\n\t\t\t\tcacheTTL: config?.cacheTTL || 30000,\n\t\t\t});\n\t\t}\n\t});\n\n\tpi.on(\"session_shutdown\", async (_event, ctx) => {\n\t\tchannel.emit(\"unloaded\", { type: \"unloaded\", reason: \"session_shutdown\" });\n\t\tpi.appendEntry(\"rules\", {\n\t\t\taction: \"unloaded\",\n\t\t\treason: \"session_shutdown\",\n\t\t\ttimestamp: Date.now(),\n\t\t});\n\t\tctx.ui.setStatus(\"rules-engine\", undefined);\n\t});\n}\n"]}
|